pg-admin-mcp 0.0.0-stage → 0.1.0

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 (130) hide show
  1. package/README.md +608 -2
  2. package/dist/config/env.js +67 -0
  3. package/dist/config/env.js.map +1 -0
  4. package/dist/config/profiles.js +257 -0
  5. package/dist/config/profiles.js.map +1 -0
  6. package/dist/core/audit-context.js +21 -0
  7. package/dist/core/audit-context.js.map +1 -0
  8. package/dist/core/audit.js +91 -0
  9. package/dist/core/audit.js.map +1 -0
  10. package/dist/core/brands.js +24 -0
  11. package/dist/core/brands.js.map +1 -0
  12. package/dist/core/classifier.js +279 -0
  13. package/dist/core/classifier.js.map +1 -0
  14. package/dist/core/errors.js +197 -0
  15. package/dist/core/errors.js.map +1 -0
  16. package/dist/core/format.js +258 -0
  17. package/dist/core/format.js.map +1 -0
  18. package/dist/core/guards.js +513 -0
  19. package/dist/core/guards.js.map +1 -0
  20. package/dist/core/identifiers.js +431 -0
  21. package/dist/core/identifiers.js.map +1 -0
  22. package/dist/core/limits.js +79 -0
  23. package/dist/core/limits.js.map +1 -0
  24. package/dist/core/logger.js +34 -0
  25. package/dist/core/logger.js.map +1 -0
  26. package/dist/core/paginate.js +195 -0
  27. package/dist/core/paginate.js.map +1 -0
  28. package/dist/core/paths.js +392 -0
  29. package/dist/core/paths.js.map +1 -0
  30. package/dist/core/policy.js +98 -0
  31. package/dist/core/policy.js.map +1 -0
  32. package/dist/core/shutdown.js +10 -0
  33. package/dist/core/shutdown.js.map +1 -0
  34. package/dist/core/stdout-guard.js +25 -0
  35. package/dist/core/stdout-guard.js.map +1 -0
  36. package/dist/db/catalog.js +1006 -0
  37. package/dist/db/catalog.js.map +1 -0
  38. package/dist/db/pool.js +158 -0
  39. package/dist/db/pool.js.map +1 -0
  40. package/dist/db/session.js +258 -0
  41. package/dist/db/session.js.map +1 -0
  42. package/dist/db/types.js +2 -0
  43. package/dist/db/types.js.map +1 -0
  44. package/dist/index.js +91 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/resources/index.js +88 -0
  47. package/dist/resources/index.js.map +1 -0
  48. package/dist/runtime/binaries.js +356 -0
  49. package/dist/runtime/binaries.js.map +1 -0
  50. package/dist/runtime/env-file.js +62 -0
  51. package/dist/runtime/env-file.js.map +1 -0
  52. package/dist/runtime/environment.js +34 -0
  53. package/dist/runtime/environment.js.map +1 -0
  54. package/dist/server.js +106 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/snapshot/capture.js +189 -0
  57. package/dist/snapshot/capture.js.map +1 -0
  58. package/dist/snapshot/diff.js +254 -0
  59. package/dist/snapshot/diff.js.map +1 -0
  60. package/dist/snapshot/migrate.js +1596 -0
  61. package/dist/snapshot/migrate.js.map +1 -0
  62. package/dist/snapshot/model.js +80 -0
  63. package/dist/snapshot/model.js.map +1 -0
  64. package/dist/tools/_archive_meta.js +138 -0
  65. package/dist/tools/_archive_meta.js.map +1 -0
  66. package/dist/tools/_batch_shared.js +73 -0
  67. package/dist/tools/_batch_shared.js.map +1 -0
  68. package/dist/tools/_export_format.js +107 -0
  69. package/dist/tools/_export_format.js.map +1 -0
  70. package/dist/tools/_object_kinds.js +97 -0
  71. package/dist/tools/_object_kinds.js.map +1 -0
  72. package/dist/tools/_relation_detail.js +55 -0
  73. package/dist/tools/_relation_detail.js.map +1 -0
  74. package/dist/tools/_shared.js +17 -0
  75. package/dist/tools/_shared.js.map +1 -0
  76. package/dist/tools/_snapshot_io.js +238 -0
  77. package/dist/tools/_snapshot_io.js.map +1 -0
  78. package/dist/tools/admin/_shared.js +18 -0
  79. package/dist/tools/admin/_shared.js.map +1 -0
  80. package/dist/tools/admin/extensions.js +80 -0
  81. package/dist/tools/admin/extensions.js.map +1 -0
  82. package/dist/tools/admin/grants.js +207 -0
  83. package/dist/tools/admin/grants.js.map +1 -0
  84. package/dist/tools/admin/index.js +138 -0
  85. package/dist/tools/admin/index.js.map +1 -0
  86. package/dist/tools/admin/maintenance.js +106 -0
  87. package/dist/tools/admin/maintenance.js.map +1 -0
  88. package/dist/tools/admin/roles.js +144 -0
  89. package/dist/tools/admin/roles.js.map +1 -0
  90. package/dist/tools/admin/sessions.js +104 -0
  91. package/dist/tools/admin/sessions.js.map +1 -0
  92. package/dist/tools/admin/stats.js +63 -0
  93. package/dist/tools/admin/stats.js.map +1 -0
  94. package/dist/tools/backup.js +288 -0
  95. package/dist/tools/backup.js.map +1 -0
  96. package/dist/tools/connection_status.js +138 -0
  97. package/dist/tools/connection_status.js.map +1 -0
  98. package/dist/tools/copy.js +355 -0
  99. package/dist/tools/copy.js.map +1 -0
  100. package/dist/tools/delete_batch.js +73 -0
  101. package/dist/tools/delete_batch.js.map +1 -0
  102. package/dist/tools/describe_object.js +87 -0
  103. package/dist/tools/describe_object.js.map +1 -0
  104. package/dist/tools/execute_sql.js +395 -0
  105. package/dist/tools/execute_sql.js.map +1 -0
  106. package/dist/tools/explain.js +134 -0
  107. package/dist/tools/explain.js.map +1 -0
  108. package/dist/tools/export.js +215 -0
  109. package/dist/tools/export.js.map +1 -0
  110. package/dist/tools/get_schema.js +148 -0
  111. package/dist/tools/get_schema.js.map +1 -0
  112. package/dist/tools/index.js +40 -0
  113. package/dist/tools/index.js.map +1 -0
  114. package/dist/tools/insert_batch.js +212 -0
  115. package/dist/tools/insert_batch.js.map +1 -0
  116. package/dist/tools/list_objects.js +160 -0
  117. package/dist/tools/list_objects.js.map +1 -0
  118. package/dist/tools/prepare_migration.js +112 -0
  119. package/dist/tools/prepare_migration.js.map +1 -0
  120. package/dist/tools/restore.js +301 -0
  121. package/dist/tools/restore.js.map +1 -0
  122. package/dist/tools/schema_diff.js +79 -0
  123. package/dist/tools/schema_diff.js.map +1 -0
  124. package/dist/tools/schema_snapshot.js +57 -0
  125. package/dist/tools/schema_snapshot.js.map +1 -0
  126. package/dist/tools/select.js +162 -0
  127. package/dist/tools/select.js.map +1 -0
  128. package/dist/tools/update_batch.js +154 -0
  129. package/dist/tools/update_batch.js.map +1 -0
  130. package/package.json +47 -5
