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.
- package/README.md +608 -2
- package/dist/config/env.js +67 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/profiles.js +257 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/core/audit-context.js +21 -0
- package/dist/core/audit-context.js.map +1 -0
- package/dist/core/audit.js +91 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/brands.js +24 -0
- package/dist/core/brands.js.map +1 -0
- package/dist/core/classifier.js +279 -0
- package/dist/core/classifier.js.map +1 -0
- package/dist/core/errors.js +197 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/format.js +258 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/guards.js +513 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/identifiers.js +431 -0
- package/dist/core/identifiers.js.map +1 -0
- package/dist/core/limits.js +79 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/logger.js +34 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paginate.js +195 -0
- package/dist/core/paginate.js.map +1 -0
- package/dist/core/paths.js +392 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/policy.js +98 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/shutdown.js +10 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/stdout-guard.js +25 -0
- package/dist/core/stdout-guard.js.map +1 -0
- package/dist/db/catalog.js +1006 -0
- package/dist/db/catalog.js.map +1 -0
- package/dist/db/pool.js +158 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/db/session.js +258 -0
- package/dist/db/session.js.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/index.js +88 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/runtime/binaries.js +356 -0
- package/dist/runtime/binaries.js.map +1 -0
- package/dist/runtime/env-file.js +62 -0
- package/dist/runtime/env-file.js.map +1 -0
- package/dist/runtime/environment.js +34 -0
- package/dist/runtime/environment.js.map +1 -0
- package/dist/server.js +106 -0
- package/dist/server.js.map +1 -0
- package/dist/snapshot/capture.js +189 -0
- package/dist/snapshot/capture.js.map +1 -0
- package/dist/snapshot/diff.js +254 -0
- package/dist/snapshot/diff.js.map +1 -0
- package/dist/snapshot/migrate.js +1596 -0
- package/dist/snapshot/migrate.js.map +1 -0
- package/dist/snapshot/model.js +80 -0
- package/dist/snapshot/model.js.map +1 -0
- package/dist/tools/_archive_meta.js +138 -0
- package/dist/tools/_archive_meta.js.map +1 -0
- package/dist/tools/_batch_shared.js +73 -0
- package/dist/tools/_batch_shared.js.map +1 -0
- package/dist/tools/_export_format.js +107 -0
- package/dist/tools/_export_format.js.map +1 -0
- package/dist/tools/_object_kinds.js +97 -0
- package/dist/tools/_object_kinds.js.map +1 -0
- package/dist/tools/_relation_detail.js +55 -0
- package/dist/tools/_relation_detail.js.map +1 -0
- package/dist/tools/_shared.js +17 -0
- package/dist/tools/_shared.js.map +1 -0
- package/dist/tools/_snapshot_io.js +238 -0
- package/dist/tools/_snapshot_io.js.map +1 -0
- package/dist/tools/admin/_shared.js +18 -0
- package/dist/tools/admin/_shared.js.map +1 -0
- package/dist/tools/admin/extensions.js +80 -0
- package/dist/tools/admin/extensions.js.map +1 -0
- package/dist/tools/admin/grants.js +207 -0
- package/dist/tools/admin/grants.js.map +1 -0
- package/dist/tools/admin/index.js +138 -0
- package/dist/tools/admin/index.js.map +1 -0
- package/dist/tools/admin/maintenance.js +106 -0
- package/dist/tools/admin/maintenance.js.map +1 -0
- package/dist/tools/admin/roles.js +144 -0
- package/dist/tools/admin/roles.js.map +1 -0
- package/dist/tools/admin/sessions.js +104 -0
- package/dist/tools/admin/sessions.js.map +1 -0
- package/dist/tools/admin/stats.js +63 -0
- package/dist/tools/admin/stats.js.map +1 -0
- package/dist/tools/backup.js +288 -0
- package/dist/tools/backup.js.map +1 -0
- package/dist/tools/connection_status.js +138 -0
- package/dist/tools/connection_status.js.map +1 -0
- package/dist/tools/copy.js +355 -0
- package/dist/tools/copy.js.map +1 -0
- package/dist/tools/delete_batch.js +73 -0
- package/dist/tools/delete_batch.js.map +1 -0
- package/dist/tools/describe_object.js +87 -0
- package/dist/tools/describe_object.js.map +1 -0
- package/dist/tools/execute_sql.js +395 -0
- package/dist/tools/execute_sql.js.map +1 -0
- package/dist/tools/explain.js +134 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/export.js +215 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/get_schema.js +148 -0
- package/dist/tools/get_schema.js.map +1 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/insert_batch.js +212 -0
- package/dist/tools/insert_batch.js.map +1 -0
- package/dist/tools/list_objects.js +160 -0
- package/dist/tools/list_objects.js.map +1 -0
- package/dist/tools/prepare_migration.js +112 -0
- package/dist/tools/prepare_migration.js.map +1 -0
- package/dist/tools/restore.js +301 -0
- package/dist/tools/restore.js.map +1 -0
- package/dist/tools/schema_diff.js +79 -0
- package/dist/tools/schema_diff.js.map +1 -0
- package/dist/tools/schema_snapshot.js +57 -0
- package/dist/tools/schema_snapshot.js.map +1 -0
- package/dist/tools/select.js +162 -0
- package/dist/tools/select.js.map +1 -0
- package/dist/tools/update_batch.js +154 -0
- package/dist/tools/update_batch.js.map +1 -0
- 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
|