chore: bootstrap independent git workflow
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

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Paperclip CTO
2026-03-26 03:49:06 +00:00
commit 7424491944
60 changed files with 7793 additions and 0 deletions

100
.github/workflows/release-smoke.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: release-smoke
on:
workflow_dispatch:
push:
paths:
- ".github/workflows/release-smoke.yml"
- "Cargo.toml"
- "Cargo.lock"
- "apps/**"
- "crates/**"
- "README.md"
- "RELEASE_RUNBOOK.md"
- "SMOKE_RUNBOOK.md"
- "scripts/release/**"
jobs:
build-release-artifacts:
name: ${{ matrix.os }} / ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
archive_ext: tar.gz
binary_name: dbtool
- os: macos-13
target: x86_64-apple-darwin
archive_ext: tar.gz
binary_name: dbtool
- os: windows-latest
target: x86_64-pc-windows-msvc
archive_ext: zip
binary_name: dbtool.exe
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Resolve release metadata
shell: bash
run: |
version="$(node scripts/release/version.mjs)"
echo "APP_VERSION=$version" >> "$GITHUB_ENV"
echo "ARTIFACT_BASENAME=dbtool-$version-${{ matrix.target }}" >> "$GITHUB_ENV"
- name: Build release binary
run: cargo build --release --bin dbtool
- name: Smoke binary on Unix
if: runner.os != 'Windows'
shell: bash
run: scripts/release/smoke-binary.sh "target/release/${{ matrix.binary_name }}"
- name: Smoke binary on Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
$binary = "target/release/${{ matrix.binary_name }}"
& $binary --help | Out-Host
& $binary --version | Out-Host
- name: Package Unix artifact
if: runner.os != 'Windows'
shell: bash
run: scripts/release/package-unix.sh "${{ matrix.target }}" "target/release/${{ matrix.binary_name }}"
- name: Package Windows artifact
if: runner.os == 'Windows'
shell: pwsh
run: |
$artifactDir = Join-Path "dist" $env:ARTIFACT_BASENAME
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
Copy-Item "target/release/${{ matrix.binary_name }}" (Join-Path $artifactDir "${{ matrix.binary_name }}")
Copy-Item "README.md" (Join-Path $artifactDir "README.md")
Copy-Item "RELEASE_RUNBOOK.md" (Join-Path $artifactDir "RELEASE_RUNBOOK.md")
Copy-Item "SMOKE_RUNBOOK.md" (Join-Path $artifactDir "SMOKE_RUNBOOK.md")
$archivePath = "dist/$env:ARTIFACT_BASENAME.zip"
if (Test-Path $archivePath) {
Remove-Item $archivePath -Force
}
Compress-Archive -Path "$artifactDir/*" -DestinationPath $archivePath
$hash = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
Set-Content -Path "$archivePath.sha256" -Value "$hash $(Split-Path $archivePath -Leaf)"
- name: Upload packaged artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_BASENAME }}
path: |
dist/${{ env.ARTIFACT_BASENAME }}.${{ matrix.archive_ext }}
dist/${{ env.ARTIFACT_BASENAME }}.${{ matrix.archive_ext }}.sha256
if-no-files-found: error

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/target
/examples/tmp
/dist
/.agents

129
ACCEPTANCE_CHECKLIST.md Normal file
View File

@@ -0,0 +1,129 @@
# dbtool-cli-v1 验收检查表(初版)
## 当前结论
- 当前环境中,项目配置声明的主工作区路径为 `/workspace/repo/dbtool-cli-v1`
- QA 首次盘点时,该路径原本不存在;当前已经补齐 Rust workspace、CLI、demo seed 资产、smoke runbook 和 release-smoke workflow 基线。
- 2026-03-26 本地 Linux 已执行CLI `--help``--version`、SQLite `connect` / `inspect` / `query` / `export`、SQLite seed 重放、Linux artifact checksum 与 artifact smoke。
- 2026-03-26 当前 QA runner 再次确认:`node v24.14.0` 可用,`docker``cargo` 均不存在;因此本机只能继续复核预编译二进制与 SQLite 路径,不能在此处完成 PostgreSQL / MySQL 的 Docker-backed smoke。
- 当前检查表仍不能直接宣布通过,因为 PostgreSQL / MySQL 的 Docker-backed smoke 与 GitHub 的 macOS / Windows release runner 还未在本 agent 环境实测。
## 与 PM / CTO 的当前对齐依据
- PM 对齐来源:项目说明与任务描述中定义的范围和完成标准。
- CTO 对齐来源:项目说明中的工程边界与跨平台目标。
- 待补强项:来自产品说明任务和架构任务的正式共享文档落地后,需要回填到本检查表。
## 发布门槛
- [x] 项目仓库已落地到可访问工作区,路径可重复获取
- [x] 安装、启动、demo、seed、smoke、Docker 路径存在且写入文档
- [ ] PostgreSQL、MySQL、SQLite 三类数据库均有可执行验证步骤
- [x] macOS、Linux、Windows 的期望行为已记录
- [x] 失败路径可复现、可记录、可区分严重级别
- [ ] 验收证据可由非开发者重复执行
## 基础可运行性
- [x] 仓库可拉起或可构建
- [x] CLI 二进制或运行命令已定义
- [x] `--help` 输出稳定
- [x] 版本信息可读取
- [ ] 缺失依赖时返回可理解错误
## 启动 / Demo / Seed / Smoke / Docker
- [x] 启动说明存在且从零可执行
- [x] demo 数据准备步骤存在
- [x] seed 步骤存在且幂等
- [x] smoke 脚本存在且可一键执行
- [ ] Dockerfile 或 compose 路径存在并可运行
- [x] 上述路径都有预期输出与失败输出说明
## 连接验证
### PostgreSQL
- [ ] 有效连接参数可成功连接
- [ ] 无效 host / port / database / user / password 会失败
- [ ] 网络不可达时返回清晰错误
- [ ] 权限不足时返回清晰错误
### MySQL
- [ ] 有效连接参数可成功连接
- [ ] 无效 host / port / database / user / password 会失败
- [ ] 网络不可达时返回清晰错误
- [ ] 权限不足时返回清晰错误
### SQLite
- [x] 有效文件路径可成功连接
- [x] 文件不存在时返回清晰错误
- [x] 路径无权限时返回清晰错误
- [x] 非数据库文件时返回清晰错误
## Schema Introspection
- [ ] 可列出 schema适用时
- [x] 可列出 table / view
- [x] 可查看列定义
- [ ] 空 schema / 空数据库表现明确
- [ ] 不支持对象类型时提示明确
## 查询执行
- [x] 可执行基础查询
- [x] 可执行带条件查询
- [x] 空结果集表现正确
- [x] 语法错误会失败且可定位
- [ ] 超时 / 长查询路径可观测
- [x] 参数化查询行为明确
## 导出流程
- [x] 导出命令存在
- [x] 输出路径规则明确
- [x] 目标文件已存在时行为明确
- [ ] 空结果导出行为明确
- [x] 编码、换行、分隔符规则明确
- [x] 导出失败时无静默成功
## 边界与回归关注
- [ ] 大结果集不会直接崩溃
- [x] 特殊字符 / Unicode 结果可处理
- [ ] NULL 值处理一致
- [ ] 时区 / 日期时间输出规则明确
- [x] 数据库间差异有记录
- [ ] 回归验证步骤可重复执行
## 2026-03-26 执行证据
- 本地 Linux`target/release/dbtool --help``target/release/dbtool --version`
- SQLite happy path`connect``inspect --schema main``inspect --schema main --table tickets``query``export --format csv|json`
- SQLite failure pathmissing file、permission denied、missing table、multi statement blocked、export overwrite blocked
- SQLite `connect` failure pathnon-db file 现在返回非零退出码,并输出 `connection failed: file is not a database`
- SQLite seed`examples/scripts/bootstrap-sqlite.sh` 使用 Node fallback 成功重放
- Linux artifact`dist/dbtool-0.1.0-x86_64-unknown-linux-gnu.tar.gz` checksum 通过,解包后二进制 `--help` / `--version` 通过
- 当前 runner 能力盘点:`node v24.14.0` 存在;`sqlite3``python3``docker``cargo` 不存在
- 当前 runner 复核:`examples/scripts/bootstrap-sqlite.sh``target/release/dbtool connect --driver sqlite ...``inspect --schema main``query --file examples/sql/sqlite/happy_path_query.sql` 再次通过
- 当前 runner 导出复核:已有文件时 `export` 明确拒绝覆盖;改用 `examples/tmp/qa-export-2.csv` 后导出 `4` 行成功,内容与 `EXPECTED_RESULTS.md` 一致
## 平台矩阵
| 场景 | Linux | macOS | Windows | 备注 |
| --- | --- | --- | --- | --- |
| 安装 / 构建 | 本地 Linux 已验证 | 待验证 | 待验证 | GitHub workflow 已配置macOS / Windows runner 未在当前环境执行 |
| CLI 启动 | 本地 Linux 已验证 | 待验证 | 待验证 | `--help` / `--version` 已纳入 release-smoke workflow |
| PostgreSQL | 待验证 | 待验证 | 待验证 | 需 Docker 环境完成真实 smoke |
| MySQL | 待验证 | 待验证 | 待验证 | 需 Docker 环境完成真实 smoke |
| SQLite | 本地 Linux 已验证 | 待验证 | 待验证 | 本地 query / export happy path 已验证 |
| 导出流程 | 本地 Linux 已验证 | 待验证 | 待验证 | CSV / JSON 本地已验证,跨平台产物仍待 CI 实测 |
## 当前阻塞
- 阻塞 1当前 agent 环境没有 `docker` 可执行文件,因此无法在此处实测 PostgreSQL / MySQL 的 `connect` / `inspect` / `query` / `export` smoke。
- 阻塞 2当前 agent 环境无法执行 GitHub 的 macOS / Windows runner因此 release artifact matrix 还缺跨平台实测证据。
- 阻塞 3当前 agent 环境也没有 `cargo`,因此即使迁入宿主机方案,仍需预编译二进制或额外准备 Rust toolchain。
- 阻塞 4发布 workflow 已落地,但仍需真实 CI 运行记录与 QA 证据来关闭发布前验收风险。

1505
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
Cargo.toml Normal file
View File

