pythia-postgres 0.1.0__py3-none-any.whl

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 (51) hide show
  1. pythia_postgres/__init__.py +5 -0
  2. pythia_postgres/__main__.py +5 -0
  3. pythia_postgres/catalog.py +59 -0
  4. pythia_postgres/cli.py +8 -0
  5. pythia_postgres/engine.py +213 -0
  6. pythia_postgres/guard.py +54 -0
  7. pythia_postgres/queries/code-objects.sql +28 -0
  8. pythia_postgres/queries/columns.sql +15 -0
  9. pythia_postgres/queries/comment-current.sql +17 -0
  10. pythia_postgres/queries/current-schema.sql +5 -0
  11. pythia_postgres/queries/dependencies-catalog.sql +66 -0
  12. pythia_postgres/queries/dependents-catalog.sql +66 -0
  13. pythia_postgres/queries/dependents-text.sql +22 -0
  14. pythia_postgres/queries/function-body.sql +8 -0
  15. pythia_postgres/queries/grep.sql +30 -0
  16. pythia_postgres/queries/name-candidates.sql +32 -0
  17. pythia_postgres/queries/object-names.sql +56 -0
  18. pythia_postgres/queries/plpgsql-check-installed.sql +7 -0
  19. pythia_postgres/queries/plpgsql-check.sql +27 -0
  20. pythia_postgres/queries/plpgsql-unchecked.sql +16 -0
  21. pythia_postgres/queries/rehearsal-limits.sql +7 -0
  22. pythia_postgres/queries/resolve.sql +34 -0
  23. pythia_postgres/queries/row-set-count.sql +11 -0
  24. pythia_postgres/queries/row-set-sample.sql +8 -0
  25. pythia_postgres/queries/search-path-catalog.sql +7 -0
  26. pythia_postgres/queries/search-path-first.sql +13 -0
  27. pythia_postgres/queries/sequence-ddl.sql +16 -0
  28. pythia_postgres/queries/server-version.sql +6 -0
  29. pythia_postgres/queries/session-power.sql +13 -0
  30. pythia_postgres/queries/source.sql +28 -0
  31. pythia_postgres/queries/table-columns.sql +16 -0
  32. pythia_postgres/queries/table-constraints.sql +13 -0
  33. pythia_postgres/queries/table-extras.sql +43 -0
  34. pythia_postgres/queries/table-head.sql +18 -0
  35. pythia_postgres/read.py +299 -0
  36. pythia_postgres/rules.py +135 -0
  37. pythia_postgres/skills/pythia-pg-apply/SKILL.md +97 -0
  38. pythia_postgres/skills/pythia-pg-apply/reference/refusals.md +26 -0
  39. pythia_postgres/skills/pythia-pg-autopilot/SKILL.md +63 -0
  40. pythia_postgres/skills/pythia-pg-deploy/SKILL.md +73 -0
  41. pythia_postgres/skills/pythia-pg-explore/SKILL.md +63 -0
  42. pythia_postgres/skills/pythia-pg-impact/SKILL.md +57 -0
  43. pythia_postgres/skills/pythia-pg-verify/SKILL.md +56 -0
  44. pythia_postgres/skills/using-pythia-pg/SKILL.md +65 -0
  45. pythia_postgres/write.py +583 -0
  46. pythia_postgres-0.1.0.dist-info/METADATA +360 -0
  47. pythia_postgres-0.1.0.dist-info/RECORD +51 -0
  48. pythia_postgres-0.1.0.dist-info/WHEEL +5 -0
  49. pythia_postgres-0.1.0.dist-info/entry_points.txt +2 -0
  50. pythia_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
  51. pythia_postgres-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,135 @@
