311 lines
9.7 KiB
Markdown
311 lines
9.7 KiB
Markdown
# TUI Backend Contract
|
||
|
||
日期:2026-04-02
|
||
作者:Senior Backend Engineer
|
||
|
||
## 1. 当前后端边界
|
||
|
||
- `crates/db-core`:数据库无关的领域对象与基础校验。
|
||
- `crates/db-config`:连接 profile、versioned profile store、active profile 语义、session-only secret 缓存、密码环境变量注入与脱敏摘要。
|
||
- `crates/db-drivers`:PostgreSQL / MySQL / SQLite 驱动实现与差异收敛。
|
||
- `crates/db-app`:`connect` / `inspect` / `query` / `export` 共享应用编排,以及 profile `load` / `save` / `delete` / `activate` / `resolve` 共享入口、结构化结果、统一错误对象,与 TUI worker 可复用的执行状态。
|
||
- `apps/cli`:参数解析、stdout/stderr 渲染、退出码。
|
||
|
||
## 2. 持久化边界
|
||
|
||
- 当前 backend 新增了一条受控本地持久化边界:`db_config::ConnectionProfileStore`。
|
||
- store 仅持久化:
|
||
- `version`
|
||
- `active_profile`
|
||
- 不含 secret 的 `ConnectionProfile[]`
|
||
- 当前 store 不持久化:
|
||
- 明文密码 / session secret
|
||
- 查询历史
|
||
- 导出历史
|
||
- 后台任务状态
|
||
- 当前写路径只有:
|
||
- 调用方显式传入路径的 profile store JSON 文件
|
||
- `export` 明确指定的输出文件
|
||
- SQLite 目标数据库本身(由用户选择)
|
||
- `SessionSecretStore` 只存在于当前进程内存;重启后不恢复。
|
||
- 当前阶段仍不引入新的数据库、本地缓存库或后台 daemon 存储契约,也不引入 migration chain。
|
||
|
||
## 3. 共享请求对象
|
||
|
||
TUI worker 应直接构造共享 Rust 对象,不要拼接 CLI flags,更不要解析 CLI 文本。
|
||
|
||
### Connect
|
||
|
||
- 输入:`db_config::ConnectionProfile`
|
||
- 说明:
|
||
- `name` 用于当前会话内的目标标识
|
||
- `target` 复用 `db_core::ConnectionTarget`
|
||
- `password_env_var` 只作为脱敏标签 / advanced fallback 保留
|
||
- `ConnectionTarget.password` 可在运行态存在,但禁止进入 `ConnectionProfileCatalog`
|
||
|
||
### Connection Management
|
||
|
||
- profile store:`db_config::ConnectionProfileStore`
|
||
- catalog:`db_config::ConnectionProfileCatalog`
|
||
- session secret cache:`db_config::SessionSecretStore`
|
||
- 保存请求:`db_app::SaveProfileRequest`
|
||
- `profile`
|
||
- `set_active`
|
||
- `session_secret`:`Preserve | Set(String) | Clear`
|
||
- 共享入口:
|
||
- `db_app::load_profile_catalog`
|
||
- `db_app::save_profile`
|
||
- `db_app::delete_profile`
|
||
- `db_app::activate_profile`
|
||
- `db_app::resolve_profile`
|
||
- `db_app::resolve_active_profile`
|
||
- `db_app::connect_saved_profile`
|
||
- 约束:
|
||
- catalog 内 profile 必须无 secret;若 `target.password` 存在,视为运行态数据,只能通过 session secret 路径保留
|
||
- `active_profile` 必须指向已存在 profile,或为 `null`
|
||
- 同名 profile 视为 upsert,不引入第二份记录
|
||
|
||
### Inspect
|
||
|
||
- 输入:`db_core::InspectRequest`
|
||
- 字段:
|
||
- `schema: Option<String>`
|
||
- `table: Option<String>`
|
||
- 约束:
|
||
- `table.is_some()` 时必须同时提供 `schema`
|
||
|
||
### Query
|
||
|
||
- 输入:`db_core::QueryRequest`
|
||
- 字段:
|
||
- `sql`
|
||
- `source`
|
||
- `parameters`
|
||
- `source` 语义:
|
||
- `QuerySource::Inline`
|
||
- `QuerySource::File`
|
||
|
||
### Export
|
||
|
||
- 输入:
|
||
- `db_core::QueryRequest`
|
||
- `db_core::ExportRequest`
|
||
- `ExportRequest` 字段:
|
||
- `format`
|
||
- `output_path`
|
||
- `overwrite`
|
||
- 约束:
|
||
- 只允许导出返回结果集的查询
|
||
- 不静默覆盖已有文件
|
||
- 不为 TUI 增加新的导出元数据存储
|
||
|
||
## 4. 共享结构化结果
|
||
|
||
TUI 应优先直接调用 `crates/db-app`,不要解析 CLI 的人类可读文本。
|
||
|
||
### Connect
|
||
|
||
- 返回 `ConnectResponse`
|
||
- 字段:
|
||
- `target.profile_name`
|
||
- `target.driver`
|
||
- `target.endpoint`
|
||
- `target.password_env_var`
|
||
- `status`
|
||
- 当前 `status` 固定为 `connected`
|
||
|
||
### Connection Management
|
||
|
||
- `load_profile_catalog` / `save_profile` / `delete_profile` / `activate_profile` 返回 `ProfileCatalogResponse`
|
||
- 字段:
|
||
- `active_profile_name`
|
||
- `profiles[]`
|
||
- `target`
|
||
- `is_active`
|
||
- `secret_source`:`none | session | env_var`
|
||
- `resolve_profile` / `resolve_active_profile` 返回运行态 `ConnectionProfile`
|
||
- 若 session secret 存在,优先注入该 secret
|
||
- 否则再尝试 `password_env_var`
|
||
- 解析阶段仍不把 secret 暴露到 summary / doc / 持久化内容
|
||
|
||
### Inspect
|
||
|
||
- 返回 `InspectResponse`
|
||
- 字段:
|
||
- `target`
|
||
- `scope.schema`
|
||
- `scope.table`
|
||
- `payload`
|
||
- `payload.kind` 为:
|
||
- `schemas`
|
||
- `tables`
|
||
- `columns`
|
||
- 当 `payload.kind=schemas` 时,每个 schema item 额外包含:
|
||
- `name`
|
||
- `availability`:`ready | restricted`
|
||
- `note`:可选;当 `availability=restricted` 时给出权限说明
|
||
- `payload.items` 允许为空;空数组不是错误
|
||
- PostgreSQL root inspect 现会保留 restricted schema 名称,并通过 item-level `availability` 暴露权限状态
|
||
- 当显式 schema inspect 命中存在但当前连接无权访问的 PostgreSQL schema 时,返回 `AppError.kind=inspect`,不再把 restricted schema 误报为 empty
|
||
|
||
### Query
|
||
|
||
- 返回 `QueryResponse`
|
||
- 字段:
|
||
- `target`
|
||
- `source.kind`:`inline` / `file`
|
||
- `source.label`
|
||
- `source.path`
|
||
- `result.columns`
|
||
- `result.rows`
|
||
- `result.rows_affected`
|
||
- `result.row_count`
|
||
- `rows_affected != null` 表示 command-style 执行成功但没有结果集
|
||
|
||
### Export
|
||
|
||
- 返回 `ExportResponse`
|
||
- 字段:
|
||
- `target`
|
||
- `source`
|
||
- `export.format`
|
||
- `export.output_path`
|
||
- `export.overwrite`
|
||
- `row_count`
|
||
|
||
## 5. 错误模型
|
||
|
||
`db-app::AppError.kind` 当前分为:
|
||
|
||
- `validation`
|
||
- `connection`
|
||
- `authentication`
|
||
- `inspect`
|
||
- `query`
|
||
- `export`
|
||
- `config`
|
||
|
||
这组分类是 TUI banner、QA 断言和未来 GUI 状态映射的当前稳定基础。
|
||
|
||
`AppError.target` 为可选字段:
|
||
|
||
- 有目标上下文时返回脱敏后的 `ConnectionSummary`
|
||
- 参数校验在目标尚未构成时可为 `null`
|
||
|
||
## 6. Worker / Channel 状态契约
|
||
|
||
`crates/db-app` 现提供:
|
||
|
||
- `AppOperation`:`connect | inspect | query | export | load_profiles | save_profile | delete_profile | activate_profile`
|
||
- `OperationState`:`running | success | empty | error`
|
||
- `AppEvent<T>`:worker / channel 推荐 envelope
|
||
|
||
推荐发送顺序:
|
||
|
||
1. 先发 `AppEvent::running(operation)`
|
||
2. 成功后发送 `response.into_event()`
|
||
3. 失败时发送 `error.into_event()`
|
||
|
||
状态语义固定如下:
|
||
|
||
- `connect`
|
||
- `running`:正在测试连接
|
||
- `success`:连接成功
|
||
- `error`:连接失败
|
||
- `inspect`
|
||
- `success`:返回非空 schema / table / column 集合
|
||
- `empty`:返回空集合,但请求本身成功
|
||
- `error`:inspect 失败;包括 restricted schema 等显式 scope 无权访问场景
|
||
- `query`
|
||
- `success`:返回至少一行,或 `rows_affected` 非空
|
||
- `empty`:返回零行且 `rows_affected == null`
|
||
- `error`:执行失败
|
||
- `export`
|
||
- `success`:文件写入成功;`row_count == 0` 仍算成功
|
||
- `error`:导出校验、查询或文件写入失败
|
||
- `load_profiles | save_profile | delete_profile | activate_profile`
|
||
- 当前先作为共享 operation 名称与 error.operation 保留,供连接管理 worker 接入
|
||
- 若后续 channelize 这些动作,应继续沿用 `running | success | error`,不要另起一套状态词
|
||
|
||
示例:
|
||
|
||
```rust
|
||
use db_app::{AppEvent, AppOperation};
|
||
|
||
sender.send(AppEvent::<()>::running(AppOperation::Query))?;
|
||
|
||
match db_app::query(&profile, &request) {
|
||
Ok(response) => sender.send(response.into_event())?,
|
||
Err(error) => sender.send(error.into_event())?,
|
||
}
|
||
```
|
||
|
||
## 7. CLI 结构化输出
|
||
|
||
CLI 继续保留默认文本输出,同时新增:
|
||
|
||
- `--result-format text`
|
||
- `--result-format json`
|
||
|
||
JSON 返回 envelope:
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"operation": "query",
|
||
"state": "success",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
- `status` 继续只区分 envelope 级成功/失败
|
||
- `state` 对齐共享应用层状态:
|
||
- `connect`:`success`
|
||
- `inspect`:`success | empty`
|
||
- `query`:`success | empty`
|
||
- `export`:`success`
|
||
|
||
错误返回 envelope:
|
||
|
||
```json
|
||
{
|
||
"status": "error",
|
||
"operation": "query",
|
||
"state": "error",
|
||
"error": {
|
||
"operation": "query",
|
||
"kind": "query",
|
||
"message": "..."
|
||
}
|
||
}
|
||
```
|
||
|
||
CLI JSON envelope 继续作为跨入口回归样本,但 TUI live path 不应以调用 CLI 二进制作为主集成方式。
|
||
|
||
## 8. Frontend 接入约束
|
||
|
||
- TUI 直接依赖 `crates/db-app` 与 `db-core`,不解析 CLI stdout/stderr
|
||
- UI 状态映射应优先使用 `OperationState`,不要自行发明另一套 success/empty/error 语义
|
||
- UI 在 schema 根列表上应优先消费 item-level `availability` / `note`,不要把 restricted schema 再降级成 empty
|
||
- `ConnectionSummary.endpoint` 与 `password_env_var` 仅用于展示脱敏上下文,不应用作 secret 来源
|
||
- 连接管理 UI 应优先消费 `ProfileCatalogResponse.secret_source` 与 active profile 语义,不自行读取持久化文件
|
||
- `AppError.kind` 必须原样进入状态区 / inspector,避免把 `validation`、`connection`、`query` 混成统一“失败”
|
||
- 若未来引入异步 worker 池,仍以共享 response/error 对象作为唯一 UI 数据源
|
||
|
||
## 9. 当前回归边界
|
||
|
||
- `crates/db-app` 回归需覆盖:
|
||
- response -> `OperationState` 映射
|
||
- `AppEvent<T>` / `AppError` 序列化
|
||
- 空 inspect / 空 query / command query 语义
|
||
- profile save/load/activate 的 secret 非持久化边界
|
||
- `apps/cli` 回归继续覆盖:
|
||
- `--result-format json` envelope
|
||
- text/json 输出在相同 `db-app` 结果上的一致性
|
||
|
||
## 10. 演进规则
|
||
|
||
- CLI 文本文案可小幅演进,但 `db-app` 结构化对象字段应保持兼容。
|
||
- 新增数据库驱动时,优先扩展 `db-drivers` 与 `db-app`,不要在 TUI/CLI 内复制数据库语义。
|
||
- 若未来扩展 profile store schema(例如新增 profile id、secret provider、连接历史),必须先 bump version 并补迁移策略文档,再进入实现。
|