# 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` - `table: Option` - 约束: - `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`: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` / `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 并补迁移策略文档,再进入实现。