chore: bootstrap independent git workflow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
100
.github/workflows/release-smoke.yml
vendored
Normal file
100
.github/workflows/release-smoke.yml
vendored
Normal 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
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
/examples/tmp
|
||||
/dist
|
||||
/.agents
|
||||
129
ACCEPTANCE_CHECKLIST.md
Normal file
129
ACCEPTANCE_CHECKLIST.md
Normal 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 path:missing file、permission denied、missing table、multi statement blocked、export overwrite blocked
|
||||
- SQLite `connect` failure path:non-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
1505
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
Normal file
29
Cargo.toml
Normal 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
58
DEFECT_CLASSIFICATION.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# dbtool-cli-v1 缺陷分类(初版)
|
||||
|
||||
## 严重级别
|
||||
|
||||
### Blocker
|
||||
|
||||
- CLI 无法启动
|
||||
- 三种目标数据库全部无法连接
|
||||
- 基础查询完全不可用
|
||||
- 导出产生错误结果且无替代路径
|
||||
- 发布路径不可构建、不可运行、不可验证
|
||||
|
||||
### Critical
|
||||
|
||||
- 任一目标数据库的核心 happy path 不可用
|
||||
- 错误提示误导用户造成高概率误操作
|
||||
- 数据结果明显错误
|
||||
- 导出文件损坏或内容错乱
|
||||
|
||||
### Major
|
||||
|
||||
- schema introspection 不完整
|
||||
- 部分查询场景失败
|
||||
- 平台间行为明显不一致
|
||||
- 失败路径不可定位
|
||||
|
||||
### Minor
|
||||
|
||||
- 帮助文案、提示文案、非阻断交互问题
|
||||
- 输出格式轻微不一致
|
||||
|
||||
## 缺陷类型
|
||||
|
||||
- 功能缺陷
|
||||
- 回归缺陷
|
||||
- 可用性缺陷
|
||||
- 可重复性缺陷
|
||||
- 文档缺陷
|
||||
- 环境 / 配置缺陷
|
||||
|
||||
## 缺陷记录最小字段
|
||||
|
||||
- 标题
|
||||
- 环境
|
||||
- 数据库类型
|
||||
- 操作系统
|
||||
- 复现步骤
|
||||
- 预期结果
|
||||
- 实际结果
|
||||
- 证据
|
||||
- 严重级别
|
||||
- 是否阻塞发布
|
||||
|
||||
## 发布阻断原则
|
||||
|
||||
- 任一 Blocker 未关闭,不得宣布通过。
|
||||
- Critical 缺陷若无明确降级方案,不得宣布通过。
|
||||
- 未复现或无证据的“怀疑问题”不能当作结论,但必须登记为风险观察。
|
||||
63
DEVELOPING.md
Normal file
63
DEVELOPING.md
Normal 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
194
GIT_WORKFLOW.md
Normal 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
32
PRE_RELEASE_CHECKLIST.md
Normal 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
218
PRODUCT_REQUIREMENTS.md
Normal 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
118
QA_RUNTIME_ENVIRONMENT.md
Normal 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 手段,只在未来多个项目都提出同类需求时再立项。**
|
||||
|
||||
### 方案 C:QA 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
112
README.md
Normal 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
83
RELEASE_RUNBOOK.md
Normal 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
126
SMOKE_RUNBOOK.md
Normal 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
74
TEST_STRATEGY.md
Normal 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
21
apps/cli/Cargo.toml
Normal 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
1069
apps/cli/src/main.rs
Normal file
File diff suppressed because it is too large
Load Diff
33
backlog/first-wave-issues.md
Normal file
33
backlog/first-wave-issues.md
Normal 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 开始提前进入。
|
||||
13
crates/db-config/Cargo.toml
Normal file
13
crates/db-config/Cargo.toml
Normal 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
123
crates/db-config/src/lib.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
9
crates/db-core/Cargo.toml
Normal file
9
crates/db-core/Cargo.toml
Normal 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
384
crates/db-core/src/lib.rs
Normal 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"
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
15
crates/db-drivers/Cargo.toml
Normal file
15
crates/db-drivers/Cargo.toml
Normal 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
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
30
docker-compose.demo.yml
Normal 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
38
examples/README.md
Normal 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.
|
||||
28
examples/fixtures/EXPECTED_RESULTS.md
Normal file
28
examples/fixtures/EXPECTED_RESULTS.md
Normal 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`
|
||||
35
examples/fixtures/mysql/init.sql
Normal file
35
examples/fixtures/mysql/init.sql
Normal 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');
|
||||
32
examples/fixtures/postgres/init.sql
Normal file
32
examples/fixtures/postgres/init.sql
Normal 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');
|
||||
34
examples/fixtures/sqlite/init.sql
Normal file
34
examples/fixtures/sqlite/init.sql
Normal 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');
|
||||
13
examples/scripts/bootstrap-mysql.sh
Executable file
13
examples/scripts/bootstrap-mysql.sh
Executable 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."
|
||||
14
examples/scripts/bootstrap-postgres.sh
Executable file
14
examples/scripts/bootstrap-postgres.sh
Executable 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."
|
||||
45
examples/scripts/bootstrap-sqlite.sh
Executable file
45
examples/scripts/bootstrap-sqlite.sh
Executable 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."
|
||||
12
examples/sql/mysql/export_query.sql
Normal file
12
examples/sql/mysql/export_query.sql
Normal 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;
|
||||
3
examples/sql/mysql/failing_query.sql
Normal file
3
examples/sql/mysql/failing_query.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
USE qa_demo;
|
||||
|
||||
SELECT * FROM missing_table;
|
||||
11
examples/sql/mysql/happy_path_query.sql
Normal file
11
examples/sql/mysql/happy_path_query.sql
Normal 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;
|
||||
12
examples/sql/postgres/export_query.sql
Normal file
12
examples/sql/postgres/export_query.sql
Normal 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;
|
||||
3
examples/sql/postgres/failing_query.sql
Normal file
3
examples/sql/postgres/failing_query.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
SET search_path TO qa_demo;
|
||||
|
||||
SELECT * FROM missing_table;
|
||||
11
examples/sql/postgres/happy_path_query.sql
Normal file
11
examples/sql/postgres/happy_path_query.sql
Normal 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;
|
||||
10
examples/sql/sqlite/export_query.sql
Normal file
10
examples/sql/sqlite/export_query.sql
Normal 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;
|
||||
1
examples/sql/sqlite/failing_query.sql
Normal file
1
examples/sql/sqlite/failing_query.sql
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM missing_table;
|
||||
9
examples/sql/sqlite/happy_path_query.sql
Normal file
9
examples/sql/sqlite/happy_path_query.sql
Normal 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
40
gui/README.md
Normal 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
291
gui/desktop-foundation.md
Normal 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
33
gui/preview-server.mjs
Normal 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
53
gui/prototype/app.js
vendored
Normal 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
293
gui/prototype/index.html
Normal 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 > 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
423
gui/prototype/styles.css
Normal 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;
|
||||
}
|
||||
}
|
||||
100
plans/2026-03-25-cli-release-smoke-baseline.md
Normal file
100
plans/2026-03-25-cli-release-smoke-baseline.md
Normal 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`
|
||||
46
plans/2026-03-25-cmp-9-desktop-gui-foundation-plan.md
Normal file
46
plans/2026-03-25-cmp-9-desktop-gui-foundation-plan.md
Normal 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 项目可直接拆成页面、组件和契约任务
|
||||
92
plans/2026-03-25-dbtool-cli-v1-delivery-tracking.md
Normal file
92
plans/2026-03-25-dbtool-cli-v1-delivery-tracking.md
Normal 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` 误判为执行不力,它们属于合理前置文档工作后遇到的依赖阻塞。
|
||||
264
plans/2026-03-25-dbtool-cli-v1-rust-architecture-plan.md
Normal file
264
plans/2026-03-25-dbtool-cli-v1-rust-architecture-plan.md
Normal 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 方向从“设计基线”升级为并行实现项目
|
||||
15
plans/2026-03-25-pm-v1-sequencing.md
Normal file
15
plans/2026-03-25-pm-v1-sequencing.md
Normal 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.
|
||||
20
plans/2026-03-25-qa-cmp-13-demo-runbook-plan.md
Normal file
20
plans/2026-03-25-qa-cmp-13-demo-runbook-plan.md
Normal 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 资产后进入阻塞状态,不伪造“已通过”。
|
||||
51
plans/2026-03-26-cmp-18-git-repo-bootstrap-plan.md
Normal file
51
plans/2026-03-26-cmp-18-git-repo-bootstrap-plan.md
Normal 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
|
||||
37
plans/2026-03-26-qa-runtime-smoke-environment.md
Normal file
37
plans/2026-03-26-qa-runtime-smoke-environment.md
Normal 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
13
scripts/git/gitea-askpass.sh
Executable 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
36
scripts/release/package-unix.sh
Executable 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
12
scripts/release/smoke-binary.sh
Executable 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
|
||||
11
scripts/release/version.mjs
Normal file
11
scripts/release/version.mjs
Normal 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]);
|
||||
Reference in New Issue
Block a user