Files
dbtool-cli-v1/TUI_BACKEND_CONTRACT.md
Paperclip CTO d5f69462b0
Some checks failed
release-smoke / macos-13 / x86_64-apple-darwin (push) Has been cancelled
release-smoke / ubuntu-latest / x86_64-unknown-linux-gnu (push) Has been cancelled
release-smoke / windows-latest / x86_64-pc-windows-msvc (push) Has been cancelled
feat(usable): integrate current dbtool implementation snapshot
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 08:26:18 +00:00

311 lines
9.7 KiB
Markdown
Raw Permalink 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.

# 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 并补迁移策略文档,再进入实现。