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,5 @@
1
+ """pythia for PostgreSQL: PL/pgSQL development for AI agents, over
2
+ pythia-core. Alpha — see README.md for what works today."""
3
+ __version__ = "0.1.0" # kept equal to pyproject.toml (tests/test_postgres.py)
4
+
5
+ from .engine import PostgresEngine # noqa: E402,F401 — needs __version__ first
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,59 @@
1
+ """Where pythia-pg's SQL lives and how it is run.
2
+
3
+ Every statement is a file in queries/, under pythia-core's contract:
4
+ pyformat binds, a header, and its binds declared here. tests/test_postgres.py
5
+ fails on drift in either direction, so queries/ stays reviewable without a
6
+ database.
7
+ """
8
+ import pathlib
9
+
10
+ from pythia_core.queries import load_query, run_query
11
+
12
+ QUERY_DIR = pathlib.Path(__file__).resolve().parent / "queries"
13
+
14
+ QUERY_BINDS = {
15
+ "server-version.sql": set(),
16
+ "current-schema.sql": set(),
17
+ "resolve.sql": {"s", "n"},
18
+ "object-names.sql": {"s", "p", "n"},
19
+ "source.sql": {"s", "n"},
20
+ "columns.sql": {"objid"},
21
+ "table-head.sql": {"objid"},
22
+ "table-columns.sql": {"objid"},
23
+ "table-constraints.sql": {"objid"},
24
+ "table-extras.sql": {"objid"},
25
+ "sequence-ddl.sql": {"objid"},
26
+ "grep.sql": {"s", "t"},
27
+ "dependents-catalog.sql": {"classid", "objid"},
28
+ "dependents-text.sql": {"pattern", "objid"},
29
+ "dependencies-catalog.sql": {"classid", "objid"},
30
+ "function-body.sql": {"objid"},
31
+ "name-candidates.sql": set(),
32
+ "plpgsql-check-installed.sql": set(),
33
+ "search-path-first.sql": {"s"},
34
+ "search-path-catalog.sql": set(),
35
+ "plpgsql-check.sql": {"s", "n"},
36
+ "plpgsql-unchecked.sql": {"s", "n"},
37
+ "code-objects.sql": {"s", "n"},
38
+ "comment-current.sql": {"s", "t", "c"},
39
+ "rehearsal-limits.sql": {"st", "lt"},
40
+ "session-power.sql": {"s"},
41
+ "row-set-count.sql": set(),
42
+ "row-set-sample.sql": set(),
43
+ }
44
+
45
+
46
+ def q(conn, name, **binds):
47
+ """(COLUMNS, rows) for one query file. Column names are upper case, as
48
+ every pythia engine reports them."""
49
+ cols, rows = run_query(conn, load_query(QUERY_DIR, name), binds)
50
+ return [str(c).upper() for c in cols], rows
51
+
52
+
53
+ def fold(name):
54
+ """A name as the catalog holds it: an unquoted identifier folds to lower
55
+ case; a double-quoted one is taken exactly, quotes removed."""
56
+ name = str(name).strip()
57
+ if len(name) >= 2 and name[0] == name[-1] == '"':
58
+ return name[1:-1].replace('""', '"')
59
+ return name.lower()
pythia_postgres/cli.py ADDED
@@ -0,0 +1,8 @@
1
+ """The `pythia-pg` command: pythia-core's command line, handed this engine."""
2
+ from pythia_core import cli
3
+
4
+ from .engine import PostgresEngine
5
+
6
+
7
+ def main(argv=None):
8
+ return cli.main(PostgresEngine(), argv)
@@ -0,0 +1,213 @@
1
+ """PostgreSQL, through psycopg 3, as a pythia-core engine.
2
+
3
+ It answers every tier of pythia_core.engine: connect (here), read (read.py),
4
+ write (write.py) — the write tier rehearsing each statement inside a
5
+ transaction, since PostgreSQL's DDL runs in one — release (a script split
6
+ where PostgreSQL would split it), lint (rules.py) and harness (guard.py).
7
+ """
8
+ import copy
9
+ import sys
10
+
11
+ from . import __version__, guard, read, write
12
+ from .catalog import QUERY_BINDS, QUERY_DIR, q # noqa: F401 — QUERY_* re-exported
13
+
14
+ CONFIG_REL = ".pythia/postgres/connections.json"
15
+ REQUIRED = ("host", "dbname", "user")
16
+
17
+
18
+ class PostgresEngine:
19
+ name = "postgres"
20
+ label = "PostgreSQL"
21
+ command = "pythia-pg"
22
+ env_prefix = "PYTHIA_PG"
23
+ version = __version__
24
+ bind_style = "pyformat"
25
+ default_port = 5432
26
+ # printed by `pythia-pg policy`: is rollback real, group by group?
27
+ rollback_notes = {
28
+ "source": "Yes — the definition is snapshotted before every write; a "
29
+ "new object's undo is a DROP.",
30
+ "comments": "Yes — the previous comment is snapshotted.",
31
+ "data_dml": "No. After commit only a backup or point-in-time recovery "
32
+ "brings rows back. Revalidation checks the rows; it is not "
33
+ "an undo.",
34
+ "structural": "No. A dropped column or table is gone; the rehearsal "
35
+ "shows what breaks before anything commits.",
36
+ "grants": "By hand — write the opposite GRANT or REVOKE.",
37
+ "session": "Not needed — it ends with the connection.",
38
+ }
39
+
40
+ # --- the connection's life ------------------------------------------------
41
+
42
+ def driver_errors(self):
43
+ try:
44
+ import psycopg
45
+ except ModuleNotFoundError:
46
+ sys.exit("The 'psycopg' package is required to connect: "
47
+ 'pip install "psycopg[binary]"')
48
+ return (psycopg.Error,)
49
+
50
+ def connect(self, entry):
51
+ import psycopg
52
+ gaps = [k for k in REQUIRED if not entry.get(k)]
53
+ if gaps:
54
+ sys.exit(f"Connection is missing {', '.join(map(repr, gaps))} — "
55
+ f"fill in {CONFIG_REL}.")
56
+ kw = {"host": entry["host"], "port": int(entry.get("port") or 5432),
57
+ "dbname": entry["dbname"], "user": entry["user"],
58
+ "connect_timeout": int(entry.get("connect_timeout") or 10),
59
+ "application_name": self.command}
60
+ # no password key means libpq's own sources (PGPASSWORD, ~/.pgpass)
61
+ if entry.get("password"):
62
+ kw["password"] = entry["password"]
63
+ if entry.get("sslmode"):
64
+ kw["sslmode"] = entry["sslmode"]
65
+ return psycopg.connect(**kw)
66
+
67
+ def session(self, conn, readonly):
68
+ if readonly:
69
+ # every transaction this command opens is READ ONLY, so
70
+ # PostgreSQL itself refuses a write — the second line of defence.
71
+ # dblink and foreign data wrappers reach past it; the agent
72
+ # account should not hold them.
73
+ conn.read_only = True
74
+ return conn
75
+
76
+ def close(self, conn):
77
+ conn.close()
78
+
79
+ def connect_failure(self, exc, conn_name, entry):
80
+ """Name the next step for the failures PostgreSQL has already
81
+ diagnosed precisely. Connection errors may carry no SQLSTATE, so the
82
+ server's message text is the fallback."""
83
+ text = str(exc)
84
+ state = getattr(exc, "sqlstate", None) or ""
85
+ low = text.lower()
86
+ who = entry.get("user") or "<the connecting user>"
87
+ head = f"Could not connect using connection {conn_name!r}: {text}"
88
+ if state == "28P01" or "password authentication failed" in low:
89
+ return (f"{head}\nWrong password for {who}. Check the entry in "
90
+ f"{CONFIG_REL} (or ~/.pgpass when it holds no password).")
91
+ if "no pg_hba.conf entry" in low:
92
+ return (f"{head}\nThe server refuses this host/user/database "
93
+ "combination. A superuser adds a line for it to "
94
+ "pg_hba.conf and reloads the configuration.")
95
+ if state == "28000" or "not permitted to log in" in low:
96
+ return (f"{head}\nThe role {who} may not log in. A superuser "
97
+ f"allows it:\n ALTER ROLE {who} LOGIN;")
98
+ if state == "3D000" or ("database" in low and "does not exist" in low):
99
+ return (f"{head}\nThe database {entry.get('dbname')!r} does not "
100
+ f"exist on this server. Check \"dbname\" in {CONFIG_REL}.")
101
+ if state == "53300" or "too many clients" in low:
102
+ return (f"{head}\nThe server is at max_connections. Retry later, "
103
+ "or ask a DBA which sessions hold the slots.")
104
+ return (f"{head}\nCheck host/port/dbname, the credentials, and that "
105
+ "the server is reachable from here. Use --conn NAME to try a "
106
+ "different entry.")
107
+
108
+ def schema_of(self, entry):
109
+ # an unquoted PostgreSQL identifier is lower case in the catalog
110
+ return str(entry.get("schema") or "public").lower()
111
+
112
+ def target_of(self, entry):
113
+ host = entry.get("host")
114
+ if not host:
115
+ return ""
116
+ target = f"{host}:{entry.get('port') or self.default_port}"
117
+ return f"{target}/{entry['dbname']}" if entry.get("dbname") else target
118
+
119
+ # --- what check asks --------------------------------------------------------
120
+
121
+ def server_version(self, conn):
122
+ version, database = q(conn, "server-version.sql")[1][0]
123
+ return f"PostgreSQL {version} — database {database}"
124
+
125
+ def current_schema(self, conn):
126
+ rows = q(conn, "current-schema.sql")[1]
127
+ return rows[0][0] if rows and rows[0] and rows[0][0] else None
128
+
129
+ # --- the read tier (read.py) -------------------------------------------------
130
+
131
+ def object_names(self, conn, schema, pattern, fetch):
132
+ return read.object_names(conn, schema, pattern, fetch)
133
+
134
+ def source(self, conn, schema, name):
135
+ return read.source(conn, schema, name)
136
+
137
+ def columns(self, conn, schema, name):
138
+ return read.columns(conn, schema, name)
139
+
140
+ def ddl(self, conn, schema, name):
141
+ return read.ddl(conn, schema, name)
142
+
143
+ def grep(self, conn, schema, text, fetch):
144
+ return read.grep(conn, schema, text, fetch)
145
+
146
+ def dependencies(self, conn, schema, name, depth):
147
+ return read.dependencies(conn, schema, name, depth)
148
+
149
+ def dependents(self, conn, schema, name, depth):
150
+ return read.dependents(conn, schema, name, depth)
151
+
152
+ def compile_errors(self, conn, schema, name):
153
+ return read.compile_errors(conn, schema, name)
154
+
155
+ # --- the write tier (write.py) -----------------------------------------------
156
+
157
+ def classify(self, text):
158
+ return write.classify(text)
159
+
160
+ def prepare_statement(self, text, group):
161
+ return write.prepare_statement(text, group)
162
+
163
+ def parse_object(self, stmt, group):
164
+ return write.parse_object(stmt, group)
165
+
166
+ def destructive_reason(self, stmt, group):
167
+ return write.destructive_reason(stmt, group)
168
+
169
+ def dml_probe(self, stmt):
170
+ return write.dml_probe(stmt)
171
+
172
+ def measure_row_set(self, conn, target, pred):
173
+ return write.measure_row_set(conn, target, pred)
174
+
175
+ def snapshot(self, conn, schema, group, ident):
176
+ return write.snapshot(conn, schema, group, ident)
177
+
178
+ def rehearse(self, conn, schema, group, stmt, ident, snapshot):
179
+ return write.rehearse(conn, schema, group, stmt, ident, snapshot)
180
+
181
+ def restore_statement(self, group, ident, before, outcome):
182
+ return write.restore_statement(group, ident, before, outcome)
183
+
184
+ def privilege_warning(self, conn, schema):
185
+ return write.privilege_warning(conn, schema)
186
+
187
+ # --- the release tier ----------------------------------------------------------
188
+
189
+ def split_script(self, text):
190
+ return write.split_script(text)
191
+
192
+ # --- the lint tier (rules.py) ---------------------------------------------------
193
+
194
+ def lint_rules(self):
195
+ from . import rules
196
+ return list(rules.RULES)
197
+
198
+ # --- the harness tier (guard.py) ---------------------------------------------
199
+
200
+ def raw_client_pattern(self):
201
+ return guard.CLIENTS
202
+
203
+ def write_sql_pattern(self):
204
+ return guard.WRITE_SQL
205
+
206
+ def script_pattern(self):
207
+ return guard.SCRIPT
208
+
209
+ def connections_template(self):
210
+ return copy.deepcopy(guard.TEMPLATE)
211
+
212
+ def skills_dir(self):
213
+ return guard.SKILLS_DIR
@@ -0,0 +1,54 @@
1
+ """The harness tier: what pythia-core's guard and install need to know
2
+ about PostgreSQL. The guard denies psql (and pgcli, pg_restore) when it
3
+ would write around the gate; reads through psql are left to the client's
4
+ own prompt.
5
+
6
+ The patterns read a shell command for what it names, never for what it
7
+ means, so a write spelled in a way they do not list gets through, and a
8
+ read that merely mentions DROP TABLE is denied. The gate behind them holds
9
+ either way (pythia-core's SECURITY.md).
10
+ """
11
+ import pathlib
12
+
13
+ CLIENTS = r"psql|pgcli|pg_restore"
14
+
15
+ # matched against the whole command, quoted SQL included
16
+ WRITE_SQL = "|".join((
17
+ r"\b(create|alter|drop|truncate|grant|revoke|reindex|cluster|vacuum|"
18
+ r"comment\s+on|insert\s+into|delete\s+from|merge\s+into|lock\s+table|"
19
+ r"refresh\s+materialized|security\s+label|reassign\s+owned|"
20
+ r"import\s+foreign\s+schema)\b",
21
+ r"\bupdate\s+(only\s+)?[\w\".]+(\s+(as\s+)?\w+)?\s+set\b",
22
+ r"\bdo\s*(\$|')", # an anonymous block
23
+ r"\bcall\s+[\w\".]+\s*\(",
24
+ r"\bcopy\b[^;|]*\bfrom\b", # COPY / \copy into a table
25
+ r"\bselect\b[^;]*\binto\b", # SELECT ... INTO makes a table
26
+ # functions that change state from a plain SELECT; sequences advance
27
+ # outside any transaction
28
+ r"\b(nextval|setval|pg_terminate_backend|pg_cancel_backend|lo_unlink|"
29
+ r"lo_import|dblink_exec|pg_reload_conf)\s*\(",
30
+ r"\\i(r|nclude(_relative)?)?\b", # psql running a script file
31
+ ))
32
+
33
+ # matched with quoted text emptied: what the shell hands the client
34
+ SCRIPT = "|".join((
35
+ r"(?-i:(^|\s)(-f\s*\S|--file(=|\s)))", # psql -f (case matters: -F is not)
36
+ r"(^|[^<])<(?![<(&])\s*\S", # stdin from a file, not <<, <(
37
+ r"\|\s*(\S*[/\\])?(psql|pgcli)\b", # a script piped into the client
38
+ r"\bpg_restore\b", # a dump into a database
39
+ ))
40
+
41
+ TEMPLATE = {
42
+ "default": "dev",
43
+ "dev": {
44
+ "host": "",
45
+ "port": 5432,
46
+ "dbname": "",
47
+ "user": "",
48
+ "password": "",
49
+ "schema": "APP_OWNER",
50
+ "sslmode": "",
51
+ },
52
+ }
53
+
54
+ SKILLS_DIR = pathlib.Path(__file__).resolve().parent / "skills"
@@ -0,0 +1,28 @@
1
+ -- Purpose: every function, procedure and view in a schema holding a name,
2
+ -- with the statement that recreates it and the signature that drops
3
+ -- it — what a write can replace, snapshotted before it runs
4
+ -- Binds: %(s)s schema, %(n)s object name (already folded)
5
+ -- Returns: UNIT, KIND, QUALIFIED, DEFINITION
6
+ select p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')'
7
+ as unit,
8
+ case p.prokind when 'p' then 'PROCEDURE' else 'FUNCTION' end as kind,
9
+ quote_ident(n.nspname) || '.' || quote_ident(p.proname) || '('
10
+ || pg_get_function_identity_arguments(p.oid) || ')' as qualified,
11
+ pg_get_functiondef(p.oid) as definition
12
+ from pg_proc p
13
+ join pg_namespace n on n.oid = p.pronamespace
14
+ where n.nspname = %(s)s
15
+ and p.proname = %(n)s
16
+ and p.prokind in ('f', 'p', 'w')
17
+ union all
18
+ select c.relname,
19
+ 'VIEW',
20
+ quote_ident(n.nspname) || '.' || quote_ident(c.relname),
21
+ 'CREATE OR REPLACE VIEW ' || quote_ident(n.nspname) || '.'
22
+ || quote_ident(c.relname) || E' AS\n' || pg_get_viewdef(c.oid, true)
23
+ from pg_class c
24
+ join pg_namespace n on n.oid = c.relnamespace
25
+ where n.nspname = %(s)s
26
+ and c.relname = %(n)s
27
+ and c.relkind = 'v'
28
+ order by 1
@@ -0,0 +1,15 @@
1
+ -- Purpose: the columns of one table or view, for `cols`
2
+ -- Binds: %(objid)s the relation's oid (from resolve.sql)
3
+ -- Returns: POSITION, COLUMN_NAME, DATA_TYPE, NULLABLE, COLUMN_DEFAULT, COMMENT
4
+ select a.attnum as position,
5
+ a.attname as column_name,
6
+ format_type(a.atttypid, a.atttypmod) as data_type,
7
+ case when a.attnotnull then 'NO' else 'YES' end as nullable,
8
+ coalesce(pg_get_expr(d.adbin, d.adrelid), '') as column_default,
9
+ coalesce(col_description(a.attrelid, a.attnum), '') as comment
10
+ from pg_attribute a
11
+ left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
12
+ where a.attrelid = %(objid)s::int8::oid
13
+ and a.attnum > 0
14
+ and not a.attisdropped
15
+ order by a.attnum
@@ -0,0 +1,17 @@
1
+ -- Purpose: the comment on a table, or on one of its columns, as it stands —
2
+ -- the snapshot a COMMENT statement is undone with
3
+ -- Binds: %(s)s schema, %(t)s table, %(c)s column, or NULL for the table
4
+ -- Returns: COMMENT (one row, NULL when there is none; no row when the table
5
+ -- does not exist)
6
+ select case when %(c)s::text is null
7
+ then obj_description(c.oid, 'pg_class')
8
+ else (select col_description(c.oid, a.attnum)
9
+ from pg_attribute a
10
+ where a.attrelid = c.oid
11
+ and a.attname = %(c)s
12
+ and not a.attisdropped)
13
+ end as comment
14
+ from pg_class c
15
+ join pg_namespace n on n.oid = c.relnamespace
16
+ where n.nspname = %(s)s
17
+ and c.relname = %(t)s
@@ -0,0 +1,5 @@
1
+ -- Purpose: where an unqualified name lands for this session: the first
2
+ -- schema on search_path that exists, NULL when none does
3
+ -- Binds: none
4
+ -- Returns: CURRENT_SCHEMA (one row)
5
+ select current_schema() as current_schema
@@ -0,0 +1,66 @@
1
+ -- Purpose: what the catalog (pg_depend) records one object as depending on
2
+ -- — directly, and for a table or view through its rewrite rules,
3
+ -- column defaults, constraints and triggers. System schemas are
4
+ -- left out.
5
+ -- Binds: %(classid)s the object's catalog (pg_class or pg_proc, as an oid)
6
+ -- %(objid)s the object's oid
7
+ -- Returns: SCHEMA, NAME, TYPE, NEXT_CLASSID, NEXT_OBJID
8
+ with parts as (
9
+ select %(classid)s::int8::oid as classid, %(objid)s::int8::oid as objid
10
+ union all
11
+ select 'pg_rewrite'::regclass::oid, r.oid
12
+ from pg_rewrite r
13
+ where %(classid)s::int8::oid = 'pg_class'::regclass::oid
14
+ and r.ev_class = %(objid)s::int8::oid
15
+ union all
16
+ select 'pg_attrdef'::regclass::oid, ad.oid
17
+ from pg_attrdef ad
18
+ where %(classid)s::int8::oid = 'pg_class'::regclass::oid
19
+ and ad.adrelid = %(objid)s::int8::oid
20
+ union all
21
+ select 'pg_constraint'::regclass::oid, k.oid
22
+ from pg_constraint k
23
+ where %(classid)s::int8::oid = 'pg_class'::regclass::oid
24
+ and k.conrelid = %(objid)s::int8::oid
25
+ union all
26
+ select 'pg_trigger'::regclass::oid, t.oid
27
+ from pg_trigger t
28
+ where %(classid)s::int8::oid = 'pg_class'::regclass::oid
29
+ and t.tgrelid = %(objid)s::int8::oid
30
+ and not t.tgisinternal
31
+ ), refs as (
32
+ select distinct d.refclassid, d.refobjid
33
+ from pg_depend d
34
+ join parts p on d.classid = p.classid and d.objid = p.objid
35
+ where d.deptype in ('n', 'a')
36
+ )
37
+ select n.nspname as schema,
38
+ c.relname as name,
39
+ case c.relkind when 'r' then 'TABLE'
40
+ when 'p' then 'PARTITIONED TABLE'
41
+ when 'v' then 'VIEW'
42
+ when 'm' then 'MATERIALIZED VIEW'
43
+ when 'S' then 'SEQUENCE'
44
+ else 'FOREIGN TABLE' end as type,
45
+ 'pg_class'::regclass::oid as next_classid,
46
+ c.oid as next_objid
47
+ from refs
48
+ join pg_class c on refs.refclassid = 'pg_class'::regclass::oid
49
+ and c.oid = refs.refobjid
50
+ join pg_namespace n on n.oid = c.relnamespace
51
+ where c.relkind in ('r', 'p', 'v', 'm', 'S', 'f')
52
+ and c.oid <> %(objid)s::int8::oid
53
+ and n.nspname not in ('pg_catalog', 'information_schema')
54
+ union all
55
+ select n.nspname,
56
+ p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')',
57
+ case p.prokind when 'p' then 'PROCEDURE' else 'FUNCTION' end,
58
+ 'pg_proc'::regclass::oid,
59
+ p.oid
60
+ from refs
61
+ join pg_proc p on refs.refclassid = 'pg_proc'::regclass::oid
62
+ and p.oid = refs.refobjid
63
+ join pg_namespace n on n.oid = p.pronamespace
64
+ where p.oid <> %(objid)s::int8::oid
65
+ and n.nspname not in ('pg_catalog', 'information_schema')
66
+ order by 3, 2
@@ -0,0 +1,66 @@
1
+ -- Purpose: what the catalog (pg_depend) records as depending on one object:
2
+ -- views and materialized views through their rewrite rules,
3
+ -- SQL-standard function bodies, triggers, constraints, column
4
+ -- defaults and row-level policies. PL/pgSQL bodies are not here —
5
+ -- dependents-text.sql looks for those.
6
+ -- Binds: %(classid)s the object's catalog (pg_class or pg_proc, as an oid)
7
+ -- %(objid)s the object's oid
8
+ -- Returns: SCHEMA, NAME, TYPE, NEXT_CLASSID, NEXT_OBJID (NULL where the walk
9
+ -- stops: nothing depends on a trigger or a constraint)
10
+ with d as (
11
+ select distinct classid, objid
12
+ from pg_depend
13
+ where refclassid = %(classid)s::int8::oid
14
+ and refobjid = %(objid)s::int8::oid
15
+ and deptype in ('n', 'a')
16
+ )
17
+ select n.nspname as schema,
18
+ c.relname as name,
19
+ case c.relkind when 'm' then 'MATERIALIZED VIEW' else 'VIEW' end as type,
20
+ 'pg_class'::regclass::oid as next_classid,
21
+ c.oid as next_objid
22
+ from d
23
+ join pg_rewrite r on d.classid = 'pg_rewrite'::regclass::oid and r.oid = d.objid
24
+ join pg_class c on c.oid = r.ev_class
25
+ join pg_namespace n on n.oid = c.relnamespace
26
+ where c.oid <> %(objid)s::int8::oid
27
+ union all
28
+ select n.nspname,
29
+ p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')',
30
+ case p.prokind when 'p' then 'PROCEDURE' else 'FUNCTION' end,
31
+ 'pg_proc'::regclass::oid,
32
+ p.oid
33
+ from d
34
+ join pg_proc p on d.classid = 'pg_proc'::regclass::oid and p.oid = d.objid
35
+ join pg_namespace n on n.oid = p.pronamespace
36
+ union all
37
+ select n.nspname, t.tgname || ' on ' || c.relname, 'TRIGGER', null, null
38
+ from d
39
+ join pg_trigger t on d.classid = 'pg_trigger'::regclass::oid and t.oid = d.objid
40
+ join pg_class c on c.oid = t.tgrelid
41
+ join pg_namespace n on n.oid = c.relnamespace
42
+ where not t.tgisinternal
43
+ union all
44
+ select n.nspname, c.relname, 'TABLE (CONSTRAINT ' || k.conname || ')', null, null
45
+ from d
46
+ join pg_constraint k on d.classid = 'pg_constraint'::regclass::oid
47
+ and k.oid = d.objid
48
+ join pg_class c on c.oid = k.conrelid
49
+ join pg_namespace n on n.oid = c.relnamespace
50
+ where k.conrelid <> %(objid)s::int8::oid
51
+ union all
52
+ select n.nspname, c.relname, 'TABLE (DEFAULT ' || a.attname || ')', null, null
53
+ from d
54
+ join pg_attrdef ad on d.classid = 'pg_attrdef'::regclass::oid and ad.oid = d.objid
55
+ join pg_class c on c.oid = ad.adrelid
56
+ join pg_attribute a on a.attrelid = ad.adrelid and a.attnum = ad.adnum
57
+ join pg_namespace n on n.oid = c.relnamespace
58
+ where c.oid <> %(objid)s::int8::oid
59
+ union all
60
+ select n.nspname, pol.polname || ' on ' || c.relname, 'POLICY', null, null
61
+ from d
62
+ join pg_policy pol on d.classid = 'pg_policy'::regclass::oid and pol.oid = d.objid
63
+ join pg_class c on c.oid = pol.polrelid
64
+ join pg_namespace n on n.oid = c.relnamespace
65
+ where c.oid <> %(objid)s::int8::oid
66
+ order by 3, 2
@@ -0,0 +1,22 @@
1
+ -- Purpose: functions whose source text holds a name as a whole word, any
2
+ -- case — the references pg_depend does not record (PL/pgSQL
3
+ -- bodies, SQL functions with a string body). A match is a lead, not
4
+ -- a proof: a column or a variable can share the name.
5
+ -- Binds: %(pattern)s the name as a whole-word regular expression, escaped
6
+ -- by the engine
7
+ -- %(objid)s the object itself, left out
8
+ -- Returns: SCHEMA, NAME, TYPE, NEXT_CLASSID, NEXT_OBJID
9
+ select n.nspname as schema,
10
+ p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' as name,
11
+ case p.prokind when 'p' then 'PROCEDURE' else 'FUNCTION' end as type,
12
+ 'pg_proc'::regclass::oid as next_classid,
13
+ p.oid as next_objid
14
+ from pg_proc p
15
+ join pg_namespace n on n.oid = p.pronamespace
16
+ join pg_language l on l.oid = p.prolang
17
+ where l.lanname in ('plpgsql', 'sql')
18
+ and n.nspname not in ('pg_catalog', 'information_schema')
19
+ and n.nspname not like 'pg\_%%'
20
+ and p.oid <> %(objid)s::int8::oid
21
+ and p.prosrc ~* %(pattern)s
22
+ order by 1, 2
@@ -0,0 +1,8 @@
1
+ -- Purpose: a function's language and its source as stored — the text the
2
+ -- engine reads for references pg_depend does not record
3
+ -- Binds: %(objid)s the function's oid
4
+ -- Returns: LANGUAGE, BODY
5
+ select l.lanname as language, p.prosrc as body
6
+ from pg_proc p
7
+ join pg_language l on l.oid = p.prolang
8
+ where p.oid = %(objid)s::int8::oid
@@ -0,0 +1,30 @@
1
+ -- Purpose: every function, procedure or view in a schema whose source holds
2
+ -- a text, any case, with that source as PostgreSQL prints it back
3
+ -- (the engine picks out the lines, numbered as `src` numbers them)
4
+ -- Binds: %(s)s schema, %(t)s the text
5
+ -- Returns: UNIT, TYPE, DEFINITION
6
+ select unit, type, definition
7
+ from (select p.proname || '(' || pg_get_function_identity_arguments(p.oid)
8
+ || ')' as unit,
9
+ case p.prokind when 'p' then 'PROCEDURE' else 'FUNCTION' end
10
+ as type,
11
+ pg_get_functiondef(p.oid) as definition
12
+ from pg_proc p
13
+ join pg_namespace n on n.oid = p.pronamespace
14
+ where n.nspname = %(s)s
15
+ and p.prokind in ('f', 'p', 'w')
16
+ union all
17
+ select c.relname,
18
+ case c.relkind when 'v' then 'VIEW' else 'MATERIALIZED VIEW' end,
19
+ -- the same text source.sql prints, so line numbers agree
20
+ case c.relkind
21
+ when 'v' then 'CREATE OR REPLACE VIEW '
22
+ else 'CREATE MATERIALIZED VIEW ' end
23
+ || quote_ident(n.nspname) || '.' || quote_ident(c.relname)
24
+ || E' AS\n' || pg_get_viewdef(c.oid, true)
25
+ from pg_class c
26
+ join pg_namespace n on n.oid = c.relnamespace
27
+ where n.nspname = %(s)s
28
+ and c.relkind in ('v', 'm')) g
29
+ where strpos(lower(definition), lower(%(t)s)) > 0
30
+ order by type, unit
@@ -0,0 +1,32 @@
1
+ -- Purpose: every relation and function outside the system schemas, by bare
2
+ -- name — what a word in a function body could be referring to
3
+ -- Binds: none
4
+ -- Returns: BARE_NAME, SCHEMA, NAME, TYPE, NEXT_CLASSID, NEXT_OBJID
5
+ select c.relname as bare_name,
6
+ n.nspname as schema,
7
+ c.relname as name,
8
+ case c.relkind when 'r' then 'TABLE'
9
+ when 'p' then 'PARTITIONED TABLE'
10
+ when 'v' then 'VIEW'
11
+ when 'm' then 'MATERIALIZED VIEW'
12
+ when 'S' then 'SEQUENCE'
13
+ else 'FOREIGN TABLE' end as type,
14
+ 'pg_class'::regclass::oid as next_classid,
15
+ c.oid as next_objid
16
+ from pg_class c
17
+ join pg_namespace n on n.oid = c.relnamespace
18
+ where c.relkind in ('r', 'p', 'v', 'm', 'S', 'f')
19
+ and n.nspname not in ('pg_catalog', 'information_schema')
20
+ and n.nspname not like 'pg\_%%'
21
+ union all
22
+ select p.proname,
23
+ n.nspname,
24
+ p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')',
25
+ case p.prokind when 'p' then 'PROCEDURE' else 'FUNCTION' end,
26
+ 'pg_proc'::regclass::oid,
27
+ p.oid
28
+ from pg_proc p
29
+ join pg_namespace n on n.oid = p.pronamespace
30
+ where p.prokind in ('f', 'p', 'w')
31
+ and n.nspname not in ('pg_catalog', 'information_schema')
32
+ and n.nspname not like 'pg\_%%'