sql-harness 0.2.4__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sql_harness-0.3.2/AGENTS.md +19 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/PKG-INFO +5 -5
- sql_harness-0.3.2/SKILL.md +195 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2/docs}/AGENTS.md +4 -3
- {sql_harness-0.2.4 → sql_harness-0.3.2/docs}/README.md +3 -3
- {sql_harness-0.2.4 → sql_harness-0.3.2/docs}/install.md +5 -1
- sql_harness-0.3.2/docs/maintainer.md +58 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/docs/standalone-install.md +3 -3
- sql_harness-0.3.2/interaction-skills/mysql/json-columns.md +43 -0
- sql_harness-0.3.2/interaction-skills/postgres/json-columns.md +46 -0
- sql_harness-0.3.2/interaction-skills/postgres/migrations.md +105 -0
- sql_harness-0.3.2/interaction-skills/ssh/auth-and-tunnels.md +145 -0
- {sql_harness-0.2.4/interaction-skills → sql_harness-0.3.2/interaction-skills/ssh}/docker-via-ssh.md +41 -15
- {sql_harness-0.2.4 → sql_harness-0.3.2}/pyproject.toml +2 -6
- sql_harness-0.3.2/skills/sql-harness/README.txt +8 -0
- sql_harness-0.3.2/skills/sql-harness/references/install.md +145 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/sql-harness +1 -1
- sql_harness-0.3.2/src/sql_harness/analytics.py +290 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/cli.py +33 -6
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/helpers.py +6 -3
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/manager.py +4 -1
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/run.py +15 -4
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/integration/test_pgexercises_practice.py +8 -4
- sql_harness-0.3.2/tests/unit/test_analytics.py +323 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/uv.lock +1 -1
- sql_harness-0.2.4/SKILL.md +0 -99
- sql_harness-0.2.4/docker-services/coturn/README.md +0 -69
- sql_harness-0.2.4/docker-services/coturn/docker-compose.yml +0 -29
- sql_harness-0.2.4/docker-services/coturn/turnserver.conf +0 -36
- sql_harness-0.2.4/docker-services/mysql/docker-compose.yml +0 -35
- sql_harness-0.2.4/docker-services/redis/docker-compose.yml +0 -24
- sql_harness-0.2.4/docs/maintainer.md +0 -60
- sql_harness-0.2.4/interaction-skills/json-columns.md +0 -30
- sql_harness-0.2.4/practice/pgexercises/README.md +0 -63
- sql_harness-0.2.4/skills/sql-harness/README.txt +0 -8
- sql_harness-0.2.4/skills/sql-harness/SKILL.md +0 -1
- sql_harness-0.2.4/skills/sql-harness/references/install.md +0 -93
- sql_harness-0.2.4/src/sql_harness/SKILL.md +0 -1
- {sql_harness-0.2.4 → sql_harness-0.3.2}/.claude-plugin/marketplace.json +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/.claude-plugin/plugin.json +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/.env.example +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/.gitignore +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/agent-workspace/README.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/agent-workspace/agent_helpers.example.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/agent-workspace/agent_helpers.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/agent-workspace/zones/example_pg/scripts/sh_demo_crud.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/agent-workspace/zones/example_pg/skills/sh_demo-schema.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/agent-workspace/zones/meta/skills/pool.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/agent-workspace/zones/meta/skills/workspace.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/docs/connections-toml-example.toml +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/docs/headless-notes.md +0 -0
- {sql_harness-0.2.4/interaction-skills/references → sql_harness-0.3.2/interaction-skills}/aggregations-advanced.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/aggregations.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/encoding-and-charset.md +0 -0
- {sql_harness-0.2.4/interaction-skills/references → sql_harness-0.3.2/interaction-skills}/joins-advanced.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/joins.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/large-result-sets.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/migrations.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/pooling.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/postgres/btree-indexes.md +0 -0
- {sql_harness-0.2.4/interaction-skills → sql_harness-0.3.2/interaction-skills/postgres}/indexes-and-explain.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/postgres/plan-reading.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/postgres/slow-queries-joins.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/postgres/specialized-indexes.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/postgres/table-optimization.md +0 -0
- /sql_harness-0.2.4/interaction-skills/references/recursive-ctes-patterns.md → /sql_harness-0.3.2/interaction-skills/recursive-ctes-advanced.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/recursive-ctes.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/save-run-cycle.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/schema-introspection.md +0 -0
- {sql_harness-0.2.4/interaction-skills → sql_harness-0.3.2/interaction-skills/ssh}/ssh.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/timeouts-and-cancellation.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/transactions.md +0 -0
- /sql_harness-0.2.4/interaction-skills/references/window-functions-catalog.md → /sql_harness-0.3.2/interaction-skills/window-functions-advanced.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/window-functions.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/interaction-skills/zone-skill-auto-surface.md +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/__init__.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/agent_loader.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/config.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/drivers/__init__.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/drivers/mysql.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/drivers/postgres.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/drivers/redis.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/drivers/sqlite.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/drivers/ssh.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/src/sql_harness/paths.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/__init__.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/conftest.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/integration/__init__.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/integration/test_mysql.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/integration/test_pg_optimization.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/integration/test_postgres.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/integration/test_ssh.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/__init__.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/test_agent_loader.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/test_cli.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/test_config.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/test_helpers.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/test_manager.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/test_paths.py +0 -0
- {sql_harness-0.2.4 → sql_harness-0.3.2}/tests/unit/test_ssh.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# AGENTS.md — pointer
|
|
2
|
+
|
|
3
|
+
> All contributor documentation lives in `docs/AGENTS.md` (single docs directory).
|
|
4
|
+
> This pointer file exists only because agent frameworks auto-load `AGENTS.md`
|
|
5
|
+
> by filename at the repo root — read the real content at `docs/AGENTS.md`.
|
|
6
|
+
|
|
7
|
+
# Code priorities
|
|
8
|
+
|
|
9
|
+
- Clarity
|
|
10
|
+
- Precision
|
|
11
|
+
- Low verbosity
|
|
12
|
+
- Versatility
|
|
13
|
+
|
|
14
|
+
# One-line summary
|
|
15
|
+
|
|
16
|
+
sql-harness is a single-process SQL + SSH CLI for agents: plaintext
|
|
17
|
+
connections in one TOML file, heredoc mode with auto-injected helpers.
|
|
18
|
+
Core code in `src/sql_harness/`, agent-editable workspace under
|
|
19
|
+
`agent-workspace/`. Full module list + contributing rules: `docs/AGENTS.md`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: sql-harness
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace.
|
|
5
5
|
Project-URL: Source, https://github.com/zhaoliuxue/much_bigpy/tree/master/lab/sql_harness
|
|
6
6
|
Project-URL: Issues, https://github.com/zhaoliuxue/much_bigpy/issues
|
|
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
A thin, single-process SQL CLI for LLM agents. Mirrors [browser-harness](https://github.com/browser-use/browser-harness)'s structure but targets relational databases (Postgres, MySQL, Redis-soon).
|
|
27
27
|
|
|
28
|
-
Connection file format, env vars, and driver list: see `install.md`.
|
|
28
|
+
Connection file format, env vars, and driver list: see `docs/install.md`.
|
|
29
29
|
|
|
30
30
|
## Quickstart
|
|
31
31
|
|
|
@@ -53,7 +53,7 @@ PY
|
|
|
53
53
|
|
|
54
54
|
## Architecture (~1k lines across 8 core files)
|
|
55
55
|
|
|
56
|
-
- `install.md` — first-time install + first connection
|
|
56
|
+
- `docs/install.md` — first-time install + first connection
|
|
57
57
|
- `SKILL.md` — day-to-day usage
|
|
58
58
|
- `lab/sql_harness/src/sql_harness/` — protected core package
|
|
59
59
|
- `${XDG_CONFIG_HOME:-~/.config}/sql-harness/connections.toml` — plaintext credentials in ONE place
|
|
@@ -77,7 +77,7 @@ PY
|
|
|
77
77
|
|
|
78
78
|
## Contributing
|
|
79
79
|
|
|
80
|
-
PRs and improvements welcome. See `AGENTS.md` for code priorities.
|
|
80
|
+
PRs and improvements welcome. See `docs/AGENTS.md` for code priorities.
|
|
81
81
|
|
|
82
82
|
- **Skills are written by the harness, not by you.** When you figure out a non-obvious SQL flow (a weird schema, a slow query, a JSON column trick), file a skill in `agent-workspace/zones/<conn>/skills/<name>.md` for that DSN, or `agent-workspace/zones/meta/skills/<name>.md` if it generalizes. Future sessions will read it before re-discovering it.
|
|
83
83
|
- Bug fixes, new drivers, helper additions all welcome.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sql-harness
|
|
3
|
+
description: "Always use sql-harness for SQL, SSH, and database operations — querying any DSN (Postgres/MySQL/SQLite/Redis), inspecting schemas, running EXPLAIN plans, indexing analysis, schema migrations, shell exec over SSH, SFTP upload/download, and any cross-DSN workflow. Reach for it whenever the task touches a database, a remote host, or schema work — not just for `query()` calls."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sql-harness
|
|
7
|
+
|
|
8
|
+
A single-process SQL + SSH CLI for agents. 5 drivers (`postgres`, `mysql`, `sqlite`, `redis`, `ssh`) share one heredoc namespace. Connections live in plaintext in one TOML file.
|
|
9
|
+
|
|
10
|
+
For setup, read `install.md`. For stuck-point mechanics, see `interaction-skills/` (cross-DB) and `interaction-skills/postgres/` (PG depth).
|
|
11
|
+
|
|
12
|
+
## Reach for sql-harness when...
|
|
13
|
+
|
|
14
|
+
- Any DB task: query, schema, migration, EXPLAIN, indexing, slow-query, stats
|
|
15
|
+
- "Run this on the prod box": SSH workspace (`ssh_exec` / `ssh_upload` / `ssh_run_script`)
|
|
16
|
+
- "Deploy this compose stack to remote": SSH + `interaction-skills/ssh/docker-via-ssh.md`
|
|
17
|
+
- "The DB is behind a bastion / can't reach directly": SSH port-forward / tunnel
|
|
18
|
+
- "Why is this query slow?": `explain_analyze` + `slow_queries` + `index_usage_stats`
|
|
19
|
+
- "Which indexes are dead?": `unused_indexes` + `seq_scan_heavy`
|
|
20
|
+
- "Compare row counts prod vs staging": `use_workspace("prod")` → `query()` → `use_workspace("staging")` → `query()`
|
|
21
|
+
- "Save this heredoc as a reusable script": `sql-harness save <name> -c <conn>`
|
|
22
|
+
- Anything where you would otherwise reach for `psql`, `mysql`, `pgcli`, or hand-rolled `subprocess` over SSH
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
sql-harness --help
|
|
28
|
+
sql-harness list
|
|
29
|
+
sql-harness add <name> --driver postgres --url 'postgresql://user:pw@host/db'
|
|
30
|
+
sql-harness add <name> --driver mysql --url 'mysql+pymysql://user:pw@host/db'
|
|
31
|
+
sql-harness add <name> --driver ssh --url 'ssh://user@host:22?key=/path/id_ed25519'
|
|
32
|
+
sql-harness test <name>
|
|
33
|
+
|
|
34
|
+
sql-harness <<'PY'
|
|
35
|
+
use_workspace("local_pg")
|
|
36
|
+
print(query("SELECT version()"))
|
|
37
|
+
print(list_tables())
|
|
38
|
+
PY
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Helpers are pre-imported; call by name in heredoc mode.
|
|
42
|
+
- **First call requires `use_workspace(name)`** — no implicit default.
|
|
43
|
+
- Set `BH_SQL_ZONE_SKILLS=1` once so `use_workspace()` auto-surfaces that zone's per-DSN skill docs (see `interaction-skills/zone-skill-auto-surface.md`).
|
|
44
|
+
- For task-specific helpers, drop them into `agent-workspace/zones/<conn>/helpers.py` and they merge into the heredoc namespace (per-zone, overriding the shared base).
|
|
45
|
+
- Other subcommands: `init` (scaffold connections.toml + install preset zone skills), `doctor` (SELECT 1 on every connection), `edit`/`remove`/`show`, `workspace {list,use,close,show}`, `save`/`run`/`scripts`, `skill` (bare: emit packaged SKILL.md; `install`/`list`/`show`), `paths`, `open`, `stats`, `version`, `ssh {exec,upload,download,run-script,info}`.
|
|
46
|
+
- Zone-scoped subcommands (`save`, `run`, `scripts`, `skill list/show`, `ssh`) need `-c <conn>` or `$BH_SQL_ACTIVE_CONNECTION`.
|
|
47
|
+
- **Analytics (ON by default)**: every CLI call and every doc read (`apply_skill`, `skill`, `skill show`) is counted into `$BH_SQL_HOME/analytics.json`; view with `sql-harness stats`. Every executed command (CLI argv or heredoc body) plus failures is appended to `$BH_SQL_HOME/sql-harness.log` (NDJSON, one record per line). Disable both with `BH_SQL_ANALYTICS=0|off|false|no`. Extension point: `from sql_harness.analytics import on, CLI_INVOKED, DOC_READ, COMMAND_EXECUTED` to attach custom listeners (observer pattern).
|
|
48
|
+
|
|
49
|
+
## Drivers & capabilities
|
|
50
|
+
|
|
51
|
+
Five drivers, one heredoc namespace. URL scheme must match the backend: MySQL requires `mysql+pymysql://`; PostgreSQL accepts `postgres://` / `postgresql://` (normalized to `postgresql+psycopg://`); SSH accepts `ssh://`, `ssh+password://`, `ssh+key://`.
|
|
52
|
+
|
|
53
|
+
### Postgres (`postgres://`)
|
|
54
|
+
|
|
55
|
+
Full SQLAlchemy + psycopg3 + 8 PG-only introspection helpers. Reach for these whenever the task is "PG + performance" or "PG + schema":
|
|
56
|
+
|
|
57
|
+
- `explain(sql)` — `EXPLAIN` plan (estimated)
|
|
58
|
+
- `explain_analyze(sql, buffers=True)` — `EXPLAIN (ANALYZE, BUFFERS)` (real times)
|
|
59
|
+
- `table_stats(name, schema=None)` — `pg_stat_user_tables` (seq vs idx scan counts, dead tuples)
|
|
60
|
+
- `index_usage_stats(table=None, schema=None)` — `pg_stat_user_indexes` (idx scan counts per index)
|
|
61
|
+
- `unused_indexes(...)` — indexes with zero idx scans (candidates for DROP)
|
|
62
|
+
- `seq_scan_heavy(...)` — tables routinely full-scanned (candidates for new indexes)
|
|
63
|
+
- `slow_queries(limit=10)` — top by `pg_stat_statements.mean_exec_time`
|
|
64
|
+
- `missing_indexes_hint(...)` — heuristic: many seq scans + table size > threshold
|
|
65
|
+
|
|
66
|
+
See `interaction-skills/postgres/` for the 8 depth docs (plan-reading, B-Tree, specialized indexes, table optimization, slow-queries-joins, jsonb, migrations, indexes-and-explain).
|
|
67
|
+
|
|
68
|
+
### MySQL (`mysql+pymysql://`)
|
|
69
|
+
|
|
70
|
+
Standard SQLAlchemy + PyMySQL. JSON syntax differs from PG — see `interaction-skills/mysql/json-columns.md`.
|
|
71
|
+
|
|
72
|
+
### SQLite (`sqlite:///path.db`)
|
|
73
|
+
|
|
74
|
+
Local dev / tests. All standard helpers work. Use `sql-harness add --driver sqlite --url 'sqlite:///<absolute-path>'` (SQLite needs absolute path).
|
|
75
|
+
|
|
76
|
+
### Redis (`redis://host:port`)
|
|
77
|
+
|
|
78
|
+
Stub driver — wired up but minimal helpers. Prefer `query()` won't work (Redis is not SQL). Skip if your task needs Redis; flag in your reply.
|
|
79
|
+
|
|
80
|
+
### SSH (`ssh://user@host:port?key=...`)
|
|
81
|
+
|
|
82
|
+
**Not a database** — a remote shell workspace. Reach for ssh whenever the task says "on the box", "the prod host", "deploy this", "upload that file", "run that command remotely":
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
use_workspace("prod-app")
|
|
86
|
+
r = ssh_exec("systemctl status myapp") # exit_code + stdout/stderr
|
|
87
|
+
ssh_upload("./deploy.sh", "/tmp/deploy.sh") # SFTP
|
|
88
|
+
ssh_download("/var/log/app.log", "./app.log") # SFTP
|
|
89
|
+
ssh_run_script("./scripts/deploy.sh") # upload + execute
|
|
90
|
+
print(ssh_info()) # user/host/port/SFTP available
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
CLI equivalent: `sql-harness ssh -c <conn> {exec|upload|download|run-script|info} <args>`.
|
|
94
|
+
|
|
95
|
+
See `interaction-skills/ssh/` for: `ssh.md` (helpers + CLI + gotchas), `docker-via-ssh.md` (docker-compose deploy loop over SSH), `auth-and-tunnels.md` (auth scheme 选型 + 堡垒机 + 端口转发).
|
|
96
|
+
|
|
97
|
+
## Generic SQL helpers (all DB drivers)
|
|
98
|
+
|
|
99
|
+
| Helper | Returns | Use |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `query(sql, params=None)` | `list[dict]` | SELECTs only |
|
|
102
|
+
| `execute(sql, params=None)` | `dict` (`lastrowid`, `rowcount`) | INSERT/UPDATE/DELETE/DDL |
|
|
103
|
+
| `list_tables(schema=None)` | `list[str]` | Schema recon — always start here |
|
|
104
|
+
| `describe(table, schema=None)` | `list[dict]` | Columns + types — before writing SQL |
|
|
105
|
+
| `table(name, schema=None, limit=None)` | `list[dict]` | Quick peek: `SELECT * FROM x LIMIT n` |
|
|
106
|
+
| `with_transaction()` | context manager yielding `Connection` | ≥ 2 statements / DDL / atomicity |
|
|
107
|
+
| `run_sql_file(path)` | `list[dict]` | Multi-statement `.sql`, one transaction |
|
|
108
|
+
| `server_version()` | `str` | Round-trip liveness check |
|
|
109
|
+
| `connection_info()` | `dict` | Current workspace metadata |
|
|
110
|
+
| `use_workspace_info(name)` | `dict` | Zone info + skills + scripts (always surfaces) |
|
|
111
|
+
|
|
112
|
+
## Interaction Skills
|
|
113
|
+
|
|
114
|
+
Stuck-point mechanics — read on demand by filename. Top-level (cross-DB):
|
|
115
|
+
|
|
116
|
+
- `interaction-skills/aggregations.md` (+ `aggregations-advanced.md`)
|
|
117
|
+
- `interaction-skills/encoding-and-charset.md`
|
|
118
|
+
- `interaction-skills/joins.md` (+ `joins-advanced.md`)
|
|
119
|
+
- `interaction-skills/large-result-sets.md`
|
|
120
|
+
- `interaction-skills/migrations.md`
|
|
121
|
+
- `interaction-skills/pooling.md`
|
|
122
|
+
- `interaction-skills/recursive-ctes.md` (+ `recursive-ctes-advanced.md`)
|
|
123
|
+
- `interaction-skills/save-run-cycle.md`
|
|
124
|
+
- `interaction-skills/schema-introspection.md`
|
|
125
|
+
- `interaction-skills/timeouts-and-cancellation.md`
|
|
126
|
+
- `interaction-skills/transactions.md`
|
|
127
|
+
- `interaction-skills/window-functions.md` (+ `window-functions-advanced.md`)
|
|
128
|
+
- `interaction-skills/zone-skill-auto-surface.md`
|
|
129
|
+
|
|
130
|
+
PG-specific depth — `interaction-skills/postgres/`:
|
|
131
|
+
|
|
132
|
+
- `interaction-skills/postgres/btree-indexes.md`
|
|
133
|
+
- `interaction-skills/postgres/indexes-and-explain.md`
|
|
134
|
+
- `interaction-skills/postgres/json-columns.md`
|
|
135
|
+
- `interaction-skills/postgres/migrations.md`
|
|
136
|
+
- `interaction-skills/postgres/plan-reading.md`
|
|
137
|
+
- `interaction-skills/postgres/slow-queries-joins.md`
|
|
138
|
+
- `interaction-skills/postgres/specialized-indexes.md`
|
|
139
|
+
- `interaction-skills/postgres/table-optimization.md`
|
|
140
|
+
|
|
141
|
+
MySQL-specific — `interaction-skills/mysql/`:
|
|
142
|
+
|
|
143
|
+
- `interaction-skills/mysql/json-columns.md`
|
|
144
|
+
|
|
145
|
+
SSH-specific — `interaction-skills/ssh/`:
|
|
146
|
+
|
|
147
|
+
- `interaction-skills/ssh/auth-and-tunnels.md`
|
|
148
|
+
- `interaction-skills/ssh/docker-via-ssh.md`
|
|
149
|
+
- `interaction-skills/ssh/ssh.md`
|
|
150
|
+
|
|
151
|
+
## Cross-DSN strategy skills
|
|
152
|
+
|
|
153
|
+
Reached via `apply_skill("pool")` etc. from any active zone — `zones/meta/skills/` is the passive fallback layer.
|
|
154
|
+
|
|
155
|
+
- `agent-workspace/zones/meta/skills/pool.md` — pool sizing, `pre_ping`, idle reuse.
|
|
156
|
+
- `agent-workspace/zones/meta/skills/workspace.md` — workspace isolation, multi-DSN workflows.
|
|
157
|
+
|
|
158
|
+
## What actually works (field-tested)
|
|
159
|
+
|
|
160
|
+
- **Start with `list_tables` + `describe`, not `query`.** Before writing SQL on an unknown DB, run `list_tables(schema=None)` then `describe("table", schema=None)` to learn the columns. Skips the trial-and-error SELECT cycle.
|
|
161
|
+
- **Set `BH_SQL_ZONE_SKILLS=1` once** so every `use_workspace()` surfaces that zone's per-DSN skill docs as a hint list (10 filenames max). Then `apply_skill(name)` reads the body when relevant.
|
|
162
|
+
- **Default to `with_transaction()`** for any write with ≥ 2 statements (insert + update, drop + create, …). Atomic + automatic rollback on exception.
|
|
163
|
+
- **For PG performance tasks, reach for the 8 PG-only helpers first** (`explain_analyze`, `slow_queries`, `unused_indexes`, `seq_scan_heavy`, `table_stats`, `index_usage_stats`, `missing_indexes_hint`). Don't hand-parse `pg_stat_*` SQL.
|
|
164
|
+
- **For remote tasks, reach for SSH workspace first.** Don't reach for raw `subprocess` + ssh-key files in agent code — `ssh_exec("systemctl restart myapp")` is one line.
|
|
165
|
+
- **For "insert returning the new id"**: psycopg supports `INSERT … RETURNING id`; MySQL has no `RETURNING` — use `execute(...)` and read `result.lastrowid`.
|
|
166
|
+
- **Streaming large result sets**: `query()` loads every row into memory. For >10k rows, use `ws.engine.connect().execution_options(stream_results=True)` and iterate manually.
|
|
167
|
+
- **Read-only "is this alive?" check**: `print(server_version())` — single round-trip, works on every DSN, no per-driver fudging.
|
|
168
|
+
- **Auth wall**: redirected to login → stop and ask the user. Don't type credentials from `connection_info`/`describe`.
|
|
169
|
+
- **Credentials in TOML**: use `${env:VAR}` indirection for prod secrets. The connections.toml loader expands these at load time.
|
|
170
|
+
- **Cross-DB SQL**: prefer ANSI syntax (`'string'`, `LIMIT n OFFSET m`, `COALESCE`, `CURRENT_TIMESTAMP`). PG-specific: `::TYPE`, `JSONB`, `RETURNING`. MySQL-specific: `AUTO_INCREMENT`, backticks, `?` placeholders (sqlalchemy normalizes to `:name`).
|
|
171
|
+
- **Save the workflow**: every working block → `sql-harness save <name> -c <conn>`. Tomorrow's session re-runs with one command. See `interaction-skills/save-run-cycle.md`.
|
|
172
|
+
|
|
173
|
+
## Design Constraints
|
|
174
|
+
|
|
175
|
+
- One connection = one workspace; never share engines across workspaces.
|
|
176
|
+
- Connection pool defaults: `size=5, recycle=3600, pre_ping=True` (per-connection overrides win).
|
|
177
|
+
- `with_transaction()` yields a `Connection`; use `conn.execute(text(...))` for raw control.
|
|
178
|
+
- `query()` returns `list[dict]`; use `execute()` for INSERT/UPDATE/DELETE.
|
|
179
|
+
- `list_tables()` and `describe()` are read-only schema introspection helpers.
|
|
180
|
+
- SSH workspaces expose `ssh_exec` / `ssh_upload` / etc. Calling them on a non-SSH workspace raises `RuntimeError`.
|
|
181
|
+
- PG-only helpers (`explain_analyze`, `table_stats`, ...) raise `NotImplementedError` on non-PG workspaces. Detect with `ws.driver.name == "postgres"`.
|
|
182
|
+
- All engines live in `SqlHarness`'s in-memory registry. Single-process, no daemon, no remote backend.
|
|
183
|
+
|
|
184
|
+
## Gotchas
|
|
185
|
+
|
|
186
|
+
- Driver label in TOML must match the URL scheme (`postgres` ⇄ `postgresql://`, `mysql` ⇄ `mysql+pymysql://`, `ssh` ⇄ `ssh://` / `ssh+password://` / `ssh+key://`).
|
|
187
|
+
- Passwords in TOML are plaintext; use `${env:VAR}` indirection for prod secrets.
|
|
188
|
+
- `query()` is for SELECTs only. For INSERTs, use `execute()`.
|
|
189
|
+
- For tables > 10k rows, use `engine.connect().execution_options(stream_results=True)` and iterate manually — `query()` loads everything into memory.
|
|
190
|
+
- SSH SFTP subsystem may be disabled on the remote host. `ssh_upload/ssh_download` raise `RuntimeError("SFTP subsystem not available")`; fall back to `tar -czf - | ssh host tar -xzf -` via `ssh_exec`.
|
|
191
|
+
- Default `ssh_exec` timeout is 30 s. Long-running commands (e.g. `pg_dump`, `tar czf`) need explicit `timeout=`.
|
|
192
|
+
|
|
193
|
+
## Domain / table skills
|
|
194
|
+
|
|
195
|
+
Per-table or per-schema skills: drop a markdown file into `agent-workspace/zones/<conn>/skills/<name>.md`, read with `apply_skill(name)`. Cross-DSN strategy goes to `zones/meta/skills/` instead. PG optimization depth (plan reading, indexes, `table_stats`, `slow_queries`, …) lives in `interaction-skills/postgres/`.
|
|
@@ -15,11 +15,12 @@ Core code lives in `lab/sql_harness/src/sql_harness/`:
|
|
|
15
15
|
- `manager.py` — SqlHarness class: holds engines, workspaces, skills registry
|
|
16
16
|
- `helpers.py` — heredoc helpers (auto-imported)
|
|
17
17
|
- `agent_loader.py` — loads `$BH_SQL_AGENT_WORKSPACE/agent_helpers.py`
|
|
18
|
+
- `analytics.py` — event bus (observer) + JSON stats + NDJSON execution log: CLI call counts, doc-read tracking, command/error history; `BH_SQL_ANALYTICS` (default ON), view with `sql-harness stats`
|
|
18
19
|
- `run.py` — heredoc entry; delegates to CLI otherwise
|
|
19
|
-
- `cli.py` — argparse subparser CLI (
|
|
20
|
+
- `cli.py` — argparse subparser CLI (18 subcommands)
|
|
20
21
|
|
|
21
22
|
`SKILL.md` tells agents how to use the harness.
|
|
22
|
-
`install.md` tells agents how to install and configure it.
|
|
23
|
+
`docs/install.md` tells agents how to install and configure it.
|
|
23
24
|
|
|
24
25
|
An agent operating the harness only edits inside `agent-workspace/`:
|
|
25
26
|
- `agent_helpers.py` — task-specific query helpers the agent adds
|
|
@@ -28,4 +29,4 @@ An agent operating the harness only edits inside `agent-workspace/`:
|
|
|
28
29
|
# Contributing
|
|
29
30
|
Consider what is really needed. Prefer the smallest diff that fixes the bug.
|
|
30
31
|
|
|
31
|
-
To add a backend: implement `drivers/<name>.py` against the Driver protocol, register in `drivers/__init__.py:get_driver()`, add `tests/unit/test_<name>.py` (see install.md §Drivers).
|
|
32
|
+
To add a backend: implement `drivers/<name>.py` against the Driver protocol, register in `drivers/__init__.py:get_driver()`, add `tests/unit/test_<name>.py` (see `docs/install.md` §Drivers).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A thin, single-process SQL CLI for LLM agents. Mirrors [browser-harness](https://github.com/browser-use/browser-harness)'s structure but targets relational databases (Postgres, MySQL, Redis-soon).
|
|
4
4
|
|
|
5
|
-
Connection file format, env vars, and driver list: see `install.md`.
|
|
5
|
+
Connection file format, env vars, and driver list: see `docs/install.md`.
|
|
6
6
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ PY
|
|
|
30
30
|
|
|
31
31
|
## Architecture (~1k lines across 8 core files)
|
|
32
32
|
|
|
33
|
-
- `install.md` — first-time install + first connection
|
|
33
|
+
- `docs/install.md` — first-time install + first connection
|
|
34
34
|
- `SKILL.md` — day-to-day usage
|
|
35
35
|
- `lab/sql_harness/src/sql_harness/` — protected core package
|
|
36
36
|
- `${XDG_CONFIG_HOME:-~/.config}/sql-harness/connections.toml` — plaintext credentials in ONE place
|
|
@@ -54,7 +54,7 @@ PY
|
|
|
54
54
|
|
|
55
55
|
## Contributing
|
|
56
56
|
|
|
57
|
-
PRs and improvements welcome. See `AGENTS.md` for code priorities.
|
|
57
|
+
PRs and improvements welcome. See `docs/AGENTS.md` for code priorities.
|
|
58
58
|
|
|
59
59
|
- **Skills are written by the harness, not by you.** When you figure out a non-obvious SQL flow (a weird schema, a slow query, a JSON column trick), file a skill in `agent-workspace/zones/<conn>/skills/<name>.md` for that DSN, or `agent-workspace/zones/meta/skills/<name>.md` if it generalizes. Future sessions will read it before re-discovering it.
|
|
60
60
|
- Bug fixes, new drivers, helper additions all welcome.
|
|
@@ -88,10 +88,14 @@ pool_size = 10
|
|
|
88
88
|
| Var | Purpose |
|
|
89
89
|
|---|---|
|
|
90
90
|
| `BH_SQL_HOME` | Override state root (`~/.config/sql-harness` by default) |
|
|
91
|
+
| `BH_SQL_CONFIG_DIR` | Override config dir (defaults to `BH_SQL_HOME`) |
|
|
91
92
|
| `BH_SQL_CONFIG_FILE` | Override connections.toml path |
|
|
92
93
|
| `BH_SQL_AGENT_WORKSPACE` | Override agent-workspace directory |
|
|
93
94
|
| `BH_SQL_RUNTIME_DIR` | Override runtime state dir |
|
|
94
95
|
| `BH_SQL_TMP_DIR` | Override temp dir |
|
|
96
|
+
| `BH_SQL_ACTIVE_CONNECTION` | Active DSN zone for zone-scoped CLI (`save`/`run`/`scripts`/`skill list/show`) when no `-c` flag |
|
|
97
|
+
| `BH_SQL_ZONE_SKILLS=1` | `use_workspace()` auto-surfaces the zone's skills/scripts |
|
|
98
|
+
| `BH_SQL_ANALYTICS` | Analysis mode (CLI call counts + doc-read tracking + execution log). **Default ON**; set `0`/`off`/`false`/`no` to disable. Data: `$BH_SQL_HOME/analytics.json` (view with `sql-harness stats`) + `$BH_SQL_HOME/sql-harness.log` (append-only NDJSON: every executed command and its outcome) |
|
|
95
99
|
| `BH_PG_URL` | Used by integration tests (skipped if unset) |
|
|
96
100
|
| `BH_MYSQL_URL` | Used by integration tests (skipped if unset) |
|
|
97
101
|
|
|
@@ -115,7 +119,7 @@ To add another SSH host: append a `[[connections]]` block with `driver = "ssh"`
|
|
|
115
119
|
| `ModuleNotFoundError: No module named 'psycopg'` | Run `uv sync` |
|
|
116
120
|
| `sql-harness: command not found` | Use `uv run sql-harness ...` or activate `.venv` |
|
|
117
121
|
| `password authentication failed` | Check URL; use `${env:VAR}` for prod secrets |
|
|
118
|
-
| `unknown driver: 'X'` | Only `postgres`, `mysql`, `redis`, `sqlite` are built-in |
|
|
122
|
+
| `unknown driver: 'X'` | Only `postgres`, `mysql`, `redis`, `sqlite`, `ssh`/`ssh+password`/`ssh+key` are built-in |
|
|
119
123
|
| Heredoc mode complains `no workspace is active` | You forgot `use_workspace(name)` |
|
|
120
124
|
| Heredoc mode complains `name 'X' is not defined` | You forgot the import — all helpers are pre-imported, just call them |
|
|
121
125
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# sql-harness standalone project
|
|
2
|
+
|
|
3
|
+
## Quick reference for the maintainer
|
|
4
|
+
|
|
5
|
+
| Task | Command |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Install (editable dev mode) | `uv tool install --python 3.12 -e .` |
|
|
8
|
+
| Build wheel + sdist | `uv build` |
|
|
9
|
+
| Publish to TestPyPI | `uv publish --repository testpypi --token $TEST_PYPI_TOKEN` |
|
|
10
|
+
| Publish to PyPI | `uv publish --token $PYPI_TOKEN` |
|
|
11
|
+
| Refresh an agent's registered skill copy | `uv run sql-harness skill install` (or `skill > ~/.claude/skills/sql-harness/SKILL.md`) |
|
|
12
|
+
|
|
13
|
+
## Why the SKILL.md sync step exists
|
|
14
|
+
|
|
15
|
+
The in-package skill docs are **generated at build time**, not kept as
|
|
16
|
+
hand-synced copies. `pyproject.toml` `[tool.hatch.build.targets.wheel.force-include]`
|
|
17
|
+
maps the repo docs into the wheel:
|
|
18
|
+
|
|
19
|
+
- `SKILL.md` → `sql_harness/_skills/SKILL.md`
|
|
20
|
+
- `interaction-skills` → `sql_harness/_skills/interaction-skills`
|
|
21
|
+
- `agent-workspace/zones/meta/skills` → `sql_harness/_skills/zones-meta-skills`
|
|
22
|
+
|
|
23
|
+
The root `SKILL.md` is the **single source of truth** (v0.3.0 consolidation —
|
|
24
|
+
the old `skills/sql-harness/SKILL.md` and `src/sql_harness/SKILL.md` copies
|
|
25
|
+
were removed). After editing it, just `uv build` again. `sql-harness skill`
|
|
26
|
+
reads `sql_harness/_skills/SKILL.md` from the installed package and falls
|
|
27
|
+
back to the repo-root `SKILL.md` in a source checkout.
|
|
28
|
+
|
|
29
|
+
## Build outputs
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
$ uv build
|
|
33
|
+
Building source distribution...
|
|
34
|
+
Building wheel from source distribution...
|
|
35
|
+
Successfully built dist\sql_harness-0.3.0.tar.gz
|
|
36
|
+
Successfully built dist\sql_harness-0.3.0-py3-none-any.whl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The wheel contains the package modules + the `_skills/` doc bundle
|
|
40
|
+
(SKILL.md + interaction-skills tree + zones-meta-skills).
|
|
41
|
+
|
|
42
|
+
## Test before publish
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# standalone venv
|
|
46
|
+
uv sync
|
|
47
|
+
uv run pytest tests/unit -q # 80 tests must pass
|
|
48
|
+
uv run sql-harness --help # CLI works
|
|
49
|
+
uv run sql-harness skill # SKILL.md emit works
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Local PyPI dry-run (no upload)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv publish --dry-run
|
|
56
|
+
# → checks both dist files against upload.pypi.org/legacy/
|
|
57
|
+
# → errors with "no OIDC token" (expected; pass --token to actually publish)
|
|
58
|
+
```
|
|
@@ -162,6 +162,6 @@ monorepo access to `from sql_harness import ...` for testing — no breakage.
|
|
|
162
162
|
|
|
163
163
|
## See also
|
|
164
164
|
|
|
165
|
-
- `lab/sql_harness/install.md` — first-time install + troubleshooting
|
|
166
|
-
- `lab/sql_harness/AGENTS.md` — code priorities + extension recipe
|
|
167
|
-
- `lab/sql_harness/README.md` — user-facing quickstart
|
|
165
|
+
- `lab/sql_harness/docs/install.md` — first-time install + troubleshooting
|
|
166
|
+
- `lab/sql_harness/docs/AGENTS.md` — code priorities + extension recipe
|
|
167
|
+
- `lab/sql_harness/docs/README.md` — user-facing quickstart
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# JSON columns — MySQL (`JSON`)
|
|
2
|
+
|
|
3
|
+
MySQL 5.7+ has a native `JSON` type. PyMySQL serializes Python `dict`/`list` to `JSON`.
|
|
4
|
+
|
|
5
|
+
## Insert
|
|
6
|
+
|
|
7
|
+
SQLAlchemy handles the conversion when passing a dict as a parameter:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
execute("INSERT INTO events (data) VALUES (:d)", {"d": {"event": "click"}})
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Query
|
|
14
|
+
|
|
15
|
+
Use `JSON_EXTRACT(col, '$.path')` or the `->` / `->>` shorthand (MySQL 8.0+):
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
rows = query("SELECT data->>'$.event' AS event FROM events WHERE JSON_EXTRACT(data, '$.event') = :e",
|
|
19
|
+
{"e": "click"})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Note `->>` in MySQL returns `TEXT` (unquoted). In PostgreSQL `->>` returns `text` of a jsonb path. The `'$.event'` path syntax is MySQL-specific.
|
|
23
|
+
|
|
24
|
+
## Indexing
|
|
25
|
+
|
|
26
|
+
MySQL 8.0+ supports functional indexes on JSON expressions:
|
|
27
|
+
|
|
28
|
+
```sql
|
|
29
|
+
CREATE INDEX idx_events_event ON events ((CAST(data->>'$.event' AS CHAR(64))));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Pre-8.0: index a generated column instead.
|
|
33
|
+
|
|
34
|
+
## Gotchas
|
|
35
|
+
|
|
36
|
+
- `JSON_EXTRACT` returns JSON; `->>` returns the unquoted scalar. For comparisons, match the operator.
|
|
37
|
+
- `->>` shorthand requires MySQL 8.0+. On 5.7 use `JSON_UNQUOTE(JSON_EXTRACT(...))`.
|
|
38
|
+
- MySQL `JSON` does NOT auto-compress (unlike PG `jsonb` TOAST). Large docs blow up row size.
|
|
39
|
+
- Whitespace handling: MySQL normalizes on input but may reformat on output (PG normalizes the same way).
|
|
40
|
+
|
|
41
|
+
## See also
|
|
42
|
+
|
|
43
|
+
- `interaction-skills/postgres/json-columns.md` — PostgreSQL `jsonb` syntax (`@>`, `?`, no `$.path`)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# JSON columns — PostgreSQL (`jsonb`)
|
|
2
|
+
|
|
3
|
+
Use PostgreSQL's binary `jsonb` type (not `json` — `jsonb` is binary-comparable and indexable).
|
|
4
|
+
|
|
5
|
+
## Insert
|
|
6
|
+
|
|
7
|
+
`psycopg3` auto-serializes Python `dict`/`list` to `jsonb`:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
execute("INSERT INTO events (data) VALUES (:d)", {"d": {"event": "click", "ts": 1234}})
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Query
|
|
14
|
+
|
|
15
|
+
Use `->` (returns `jsonb`), `->>` (returns `text`), and the containment operator `@>`:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
rows = query("SELECT data->>'event' AS event FROM events WHERE data @> :filter",
|
|
19
|
+
{"filter": {"event": "click"}})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Indexing
|
|
23
|
+
|
|
24
|
+
`jsonb` supports GIN indexes on the whole document (fast for `@>` / `?` / `?&` / `?|`):
|
|
25
|
+
|
|
26
|
+
```sql
|
|
27
|
+
CREATE INDEX idx_events_data ON events USING GIN (data);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For paths you hit often, a btree expression index:
|
|
31
|
+
|
|
32
|
+
```sql
|
|
33
|
+
CREATE INDEX idx_events_event ON events ((data->>'event'));
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Gotchas
|
|
37
|
+
|
|
38
|
+
- Prefer `jsonb` over `json` — `json` is text-stored, no index support beyond expression indexes, whitespace-preserved.
|
|
39
|
+
- Comparing `data = '{"event":"click"}'::jsonb` works (SQLAlchemy normalizes JSON whitespace).
|
|
40
|
+
- `JSONB` aggregates (`jsonb_agg`, `jsonb_object_agg`) exist; for plain text JSON use `string_agg`.
|
|
41
|
+
- GIN on `jsonb` is large — measure before adding to high-write tables.
|
|
42
|
+
|
|
43
|
+
## See also
|
|
44
|
+
|
|
45
|
+
- `interaction-skills/postgres/specialized-indexes.md` — GIN/GiST/BRIN tradeoffs
|
|
46
|
+
- `interaction-skills/mysql/json-columns.md` — MySQL JSON syntax (different operators)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# PG Data Movement
|
|
2
|
+
|
|
3
|
+
Move data in/out/around PostgreSQL: logical dump + restore, bulk COPY, and
|
|
4
|
+
logical replication. (Schema migrations -- ALTER / CREATE under transaction
|
|
5
|
+
-- live in `migrations.md`; this file is for moving data across databases or
|
|
6
|
+
in/out of tables.)
|
|
7
|
+
|
|
8
|
+
## Detection
|
|
9
|
+
|
|
10
|
+
- **Move a whole database** to a new server -> `pg_dump` + `pg_restore`.
|
|
11
|
+
- **Bulk-load** a big table (>1M rows) -> `COPY` (10-100x faster than `INSERT`).
|
|
12
|
+
- **Live replication** of changes from one PG to another (zero-downtime
|
|
13
|
+
migration, read replica, aggregation) -> logical replication (publication
|
|
14
|
+
+ subscription).
|
|
15
|
+
- **One-shot export** for analysis / downstream pipeline -> `COPY` /
|
|
16
|
+
`psql \copy`.
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
|
|
20
|
+
### 1. Logical dump + restore (server-side CLI)
|
|
21
|
+
|
|
22
|
+
`pg_dump` and `pg_restore` live on the PG host, not in SQL. Run them via
|
|
23
|
+
the SSH workspace that hosts your PG:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
use_workspace("2026aliyun") # SSH workspace on the PG host
|
|
27
|
+
|
|
28
|
+
ssh_exec("pg_dump -Fc -d sourcedb -f /tmp/sourcedb.dump") # custom format, compressed
|
|
29
|
+
ssh_exec("createdb targetdb") # empty target
|
|
30
|
+
ssh_exec("pg_restore -d targetdb --no-owner --jobs=4 /tmp/sourcedb.dump")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Flags worth knowing:
|
|
34
|
+
|
|
35
|
+
- `-Fc` custom format (compressed, parallel-restoreable). `-Fp` plain SQL.
|
|
36
|
+
- `-s` schema-only / `-a` data-only. `-t public.orders` one table.
|
|
37
|
+
- `--no-owner` strips `OWNED BY` from objects (safer cross-cluster).
|
|
38
|
+
|
|
39
|
+
Cross-version note: `pg_dump` from the **target's** major is safest.
|
|
40
|
+
`pg_dump` 16 can read PG 15 / 14; restore with the target's own
|
|
41
|
+
`pg_restore`. Going major-down (PG 16 dump -> PG 14) is **not supported**.
|
|
42
|
+
|
|
43
|
+
### 2. Bulk COPY
|
|
44
|
+
|
|
45
|
+
`COPY` bypasses per-row parse/plan/execute -- it's the right tool for
|
|
46
|
+
anything >10k rows. `sql-harness`'s `execute` covers server-side file
|
|
47
|
+
reads; client-streaming needs psql's `\copy` via SSH.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
use_workspace("2026aliyun")
|
|
51
|
+
|
|
52
|
+
# (a) COPY TO -- export a query to a server-side CSV. psql \copy wraps
|
|
53
|
+
# COPY and lets a client feed/collect the stream.
|
|
54
|
+
ssh_exec(r"""psql -U $PG_USER sourcedb -c "\copy (SELECT id,email FROM users WHERE created_at>'2025-01-01') TO '/tmp/users.csv' WITH CSV HEADER" """)
|
|
55
|
+
ssh_download("/tmp/users.csv", "./users.csv") # pull it local
|
|
56
|
+
|
|
57
|
+
# (b) COPY FROM file -- server reads directly. Needs superuser OR
|
|
58
|
+
# `GRANT pg_read_server_files TO <role>` (PG 14+).
|
|
59
|
+
execute("COPY users (id, email) FROM '/var/lib/pg/imports/users.csv' WITH CSV HEADER")
|
|
60
|
+
|
|
61
|
+
# (c) COPY FROM STDIN -- client streams. `execute` can't do streaming;
|
|
62
|
+
# psql \copy from a *local* file is the practical path:
|
|
63
|
+
ssh_exec(r"""psql -h $PG_HOST -U $PG_USER targetdb -c "\copy users (id,email) FROM '/local/users.csv' WITH CSV HEADER" """)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Logical replication (publication + subscription)
|
|
67
|
+
|
|
68
|
+
For live, ongoing PG-to-PG replication of row changes. SQL-only on both
|
|
69
|
+
sides; the only server config is `wal_level = logical` on the source.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
use_workspace("source_pg")
|
|
73
|
+
# Source: declare what to publish. Default = all DML on the named tables.
|
|
74
|
+
execute("CREATE PUBLICATION pub_users FOR TABLE users")
|
|
75
|
+
# Add `WITH (publish = 'insert,update,delete,truncate')` to include TRUNCATEs.
|
|
76
|
+
|
|
77
|
+
use_workspace("target_pg")
|
|
78
|
+
# Target: subscribe. Initial copy runs, then changes stream.
|
|
79
|
+
execute("""CREATE SUBSCRIPTION sub_users
|
|
80
|
+
CONNECTION 'host=src.db port=5432 user=repl password=... dbname=source'
|
|
81
|
+
PUBLICATION pub_users""")
|
|
82
|
+
|
|
83
|
+
# Monitor catch-up from the TARGET:
|
|
84
|
+
for r in query("SELECT subname, status, received_lsn, latest_end_lsn FROM pg_stat_subscription"):
|
|
85
|
+
print(r) # status='streaming' + latest_end_lsn advancing = caught up
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Gotchas
|
|
89
|
+
|
|
90
|
+
- **Logical replication does NOT replicate**: schema (DDL), sequence state
|
|
91
|
+
(manually sync with `pg_dump --section=pre-data` on the source +
|
|
92
|
+
`pg_restore --section=post-data` on target), or `TRUNCATE` unless the
|
|
93
|
+
publication includes `publish='truncate'`.
|
|
94
|
+
- **Tables need a PK** (or explicit `REPLICA IDENTITY`) -- without it,
|
|
95
|
+
`UPDATE`/`DELETE` can't replicate.
|
|
96
|
+
- **`COPY FROM` file path** is server-side (`SHOW data_directory`).
|
|
97
|
+
Permissions: superuser OR `pg_read_server_files` (PG 14+).
|
|
98
|
+
- **`pg_dump` cross-major** only goes *up* (or equal). PG 14 dump -> PG 16
|
|
99
|
+
is fine; PG 16 dump -> PG 14 is rejected.
|
|
100
|
+
- **`pg_restore --jobs=N`** uses N cores but each job = one transaction
|
|
101
|
+
per table -- mid-restore failures leave the DB half-loaded. For
|
|
102
|
+
restartability, restore one table at a time.
|
|
103
|
+
- **Logical replication initial copy** puts write load on the source; for
|
|
104
|
+
very large DBs, stop writes briefly or use `ALTER SUBSCRIPTION ...
|
|
105
|
+
DISABLE` then re-enable after catching up.
|