1
+ """PostgreSQL's lint rules: what PostgreSQL accepts and a reviewer would not.
2
+
3
+ Each rule reads the statement as the lexer does — strings and comments
4
+ blanked, function bodies read as code of their own — so a keyword inside a
5
+ literal never fires one. A rule that PostgreSQL itself enforces when the
6
+ statement runs (a RAISE with the wrong number of arguments, say) does not
7
+ belong here: the rehearsal already reports it.
8
+ """
9
+ import re
10
+
11
+ from .write import _DOLLAR, lex
12
+
13
+
14
+ def _line(text, pos):
15
+ return text.count("\n", 0, pos) + 1
16
+
17
+
18
+ def _bodies(stmt):
19
+ """(offset, body) of every dollar-quoted body: a function's code."""
20
+ out, i, code = [], 0, lex(stmt).uncommented
21
+ while True:
22
+ m = _DOLLAR.search(code, i)
23
+ if not m:
24
+ return out
25
+ end = code.find(m.group(0), m.end())
26
+ if end < 0:
27
+ return out
28
+ out.append((m.end(), stmt[m.end():end]))
29
+ i = end + len(m.group(0))
30
+
31
+
32
+ def _code_parts(stmt):
33
+ """(offset, masked code) of the statement and of each body inside it."""
34
+ return [(0, lex(stmt).masked.lower())] + [
35
+ (off, lex(body).masked.lower()) for off, body in _bodies(stmt)]
36
+
37
+
38
+ def _hits(stmt, pattern, message, parts=None):
39
+ out = []
40
+ for off, code in parts if parts is not None else [(0, lex(stmt).masked.lower())]:
41
+ for m in re.finditer(pattern, code):
42
+ out.append((_line(stmt, off + m.start()), message))
43
+ return out
44
+
45
+
46
+ def definer_search_path(stmt, group, run):
47
+ """CREATE FUNCTION ... SECURITY DEFINER, or ALTER FUNCTION ... SECURITY
48
+ DEFINER, with no SET search_path in the same statement."""
49
+ code = lex(stmt).masked.lower()
50
+ m = re.search(r"\bsecurity\s+definer\b", code)
51
+ if not m or re.search(r"\bset\s+search_path\b", code):
52
+ return []
53
+ return [(_line(stmt, m.start()),
54
+ "SECURITY DEFINER runs with its owner's rights, and without SET "
55
+ "search_path any role that can create objects in a schema on the "
56
+ "caller's path can put its own function in the way. Add "
57
+ "SET search_path = <schema>, pg_temp")]
58
+
59
+
60
+ def _ddl(pattern, message):
61
+ def check(stmt, group, run):
62
+ return _hits(stmt, pattern, message) if group == "structural" else []
63
+ return check
64
+
65
+
66
+ def select_star(stmt, group, run):
67
+ if group != "source":
68
+ return []
69
+ return _hits(stmt, r"\bselect\s+(?:distinct\s+)?\*", "SELECT * in stored code: a "
70
+ "view freezes the column list when it is created, and a "
71
+ "function breaks when a column is added — name the columns",
72
+ _code_parts(stmt))
73
+
74
+
75
+ def not_in_subquery(stmt, group, run):
76
+ return _hits(stmt, r"\bnot\s+in\s*\(\s*select\b", "NOT IN (SELECT ...) is "
77
+ "never true when the subquery returns a NULL — use NOT EXISTS",
78
+ _code_parts(stmt))
79
+
80
+
81
+ _NAME = r'((?:"[^"]+"|[a-z_][\w$]*)\s*\.\s*)?("[^"]+"|[a-z_][\w$]*)'
82
+
83
+
84
+ def _fold(name):
85
+ return name[1:-1] if name.startswith('"') else name.lower()
86
+
87
+
88
+ def table_comment(stmt, group, run):
89
+ if group != "structural":
90
+ return []
91
+ code = lex(stmt).uncommented
92
+ m = re.match(r"\s*create\s+(?:(?:global|local)\s+)?(?:unlogged\s+)?table\s+"
93
+ r"(?:if\s+not\s+exists\s+)?" + _NAME, code, re.I)
94
+ if not m:
95
+ return []
96
+ table = _fold(m.group(2))
97
+ for other in run:
98
+ c = re.match(r"\s*comment\s+on\s+table\s+" + _NAME, lex(other).uncommented, re.I)
99
+ if c and _fold(c.group(2)) == table:
100
+ return []
101
+ return [(_line(stmt, m.start(2)), f"table {table} has no COMMENT ON TABLE in this "
102
+ "change — say what a row is, in the same change that creates it")]
103
+
104
+
105
+ RULES = [
106
+ {"id": "pg-definer-search-path", "severity": "error",
107
+ "summary": "SECURITY DEFINER functions set their search_path",
108
+ "check": definer_search_path},
109
+ {"id": "pg-timestamp-without-tz", "severity": "warn",
110
+ "summary": "timestamp without time zone stores a wall-clock time, not a moment",
111
+ "check": _ddl(r"\btimestamp\b(?!\s*(?:\(\s*\d+\s*\))?\s*with\s+time\s+zone)",
112
+ "timestamp without time zone records a wall-clock time and "
113
+ "loses the moment — use timestamptz")},
114
+ {"id": "pg-char-n", "severity": "warn",
115
+ "summary": "char(n) pads with spaces",
116
+ "check": _ddl(r"\b(?:char|character)\s*\(\s*\d+\s*\)",
117
+ "char(n) pads with spaces and compares surprisingly — use "
118
+ "text or varchar(n)")},
119
+ {"id": "pg-money", "severity": "warn",
120
+ "summary": "money depends on lc_monetary",
121
+ "check": _ddl(r"\bmoney\b", "money is formatted by lc_monetary and rounds "
122
+ "to it — use numeric")},
123
+ {"id": "pg-serial", "severity": "info",
124
+ "summary": "identity columns over serial",
125
+ "check": _ddl(r"\b(?:small|big)?serial[248]?\b",
126
+ "serial is a sequence with loose ownership — prefer "
127
+ "GENERATED ALWAYS AS IDENTITY")},
128
+ {"id": "pg-select-star", "severity": "warn",
129
+ "summary": "no SELECT * in views and functions", "check": select_star},
130
+ {"id": "pg-not-in-subquery", "severity": "warn",
131
+ "summary": "NOT EXISTS over NOT IN (SELECT ...)", "check": not_in_subquery},
132
+ {"id": "pg-table-comment", "severity": "warn",
133
+ "summary": "every new table carries a comment in the same change",
134
+ "check": table_comment},
135
+ ]
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: pythia-pg-apply
3
+ description: Use when landing any change in PostgreSQL - creating or replacing a function, procedure, view or trigger, a COMMENT ON, DDL, a GRANT, an INSERT/UPDATE/DELETE, or restoring a previous version. Every write goes through pythia-pg apply - one statement, rehearsed, previewed, approved by the developer, then committed.
4
+ ---
5
+
6
+ # Apply through the gate
7
+
8
+ **Announce at start:** "Using pythia-pg-apply — preview first, then the developer approves."
9
+
10
+ **Phase:** Ask → Do — the preview is the Ask, the confirm is the Do
11
+
12
+ PostgreSQL runs DDL inside a transaction, so pythia-pg rehearses every
13
+ statement for real — runs it, checks what broke, rolls it back — and shows
14
+ the developer the result before anything is kept. psql, a driver script or
15
+ an MCP SQL tool would write with no snapshot, no rehearsal and no journal;
16
+ the guard denies them.
17
+
18
+ ## The Iron Law
19
+
20
+ ```
21
+ NO WRITE WITHOUT A PREVIEW THE DEVELOPER SAW AND APPROVED
22
+ ```
23
+
24
+ ## The Workflow
25
+
26
+ 1. **Impact first** (`pythia-pg-impact`) — the number was known before the
27
+ code was written.
28
+ 2. **One statement per file**, linted first: `pythia-pg lint <files>` — a
29
+ change's files in one run, since a COMMENT ON in one counts for the
30
+ table in another. Errors are fixed, not argued. `CREATE OR REPLACE
31
+ FUNCTION ...;` goes in its own `.sql` file. Several statements, DO blocks, CALL, psql meta-commands and
32
+ anything that cannot run in a transaction are refused — see
33
+ `reference/refusals.md` for each refusal and its way forward.
34
+ 3. **Preview:** `pythia-pg apply change.sql`. Nothing is kept. It prints:
35
+ - the diff of the definition before and after, as PostgreSQL prints both;
36
+ - the rehearsal: `nothing broke`, what it **would break**, or
37
+ `NOT VERIFIED` with the reason;
38
+ - the rows a DML statement touched, with a sample;
39
+ - warnings: another overload with the same name, `! DESTRUCTIVE`;
40
+ - a token, and the approval doors.
41
+ 4. **Relay the preview verbatim** — diff, rehearsal, rows, warnings. Do not
42
+ summarise away a broken dependent or a NOT VERIFIED line.
43
+ 5. **Ask, through the first door that works** — the next one unasked when
44
+ a door fails (the hook or the tool result says which):
45
+ 1. **AskUserQuestion** (Claude Code): run
46
+ `pythia-pg approve --card <token>` and ask with exactly its output,
47
+ options Approve / Reject, never with answers filled in. The answer
48
+ hook records the approval.
49
+ 2. **The `pythia_pg_approve` tool** (MCP; Codex starts here), with the
50
+ token. The developer answers its prompt.
51
+ 3. **The developer's terminal:** relay `pythia-pg approve <token>`.
52
+
53
+ Never mint a grant, pipe anything into `approve --hook`, or set
54
+ `PYTHIA_PG_CI` — the guard denies all three. A Reject means stop and ask
55
+ what should change.
56
+ 6. **Confirm:** `pythia-pg apply change.sql --confirm <token>`. It rehearses
57
+ again and commits only if the file, the object, the rows and the
58
+ verification still match what was approved; otherwise it rolls back and
59
+ names what moved — preview again and get the new token approved.
60
+ 7. **Verify** (`pythia-pg-verify`) before any "done".
61
+
62
+ ## Exit codes are the verdict
63
+
64
+ | Exit | Meaning | You say |
65
+ |---|---|---|
66
+ | 0 | written and verified (or NOT VERIFIED, said plainly) | what landed, and what was not checked |
67
+ | 1 | refused, nothing written | the reason, verbatim; never route around it |
68
+ | 3 | written **but broken** | "broken" first: the errors and the printed restore command; the developer chooses fix-forward or restore |
69
+
70
+ Never read the exit code through a pipe: `pythia-pg apply ... | tail` returns
71
+ tail's code.
72
+
73
+ ## Policy
74
+
75
+ `data_dml`, `structural` and `grants` are `deny` by default; `source` and
76
+ `comments` need the developer's approval. A deny is relayed with its reason.
77
+ `pythia-pg policy` shows the policy and whether rollback is real per group.
78
+ Loosening it (`policy set`) and `--yes` are the developer's acts, at a
79
+ console — never yours.
80
+
81
+ ## Restore is a write
82
+
83
+ `pythia-pg history <name>` lists every version the journal holds;
84
+ `pythia-pg journal restore <entry>` previews putting one back, and goes
85
+ through the same approval and `--confirm <token>`. Only definitions and
86
+ comments have a restore: rows, a dropped column or a revoked grant do not
87
+ come back.
88
+
89
+ ## Red Flags — STOP if you catch yourself thinking
90
+
91
+ | Thought | Reality |
92
+ |---------|---------|
93
+ | "It's a one-line fix, psql is faster" | Faster to write, and the only copy of what it replaced is gone. |
94
+ | "The developer said go ahead earlier" | They approve a token, after seeing its preview. |
95
+ | "Exit 3, but it mostly worked" | Exit 3 is broken. Say so first. |
96
+ | "I'll split the script and apply each part quietly" | Each part is previewed and approved on its own. |
97
+ | "The preview is long, I'll summarise" | The developer approves what they saw. Relay it. |
@@ -0,0 +1,26 @@
1
+ # Refusals and the way forward
2
+
3
+ `pythia-pg apply` refuses what it cannot rehearse, verify or undo honestly.
4
+ Each refusal exits 1 and writes nothing. Relay the reason, then offer the
5
+ way forward below — never run the statement another way.
6
+
7
+ | Refused | Why | Way forward |
8
+ |---|---|---|
9
+ | Several statements in one file | each is previewed, approved and journaled on its own | one file per statement, applied in order |
10
+ | `DO $$ ... $$` | an anonymous block can run anything; no policy group fits | put the logic in a function and apply that |
11
+ | `CALL proc(...)` | runs a body that can do anything | apply the statements it would run, or the developer runs it by hand |
12
+ | `WITH ... INSERT/UPDATE/DELETE` | hides its writes inside a query | a plain INSERT, UPDATE or DELETE |
13
+ | `SELECT`, `EXPLAIN`, `SHOW` | apply takes writes; a SELECT can call a function that writes | reads go through `ls`, `src`, `cols`, `grep` |
14
+ | `COPY` | reads or writes files outside the database | the developer runs it by hand |
15
+ | `CREATE INDEX CONCURRENTLY`, `VACUUM`, `ALTER SYSTEM`, `CREATE DATABASE` | cannot run inside a transaction: no rehearsal, no rollback | the developer runs it by hand, with a DBA watching |
16
+ | `CREATE TEMP ...` | dies with the connection, nothing left to verify | not needed through apply |
17
+ | `SET ROLE`, `SET SESSION AUTHORIZATION` | changes who verifies the write | connect as the right account |
18
+ | `BEGIN`, `COMMIT`, `SAVEPOINT`, `LOCK` | pythia-pg owns the transaction | drop the transaction control |
19
+ | psql meta-commands (`\i`, `\set`, `\gexec`) | pythia-pg runs SQL, not psql scripts | plain SQL in the file |
20
+ | a file that ends inside a quote or `$$` body | cannot be read safely | close the quote |
21
+ | a schema other than the connection's | writing across schemas is refused | `--conn` for the connection that owns it |
22
+ | a group set to `deny` | the policy | relay it; loosening is the developer's `policy set`, at a console |
23
+
24
+ When the gate refuses, the developer may still decide to run the statement
25
+ by hand. Hand them the file and, for a definition, the restore from
26
+ `pythia-pg history <name>` — never run it for them through psql.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: pythia-pg-autopilot
3
+ description: Use when the developer wants the agent to work on PostgreSQL without approving every write - "do it all without asking me", "just apply them", "tự động hoàn toàn". Asks once for a lease that names exactly what it covers, then reports every write landed under it.
4
+ ---
5
+
6
+ # Autopilot — one lease, never a blank cheque
7
+
8
+ **Announce at start:** "Using pythia-pg-autopilot — one request, covering only what the task needs."
9
+
10
+ **Phase:** Ask → Do — the lease card is the one Ask; every covered write is a Do that is reported
11
+
12
+ A lease is the developer's delegation, not yours. It covers one connection,
13
+ the statement groups it names, a time and a number of writes. Inside it,
14
+ covered writes land without a per-write approval — still snapshotted,
15
+ rehearsed, verified and journaled. Outside it, the normal gate applies.
16
+
17
+ ## The Iron Law
18
+
19
+ ```
20
+ ASK FOR THE NARROWEST LEASE THAT FITS, AND REPORT EVERY WRITE UNDER IT
21
+ ```
22
+
23
+ ## The Workflow
24
+
25
+ 1. **Settle the task first.** Open decisions are asked before any lease
26
+ (`using-pythia-pg`).
27
+ 2. **Request the narrowest lease that fits:**
28
+ `pythia-pg autopilot request --groups source,comments --hours 2
29
+ --max-writes 30 --note "rename the order functions"`. Name only the
30
+ groups the task needs; add `--destructive` only when the developer asked
31
+ for drops.
32
+ 3. **Ask once, through the doors** — exactly as `pythia-pg-apply` does, with
33
+ the request's 8-digit token: AskUserQuestion with the output of
34
+ `pythia-pg approve --card <token>`, then the `pythia_pg_approve` tool,
35
+ then the developer's terminal. `pythia-pg autopilot on` is the
36
+ developer's own command, at a console — never yours.
37
+ 4. **Work:** `pythia-pg apply <file>` and `pythia-pg deploy <files>` land
38
+ what the lease covers, with no `--confirm`. When one prints
39
+ `! not landed under autopilot — <why>`, that write needs its own
40
+ approval: relay its preview and ask, as `pythia-pg-apply` does.
41
+ 5. **Stop when the lease pauses.** A write that breaks something, or five
42
+ failures in a row, pauses it: show the developer the result before
43
+ anything else. Exit 3 is still "broken", never "done".
44
+ 6. **Report:** `pythia-pg autopilot report` lists every write with its
45
+ verdict and its restore. Relay it at the end, and `pythia-pg autopilot
46
+ off` when the task is done.
47
+
48
+ ## What a lease never covers
49
+
50
+ Another connection, or one marked `"protected": true` (or an `"env"` of
51
+ prod / production / prd / live) in the connections file; a group the card
52
+ did not name; a destructive statement the card did not include; an UPDATE
53
+ or DELETE of more than 1000 rows; anything after it expired, was spent,
54
+ revoked or paused.
55
+
56
+ ## Red Flags — STOP if you catch yourself thinking
57
+
58
+ | Thought | Reality |
59
+ |---------|---------|
60
+ | "I'll ask for all groups, to be safe" | Safe for whom? Ask for what the task needs. |
61
+ | "It paused, but the next write is unrelated" | Paused means stop and show the developer. |
62
+ | "Covered writes don't need reporting" | Every one is reported: `autopilot report`. |
63
+ | "I'll approve my own request" | Approval is the developer's, through a door. The guard denies the rest. |
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: pythia-pg-deploy
3
+ description: Use when landing several PostgreSQL statements together - a release script, a migration, DDL followed by the data change that needs it, or more than one file of changes. pythia-pg deploy rehearses the whole release in one transaction and lands all of it or none of it, under one approval.
4
+ ---
5
+
6
+ # Deploy a release as one transaction
7
+
8
+ **Announce at start:** "Using pythia-pg-deploy — the release is rehearsed as one, then approved as one."
9
+
10
+ **Phase:** Ask → Do — one card for the whole release is the Ask; the confirm is the Do
11
+
12
+ PostgreSQL runs DDL inside a transaction, so a whole release can be
13
+ rehearsed and committed as one: if statement 7 fails, statements 1–6 are
14
+ not left half-landed. Running a migration through psql gives that up, and
15
+ the snapshot, the rehearsal and the journal with it.
16
+
17
+ ## The Iron Law
18
+
19
+ ```
20
+ A RELEASE LANDS WHOLE, AS APPROVED, OR NOT AT ALL
21
+ ```
22
+
23
+ ## The Workflow
24
+
25
+ 1. **Impact first** (`pythia-pg-impact`) for every object the release
26
+ changes.
27
+ 2. **The scripts:** plain SQL, statements ending in `;`, in the order they
28
+ must run — one file or several, named in that order on the command line.
29
+ No `BEGIN`/`COMMIT` (pythia-pg owns the transaction), no `SET`, no psql
30
+ meta-commands, nothing that cannot run in a transaction
31
+ (`pythia-pg-apply` lists each refusal and its way forward).
32
+ 3. **Preview:** `pythia-pg deploy 01_schema.sql 02_data.sql`. Every
33
+ statement runs in order inside one transaction — an UPDATE of a column
34
+ added two statements earlier is rehearsed against that column — and
35
+ then everything rolls back. It prints one card: every statement, the rows
36
+ each DML statement touches, what the rehearsal of the whole broke or
37
+ could not verify, and one 10-digit token.
38
+ 4. **Relay the card verbatim**, then ask through the doors exactly as
39
+ `pythia-pg-apply` does — AskUserQuestion with the output of
40
+ `pythia-pg approve --card <token>`, then the `pythia_pg_approve` tool,
41
+ then `pythia-pg approve <token>` in the developer's terminal. A
42
+ statement's own token is refused: a release is approved as a whole.
43
+ 5. **Confirm:** `pythia-pg deploy 01_schema.sql 02_data.sql --confirm
44
+ <token>`, with the same files in the same order. It checks every
45
+ statement is byte-identical to what was approved before anything runs,
46
+ then runs the release again in one transaction and commits only if every
47
+ statement still matches — its object, its rows, its verification.
48
+ Otherwise the whole release rolls back, says which statement moved, and
49
+ the approval stays unspent.
50
+ 6. **Verify** (`pythia-pg-verify`): exit 0 clean, 3 written but broken.
51
+
52
+ ## Policy and undo
53
+
54
+ Every group in the release must be allowed: `structural` and `data_dml`
55
+ are `deny` until the developer runs `pythia-pg policy set <group> confirm`
56
+ at a console. After commit, functions, views and comments are restorable
57
+ statement by statement (`pythia-pg journal restore <entry>`, newest first,
58
+ as the report lists them); added columns and changed rows are not.
59
+
60
+ ## Red Flags — STOP if you catch yourself thinking
61
+
62
+ | Thought | Reality |
63
+ |---------|---------|
64
+ | "psql -f is how migrations run" | Not here: no rehearsal, no snapshot, no all-or-nothing. The guard denies it. |
65
+ | "I'll apply the statements one by one" | Then a failure halfway leaves a half release. Deploy them together. |
66
+ | "Just one more statement after approval" | The scripts changed: nothing runs. Preview again. |
67
+ | "It rolled back, so I'll retry with --yes" | `--yes` is the developer's flag. Find what moved, preview again. |
68
+
69
+ ## When NOT to use this skill
70
+
71
+ - One statement → `pythia-pg-apply`.
72
+ - A statement that cannot run in a transaction (`CREATE INDEX
73
+ CONCURRENTLY`, `VACUUM`) → the developer runs it by hand.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: pythia-pg-explore
3
+ description: Use when explaining, investigating or reading PostgreSQL objects - what a function does, where a table is used, why something fails, what a schema holds. Asks the live database through pythia-pg read commands, never a dump or memory.
4
+ ---
5
+
6
+ # Explore the live database
7
+
8
+ **Announce at start:** "Using pythia-pg-explore to read the live database."
9
+
10
+ **Phase:** Learn — facts from the catalog, before any opinion
11
+
12
+ A repository's SQL files drift from the database: a function edited in
13
+ place, an index added by hand, a view nobody exported. An explanation built
14
+ on the dump is an explanation of a database that does not exist.
15
+
16
+ ## The Iron Law
17
+
18
+ ```
19
+ EVERY FACT COMES FROM THE LIVE CATALOG, WITH THE COMMAND THAT SHOWED IT
20
+ ```
21
+
22
+ ## The commands
23
+
24
+ | Question | Command |
25
+ |---|---|
26
+ | Where am I, and where do unqualified names land? | `pythia-pg check` |
27
+ | Which objects match a name? | `pythia-pg ls "t_order%"` |
28
+ | What does this function / view / trigger say? | `pythia-pg src <name>` |
29
+ | What columns does this table or view have? | `pythia-pg cols <name>` |
30
+ | What statement would create it? | `pythia-pg ddl <name>` |
31
+ | Where is this text used in source? | `pythia-pg grep <text>` |
32
+ | What does it use? | `pythia-pg deps <name>` |
33
+ | What uses it? | `pythia-pg impact <name>` (see `pythia-pg-impact`) |
34
+ | What does the checker say is broken? | `pythia-pg errors [<name>]` |
35
+
36
+ `--json` gives structured output on every command. Line numbers in `src`
37
+ are PostgreSQL's own, so they match the line in an error message.
38
+
39
+ ## Reading the output honestly
40
+
41
+ - **Truncation is announced.** No `-- truncated` line means you saw
42
+ everything. With one, say so, or fetch the rest with `--offset` /
43
+ `--limit 0`.
44
+ - **`[text]` rows are leads, not proofs.** PostgreSQL's catalog does not
45
+ record what a PL/pgSQL body calls; pythia-pg finds those by name in the
46
+ source and marks them. Say which rows are which.
47
+ - **`-- blind spot:` lines are part of the answer.** Relay them; never call
48
+ a dependency list complete when one is printed.
49
+ - **`NOT VERIFIED` means nothing was checked there** (no `plpgsql_check`, or
50
+ a language it cannot read). It is never "no errors".
51
+
52
+ ## Red flags
53
+
54
+ | Thought | Reality |
55
+ |---------|---------|
56
+ | "The migration file shows the table" | The database may not. `pythia-pg cols`. |
57
+ | "`errors` printed nothing, so it is clean" | Only if no NOT VERIFIED line was printed. |
58
+ | "I'll open connections.json to see the host" | Never: `pythia-pg connections` lists it without passwords. |
59
+
60
+ ## Hand-off
61
+
62
+ Explaining only → done, with the commands quoted. A change follows →
63
+ `pythia-pg-impact` before any code is written.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: pythia-pg-impact
3
+ description: Use before proposing or writing any change to a PostgreSQL object - a function, procedure, trigger, view, table, column or type. Impact analysis comes first, because what depends on an object decides how careful the change must be.
4
+ ---
5
+
6
+ # Impact Before Change
7
+
8
+ **Announce at start:** "Using pythia-pg-impact to measure the blast radius first."
9
+
10
+ **Phase:** Learn — and the first Ask gate: a large blast radius reaches the developer before any code is written
11
+
12
+ A dropped column breaks every function that reads it — at call time, not
13
+ when the column goes, because PostgreSQL does not record what a PL/pgSQL
14
+ body uses. The cost of knowing first is one command.
15
+
16
+ ## The Iron Law
17
+
18
+ ```
19
+ NO CHANGE PROPOSED WITHOUT ITS IMPACT MEASURED FIRST
20
+ ```
21
+
22
+ ## The Workflow
23
+
24
+ 1. `pythia-pg impact <name> --depth 2` — everything that depends on it, as
25
+ a tree, ending with the summary:
26
+ `-- impact: N dependent objects (... matched by name as text ...)`.
27
+ 2. `pythia-pg deps <name> --depth 2` — the other direction, what it uses;
28
+ read it when the change touches calls or table access.
29
+ 3. `pythia-pg errors` — the baseline. Anything broken before the change
30
+ must not be blamed on it later.
31
+
32
+ ## Reading the numbers
33
+
34
+ | Result | What to do |
35
+ |---|---|
36
+ | 0 dependents | Say so, with the output as evidence, and proceed. |
37
+ | 1–9 dependents | List them to the developer alongside the proposal. |
38
+ | `! show this list to the developer` (10+, or one in another schema) | Show the tree and get an explicit go-ahead **before writing any code**. |
39
+ | `[text]` rows | Leads found by name in function source: name them as such. |
40
+ | `-- blind spot:` lines | Relay them. Never call the list complete. |
41
+
42
+ Overloads count separately: `f_order_total(integer)` and
43
+ `f_order_total(integer, boolean)` are two functions, and impact walks each.
44
+
45
+ ## Red Flags — STOP if you catch yourself thinking
46
+
47
+ | Thought | Reality |
48
+ |---------|---------|
49
+ | "It's just a small helper" | Helpers are the most-depended-on objects there are. |
50
+ | "I'll check impact after writing the code" | Then the number cannot change the design. Too late. |
51
+ | "The catalog shows no dependents, so nothing calls it" | Function bodies are not in the catalog — read the `[text]` rows and blind spots. |
52
+ | "apply's preview shows what breaks anyway" | That confirms a number you should already know; it is not discovery. |
53
+
54
+ ## Hand-off
55
+
56
+ Impact known and acceptable → write the change (one statement per file) →
57
+ `pythia-pg-apply` to land it through the gate.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: pythia-pg-verify
3
+ description: Use before claiming any PostgreSQL change is done, fixed, working or safe - after an apply, a restore, or a fix to a broken function. Evidence first - exit codes, the checker, the dependents - and "broken" or "not verified" said plainly.
4
+ ---
5
+
6
+ # Evidence before "done"
7
+
8
+ **Announce at start:** "Using pythia-pg-verify — evidence before done."
9
+
10
+ **Phase:** Do — the last step of every change
11
+
12
+ PostgreSQL accepts a PL/pgSQL function that references a column that does
13
+ not exist; it fails when called. "It applied" is not "it works", and a
14
+ claim of done without evidence costs the developer the debugging later.
15
+
16
+ ## The Iron Law
17
+
18
+ ```
19
+ NO COMPLETION CLAIM WITHOUT THE EVIDENCE PRINTED IN THIS SESSION
20
+ ```
21
+
22
+ ## The Checklist
23
+
24
+ 1. **Every write's exit code, unpiped.** 0 clean, 3 written-but-broken.
25
+ A 3 means the report starts with "broken".
26
+ 2. **`pythia-pg errors <name>`** for each object you touched, and for each
27
+ dependent that `impact` listed. Compare with the baseline taken before
28
+ the change: only new findings are yours.
29
+ 3. **`NOT VERIFIED` is reported as such.** Without `plpgsql_check`, or for a
30
+ language it cannot read, nothing was checked: say "applied, not
31
+ verified", never "clean".
32
+ 4. **The dependents.** If the change altered a signature, a column or a
33
+ return type, read the `[text]` rows from `impact` again — they are where
34
+ a call-time failure hides.
35
+ 5. **The journal.** `pythia-pg history <name>` shows the entry; quote its
36
+ restore command in the report for a change the developer may want back.
37
+
38
+ ## How to report
39
+
40
+ ```
41
+ Applied f_order_total (FUNCTION) — exit 0.
42
+ errors f_order_total: none (plpgsql_check).
43
+ Dependents: v_order_summary [catalog], f_order_paid [text] — errors: none.
44
+ Restore if needed: pythia-pg journal restore <entry>
45
+ ```
46
+
47
+ Say "broken" first when it is; say "not verified" plainly when it is.
48
+
49
+ ## Red Flags — STOP if you catch yourself thinking
50
+
51
+ | Thought | Reality |
52
+ |---------|---------|
53
+ | "apply printed Applied" | Read the exit code and the rehearsal lines, not the word. |
54
+ | "errors printed nothing" | With no NOT VERIFIED line, that is evidence. With one, it is not. |
55
+ | "The dependents compiled before" | PL/pgSQL is checked at call time; check them now. |
56
+ | "I'll verify after reporting" | Then the report is a guess. |
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: using-pythia-pg
3
+ description: Use when starting any conversation that touches PostgreSQL, PL/pgSQL, a schema, a function, a trigger or a view - it picks the right pythia-pg skill BEFORE any response or action, including clarifying questions. Fires on plain requests in any language - "build/add/fix/explain/review X", "lam/them/sua/tao/giai thich/kiem tra X" (Vietnamese, with or without diacritics). Also use whenever unsure which pythia-pg skill applies.
4
+ ---
5
+
6
+ # Using pythia-pg — the pocket handbook
7
+
8
+ **Phase:** Learn → Ask → Do — this page opens the right one
9
+
10
+ pythia-pg is pythia for PostgreSQL. Its one job here is consistency:
11
+ whether a session measures the blast radius and asks before it writes
12
+ should not depend on the agent's mood that day.
13
+
14
+ ## The Rule
15
+
16
+ If there is even a small chance a pythia-pg skill applies, open it **before
17
+ acting** — before answering, before exploring, before clarifying questions.
18
+ Opened the wrong one? Close it, no harm done.
19
+
20
+ ## Route by what the developer said
21
+
22
+ | The request sounds like | Open first | Then |
23
+ |---|---|---|
24
+ | "explain X / why / how does it work" ("giải thích") | `pythia-pg-explore` | |
25
+ | "is it safe to touch X" | `pythia-pg-impact` | |
26
+ | "change / fix / add X" ("sửa", "thêm") | `pythia-pg-impact` | write, then `pythia-pg-apply` |
27
+ | "land / apply this" — one statement | `pythia-pg-apply` | `pythia-pg-verify` |
28
+ | "run this migration / script / release" ("chạy script") | `pythia-pg-deploy` | `pythia-pg-verify` |
29
+ | "do it all without asking me" ("tự động hoàn toàn") | `pythia-pg-autopilot` — one lease, never a blank cheque | the task's own skills |
30
+ | "undo / restore X" | `pythia-pg-apply` (a restore is a write) | `pythia-pg-verify` |
31
+ | an error from PostgreSQL, a broken function | `pythia-pg-explore` (`errors`, `src`) | |
32
+ | about to say done / fixed / works | `pythia-pg-verify` — evidence first | |
33
+ | setup, connection failures, the hooks not running | `pythia-pg doctor`, then `pythia-pg check` | |
34
+
35
+ Priority when several apply: **process before construction** — impact
36
+ before writing, apply's preview before any write, verify before "done".
37
+ Announce the skill you opened ("Using pythia-pg-impact — ...") so the
38
+ developer sees which page you are on.
39
+
40
+ ## A request with open decisions
41
+
42
+ A change that leaves real decisions open — data kept or deleted, a column's
43
+ type, who may call a function, a security or audit trade-off — becomes
44
+ questions with options and a recommendation, asked **before** any code is
45
+ written. A new decision found mid-build stops the build.
46
+
47
+ ## Red flags
48
+
49
+ | Thought | Reality |
50
+ |---------|---------|
51
+ | "This task is simple" | Simple tasks hide decisions; ask the open ones first. |
52
+ | "I'll just run it in psql" | psql has no snapshot, no rehearsal, no journal. The guard denies it. |
53
+ | "I remember the schema" | The live catalog is the truth; a dump drifts. `pythia-pg ls`. |
54
+ | "I remember what that skill says" | Open the current page. |
55
+
56
+ ## Session state
57
+
58
+ The session-start brief ends with `STATE` lines: the connections (the
59
+ default is starred) and the write policy per group. Read them before
60
+ deciding whether a write is even allowed — `data_dml`, `structural` and
61
+ `grants` are `deny` until the developer says otherwise.
62
+
63
+ No STATE line this session means the hooks are not running and the guard
64
+ is off: say so to the developer before any write and ask them to run
65
+ `pythia-pg doctor`. Every rule still binds.