chore: bootstrap independent git workflow
Some checks failed
release-smoke / macos-13 / x86_64-apple-darwin (push) Has been cancelled
release-smoke / ubuntu-latest / x86_64-unknown-linux-gnu (push) Has been cancelled
release-smoke / windows-latest / x86_64-pc-windows-msvc (push) Has been cancelled

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

38
examples/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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