Files
alterdb/README.md
T
2026-10-02 13:11:03 +08:00

274 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# alterdb
> 以 SQLite 為底層存儲格式的自定義數據庫引擎,附帶開箱即用的 Web 管理介面。
**狀態:早期開發中** — axum HTTP API 骨架、Askama 渲染的首頁與 SQLite 存儲層(rusqlite + 連接池)已就緒,數據模型 Account(帳戶)與 Database(外部數據庫連接,mysql / postgres)的 CRUD 與 REST API 已可用;Web 介面的登入 / 會話(Cookie + 進程內存會話存儲)、變更密碼與數據庫連接管理頁已完成,連接詳情頁可實際連線目標庫(測試連接狀態、瀏覽數據表),數據表管理頁可檢視欄位結構並新增 / 刪除欄位,數據管理頁可分頁瀏覽並編輯(以主鍵定位修改資料列)表內資料,管理介面其餘部分與查詢引擎仍在開發中。
## 專案簡介
alterdb 是一個使用 Rust 編寫的自定義數據庫引擎。它不是又一個 SQLite 管理工具,而是:
- **自定義查詢引擎**:對外提供自己的查詢介面與數據模型,查詢的解析、規劃與執行均由 alterdb 自行實作;
- **SQLite 作為底層存儲**:數據最終落在單一 SQLite 檔案中,直接享受其成熟可靠的持久化與事務能力,而無需從零重造存儲層;
- **Web 管理介面**:以本地伺服器方式運行,打開瀏覽器即可瀏覽數據、執行查詢、管理表結構。
## 核心特性(規劃中)
- 自定義查詢引擎:查詢的解析與執行由本項目實作,不直接透傳 SQL
- SQLite 存儲後端:單檔案持久化,支援事務
- Web 管理介面:
- 數據庫 / 表 / 結構瀏覽
- 線上執行查詢,結果以表格呈現
- 數據新增、編輯、刪除
- 單一可執行檔部署:編譯為單一 binary,內嵌前端資源,無外部運行時依賴
## 架構規劃
```text
┌─────────────┐ HTTP ┌───────────────────┐
│ 瀏覽器 │ ◄────────────► │ HTTP API 伺服器 │
│ (Web UI) │ │ (Rust) │
└─────────────┘ └─────────┬─────────┘
│
┌─────────▼─────────┐
│ 查詢引擎 │
│ 解析 / 規劃 / 執行 │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 存儲層 │
│ (SQLite 檔案) │
└───────────────────┘
```
技術選型:
| 項目 | 選擇 | 備註 |
| --- | --- | --- |
| 語言 | Rust(edition 2024) | 需 Rust 1.85+ |
| Web 框架 | [axum](https://github.com/tokio-rs/axum) 0.8 + tokio | REST API;中間件透過 tower-http |
| 模板引擎 | [Askama](https://github.com/askama-rs/askama) 0.16 | 編譯期模板,HTML 內嵌於執行檔 |
| 存儲 | SQLite([rusqlite](https://github.com/rusqlite/rusqlite) bundled) | deadpool-sqlite 連接池;WAL 模式 |
| 外部庫連線 | [mysql_async](https://github.com/blackbeam/mysql_async) / [tokio-postgres](https://github.com/sfackler/rust-postgres) | 連接測試與數據表瀏覽;暫未啟用 TLS |
| 密碼雜湊 | [argon2](https://github.com/RustCrypto/password-hashes) 0.5 | 帳戶密碼以 Argon2id + 隨機鹽雜湊存儲,永不落明文 |
## 快速開始
環境需求:[Rust](https://www.rust-lang.org/) 1.85 或以上版本(本項目使用 edition 2024)。
```bash
# 編譯並運行
cargo run
# 伺服器啟動後:
# - 管理介面首頁:http://127.0.0.1:8080/(未登入會被導向 /login)
# - 數據庫連接管理頁:http://127.0.0.1:8080/databases(需登入)
# - 變更密碼頁:http://127.0.0.1:8080/password(需登入)
# - 登入頁:http://127.0.0.1:8080/login(默認帳號 admin / admin)
# - 健康檢查 API:http://127.0.0.1:8080/api/health
# - 帳戶 API:http://127.0.0.1:8080/api/accounts
# - 數據庫連接 API:http://127.0.0.1:8080/api/databases
```
資料庫檔案默認為工作目錄下的 `alterdb.db`,可透過環境變數 `ALTERDB_DB_PATH` 指定其他路徑。
SQLite 數據檔(`*.db` 等)已被 `.gitignore` 排除,不會被誤提交到版本庫。
## 登入與會話
Web 介面頁面(目前為首頁 `/`)需登入後才能訪問,未登入的請求會被重定向到 `/login`:
| 方法 | 路徑 | 說明 |
| --- | --- | --- |
| GET | `/login` | 登入頁(已登入者直接回到首頁);帶 `?changed=1` 時顯示密碼變更成功提示 |
| POST | `/login` | 提交用戶名 / 密碼;成功建立會話並下發 Cookie |
| POST | `/logout` | 銷毀會話並清除 Cookie |
| GET | `/password` | 變更密碼頁(需登入) |
| POST | `/password` | 校驗原密碼後更新密碼;成功即銷毀該帳戶全部會話,導向 `/login?changed=1` |
實作要點:
- 密碼以 Argon2id 校驗;帳號不存在時仍執行一次雜湊運算,避免以回應時間差列舉用戶名;已停用(`is_active = false`)的帳戶無法登入
- 會話保存在**進程內存**,以 256 位隨機 token 為鑰匙,透過 `HttpOnly` + `SameSite=Lax` Cookie 傳遞,有效期 24 小時;伺服器重啟後會話失效(重新登入即可)
- 變更密碼需驗證原密碼;新密碼長度 8–128 字符、兩次輸入需一致且不得與原密碼相同;成功後以新 Argon2 雜湊覆寫,該帳戶**全部會話(含當前)即刻失效**,需以新密碼重新登入
- `/api/*` 端點**暫不要求會話**,後續將統一接入認證
## Web 管理介面
登入後的頁面共用側邊導航欄(`_shell.html` 應用外殼):左側為品牌、主導航(首頁 / 數據庫,當前頁以 `aria-current="page"` 高亮)與底部的登入者資訊、變更密碼入口及登出;窄屏(≤ 48rem)自動折疊為頂欄。登入頁與 404 頁保持無側欄的獨立版式。頁面外殼由 Askama 渲染,數據操作由內嵌 vanilla JS 調用 `/api/*` 完成(無任何外部前端依賴):
全站支援深淺色主題:預設跟隨系統偏好(`color-scheme: light dark`),點擊側邊欄底部(獨立版式頁為頁首右側)的「深色 / 淺色模式」按鈕可手動固定,選擇保存在 `localStorage`(鍵 `theme`),並由 `<head>` 內的同步內聯腳本在首幀繪製前重新套用,避免刷新閃爍。配色完全由 `color-scheme` 驅動(`currentColor` 混色與 `light-dark()`),無任何主題相關的服務端狀態。
| 路徑 | 說明 |
| --- | --- |
| `/` | 首頁:服務資訊與各管理入口 |
| `/databases` | 數據庫連接列表(名稱連到詳情頁;含刪除) |
| `/databases/new` | 新增連接(獨立表單頁) |
| `/databases/{id}` | 連接詳情頁:服務端渲染連接資訊,頁面載入即自動測試連接(狀態徽章 + 伺服器版本 + 延遲)並列出目標庫當前 schema 的數據表(名稱連到表管理頁);可「重新測試」 |
| `/databases/{id}/edit` | 編輯連接(獨立表單頁,服務端預填現值;連接不存在返回 404) |
| `/databases/{id}/tables/{table}` | 數據表管理頁:欄位結構(名稱 / 類型 / 可空 / 默認值,支援新增與刪除欄位),由內嵌 JS 即時連線目標庫取得;頁面不請求表內資料 |
| `/databases/{id}/tables/{table}/data` | 數據管理頁:表內資料分頁瀏覽(每頁 25–200 列、顯示總列數)與編輯資料列(以主鍵定位,逐欄輸入、可空欄位可設 NULL),由內嵌 JS 即時連線目標庫取得 |
| `/password` | 變更密碼(表單:原密碼 / 新密碼 / 確認新密碼;成功後全部會話失效並導向登入頁) |
`/databases` 相關頁面要點:
- 埠留空按類型自動補默認(MySQL 3306 / PostgreSQL 5432)
- 編輯頁由服務端預填現值,但密碼**永不回填**(API 與頁面皆不輸出密碼);編輯時密碼欄留空表示**保留原密碼**(對應 PUT 語義:`password` 鍵缺省=保留、空字串=清空、有值=覆寫)
- 表單提交由 JS 調用 `/api/databases`,成功後跳回列表頁;操作錯誤(驗證失敗、名稱衝突等)以頁面錯誤條展示 API 返回的錯誤訊息
- 詳情頁(`/databases/{id}`)載入時自動並行發起連接測試與數據表查詢,狀態以徽章呈現(已連接 / 無法連接 / 測試中),數據表類型映射為「表 / 視圖」
- 表管理頁(`/databases/{id}/tables/{table}`)載入時僅載入欄位結構,不請求表內資料;工具列「數據管理」連到對應的數據管理頁
- 數據管理頁(`/databases/{id}/tables/{table}/data`)載入時載入第一頁資料;支援換頁、調整每頁列數與手動刷新,NULL 儲存格以斜體 `NULL` 呈現,過長內容截斷並以懸浮提示顯示全文;頁面對視圖(VIEW)同樣適用
- 數據管理頁可**編輯資料列**:有主鍵的表每列附「編輯」按鈕,對話框逐欄輸入(可空欄位附 NULL 勾選),只送出有變更的欄位,以編輯前的主鍵原值定位資料列;沒有主鍵的表(含視圖)僅能瀏覽並顯示提示;目標列已被刪除或主鍵已變更時返回衝突錯誤並保留表單內容
- 表管理頁的欄位結構區可**新增欄位**(內嵌表單:名稱 / 類型(含常用建議)/ 可空(默認允許)/ 默認值)與**刪除欄位**(紅色按鈕,彈出確認對話框後才執行);結構變更成功後自動刷新欄位結構
- 所有動態內容經 `textContent` 寫入,不以字串拼接 HTML
## 帳戶 API
管理介面用戶帳戶的 CRUD 端點(後續將由自定義查詢引擎統一驅動):
| 方法 | 路徑 | 說明 |
| --- | --- | --- |
| GET | `/api/accounts` | 列出所有帳戶 |
| POST | `/api/accounts` | 建立帳戶;`username`、`email` 必填且全表唯一 |
| GET | `/api/accounts/{id}` | 取得單一帳戶,不存在時返回 404 |
| PUT | `/api/accounts/{id}` | 覆寫帳戶可編輯欄位(`updated_at` 自動刷新) |
| DELETE | `/api/accounts/{id}` | 刪除帳戶 |
輸入會先被正規化(去除首尾空白、`display_name` 缺省時同 `username`、`is_active` 缺省為啟用);驗證失敗返回 400,唯一欄位衝突返回 409。
### 默認管理員帳號
資料庫初始化時會自動建立默認管理員帳號(僅在不存在時建立,重啟不會覆蓋日後的變更):
- 用戶名:`admin`
- 密碼:`admin`
密碼以 Argon2 雜湊存儲,`password_hash` 欄位永不透過 API 序列化輸出;既有舊版資料庫啟動時會自動遷移(補上 `password_hash` 欄位)。**初始密碼即帳號名,建議儘早變更。**
```bash
# 建立帳戶
curl -X POST http://127.0.0.1:8080/api/accounts \
-H 'Content-Type: application/json' \
-d '{"username": "alice", "email": "alice@example.com", "display_name": "Alice"}'
# 列出所有帳戶
curl http://127.0.0.1:8080/api/accounts
```
## 數據庫連接 API
管理外部數據庫連接(MySQL / PostgreSQL)的 CRUD 端點,以及對目標庫的**實際連線**操作(測試連接、列出數據表、檢視欄位結構、分頁瀏覽與修改資料,由 `mysql_async` / `tokio-postgres` 驅動):
| 方法 | 路徑 | 說明 |
| --- | --- | --- |
| GET | `/api/databases` | 列出所有連接 |
| POST | `/api/databases` | 建立連接;`name` 必填且全表唯一 |
| GET | `/api/databases/{id}` | 取得單一連接,不存在時返回 404 |
| PUT | `/api/databases/{id}` | 覆寫連接可編輯欄位(`updated_at` 自動刷新) |
| DELETE | `/api/databases/{id}` | 刪除連接 |
| POST | `/api/databases/{id}/test` | 實際連線目標庫並查詢伺服器版本;成功與失敗都是 200(結果在 `ok` 欄位,附 `server_version` / `error` 與 `elapsed_ms`),連接記錄不存在返回 404 |
| GET | `/api/databases/{id}/tables` | 列出目標庫當前 schema 的數據表(`name` + `kind`,按名稱排序);連不上目標時返回 502 並附錯誤訊息 |
| GET | `/api/databases/{id}/tables/{table}/columns` | 列出資料表欄位結構(`name` / `type` / `nullable` / `default` / `primary_key`,按表定義順序);資料表不存在返回 404 |
| POST | `/api/databases/{id}/tables/{table}/columns` | 新增欄位(`ALTER TABLE … ADD COLUMN`):`name`(≤64 字符)、`type`(原生 SQL 片段,如 `varchar(255)`)、`nullable`(缺省允許)、`default`(可選,原生 SQL 片段);欄位名衝突返回 409,成功返回 204 |
| DELETE | `/api/databases/{id}/tables/{table}/columns/{column}` | 刪除欄位(`ALTER TABLE … DROP COLUMN`,資料一併丟失且無法復原);欄位不存在返回 404,成功返回 204 |
| GET | `/api/databases/{id}/tables/{table}/rows` | 分頁瀏覽資料:`?limit=&offset=`(limit 1–200、缺省 50;offset ≥ 0),回應含 `columns`、字串化的 `rows`(NULL 為 `null`)、`total`(`COUNT(*)`)與生效的分頁參數 |
| PATCH | `/api/databases/{id}/tables/{table}/rows` | 修改資料列:body 為 `{"where": {主鍵欄位: 原值}, "set": {欄位: 新值}}`(值為字串或 `null`=設為 NULL,按欄位型別隱式轉換);`where` 須恰好覆蓋主鍵欄位,`set` 欄位須存在且值 ≤65536 字符;目標列不存在(已被刪除或主鍵已變更)返回 409,成功返回 204 |
連線操作要點:
- 每次操作建立全新連線、用完即斷(不做連接複用);整體逾時 5 秒,目標不可達時不會長時間佔住請求
- 錯誤訊息展平驅動庫的錯誤鏈,可直接區分連線被拒、認證失敗等原因(如 `db error: FATAL: password authentication failed for user "app"`)
- 表名 / 欄位名拼入 SQL 前先經 information_schema 白名單核對(不存在的表或欄位直接 404,不進入 SQL),再以驅動各自的引號規則轉義(MySQL 反引號、PostgreSQL 雙引號),杜絕經標識符注入
- 新增欄位的 `type` / `default` 是**原生 SQL 片段**(管理工具的固有語義):驗證攔截分號、註解起始符(`--`、`/*`)與控制字元,配合兩驅動皆走的預備語句協議(不啟用多語句),即使片段被惡意構造也無法執行第二條語句;目標端語義錯誤(如對有資料的表加 NOT NULL 無默認值欄位)由驅動錯誤原樣回報(502)
- 刪除欄位屬**不可復原的破壞性操作**;`/api/*` 端點暫不要求會話(接入認證前,對外暴露埠時須自行做好網路隔離)
- 瀏覽資料時儲存格一律轉為字串表示(PostgreSQL 逐欄 `::text` 轉換),NULL 保留為 `null` 由前端呈現
- 修改資料列以**主鍵定位**(欄位是否為主鍵經 `TABLE_CONSTRAINTS` 連接 `KEY_COLUMN_USAGE` 判定):MySQL 的值以預備語句參數綁定(並啟用 `CLIENT_FOUND_ROWS`,使 affected rows 為匹配列數);PostgreSQL 的值以引號加倍轉義的字面值拼入(標準遵循字串下反斜線無特殊含義),未知型別字面值由伺服器隱式轉換為欄位型別;型別無法轉換、違反約束等錯誤由目標庫原樣回報(502)
- **暫未啟用 TLS**:對目標庫的連線為明文,僅適合配合受信任網路使用(MySQL 的 `caching_sha2_password` 認證在無 TLS 下經伺服器 RSA 金鑰完成)
輸入欄位:
| 欄位 | 必填 | 說明 |
| --- | --- | --- |
| `name` | 是 | 連接顯示名稱,全表唯一,≤ 128 字符 |
| `type` | 是 | 數據庫類型,目前僅支援 `"mysql"` / `"postgres"` |
| `host` | 是 | 主機名或 IP,≤ 255 字符 |
| `port` | 否 | 埠,1–65535;缺省按類型補 3306(mysql)/ 5432(postgres) |
| `username` | 是 | 連接用戶名,≤ 64 字符 |
| `password` | 否 | 連接密碼,默認空字串;**永不透過 API 序列化輸出**(連線需明文可用,故不做單向雜湊) |
| `database_name` | 是 | 目標資料庫名,≤ 64 字符 |
輸入會先被正規化(去除首尾空白,密碼除外;`port` 缺省補默認);驗證失敗返回 400,`name` 衝突返回 409,`type` 為不支援的類型時由反序列化直接拒絕。
```bash
# 建立連接(port 自動補 3306,回應不含 password)
curl -X POST http://127.0.0.1:8080/api/databases \
-H 'Content-Type: application/json' \
-d '{"name": "shop-prod", "type": "mysql", "host": "10.0.0.5",
"username": "app", "password": "secret", "database_name": "shop"}'
# 列出所有連接
curl http://127.0.0.1:8080/api/databases
```
## 開發路線圖
- [ ] **階段一:存儲層 MVP(進行中)** — 連接池(deadpool-sqlite)與 WAL 初始化已完成;Account 模型的 CRUD 與 REST API(`/api/accounts`)已就緒,通用表管理開發中
- [ ] **階段二:自定義查詢引擎** — 查詢語法定義、解析與執行
- [ ] **階段三:HTTP API(進行中)** — axum 伺服器骨架、健康檢查與帳戶 CRUD 端點已完成,登入 / 登出 / 會話已就緒(頁面已受保護,API 端點接入認證開發中),其餘 API 端點開發中
- [ ] **階段四:Web 管理介面(進行中)** — 登入頁、會話、變更密碼頁、數據庫連接管理頁(`/databases`)、連接詳情頁(實際連線測試 + 數據表瀏覽)、數據表管理頁(欄位結構 + 新增 / 刪除欄位)與數據管理頁(表內資料分頁瀏覽 + 以主鍵定位編輯資料列)已完成;瀏覽器端 UI:查詢執行、新增 / 刪除資料列開發中
- [ ] **進階功能** — 匯入 / 匯出、備份、多用戶與權限等
## 專案現狀與結構
目前已完成 HTTP API 骨架、存儲層接線、首頁渲染與登入 / 會話(axum + rusqlite + askama),並以 Account(帳戶)打通「模型 → 存儲 → REST API」全鏈路;Database(外部數據庫連接,mysql / postgres)為第二個模型,同構接入同一鏈路,且已具備實際連線能力(`connect` 模組:測試連接、列出數據表、檢視欄位結構、分頁瀏覽資料):
```text
alterdb/
├── Cargo.toml # 項目清單(axum / tokio / rusqlite / askama / mysql_async / tokio-postgres 等)
├── .gitignore
├── README.md
├── templates/ # Askama 模板(編譯期內嵌進執行檔)
│ ├── base.html # 頁面骨架(基礎樣式與布局塊)
│ ├── _shell.html # 應用外殼:側邊導航欄(品牌 / 導航 / 登入者與登出)+ 主內容區
│ ├── index.html # 首頁(登入後可見)
│ ├── databases.html # 數據庫連接列表頁(外殼 + 內嵌 JS 調用 /api/databases)
│ ├── database_detail.html # 連接詳情頁(連接資訊 + 連接狀態 + 數據表,內嵌 JS 即時連線)
│ ├── table_detail.html # 數據表管理頁(欄位結構管理,內嵌 JS 即時連線)
│ ├── table_data.html # 數據管理頁(表內資料分頁瀏覽 + 編輯資料列,內嵌 JS 即時連線)
│ ├── database_form.html # 新增 / 編輯連接共用表單頁(服務端預填 + 提交 JS)
│ ├── password.html # 變更密碼頁(原密碼 + 新密碼兩次確認)
│ ├── login.html # 登入頁(無側欄)
│ └── 404.html # 404 頁(無側欄)
└── src/
├── main.rs # 程序入口:初始化存儲層並啟動 HTTP 伺服器
├── state.rs # 應用共享狀態(資料庫連接池 + 會話存儲)
├── session.rs # 會話管理:進程內存存儲、Cookie 解析與 CurrentUser 提取器
├── connect.rs # 實際連線能力:測試連接(版本 / 延遲)、列出數據表、檢視欄位結構與分頁瀏覽資料(mysql_async / tokio-postgres)
├── models/ # 模型層:數據結構與輸入驗證(不含存儲細節)
│ ├── account.rs # Account 模型(帳戶)與輸入正規化、密碼雜湊
│ └── database.rs # Database 模型(外部數據庫連接)與 DatabaseType 枚舉
├── storage/ # 存儲層:各模型對應表的 SQL 與 CRUD
│ ├── mod.rs # 連接池建立、資料庫初始化與共用錯誤類型
│ ├── account.rs # accounts 表結構與 CRUD
│ └── database.rs # databases 表結構與 CRUD
└── controllers/ # 控制器層(HTTP API + 頁面)
├── mod.rs # 路由匯總
├── accounts.rs # 帳戶 CRUD API(/api/accounts)
├── auth.rs # 登入 / 登出 / 變更密碼(/login、/logout、/password)
├── database.rs # 數據庫連接 CRUD API 與連線操作(/api/databases)
├── health.rs # 健康檢查 API(GET /api/health,含資料庫狀態)
└── pages.rs # 頁面渲染(GET /、/databases 等,需登入)
```
## 開發
```bash
cargo build # 編譯
cargo test # 執行測試
cargo run # 運行
```
## 授權
尚未決定(TBD)。