@@ -0,0 +1,29 @@
[workspace]
members = [
"apps/cli",
"crates/db-config",
"crates/db-core",
"crates/db-drivers",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.82"
[workspace.dependencies]
db-config = { path = "crates/db-config" }
db-core = { path = "crates/db-core" }
db-drivers = { path = "crates/db-drivers" }
mysql = { version = "28.0.0", default-features = false, features = ["minimal-rust"] }
postgres = "0.19.12"
rusqlite = { version = "0.37.0", features = ["bundled"] }
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
dbg_macro = "deny"
todo = "deny"
unwrap_used = "deny"

58
DEFECT_CLASSIFICATION.md Normal file
View File

@@ -0,0 +1,58 @@
# dbtool-cli-v1 缺陷分类(初版)
## 严重级别
### Blocker
- CLI 无法启动
- 三种目标数据库全部无法连接
- 基础查询完全不可用
- 导出产生错误结果且无替代路径
- 发布路径不可构建、不可运行、不可验证
### Critical
- 任一目标数据库的核心 happy path 不可用
- 错误提示误导用户造成高概率误操作
- 数据结果明显错误
- 导出文件损坏或内容错乱
### Major
- schema introspection 不完整
- 部分查询场景失败
- 平台间行为明显不一致
- 失败路径不可定位
### Minor
- 帮助文案、提示文案、非阻断交互问题
- 输出格式轻微不一致
## 缺陷类型
- 功能缺陷
- 回归缺陷
- 可用性缺陷
- 可重复性缺陷
- 文档缺陷
- 环境 / 配置缺陷
## 缺陷记录最小字段
- 标题
- 环境
- 数据库类型
- 操作系统
- 复现步骤
- 预期结果
- 实际结果
- 证据
- 严重级别
- 是否阻塞发布
## 发布阻断原则
- 任一 Blocker 未关闭,不得宣布通过。
- Critical 缺陷若无明确降级方案,不得宣布通过。
- 未复现或无证据的“怀疑问题”不能当作结论,但必须登记为风险观察。

63
DEVELOPING.md Normal file
View File

@@ -0,0 +1,63 @@
# Developing dbtool-cli-v1
## 本地开发
```bash
cargo fmt
cargo test
cargo run -p dbtool-cli -- --help
```
Git 仓库、branch、commit、push 与 agent 认证规范见 `GIT_WORKFLOW.md`
如果当前 Linux 环境没有 `cc`,改用一个 C 兼容 linker例如 `zig cc`
```bash
cat > /tmp/zig-cc <<'EOF'
#!/bin/sh
exec zig cc "$@"
EOF
chmod +x /tmp/zig-cc
export CC=/tmp/zig-cc
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/tmp/zig-cc
cargo test
cargo run -p dbtool-cli -- --help
```
## 当前 crate 责任
- `apps/cli`:命令行入口、参数解析、退出码与用户可见输出
- `crates/db-core`:领域模型、输入约束、公共错误
- `crates/db-config`:连接 profile、配置抽象、脱敏摘要
- `crates/db-drivers`:驱动 trait 与按数据库分发的边界
## 当前约束
- 当前 PostgreSQL、MySQL 与 SQLite 已实现真实数据库 I/O
- `export` 已通过共享查询结果模型实现 CSV / JSON 写出
- 子命令的 flag 形状已稳定到可供 QA / GUI 复用的程度,后续只能做兼容性演进
- 错误信息必须避免泄露密码等敏感信息
- PostgreSQL 非参数化查询使用 simple query可执行多语句 SQL 文件;带 `--param` 时会切到 prepared execution因此应传单条参数化语句
- MySQL 非参数化查询使用 text protocol可执行多语句 SQL 文件;带 `--param` 时会切到 positional prepared execution因此应传单条参数化语句
- MySQL `inspect` 继续复用 `--schema` flag但语义上对应数据库名
- SQLite `inspect` 也复用 `--schema` flag但 demo 路径固定使用 `main`
- SQLite 查询当前按单条 statement 执行;多语句 SQL 会返回显式错误,而不是隐式忽略后续语句
- `export` 当前要求查询必须返回结果集;非结果集语句会被明确拒绝
- `export` 当前要求父目录已存在,且默认拒绝覆盖已有文件,除非显式传入 `--overwrite`
## 验证路径
- `cargo build` 验证 workspace 可编译
- `cargo test` 验证核心输入校验、脱敏逻辑和 CLI 参数解析
- `cargo run -p dbtool-cli -- --help` 验证 CLI 可启动
- `cargo run -p dbtool-cli -- connect --help``cargo run -p dbtool-cli -- inspect --help` 验证关键子命令可启动
- `cargo run -p dbtool-cli -- query ... --file <sql>` 验证 SQL 文件读取入口
- `cargo run -p dbtool-cli -- export ... --format <csv|json> --output <path>` 验证导出路径
- `./examples/scripts/bootstrap-sqlite.sh` + SQLite CLI happy path 验证本地样例数据库闭环
- 无系统 linker 的 Linux 环境使用 `zig cc` 作为 host linker 验证路径
## 发布 smoke
- GitHub Actions baseline: `.github/workflows/release-smoke.yml`
- 发布与回滚说明:`RELEASE_RUNBOOK.md`
- 本地 Linux 产物复核:`scripts/release/smoke-binary.sh <binary>``scripts/release/package-unix.sh <target-triple> <binary>`

194
GIT_WORKFLOW.md Normal file
View File

@@ -0,0 +1,194 @@
# dbtool-cli-v1 Git 仓库与 Agent 提交规范
日期2026-03-26
作者CTO
## 1. 结论
- `dbtool-cli-v1` 应立即作为独立 Git 仓库初始化不再依赖任何“Paperclip 根仓库”语义。
- 默认远程使用 HTTPS
- `https://gitea.shay7sev.site/paperclip/dbtool-cli-v1.git`
- agent 不允许直接 push 到默认分支 `main`
- 一个实现类 issue 允许多次 commit但每次都必须对应清晰检查点。
- 前端、后端各自对自己负责的 issue 分支与提交负责;跨前后端/集成类 issue 由该 issue 的 owner 负责最终整合与合并。
- `main` 应开启 protected branch常规变更走 branch + review gate只有一次性仓库 bootstrap 允许例外。
## 2. 独立仓库边界
从本次初始化开始,项目根目录就是唯一仓库边界:
- 仓库根:`/workspace/repo/dbtool-cli-v1`
- Git 元数据:`/workspace/repo/dbtool-cli-v1/.git`
- 远程名:`origin`
- 默认分支:`main`
这意味着:
- 共享计划、backlog、runbook、代码和测试都跟随本仓库历史演进。
- 后续任何 agent 都应在本仓库内提交,不再把该项目视为上层 monorepo 的子目录。
- 若未来存在前后端共存结构,也应继续共用此仓库边界,按目录分工,而不是拆成“一个产品多个隐藏仓库”。
## 3. 远程接入方案结论
### 推荐HTTPS + Gitea bot token
推荐把 HTTPS 作为默认 push 方案,原因如下:
- 容器内不依赖宿主机 SSH agent 或私钥转发。
- 更容易通过 Paperclip secret / 环境变量注入。
- 更适合做统一脚本化封装,复用到前端、后端和全栈项目。
- 轮换 token 和停用单个 bot 凭据更直接。
建议采用:
- 一个专用 Gitea bot / service account
- 仅授予目标仓库写权限
- token 不写入仓库,不落盘到共享文档
- agent 运行时通过环境变量注入:
- `GITEA_USERNAME`
- `GITEA_TOKEN`
### 备选SSH bot key / deploy key
SSH 可以作为组织偏好的备选,但不是当前默认推荐:
- 优点:不需要 HTTP token可与已有 SSH 管理习惯一致。
- 缺点:容器内要管理私钥文件、`known_hosts`、权限位与 key 分发。
- 对多项目复用来说,落地复杂度高于 HTTPS。
只有在以下条件成立时再选 SSH
- 组织已有稳定的 bot key 生命周期管理;
- 已有容器内密钥挂载与 host fingerprint 管理机制;
- 明确要求禁用 HTTPS token。
## 4. Agent 认证与 push 机制
### 最小可落地方案
1. 运行时向 agent 注入:
- `GITEA_USERNAME`
- `GITEA_TOKEN`
2. 使用 `GIT_ASKPASS` 辅助脚本回答 Git 的用户名/密码提示。
3. 本地仓库远程固定为:
- `origin -> https://gitea.shay7sev.site/paperclip/dbtool-cli-v1.git`
4. push 时只推送当前工作分支,不推送 `main`
仓库内已提供最小脚本:
- `scripts/git/gitea-askpass.sh`
示例:
```bash
export GITEA_USERNAME=paperclip-bot
export GITEA_TOKEN=***REDACTED***
export GIT_ASKPASS="$PWD/scripts/git/gitea-askpass.sh"
git push -u origin HEAD
```
### 安全边界
- token 只通过运行时环境注入。
- 禁止把 token 写入 `.git/config`、shell history 或文档。
- bot 凭据只用于 Git 读写,不复用个人账号。
- 若 agent 只需 commit、不需 push则不注入 `GITEA_TOKEN`
## 5. Commit / Branch / Push 规则
### Commit 粒度
实现类 issue 允许多次 commit但必须满足
- 每次 commit 对应一个清晰检查点;
- 检查点至少满足“代码可读”或“文档可评审”;
- 不允许把多个无关 issue 混在一个 commit
- 未达到检查点时可继续在工作区迭代,不强制碎片化提交。
推荐检查点:
- 一个子命令可运行
- 一个 defect 已复现并修复
- 一组测试新增并通过
- 一个 runbook / governance 文档完成首版
### Branch 规则
默认分支命名:
- agent`agent/<agent-name>/<issue-id>-<slug>`
- human`user/<name>/<issue-id>-<slug>`
示例:
- `agent/backend/CMP-14-sqlite-connect-validation`
- `agent/cto/CMP-18-git-bootstrap`
- `user/alice/CMP-12-release-smoke`
### Push 规则
- agent 不直接 push `main`
- agent 只 push 自己负责 issue 的工作分支
- 合并到 `main` 前必须经过 review gate
- 仅仓库第一次 bootstrap 可由指定 owner 执行一次受控初始化推送
## 6. Protected Branch / Review Gate
`main` 应立即启用以下保护:
- 禁止直接 push
- 禁止 force push
- 至少 1 个 review
- CI / smoke 必须通过后才允许合并
当前阶段的最小 gate
- Rust 项目:`cargo test`
- CLI 可启动检查:`cargo run -p dbtool-cli -- --help`
- 发布链路 issue 涉及时,再叠加 release smoke
## 7. 前端 / 后端 / 集成 issue 提交责任
- 后端 issue由后端 owner 负责提交、push、自测说明和最终合并准备
- 前端 issue由前端 owner 负责提交、push、预览说明和最终合并准备
- 测试 / QA issue由 QA owner 提交测试资产、矩阵、runbook 和证据脚本
- 集成类 issue由该 issue 明确 owner 负责最终整合提交
管理规则:
- 不存在“别人顺手帮你提交”的默认机制
- 如需跨角色修改,必须在 issue 上明确 owner 与验收边界
- 多人参与同一产品仓库时,各自 push 自己的分支,由集成 owner 收口
## 8. dbtool-cli-v1 当前落地动作
本次已完成:
- 在项目根目录初始化独立 Git 仓库
- 确认目标远程仓库页面可达:
- `https://gitea.shay7sev.site/paperclip/dbtool-cli-v1`
- 配置 `origin`
- 完成首次本地 commit证明 agent 在受控条件下具备 commit 能力
本次未做:
- 未直接 push 到远程默认分支
- 未在仓库中存放任何长期凭据
- 未跳过未来应有的 branch protection / review gate
## 9. 后续复用方式
该机制可直接复用到前后端共存项目:
1. 每个产品/项目一个独立仓库
2. 统一使用 `origin` + HTTPS bot token
3. 统一使用 `agent/...` / `user/...` 分支命名
4. 统一要求“一个 issue 多次 commit 允许,但每次必须是清晰检查点”
5. 统一由 issue owner 对最终提交与合并负责
如果未来要平台化,可以把以下内容抽成公司级模板:
- `GIT_WORKFLOW.md`
- `scripts/git/gitea-askpass.sh`
- protected branch 默认策略
- Paperclip agent 运行时凭据注入规范

32
PRE_RELEASE_CHECKLIST.md Normal file
View File

@@ -0,0 +1,32 @@
# dbtool-cli-v1 发布前检查清单(初版)
## 发布前必须确认
- [ ] 当前发布版本、提交来源、构建来源明确
- [x] 项目工作区可访问
- [ ] README / 运行说明与实际一致
- [x] smoke 路径可执行
- [x] demo / seed 路径可执行
- [ ] Docker 路径可执行(如果承诺提供)
## 功能门槛
- [ ] PostgreSQL happy path 通过
- [ ] MySQL happy path 通过
- [x] SQLite happy path 通过
- [x] 至少一条连接失败路径已验证
- [x] 至少一条查询失败路径已验证
- [x] 至少一条导出失败路径已验证
## 证据门槛
- [x] 验收检查表已更新
- [x] 失败路径有复现步骤
- [x] 已知风险有记录
- [x] 未验证项有明确原因
## 当前状态
- 当前清单已具备可执行代码、SQLite 主流程证据、SQLite non-db file failure-path 证据和 Linux release artifact smoke 证据,但仍不可整体判定通过。
- 原因 1当前 agent 环境无法执行 GitHub 的 macOS / Windows runner也没有 `docker` 来完成 PostgreSQL / MySQL 的真实 smoke 复核。
- 原因 2当前 agent 环境没有 `cargo`,后续宿主机复核必须提前准备 Rust toolchain 或直接使用预编译二进制。

218
PRODUCT_REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,218 @@
# dbtool-cli-v1 Product Requirements
## Document Purpose
This document defines the first release boundary for `dbtool-cli-v1` so the CTO can split implementation work without guessing and QA can derive a repeatable acceptance checklist.
## Current Demo Audit
- As of 2026-03-25, the configured project workspace had no repository contents, no Rust workspace, no README, and no runnable CLI binary.
- The current project state is therefore a scoped backlog plus a project goal, not an already-working demo.
- This document should be treated as the product baseline for implementation, not as a polish pass on existing behavior.
## Target Users
### Primary User
Terminal-comfortable technical operators who need one consistent way to work across PostgreSQL, MySQL, and SQLite without switching between three native tools.
Typical roles:
- backend engineers investigating application data
- data engineers validating schema and query output
- QA or support engineers collecting evidence from a database during debugging
### User Need
The user needs a single local CLI that can:
1. verify a connection
2. inspect database structure
3. execute SQL
4. export query results
The value is consistency across supported databases, not feature depth equal to a full SQL IDE.
## Product Problem Statement
Today, the operator would otherwise need to learn different connection rules, inspection commands, and export workflows across `psql`, MySQL tools, and SQLite tooling. `dbtool-cli-v1` should reduce that tool-switching cost for the most common technical workflows.
## Primary V1 Workflow
The most important end-to-end flow is:
1. choose a supported database target
2. confirm the tool can connect
3. inspect schemas, tables, and columns to find the right object
4. execute SQL against that object
5. export the result for analysis, sharing, or evidence capture
If this flow is not smooth, V1 is not successful.
## V1 Scope
### In Scope
- local CLI only
- database support for PostgreSQL, MySQL, and SQLite only
- explicit connection selection for each run
- schema and table inspection
- column-level metadata inspection
- ad hoc SQL execution
- `.sql` file execution
- export of query results to CSV and JSON
- clear terminal output and actionable failure messages
### Product Boundaries
- V1 is an operator tool, not a visual database client.
- V1 must optimize for correctness and consistency before convenience features.
- Exact command names, flag shapes, crate boundaries, and config storage are implementation choices owned by the CTO as long as they satisfy the acceptance criteria below.
## Non-Goals
The following are explicitly out of scope for V1:
- desktop GUI or web GUI
- support for databases beyond PostgreSQL, MySQL, and SQLite
- migration management or schema-editing workflows
- ORM features, query builders, or notebook-style authoring
- background sync, job scheduling, alerts, or long-running daemon behavior
- shared team workspaces, cloud sync, or centralized connection management
- dashboards, charting, or analytics visualization
- advanced IDE features such as autocomplete, visual explain plans, or query history UX beyond what is minimally needed for CLI execution
## Acceptance Criteria
### Global Acceptance
- All supported flows use one coherent CLI mental model across PostgreSQL, MySQL, and SQLite.
- Every failure returns a non-zero exit path and an actionable terminal message.
- Secrets are not echoed back in plain text error output.
- Driver-specific differences that affect user behavior are documented in the project README or command help.
### Connect
The `connect` flow is acceptable when:
- the user can provide a supported database type plus connection input and the CLI validates that target
- success output clearly confirms which database type and target was reached without leaking secrets
- unsupported database types are rejected before an attempted connection
- authentication errors, network errors, unreachable hosts, and SQLite file path errors are distinguishable enough for the user to act
- a failed connection does not create the impression that later commands are safe to run
### Inspect
The `inspect` flow is acceptable when:
- after selecting a valid target, the user can list available schemas or equivalent top-level objects for that database
- the user can list tables or views within the chosen scope
- the user can inspect columns for a table or view and see at least name, type, nullability, and key or primary-key indication when the driver exposes it
- empty databases or empty scopes return a clear "no objects found" style outcome instead of a stack trace
- SQLite differences are handled gracefully even though it does not mirror server-database structure exactly
### Query
The `query` flow is acceptable when:
- the user can run SQL entered inline or from a `.sql` file
- row-returning statements show column headers and row output in a human-readable terminal format
- non-row-returning statements show a clear execution summary and affected row count when the driver provides it
- invalid SQL, permission failures, and driver execution failures return actionable errors
- empty result sets are treated as successful execution, not as a tool failure
- the output format remains suitable for terminal reading and shell piping
### Export
The `export` flow is acceptable when:
- the user can export query results to CSV or JSON
- the export path is explicit
- the CLI does not silently overwrite an existing file unless the user explicitly allows overwrite behavior
- exported row counts match the query result set
- text output is UTF-8 and stable for common multilingual content
- export failures never report success and leave the user with a clear next action
## Recommended User Scenarios
### Scenario A: Backend Debugging
A backend engineer needs to verify whether a bug is caused by bad data:
1. connect to a PostgreSQL or MySQL environment
2. inspect the relevant schema and table
3. run a targeted query
4. export the result to JSON or CSV for a bug report
### Scenario B: Local SQLite Verification
A QA engineer receives a local SQLite file:
1. point the CLI at the file path
2. inspect available tables
3. run a validation query
4. export evidence for a test record
These scenarios should remain happy-path examples in engineering docs and QA checks.
## CLI To Product Evolution
GUI is not in the current scope, but the CLI should evolve in a way that supports a later product surface.
The CLI should establish stable product concepts:
- connection target
- catalog or schema browser state
- query execution
- result set
- export job
Future desktop or web surfaces should map directly to the same concepts:
- connection manager
- schema browser
- query editor
- results table
- export action
### Evolution Rule
The future GUI should reuse the same underlying product workflow, not invent a separate one. The CLI is the first validation surface for product behavior.
### Implication For Engineering
Engineering should avoid CLI-only shortcuts that would make later UI reuse difficult, especially around:
- connection target representation
- inspection result structure
- query result structure
- export request inputs and outputs
This is a product direction, not a mandate for a specific architecture.
## Handoff Notes For CTO
The following decisions are fixed for V1:
- supported databases: PostgreSQL, MySQL, SQLite
- surface: local CLI
- critical path: connect, inspect, query, export
- GUI: out of scope
- acceptance must cover both success and obvious failure paths
The following decisions remain implementation-owned by the CTO:
- exact command names and flags
- Rust workspace structure
- connection config format and storage approach
- output rendering library choices
- packaging and distribution mechanics
## Release Readiness Definition
`dbtool-cli-v1` is ready for a first release when:
- the primary workflow works across PostgreSQL, MySQL, and SQLite
- the documented non-goals have not crept into scope
- QA can execute acceptance checks directly from this document and companion test material
- CTO can map the remaining work into engineering tasks without reopening basic product questions

118
QA_RUNTIME_ENVIRONMENT.md Normal file
View File

@@ -0,0 +1,118 @@
# PostgreSQL / MySQL Runtime Smoke 环境建议
## 结论
当前阶段的推荐方案是:**让 QA 在宿主机执行 smoke但数据库仍使用仓库内的 Docker Compose 临时拉起**。
- 继续复用现有 `docker-compose.demo.yml`、bootstrap 脚本和 SQL fixtures
- 不依赖长期运行的宿主机 PostgreSQL / MySQL
- 不在当前阶段扩展 `local-db-codex` 运行时去支持 Docker
这条路径最符合当前项目状态:仓库里的 smoke 输入已经存在,真正缺的是一个有 Docker 的执行宿主,而不是新的产品代码或平台改造。
## 三种方案对比
### 方案 A宿主机长期运行 PostgreSQL / MySQL
优点:
- 首次接入快
- QA 不需要每次拉起容器
缺点:
- 状态容易漂移,重复执行时很难保证数据库干净
- 需要长期维护端口、用户、权限和版本
- 一旦多人共用宿主机,测试数据会互相污染
- 更适合共享开发环境,不适合作为 release smoke 证据来源
结论:**可作为临时兜底,不作为推荐主路径。**
### 方案 B为 `local-db-codex` 部署增加 Docker runtime 能力
优点:
- 从 agent 视角最方便,理论上可以把 smoke 也放回当前容器体系
- 后续若多个项目都依赖容器型集成测试,长期上限更高
缺点:
- 这是平台/安全/运维改造,不是当前仓库内的小改动
- 会引入容器嵌套、权限边界、镜像缓存、资源隔离等额外复杂度
- 当前项目的真实瓶颈是缺可执行宿主,不是缺应用层代码
结论:**不适合作为本阶段 unblock 手段,只在未来多个项目都提出同类需求时再立项。**
### 方案 CQA runner 在宿主机执行,数据库通过 Docker Compose 临时拉起
优点:
- 直接复用现有仓库资产,落地最快
- 每次 `up` / `down -v` 都是干净环境,可重复性最好
- 不要求改 `local-db-codex` 平台
- 后续可平滑迁移到自托管 runner 或专用 QA 主机
缺点:
- 需要一台具备 Docker / `docker compose` 的宿主机
- 宿主机上仍需准备 Rust toolchain 或 release binary
结论:**这是当前推荐方案。**
## 推荐执行方式
### 宿主机要求
- Linux 或 macOS 宿主机
- 已安装 Docker 和 `docker compose`
- 可运行 Rust `cargo`,或已取得本仓库 release / debug 二进制
- 2026-03-26 当前 QA agent runner 不满足该要求:它有 `node v24.14.0` 和预编译 `target/release/dbtool`,但缺少 `docker``cargo`
### 使用步骤
1. 在宿主机拉取当前仓库代码并进入项目根目录
2. 准备 CLI
- 开发验证:`cargo build`
- 或使用已生成的 `target/debug/dbtool` / `target/release/dbtool`
3. 导出密码环境变量:
```bash
export DBTOOL_PASSWORD=dbtool
```
4. 拉起 PostgreSQL / MySQL
```bash
docker compose -f docker-compose.demo.yml up -d postgres mysql
```
5. 导入 demo 数据:
```bash
./examples/scripts/bootstrap-postgres.sh
./examples/scripts/bootstrap-mysql.sh
```
6.`SMOKE_RUNBOOK.md` 执行 PostgreSQL / MySQL 的 `connect` / `inspect` / `query` / `export`
7. 将结果回填到 `ACCEPTANCE_CHECKLIST.md``TEST_STRATEGY.md``PRE_RELEASE_CHECKLIST.md`
8. 清理环境:
```bash
docker compose -f docker-compose.demo.yml down -v
```
## 团队职责
- 后端:维护 `docker-compose.demo.yml`、bootstrap 脚本、fixtures 和命令契约
- QA在宿主机执行 smoke沉淀证据和失败复现记录
- 前端:当前继续 gated不参与此轮 runtime smoke
- CTO维持方案边界若没有 Docker-capable host再升级为平台支持请求
## 风险与维护成本
- 宿主机不可用:需要指定一台固定 QA 主机或自托管 runner
- 二进制与文档漂移QA 执行时必须绑定具体 commit 或 release artifact
- 端口冲突:优先使用专用 QA 主机;必要时再参数化 compose 端口
- 当前 runner 与目标宿主能力不一致:本地可继续复核 SQLite 与预编译二进制,但 PostgreSQL / MySQL Docker smoke 必须迁移到符合要求的宿主机
总体维护成本低,因为主路径复用仓库现有资产,不引入新的长期数据库运维负担,也不引入新的平台能力改造。

112
README.md Normal file
View File

@@ -0,0 +1,112 @@
# dbtool-cli-v1
`dbtool-cli-v1` 是一个面向 PostgreSQL、MySQL 和 SQLite 的跨数据库 CLI 工具。
当前仓库已经具备 Rust workspace 基线,并已落地 PostgreSQL、MySQL 与 SQLite 的首个 happy path
-`Cargo` workspace
- `apps/cli` 可执行入口
- `crates/db-core` 领域模型与输入校验
- `crates/db-config` 连接 profile 与脱敏摘要
- `crates/db-drivers` 驱动边界与 PostgreSQL / MySQL / SQLite 实现
## 当前状态
当前已实现:
- PostgreSQL `connect`
- PostgreSQL `inspect`
- PostgreSQL `query`
- MySQL `connect`
- MySQL `inspect`
- MySQL `query`
- SQLite `connect`
- SQLite `inspect`
- SQLite `query`
- `export` 到 CSV
- `export` 到 JSON
- `.sql` 文件读取
- `--password-env` 密码注入且保持输出脱敏
已验证:
- 标准环境:`cargo build`
- 标准环境:`cargo test`
- 标准环境:`cargo run -p dbtool-cli -- --help`
- 链接器受限的 Linux 环境:使用 `zig cc` 作为 host linker 执行 `cargo check` / `cargo test`
当前 `connect` / `inspect` / `query` / `export` 在 PostgreSQL、MySQL 与 SQLite 的共享抽象上都已经可用;当前剩余工作主要是 Docker 环境下的跨数据库 smoke 与发布链路。
## Workspace 结构
```text
dbtool-cli-v1/
Cargo.toml
README.md
DEVELOPING.md
apps/
cli/
Cargo.toml
src/main.rs
crates/
db-core/
db-config/
db-drivers/
```
## 快速开始
```bash
cargo run -p dbtool-cli -- --help
export DBTOOL_PASSWORD=dbtool
cargo run -p dbtool-cli -- connect --driver postgres --host 127.0.0.1 --port 55432 --database dbtool_demo --username dbtool --password-env DBTOOL_PASSWORD
cargo run -p dbtool-cli -- inspect --driver postgres --host 127.0.0.1 --port 55432 --database dbtool_demo --username dbtool --password-env DBTOOL_PASSWORD --schema qa_demo
cargo run -p dbtool-cli -- query --driver postgres --host 127.0.0.1 --port 55432 --database dbtool_demo --username dbtool --password-env DBTOOL_PASSWORD --file examples/sql/postgres/happy_path_query.sql
cargo run -p dbtool-cli -- connect --driver mysql --host 127.0.0.1 --port 53306 --database qa_demo --username dbtool --password-env DBTOOL_PASSWORD
cargo run -p dbtool-cli -- inspect --driver mysql --host 127.0.0.1 --port 53306 --database qa_demo --username dbtool --password-env DBTOOL_PASSWORD --schema qa_demo
cargo run -p dbtool-cli -- query --driver mysql --host 127.0.0.1 --port 53306 --database qa_demo --username dbtool --password-env DBTOOL_PASSWORD --file examples/sql/mysql/happy_path_query.sql
cargo run -p dbtool-cli -- connect --driver sqlite --path examples/tmp/dbtool-demo.sqlite
cargo run -p dbtool-cli -- inspect --driver sqlite --path examples/tmp/dbtool-demo.sqlite --schema main
cargo run -p dbtool-cli -- query --driver sqlite --path examples/tmp/dbtool-demo.sqlite --file examples/sql/sqlite/happy_path_query.sql
cargo run -p dbtool-cli -- export --driver sqlite --path examples/tmp/dbtool-demo.sqlite --file examples/sql/sqlite/export_query.sql --format csv --output examples/tmp/export.csv
```
如果当前 Linux 环境没有系统 `cc`,可以改用一个 C 兼容 linker例如 `zig cc`
```bash
cat > /tmp/zig-cc <<'EOF'
#!/bin/sh
exec zig cc "$@"
EOF
chmod +x /tmp/zig-cc
export CC=/tmp/zig-cc
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/tmp/zig-cc
cargo test
cargo run -p dbtool-cli -- --help
```
最小发布与产物 smoke 已定义在 `.github/workflows/release-smoke.yml``RELEASE_RUNBOOK.md`;当前 Linux 还可本地执行 `scripts/release/smoke-binary.sh``scripts/release/package-unix.sh` 复核产物命名与 checksum。
Git 仓库初始化、agent 提交规范与远程接入方式见 `GIT_WORKFLOW.md`
## PostgreSQL / MySQL / SQLite 行为说明
- 非参数化 `query` 会走 PostgreSQL simple query 协议,因此支持包含多条语句的 `.sql` 文件,例如 `SET search_path ...; SELECT ...`
- 非参数化 `query` 在 MySQL 上走 text protocol因此也支持包含多条语句的 `.sql` 文件,例如 `USE qa_demo; SELECT ...`
-`--param``query` 会切换到 prepared execution当前要求 SQL 为单条参数化语句
- 凭据只通过 `--password-env` 注入,错误和成功输出都不会回显密码
- MySQL `inspect` 里的 `schema` 参数当前映射到数据库名;顶层 `inspect` 返回的是数据库列表
- SQLite 顶层 `inspect` 返回附加数据库列表demo happy path 使用 `main``--schema main` 会列出表,`--schema main --table <table>` 会列出列
- SQLite 当前使用单条 statement 执行模型;若传入多语句 SQL会明确返回 `Multiple statements provided`
- `export` 只接受返回结果集的查询;若 SQL 只有 rows affected 而没有结果集,会明确报错
- `export` 不会静默覆盖已有文件,且会在父目录不存在时给出显式下一步提示
## 设计原则
- CLI、未来 GUI 与自动化场景共享同一套核心语义
- 先稳定领域边界、错误模型和可测试输入校验
- 驱动差异留在 `db-drivers`,不污染公共 crate
## 下一步
- `CMP-12`:在此 workspace 基线之上接入跨平台打包与 smoke

83
RELEASE_RUNBOOK.md Normal file
View File

@@ -0,0 +1,83 @@
# dbtool-cli-v1 Release Runbook
## Purpose
This runbook defines the smallest release artifact contract for `dbtool-cli-v1`.
## Current Baseline
- Release smoke is implemented in `.github/workflows/release-smoke.yml`.
- The workflow targets Linux, macOS, and Windows release runners.
- Each runner builds the release binary, runs `--help` and `--version`, packages one distributable archive, and emits a SHA256 checksum.
- Local validation in the current backend environment is limited to the Linux packaging path; macOS and Windows still require GitHub-hosted runners or equivalent CI workers.
## Artifact Contract
- Linux: `dbtool-<version>-x86_64-unknown-linux-gnu.tar.gz`
- macOS: `dbtool-<version>-x86_64-apple-darwin.tar.gz`
- Windows: `dbtool-<version>-x86_64-pc-windows-msvc.zip`
- Checksum: `<artifact-name>.sha256`
Each packaged artifact contains:
- `dbtool` or `dbtool.exe`
- `README.md`
- `RELEASE_RUNBOOK.md`
- `SMOKE_RUNBOOK.md`
## CI Steps
Each matrix entry in `.github/workflows/release-smoke.yml` performs:
1. install stable Rust
2. `cargo build --release --bin dbtool`
3. smoke the binary with `--help` and `--version`
4. package one archive with the platform naming contract
5. generate a SHA256 sidecar file
6. upload the archive and checksum as workflow artifacts
## Local Linux Validation
In a Linux environment with the same linker workaround used by the repo:
```bash
export HOME="$AGENT_HOME"
export CARGO_HOME="$AGENT_HOME/.cargo"
export RUSTUP_HOME="$AGENT_HOME/.rustup"
export PATH="$CARGO_HOME/bin:$PATH"
export CC="$AGENT_HOME/tools/zig-cc"
export AR="$AGENT_HOME/tools/zig-ar"
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$AGENT_HOME/tools/zig-cc"
cargo build --release --bin dbtool
scripts/release/smoke-binary.sh target/release/dbtool
scripts/release/package-unix.sh x86_64-unknown-linux-gnu target/release/dbtool
```
Expected outputs:
- `dist/dbtool-<version>-x86_64-unknown-linux-gnu.tar.gz`
- `dist/dbtool-<version>-x86_64-unknown-linux-gnu.tar.gz.sha256`
## QA Usage
For a produced artifact, QA should:
1. verify the SHA256 checksum
2. unpack the archive
3. run `dbtool --help`
4. run `dbtool --version`
5. follow `SMOKE_RUNBOOK.md` for database-backed validation in a Docker-capable environment
## Rollback
If a newly built artifact fails smoke or QA validation:
1. stop distributing the new archive set
2. keep the failing archive and checksum for investigation
3. redirect consumers back to the most recent previously verified artifact set
4. record the failure cause, affected platform, and whether the issue is build-time, package-time, or runtime
## Current Blocker
This runbook cannot claim fully verified cross-platform release readiness until the workflow runs successfully on real Linux, macOS, and Windows runners and QA completes the Docker-backed smoke path.

126
SMOKE_RUNBOOK.md Normal file
View File

@@ -0,0 +1,126 @@
# dbtool-cli-v1 Smoke Runbook
## Purpose
This runbook defines the smallest repeatable cross-database validation path for `connect`, `inspect`, `query`, and `export`.
## Current Status
- Demo databases and query fixtures are prepared.
- A repeatable Docker path exists for PostgreSQL and MySQL.
- 推荐执行环境已明确为“宿主机执行 + Docker Compose 临时数据库”,详见 `QA_RUNTIME_ENVIRONMENT.md`
- SQLite bootstrap exists for local execution.
- PostgreSQL `connect`, `inspect`, and `query` are now implemented in the Rust CLI.
- MySQL `connect`, `inspect`, and `query` are now implemented in the Rust CLI.
- SQLite `connect`, `inspect`, and `query` are now implemented in the Rust CLI.
- `export` to CSV / JSON is now implemented in the Rust CLI.
- Release artifact smoke is now defined separately in `RELEASE_RUNBOOK.md`.
- This runbook remains partially staged only because PostgreSQL / MySQL runtime smoke still requires a Docker-capable environment.
## Preconditions
- Docker with `docker compose`
- one of `sqlite3`, `node`, or `python3` for SQLite bootstrap
- compiled CLI binary from the Rust workspace baseline
## Bootstrap
### 1. Start PostgreSQL and MySQL
```bash
docker compose -f docker-compose.demo.yml up -d postgres mysql
```
### 2. Seed PostgreSQL
```bash
./examples/scripts/bootstrap-postgres.sh
```
Expected result:
- database: `dbtool_demo`
- schema: `qa_demo`
- tables: `accounts`, `tickets`
### 3. Seed MySQL
```bash
./examples/scripts/bootstrap-mysql.sh
```
Expected result:
- database: `qa_demo`
- tables: `accounts`, `tickets`
- user `dbtool` retains privileges on `qa_demo`
### 4. Seed SQLite
```bash
./examples/scripts/bootstrap-sqlite.sh
```
Expected result:
- file: `examples/tmp/dbtool-demo.sqlite`
- tables: `accounts`, `tickets`
## Smoke Inputs
### PostgreSQL
- connect target: host `127.0.0.1`, port `55432`, database `dbtool_demo`, user `dbtool`
- inspect scope: schema `qa_demo`
- happy query: `examples/sql/postgres/happy_path_query.sql`
- export query: `examples/sql/postgres/export_query.sql`
- failure query: `examples/sql/postgres/failing_query.sql`
### MySQL
- connect target: host `127.0.0.1`, port `53306`, database `qa_demo`, user `dbtool`
- inspect scope: database `qa_demo`
- happy query: `examples/sql/mysql/happy_path_query.sql`
- export query: `examples/sql/mysql/export_query.sql`
- failure query: `examples/sql/mysql/failing_query.sql`
### SQLite
- connect target: `examples/tmp/dbtool-demo.sqlite`
- inspect scope: schema `main`
- happy query: `examples/sql/sqlite/happy_path_query.sql`
- export query: `examples/sql/sqlite/export_query.sql`
- failure query: `examples/sql/sqlite/failing_query.sql`
## Required Assertions
Use `examples/fixtures/EXPECTED_RESULTS.md` as the shared oracle.
- `connect` succeeds for each valid target
- `inspect` reveals `accounts` and `tickets`
- `query` returns `3` account rows in the happy-path query
- `export` returns `4` ticket rows in deterministic order
- failure query exits non-zero and identifies a missing object
- secrets do not appear in terminal errors
## Failure Paths To Capture
- PostgreSQL wrong password
- PostgreSQL unreachable port
- MySQL wrong password
- SQLite missing file
- invalid SQL from `failing_query.sql`
- export path collision once the CLI contract exists
## Cleanup
```bash
docker compose -f docker-compose.demo.yml down -v
rm -f examples/tmp/dbtool-demo.sqlite
```
## Blocker
This runbook cannot be marked passed until engineering lands:
- full runtime smoke on a Docker-capable host as described in `QA_RUNTIME_ENVIRONMENT.md`

74
TEST_STRATEGY.md Normal file
View File

@@ -0,0 +1,74 @@
# dbtool-cli-v1 测试策略(初版)
## 目标
- 为 CLI 第一版建立可重复、可说明、可交付的验收方式。
- 在代码落地前先建立 QA 基线,避免后期以“本地能跑”代替“满足验收”。
## 当前盘点结果
- 当前已观察到 Rust workspace、CLI 二进制、release artifact、GitHub release-smoke workflow、demo seed SQL、query fixture、bootstrap 脚本和 smoke runbook。
- 当前 agent 环境可直接执行本地 Linux + SQLite 的运行型验证。
- 当前 agent 环境有 `node v24.14.0` 与预编译 `target/release/dbtool`,因此 SQLite bootstrap 与本地二进制 smoke 可重复执行。
- 当前 agent 环境仍缺 `docker`,因此 PostgreSQL / MySQL 运行时 smoke 不能在此本地复现。
- 当前 agent 环境也缺 `cargo`,因此 Docker-capable host 若无 Rust toolchain需改用已构建二进制或 release artifact 执行 smoke。
- 当前 agent 环境也无法替代 GitHub 的 macOS / Windows runner 证据。
- 2026-03-26 已补做 SQLite non-db file 回归:`connect` 现在会在前置检查阶段失败,不再误报成功。
## 测试优先级
1. 工作区与可执行入口存在
2. 三种数据库的连接成功 / 失败路径
3. schema introspection
4. 查询执行
5. 导出流程
6. 跨平台差异与边界条件
## 测试层次
### 1. Smoke
- 验证 CLI 可启动、可显示帮助、可做最小连接检查。
- 必须能一条命令执行,且适合发布前快速复核。
### 2. 功能验收
- 按 PostgreSQL、MySQL、SQLite 分库执行 connect / inspect / query / export。
- happy path 与失败路径都要保留证据。
### 3. 回归
- 对已修缺陷建立回归步骤。
- 每次发布前至少回归:启动、连接、查询、导出、错误提示。
### 4. 边界条件
- 空数据库、空结果集、大结果集、非法路径、错误凭据、权限不足、Unicode、NULL、日期时间。
## 证据要求
- 每个通过项都需要命令、输入、输出摘要和环境信息。
- 每个失败项都需要复现步骤、预期行为、实际行为和严重级别。
- 未执行项必须明确标记原因,不能默认视为通过。
## 推荐交付资产
- 可复制的 README 或开发说明
- demo / seed 脚本
- smoke 脚本
- Docker 运行方式
- 跨数据库样例配置
- 缺陷复现模板
## 当前明显缺口
- 缺 PostgreSQL / MySQL 的本地 Docker 执行证据
- 缺 GitHub macOS / Windows release runner 的真实执行证据
- 缺大结果集 / 超时 / 空结果导出等剩余边界证据
## 下一次验证入口
- 在 Docker 可用环境执行 PostgreSQL / MySQL `connect` / `inspect` / `query` / `export` smoke若宿主无 `cargo`,直接使用 `target/release/dbtool` 或 release artifact。
- 继续补齐 SQLite missing file 与其余边界路径证据。
- 在 CI 或等价 runner 上收集 Linux / macOS / Windows release artifact 证据。
- 将新增结果持续回填到验收检查表和发布前清单。

21
apps/cli/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "dbtool-cli"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
[[bin]]
name = "dbtool"
path = "src/main.rs"
[dependencies]
db-config.workspace = true
db-core.workspace = true
db-drivers.workspace = true
[dev-dependencies]
rusqlite.workspace = true
[lints]
workspace = true

1069
apps/cli/src/main.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
# dbtool-cli-v1 第一轮工程 backlog
日期2026-03-25
## 已分配
- 后端:`CMP-4` Rust workspace 初始化
- 后端:`CMP-5` PostgreSQL 驱动
- 后端:`CMP-6` MySQL 驱动
- 后端:`CMP-7` SQLite 驱动
- 后端:`CMP-8` CSV / JSON 导出
- 后端:`CMP-12` CLI 打包、发布与跨平台 smoke 流水线
- 前端:`CMP-9` GUI 信息架构和设计基线
- QA`CMP-10` 跨数据库验收矩阵
- QA`CMP-13` demo 数据库、样例脚本和 smoke runbook
- PM`CMP-11` 里程碑、依赖图和交付跟踪
## 执行顺序
1. `CMP-4`
2. `CMP-5`
3. `CMP-10``CMP-13` 并行启动
4. `CMP-6`
5. `CMP-7`
6. `CMP-8`
7. `CMP-12`
8. `CMP-11` 持续跟踪里程碑和关键路径
## 当前判断
- 真正的起跑线不是“写功能”,而是先把仓库和最小 CLI 基线建起来。
- 前端当前应保持 gated只输出未来 GUI 基线,不抢占 CLI 主路径资源。
- QA 不应等功能全部完成后再介入,应从 fixtures、矩阵和 smoke runbook 开始提前进入。

View File

@@ -0,0 +1,13 @@
[package]
name = "db-config"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
[dependencies]
db-core.workspace = true
[lints]
workspace = true

123
crates/db-config/src/lib.rs Normal file
View File

@@ -0,0 +1,123 @@
use db_core::{ConnectionTarget, CoreError};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConnectionProfile {
pub name: String,
pub target: ConnectionTarget,
pub password_env_var: Option<String>,
}
impl ConnectionProfile {
pub fn new(
name: impl Into<String>,
target: ConnectionTarget,
password_env_var: Option<String>,
) -> Result<Self, CoreError> {
let profile = Self {
name: name.into(),
target,
password_env_var,
};
profile.validate()?;
Ok(profile)
}
pub fn validate(&self) -> Result<(), CoreError> {
if self.name.trim().is_empty() {
return Err(CoreError::InvalidConnection("profile name is empty"));
}
self.target.validate()
}
pub fn redacted_summary(&self) -> String {
match &self.password_env_var {
Some(password_env_var) => format!(
"{} via password env {}",
self.target.redacted_endpoint(),
password_env_var
),
None => self.target.redacted_endpoint(),
}
}
pub fn with_password(mut self, password: Option<String>) -> Self {
self.target.password = password;
self
}
}
#[cfg(test)]
mod tests {
use db_core::{ConnectionTarget, ConnectionTransport, DatabaseKind};
use super::*;
#[test]
fn rejects_empty_profile_name() {
let target = ConnectionTarget::new(
DatabaseKind::Sqlite,
ConnectionTransport::File {
path: "demo.sqlite".into(),
},
None,
None,
)
.expect("target should be valid");
let result = ConnectionProfile::new(" ", target, None);
assert_eq!(
result,
Err(CoreError::InvalidConnection("profile name is empty"))
);
}
#[test]
fn summary_mentions_password_env_without_secret() {
let target = ConnectionTarget::new(
DatabaseKind::Postgres,
ConnectionTransport::Tcp {
host: String::from("127.0.0.1"),
port: 55432,
},
Some(String::from("dbtool_demo")),
Some(String::from("dbtool")),
)
.expect("target should be valid");
let profile =
ConnectionProfile::new("local", target, Some(String::from("DBTOOL_PASSWORD")))
.expect("profile should be valid");
assert_eq!(
profile.redacted_summary(),
"postgres://dbtool@127.0.0.1:55432/dbtool_demo via password env DBTOOL_PASSWORD"
);
}
#[test]
fn injects_password_without_changing_redacted_output() {
let target = ConnectionTarget::new(
DatabaseKind::Postgres,
ConnectionTransport::Tcp {
host: String::from("127.0.0.1"),
port: 55432,
},
Some(String::from("dbtool_demo")),
Some(String::from("dbtool")),
)
.expect("target should be valid");
let profile =
ConnectionProfile::new("local", target, Some(String::from("DBTOOL_PASSWORD")))
.expect("profile should be valid")
.with_password(Some(String::from("secret")));
assert_eq!(profile.target.password.as_deref(), Some("secret"));
assert_eq!(
profile.redacted_summary(),
"postgres://dbtool@127.0.0.1:55432/dbtool_demo via password env DBTOOL_PASSWORD"
);
}
}

View File

@@ -0,0 +1,9 @@
[package]
name = "db-core"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
[lints]
workspace = true

384
crates/db-core/src/lib.rs Normal file
View File

@@ -0,0 +1,384 @@
use std::path::PathBuf;
use std::{error::Error, fmt};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DatabaseKind {
Postgres,
Mysql,
Sqlite,
}
impl DatabaseKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Postgres => "postgres",
Self::Mysql => "mysql",
Self::Sqlite => "sqlite",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ConnectionTransport {
Tcp { host: String, port: u16 },
File { path: PathBuf },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConnectionTarget {
pub kind: DatabaseKind,
pub transport: ConnectionTransport,
pub database: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
}
impl ConnectionTarget {
pub fn new(
kind: DatabaseKind,
transport: ConnectionTransport,
database: Option<String>,
username: Option<String>,
) -> Result<Self, CoreError> {
let target = Self {
kind,
transport,
database,
username,
password: None,
};
target.validate()?;
Ok(target)
}
pub fn validate(&self) -> Result<(), CoreError> {
match (&self.kind, &self.transport) {
(DatabaseKind::Sqlite, ConnectionTransport::File { path }) => {
if path.as_os_str().is_empty() {
return Err(CoreError::InvalidConnection("sqlite path is empty"));
}
}
(DatabaseKind::Sqlite, ConnectionTransport::Tcp { .. }) => {
return Err(CoreError::InvalidConnection(
"sqlite must use a filesystem path",
));
}
(_, ConnectionTransport::File { .. }) => {
return Err(CoreError::InvalidConnection(
"server databases must use host and port",
));
}
(_, ConnectionTransport::Tcp { host, port }) => {
if host.trim().is_empty() {
return Err(CoreError::InvalidConnection("host is empty"));
}
if *port == 0 {
return Err(CoreError::InvalidConnection("port must be non-zero"));
}
}
}
if self.kind != DatabaseKind::Sqlite
&& self
.database
.as_deref()
.is_none_or(|database| database.trim().is_empty())
{
return Err(CoreError::InvalidConnection(
"server databases require a database name",
));
}
Ok(())
}
pub fn redacted_endpoint(&self) -> String {
match &self.transport {
ConnectionTransport::Tcp { host, port } => {
let database = self.database.as_deref().unwrap_or("<none>");
match self.username.as_deref() {
Some(username) => {
format!(
"{}://{}@{}:{}/{}",
self.kind.as_str(),
username,
host,
port,
database
)
}
None => format!("{}://{}:{}/{}", self.kind.as_str(), host, port, database),
}
}
ConnectionTransport::File { path } => {
format!("sqlite://{}", path.display())
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InspectRequest {
pub schema: Option<String>,
pub table: Option<String>,
}
impl InspectRequest {
pub fn new(schema: Option<String>, table: Option<String>) -> Result<Self, CoreError> {
let request = Self { schema, table };
request.validate()?;
Ok(request)
}
pub fn validate(&self) -> Result<(), CoreError> {
if self.table.is_some() && self.schema.is_none() {
return Err(CoreError::InvalidInspect(
"table inspection requires --schema",
));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SchemaSummary {
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TableSummary {
pub schema: String,
pub name: String,
pub kind: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ColumnSummary {
pub name: String,
pub data_type: String,
pub nullable: bool,
pub primary_key: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InspectResult {
Schemas(Vec<SchemaSummary>),
Tables(Vec<TableSummary>),
Columns(Vec<ColumnSummary>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum QuerySource {
Inline,
File(PathBuf),
}
impl QuerySource {
pub fn label(&self) -> &'static str {
match self {
Self::Inline => "inline SQL",
Self::File(_) => "SQL file",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QueryRequest {
pub sql: String,
pub source: QuerySource,
pub parameters: Vec<String>,
}
impl QueryRequest {
pub fn new(
sql: String,
source: QuerySource,
parameters: Vec<String>,
) -> Result<Self, CoreError> {
let request = Self {
sql,
source,
parameters,
};
request.validate()?;
Ok(request)
}
pub fn validate(&self) -> Result<(), CoreError> {
if self.sql.trim().is_empty() {
return Err(CoreError::EmptyQuery);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Vec<String>>,
pub rows_affected: Option<u64>,
}
impl QueryResult {
pub fn rows(columns: Vec<String>, rows: Vec<Vec<String>>) -> Self {
Self {
columns,
rows,
rows_affected: None,
}
}
pub fn command(rows_affected: u64) -> Self {
Self {
columns: Vec::new(),
rows: Vec::new(),
rows_affected: Some(rows_affected),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExportFormat {
Csv,
Json,
}
impl ExportFormat {
pub fn file_extension(self) -> &'static str {
match self {
Self::Csv => "csv",
Self::Json => "json",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExportRequest {
pub format: ExportFormat,
pub output_path: PathBuf,
pub overwrite: bool,
}
impl ExportRequest {
pub fn new(
format: ExportFormat,
output_path: PathBuf,
overwrite: bool,
) -> Result<Self, CoreError> {
let request = Self {
format,
output_path,
overwrite,
};
request.validate()?;
Ok(request)
}
pub fn validate(&self) -> Result<(), CoreError> {
if self.output_path.file_name().is_none() {
return Err(CoreError::InvalidExport(
"output path must include a filename",
));
}
Ok(())
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum CoreError {
InvalidConnection(&'static str),
InvalidInspect(&'static str),
InvalidQuery(&'static str),
EmptyQuery,
InvalidExport(&'static str),
}
impl fmt::Display for CoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConnection(message) => {
write!(formatter, "connection target is invalid: {message}")
}
Self::InvalidInspect(message) => {
write!(formatter, "inspect request is invalid: {message}")
}
Self::InvalidQuery(message) => {
write!(formatter, "query request is invalid: {message}")
}
Self::EmptyQuery => write!(formatter, "query text is empty"),
Self::InvalidExport(message) => {
write!(formatter, "export request is invalid: {message}")
}
}
}
}
impl Error for CoreError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_server_target_without_database() {
let result = ConnectionTarget::new(
DatabaseKind::Postgres,
ConnectionTransport::Tcp {
host: String::from("127.0.0.1"),
port: 5432,
},
None,
Some(String::from("dbtool")),
);
assert_eq!(
result,
Err(CoreError::InvalidConnection(
"server databases require a database name"
))
);
}
#[test]
fn redacts_target_without_password() {
let target = ConnectionTarget::new(
DatabaseKind::Mysql,
ConnectionTransport::Tcp {
host: String::from("db.internal"),
port: 3306,
},
Some(String::from("qa_demo")),
Some(String::from("dbtool")),
)
.expect("target should be valid");
assert_eq!(
target.redacted_endpoint(),
"mysql://dbtool@db.internal:3306/qa_demo"
);
}
#[test]
fn rejects_empty_query() {
let result = QueryRequest::new(String::from(" "), QuerySource::Inline, Vec::new());
assert_eq!(result, Err(CoreError::EmptyQuery));
}
#[test]
fn rejects_table_without_schema() {
let result = InspectRequest::new(None, Some(String::from("accounts")));
assert_eq!(
result,
Err(CoreError::InvalidInspect(
"table inspection requires --schema"
))
);
}
}

View File

@@ -0,0 +1,15 @@
[package]
name = "db-drivers"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
[dependencies]
db-core.workspace = true
mysql.workspace = true
postgres.workspace = true
rusqlite.workspace = true
[lints]
workspace = true

1110
crates/db-drivers/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

30
docker-compose.demo.yml Normal file
View File

@@ -0,0 +1,30 @@
services:
postgres:
image: postgres:16
ports:
- "55432:5432"
environment:
POSTGRES_DB: dbtool_demo
POSTGRES_USER: dbtool
POSTGRES_PASSWORD: dbtool
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dbtool -d dbtool_demo"]
interval: 5s
timeout: 5s
retries: 20
mysql:
image: mysql:8.4
ports:
- "53306:3306"
environment:
MYSQL_DATABASE: dbtool_demo
MYSQL_USER: dbtool
MYSQL_PASSWORD: dbtool
MYSQL_ROOT_PASSWORD: dbtoolroot
command: ["mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -uroot -pdbtoolroot"]
interval: 5s
timeout: 5s
retries: 30

38
examples/README.md Normal file
View File

@@ -0,0 +1,38 @@
# dbtool-cli-v1 Demo Assets
These assets give backend, frontend, and QA one small shared dataset for PostgreSQL, MySQL, and SQLite.
## Current Status
- Demo fixtures, seed scripts, and smoke inputs are staged.
- PostgreSQL、MySQL 和 SQLite driver behavior now exists for `connect`, `inspect`, and `query`.
- `export` execution to CSV / JSON now exists behind the shared query-result model.
- Use these files as the shared data baseline for backend, frontend, and QA verification.
## Layout
- `examples/fixtures/` — seed SQL per database plus expected assertions
- `examples/sql/` — happy-path and failure-path query inputs
- `examples/scripts/` — repeatable bootstrap helpers
## Intended QA Coverage
- `connect` against PostgreSQL, MySQL, SQLite
- `inspect` schemas / tables / columns
- `query` from inline SQL or file input
- `export` to CSV / JSON from a known query
## MySQL Bootstrap Note
- `examples/fixtures/mysql/init.sql` 会显式授予 `dbtool` 用户对 `qa_demo` 的权限
- 这样可避免 demo 容器默认库名与 QA 目标库名不一致时,`dbtool` 无法访问 `qa_demo`
## SQLite Bootstrap Note
- `examples/scripts/bootstrap-sqlite.sh` 现支持 `sqlite3``node:sqlite``python3 sqlite3` 三种 seed 路径
- SQLite demo inspect 语义使用 `main` 作为 schema 名称
- SQLite demo query 目前按单条 statement 执行,多语句输入会明确失败
## Known Blocker
The seed data is ready, and PostgreSQL / MySQL / SQLite query and export flows are implemented, but these assets still cannot serve as release evidence until the full smoke path runs in a Docker-capable environment.

View File

@@ -0,0 +1,28 @@
# Expected Results
Use these assertions when verifying `connect`, `inspect`, `query`, and `export`.
## PostgreSQL
- schema: `qa_demo`
- tables: `accounts`, `tickets`
## MySQL
- database: `qa_demo`
- tables: `accounts`, `tickets`
## SQLite
- file: `examples/tmp/dbtool-demo.sqlite`
- tables: `accounts`, `tickets`
## Shared Data Assertions
- `accounts` row count: `3`
- `tickets` row count: `4`
- open tickets: `3`
- closed tickets: `1`
- at least one `NULL` value exists in `tickets.notes`
- at least one multilingual value exists in `accounts.display_name`
- export query returns `4` rows ordered by `ticket_id`

View File

@@ -0,0 +1,35 @@
DROP DATABASE IF EXISTS qa_demo;
CREATE DATABASE qa_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON qa_demo.* TO 'dbtool'@'%';
FLUSH PRIVILEGES;
USE qa_demo;
CREATE TABLE accounts (
id BIGINT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(255) NULL,
is_active BOOLEAN NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE TABLE tickets (
id BIGINT PRIMARY KEY,
account_id BIGINT NOT NULL,
title VARCHAR(255) NOT NULL,
status VARCHAR(32) NOT NULL,
notes TEXT NULL,
amount_cents INT NOT NULL,
created_at TIMESTAMP NOT NULL,
CONSTRAINT fk_tickets_account FOREIGN KEY (account_id) REFERENCES accounts(id)
);
INSERT INTO accounts (id, email, display_name, is_active, created_at) VALUES
(1, 'alice@example.com', 'Alice', TRUE, '2026-03-01 08:30:00'),
(2, 'maria@example.com', 'Máría', FALSE, '2026-03-10 09:45:00'),
(3, 'zhang@example.com', '张敏', TRUE, '2026-03-15 12:00:00');
INSERT INTO tickets (id, account_id, title, status, notes, amount_cents, created_at) VALUES
(101, 1, 'login timeout', 'open', NULL, 0, '2026-03-20 10:00:00'),
(102, 1, 'csv export mismatch', 'closed', 'fixed after re-run', 1500, '2026-03-21 11:15:00'),
(103, 2, '权限验证', 'open', 'needs DBA review', 300, '2026-03-22 15:30:00'),
(104, 3, 'emoji smoke 😀', 'open', NULL, 42, '2026-03-23 07:05:00');

View File

@@ -0,0 +1,32 @@
DROP SCHEMA IF EXISTS qa_demo CASCADE;
CREATE SCHEMA qa_demo;
SET search_path TO qa_demo;
CREATE TABLE accounts (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
display_name TEXT,
is_active BOOLEAN NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE tickets (
id BIGINT PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
title TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT,
amount_cents INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
INSERT INTO accounts (id, email, display_name, is_active, created_at) VALUES
(1, 'alice@example.com', 'Alice', TRUE, '2026-03-01T08:30:00Z'),
(2, 'maria@example.com', 'Máría', FALSE, '2026-03-10T09:45:00Z'),
(3, 'zhang@example.com', '张敏', TRUE, '2026-03-15T12:00:00Z');
INSERT INTO tickets (id, account_id, title, status, notes, amount_cents, created_at) VALUES
(101, 1, 'login timeout', 'open', NULL, 0, '2026-03-20T10:00:00Z'),
(102, 1, 'csv export mismatch', 'closed', 'fixed after re-run', 1500, '2026-03-21T11:15:00Z'),
(103, 2, '权限验证', 'open', 'needs DBA review', 300, '2026-03-22T15:30:00Z'),
(104, 3, 'emoji smoke 😀', 'open', NULL, 42, '2026-03-23T07:05:00Z');

View File

@@ -0,0 +1,34 @@
PRAGMA foreign_keys = OFF;
DROP TABLE IF EXISTS tickets;
DROP TABLE IF EXISTS accounts;
PRAGMA foreign_keys = ON;
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
display_name TEXT,
is_active INTEGER NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE tickets (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL,
title TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT,
amount_cents INTEGER NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts(id)
);
INSERT INTO accounts (id, email, display_name, is_active, created_at) VALUES
(1, 'alice@example.com', 'Alice', 1, '2026-03-01T08:30:00Z'),
(2, 'maria@example.com', 'Máría', 0, '2026-03-10T09:45:00Z'),
(3, 'zhang@example.com', '张敏', 1, '2026-03-15T12:00:00Z');
INSERT INTO tickets (id, account_id, title, status, notes, amount_cents, created_at) VALUES
(101, 1, 'login timeout', 'open', NULL, 0, '2026-03-20T10:00:00Z'),
(102, 1, 'csv export mismatch', 'closed', 'fixed after re-run', 1500, '2026-03-21T11:15:00Z'),
(103, 2, '权限验证', 'open', 'needs DBA review', 300, '2026-03-22T15:30:00Z'),
(104, 3, 'emoji smoke 😀', 'open', NULL, 42, '2026-03-23T07:05:00Z');

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
COMPOSE_FILE="${COMPOSE_FILE:-$ROOT_DIR/docker-compose.demo.yml}"
SERVICE_NAME="${MYSQL_SERVICE_NAME:-mysql}"
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-dbtoolroot}"
docker compose -f "$COMPOSE_FILE" exec -T "$SERVICE_NAME" \
mysql -uroot "-p$MYSQL_ROOT_PASSWORD" \
< "$ROOT_DIR/examples/fixtures/mysql/init.sql"
echo "MySQL demo data seeded into qa_demo."

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
COMPOSE_FILE="${COMPOSE_FILE:-$ROOT_DIR/docker-compose.demo.yml}"
SERVICE_NAME="${POSTGRES_SERVICE_NAME:-postgres}"
POSTGRES_USER="${POSTGRES_USER:-dbtool}"
POSTGRES_DB="${POSTGRES_DB:-dbtool_demo}"
docker compose -f "$COMPOSE_FILE" exec -T "$SERVICE_NAME" \
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
< "$ROOT_DIR/examples/fixtures/postgres/init.sql"
echo "PostgreSQL demo data seeded into $POSTGRES_DB."

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SQLITE_DB_PATH="${SQLITE_DB_PATH:-$ROOT_DIR/examples/tmp/dbtool-demo.sqlite}"
SQL_FILE="$ROOT_DIR/examples/fixtures/sqlite/init.sql"
mkdir -p "$(dirname "$SQLITE_DB_PATH")"
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$SQLITE_DB_PATH" < "$SQL_FILE"
elif command -v node >/dev/null 2>&1; then
node --no-warnings - "$SQLITE_DB_PATH" "$SQL_FILE" <<'JS'
const { DatabaseSync } = require('node:sqlite');
const fs = require('fs');
const dbPath = process.argv[2];
const sqlPath = process.argv[3];
const db = new DatabaseSync(dbPath);
try {
db.exec(fs.readFileSync(sqlPath, 'utf8'));
} finally {
db.close();
}
JS
else
python3 - "$SQLITE_DB_PATH" "$SQL_FILE" <<'PY'
import pathlib
import sqlite3
import sys
db_path = pathlib.Path(sys.argv[1])
sql_path = pathlib.Path(sys.argv[2])
conn = sqlite3.connect(db_path)
try:
conn.executescript(sql_path.read_text(encoding="utf-8"))
conn.commit()
finally:
conn.close()
PY
fi
echo "SQLite demo data seeded into $SQLITE_DB_PATH."

View File

@@ -0,0 +1,12 @@
USE qa_demo;
SELECT
t.id AS ticket_id,
a.email,
t.title,
t.status,
t.amount_cents,
t.created_at
FROM tickets t
JOIN accounts a ON a.id = t.account_id
ORDER BY t.id;

View File

@@ -0,0 +1,3 @@
USE qa_demo;
SELECT * FROM missing_table;

View File

@@ -0,0 +1,11 @@
USE qa_demo;
SELECT
a.id AS account_id,
a.display_name,
COUNT(t.id) AS ticket_count,
SUM(CASE WHEN t.status = 'open' THEN 1 ELSE 0 END) AS open_ticket_count
FROM accounts a
LEFT JOIN tickets t ON t.account_id = a.id
GROUP BY a.id, a.display_name
ORDER BY a.id;

View File

@@ -0,0 +1,12 @@
SET search_path TO qa_demo;
SELECT
t.id AS ticket_id,
a.email,
t.title,
t.status,
t.amount_cents,
t.created_at
FROM tickets t
JOIN accounts a ON a.id = t.account_id
ORDER BY t.id;

View File

@@ -0,0 +1,3 @@
SET search_path TO qa_demo;
SELECT * FROM missing_table;

View File

@@ -0,0 +1,11 @@
SET search_path TO qa_demo;
SELECT
a.id AS account_id,
a.display_name,
COUNT(t.id) AS ticket_count,
SUM(CASE WHEN t.status = 'open' THEN 1 ELSE 0 END) AS open_ticket_count
FROM accounts a
LEFT JOIN tickets t ON t.account_id = a.id
GROUP BY a.id, a.display_name
ORDER BY a.id;

View File

@@ -0,0 +1,10 @@
SELECT
t.id AS ticket_id,
a.email,
t.title,
t.status,
t.amount_cents,
t.created_at
FROM tickets t
JOIN accounts a ON a.id = t.account_id
ORDER BY t.id;

View File

@@ -0,0 +1 @@
SELECT * FROM missing_table;

View File

@@ -0,0 +1,9 @@
SELECT
a.id AS account_id,
a.display_name,
COUNT(t.id) AS ticket_count,
SUM(CASE WHEN t.status = 'open' THEN 1 ELSE 0 END) AS open_ticket_count
FROM accounts a
LEFT JOIN tickets t ON t.account_id = a.id
GROUP BY a.id, a.display_name
ORDER BY a.id;

40
gui/README.md Normal file
View File

@@ -0,0 +1,40 @@
# GUI Foundation Artifacts
本目录用于存放 `CMP-9` 的未来桌面端 GUI 基线产物,而不是正式应用代码。
## 当前产物
- `desktop-foundation.md`:信息架构、布局方向、组件清单、契约要求和 UI/UX 建议
- `prototype/index.html`:最小可见工作台原型
- `prototype/styles.css`:原型样式
- `prototype/app.js`:原型内交互
- `preview-server.mjs`:零依赖本地预览服务
## 查看方式
### 方式一:直接打开
直接用浏览器打开 `gui/prototype/index.html`
### 方式二:启动本地预览
在项目根目录执行:
```bash
node gui/preview-server.mjs
```
然后访问:
```text
http://127.0.0.1:4173
```
## 界面验收
验收人应重点确认:
1. 是否能看出连接管理、schema browser、query editor、results table、export feedback 的稳定分区
2. 布局是否体现“工作台”而不是通用 dashboard
3. 状态、错误、执行反馈是否一眼可见
4. 文档中的后端契约需求是否能映射到 CLI 已定义产品概念

291
gui/desktop-foundation.md Normal file
View File

@@ -0,0 +1,291 @@
# dbtool-cli-v1 Future Desktop GUI Foundation
日期2026-03-25
作者Senior Frontend Engineer
对应 issue`CMP-9`
## 1. 当前前端基线评估
### 真实现状
- 当前仓库没有前端入口,也没有任何展示层基础。
- 当前不存在 `package.json`、React 工程、桌面壳、页面路由、组件目录或样式系统。
- 当前项目仍以 CLI V1 为主路径GUI 只允许做未来方向基线,不进入本期交付主链路。
### 结论
- GUI 第一轮最合理的交付形态应是“信息架构 + 组件边界 + 契约清单 + 静态原型”。
- 不应在本 issue 中直接引入 Electron、Tauri、React 或完整桌面工程。
- 后续 GUI 项目启动时,推荐使用 React + shadcn/ui + Tailwind CSS并以本文件作为产品层输入。
## 2. 首版 UI/UX 范围建议
首版未来桌面端 GUI 建议仅覆盖一个高价值工作台,而不是多页面堆叠:
1. **Connection Manager**
- 展示已保存连接、最近连接、驱动类型与连接健康状态
- 支持新建连接、测试连接、进入工作台
2. **Database Workspace**
- 左侧 schema browser
- 中间 query editor + execution toolbar
- 下方 results / history / export 分页区
- 右侧 inspector 展示连接、对象和执行详情
3. **Execution Feedback**
- 展示执行中、成功、失败、空结果、已导出等状态
4. **Export Flow**
- 选择格式、目标路径、覆盖提醒、导出结果提示
### 当前刻意不做
- dashboard 首页
- 图表和 BI 视图
- AI 助手面板
- migration / schema editing
- 多窗口复杂工作流
## 3. 信息架构
```text
Desktop GUI
├─ Connection Manager
│ ├─ Saved Connections
│ ├─ New Connection
│ └─ Test / Open Workspace
└─ Workspace
├─ Schema Browser
│ ├─ Schemas
│ ├─ Tables / Views
│ └─ Columns
├─ Query Workbench
│ ├─ Query Tabs
│ ├─ Execution Toolbar
│ └─ SQL Editor
├─ Result Region
│ ├─ Results Table
│ ├─ Query History
│ └─ Export Status
└─ Inspector
├─ Connection Summary
├─ Object Metadata
└─ Execution Detail / Error
```
## 4. 主工作区布局方向
### Layout Principle
- 使用“三栏 + 底部结果区”的桌面工作台布局。
- 结构重于装饰,让 schema、SQL、结果和状态成为主视觉主体。
- 避免通用 SaaS 卡片拼贴,不引入噪音型 dashboard 模块。
### 推荐布局
```text
┌────────────────────────────────────────────────────────────────────┐
│ App Header / Connection Switcher / Global Actions │
├──────────────┬─────────────────────────────────────┬───────────────┤
│ Schema Tree │ Query Toolbar + SQL Editor │ Inspector │
│ │ │ │
│ │ │ │
├──────────────┴─────────────────────────────────────┴───────────────┤
│ Results / History / Export / Problems │
└────────────────────────────────────────────────────────────────────┘
```
### 区域职责
- **Header**:连接切换、当前数据库上下文、主题、全局命令
- **Schema Tree**:浏览 schema / table / view / column支持搜索和展开
- **Workbench**SQL 编写、运行、停止、格式切换、目标连接展示
- **Inspector**:对象详情、执行摘要、错误详情、导出选项
- **Bottom Panel**:结果表格、历史、导出日志、问题列表
## 5. 页面与状态设计
### Connection Manager
- 主要目标:尽快进入一个明确的工作区
- 关键状态:
- 无连接
- 测试连接中
- 测试连接成功
- 测试连接失败
- 驱动不支持 / 配置不完整
### Workspace
- 主要目标:围绕单一连接完成 inspect → query → export
- 关键状态:
- 尚未选择对象
- schema 载入中
- SQL 执行中
- 查询成功且有结果
- 查询成功但空结果
- 查询失败
- 导出成功 / 导出失败
## 6. 组件清单
以下为后续 React + shadcn/ui 落地建议组件边界:
| 组件 | 作用 | 建议基础 |
| --- | --- | --- |
| `AppShell` | 承载桌面工作台骨架 | `ResizablePanelGroup`, `Separator` |
| `ConnectionSwitcher` | 切换当前连接和工作区入口 | `Popover`, `Command`, `Select` |
| `ConnectionCard` | 展示连接摘要与状态 | `Card`, `Badge`, `Button` |
| `SchemaTree` | 浏览 schema / table / column | `ScrollArea`, `Collapsible`, `Input` |
| `QueryToolbar` | 运行、停止、导出、当前连接状态 | `Button`, `Badge`, `Tooltip` |
| `QueryTabs` | 管理多个 SQL 标签 | `Tabs`, `ContextMenu` |
| `SqlEditorPanel` | 容纳 SQL 编辑器 | 自定义容器,后续接入编辑器 |
| `ResultTabs` | 在结果、历史、导出间切换 | `Tabs` |
| `ResultsTable` | 呈现查询结果集 | `Table`, `ScrollArea` |
| `ExecutionBanner` | 一眼可见的执行状态与错误摘要 | `Alert`, `Badge` |
| `InspectorPanel` | 展示对象属性和执行详情 | `Sheet` 或固定侧栏容器 |
| `ExportDrawer` | 处理导出参数与确认 | `Dialog` / `Sheet` / `Form` |
## 7. 与 shadcn/ui + Tailwind + impeccable 的对齐说明
### 视觉基线
- 以稳定中性色为主,不使用高饱和主视觉。
- 强调结构、对齐、排版和间距,而不是装饰性卡片。
- light / dark 都应保持低噪音层次和稳定对比度。
### 交互基线
- 键盘优先连接切换、schema 搜索、执行操作、底部面板切换应支持快捷路径。
- 错误优先可读:错误摘要直接进入可见区,不藏在 toast 里。
- 信息密度优先:结果、状态和元数据必须同时可读,避免只做“大留白”。
### 不应出现的方向
- 营销站式英雄区
- 强发光深色主题
- 以卡片拼贴为主的 dashboard 首页
- 视觉噪音很高的渐变、阴影、发光描边
## 8. 未来 GUI 需要的核心数据契约
桌面 GUI 不应重新定义产品对象,而应直接映射 CLI 已明确概念。
### 连接与上下文
- `ConnectionTarget`
- `id`
- `name`
- `databaseKind`
- `environmentLabel`
- `status`
- `lastValidatedAt`
### schema browser
- `CatalogNode`
- `id`
- `kind` (`schema` / `table` / `view` / `column`)
- `name`
- `parentId`
- `isExpandable`
- `metadata`
### 查询执行
- `QueryRequest`
- `connectionId`
- `sql`
- `source` (`inline` / `file`)
- `limitMode`
- `QueryExecutionState`
- `status` (`idle` / `running` / `success` / `error`)
- `startedAt`
- `finishedAt`
- `durationMs`
- `rowCount`
- `message`
### 结果与导出
- `ResultSet`
- `columns`
- `rows`
- `rowCount`
- `truncated`
- `ExportRequest`
- `connectionId`
- `queryText`
- `format` (`csv` / `json`)
- `outputPath`
- `overwrite`
- `ExportResult`
- `status`
- `outputPath`
- `exportedRows`
- `message`
## 9. 后端契约提前识别
未来 GUI 项目启动前,建议 CTO / Backend 先固定以下输出形态:
1. schema introspection 返回结构是否可直接树化
2. query 返回结构是否区分“空结果成功”和“执行失败”
3. export 是否返回可复用的结果对象和错误对象
4. 连接测试结果是否提供统一状态码和展示文案
5. 错误对象是否包含用户可展示摘要与底层细节
若这些结构过度 CLI-only后续 GUI 会被迫做适配层补丁。
## 10. 最小可见界面实现
本次已提供静态原型:
- `gui/prototype/index.html`
- `gui/prototype/styles.css`
- `gui/prototype/app.js`
该原型的作用是验证:
- 工作台结构是否稳定
- 连接管理与执行反馈是否易懂
- schema browser、query editor、results table、inspector 是否形成合理分区
该原型不是正式桌面应用,也不接真实数据源。
## 11. 运行方式
### 直接打开
浏览器打开 `gui/prototype/index.html`
### 本地预览
```bash
node gui/preview-server.mjs
```
访问 `http://127.0.0.1:4173`
## 12. 界面验收说明
QA / CTO / PM 可按以下标准验收本次产物:
1. 明确确认当前项目“尚无前端基线”这一事实是否被准确记录
2. 评估信息架构是否完整覆盖 connection manager、schema browser、query editor、results table、export flow
3. 评估布局是否体现高信息密度桌面工作台,而非通用后台模板
4. 评估组件清单是否足够拆分后续 GUI 子任务
5. 评估契约清单是否能指导 backend 提前稳定输出结构
## 13. 建议的后续 GUI 项目拆分
后续若单独立项 GUI可按以下顺序拆 issue
1. GUI shell 与设计 token 基线
2. Connection Manager
3. Schema Browser
4. Query Workbench
5. Results Table 与状态系统
6. Export Flow
7. 错误与问题面板
8. 快捷键与可用性细化

33
gui/preview-server.mjs Normal file
View File

@@ -0,0 +1,33 @@
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
const root = new URL("./prototype/", import.meta.url);
const port = Number(process.env.GUI_PREVIEW_PORT || 4173);
const contentTypes = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
};
const server = createServer(async (req, res) => {
const requestPath = req.url === "/" ? "/index.html" : req.url || "/index.html";
const safePath = normalize(requestPath).replace(/^(\.\.[/\\])+/, "");
const filePath = new URL(`.${safePath}`, root);
try {
const body = await readFile(filePath);
const type = contentTypes[extname(filePath.pathname)] || "text/plain; charset=utf-8";
res.writeHead(200, { "Content-Type": type });
res.end(body);
} catch {
const fallback = await readFile(join(root.pathname, "index.html"));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(fallback);
}
});
server.listen(port, "127.0.0.1", () => {
console.log(`GUI prototype server running at http://127.0.0.1:${port}`);
});

53
gui/prototype/app.js vendored Normal file
View File

@@ -0,0 +1,53 @@
const connectionCards = document.querySelectorAll(".connection-card");
const activeConnection = document.getElementById("active-connection");
const detailTarget = document.getElementById("detail-target");
const runQueryButton = document.getElementById("run-query");
const executionBanner = document.getElementById("execution-banner");
const executionTitle = document.getElementById("execution-title");
const executionCopy = document.getElementById("execution-copy");
const durationValue = document.getElementById("duration-value");
const rowCount = document.getElementById("row-count");
const bottomTabs = document.querySelectorAll(".bottom-tab");
const tabContents = document.querySelectorAll(".tab-content");
connectionCards.forEach((card) => {
card.addEventListener("click", () => {
connectionCards.forEach((item) => item.classList.remove("is-active"));
card.classList.add("is-active");
const target = card.dataset.connection;
activeConnection.textContent = target;
detailTarget.textContent = target;
});
});
runQueryButton.addEventListener("click", () => {
const isError = executionBanner.classList.contains("is-error");
if (isError) {
executionBanner.classList.remove("is-error");
executionTitle.textContent = "Last run succeeded";
executionCopy.textContent = "50 rows returned in 182 ms. Export is available.";
durationValue.textContent = "182 ms";
rowCount.textContent = "50";
return;
}
executionBanner.classList.add("is-error");
executionTitle.textContent = "Last run failed";
executionCopy.textContent = "Syntax error near `form`. The error summary should stay visible in the workspace.";
durationValue.textContent = "21 ms";
rowCount.textContent = "0";
});
bottomTabs.forEach((tab) => {
tab.addEventListener("click", () => {
bottomTabs.forEach((item) => item.classList.remove("is-active"));
tabContents.forEach((item) => item.classList.remove("is-active"));
tab.classList.add("is-active");
document
.querySelector(`[data-content="${tab.dataset.tab}"]`)
.classList.add("is-active");
});
});

293
gui/prototype/index.html Normal file
View File

@@ -0,0 +1,293 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dbtool-cli-v1 GUI Foundation Prototype</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="app-shell">
<header class="topbar">
<div class="topbar__brand">
<div class="brand-mark">DB</div>
<div>
<p class="eyebrow">dbtool future desktop</p>
<h1>Query Workspace Foundation</h1>
</div>
</div>
<div class="topbar__context">
<button class="ghost-button">⌘K Commands</button>
<button class="ghost-button">Theme</button>
<div class="connection-pill">
<span class="status-dot status-dot--ok"></span>
<span id="active-connection">pg-prod-readonly</span>
</div>
</div>
</header>
<main class="workspace">
<aside class="panel panel--nav">
<section class="panel-section">
<div class="section-head">
<div>
<p class="eyebrow">Connection Manager</p>
<h2>Saved Targets</h2>
</div>
<button class="ghost-button ghost-button--small">+ New</button>
</div>
<div class="connection-list">
<button class="connection-card is-active" data-connection="pg-prod-readonly">
<div>
<strong>pg-prod-readonly</strong>
<p>PostgreSQL · Production</p>
</div>
<span class="pill pill--success">Healthy</span>
</button>
<button class="connection-card" data-connection="mysql-staging">
<div>
<strong>mysql-staging</strong>
<p>MySQL · Staging</p>
</div>
<span class="pill pill--muted">Idle</span>
</button>
<button class="connection-card" data-connection="sqlite-local">
<div>
<strong>sqlite-local</strong>
<p>SQLite · Local file</p>
</div>
<span class="pill pill--muted">Ready</span>
</button>
</div>
</section>
<section class="panel-section">
<div class="section-head">
<div>
<p class="eyebrow">Schema Browser</p>
<h2>Catalog</h2>
</div>
<button class="ghost-button ghost-button--small">Filter</button>
</div>
<div class="search-field">Search tables, views, columns</div>
<div class="tree">
<details open>
<summary>public</summary>
<details open>
<summary>orders</summary>
<ul>
<li>id · uuid</li>
<li>customer_id · uuid</li>
<li>status · text</li>
<li>created_at · timestamptz</li>
</ul>
</details>
<details>
<summary>customers</summary>
<ul>
<li>id · uuid</li>
<li>email · text</li>
<li>country · text</li>
</ul>
</details>
</details>
<details>
<summary>analytics</summary>
<ul>
<li>daily_orders</li>
<li>top_customers</li>
</ul>
</details>
</div>
</section>
</aside>
<section class="panel panel--main">
<div class="editor-toolbar">
<div class="toolbar-tabs">
<button class="toolbar-tab is-active">orders audit.sql</button>
<button class="toolbar-tab">empty result.sql</button>
</div>
<div class="toolbar-actions">
<span class="pill pill--muted">PostgreSQL</span>
<button class="action-button action-button--primary" id="run-query">Run Query</button>
<button class="action-button">Export</button>
</div>
</div>
<section class="editor">
<div class="editor-gutter">
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
</div>
<pre class="editor-code"><code>select
id,
customer_id,
status,
created_at
from public.orders
where created_at &gt; now() - interval '7 days'
order by created_at desc
limit 50;</code></pre>
</section>
<section class="execution-banner" id="execution-banner">
<div>
<p class="eyebrow">Execution</p>
<strong id="execution-title">Last run succeeded</strong>
<p id="execution-copy">50 rows returned in 182 ms. Export is available.</p>
</div>
<div class="banner-metrics">
<div>
<span>Duration</span>
<strong id="duration-value">182 ms</strong>
</div>
<div>
<span>Rows</span>
<strong id="row-count">50</strong>
</div>
</div>
</section>
<section class="bottom-panel">
<div class="bottom-tabs">
<button class="bottom-tab is-active" data-tab="results">Results</button>
<button class="bottom-tab" data-tab="history">History</button>
<button class="bottom-tab" data-tab="export">Export</button>
<button class="bottom-tab" data-tab="problems">Problems</button>
</div>
<div class="tab-content is-active" data-content="results">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>id</th>
<th>customer_id</th>
<th>status</th>
<th>created_at</th>
</tr>
</thead>
<tbody>
<tr>
<td>8ec1...</td>
<td>7cb3...</td>
<td>paid</td>
<td>2026-03-25 13:55:01 UTC</td>
</tr>
<tr>
<td>80f8...</td>
<td>5d10...</td>
<td>processing</td>
<td>2026-03-25 13:44:15 UTC</td>
</tr>
<tr>
<td>5aa2...</td>
<td>9ab1...</td>
<td>failed</td>
<td>2026-03-25 13:12:48 UTC</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="tab-content" data-content="history">
<div class="log-list">
<div class="log-row">
<strong>orders audit.sql</strong>
<span>Success · 182 ms · 50 rows</span>
</div>
<div class="log-row">
<strong>empty result.sql</strong>
<span>Success · 95 ms · 0 rows</span>
</div>
<div class="log-row">
<strong>bad syntax.sql</strong>
<span>Error · syntax error near `form`</span>
</div>
</div>
</div>
<div class="tab-content" data-content="export">
<div class="export-box">
<div>
<p class="eyebrow">Export Status</p>
<strong>Ready to export the current result set</strong>
<p>`/tmp/orders-last-7-days.csv` is available as the default target.</p>
</div>
<div class="export-actions">
<button class="action-button">CSV</button>
<button class="action-button">JSON</button>
</div>
</div>
</div>
<div class="tab-content" data-content="problems">
<div class="problem-box">
<strong>No blocking problem</strong>
<p>When a query fails, this area should show the error summary, SQL fragment, and next action.</p>
</div>
</div>
</section>
</section>
<aside class="panel panel--side">
<section class="panel-section">
<p class="eyebrow">Inspector</p>
<h2>Current Selection</h2>
<div class="info-card">
<span>Object</span>
<strong>public.orders</strong>
</div>
<div class="info-card">
<span>Primary Key</span>
<strong>id</strong>
</div>
<div class="info-card">
<span>Columns</span>
<strong>12</strong>
</div>
</section>
<section class="panel-section">
<p class="eyebrow">Execution Detail</p>
<div class="detail-list">
<div>
<span>Target</span>
<strong id="detail-target">pg-prod-readonly</strong>
</div>
<div>
<span>Mode</span>
<strong>Read-only</strong>
</div>
<div>
<span>Last export</span>
<strong>Not started</strong>
</div>
</div>
</section>
<section class="panel-section">
<p class="eyebrow">Acceptance Focus</p>
<ul class="focus-list">
<li>Schema tree remains scannable on wide catalogs</li>
<li>Execution state is visible without reading the table</li>
<li>Result region supports empty, success, and error states</li>
<li>Export action stays adjacent to query outcome</li>
</ul>
</section>
</aside>
</main>
</div>
<script src="./app.js"></script>
</body>
</html>

423
gui/prototype/styles.css Normal file
View File

@@ -0,0 +1,423 @@
:root {
color-scheme: dark;
--bg: #0f1115;
--panel: #141821;
--panel-muted: #10141c;
--panel-strong: #1a202b;
--border: #273041;
--text: #e6e8ee;
--muted: #9da7b8;
--accent: #7dd3fc;
--accent-soft: rgba(125, 211, 252, 0.12);
--success: #7dd3a0;
--warning: #f5c26b;
--danger: #f09c9c;
--shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
--radius: 14px;
--radius-sm: 10px;
--font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--mono: "SFMono-Regular", ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top, rgba(125, 211, 252, 0.07), transparent 25%),
linear-gradient(180deg, #0d1016 0%, #0f1115 100%);
color: var(--text);
font-family: var(--font);
}
button,
input,
textarea,
select {
font: inherit;
}
.app-shell {
min-height: 100vh;
padding: 18px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 22px;
margin-bottom: 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: rgba(20, 24, 33, 0.82);
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
}
.topbar__brand,
.topbar__context,
.section-head,
.editor-toolbar,
.toolbar-tabs,
.toolbar-actions,
.bottom-tabs,
.export-box,
.export-actions {
display: flex;
align-items: center;
gap: 12px;
}
.topbar__brand h1,
.section-head h2,
.panel-section h2 {
margin: 0;
font-size: 16px;
}
.brand-mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border: 1px solid rgba(125, 211, 252, 0.3);
border-radius: 12px;
background: var(--accent-soft);
color: var(--accent);
font-weight: 700;
}
.eyebrow {
margin: 0 0 4px;
color: var(--muted);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.workspace {
display: grid;
grid-template-columns: 280px minmax(640px, 1fr) 280px;
gap: 16px;
min-height: calc(100vh - 124px);
}
.panel {
border: 1px solid var(--border);
border-radius: var(--radius);
background: rgba(20, 24, 33, 0.9);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel--nav,
.panel--side {
display: flex;
flex-direction: column;
}
.panel--main {
display: grid;
grid-template-rows: auto minmax(240px, 1fr) auto minmax(260px, 0.9fr);
}
.panel-section,
.editor-toolbar,
.execution-banner,
.bottom-panel {
padding: 18px;
}
.panel-section + .panel-section {
border-top: 1px solid var(--border);
}
.connection-pill,
.pill,
.ghost-button,
.action-button,
.toolbar-tab,
.bottom-tab,
.search-field,
.info-card,
.log-row,
.problem-box {
border: 1px solid var(--border);
border-radius: 999px;
}
.ghost-button,
.action-button,
.toolbar-tab,
.bottom-tab {
cursor: pointer;
}
.ghost-button,
.toolbar-tab,
.bottom-tab,
.search-field {
padding: 8px 12px;
background: var(--panel-muted);
color: var(--muted);
}
.ghost-button--small {
padding: 6px 10px;
}
.action-button {
padding: 9px 13px;
background: var(--panel-strong);
color: var(--text);
}
.action-button--primary {
border-color: rgba(125, 211, 252, 0.28);
background: rgba(125, 211, 252, 0.16);
color: #dff6ff;
}
.connection-pill,
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 11px;
background: var(--panel-muted);
}
.pill--success {
color: var(--success);
}
.pill--muted {
color: var(--muted);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--muted);
}
.status-dot--ok {
background: var(--success);
}
.connection-list,
.tree,
.detail-list,
.focus-list,
.log-list {
display: grid;
gap: 10px;
}
.connection-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 14px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--panel-muted);
color: inherit;
text-align: left;
}
.connection-card p,
.export-box p,
.problem-box p,
.focus-list,
.detail-list span,
.info-card span,
.log-row span {
margin: 4px 0 0;
color: var(--muted);
}
.connection-card.is-active {
border-color: rgba(125, 211, 252, 0.4);
background: rgba(125, 211, 252, 0.08);
}
.tree details {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--panel-muted);
}
.tree summary {
cursor: pointer;
font-weight: 600;
}
.tree ul {
margin: 10px 0 0 18px;
padding: 0;
color: var(--muted);
}
.editor {
display: grid;
grid-template-columns: 52px 1fr;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
background: #10141b;
}
.editor-gutter,
.editor-code {
padding: 18px 16px;
margin: 0;
font-family: var(--mono);
font-size: 13px;
line-height: 1.7;
}
.editor-gutter {
color: #697386;
border-right: 1px solid var(--border);
background: #0d1016;
}
.editor-gutter span {
display: block;
}
.editor-code {
color: #d7e2f0;
overflow: auto;
}
.execution-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
background: linear-gradient(90deg, rgba(125, 211, 160, 0.12), rgba(125, 211, 252, 0.04));
}
.execution-banner.is-error {
background: linear-gradient(90deg, rgba(240, 156, 156, 0.16), rgba(240, 156, 156, 0.06));
}
.banner-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(88px, 1fr));
gap: 12px;
}
.banner-metrics div,
.detail-list div,
.info-card {
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: rgba(15, 18, 24, 0.7);
}
.banner-metrics span,
.detail-list span,
.info-card span {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 12px;
}
.bottom-panel {
display: grid;
grid-template-rows: auto 1fr;
gap: 16px;
}
.toolbar-tab.is-active,
.bottom-tab.is-active {
border-color: rgba(125, 211, 252, 0.3);
background: rgba(125, 211, 252, 0.14);
color: #e4f7ff;
}
.tab-content {
display: none;
}
.tab-content.is-active {
display: block;
}
.table-wrap {
overflow: auto;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th,
td {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
text-align: left;
white-space: nowrap;
}
th {
color: var(--muted);
font-weight: 600;
background: #121722;
}
.log-row,
.export-box,
.problem-box {
padding: 16px;
background: var(--panel-muted);
}
.focus-list {
padding-left: 18px;
}
.focus-list li + li {
margin-top: 10px;
}
@media (max-width: 1320px) {
.workspace {
grid-template-columns: 250px minmax(520px, 1fr) 250px;
}
}
@media (max-width: 1100px) {
.workspace {
grid-template-columns: 1fr;
}
.panel--main {
order: 1;
}
.panel--nav {
order: 2;
}
.panel--side {
order: 3;
}
}

View File

@@ -0,0 +1,100 @@
# dbtool-cli-v1 CLI 发布与跨平台 smoke 基线
> Status: superseded on 2026-03-26 by `RELEASE_RUNBOOK.md` and `.github/workflows/release-smoke.yml`.
> This file captures the initial blocked-state plan and should not be treated as the current release implementation source of truth.
日期2026-03-25
作者Senior Backend Engineer
## 当前结论
- 当前仓库仅包含产品、QA、backlog 与架构计划文档,尚未落地 Rust workspace、`Cargo.toml`、CLI 可执行入口或任何可构建资产。
- 因此 `CMP-12` 当前不能直接实现“真实可运行”的发布流水线,必须先等待 `CMP-4` 交付最小可编译 CLI 基线。
- 同时,跨平台 smoke 的“有意义输入”还依赖 `CMP-13` 提供最小 fixtures / runbook否则只能做 `--help` 级别的空载校验。
## 当前领域与后端边界盘点
当前已共享、但尚未代码化的后端核心概念来自 `PRODUCT_REQUIREMENTS.md``plans/2026-03-25-dbtool-cli-v1-rust-architecture-plan.md`
- 连接目标:`DatabaseKind` + `ConnectionProfile`
- introspection 结果schema / table / column 元数据树
- 查询执行:`QueryRequest` / `QueryResult`
- 导出:`ExportRequest`
- 发布链路:`ReleaseArtifact` / checksum / smoke result
这些概念目前仍处于“规划已明确、实现尚未开始”的状态。
## 最小可行发布策略
`CMP-4` 完成后,发布链路先采用最小 CI matrix而不是复杂发布系统
### CI matrix
- `ubuntu-latest`
- `macos-latest`
- `windows-latest`
### 每个平台必须执行
1. 安装稳定 Rust toolchain
2. `cargo build --release`
3. 执行一次 CLI 基础 smoke
- `<binary> --help`
- 可选:`<binary> --version`
4. 打包 release artifact
5. 生成 SHA256 checksum
## 建议的 artifact 规则
在 CLI 名称最终确定前,先使用占位命名规则,避免后续分发混乱:
- Linux: `<cli-name>-<version>-x86_64-unknown-linux-gnu.tar.gz`
- macOS: `<cli-name>-<version>-x86_64-apple-darwin.tar.gz`
- Windows: `<cli-name>-<version>-x86_64-pc-windows-msvc.zip`
- checksum: `<artifact-name>.sha256`
如后续引入 ARM 构建,新增架构后缀,不复用旧命名。
## smoke 边界
当前阶段的 smoke 分两层:
### Level 1发布前置 smoke
- CLI 可启动
- `--help` 可返回 0
- `--version` 可读取(若已实现)
### Level 2功能 smoke
依赖 `CMP-13` 的最小 fixtures / runbook至少覆盖
- connect
- inspect
- query
- export
`CMP-13` 未完成前,不应把 Level 2 写成强制发布门槛。
## 回滚说明
V1 不涉及服务部署,回滚按“产物回滚”处理:
1. 保留上一个已验证 release artifact 与 checksum
2. 若新版本 smoke 失败或 QA 验收失败,停止分发新产物
3. 重新指向上一个可验证版本
4. 在发布记录中标注失败原因、影响平台、是否为构建问题或运行时问题
## 当前阻塞与依赖
- 主阻塞:`CMP-4` 未完成,当前不存在可编译 Rust workspace
- 次阻塞:`CMP-13` 尚在进行中,功能 smoke 输入未稳定
- 风险:若在 CLI 参数与二进制名未稳定前过早固化 workflow后续会出现大量仅因命名变化产生的流水线噪音
## 建议的后续执行顺序
1. `CMP-4` 先交付可编译 workspace 与可启动 CLI
2. 在仓库中落地 `.github/workflows/release-smoke.yml`
3. 先接入 `--help` / `--version` smoke
4.`CMP-13` 产出 fixtures 后,再把功能 smoke 接入 CI
5. 待命令面稳定后,再评估是否需要 `cargo-dist`

View File

@@ -0,0 +1,46 @@
# CMP-9 Future Desktop GUI Foundation Plan
日期2026-03-25
作者Senior Frontend Engineer
## Goal
在不进入完整桌面产品实现的前提下,为 `dbtool-cli-v1` 产出可交付的未来 GUI 基线,供 CTO 和 PM 用于后续项目拆分、契约澄清和范围控制。
## Current Assessment
- 当前仓库没有前端入口、`package.json`、React 应用、桌面壳或任何展示层代码。
- 当前前端最合适的交付物不是“桌面应用实现”,而是共享信息架构文档、组件边界说明和一个最小可见原型。
- GUI 必须复用 CLI 已定义的产品概念连接目标、schema browser、query editor、result set、export action。
## Planned Steps
1. 确认当前仓库没有现成前端基线,并记录约束
2. 定义桌面端信息架构、主工作区布局和页面边界
3. 识别 GUI 需要的核心数据契约与状态模型
4. 交付一个静态可预览的工作台原型,验证布局与信息密度方向
5. 补充查看方式和界面验收点,方便 CTO / PM / QA 复核
## Output Boundaries
本任务输出:
- GUI 信息架构
- 主工作区布局方向
- 组件清单
- 契约需求清单
- 静态原型
- 运行与验收说明
本任务不输出:
- Electron / Tauri 工程初始化
- React + shadcn/ui 真正应用代码
- 可连接真实数据库的 GUI
- 新增或修改后端契约实现
## Validation Rule
- 原型必须可以本地直接预览
- 文档必须明确“当前无前端基线”的事实
- 文档必须让后续 GUI 项目可直接拆成页面、组件和契约任务

View File

@@ -0,0 +1,92 @@
# dbtool-cli-v1 里程碑、依赖图与交付跟踪
日期2026-03-25
作者CTO
## 1. 当前真实状态
- 项目工作区现已存在但仓库内仍只有产品、QA、计划和 backlog 文档。
- 当前尚未落地 Rust workspace、`Cargo.toml`、CLI 二进制入口、demo、seed、smoke 或 CI 发布资产。
- 因此当前阶段不是“实现中后期”,而是“需求和执行顺序已明确,代码基线尚未开始”。
## 2. 里程碑建议
| 里程碑 | 目标 | 对应 issue | 当前状态 | 说明 |
| --- | --- | --- | --- | --- |
| M0 | 范围与架构收敛 | `CMP-2`, `CMP-3` | 已完成 | 产品边界和 Rust 架构都已形成共享文档 |
| M1 | 工程基线可启动 | `CMP-4` | 未开始 | 必须先产出可编译 workspace 和可运行 CLI |
| M2 | 首个数据库 happy path | `CMP-5` | 未开始 | 先用 PostgreSQL 验证 connect / inspect / query 主链路 |
| M3 | QA 输入与发布前置基线 | `CMP-10`, `CMP-12`, `CMP-13` | `CMP-10` blocked / `CMP-12` blocked / `CMP-13` in progress | QA 和发布资产可先建文档与 runbook但受代码基线约束 |
| M4 | 多数据库覆盖 | `CMP-6`, `CMP-7` | 未开始 | 在统一抽象上补齐 MySQL、SQLite |
| M5 | 导出闭环与首发准备 | `CMP-8` | 未开始 | 形成可验收的 export 能力并衔接 smoke |
| M6 | 未来 GUI 准备 | `CMP-9` | 未开始 | 保持 gated不进入 CLI v1 关键路径 |
## 3. 依赖关系图
### 逻辑依赖
```text
CMP-2 + CMP-3
CMP-4
CMP-5
↙ ↘
CMP-13 CMP-6
↓ ↓
CMP-10 CMP-7
↘ ↙
CMP-8
CMP-12
CMP-9 独立存在,但不进入 CLI v1 主路径
```
### 当前执行关键路径
在当前 owner 分布下,真正会拖慢交付的不是文档依赖,而是后端实现集中度:
`CMP-4``CMP-5``CMP-6``CMP-7``CMP-8`
说明:
- `CMP-4` 未完成前,`CMP-12` 只能停留在发布设计层。
- `CMP-13` 可以先产出 runbook但没有 CLI 基线时无法形成真实 smoke 输入。
- `CMP-10` 已有首版验收文档,但真实验证仍依赖后续功能实现。
## 4. 关键风险
### P0
- 代码基线仍为空,当前没有任何可运行、可测试、可发布资产。
- `CMP-4``CMP-8` 集中在同一位后端工程师名下,形成天然串行风险。
### P1
- `CMP-10``CMP-12` 已先进入 blocked说明 QA 与发布链路会早于功能被卡住。
- 如果 `CMP-9` 过早推进,会挤占 CLI 主路径注意力并制造范围噪音。
### P2
- 目前缺 demo、seed、smoke、Docker、README 运行说明,后续每个功能 issue 都容易因缺验证入口而返工。
## 5. 各角色第一轮职责
- 后端:先完成 `CMP-4`,再按 `CMP-5``CMP-6``CMP-7``CMP-8` 推进。
- QA继续完成 `CMP-13` 的样例与 runbook并把 `CMP-10` 保持为“文档先行、实测待代码”的状态。
- 前端:仅推进 `CMP-9` 的未来 GUI 信息架构,不进入 CLI 实现路径。
- CTO维护关键路径、阻塞升级、issue 排序和跨角色依赖收敛。
## 6. 状态同步节奏
- 日常节奏:每次 heartbeat 只同步真实状态变化,不做“空刷新”。
- 里程碑节奏:每完成一个里程碑,立即更新本跟踪文档和 backlog 判断。
- 阻塞节奏:出现阻塞时,同一 heartbeat 内更新 issue 状态、阻塞原因和上游 owner。
- 向 CEO 汇报的固定项当前关键路径、P0 风险、owner 负载集中度、是否需要调整优先级。
## 7. 当前管理判断
- 当前不建议扩编。真实瓶颈仍是“代码基线未启动”,不是“人手已经证明不足”。
- 当前最该做的是尽快让 `CMP-4` 开工并交付最小可编译 CLI。
-`CMP-4` 完成前,不应把 blocked 的 `CMP-10` / `CMP-12` 误判为执行不力,它们属于合理前置文档工作后遇到的依赖阻塞。

View File

@@ -0,0 +1,264 @@
# dbtool-cli-v1 Rust 架构方案与执行计划
日期2026-03-25
作者CTO
## 1. 当前真实落地情况
- Paperclip 中已经建立项目 `dbtool-cli-v1`,并已创建第一批工程 issue。
- 配置中的项目根目录是 `/workspace/repo/dbtool-cli-v1`,但截至今天该目录原本并不存在,说明代码仓库尚未真正初始化。
- 当前没有可验证的 Rust workspace、可执行 CLI、demo 数据、测试链路、发布链路或跨平台产物。
- 因此本阶段的真实状态不是“已有实现等待扩展”,而是“需求和任务已经拆出,但代码基线仍未落地”。
## 2. V1 工程目标
V1 只交付 CLI不交付 GUI。CLI 必须覆盖:
- 连接 PostgreSQL、MySQL、SQLite
- 浏览 schema / table / column
- 执行临时查询
- 执行脚本
- 导出 CSV / JSON
当前阶段明确 gated
- 完整桌面 GUI
- 插件系统
- 多进程或服务化架构
- 复杂 ORM / migration 编排
## 3. 推荐 Rust workspace 结构
```text
dbtool-cli-v1/
Cargo.toml
Cargo.lock
README.md
plans/
backlog/
apps/
dbtool-cli/
Cargo.toml
src/
crates/
db-core/
Cargo.toml
src/
db-drivers/
Cargo.toml
src/
db-config/
Cargo.toml
src/
examples/
fixtures/
scripts/
.github/
workflows/
```
### crate 边界
#### `apps/dbtool-cli`
- 命令行入口
- 参数解析
- 表格/文本输出
- 调用 `db-core` 用例
- 初期先承载导出命令编排,避免过早拆出额外 crate
#### `crates/db-core`
- 领域模型连接配置、schema 树、查询请求/响应、导出请求
- 通用错误模型
- 驱动能力 trait
- 应用层用例connect、inspect、query、run-script、export
#### `crates/db-drivers`
- PostgreSQL / MySQL / SQLite 的具体实现
-`db-core` trait 的适配
- feature flag 控制三类数据库依赖
- 统一管理连接池、SQL 方言差异和 introspection SQL
#### `crates/db-config`
- 连接 profile
- DSN 解析
- 本地配置文件读写
- 环境变量覆盖逻辑
## 4. 驱动抽象策略
V1 不做插件化驱动系统,也不做单独驱动进程。采用“核心 trait + 三个内建实现”的最小可行方案。
### 建议抽象
- `DatabaseKind`
- `ConnectionProfile`
- `DatabaseDriver`
- `CatalogIntrospector`
- `QueryExecutor`
### 设计原则
- `db-core` 只定义能力和标准化返回结构,不依赖具体数据库类型。
- `db-drivers` 内部按 `postgres``mysql``sqlite` 模块实现,但对上层暴露统一入口。
- 不使用过度抽象的通用连接层来抹平所有数据库差异;差异应在驱动层显式处理并记录。
- 不把 CLI 输出格式、表格渲染、交互提示放进核心 crate。
### 技术选型
- 异步运行时:`tokio`
- CLI 参数:`clap`
- 数据库访问:优先采用 `sqlx`,因为它原生覆盖 PostgreSQL、MySQL、SQLite能减少多套驱动栈带来的维护成本
- TLS默认 `rustls`
备注:`sqlx` 当前官方仓库明确覆盖 PostgreSQL、MySQL、SQLite打包链路可在仓库稳定后再接入 `cargo-dist`,当前不建议先引入额外发布复杂度。
## 5. 依赖关系与推进顺序
### Phase 0建基线
1. 初始化 Rust workspace
2. 跑通 CLI 二进制和 `--help`
3. 建立基础 README、开发说明、样例目录
### Phase 1建公共能力
1. 定义 `db-core` trait 与标准返回结构
2. 完成 `db-config`
3. 建立驱动 contract test 基座
### Phase 2按数据库逐个接入
1. PostgreSQL
2. MySQL
3. SQLite
顺序理由:先做服务型主路径,再覆盖文件型数据库差异。
### Phase 3补全导出与稳定性
1. CSV / JSON 导出
2. demo fixtures
3. smoke runbook
4. 跨平台打包
## 6. 测试策略
### 单元测试
- `db-config`profile 解析、环境变量覆盖、路径处理
- `db-core`:查询参数、错误转换、导出请求校验
### contract test
- 对三种数据库复用同一组行为测试:
- 连接成功
- 连接失败
- schema 列举
- table 描述
- 简单查询
- 参数化查询
- 导出
### 集成测试
- PostgreSQL / MySQL使用容器启动测试实例
- SQLite使用临时文件数据库
### CLI smoke test
- `dbtool connect`
- `dbtool inspect`
- `dbtool query`
- `dbtool export`
CLI 输出建议用 snapshot 测试保护,但只用于稳定文本输出,不替代行为测试。
## 7. 打包与发布策略
当前不需要“部署”,需要的是“发布可执行 CLI”。
### 第一阶段
- 先用 CI matrix 生成 macOS、Linux、Windows release binary
- 每个平台至少执行一次 `--help` smoke test
- 产出压缩包和 checksum
### 第二阶段
- 当命令面稳定后,再接入 `cargo-dist` 统一发布描述和产物整理
这个顺序比一开始就引入复杂发布工具更稳妥。
## 8. 前后端与 QA 第一轮职责
### 后端
- 初始化 workspace
- 定义核心抽象
- 按 PostgreSQL → MySQL → SQLite 顺序交付驱动
- 落地导出能力
- 建立打包流水线
### 前端
- 不做 GUI 实现
- 产出未来桌面端信息架构
- 明确连接管理、schema browser、query editor、results table 的交互边界
- 提前识别未来 GUI 对 CLI / core 层的契约要求
### QA
- 建立跨数据库验收矩阵
- 落地 demo fixtures 与 smoke runbook
- 对 connect / inspect / query / export 建可重复回归路径
## 9. 与现有 issue 的对应关系
### 已有 issue
- `CMP-4`:初始化 Rust workspace
- `CMP-5`PostgreSQL
- `CMP-6`MySQL
- `CMP-7`SQLite
- `CMP-8`CSV / JSON 导出
- `CMP-9`:未来 GUI 信息架构
- `CMP-10`:跨数据库验收矩阵
- `CMP-11`:里程碑、依赖图和交付跟踪
### 已补充的 issue
- `CMP-12`CLI 打包、发布与跨平台 smoke 流水线
- `CMP-13`demo 数据库、样例脚本与 smoke runbook
## 10. 当前关键风险
### P0
- 项目路径之前不存在,说明仓库尚未初始化,`CMP-4` 是所有实现工作的真实前置
### P1
- 产品范围文档 `CMP-2` 尚未完成前,部分命令细节和验收文字仍需与 PM 对齐
- 当前只有一名后端工程师,驱动接入和发布链路都压在同一人身上,关键路径较长
### P2
- 没有 demo fixtures 时QA 和后续 smoke automation 难以尽早稳定
## 11. 招聘建议
当前先不建议立即扩编。
原因:
- 现阶段瓶颈是仓库初始化、架构收敛和第一条实现链路,不是人手数量
- 在 PostgreSQL 主链路和 CI/发布链路跑通前,新增工程师的边际收益有限
触发招聘的条件:
- `CMP-4``CMP-5` 跑通后,后端仍被 MySQL / SQLite / release pipeline 长时间阻塞
- 或 GUI 方向从“设计基线”升级为并行实现项目

View File

@@ -0,0 +1,15 @@
# dbtool-cli-v1 PM Sequencing
## Recommended Delivery Order
1. bootstrap an empty but runnable Rust CLI workspace
2. establish a shared connection and inspection model
3. ship PostgreSQL happy path for connect, inspect, and query
4. reach MySQL parity on the same workflow
5. reach SQLite parity with documented structural differences
6. add CSV and JSON export on top of query results
7. validate the cross-database acceptance matrix before any GUI follow-on
## Sequencing Rule
Do not start GUI execution work until the CLI acceptance path is stable enough to validate the product workflow end to end.

View File

@@ -0,0 +1,20 @@
# CMP-13 QA Demo / Smoke Plan
日期2026-03-25
## Goal
`dbtool-cli-v1` 提供可重复的 demo 数据、样例 SQL 输入和 smoke runbook并明确当前不可执行的阻塞点。
## Planned Steps
1. 盘点当前仓库与共享 QA 基线
2. 设计跨数据库最小样例数据
3. 落地 PostgreSQL / MySQL / SQLite seed 资产
4. 落地本地 bootstrap 与 Docker demo 路径
5. 落地 smoke runbook 与预期断言
6. 记录阻塞并同步 CTO / PM
## Exit Rule
如果 CLI 可执行入口和命令契约仍未落地,则本任务输出共享 QA 资产后进入阻塞状态,不伪造“已通过”。

View File

@@ -0,0 +1,51 @@
# CMP-18 Git 仓库与 agent 提交推送机制计划
日期2026-03-26
作者CTO
## 目标
`dbtool-cli-v1` 从“已有工作区但没有 Git 边界”切换为“有独立仓库、有远程、有提交规则、有后续可复用 push 机制”的项目。
## 阶段拆解
### Phase 0仓库边界落地
- 在项目根目录初始化独立 Git 仓库
- 设定默认分支 `main`
- 配置远程 `origin`
- 完成首次受控 commit
### Phase 1提交与推送治理
- 明确推荐认证方案HTTPS + bot token
- 提供最小 `GIT_ASKPASS` 脚本
- 明确 commit、branch、push、review 规则
- 明确前端、后端、QA、集成类 issue 的提交责任边界
### Phase 2平台化复用
- 将本仓库的 Git 工作流沉淀为模板
- 未来在前端、后端、全栈项目中复用同一认证注入与 branch 规则
## 第一轮职责
- CTO完成仓库 bootstrap、治理文档、认证方案定稿
- 后端:按 issue 独立提交后端改动,不再等待“统一代提交”
- 前端:未来 GUI / Web 项目沿用同一 branch 与 review 规则
- QA验证受保护分支下的 smoke / CI gate 是否满足合并要求
## 风险与约束
- 当前未注入 Gitea 写权限凭据,因此本次只验证本地 commit不验证远程 push
- 若未来允许 agent push则必须先在 Gitea 启用 protected branch
- 若未来一个 issue 涉及多人协作,必须先明确最终集成 owner避免“都能改、没人收口”
## 完成定义
满足以下条件即可认为 `CMP-18` 达成:
- 本地已是独立 Git 仓库
- 远程仓库 URL 已明确并配置
- 共享文档已写清推荐认证与治理规则
- agent 已完成一次受控 commit

View File

@@ -0,0 +1,37 @@
# dbtool-cli-v1 PostgreSQL / MySQL runtime smoke 环境计划
日期2026-03-26
## 背景
- 当前仓库已具备 `docker-compose.demo.yml`、PostgreSQL / MySQL bootstrap 脚本、fixtures 与 `SMOKE_RUNBOOK.md`
- QA 已完成 Linux 本地 SQLite smoke但 PostgreSQL / MySQL runtime smoke 仍因当前 runner 无 Docker 而阻塞
- 当前问题的根因是**执行宿主缺失**,不是产品功能缺失
## 决策
采用**宿主机执行 + Docker Compose 临时数据库**作为当前标准方案。
- 推荐说明见 `QA_RUNTIME_ENVIRONMENT.md`
- 不采用长期宿主机数据库作为主路径
- 不在当前阶段为 `local-db-codex` 增加 Docker runtime
## 第一轮职责
- 后端:保持 compose / seed / SQL 输入稳定
- QA在 Docker-capable host 执行 PostgreSQL / MySQL smoke 并回填证据
- CTO完成方案裁决、任务拆解与阻塞升级
- 前端:保持 gated不进入当前执行链路
## 完成标准
- QA 能按 `QA_RUNTIME_ENVIRONMENT.md``SMOKE_RUNBOOK.md` 完成 PostgreSQL smoke
- QA 能按 `QA_RUNTIME_ENVIRONMENT.md``SMOKE_RUNBOOK.md` 完成 MySQL smoke
- 若执行仍失败阻塞必须明确归因到宿主、Docker、二进制或产品缺陷之一
## 2026-03-26 当前执行记录
- 当前 QA runner 已验证存在预编译 `target/release/dbtool`,且 `--help` / `--version` 正常
- 当前 QA runner 可通过 `examples/scripts/bootstrap-sqlite.sh` 复核 SQLite happy path
- 当前 QA runner 不存在 `docker`,因此无法拉起 `docker-compose.demo.yml`
- 当前 QA runner 不存在 `cargo`,因此宿主机执行时需提前准备 Rust toolchain 或直接分发预编译二进制

13
scripts/git/gitea-askpass.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
case "$1" in
*Username*)
printf '%s\n' "${GITEA_USERNAME:?GITEA_USERNAME is required}"
;;
*Password*)
printf '%s\n' "${GITEA_TOKEN:?GITEA_TOKEN is required}"
;;
*)
exit 1
;;
esac

36
scripts/release/package-unix.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "usage: scripts/release/package-unix.sh <target-triple> <binary-path>" >&2
exit 2
fi
target_triple="$1"
binary_path="$2"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
version="$(node "$repo_root/scripts/release/version.mjs")"
artifact_basename="dbtool-$version-$target_triple"
artifact_dir="$repo_root/dist/$artifact_basename"
archive_path="$repo_root/dist/$artifact_basename.tar.gz"
checksum_path="$archive_path.sha256"
mkdir -p "$repo_root/dist"
rm -rf "$artifact_dir"
mkdir -p "$artifact_dir"
cp "$binary_path" "$artifact_dir/dbtool"
cp "$repo_root/README.md" "$artifact_dir/README.md"
cp "$repo_root/RELEASE_RUNBOOK.md" "$artifact_dir/RELEASE_RUNBOOK.md"
cp "$repo_root/SMOKE_RUNBOOK.md" "$artifact_dir/SMOKE_RUNBOOK.md"
tar -czf "$archive_path" -C "$repo_root/dist" "$artifact_basename"
(
cd "$repo_root/dist"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$(basename "$archive_path")" > "$(basename "$checksum_path")"
else
shasum -a 256 "$(basename "$archive_path")" > "$(basename "$checksum_path")"
fi
)

12
scripts/release/smoke-binary.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "usage: scripts/release/smoke-binary.sh <binary-path>" >&2
exit 2
fi
binary_path="$1"
"$binary_path" --help
"$binary_path" --version

View File

@@ -0,0 +1,11 @@
import { readFileSync } from "node:fs";
const cargoToml = readFileSync(new URL("../../Cargo.toml", import.meta.url), "utf8");
const match = cargoToml.match(/\[workspace\.package\][\s\S]*?version\s*=\s*"([^"]+)"/);
if (!match) {
console.error("failed to resolve workspace.package.version from Cargo.toml");
process.exit(1);
}
process.stdout.write(match[1]);