@@ -0,0 +1,1006 @@
1
+ /**
2
+ * The one place all `pg_catalog` SQL lives. `list_objects`, `describe_object`,
3
+ * `get_schema`, and later resources call these instead of writing their own
4
+ * queries — this is where the "never `information_schema`" rule (design.md
5
+ * risk 5) is actually enforced: every query below reads `pg_catalog` directly,
6
+ * which is the only place partial-index predicates (`pg_index.indpred`) and
7
+ * row-level-security policies (`pg_policy`) are visible at all.
8
+ *
9
+ * Every function takes an already-checked-out `client: PoolClient` — this
10
+ * module never acquires one itself (`db/session.ts` is the only acquirer), so
11
+ * a caller that needs several of these under one consistent snapshot (e.g.
12
+ * `describe_object` pulling columns + indexes + triggers together) runs them
13
+ * all inside a single `withSession`/`withTx` callback.
14
+ *
15
+ * All queries are parameterized. Listing-level functions take a plain
16
+ * `readonly string[]` of schema names because that list is always either
17
+ * profile config (`Profile.schemas`, trusted, never interpolated — only ever
18
+ * bound) or the output of {@link listSchemas} itself; only bound parameters
19
+ * touch SQL text, so no `SafeIdent` is needed there. Per-relation functions
20
+ * take a {@link RelationRef} (an oid plus catalog-verified `SafeIdent`s)
21
+ * because those are always called after `core/identifiers.ts#resolveRelation`.
22
+ */
23
+ async function rows(client, text, params) {
24
+ const result = await client.query(text, params);
25
+ return result.rows;
26
+ }
27
+ // pg_class.relkind for every relation kind this server exposes. Kept local
28
+ // (rather than re-exported from core/identifiers.ts) since it is a tiny,
29
+ // stable table duplicated in exactly one other place for a different purpose.
30
+ const RELKIND_TO_KIND = {
31
+ r: 'table',
32
+ p: 'partitioned_table',
33
+ v: 'view',
34
+ m: 'materialized_view',
35
+ f: 'foreign_table',
36
+ };
37
+ const RELATION_RELKINDS = Object.keys(RELKIND_TO_KIND);
38
+ // ---------------------------------------------------------------------------
39
+ // Database-wide listings
40
+ // ---------------------------------------------------------------------------
41
+ /**
42
+ * `schemas` unset lists every non-system schema; system schemas (`pg_%`, `information_schema`) are always excluded.
43
+ * `namePattern` (a SQL `LIKE`/`ILIKE` pattern, `%`/`_` wildcards) is bound as `$N` and evaluated by PostgreSQL itself
44
+ * — never matched in JS. PostgreSQL's `LIKE` engine is linear in input length; a naive `pattern -> RegExp` translation
45
+ * (the historical approach here, `tools/_object_kinds.ts#patternToRegExp`) is not, and a caller-supplied pattern like
46
+ * `%%%%%%%%z` turns that into catastrophic backtracking — the whole server would hang evaluating one pattern against
47
+ * one name. Binding as a value into `ILIKE` sidesteps the vulnerability class entirely instead of trying to detect it.
48
+ */
49
+ export async function listSchemas(client, schemas, namePattern) {
50
+ const result = await rows(client, `SELECT n.oid::int AS oid, n.nspname AS name, pg_get_userbyid(n.nspowner) AS owner
51
+ FROM pg_catalog.pg_namespace n
52
+ WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'
53
+ AND ($1::text[] IS NULL OR n.nspname = ANY($1))
54
+ AND ($2::text IS NULL OR n.nspname ILIKE $2)
55
+ ORDER BY n.nspname`, [schemas ?? null, namePattern ?? null]);
56
+ return result;
57
+ }
58
+ export async function listRelations(client, schemas, opts = {}) {
59
+ if (schemas.length === 0)
60
+ return [];
61
+ const relkinds = opts.kinds === undefined
62
+ ? RELATION_RELKINDS
63
+ : Object.entries(RELKIND_TO_KIND)
64
+ .filter(([, kind]) => opts.kinds?.includes(kind))
65
+ .map(([relkind]) => relkind);
66
+ if (relkinds.length === 0)
67
+ return [];
68
+ const result = await rows(client, `SELECT c.oid::int AS oid, n.nspname AS schema, c.relname AS name, c.relkind AS relkind,
69
+ pg_get_userbyid(c.relowner) AS owner, obj_description(c.oid, 'pg_class') AS comment,
70
+ NULLIF(c.reltuples, -1) AS estimated_rows
71
+ FROM pg_catalog.pg_class c
72
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
73
+ WHERE n.nspname = ANY($1) AND c.relkind = ANY($2::"char"[])
74
+ AND ($3::text IS NULL OR c.relname ILIKE $3)
75
+ ORDER BY n.nspname, c.relname`, [schemas, relkinds, opts.namePattern ?? null]);
76
+ return result.map((r) => ({
77
+ oid: r.oid,
78
+ schema: r.schema,
79
+ name: r.name,
80
+ kind: RELKIND_TO_KIND[r.relkind] ?? 'table',
81
+ owner: r.owner,
82
+ comment: r.comment,
83
+ estimatedRows: r.estimated_rows === null ? null : Math.round(r.estimated_rows),
84
+ }));
85
+ }
86
+ export async function listSequences(client, schemas, namePattern) {
87
+ if (schemas.length === 0)
88
+ return [];
89
+ const result = await rows(client, `SELECT c.oid::int AS oid, n.nspname AS schema, c.relname AS name,
90
+ pg_get_userbyid(c.relowner) AS owner, obj_description(c.oid, 'pg_class') AS comment
91
+ FROM pg_catalog.pg_class c
92
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
93
+ WHERE n.nspname = ANY($1) AND c.relkind = 'S'
94
+ AND ($2::text IS NULL OR c.relname ILIKE $2)
95
+ ORDER BY n.nspname, c.relname`, [schemas, namePattern ?? null]);
96
+ return result;
97
+ }
98
+ export async function listFunctions(client, schemas, namePattern) {
99
+ if (schemas.length === 0)
100
+ return [];
101
+ const result = await rows(client, `SELECT p.oid::int AS oid, n.nspname AS schema, p.proname AS name,
102
+ pg_get_function_identity_arguments(p.oid) AS arg_types,
103
+ pg_get_function_result(p.oid) AS return_type,
104
+ l.lanname AS language, p.prokind AS kind,
105
+ obj_description(p.oid, 'pg_proc') AS comment
106
+ FROM pg_catalog.pg_proc p
107
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
108
+ JOIN pg_catalog.pg_language l ON l.oid = p.prolang
109
+ WHERE n.nspname = ANY($1)
110
+ AND ($2::text IS NULL OR p.proname ILIKE $2)
111
+ ORDER BY n.nspname, p.proname`, [schemas, namePattern ?? null]);
112
+ return result.map((r) => ({
113
+ oid: r.oid,
114
+ schema: r.schema,
115
+ name: r.name,
116
+ argTypes: r.arg_types,
117
+ returnType: r.return_type,
118
+ language: r.language,
119
+ kind: r.kind,
120
+ comment: r.comment,
121
+ }));
122
+ }
123
+ export async function listEnumTypes(client, schemas, namePattern) {
124
+ if (schemas.length === 0)
125
+ return [];
126
+ const result = await rows(client, `SELECT t.oid::int AS oid, n.nspname AS schema, t.typname AS name,
127
+ ARRAY(SELECT e.enumlabel FROM pg_catalog.pg_enum e
128
+ WHERE e.enumtypid = t.oid ORDER BY e.enumsortorder)::text[] AS labels,
129
+ obj_description(t.oid, 'pg_type') AS comment
130
+ FROM pg_catalog.pg_type t
131
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
132
+ WHERE t.typtype = 'e' AND n.nspname = ANY($1)
133
+ AND ($2::text IS NULL OR t.typname ILIKE $2)
134
+ ORDER BY n.nspname, t.typname`, [schemas, namePattern ?? null]);
135
+ return result;
136
+ }
137
+ /** `namePattern` filters extension names; schema scoping is applied by the caller (`tools/_object_kinds.ts`) since an extension's schema is not part of this query's own `WHERE`. */
138
+ export async function listExtensions(client, namePattern) {
139
+ return rows(client, `SELECT e.extname AS name, e.extversion AS version, n.nspname AS schema,
140
+ obj_description(e.oid, 'pg_extension') AS comment
141
+ FROM pg_catalog.pg_extension e
142
+ JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace
143
+ WHERE ($1::text IS NULL OR e.extname ILIKE $1)
144
+ ORDER BY e.extname`, [namePattern ?? null]);
145
+ }
146
+ /**
147
+ * Server version and `current_user`'s privileges — `postgres_connection_status`'s
148
+ * probe. Lives here (not inlined in `tools/connection_status.ts`) per this
149
+ * module's own rule: `catalog.ts` is the one place `pg_catalog` SQL lives.
150
+ * `null` only if the server returns neither row, which should never happen on
151
+ * a reachable connection.
152
+ */
153
+ export async function getCurrentRoleInfo(client) {
154
+ const versionRows = await rows(client, `SELECT current_setting('server_version') AS version`, []);
155
+ const roleRows = await rows(client, `SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolreplication, rolbypassrls
156
+ FROM pg_catalog.pg_roles WHERE rolname = current_user`, []);
157
+ const version = versionRows[0]?.version;
158
+ const role = roleRows[0];
159
+ if (version === undefined || role === undefined)
160
+ return null;
161
+ return {
162
+ serverVersion: version,
163
+ role: role.rolname,
164
+ isSuperuser: role.rolsuper,
165
+ canCreateRole: role.rolcreaterole,
166
+ canCreateDb: role.rolcreatedb,
167
+ canReplicate: role.rolreplication,
168
+ bypassRls: role.rolbypassrls,
169
+ };
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // Per-relation detail — every function below takes a resolved RelationRef.
173
+ // ---------------------------------------------------------------------------
174
+ export async function getColumns(client, rel) {
175
+ const result = await rows(client, `SELECT a.attname AS name, a.attnum AS ordinal,
176
+ format_type(a.atttypid, a.atttypmod) AS data_type,
177
+ a.attnotnull AS not_null,
178
+ pg_get_expr(ad.adbin, ad.adrelid) AS default_expr,
179
+ a.attgenerated <> '' AS is_generated,
180
+ col_description(a.attrelid, a.attnum) AS comment
181
+ FROM pg_catalog.pg_attribute a
182
+ LEFT JOIN pg_catalog.pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
183
+ WHERE a.attrelid = $1 AND a.attnum > 0 AND NOT a.attisdropped
184
+ ORDER BY a.attnum`, [rel.oid]);
185
+ return result.map((r) => ({
186
+ name: r.name,
187
+ ordinal: r.ordinal,
188
+ dataType: r.data_type,
189
+ notNull: r.not_null,
190
+ defaultExpr: r.default_expr,
191
+ isGenerated: r.is_generated,
192
+ comment: r.comment,
193
+ }));
194
+ }
195
+ /**
196
+ * Column list for a `conkey`/`confkey` array, in index/constraint-defined order (not `attnum` order).
197
+ *
198
+ * Cast to `text[]` (OID 1009): `attname` is Postgres type `name`, so an uncast
199
+ * `ARRAY(...)` here comes back as `name[]` (OID 1003) — a type `pg-types` has
200
+ * no built-in array parser for, unlike `text[]`. Without the cast, `pg` hands
201
+ * back the raw `'{a,b,c}'` wire literal instead of a real JS array.
202
+ */
203
+ const ORDERED_COLUMNS_SUBQUERY = (keyExpr, relidExpr) => `
204
+ ARRAY(
205
+ SELECT a.attname
206
+ FROM unnest(${keyExpr}) WITH ORDINALITY AS k(attnum, ord)
207
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = ${relidExpr} AND a.attnum = k.attnum
208
+ ORDER BY k.ord
209
+ )::text[]`;
210
+ export async function getPrimaryKey(client, rel) {
211
+ const result = await rows(client, `SELECT con.conname AS name, ${ORDERED_COLUMNS_SUBQUERY('con.conkey', 'con.conrelid')} AS columns
212
+ FROM pg_catalog.pg_constraint con
213
+ WHERE con.conrelid = $1 AND con.contype = 'p'`, [rel.oid]);
214
+ return result[0] ?? null;
215
+ }
216
+ export async function getForeignKeys(client, rel) {
217
+ const ACTION = { a: 'NO ACTION', r: 'RESTRICT', c: 'CASCADE', n: 'SET NULL', d: 'SET DEFAULT' };
218
+ const result = await rows(client, `SELECT con.conname AS name,
219
+ ${ORDERED_COLUMNS_SUBQUERY('con.conkey', 'con.conrelid')} AS columns,
220
+ rn.nspname AS ref_schema, rc.relname AS ref_table,
221
+ ${ORDERED_COLUMNS_SUBQUERY('con.confkey', 'con.confrelid')} AS ref_columns,
222
+ con.confupdtype AS on_update_code, con.confdeltype AS on_delete_code,
223
+ pg_get_constraintdef(con.oid) AS definition
224
+ FROM pg_catalog.pg_constraint con
225
+ JOIN pg_catalog.pg_class rc ON rc.oid = con.confrelid
226
+ JOIN pg_catalog.pg_namespace rn ON rn.oid = rc.relnamespace
227
+ WHERE con.conrelid = $1 AND con.contype = 'f'
228
+ ORDER BY con.conname`, [rel.oid]);
229
+ return result.map((r) => ({
230
+ name: r.name,
231
+ columns: r.columns,
232
+ refSchema: r.ref_schema,
233
+ refTable: r.ref_table,
234
+ refColumns: r.ref_columns,
235
+ onUpdate: ACTION[r.on_update_code] ?? r.on_update_code,
236
+ onDelete: ACTION[r.on_delete_code] ?? r.on_delete_code,
237
+ definition: r.definition,
238
+ }));
239
+ }
240
+ export async function getUniqueConstraints(client, rel) {
241
+ return rows(client, `SELECT con.conname AS name, ${ORDERED_COLUMNS_SUBQUERY('con.conkey', 'con.conrelid')} AS columns
242
+ FROM pg_catalog.pg_constraint con
243
+ WHERE con.conrelid = $1 AND con.contype = 'u'
244
+ ORDER BY con.conname`, [rel.oid]);
245
+ }
246
+ export async function getCheckConstraints(client, rel) {
247
+ return rows(client, `SELECT con.conname AS name, pg_get_constraintdef(con.oid) AS definition
248
+ FROM pg_catalog.pg_constraint con
249
+ WHERE con.conrelid = $1 AND con.contype = 'c'
250
+ ORDER BY con.conname`, [rel.oid]);
251
+ }
252
+ export async function getIndexes(client, rel) {
253
+ const result = await rows(client, `SELECT ic.relname AS name,
254
+ pg_get_indexdef(i.indexrelid) AS definition,
255
+ i.indisunique AS is_unique,
256
+ i.indisprimary AS is_primary,
257
+ pg_get_expr(i.indpred, i.indrelid) AS predicate,
258
+ (SELECT array_agg(COALESCE(a.attname, '(expression)') ORDER BY k.ord)::text[]
259
+ FROM unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)
260
+ LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
261
+ ) AS columns
262
+ FROM pg_catalog.pg_index i
263
+ JOIN pg_catalog.pg_class ic ON ic.oid = i.indexrelid
264
+ WHERE i.indrelid = $1
265
+ ORDER BY ic.relname`, [rel.oid]);
266
+ return result.map((r) => ({
267
+ name: r.name,
268
+ definition: r.definition,
269
+ isUnique: r.is_unique,
270
+ isPrimary: r.is_primary,
271
+ columns: r.columns ?? [],
272
+ predicate: r.predicate,
273
+ }));
274
+ }
275
+ // ---------------------------------------------------------------------------
276
+ // Batched per-relation detail — one round trip for N relations instead of N.
277
+ // `get_schema`'s `detail: "full"` used to call the four single-relation
278
+ // functions above once per relation (`Promise.all` over the same client, no
279
+ // less — see `tools/_relation_detail.ts`'s module doc for why that alone was
280
+ // a bug): 4 catalog round trips per relation, so 40 objects meant ~160
281
+ // queries and the profile's default `maxRows` (500) meant up to ~2000
282
+ // sequential round trips on one connection. These four take `oid = ANY($1)`
283
+ // instead of `oid = $1` and return a `Map<oid, T[]>` (or `Map<oid, T>` for the
284
+ // at-most-one-per-relation primary key) so the caller pays 4 queries total,
285
+ // not 4 per relation. Not part of the single-relation `describe_object` path
286
+ // — that already runs exactly 1 relation through the unbatched functions
287
+ // above, where batching would add overhead for no benefit.
288
+ // ---------------------------------------------------------------------------
289
+ export async function getColumnsBatch(client, oids) {
290
+ const out = new Map();
291
+ if (oids.length === 0)
292
+ return out;
293
+ const result = await rows(client, `SELECT a.attrelid::int AS relid, a.attname AS name, a.attnum AS ordinal,
294
+ format_type(a.atttypid, a.atttypmod) AS data_type,
295
+ a.attnotnull AS not_null,
296
+ pg_get_expr(ad.adbin, ad.adrelid) AS default_expr,
297
+ a.attgenerated <> '' AS is_generated,
298
+ col_description(a.attrelid, a.attnum) AS comment
299
+ FROM pg_catalog.pg_attribute a
300
+ LEFT JOIN pg_catalog.pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
301
+ WHERE a.attrelid = ANY($1) AND a.attnum > 0 AND NOT a.attisdropped
302
+ ORDER BY a.attrelid, a.attnum`, [oids]);
303
+ for (const r of result) {
304
+ const list = out.get(r.relid) ?? [];
305
+ list.push({
306
+ name: r.name,
307
+ ordinal: r.ordinal,
308
+ dataType: r.data_type,
309
+ notNull: r.not_null,
310
+ defaultExpr: r.default_expr,
311
+ isGenerated: r.is_generated,
312
+ comment: r.comment,
313
+ });
314
+ out.set(r.relid, list);
315
+ }
316
+ return out;
317
+ }
318
+ export async function getPrimaryKeysBatch(client, oids) {
319
+ const out = new Map();
320
+ if (oids.length === 0)
321
+ return out;
322
+ const result = await rows(client, `SELECT con.conrelid::int AS relid, con.conname AS name, ${ORDERED_COLUMNS_SUBQUERY('con.conkey', 'con.conrelid')} AS columns
323
+ FROM pg_catalog.pg_constraint con
324
+ WHERE con.conrelid = ANY($1) AND con.contype = 'p'`, [oids]);
325
+ for (const r of result)
326
+ out.set(r.relid, { name: r.name, columns: r.columns });
327
+ return out;
328
+ }
329
+ export async function getForeignKeysBatch(client, oids) {
330
+ const out = new Map();
331
+ if (oids.length === 0)
332
+ return out;
333
+ const ACTION = { a: 'NO ACTION', r: 'RESTRICT', c: 'CASCADE', n: 'SET NULL', d: 'SET DEFAULT' };
334
+ const result = await rows(client, `SELECT con.conrelid::int AS relid, con.conname AS name,
335
+ ${ORDERED_COLUMNS_SUBQUERY('con.conkey', 'con.conrelid')} AS columns,
336
+ rn.nspname AS ref_schema, rc.relname AS ref_table,
337
+ ${ORDERED_COLUMNS_SUBQUERY('con.confkey', 'con.confrelid')} AS ref_columns,
338
+ con.confupdtype AS on_update_code, con.confdeltype AS on_delete_code,
339
+ pg_get_constraintdef(con.oid) AS definition
340
+ FROM pg_catalog.pg_constraint con
341
+ JOIN pg_catalog.pg_class rc ON rc.oid = con.confrelid
342
+ JOIN pg_catalog.pg_namespace rn ON rn.oid = rc.relnamespace
343
+ WHERE con.conrelid = ANY($1) AND con.contype = 'f'
344
+ ORDER BY con.conrelid, con.conname`, [oids]);
345
+ for (const r of result) {
346
+ const list = out.get(r.relid) ?? [];
347
+ list.push({
348
+ name: r.name,
349
+ columns: r.columns,
350
+ refSchema: r.ref_schema,
351
+ refTable: r.ref_table,
352
+ refColumns: r.ref_columns,
353
+ onUpdate: ACTION[r.on_update_code] ?? r.on_update_code,
354
+ onDelete: ACTION[r.on_delete_code] ?? r.on_delete_code,
355
+ definition: r.definition,
356
+ });
357
+ out.set(r.relid, list);
358
+ }
359
+ return out;
360
+ }
361
+ export async function getIndexesBatch(client, oids, opts = {}) {
362
+ const out = new Map();
363
+ if (oids.length === 0)
364
+ return out;
365
+ const result = await rows(client, `SELECT i.indrelid::int AS relid, ic.relname AS name,
366
+ pg_get_indexdef(i.indexrelid) AS definition,
367
+ i.indisunique AS is_unique,
368
+ i.indisprimary AS is_primary,
369
+ pg_get_expr(i.indpred, i.indrelid) AS predicate,
370
+ (SELECT array_agg(COALESCE(a.attname, '(expression)') ORDER BY k.ord)::text[]
371
+ FROM unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)
372
+ LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
373
+ ) AS columns
374
+ FROM pg_catalog.pg_index i
375
+ JOIN pg_catalog.pg_class ic ON ic.oid = i.indexrelid
376
+ WHERE i.indrelid = ANY($1)
377
+ AND ($2::bool IS NOT TRUE
378
+ OR NOT EXISTS (SELECT 1 FROM pg_catalog.pg_constraint con WHERE con.conindid = i.indexrelid))
379
+ ORDER BY i.indrelid, ic.relname`, [oids, opts.excludeConstraintBacked ?? false]);
380
+ for (const r of result) {
381
+ const list = out.get(r.relid) ?? [];
382
+ list.push({ name: r.name, definition: r.definition, isUnique: r.is_unique, isPrimary: r.is_primary, columns: r.columns ?? [], predicate: r.predicate });
383
+ out.set(r.relid, list);
384
+ }
385
+ return out;
386
+ }
387
+ // ---------------------------------------------------------------------------
388
+ // Snapshot capture support — everything below is batched (`= ANY($1)`) or
389
+ // schema-scoped (`= ANY($1)` over schema names), because `snapshot/capture.ts`
390
+ // must cost a fixed number of round trips for a whole schema, not a number
391
+ // that grows with the relation count (see `_relation_detail.ts`'s module doc
392
+ // for the 166-queries-for-40-objects regression these shapes exist to avoid).
393
+ // ---------------------------------------------------------------------------
394
+ /** `SELECT current_database()` — cast to `text`, since `name` is not a type any caller should be handed raw. */
395
+ export async function getCurrentDatabase(client) {
396
+ const result = await rows(client, `SELECT current_database()::text AS name`, []);
397
+ return result[0]?.name ?? '';
398
+ }
399
+ /**
400
+ * Columns with everything a schema *snapshot* needs and `describe_object` does
401
+ * not: identity kind, generation kind, and a non-default collation. Kept
402
+ * separate from {@link getColumnsBatch} rather than widening `ColumnInfo`,
403
+ * because widening it would silently change the JSON `postgres_describe_object`
404
+ * and `postgres_get_schema` have been returning since Phase 1.
405
+ *
406
+ * `attcollation <> typcollation` is the whole point of the collation column:
407
+ * every collatable column has *a* collation, and reporting the type's own
408
+ * default on every one of them would bury the handful that actually override it.
409
+ */
410
+ export async function getSnapshotColumnsBatch(client, oids) {
411
+ const out = new Map();
412
+ if (oids.length === 0)
413
+ return out;
414
+ const result = await rows(client, `SELECT a.attrelid::int AS relid, a.attname AS name, a.attnum AS ordinal,
415
+ format_type(a.atttypid, a.atttypmod) AS data_type,
416
+ a.attnotnull AS not_null,
417
+ pg_get_expr(ad.adbin, ad.adrelid) AS default_expr,
418
+ CASE a.attidentity WHEN 'a' THEN 'always' WHEN 'd' THEN 'by default' ELSE NULL END AS identity,
419
+ CASE a.attgenerated WHEN 's' THEN 'stored' ELSE NULL END AS generated,
420
+ (SELECT co.collname::text FROM pg_catalog.pg_collation co
421
+ WHERE co.oid = a.attcollation AND a.attcollation <> t.typcollation) AS collation,
422
+ col_description(a.attrelid, a.attnum) AS comment
423
+ FROM pg_catalog.pg_attribute a
424
+ JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
425
+ LEFT JOIN pg_catalog.pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
426
+ WHERE a.attrelid = ANY($1) AND a.attnum > 0 AND NOT a.attisdropped
427
+ ORDER BY a.attrelid, a.attnum`, [oids]);
428
+ for (const r of result) {
429
+ const list = out.get(r.relid) ?? [];
430
+ list.push({
431
+ name: r.name,
432
+ ordinal: r.ordinal,
433
+ dataType: r.data_type,
434
+ notNull: r.not_null,
435
+ defaultExpr: r.default_expr,
436
+ identity: r.identity === 'always' || r.identity === 'by default' ? r.identity : null,
437
+ generated: r.generated === 'stored' ? 'stored' : null,
438
+ collation: r.collation,
439
+ comment: r.comment,
440
+ });
441
+ out.set(r.relid, list);
442
+ }
443
+ return out;
444
+ }
445
+ /**
446
+ * Every constraint of the requested `contype`s for many relations in **one**
447
+ * query, as `pg_get_constraintdef` text.
448
+ *
449
+ * The definition text is the normalization: it is the server's own canonical
450
+ * rendering, so it already carries the constraint's column order, `MATCH`,
451
+ * `ON DELETE`, `DEFERRABLE`, `NOT VALID`, `NULLS NOT DISTINCT` and the
452
+ * reformatted check expression — all the things a hand-assembled structural
453
+ * comparison gets subtly wrong and then reports as a phantom change.
454
+ */
455
+ export async function getConstraintDefsBatch(client, oids, contypes) {
456
+ const out = new Map();
457
+ if (oids.length === 0 || contypes.length === 0)
458
+ return out;
459
+ const result = await rows(client, `SELECT con.conrelid::int AS relid, con.conname AS name, con.contype::text AS contype,
460
+ pg_get_constraintdef(con.oid) AS definition,
461
+ rn.nspname AS ref_schema, rc.relname AS ref_table
462
+ FROM pg_catalog.pg_constraint con
463
+ LEFT JOIN pg_catalog.pg_class rc ON rc.oid = con.confrelid
464
+ LEFT JOIN pg_catalog.pg_namespace rn ON rn.oid = rc.relnamespace
465
+ WHERE con.conrelid = ANY($1) AND con.contype = ANY($2::"char"[])
466
+ ORDER BY con.conrelid, con.contype, con.conname`, [oids, contypes]);
467
+ for (const r of result) {
468
+ const list = out.get(r.relid) ?? [];
469
+ list.push({ name: r.name, type: r.contype, definition: r.definition, refSchema: r.ref_schema, refTable: r.ref_table });
470
+ out.set(r.relid, list);
471
+ }
472
+ return out;
473
+ }
474
+ /**
475
+ * `relrowsecurity`/`relforcerowsecurity`, the view definition, and the
476
+ * inheritance/partition attachment for many relations — one `pg_class` pass
477
+ * instead of one query per concern. `inherits` is the parent list for both
478
+ * classical inheritance and partitioning (a partition has exactly one);
479
+ * `partitionBound` is `null` for anything that is not a partition.
480
+ */
481
+ export async function getRelationExtrasBatch(client, oids) {
482
+ const out = new Map();
483
+ if (oids.length === 0)
484
+ return out;
485
+ const result = await rows(client, `SELECT c.oid::int AS relid,
486
+ c.relrowsecurity AS rls_enabled,
487
+ c.relforcerowsecurity AS rls_forced,
488
+ CASE WHEN c.relkind IN ('v', 'm') THEN pg_get_viewdef(c.oid, true) END AS view_definition,
489
+ ARRAY(SELECT pn.nspname || '.' || pc.relname
490
+ FROM pg_catalog.pg_inherits inh
491
+ JOIN pg_catalog.pg_class pc ON pc.oid = inh.inhparent
492
+ JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace
493
+ WHERE inh.inhrelid = c.oid
494
+ ORDER BY inh.inhseqno)::text[] AS inherits,
495
+ pg_get_expr(c.relpartbound, c.oid) AS partition_bound
496
+ FROM pg_catalog.pg_class c
497
+ WHERE c.oid = ANY($1)`, [oids]);
498
+ for (const r of result) {
499
+ out.set(r.relid, {
500
+ rowSecurityEnabled: r.rls_enabled,
501
+ rowSecurityForced: r.rls_forced,
502
+ viewDefinition: r.view_definition,
503
+ inherits: r.inherits ?? [],
504
+ partitionBound: r.partition_bound,
505
+ });
506
+ }
507
+ return out;
508
+ }
509
+ /** Row-level-security policies for many relations. Roles are `ORDER BY`ed — `oid = ANY(...)` alone returns them in physical catalog order, which is not stable. */
510
+ export async function getPoliciesBatch(client, oids) {
511
+ const out = new Map();
512
+ if (oids.length === 0)
513
+ return out;
514
+ const COMMAND = { r: 'SELECT', a: 'INSERT', w: 'UPDATE', d: 'DELETE', '*': 'ALL' };
515
+ const result = await rows(client, `SELECT pol.polrelid::int AS relid, pol.polname AS name, pol.polcmd::text AS polcmd, pol.polpermissive AS permissive,
516
+ CASE WHEN pol.polroles = '{0}' THEN ARRAY['public']::text[]
517
+ ELSE ARRAY(SELECT rolname FROM pg_catalog.pg_roles WHERE oid = ANY(pol.polroles) ORDER BY rolname)::text[] END AS roles,
518
+ pg_get_expr(pol.polqual, pol.polrelid) AS using_expr,
519
+ pg_get_expr(pol.polwithcheck, pol.polrelid) AS with_check_expr
520
+ FROM pg_catalog.pg_policy pol
521
+ WHERE pol.polrelid = ANY($1)
522
+ ORDER BY pol.polrelid, pol.polname`, [oids]);
523
+ for (const r of result) {
524
+ const list = out.get(r.relid) ?? [];
525
+ list.push({
526
+ name: r.name,
527
+ command: COMMAND[r.polcmd] ?? 'ALL',
528
+ permissive: r.permissive,
529
+ roles: r.roles,
530
+ using: r.using_expr,
531
+ withCheck: r.with_check_expr,
532
+ });
533
+ out.set(r.relid, list);
534
+ }
535
+ return out;
536
+ }
537
+ /** Sequence parameters for every sequence in `schemas` — the batched counterpart of {@link getSequenceDetail}. */
538
+ export async function listSequenceDetails(client, schemas) {
539
+ if (schemas.length === 0)
540
+ return [];
541
+ const result = await rows(client, `SELECT c.oid::int AS oid, n.nspname AS schema, c.relname AS name,
542
+ pg_get_userbyid(c.relowner) AS owner, obj_description(c.oid, 'pg_class') AS comment,
543
+ s.data_type::text AS data_type, s.start_value::text AS start_value, s.min_value::text AS min_value,
544
+ s.max_value::text AS max_value, s.increment_by::text AS increment_by, s.cycle,
545
+ s.cache_size::text AS cache_size, s.last_value::text AS last_value
546
+ FROM pg_catalog.pg_class c
547
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
548
+ JOIN pg_catalog.pg_sequences s ON s.schemaname = n.nspname AND s.sequencename = c.relname
549
+ WHERE n.nspname = ANY($1) AND c.relkind = 'S'
550
+ ORDER BY n.nspname, c.relname`, [schemas]);
551
+ return result.map((r) => ({
552
+ oid: r.oid,
553
+ schema: r.schema,
554
+ name: r.name,
555
+ owner: r.owner,
556
+ comment: r.comment,
557
+ dataType: r.data_type,
558
+ startValue: r.start_value,
559
+ minValue: r.min_value,
560
+ maxValue: r.max_value,
561
+ incrementBy: r.increment_by,
562
+ cycle: r.cycle,
563
+ cacheSize: r.cache_size,
564
+ lastValue: r.last_value,
565
+ }));
566
+ }
567
+ /**
568
+ * {@link listFunctions} plus `pg_proc.prosrc`, so a changed function *body* is
569
+ * a visible schema change rather than a silent one. Deliberately not
570
+ * `pg_get_functiondef`, which {@link getFunctionDetails} uses: that function
571
+ * raises `ERROR: "…" is an aggregate function` for `prokind = 'a'`, and one
572
+ * aggregate anywhere in scope would fail a whole snapshot.
573
+ */
574
+ export async function listFunctionSources(client, schemas) {
575
+ if (schemas.length === 0)
576
+ return [];
577
+ const result = await rows(client, `SELECT p.oid::int AS oid, n.nspname AS schema, p.proname AS name,
578
+ pg_get_function_identity_arguments(p.oid) AS arg_types,
579
+ pg_get_function_result(p.oid) AS return_type,
580
+ l.lanname AS language, p.prokind AS kind,
581
+ obj_description(p.oid, 'pg_proc') AS comment,
582
+ p.prosrc AS body
583
+ FROM pg_catalog.pg_proc p
584
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
585
+ JOIN pg_catalog.pg_language l ON l.oid = p.prolang
586
+ WHERE n.nspname = ANY($1)
587
+ ORDER BY n.nspname, p.proname, p.oid`, [schemas]);
588
+ return result.map((r) => ({
589
+ oid: r.oid,
590
+ schema: r.schema,
591
+ name: r.name,
592
+ argTypes: r.arg_types,
593
+ returnType: r.return_type,
594
+ language: r.language,
595
+ kind: r.kind,
596
+ comment: r.comment,
597
+ body: r.body,
598
+ }));
599
+ }
600
+ const TG_BEFORE = 1 << 1; // 2
601
+ const TG_INSTEAD = 1 << 6; // 64
602
+ const TG_INSERT = 1 << 2; // 4
603
+ const TG_DELETE = 1 << 3; // 8
604
+ const TG_UPDATE = 1 << 4; // 16
605
+ const TG_TRUNCATE = 1 << 5; // 32
606
+ function decodeTriggerTiming(tgtype) {
607
+ if ((tgtype & TG_INSTEAD) !== 0)
608
+ return 'INSTEAD OF';
609
+ if ((tgtype & TG_BEFORE) !== 0)
610
+ return 'BEFORE';
611
+ return 'AFTER';
612
+ }
613
+ function decodeTriggerEvents(tgtype) {
614
+ const events = [];
615
+ if ((tgtype & TG_INSERT) !== 0)
616
+ events.push('INSERT');
617
+ if ((tgtype & TG_DELETE) !== 0)
618
+ events.push('DELETE');
619
+ if ((tgtype & TG_UPDATE) !== 0)
620
+ events.push('UPDATE');
621
+ if ((tgtype & TG_TRUNCATE) !== 0)
622
+ events.push('TRUNCATE');
623
+ return events;
624
+ }
625
+ export async function getTriggers(client, rel) {
626
+ const result = await rows(client, `SELECT t.tgname AS name, t.tgtype::int2 AS tgtype, pg_get_triggerdef(t.oid) AS definition
627
+ FROM pg_catalog.pg_trigger t
628
+ WHERE t.tgrelid = $1 AND NOT t.tgisinternal
629
+ ORDER BY t.tgname`, [rel.oid]);
630
+ return result.map((r) => ({
631
+ name: r.name,
632
+ timing: decodeTriggerTiming(r.tgtype),
633
+ events: decodeTriggerEvents(r.tgtype),
634
+ definition: r.definition,
635
+ }));
636
+ }
637
+ /** Non-internal triggers for many relations — the batched counterpart of {@link getTriggers}, for `snapshot/capture.ts`. */
638
+ export async function getTriggersBatch(client, oids) {
639
+ const out = new Map();
640
+ if (oids.length === 0)
641
+ return out;
642
+ const result = await rows(client, `SELECT t.tgrelid::int AS relid, t.tgname AS name, t.tgtype::int2 AS tgtype, pg_get_triggerdef(t.oid) AS definition
643
+ FROM pg_catalog.pg_trigger t
644
+ WHERE t.tgrelid = ANY($1) AND NOT t.tgisinternal
645
+ ORDER BY t.tgrelid, t.tgname`, [oids]);
646
+ for (const r of result) {
647
+ const list = out.get(r.relid) ?? [];
648
+ list.push({ name: r.name, timing: decodeTriggerTiming(r.tgtype), events: decodeTriggerEvents(r.tgtype), definition: r.definition });
649
+ out.set(r.relid, list);
650
+ }
651
+ return out;
652
+ }
653
+ export async function getRowSecurity(client, rel) {
654
+ const [flags] = await rows(client, `SELECT relrowsecurity AS enabled, relforcerowsecurity AS forced
655
+ FROM pg_catalog.pg_class WHERE oid = $1`, [rel.oid]);
656
+ const COMMAND = { r: 'SELECT', a: 'INSERT', w: 'UPDATE', d: 'DELETE', '*': 'ALL' };
657
+ const policyRows = await rows(client, `SELECT pol.polname AS name, pol.polcmd::text AS polcmd, pol.polpermissive AS permissive,
658
+ CASE WHEN pol.polroles = '{0}' THEN ARRAY['public']::text[]
659
+ ELSE ARRAY(SELECT rolname FROM pg_catalog.pg_roles WHERE oid = ANY(pol.polroles) ORDER BY rolname)::text[] END AS roles,
660
+ pg_get_expr(pol.polqual, pol.polrelid) AS using_expr,
661
+ pg_get_expr(pol.polwithcheck, pol.polrelid) AS with_check_expr
662
+ FROM pg_catalog.pg_policy pol
663
+ WHERE pol.polrelid = $1
664
+ ORDER BY pol.polname`, [rel.oid]);
665
+ return {
666
+ enabled: flags?.enabled ?? false,
667
+ forced: flags?.forced ?? false,
668
+ policies: policyRows.map((r) => ({
669
+ name: r.name,
670
+ command: COMMAND[r.polcmd] ?? 'ALL',
671
+ permissive: r.permissive,
672
+ roles: r.roles,
673
+ using: r.using_expr,
674
+ withCheck: r.with_check_expr,
675
+ })),
676
+ };
677
+ }
678
+ /** Table/view permissions — a thin `'table'`-kind wrapper over {@link getPermissionsByKind}, which is the fix for this function's former hardcoded `acldefault('r', …)` (only ever correct for tables/views; see that function's doc for the other four object kinds). */
679
+ export async function getPermissions(client, rel) {
680
+ return getPermissionsByKind(client, 'table', rel.oid);
681
+ }
682
+ // ---------------------------------------------------------------------------
683
+ // describe_object / get_schema composition helpers — inbound FKs, view
684
+ // definitions, and the non-relation kinds resolveRelation does not cover
685
+ // (sequences, functions, enum types). The latter three take plain bound
686
+ // schema/name params, never a RelationRef: they are not relations
687
+ // (relkind 'S'/pg_proc/pg_type), so there is nothing for resolveRelation to
688
+ // resolve and no SafeIdent is needed — see core/identifiers.ts's module doc.
689
+ // ---------------------------------------------------------------------------
690
+ /** Owner/comment/estimated-row-count for one already-resolved relation, by oid — the single-object counterpart of {@link listRelations}. */
691
+ export async function getRelationMeta(client, rel) {
692
+ const result = await rows(client, `SELECT pg_get_userbyid(c.relowner) AS owner, obj_description(c.oid, 'pg_class') AS comment,
693
+ NULLIF(c.reltuples, -1) AS estimated_rows
694
+ FROM pg_catalog.pg_class c
695
+ WHERE c.oid = $1`, [rel.oid]);
696
+ const r = result[0];
697
+ if (r === undefined)
698
+ return null;
699
+ return { owner: r.owner, comment: r.comment, estimatedRows: r.estimated_rows === null ? null : Math.round(r.estimated_rows) };
700
+ }
701
+ /** `pg_get_viewdef` — the reconstructed `SELECT` behind a view or materialized view. */
702
+ export async function getViewDefinition(client, rel) {
703
+ const result = await rows(client, `SELECT pg_get_viewdef($1::oid, true) AS definition`, [rel.oid]);
704
+ return result[0]?.definition ?? null;
705
+ }
706
+ /**
707
+ * Foreign keys pointing AT `rel` from other tables — the mirror of
708
+ * {@link getForeignKeys}. An agent planning a delete needs these: they are
709
+ * what would block or cascade, not the FKs `rel` itself declares.
710
+ */
711
+ export async function getInboundForeignKeys(client, rel) {
712
+ const ACTION = { a: 'NO ACTION', r: 'RESTRICT', c: 'CASCADE', n: 'SET NULL', d: 'SET DEFAULT' };
713
+ const result = await rows(client, `SELECT con.conname AS name, cn.nspname AS schema, cc.relname AS table,
714
+ ${ORDERED_COLUMNS_SUBQUERY('con.conkey', 'con.conrelid')} AS columns,
715
+ ${ORDERED_COLUMNS_SUBQUERY('con.confkey', 'con.confrelid')} AS ref_columns,
716
+ con.confupdtype AS on_update_code, con.confdeltype AS on_delete_code,
717
+ pg_get_constraintdef(con.oid) AS definition
718
+ FROM pg_catalog.pg_constraint con
719
+ JOIN pg_catalog.pg_class cc ON cc.oid = con.conrelid
720
+ JOIN pg_catalog.pg_namespace cn ON cn.oid = cc.relnamespace
721
+ WHERE con.confrelid = $1 AND con.contype = 'f'
722
+ ORDER BY cn.nspname, cc.relname, con.conname`, [rel.oid]);
723
+ return result.map((r) => ({
724
+ name: r.name,
725
+ schema: r.schema,
726
+ table: r.table,
727
+ columns: r.columns,
728
+ refColumns: r.ref_columns,
729
+ onUpdate: ACTION[r.on_update_code] ?? r.on_update_code,
730
+ onDelete: ACTION[r.on_delete_code] ?? r.on_delete_code,
731
+ definition: r.definition,
732
+ }));
733
+ }
734
+ /**
735
+ * Sequence parameters and current value by schema/name. `pg_sequences` is a
736
+ * `pg_catalog` system view (not `information_schema`), so this still honors
737
+ * design.md risk 5. Returns `null` when no such sequence exists.
738
+ */
739
+ export async function getSequenceDetail(client, schema, name) {
740
+ const result = await rows(client, `SELECT c.oid::int AS oid, n.nspname AS schema, c.relname AS name,
741
+ pg_get_userbyid(c.relowner) AS owner, obj_description(c.oid, 'pg_class') AS comment,
742
+ s.data_type::text AS data_type, s.start_value::text AS start_value, s.min_value::text AS min_value,
743
+ s.max_value::text AS max_value, s.increment_by::text AS increment_by, s.cycle,
744
+ s.cache_size::text AS cache_size, s.last_value::text AS last_value
745
+ FROM pg_catalog.pg_class c
746
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
747
+ JOIN pg_catalog.pg_sequences s ON s.schemaname = n.nspname AND s.sequencename = c.relname
748
+ WHERE n.nspname = $1 AND c.relname = $2 AND c.relkind = 'S'`, [schema, name]);
749
+ const r = result[0];
750
+ if (r === undefined)
751
+ return null;
752
+ return {
753
+ oid: r.oid,
754
+ schema: r.schema,
755
+ name: r.name,
756
+ owner: r.owner,
757
+ comment: r.comment,
758
+ dataType: r.data_type,
759
+ startValue: r.start_value,
760
+ minValue: r.min_value,
761
+ maxValue: r.max_value,
762
+ incrementBy: r.increment_by,
763
+ cycle: r.cycle,
764
+ cacheSize: r.cache_size,
765
+ lastValue: r.last_value,
766
+ };
767
+ }
768
+ /**
769
+ * Every overload named `schema.name`, each with its full `CREATE FUNCTION`
770
+ * body via `pg_get_functiondef`. Functions can be overloaded, so this always
771
+ * returns an array — never assume a single match.
772
+ */
773
+ export async function getFunctionDetails(client, schema, name) {
774
+ const result = await rows(client, `SELECT p.oid::int AS oid, n.nspname AS schema, p.proname AS name,
775
+ pg_get_function_identity_arguments(p.oid) AS arg_types,
776
+ pg_get_function_result(p.oid) AS return_type,
777
+ l.lanname AS language, p.prokind AS kind,
778
+ obj_description(p.oid, 'pg_proc') AS comment,
779
+ pg_get_functiondef(p.oid) AS definition
780
+ FROM pg_catalog.pg_proc p
781
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
782
+ JOIN pg_catalog.pg_language l ON l.oid = p.prolang
783
+ WHERE n.nspname = $1 AND p.proname = $2
784
+ ORDER BY p.oid`, [schema, name]);
785
+ return result.map((r) => ({
786
+ oid: r.oid,
787
+ schema: r.schema,
788
+ name: r.name,
789
+ argTypes: r.arg_types,
790
+ returnType: r.return_type,
791
+ language: r.language,
792
+ kind: r.kind,
793
+ comment: r.comment,
794
+ definition: r.definition,
795
+ }));
796
+ }
797
+ export async function listRoles(client, namePattern) {
798
+ const result = await rows(client, `SELECT r.oid::int AS oid, r.rolname AS name, r.rolsuper AS superuser, r.rolinherit AS inherit,
799
+ r.rolcreaterole AS create_role, r.rolcreatedb AS create_db, r.rolcanlogin AS can_login,
800
+ r.rolreplication AS replication, r.rolbypassrls AS bypass_rls, r.rolconnlimit AS connection_limit,
801
+ r.rolvaliduntil::text AS valid_until,
802
+ ARRAY(SELECT m.rolname FROM pg_catalog.pg_auth_members am
803
+ JOIN pg_catalog.pg_roles m ON m.oid = am.roleid
804
+ WHERE am.member = r.oid ORDER BY m.rolname)::text[] AS member_of
805
+ FROM pg_catalog.pg_roles r
806
+ WHERE ($1::text IS NULL OR r.rolname ILIKE $1)
807
+ ORDER BY r.rolname`, [namePattern ?? null]);
808
+ return result.map((r) => ({
809
+ oid: r.oid,
810
+ name: r.name,
811
+ superuser: r.superuser,
812
+ inherit: r.inherit,
813
+ createRole: r.create_role,
814
+ createDb: r.create_db,
815
+ canLogin: r.can_login,
816
+ replication: r.replication,
817
+ bypassRls: r.bypass_rls,
818
+ connectionLimit: r.connection_limit,
819
+ validUntil: r.valid_until,
820
+ memberOf: r.member_of ?? [],
821
+ }));
822
+ }
823
+ /**
824
+ * Objects a role owns or otherwise blocks `DROP ROLE` on, rendered via
825
+ * `pg_describe_object` — the precondition `postgres_admin`'s `roles_drop`
826
+ * checks BEFORE attempting the drop, so the failure surfaces as a readable
827
+ * list with a next action instead of PostgreSQL's raw
828
+ * `2BP01 role "x" cannot be dropped because some objects depend on it`.
829
+ */
830
+ export async function getRoleDependencies(client, roleOid) {
831
+ const result = await rows(client, `SELECT DISTINCT pg_catalog.pg_describe_object(dep.classid, dep.objid, dep.objsubid) AS object
832
+ FROM pg_catalog.pg_shdepend dep
833
+ WHERE dep.refobjid = $1 AND dep.refclassid = 'pg_catalog.pg_authid'::regclass
834
+ ORDER BY 1`, [roleOid]);
835
+ return result.map((r) => r.object);
836
+ }
837
+ /** `pg_available_extensions` — every extension whose control file is on this server, installed or not. Complements {@link listExtensions}, which is installed-only. */
838
+ export async function listAvailableExtensions(client, namePattern) {
839
+ const result = await rows(client, `SELECT name, default_version, installed_version, comment
840
+ FROM pg_catalog.pg_available_extensions
841
+ WHERE ($1::text IS NULL OR name ILIKE $1)
842
+ ORDER BY name`, [namePattern ?? null]);
843
+ return result.map((r) => ({ name: r.name, defaultVersion: r.default_version, installedVersion: r.installed_version, comment: r.comment }));
844
+ }
845
+ /** Exported for `test/unit/admin.test.ts` — the fixed table {@link getPermissionsByKind} splices `typeCode` from; asserting the five mappings directly is cheaper than mocking a `PoolClient` to exercise it indirectly. */
846
+ export const ACL_SOURCE = {
847
+ table: { table: 'pg_catalog.pg_class', aclCol: 'relacl', ownerCol: 'relowner', typeCode: 'r' },
848
+ sequence: { table: 'pg_catalog.pg_class', aclCol: 'relacl', ownerCol: 'relowner', typeCode: 'S' },
849
+ function: { table: 'pg_catalog.pg_proc', aclCol: 'proacl', ownerCol: 'proowner', typeCode: 'f' },
850
+ database: { table: 'pg_catalog.pg_database', aclCol: 'datacl', ownerCol: 'datdba', typeCode: 'd' },
851
+ schema: { table: 'pg_catalog.pg_namespace', aclCol: 'nspacl', ownerCol: 'nspowner', typeCode: 'n' },
852
+ };
853
+ /** The generalized form of {@link getPermissions} — correct for any of the five ACL-bearing catalogs, keyed by {@link AclObjectKind}. */
854
+ export async function getPermissionsByKind(client, kind, oid) {
855
+ const src = ACL_SOURCE[kind];
856
+ return rows(client, `SELECT CASE WHEN a.grantee = 0 THEN 'public' ELSE a.grantee::regrole::text END AS grantee,
857
+ a.privilege_type AS privilege, a.is_grantable AS grantable
858
+ FROM ${src.table} t,
859
+ LATERAL aclexplode(COALESCE(t.${src.aclCol}, acldefault('${src.typeCode}', t.${src.ownerCol}))) AS a
860
+ WHERE t.oid = $1
861
+ ORDER BY 1, 2`, [oid]);
862
+ }
863
+ /** `pg_stat_activity`, this database only by default (`datname = current_database()`) — set `allDatabases` to see the whole cluster. */
864
+ export async function listSessions(client, allDatabases = false) {
865
+ const result = await rows(client, `SELECT pid, usename, datname, client_addr::text AS client_addr, application_name,
866
+ backend_start::text AS backend_start, query_start::text AS query_start,
867
+ state, wait_event_type, wait_event, backend_type, query
868
+ FROM pg_catalog.pg_stat_activity
869
+ WHERE $1::bool OR datname = current_database()
870
+ ORDER BY pid`, [allDatabases]);
871
+ return result.map((r) => ({
872
+ pid: r.pid,
873
+ usename: r.usename,
874
+ datname: r.datname,
875
+ clientAddr: r.client_addr,
876
+ applicationName: r.application_name,
877
+ backendStart: r.backend_start,
878
+ queryStart: r.query_start,
879
+ state: r.state,
880
+ waitEventType: r.wait_event_type,
881
+ waitEvent: r.wait_event,
882
+ backendType: r.backend_type,
883
+ query: r.query,
884
+ }));
885
+ }
886
+ export async function getBackendPid(client) {
887
+ const result = await rows(client, `SELECT pg_backend_pid() AS pid`, []);
888
+ return result[0]?.pid ?? -1;
889
+ }
890
+ /** `backend_type`/`usename` for one pid — lets `sessions_terminate` refuse a system process (`backend_type <> 'client backend'`) by kind, not merely by pid. `null` when the pid is gone. */
891
+ export async function getBackendInfo(client, pid) {
892
+ const result = await rows(client, `SELECT backend_type, usename FROM pg_catalog.pg_stat_activity WHERE pid = $1`, [pid]);
893
+ const r = result[0];
894
+ return r === undefined ? null : { backendType: r.backend_type, usename: r.usename };
895
+ }
896
+ /** `pg_stat_user_tables` joined to `pg_statio_user_tables` by `relid` (not by name — see `_relation_detail.ts`'s module doc for why a name join is the wrong shape) plus `pg_total_relation_size`. `deadTuples`/`liveTuples` is the bloat *estimate* this server offers: a true bloat number needs `pgstattuple`, which is not assumed installed. */
897
+ export async function getTableStats(client, schemas, namePattern) {
898
+ if (schemas.length === 0)
899
+ return [];
900
+ const result = await rows(client, `SELECT s.schemaname AS schema, s.relname AS name,
901
+ s.seq_scan::text AS seq_scan, s.idx_scan::text AS idx_scan,
902
+ s.n_tup_ins::text AS rows_inserted, s.n_tup_upd::text AS rows_updated, s.n_tup_del::text AS rows_deleted,
903
+ s.n_live_tup::text AS live_tuples, s.n_dead_tup::text AS dead_tuples,
904
+ s.last_vacuum::text AS last_vacuum, s.last_autovacuum::text AS last_autovacuum,
905
+ s.last_analyze::text AS last_analyze, s.last_autoanalyze::text AS last_autoanalyze,
906
+ io.heap_blks_read::text AS heap_blks_read, io.heap_blks_hit::text AS heap_blks_hit,
907
+ pg_total_relation_size(s.relid)::text AS total_bytes
908
+ FROM pg_catalog.pg_stat_user_tables s
909
+ LEFT JOIN pg_catalog.pg_statio_user_tables io ON io.relid = s.relid
910
+ WHERE s.schemaname = ANY($1) AND ($2::text IS NULL OR s.relname ILIKE $2)
911
+ ORDER BY s.schemaname, s.relname`, [schemas, namePattern ?? null]);
912
+ return result.map((r) => ({
913
+ schema: r.schema,
914
+ name: r.name,
915
+ seqScan: r.seq_scan,
916
+ idxScan: r.idx_scan,
917
+ rowsInserted: r.rows_inserted,
918
+ rowsUpdated: r.rows_updated,
919
+ rowsDeleted: r.rows_deleted,
920
+ liveTuples: r.live_tuples,
921
+ deadTuples: r.dead_tuples,
922
+ lastVacuum: r.last_vacuum,
923
+ lastAutovacuum: r.last_autovacuum,
924
+ lastAnalyze: r.last_analyze,
925
+ lastAutoanalyze: r.last_autoanalyze,
926
+ heapBlksRead: r.heap_blks_read,
927
+ heapBlksHit: r.heap_blks_hit,
928
+ totalBytes: r.total_bytes,
929
+ }));
930
+ }
931
+ export async function getIndexStats(client, schemas, namePattern) {
932
+ if (schemas.length === 0)
933
+ return [];
934
+ const result = await rows(client, `SELECT s.schemaname AS schema, s.relname AS table, s.indexrelname AS index,
935
+ s.idx_scan::text AS idx_scan, s.idx_tup_read::text AS idx_tup_read, s.idx_tup_fetch::text AS idx_tup_fetch,
936
+ io.idx_blks_read::text AS idx_blks_read, io.idx_blks_hit::text AS idx_blks_hit,
937
+ pg_relation_size(s.indexrelid)::text AS size_bytes
938
+ FROM pg_catalog.pg_stat_user_indexes s
939
+ LEFT JOIN pg_catalog.pg_statio_user_indexes io ON io.indexrelid = s.indexrelid
940
+ WHERE s.schemaname = ANY($1) AND ($2::text IS NULL OR s.relname ILIKE $2)
941
+ ORDER BY s.schemaname, s.relname, s.indexrelname`, [schemas, namePattern ?? null]);
942
+ return result.map((r) => ({
943
+ schema: r.schema,
944
+ table: r.table,
945
+ index: r.index,
946
+ idxScan: r.idx_scan,
947
+ idxTupRead: r.idx_tup_read,
948
+ idxTupFetch: r.idx_tup_fetch,
949
+ idxBlksRead: r.idx_blks_read,
950
+ idxBlksHit: r.idx_blks_hit,
951
+ sizeBytes: r.size_bytes,
952
+ }));
953
+ }
954
+ /** `state <> 'idle'` and running longer than `thresholdMs` — `query_start` is null for a backend between statements, hence the extra guard. */
955
+ export async function getLongRunningQueries(client, thresholdMs) {
956
+ const result = await rows(client, `SELECT pid, usename, datname, state,
957
+ extract(epoch FROM (now() - query_start))::float8 * 1000 AS duration_ms_raw,
958
+ (extract(epoch FROM (now() - query_start)) * 1000)::bigint::text AS duration_ms,
959
+ query
960
+ FROM pg_catalog.pg_stat_activity
961
+ WHERE state IS DISTINCT FROM 'idle' AND query_start IS NOT NULL
962
+ AND extract(epoch FROM (now() - query_start)) * 1000 >= $1
963
+ ORDER BY query_start ASC`, [thresholdMs]);
964
+ return result.map((r) => ({ pid: r.pid, usename: r.usename, datname: r.datname, state: r.state, durationMs: r.duration_ms, query: r.query }));
965
+ }
966
+ /** The canonical `pg_locks` self-join for "who is blocking whom" — every lock field that matters for `NOT DISTINCT FROM` equality, per the PostgreSQL wiki's own reference query. */
967
+ export async function getBlockedQueries(client) {
968
+ const result = await rows(client, `SELECT blocked.pid AS blocked_pid, blocked.usename AS blocked_user, blocked.query AS blocked_query,
969
+ blocking.pid AS blocking_pid, blocking.usename AS blocking_user, blocking.query AS blocking_query
970
+ FROM pg_catalog.pg_locks blocked_locks
971
+ JOIN pg_catalog.pg_stat_activity blocked ON blocked.pid = blocked_locks.pid
972
+ JOIN pg_catalog.pg_locks blocking_locks
973
+ ON blocking_locks.locktype = blocked_locks.locktype
974
+ AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
975
+ AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
976
+ AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
977
+ AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
978
+ AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
979
+ AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
980
+ AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
981
+ AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
982
+ AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
983
+ AND blocking_locks.pid <> blocked_locks.pid
984
+ JOIN pg_catalog.pg_stat_activity blocking ON blocking.pid = blocking_locks.pid
985
+ WHERE NOT blocked_locks.granted`, []);
986
+ return result.map((r) => ({
987
+ blockedPid: r.blocked_pid,
988
+ blockedUser: r.blocked_user,
989
+ blockedQuery: r.blocked_query,
990
+ blockingPid: r.blocking_pid,
991
+ blockingUser: r.blocking_user,
992
+ blockingQuery: r.blocking_query,
993
+ }));
994
+ }
995
+ /** One enum type by schema/name — the single-object counterpart of {@link listEnumTypes}. `null` when no such enum exists. */
996
+ export async function getEnumTypeDetail(client, schema, name) {
997
+ const result = await rows(client, `SELECT t.oid::int AS oid, n.nspname AS schema, t.typname AS name,
998
+ ARRAY(SELECT e.enumlabel FROM pg_catalog.pg_enum e
999
+ WHERE e.enumtypid = t.oid ORDER BY e.enumsortorder)::text[] AS labels,
1000
+ obj_description(t.oid, 'pg_type') AS comment
1001
+ FROM pg_catalog.pg_type t
1002
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
1003
+ WHERE t.typtype = 'e' AND n.nspname = $1 AND t.typname = $2`, [schema, name]);
1004
+ return result[0] ?? null;
1005
+ }
1006
+ //# sourceMappingURL=catalog.js.map