sql-harness 0.2.4__tar.gz → 0.3.1__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.
Files changed (96) hide show
  1. {sql_harness-0.2.4 → sql_harness-0.3.1}/PKG-INFO +1 -1
  2. {sql_harness-0.2.4 → sql_harness-0.3.1}/SKILL.md +18 -5
  3. sql_harness-0.3.1/interaction-skills/mysql/json-columns.md +43 -0
  4. sql_harness-0.3.1/interaction-skills/postgres/json-columns.md +46 -0
  5. sql_harness-0.3.1/interaction-skills/postgres/migrations.md +105 -0
  6. sql_harness-0.3.1/interaction-skills/ssh/auth-and-tunnels.md +145 -0
  7. {sql_harness-0.2.4/interaction-skills → sql_harness-0.3.1/interaction-skills/ssh}/docker-via-ssh.md +41 -15
  8. {sql_harness-0.2.4 → sql_harness-0.3.1}/pyproject.toml +1 -5
  9. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/cli.py +5 -5
  10. {sql_harness-0.2.4 → sql_harness-0.3.1}/uv.lock +1 -1
  11. sql_harness-0.2.4/docker-services/coturn/README.md +0 -69
  12. sql_harness-0.2.4/docker-services/coturn/docker-compose.yml +0 -29
  13. sql_harness-0.2.4/docker-services/coturn/turnserver.conf +0 -36
  14. sql_harness-0.2.4/docker-services/mysql/docker-compose.yml +0 -35
  15. sql_harness-0.2.4/docker-services/redis/docker-compose.yml +0 -24
  16. sql_harness-0.2.4/interaction-skills/json-columns.md +0 -30
  17. sql_harness-0.2.4/practice/pgexercises/README.md +0 -63
  18. sql_harness-0.2.4/skills/sql-harness/SKILL.md +0 -1
  19. sql_harness-0.2.4/src/sql_harness/SKILL.md +0 -1
  20. {sql_harness-0.2.4 → sql_harness-0.3.1}/.claude-plugin/marketplace.json +0 -0
  21. {sql_harness-0.2.4 → sql_harness-0.3.1}/.claude-plugin/plugin.json +0 -0
  22. {sql_harness-0.2.4 → sql_harness-0.3.1}/.env.example +0 -0
  23. {sql_harness-0.2.4 → sql_harness-0.3.1}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  24. {sql_harness-0.2.4 → sql_harness-0.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {sql_harness-0.2.4 → sql_harness-0.3.1}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  26. {sql_harness-0.2.4 → sql_harness-0.3.1}/.gitignore +0 -0
  27. {sql_harness-0.2.4 → sql_harness-0.3.1}/AGENTS.md +0 -0
  28. {sql_harness-0.2.4 → sql_harness-0.3.1}/README.md +0 -0
  29. {sql_harness-0.2.4 → sql_harness-0.3.1}/agent-workspace/README.md +0 -0
  30. {sql_harness-0.2.4 → sql_harness-0.3.1}/agent-workspace/agent_helpers.example.py +0 -0
  31. {sql_harness-0.2.4 → sql_harness-0.3.1}/agent-workspace/agent_helpers.py +0 -0
  32. {sql_harness-0.2.4 → sql_harness-0.3.1}/agent-workspace/zones/example_pg/scripts/sh_demo_crud.py +0 -0
  33. {sql_harness-0.2.4 → sql_harness-0.3.1}/agent-workspace/zones/example_pg/skills/sh_demo-schema.md +0 -0
  34. {sql_harness-0.2.4 → sql_harness-0.3.1}/agent-workspace/zones/meta/skills/pool.md +0 -0
  35. {sql_harness-0.2.4 → sql_harness-0.3.1}/agent-workspace/zones/meta/skills/workspace.md +0 -0
  36. {sql_harness-0.2.4 → sql_harness-0.3.1}/docs/connections-toml-example.toml +0 -0
  37. {sql_harness-0.2.4 → sql_harness-0.3.1}/docs/headless-notes.md +0 -0
  38. {sql_harness-0.2.4 → sql_harness-0.3.1}/docs/maintainer.md +0 -0
  39. {sql_harness-0.2.4 → sql_harness-0.3.1}/docs/standalone-install.md +0 -0
  40. {sql_harness-0.2.4 → sql_harness-0.3.1}/install.md +0 -0
  41. {sql_harness-0.2.4/interaction-skills/references → sql_harness-0.3.1/interaction-skills}/aggregations-advanced.md +0 -0
  42. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/aggregations.md +0 -0
  43. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/encoding-and-charset.md +0 -0
  44. {sql_harness-0.2.4/interaction-skills/references → sql_harness-0.3.1/interaction-skills}/joins-advanced.md +0 -0
  45. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/joins.md +0 -0
  46. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/large-result-sets.md +0 -0
  47. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/migrations.md +0 -0
  48. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/pooling.md +0 -0
  49. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/postgres/btree-indexes.md +0 -0
  50. {sql_harness-0.2.4/interaction-skills → sql_harness-0.3.1/interaction-skills/postgres}/indexes-and-explain.md +0 -0
  51. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/postgres/plan-reading.md +0 -0
  52. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/postgres/slow-queries-joins.md +0 -0
  53. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/postgres/specialized-indexes.md +0 -0
  54. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/postgres/table-optimization.md +0 -0
  55. /sql_harness-0.2.4/interaction-skills/references/recursive-ctes-patterns.md → /sql_harness-0.3.1/interaction-skills/recursive-ctes-advanced.md +0 -0
  56. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/recursive-ctes.md +0 -0
  57. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/save-run-cycle.md +0 -0
  58. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/schema-introspection.md +0 -0
  59. {sql_harness-0.2.4/interaction-skills → sql_harness-0.3.1/interaction-skills/ssh}/ssh.md +0 -0
  60. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/timeouts-and-cancellation.md +0 -0
  61. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/transactions.md +0 -0
  62. /sql_harness-0.2.4/interaction-skills/references/window-functions-catalog.md → /sql_harness-0.3.1/interaction-skills/window-functions-advanced.md +0 -0
  63. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/window-functions.md +0 -0
  64. {sql_harness-0.2.4 → sql_harness-0.3.1}/interaction-skills/zone-skill-auto-surface.md +0 -0
  65. {sql_harness-0.2.4 → sql_harness-0.3.1}/skills/sql-harness/README.txt +0 -0
  66. {sql_harness-0.2.4 → sql_harness-0.3.1}/skills/sql-harness/references/install.md +0 -0
  67. {sql_harness-0.2.4 → sql_harness-0.3.1}/sql-harness +0 -0
  68. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/__init__.py +0 -0
  69. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/agent_loader.py +0 -0
  70. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/config.py +0 -0
  71. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/drivers/__init__.py +0 -0
  72. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/drivers/mysql.py +0 -0
  73. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/drivers/postgres.py +0 -0
  74. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/drivers/redis.py +0 -0
  75. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/drivers/sqlite.py +0 -0
  76. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/drivers/ssh.py +0 -0
  77. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/helpers.py +0 -0
  78. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/manager.py +0 -0
  79. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/paths.py +0 -0
  80. {sql_harness-0.2.4 → sql_harness-0.3.1}/src/sql_harness/run.py +0 -0
  81. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/__init__.py +0 -0
  82. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/conftest.py +0 -0
  83. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/integration/__init__.py +0 -0
  84. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/integration/test_mysql.py +0 -0
  85. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/integration/test_pg_optimization.py +0 -0
  86. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/integration/test_pgexercises_practice.py +0 -0
  87. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/integration/test_postgres.py +0 -0
  88. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/integration/test_ssh.py +0 -0
  89. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/__init__.py +0 -0
  90. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/test_agent_loader.py +0 -0
  91. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/test_cli.py +0 -0
  92. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/test_config.py +0 -0
  93. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/test_helpers.py +0 -0
  94. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/test_manager.py +0 -0
  95. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/test_paths.py +0 -0
  96. {sql_harness-0.2.4 → sql_harness-0.3.1}/tests/unit/test_ssh.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-harness
3
- Version: 0.2.4
3
+ Version: 0.3.1
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
@@ -33,31 +33,44 @@ PY
33
33
  Stuck-point mechanics — one file per topic, bare filenames only:
34
34
 
35
35
  - `interaction-skills/aggregations.md`
36
- - `interaction-skills/docker-via-ssh.md`
36
+ - `interaction-skills/aggregations-advanced.md`
37
37
  - `interaction-skills/encoding-and-charset.md`
38
- - `interaction-skills/indexes-and-explain.md`
39
38
  - `interaction-skills/joins.md`
40
- - `interaction-skills/json-columns.md`
39
+ - `interaction-skills/joins-advanced.md`
41
40
  - `interaction-skills/large-result-sets.md`
42
41
  - `interaction-skills/migrations.md`
43
42
  - `interaction-skills/pooling.md`
44
43
  - `interaction-skills/recursive-ctes.md`
44
+ - `interaction-skills/recursive-ctes-advanced.md`
45
45
  - `interaction-skills/save-run-cycle.md`
46
46
  - `interaction-skills/schema-introspection.md`
47
- - `interaction-skills/ssh.md`
48
47
  - `interaction-skills/timeouts-and-cancellation.md`
49
48
  - `interaction-skills/transactions.md`
50
49
  - `interaction-skills/window-functions.md`
50
+ - `interaction-skills/window-functions-advanced.md`
51
51
  - `interaction-skills/zone-skill-auto-surface.md`
52
52
 
53
- PG optimization depth — `interaction-skills/postgres/`:
53
+ PG-specific depth — `interaction-skills/postgres/`:
54
54
 
55
55
  - `interaction-skills/postgres/btree-indexes.md`
56
+ - `interaction-skills/postgres/indexes-and-explain.md`
57
+ - `interaction-skills/postgres/json-columns.md`
58
+ - `interaction-skills/postgres/migrations.md`
56
59
  - `interaction-skills/postgres/plan-reading.md`
57
60
  - `interaction-skills/postgres/slow-queries-joins.md`
58
61
  - `interaction-skills/postgres/specialized-indexes.md`
59
62
  - `interaction-skills/postgres/table-optimization.md`
60
63
 
64
+ MySQL-specific — `interaction-skills/mysql/`:
65
+
66
+ - `interaction-skills/mysql/json-columns.md`
67
+
68
+ SSH-specific — `interaction-skills/ssh/`:
69
+
70
+ - `interaction-skills/ssh/auth-and-tunnels.md`
71
+ - `interaction-skills/ssh/docker-via-ssh.md`
72
+ - `interaction-skills/ssh/ssh.md`
73
+
61
74
  ## Cross-DSN strategy skills
62
75
 
63
76
  Read these when working across multiple connections, tuning pool behavior, or operating remote hosts. Reached via `apply_skill("pool")` etc. from any active zone — `zones/meta/skills/` is the passive fallback layer (the legacy global `agent-workspace/skills/` was collapsed into a regular zone so `apply_skill()` has a single shape: active zone or meta).
@@ -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.
@@ -0,0 +1,145 @@
1
+ # SSH — auth 选型与端口转发
2
+
3
+ `ssh.md` 写了怎么用 helpers(`ssh_exec / upload / download`)和 CLI。本篇覆盖两块它没写的:(1) 选哪种 auth(key / password / ssh-agent / `ssh+key` / env fallback),(2) 用 SSH workspace 做端口转发把"躲在堡垒机后面的 DB"暴露成本地端口。
4
+
5
+ > 全文代码块已注入的 helper:`ssh_exec / ssh_upload / ssh_download / use_workspace / ssh_info`。
6
+
7
+ ## Detection
8
+
9
+ - `ssh_exec` 弹 `RuntimeError("...no tables...")` 或 paramiko `Auth failed` / `No valid authenticators` → 跳 [§1](#1-auth-选型)
10
+ - PG/MySQL workspace 直连 `internal-db-1.corp:5432` 超时,但 SSH workspace 能登堡垒机 → 跳 [§2](#2-端口转发tunneling)
11
+ - 远程主机有多个 SSH key 想用 ssh-agent 托管 → §1.3
12
+
13
+ ---
14
+
15
+ ## 1. Auth 选型
16
+
17
+ ### 1.1 三种 scheme 对照
18
+
19
+ `drivers/ssh.py:_parse_url` 支持的三种 URL 形式:
20
+
21
+ | URL 形式 | 何时用 | 取 key 顺序 |
22
+ |---|---|---|
23
+ | `ssh://user@host:port` | 默认 key auth | `?key=...` → `$BH_SSH_KEY` → `~/.ssh/id_ed25519` → `id_rsa` → `id_ecdsa`(`KEY_FALLBACK`) |
24
+ | `ssh+key://user@host:port?key=/abs/path` | key 不在默认路径 | URL 里的 `?key=` 单独覆盖 |
25
+ | `ssh+password://user:pw@host:port` | 跳板 / 临时主机,没配 key | URL 嵌入密码;也可用 `$BH_SSH_PASSWORD` |
26
+
27
+ **注意**:`ssh+password` 要求密码嵌入 URL。不想明文落 TOML,把密码放 `$BH_SSH_PASSWORD` 并用 `ssh://user@host`,但驱动**不会**自动回退到 env 密码(仅 key 走 env fallback)。这是常见混淆点。
28
+
29
+ ### 1.2 用哪个连接串
30
+
31
+ ```
32
+ 有 ed25519 key 在 ~/.ssh/ 且 authorized_keys 已加?
33
+ ├─ 是 → ssh://user@host (默认 key path)
34
+ └─ 否:
35
+ 有密码但没 key? → ssh+password://user:pw@host
36
+ key 在非默认路径? → ssh://user@host?key=/path/to/k
37
+ key 在 ssh-agent? → §1.3
38
+ ```
39
+
40
+ ### 1.3 ssh-agent
41
+
42
+ paramiko 默认不会主动连 ssh-agent。两条路:
43
+
44
+ ```python
45
+ # 路径 A(最常用):导出私钥文件路径,给驱动
46
+ # connections.toml:
47
+ # url = "ssh://app@bastion.corp?key=/home/me/.ssh/bastion_ed25519"
48
+ # 这是 ssh.md 默认推荐的。
49
+
50
+ # 路径 B(agent 转发):用 ssh-agent 把 key 放进 socket,paramiko 通过
51
+ # SSHAgentKey 适配。当前 ssh-harness 没适配;如需要 agent 转发,得
52
+ # 自己 patch driver。阶段性建议 → 用路径 A。
53
+ ```
54
+
55
+ ### 1.4 key 不被接受(`Auth failed` / `No valid authenticators`)
56
+
57
+ 8 成是这几个原因,按频率排:
58
+
59
+ 1. **key 文件权限太开放**(paramiko 严格遵循 OpenSSH 规则)
60
+ ```
61
+ chmod 600 ~/.ssh/id_ed25519
62
+ ```
63
+ 2. **authorized_keys 里这 key 被加过 passphrase**——paramiko 不弹 UI。
64
+ 解决:`ssh-keygen -p -f ~/.ssh/id_ed25519` 清掉 passphrase,或 `ssh-add ~/.ssh/id_ed25519`。
65
+ 3. **`?key=` 路径拼错**:相对路径相对 pwd——绝对路径最稳。
66
+ 4. **用户名错了**:`ssh://root@host` vs `ssh://ubuntu@host`,老 OpenSSH 默认禁 root。
67
+
68
+ 排查:
69
+
70
+ ```python
71
+ use_workspace("bastion")
72
+ print(ssh_info()) # 看 user/host/port/key_path 解析对不对
73
+ ssh_exec("whoami") # 验证通了
74
+ ```
75
+
76
+ ### 1.5 env fallback 默认 key 优先级
77
+
78
+ 看 `drivers/ssh.py:KEY_FALLBACK = ("~/.ssh/id_ed25519", "~/.ssh/id_rsa", "~/.ssh/id_ecdsa")`:
79
+
80
+ ```python
81
+ r = ssh_exec("ls -l ~/.ssh/id_ed25519 ~/.ssh/id_rsa 2>&1")
82
+ print(r["stdout"])
83
+ # 第一个存在的就是 driver 选的
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 2. 端口转发(Tunneling)
89
+
90
+ 业务 DB 不对公网开、只对堡垒机的内网暴露时。三条路:
91
+
92
+ ### 2.1 远程 `ssh -L`(堡垒机侧启端口)
93
+
94
+ ```python
95
+ use_workspace("bastion")
96
+ ssh_exec(
97
+ "ssh -fN -L 0.0.0.0:15432:internal-db.corp:5432 jump@internal-db.corp",
98
+ timeout=5,
99
+ )
100
+ r = ssh_exec("ss -lntp | grep 15432", timeout=5)
101
+ assert "15432" in r["stdout"]
102
+ ```
103
+
104
+ > 局限:转发启在堡垒机上,**你本机** psql 连不到堡垒机的 `15432`(除非堡垒机做了 reverse tunnel)。适合堡垒机里跑 docker-compose;不适合"我本地 psql 直连"。
105
+
106
+ ### 2.2 paramiko `open_channel` 反向隧道
107
+
108
+ 让堡垒机把 internal-db 的 5432 反向暴露到堡垒机的 15432,然后用堡垒机侧 `psql` / `mysql` 客户端工具调试:
109
+
110
+ ```python
111
+ use_workspace("bastion")
112
+ import paramiko
113
+ ws = current_workspace()
114
+ transport = ws.engine.client.get_transport()
115
+ reverse = transport.request_port_forward("0.0.0.0", 15432,
116
+ ("internal-db.corp", 5432))
117
+ try:
118
+ r = ssh_exec("PGPASSWORD=... psql -h 127.0.0.1 -p 15432 -U app -c 'select 1'")
119
+ finally:
120
+ transport.cancel_port_forward("0.0.0.0", 15432)
121
+ ```
122
+
123
+ ### 2.3 真要让你本机 SQL 客户端连
124
+
125
+ sql-harness 不内置 SSH SOCKS 代理;本机 `psql`/`TablePlus` 直连躲在堡垒机后的 DB,最干净是用 OpenSSH 客户端 `ssh -L 15432:internal-db.corp:5432 bastion`(sql-harness 之外的工具)。
126
+
127
+ 如果坚持在 sql-harness 里搞,用 §2.2 + 堡垒机启端口 + 跑 `psql`。
128
+
129
+ ---
130
+
131
+ ## Gotchas
132
+
133
+ - **paramiko 不会自动用 ssh-agent**——见 §1.3。
134
+ - **`ssh+password://` 必须嵌入密码**——`BH_SSH_PASSWORD` env 不会回退(仅 key 走 env fallback)。
135
+ - **堡垒机转发时 `ssh -fN` 在某些堡垒机受限**——`AllowTcpForwarding no`。失败就 §2.2 走 paramiko `request_port_forward`。
136
+ - **`request_port_forward` 返回的对象必须在 finally 里 cancel**,否则下次 `use_workspace` 复用 transport 报端口占用。
137
+ - **workspace disconnect 不自动清 tunnel**——硬规矩:tunnel 用 try/finally 包起来。
138
+ - **Windows → Linux 堡垒机的 key 权限**:`scp` 把 600 丢没的话 OpenSSH 服务端会拒。`scp` 后 `chmod 600` 或用 `ssh-copy-id`(用 sftp,保留权限)。
139
+ - **堡垒机禁用 SFTP subsystem**:`ssh_upload/ssh_download` 报 `RuntimeError("SFTP subsystem not available")`。回退 `tar -czf - | ssh host tar -xzf -` 经 `ssh_exec` 流式传。
140
+
141
+ ## See also
142
+
143
+ - `interaction-skills/ssh/ssh.md` — mechanic(helpers / CLI / 基础 gotchas)
144
+ - `interaction-skills/ssh/docker-via-ssh.md` — 通过 SSH workspace 把 docker-compose 部署到远程
145
+ - `docs/connections-toml-example.toml` — `ssh://` / `ssh+password://` / `ssh+key://` 三种真实示例
@@ -1,9 +1,10 @@
1
1
  # Docker-via-SSH — deploy a docker-compose service to a REMOTE host
2
2
 
3
- > **The root logic for all docker operations in sql-harness.** Templates live
4
- > locally (shipped with the skill); `sql-harness ssh` uploads them to the remote
5
- > host and runs `docker compose`. Everything runs **on the remote**, not locally.
6
- > Mirrors how you'd deploy by hand over SSH, but scripted + repeatable.
3
+ > **The root logic for all docker operations in sql-harness.** You bring the
4
+ > `docker-compose.yml` (and config files) locally; `sql-harness ssh` uploads
5
+ > them to the remote host and runs `docker compose`. Everything runs **on the
6
+ > remote**, not locally. Mirrors how you'd deploy by hand over SSH, but
7
+ > scripted + repeatable.
7
8
 
8
9
  ## Detection
9
10
 
@@ -34,11 +35,12 @@ self-contained (compose + config + data + logs all relative to that dir):
34
35
 
35
36
  ## The 4-step deploy loop
36
37
 
37
- ### Step 1 — write the compose locally (template shipped with the skill)
38
+ ### Step 1 — write the compose locally
38
39
 
39
- Templates live in `lab/sql_harness/docker-services/<service>/` (local repo).
40
- Each ships a `docker-compose.yml` + config files, all mounts relative to the
41
- service dir (`./data`, `./config` — NOT `./<service>/data`, which double-nests).
40
+ This mechanic assumes you have a local `docker-compose.yml` + any config files
41
+ you want to ship. The shipped template dir is gone in this build — bring your
42
+ own. All mounts must be **relative** to the service dir (`./data`, `./config`
43
+ — NOT `./<service>/data`, which double-nests).
42
44
 
43
45
  ### Step 2 — upload to the remote
44
46
 
@@ -122,15 +124,39 @@ the save→run cycle applied to infrastructure.
122
124
  yaml parse errors on the remote. Always strip CRLF before upload
123
125
  (`sed -i 's/\r$//' file.yml` or set `git config core.autocrlf false`).
124
126
 
125
- ## Service-specific gotchas
127
+ ## Service-specific gotchas — coturn
126
128
 
127
- coturn has its own war stories (NAT `external-ip`, 49152-65535 UDP port-range
128
- iptables OOM / exit-137, `network_mode: host`, config-as-dir vs config-as-file).
129
- Those live in `docker-services/coturn/README.md` — read before deploying coturn.
129
+ coturn (TURN/STUN relay) has war stories that bit production. All live here in the mechanic — not duplicated in the template dir.
130
+
131
+ **Key config knobs** (`turnserver.conf`):
132
+ - `realm` — your domain / public hostname (clients see this in their URL).
133
+ - `external-ip` — server's PUBLIC ip when behind NAT (Aliyun ECS is). Set as `AUTO` in shipped template; deploy script substitutes real public ip before `up -d`.
134
+ - `listening-ip=0.0.0.0` — bind interface (private is fine; only `external-ip` needs to be public).
135
+ - `listening-port=3478` (TURN/STUN), `tls-listening-port=5349` (TURNS over TLS).
136
+ - `min-port=49152` / `max-port=65535` — relay UDP range. Must match firewall.
137
+ - `user=<u>:<p>` + `lt-cred-mech` — long-term credential mechanism.
138
+
139
+ Test creds after deploy:
140
+ ```bash
141
+ turnutils_uclient -u webrtc -w webrtc <server-ip> 3478
142
+ ```
143
+
144
+ **Gotchas**:
145
+
146
+ - **`external-ip` behind NAT**: Aliyun ECS is behind NAT — coturn's `turnserver.conf` `external-ip=` must be the **public** ip, not the private one `listening-ip=0.0.0.0` binds. Templated as `AUTO`; substitute at deploy time (`sed -i "s/external-ip=AUTO/external-ip=$PUBIP/"`).
147
+
148
+ - **Firewall / security group**: docker publishes ports on the host, but Aliyun security group + host firewall (ufw/firewalld) must ALSO allow them. The coturn relay UDP range (49152-65535) is the easy one to forget. Open in security group, NOT via docker `ports:`.
149
+
150
+ - **Don't publish the relay port range via `ports:`** — publishing coturn's relay range `49152-65535` (~16k ports) makes Docker create ~16k iptables DNAT rules, which **OOM-kills the container start** (exit 137) on small hosts (Aliyun ECS). Use `network_mode: host` instead — coturn binds the range natively, zero iptables overhead. This is the official coturn/docker recommendation.
151
+
152
+ - **`network_mode: host` disables `ports:`** — the container binds host ports directly; don't also list `ports:` (compose rejects the combo). All port management moves to the host firewall + cloud security group.
153
+
154
+ - **Config-as-dir gotcha**: if `./turnserver.conf` doesn't exist locally when docker tries to mount it, Docker creates a **directory** at that path (not a file) → coturn fails to read config. Always upload the file BEFORE `up -d`, or pre-`touch` it on the remote.
155
+
156
+ **Why host networking (rationale)**: coturn's relay UDP range (49152-65535 = ~16k ports) must NOT be published via docker `-p` — Docker would create ~16k iptables DNAT rules, OOM-killing the daemon on small hosts. Host networking lets coturn bind the range natively, zero iptables overhead. Ports to open in host firewall / cloud security group: `3478/tcp+udp`, `5349/tcp+udp`, `49152-65535/udp` (relay range — open in security group, NOT in docker).
130
157
 
131
158
  ## See also
132
159
 
133
160
  - This file's "Detection" / "Approach" sections (decision P: the legacy strategy stub in `agent-workspace/zones/meta/skills/docker-deploy.md` was collapsed into the mechanic; no separate doc).
134
- - `interaction-skills/ssh.md` — the raw ssh_exec/upload/download helpers
135
- - `docker-services/<svc>/`shipped templates (coturn, mysql, redis, ...)
136
- - `docker-services/<svc>/README.md` — service-specific gotchas (e.g. coturn)
161
+ - `interaction-skills/ssh/ssh.md` — the raw ssh_exec/upload/download helpers
162
+ - `interaction-skills/ssh/auth-and-tunnels.md`SSH auth scheme 选型 + 端口转发
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql-harness"
3
- version = "0.2.4"
3
+ version = "0.3.1"
4
4
  description = "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
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -42,7 +42,6 @@ build-backend = "hatchling.build"
42
42
  # Mirrors browser-harness's setuptools src-layout. The package source is
43
43
  # under `src/sql_harness/`; the package name (top-level import) is `sql_harness`.
44
44
  packages = ["src/sql_harness"]
45
- artifacts = ["src/sql_harness/SKILL.md"]
46
45
 
47
46
  [tool.hatch.build.targets.wheel.force-include]
48
47
  # Bundle the full skill docs into the wheel so `sql-harness skill install`
@@ -53,9 +52,6 @@ artifacts = ["src/sql_harness/SKILL.md"]
53
52
  "SKILL.md" = "sql_harness/_skills/SKILL.md"
54
53
  "interaction-skills" = "sql_harness/_skills/interaction-skills"
55
54
  "agent-workspace/zones/meta/skills" = "sql_harness/_skills/zones-meta-skills"
56
- # Docker service templates (coturn/mysql/redis/...) — the docker-via-ssh mechanic
57
- # uploads these to ~/sql-harness/<svc>/ on a remote host.
58
- "docker-services" = "sql_harness/_skills/docker-services"
59
55
 
60
56
  [tool.pytest.ini_options]
61
57
  # Mirrors browser-harness's `pythonpath = ["src"]` — tests import `sql_harness`
@@ -343,17 +343,17 @@ def _packaged_skill_body() -> str | None:
343
343
 
344
344
  Mirrors browser-harness's run.py:_print_skill, which reads the packaged
345
345
  SKILL.md via importlib.resources so it works from both a source checkout
346
- and a built wheel. Falls back to the repo-level skills/sql-harness/SKILL.md.
346
+ and a built wheel. Falls back to the repo-level SKILL.md (root).
347
347
  """
348
- # 1. in-package: src/sql_harness/SKILL.md (force-included in the wheel)
348
+ # 1. in-package: sql_harness/_skills/SKILL.md (force-included in the wheel)
349
349
  try:
350
- ref = importlib_resources.files("sql_harness").joinpath("SKILL.md")
350
+ ref = importlib_resources.files("sql_harness").joinpath("_skills", "SKILL.md")
351
351
  if ref.is_file():
352
352
  return ref.read_text(encoding="utf-8")
353
353
  except (ModuleNotFoundError, FileNotFoundError):
354
354
  pass
355
- # 2. repo-level fallback (source checkout)
356
- repo = _package_root_dir() / "skills" / "sql-harness" / "SKILL.md"
355
+ # 2. repo-level fallback (source checkout): lab/sql_harness/SKILL.md
356
+ repo = _package_root_dir() / "SKILL.md"
357
357
  if repo.is_file():
358
358
  return repo.read_text(encoding="utf-8")
359
359
  return None
@@ -446,7 +446,7 @@ wheels = [
446
446
 
447
447
  [[package]]
448
448
  name = "sql-harness"
449
- version = "0.2.3"
449
+ version = "0.3.0"
450
450
  source = { editable = "." }
451
451
  dependencies = [
452
452
  { name = "paramiko" },
@@ -1,69 +0,0 @@
1
- # coturn (TURN/STUN server) — docker-services template
2
-
3
- Self-contained TURN/STUN relay for WebRTC clients. Deploy via
4
- `../../interaction-skills/docker-via-ssh.md`. Service dir on the remote:
5
- `~/sql-harness/coturn/`.
6
-
7
- ## Key config knobs (`turnserver.conf`)
8
-
9
- - `realm` — your domain / public hostname (clients see this in their URL).
10
- - `external-ip` — the server's PUBLIC ip when behind NAT (Aliyun ECS is).
11
- Set as `AUTO` in the shipped template; the deploy script substitutes the
12
- real public ip before `up -d`.
13
- - `listening-ip=0.0.0.0` — bind interface (private is fine; only `external-ip`
14
- needs to be public).
15
- - `listening-port=3478` (TURN/STUN), `tls-listening-port=5349` (TURNS over TLS).
16
- - `min-port=49152` / `max-port=65535` — relay UDP range. Must match the firewall.
17
- - `user=<u>:<p>` + `lt-cred-mech` — long-term credential mechanism.
18
-
19
- Test creds after deploy:
20
- ```bash
21
- turnutils_uclient -u webrtc -w webrtc <server-ip> 3478
22
- ```
23
-
24
- ## Service-specific gotchas (from production)
25
-
26
- - **`external-ip` behind NAT**: Aliyun ECS is behind NAT — coturn's
27
- `turnserver.conf` `external-ip=` must be the **public** ip, not the private
28
- one `listening-ip=0.0.0.0` binds. Templated as `AUTO`; substitute at deploy
29
- time (`sed -i "s/external-ip=AUTO/external-ip=$PUBIP/"`).
30
-
31
- - **Firewall / security group**: docker-publishes ports on the host, but Aliyun
32
- security group + host firewall (ufw/firewalld) must ALSO allow them. The
33
- coturn relay UDP range (49152-65535) is the easy one to forget. Open in
34
- security group, NOT via docker `ports:`.
35
-
36
- - **Don't publish the relay port range via `ports:`** — publishing coturn's
37
- relay range `49152-65535` (16k ports) makes Docker create ~16k iptables
38
- DNAT rules, which **OOM-kills the container start** (exit 137) on small
39
- hosts (Aliyun ECS). Use `network_mode: host` instead — coturn binds the
40
- range natively, zero iptables overhead. This is the official
41
- coturn/docker recommendation, and the shipped `docker-compose.yml` already
42
- does this.
43
-
44
- - **`network_mode: host` disables `ports:`** — the container binds host ports
45
- directly; don't also list `ports:` (compose rejects the combo). All port
46
- management moves to the host firewall + cloud security group.
47
-
48
- - **Config-as-dir gotcha**: if `./turnserver.conf` doesn't exist locally when
49
- docker tries to mount it, Docker creates a **directory** at that path
50
- (not a file) → coturn fails to read config. Always upload the file BEFORE
51
- `up -d`, or pre-`touch` it on the remote.
52
-
53
- ## Why host networking (rationale)
54
-
55
- coturn's relay UDP range (49152-65535 = ~16k ports) must NOT be published via
56
- docker `-p` — Docker would create ~16k iptables DNAT rules, OOM-killing the
57
- daemon on small hosts (Aliyun ECS etc.). Host networking lets coturn bind
58
- the range natively, zero iptables overhead. This is the official
59
- coturn/docker recommendation.
60
-
61
- Ports to open in the host firewall / cloud security group:
62
- - 3478/tcp + 3478/udp (TURN/STUN)
63
- - 5349/tcp + 5349/udp (TURNS over TLS)
64
- - 49152-65535/udp (relay range — open in security group, NOT in docker)
65
-
66
- ## Files in this template
67
-
68
- - `docker-compose.yml` — host-networked coturn container.
69
- - `turnserver.conf` — minimal config; edit `realm`/`external-ip`/`user` before deploy.
@@ -1,29 +0,0 @@
1
- # coturn via docker-compose — HOST NETWORKING (production-correct)
2
- #
3
- # Why host networking: coturn's relay UDP range (49152-65535 = ~16k ports)
4
- # must NOT be published via docker `-p` — Docker would create ~16k iptables
5
- # DNAT rules, OOM-killing the daemon on small hosts (Aliyun ECS etc.).
6
- # Host networking lets coturn bind the range natively, zero iptables overhead.
7
- # This is the official coturn/docker recommendation.
8
- #
9
- # Ports to open in the host firewall / cloud security group:
10
- # 3478/tcp + 3478/udp (TURN/STUN)
11
- # 5349/tcp + 5349/udp (TURNS over TLS)
12
- # 49152-65535/udp (relay range — open in security group, NOT in docker)
13
-
14
- services:
15
- coturn:
16
- image: coturn/coturn:latest
17
- container_name: coturn
18
- restart: unless-stopped
19
- network_mode: host # ← binds directly to host ports; no `ports:`
20
- volumes:
21
- # Relative to THIS dir (~/sql-harness/coturn/) — data + config cohesive.
22
- - ./turnserver.conf:/etc/turnserver.conf:ro
23
- - ./data:/var/lib/coturn
24
- command: ["-c", "/etc/turnserver.conf"]
25
- logging:
26
- driver: json-file
27
- options:
28
- max-size: "10m"
29
- max-file: "3"
@@ -1,36 +0,0 @@
1
- # coturn (TURN/STUN server) — minimal config
2
- # Deploy: see ../../interaction-skills/docker-via-ssh.md
3
- #
4
- # EDIT before deploy:
5
- # - realm: your domain or public hostname
6
- # - external-ip: the server's PUBLIC ip (needed when behind NAT)
7
- # - user: <user>:<pass> for TURN credentials (used by WebRTC clients)
8
- #
9
- # Test credentials after deploy:
10
- # turnutils_uclient -u webrtc -w webrtc <server-ip> 3478
11
-
12
- realm=turn.zhlx.dev
13
- listening-ip=0.0.0.0
14
- # external-ip must be the PUBLIC ip when the host is behind NAT (Aliyun ECS is).
15
- # Replace with the real public ip at deploy time (the deploy script does this).
16
- external-ip=AUTO
17
- listening-port=3478
18
- tls-listening-port=5349
19
-
20
- # Credentials (long-term mechanism)
21
- user=webrtc:webrtc
22
- lt-cred-mech
23
-
24
- # Relay port range — must match the UDP ports opened in docker-compose + firewall
25
- min-port=49152
26
- max-port=65535
27
-
28
- # Fingerprint + no-loopback-prevention niceties
29
- fingerprint
30
- no-cli
31
- no-tls
32
- no-dtls
33
-
34
- # Logging
35
- log-file=stdout
36
- simple-log
@@ -1,35 +0,0 @@
1
- # MySQL 8.0 — self-contained service template
2
- # Deploy: see ../../interaction-skills/docker-via-ssh.md
3
- # All mounts are relative to THIS dir (~/sql-harness/mysql/) — cohesive.
4
- # (Adapted from the upstream `dr` repo with the double-nesting bug fixed:
5
- # mounts are ./data not ./mysql/data.)
6
-
7
- services:
8
- mysql:
9
- image: mysql:8.0
10
- container_name: mysql
11
- restart: unless-stopped
12
- ports:
13
- - "3306:3306"
14
- environment:
15
- MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-223456}
16
- MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
17
- MYSQL_USER: ${MYSQL_USER:-app}
18
- MYSQL_PASSWORD: ${MYSQL_PASSWORD:-223456}
19
- TZ: Asia/Shanghai
20
- volumes:
21
- - ./data:/var/lib/mysql
22
- - ./conf:/etc/mysql/conf.d
23
- - ./init:/docker-entrypoint-initdb.d
24
- - ./logs:/var/log/mysql
25
- command:
26
- - --default-authentication-plugin=mysql_native_password
27
- - --character-set-server=utf8mb4
28
- - --collation-server=utf8mb4_unicode_ci
29
- - --explicit_defaults_for_timestamp=1
30
- - --lower_case_table_names=1
31
- healthcheck:
32
- test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-223456}"]
33
- timeout: 10s
34
- retries: 3
35
- start_period: 30s
@@ -1,24 +0,0 @@
1
- # Redis 7 — self-contained service template
2
- # Deploy: see ../../interaction-skills/docker-via-ssh.md
3
- # Mounts relative to THIS dir (~/sql-harness/redis/) — cohesive.
4
- # (Adapted from `dr` with double-nesting fixed: ./data not ./redis/data.)
5
-
6
- services:
7
- redis:
8
- image: redis:7-alpine
9
- container_name: redis
10
- restart: unless-stopped
11
- ports:
12
- - "6379:6379"
13
- volumes:
14
- - ./data:/data
15
- - ./redis.conf:/usr/local/etc/redis/redis.conf:ro
16
- command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
17
- sysctls:
18
- - net.core.somaxconn=511
19
- healthcheck:
20
- test: ["CMD", "redis-cli", "ping"]
21
- interval: 30s
22
- timeout: 10s
23
- retries: 3
24
- start_period: 10s
@@ -1,30 +0,0 @@
1
- # JSON columns
2
-
3
- ## PostgreSQL (`jsonb`)
4
-
5
- psycopg3 auto-serializes Python `dict`/`list` to `jsonb`. Query with `->`, `->>`, `@>`:
6
-
7
- ```python
8
- execute("INSERT INTO events (data) VALUES (:d)", {"d": {"event": "click", "ts": 1234}})
9
- rows = query("SELECT data->>'event' AS event FROM events WHERE data @> :filter",
10
- {"filter": {"event": "click"}})
11
- ```
12
-
13
- ## MySQL (`JSON`)
14
-
15
- PyMySQL auto-serializes Python `dict`/`list` to `JSON`. Query with `->`, `->>`, `JSON_EXTRACT`:
16
-
17
- ```python
18
- execute("INSERT INTO events (data) VALUES (%s)", {"event": "click"})
19
- # Wait — execute uses :name style. SQLAlchemy handles conversion:
20
- rows = query("SELECT data->>'$.event' AS event FROM events WHERE JSON_EXTRACT(data, '$.event') = :e",
21
- {"e": "click"})
22
- ```
23
-
24
- ## Gotchas
25
-
26
- - PostgreSQL `jsonb` is binary-comparable and indexable; `json` (text) is not. Prefer `jsonb`.
27
- - On PostgreSQL, comparing `data = '{"event": "click"}'::jsonb` works; comparing `data = '{"event":"click"}'` (without spaces) is the canonical form. SQLAlchemy normalizes.
28
- - MySQL `JSON` stores in a binary internal format; output may differ from input whitespace.
29
- - For nested-path queries on MySQL 5.7+, use `JSON_EXTRACT`/`->>`. The `->>` shorthand requires MySQL 8.0+.
30
- - Large JSON docs: store compressed (PG `TOAST` handles it; MySQL `JSON` does not auto-compress).
@@ -1,63 +0,0 @@
1
- # PG Exercises Practice
2
-
3
- 71 real [pgexercises](https://pgexercises.com/) problems organized as one `.sql` file per problem, with `cd`-schema fixtures and `run_sql_file` replay.
4
-
5
- ## File Format
6
-
7
- Each problem lives in a single `.sql` file. Conventions:
8
-
9
- - **Header line** = the question (human-readable, free text).
10
- - **Body lines** = the SQL answer.
11
- - **Comment lines** starting with `--` are ignored at replay time (use them to annotate).
12
- - File naming: `<category>_<NN>_<slug>.sql` — e.g. `basic_01_retrieve_everything.sql`, `joins_01.sql`. Flat directory, prefixed by category.
13
-
14
- The replay splitter (`_split_sql`) respects single-quoted strings, double-quoted identifiers, `--` and `/* */` comments, and PG `$tag$` dollar-quoting.
15
-
16
- ## Replay
17
-
18
- Always use `run_sql_file` to replay an answer. The helper splits the file on top-level `;` and runs every statement in one transaction on the active workspace; the final statement's result set comes back as `list[dict]`.
19
-
20
- 1. **Load schema + seed once** (PG only — uses `cd` schema with `facilities`, `members`, `bookings`):
21
-
22
- ```bash
23
- uv run sql-harness <<'PY'
24
- use_workspace("local_pg")
25
- run_sql_file("lab/sql_harness/practice/pgexercises/schema.sql")
26
- run_sql_file("lab/sql_harness/practice/pgexercises/seed.sql")
27
- PY
28
- ```
29
-
30
- Expected seed counts: `cd.facilities=9`, `cd.members=15`, `cd.bookings=35`.
31
-
32
- 2. **Replay an answer** from the heredoc, or from the shell:
33
-
34
- ```bash
35
- uv run sql-harness run_sql_file \
36
- practice/pgexercises/basic_01_retrieve_everything.sql -c local_pg
37
- ```
38
-
39
- ## Performance Tuning
40
-
41
- After a correct answer, wrap the body in `EXPLAIN (ANALYZE, BUFFERS) ...` via `explain_analyze` to get a real plan + actual timings:
42
-
43
- ```bash
44
- uv run sql-harness <<'PY'
45
- use_workspace("local_pg")
46
- explain_analyze("SELECT ... FROM cd.bookings WHERE ...")
47
- PY
48
- ```
49
-
50
- `explain_analyze` is PG-only. It returns one row per plan line (`{'QUERY PLAN': '<line>'}`). Supports `format='text'|'json'|'xml'|'yaml'`.
51
-
52
- For tuning depth, read:
53
-
54
- - `interaction-skills/postgres/plan-reading.md` — how to read an `EXPLAIN ANALYZE` plan.
55
- - `interaction-skills/postgres/btree-indexes.md` — the index to add when a scan shows up.
56
- - `interaction-skills/postgres/slow-queries-joins.md` — join-order and predicate pushdown.
57
- - `interaction-skills/postgres/table-optimization.md` — vacuum/analyze, stats targets.
58
-
59
- ## Adding Problems
60
-
61
- To add a new pgexercises problem, drop a new `<category>_<NN>_<slug>.sql` file into `practice/pgexercises/`, following the same header/body convention. Pick the next free `<NN>` in its category prefix (`basic_`, `joins_`, `aggregations`, `modifying`, `date`, `string`, `recursive`, …).
62
-
63
- Re-run `schema.sql` + `seed.sql` only when you change fixtures; existing problem files don't need re-loading.
@@ -1 +0,0 @@
1
- ../../SKILL.md
@@ -1 +0,0 @@
1
- ../../SKILL.md
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes