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.
- pythia_postgres/__init__.py +5 -0
- pythia_postgres/__main__.py +5 -0
- pythia_postgres/catalog.py +59 -0
- pythia_postgres/cli.py +8 -0
- pythia_postgres/engine.py +213 -0
- pythia_postgres/guard.py +54 -0
- pythia_postgres/queries/code-objects.sql +28 -0
- pythia_postgres/queries/columns.sql +15 -0
- pythia_postgres/queries/comment-current.sql +17 -0
- pythia_postgres/queries/current-schema.sql +5 -0
- pythia_postgres/queries/dependencies-catalog.sql +66 -0
- pythia_postgres/queries/dependents-catalog.sql +66 -0
- pythia_postgres/queries/dependents-text.sql +22 -0
- pythia_postgres/queries/function-body.sql +8 -0
- pythia_postgres/queries/grep.sql +30 -0
- pythia_postgres/queries/name-candidates.sql +32 -0
- pythia_postgres/queries/object-names.sql +56 -0
- pythia_postgres/queries/plpgsql-check-installed.sql +7 -0
- pythia_postgres/queries/plpgsql-check.sql +27 -0
- pythia_postgres/queries/plpgsql-unchecked.sql +16 -0
- pythia_postgres/queries/rehearsal-limits.sql +7 -0
- pythia_postgres/queries/resolve.sql +34 -0
- pythia_postgres/queries/row-set-count.sql +11 -0
- pythia_postgres/queries/row-set-sample.sql +8 -0
- pythia_postgres/queries/search-path-catalog.sql +7 -0
- pythia_postgres/queries/search-path-first.sql +13 -0
- pythia_postgres/queries/sequence-ddl.sql +16 -0
- pythia_postgres/queries/server-version.sql +6 -0
- pythia_postgres/queries/session-power.sql +13 -0
- pythia_postgres/queries/source.sql +28 -0
- pythia_postgres/queries/table-columns.sql +16 -0
- pythia_postgres/queries/table-constraints.sql +13 -0
- pythia_postgres/queries/table-extras.sql +43 -0
- pythia_postgres/queries/table-head.sql +18 -0
- pythia_postgres/read.py +299 -0
- pythia_postgres/rules.py +135 -0
- pythia_postgres/skills/pythia-pg-apply/SKILL.md +97 -0
- pythia_postgres/skills/pythia-pg-apply/reference/refusals.md +26 -0
- pythia_postgres/skills/pythia-pg-autopilot/SKILL.md +63 -0
- pythia_postgres/skills/pythia-pg-deploy/SKILL.md +73 -0
- pythia_postgres/skills/pythia-pg-explore/SKILL.md +63 -0
- pythia_postgres/skills/pythia-pg-impact/SKILL.md +57 -0
- pythia_postgres/skills/pythia-pg-verify/SKILL.md +56 -0
- pythia_postgres/skills/using-pythia-pg/SKILL.md +65 -0
- pythia_postgres/write.py +583 -0
- pythia_postgres-0.1.0.dist-info/METADATA +360 -0
- pythia_postgres-0.1.0.dist-info/RECORD +51 -0
- pythia_postgres-0.1.0.dist-info/WHEEL +5 -0
- pythia_postgres-0.1.0.dist-info/entry_points.txt +2 -0
- pythia_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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,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,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
|
pythia_postgres/guard.py
ADDED
|
@@ -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,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\_%%'
|