@kazurolabs/psql-explorer 0.0.0-stage → 0.3.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +505 -2
  3. package/RELEASES.md +87 -0
  4. package/dist/app.js +39 -0
  5. package/dist/cli.js +307 -0
  6. package/dist/commands/configgen.js +128 -0
  7. package/dist/commands/explore.js +388 -0
  8. package/dist/commands/terminal-prompt.js +68 -0
  9. package/dist/config/load.js +324 -0
  10. package/dist/config/resolve-environment.js +112 -0
  11. package/dist/config/resolvers/env.js +12 -0
  12. package/dist/config/resolvers/file.js +24 -0
  13. package/dist/config/resolvers/groups.js +1 -0
  14. package/dist/config/resolvers/index.js +49 -0
  15. package/dist/config/resolvers/literal.js +7 -0
  16. package/dist/config/resolvers/prompt.js +159 -0
  17. package/dist/config/resolvers/stored.js +13 -0
  18. package/dist/config/resolvers/types.js +18 -0
  19. package/dist/config/resolvers/vault.js +283 -0
  20. package/dist/config/schema.js +24 -0
  21. package/dist/config/source.js +105 -0
  22. package/dist/db/connect.js +55 -0
  23. package/dist/db/identifiers.js +6 -0
  24. package/dist/db/paths.js +14 -0
  25. package/dist/db/store.js +323 -0
  26. package/dist/db/tester.js +190 -0
  27. package/dist/db/validate.js +99 -0
  28. package/dist/environments.js +140 -0
  29. package/dist/explorer/alter.js +493 -0
  30. package/dist/explorer/catalog.js +414 -0
  31. package/dist/explorer/connect-entry.js +42 -0
  32. package/dist/explorer/cypher-parse.js +478 -0
  33. package/dist/explorer/cypher.js +963 -0
  34. package/dist/explorer/ddl.js +502 -0
  35. package/dist/explorer/dependents.js +222 -0
  36. package/dist/explorer/design-load.js +254 -0
  37. package/dist/explorer/design.js +45 -0
  38. package/dist/explorer/edit.js +108 -0
  39. package/dist/explorer/explain.js +37 -0
  40. package/dist/explorer/filter.js +121 -0
  41. package/dist/explorer/format.js +134 -0
  42. package/dist/explorer/graph.js +679 -0
  43. package/dist/explorer/gucs.js +31 -0
  44. package/dist/explorer/profile.js +102 -0
  45. package/dist/explorer/references.js +95 -0
  46. package/dist/explorer/rows.js +64 -0
  47. package/dist/explorer/schema-diff.js +360 -0
  48. package/dist/explorer/search.js +309 -0
  49. package/dist/explorer/security.js +152 -0
  50. package/dist/explorer/seed.js +1376 -0
  51. package/dist/explorer/session.js +122 -0
  52. package/dist/explorer/stats.js +135 -0
  53. package/dist/explorer/timeout.js +13 -0
  54. package/dist/prompts.js +52 -0
  55. package/dist/screens/environments.js +229 -0
  56. package/dist/screens/explorer.js +145 -0
  57. package/dist/screens/header.js +8 -0
  58. package/dist/screens/index.js +13 -0
  59. package/dist/screens/new-connection.js +178 -0
  60. package/dist/screens/panes/common.js +35 -0
  61. package/dist/screens/panes/history.js +36 -0
  62. package/dist/screens/panes/info.js +61 -0
  63. package/dist/screens/panes/query.js +113 -0
  64. package/dist/screens/panes/table.js +178 -0
  65. package/dist/screens/panes/tables.js +142 -0
  66. package/dist/screens/prompt-modal.js +18 -0
  67. package/dist/screens/record-view.js +42 -0
  68. package/dist/screens/result-grid.js +126 -0
  69. package/dist/screens/scroll.js +12 -0
  70. package/dist/screens/splash.js +78 -0
  71. package/dist/screens/terminal.js +25 -0
  72. package/dist/screens/types.js +1 -0
  73. package/dist/services.js +1 -0
  74. package/dist/startup.js +84 -0
  75. package/dist/web/server.js +988 -0
  76. package/package.json +69 -5
  77. package/web/dist/assets/anton-latin-400-normal-AUNGEG_V.woff +0 -0
  78. package/web/dist/assets/anton-latin-400-normal-Byf51wtH.woff2 +0 -0
  79. package/web/dist/assets/anton-latin-ext-400-normal-BMODBQc6.woff +0 -0
  80. package/web/dist/assets/anton-latin-ext-400-normal-SyiqE2Jt.woff2 +0 -0
  81. package/web/dist/assets/anton-vietnamese-400-normal-2FfR1wHA.woff +0 -0
  82. package/web/dist/assets/anton-vietnamese-400-normal-CkBxLiRJ.woff2 +0 -0
  83. package/web/dist/assets/antonio-latin-400-normal-B7iMKlzw.woff +0 -0
  84. package/web/dist/assets/antonio-latin-400-normal-CZjzwNAN.woff2 +0 -0
  85. package/web/dist/assets/antonio-latin-700-normal-BY3mCh47.woff +0 -0
  86. package/web/dist/assets/antonio-latin-700-normal-DSH-lEDG.woff2 +0 -0
  87. package/web/dist/assets/antonio-latin-ext-400-normal-nE82dizb.woff2 +0 -0
  88. package/web/dist/assets/antonio-latin-ext-400-normal-qA9Jb92z.woff +0 -0
  89. package/web/dist/assets/antonio-latin-ext-700-normal-BdSkr58W.woff +0 -0
  90. package/web/dist/assets/antonio-latin-ext-700-normal-D-dvgqjr.woff2 +0 -0
  91. package/web/dist/assets/button-ClA70PLR.js +1 -0
  92. package/web/dist/assets/code-xml-CXkpHeU9.js +1 -0
  93. package/web/dist/assets/compare-view-DKl5l_Jw.js +2 -0
  94. package/web/dist/assets/copy-CCXzeW-s.js +1 -0
  95. package/web/dist/assets/cypher-view-Pd0WFH8F.js +13 -0
  96. package/web/dist/assets/design-view-CIHSmH4u.js +9 -0
  97. package/web/dist/assets/erd-view-CV7mFGAN.js +1 -0
  98. package/web/dist/assets/format-sql-BLj5_RDZ.js +5 -0
  99. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  100. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  101. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  102. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  103. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  104. package/web/dist/assets/graph-force-yBLyErpS.js +1 -0
  105. package/web/dist/assets/graph-link-OCo_s0zO.js +1 -0
  106. package/web/dist/assets/graph-view-sM1OlByf.js +3 -0
  107. package/web/dist/assets/index-CDcnUacl.js +10 -0
  108. package/web/dist/assets/index-OzJqnJWG.css +2 -0
  109. package/web/dist/assets/key-round-Cv_7QeED.js +1 -0
  110. package/web/dist/assets/loader-circle-BK_GMdvN.js +1 -0
  111. package/web/dist/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
  112. package/web/dist/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
  113. package/web/dist/assets/orbitron-latin-900-normal-BbJatbhp.woff +0 -0
  114. package/web/dist/assets/orbitron-latin-900-normal-DrIi7unX.woff2 +0 -0
  115. package/web/dist/assets/query-builder-view-ChNFb51g.js +4 -0
  116. package/web/dist/assets/rotate-ccw-CdCaf7Q6.js +1 -0
  117. package/web/dist/assets/route-DO6IZ7of.js +15 -0
  118. package/web/dist/assets/seed-view-DFNB8WHE.js +7 -0
  119. package/web/dist/assets/share-tech-mono-latin-400-normal-15MgdvEM.woff +0 -0
  120. package/web/dist/assets/share-tech-mono-latin-400-normal-CCqdbj5z.woff2 +0 -0
  121. package/web/dist/assets/shield-alert-CMS-3j4U.js +1 -0
  122. package/web/dist/assets/sliders-horizontal-Dg51yc-C.js +1 -0
  123. package/web/dist/assets/sql-tabs-B4jt6bQj.js +2 -0
  124. package/web/dist/assets/table-DfMz2f8M.js +1 -0
  125. package/web/dist/assets/triangle-alert-COGnHCD1.js +1 -0
  126. package/web/dist/assets/zone-D9dXk8tn.js +5 -0
  127. package/web/dist/index.html +26 -0
@@ -0,0 +1,414 @@
1
+ // What is in a database, read from the system catalogs: schemas, their tables, and each table's
2
+ // columns, indexes and constraints. Every name from outside is passed as a parameter or quoted.
3
+ import { quoteIdentifier } from '../db/identifiers.js';
4
+ const kinds = {
5
+ r: 'table',
6
+ p: 'partitioned',
7
+ v: 'view',
8
+ m: 'materialized',
9
+ f: 'foreign',
10
+ };
11
+ export const qualified = (table) => `${quoteIdentifier(table.schema)}.${quoteIdentifier(table.name)}`;
12
+ export const tableLabel = (table) => `${table.schema}.${table.name}`;
13
+ /** "users" → in the default schema; "app.users" → in app. */
14
+ export function parseTableRef(text, defaultSchema) {
15
+ const at = text.indexOf('.');
16
+ if (at <= 0 || at === text.length - 1)
17
+ return { schema: defaultSchema, name: text };
18
+ return { schema: text.slice(0, at), name: text.slice(at + 1) };
19
+ }
20
+ const constraintKinds = {
21
+ p: 'primary key',
22
+ f: 'foreign key',
23
+ u: 'unique',
24
+ c: 'check',
25
+ x: 'exclusion',
26
+ n: 'not null',
27
+ t: 'trigger',
28
+ };
29
+ const functionKinds = {
30
+ f: 'function',
31
+ p: 'procedure',
32
+ a: 'aggregate',
33
+ w: 'window',
34
+ };
35
+ const typeKinds = {
36
+ e: 'enum',
37
+ c: 'composite',
38
+ d: 'domain',
39
+ r: 'range',
40
+ m: 'multirange',
41
+ };
42
+ const count = (value) => (value === null ? null : Number(value));
43
+ const flag = (value) => value === 't';
44
+ /** The schemas a person works with: not pg_catalog, information_schema or pg_toast and the like. */
45
+ export async function listSchemas(session) {
46
+ const rows = await session.rows(`SELECT n.nspname AS name, pg_catalog.pg_get_userbyid(n.nspowner) AS owner,
47
+ (SELECT count(*) FROM pg_catalog.pg_class c
48
+ WHERE c.relnamespace = n.oid AND c.relkind IN ('r', 'p', 'v', 'm', 'f')) AS tables
49
+ FROM pg_catalog.pg_namespace n
50
+ WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'
51
+ ORDER BY n.nspname`);
52
+ return rows.map((row) => ({
53
+ name: row.name ?? '',
54
+ owner: row.owner ?? '',
55
+ tables: Number(row.tables),
56
+ }));
57
+ }
58
+ /** Tables, views, materialized views and foreign tables of a schema, by name (or just `name`). */
59
+ export async function listTables(session, schema, name) {
60
+ const rows = await session.rows(`SELECT c.relname AS name, c.relkind AS kind,
61
+ CASE WHEN c.reltuples < 0 OR c.relkind IN ('v', 'f') THEN NULL
62
+ ELSE c.reltuples::int8 END AS estimated_rows,
63
+ CASE WHEN c.relkind IN ('r', 'p', 'm') THEN pg_catalog.pg_total_relation_size(c.oid)
64
+ END AS total_bytes,
65
+ to_char(greatest(s.last_vacuum, s.last_autovacuum) AT TIME ZONE 'UTC',
66
+ 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS last_vacuum,
67
+ to_char(greatest(s.last_analyze, s.last_autoanalyze) AT TIME ZONE 'UTC',
68
+ 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS last_analyze,
69
+ pg_catalog.obj_description(c.oid, 'pg_class') AS comment
70
+ FROM pg_catalog.pg_class c
71
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
72
+ LEFT JOIN pg_catalog.pg_stat_all_tables s ON s.relid = c.oid
73
+ WHERE n.nspname = $1 AND c.relkind IN ('r', 'p', 'v', 'm', 'f')
74
+ AND ($2::text IS NULL OR c.relname = $2)
75
+ ORDER BY c.relname`, [schema, name ?? null]);
76
+ return rows.map((row) => ({
77
+ schema,
78
+ name: row.name ?? '',
79
+ kind: kinds[row.kind ?? ''] ?? 'table',
80
+ estimatedRows: count(row.estimated_rows),
81
+ totalBytes: count(row.total_bytes),
82
+ lastVacuum: row.last_vacuum,
83
+ lastAnalyze: row.last_analyze,
84
+ comment: row.comment,
85
+ }));
86
+ }
87
+ export async function listColumns(session, table) {
88
+ const rows = await session.rows(`SELECT a.attname AS name, pg_catalog.format_type(a.atttypid, a.atttypmod) AS type,
89
+ NOT a.attnotnull AS nullable,
90
+ pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS default,
91
+ EXISTS (SELECT 1 FROM pg_catalog.pg_index i
92
+ WHERE i.indrelid = a.attrelid AND i.indisprimary AND a.attnum = ANY (i.indkey)) AS primary_key,
93
+ EXISTS (SELECT 1 FROM pg_catalog.pg_index i
94
+ WHERE i.indrelid = a.attrelid AND i.indisunique AND i.indnkeyatts = 1
95
+ AND i.indpred IS NULL AND i.indkey[0] = a.attnum) AS is_unique,
96
+ fk.ref_schema, fk.ref_table, fk.ref_column,
97
+ pg_catalog.col_description(a.attrelid, a.attnum) AS comment
98
+ FROM pg_catalog.pg_attribute a
99
+ LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
100
+ LEFT JOIN LATERAL (
101
+ SELECT rn.nspname AS ref_schema, rc.relname AS ref_table, ra.attname AS ref_column
102
+ FROM pg_catalog.pg_constraint c
103
+ CROSS JOIN LATERAL unnest(c.conkey, c.confkey) AS k(attnum, refattnum)
104
+ JOIN pg_catalog.pg_class rc ON rc.oid = c.confrelid
105
+ JOIN pg_catalog.pg_namespace rn ON rn.oid = rc.relnamespace
106
+ JOIN pg_catalog.pg_attribute ra ON ra.attrelid = c.confrelid AND ra.attnum = k.refattnum
107
+ WHERE c.conrelid = a.attrelid AND c.contype = 'f' AND k.attnum = a.attnum
108
+ ORDER BY c.conname
109
+ LIMIT 1
110
+ ) fk ON true
111
+ WHERE a.attrelid = $1::regclass AND a.attnum > 0 AND NOT a.attisdropped
112
+ ORDER BY a.attnum`, [qualified(table)]);
113
+ return rows.map((row) => ({
114
+ name: row.name ?? '',
115
+ type: row.type ?? '',
116
+ nullable: flag(row.nullable),
117
+ default: row.default,
118
+ primaryKey: flag(row.primary_key),
119
+ unique: flag(row.is_unique),
120
+ references: row.ref_table === null
121
+ ? null
122
+ : { schema: row.ref_schema ?? '', table: row.ref_table ?? '', column: row.ref_column ?? '' },
123
+ comment: row.comment,
124
+ }));
125
+ }
126
+ export async function listIndexes(session, table) {
127
+ const rows = await session.rows(`SELECT i.relname AS name, pg_catalog.pg_get_indexdef(ix.indexrelid) AS definition,
128
+ ix.indisprimary AS primary, ix.indisunique AS unique,
129
+ pg_catalog.pg_relation_size(ix.indexrelid) AS bytes
130
+ FROM pg_catalog.pg_index ix
131
+ JOIN pg_catalog.pg_class i ON i.oid = ix.indexrelid
132
+ WHERE ix.indrelid = $1::regclass
133
+ ORDER BY ix.indisprimary DESC, i.relname`, [qualified(table)]);
134
+ return rows.map((row) => ({
135
+ name: row.name ?? '',
136
+ definition: row.definition ?? '',
137
+ primary: flag(row.primary),
138
+ unique: flag(row.unique),
139
+ bytes: Number(row.bytes),
140
+ }));
141
+ }
142
+ export async function listConstraints(session, table) {
143
+ const rows = await session.rows(`SELECT conname AS name, contype AS kind, pg_catalog.pg_get_constraintdef(oid, true) AS definition
144
+ FROM pg_catalog.pg_constraint
145
+ WHERE conrelid = $1::regclass
146
+ ORDER BY array_position(ARRAY['p', 'u', 'f', 'c', 'x', 'n', 't']::"char"[], contype), conname`, [qualified(table)]);
147
+ return rows.map((row) => ({
148
+ name: row.name ?? '',
149
+ kind: constraintKinds[row.kind ?? ''] ?? 'check',
150
+ definition: row.definition ?? '',
151
+ }));
152
+ }
153
+ /** Columns, indexes and constraints of one table. Throws when there is no such table. */
154
+ export async function describeTable(session, table) {
155
+ const columns = await listColumns(session, table);
156
+ const indexes = await listIndexes(session, table);
157
+ const constraints = await listConstraints(session, table);
158
+ return { table, columns, indexes, constraints };
159
+ }
160
+ export async function listFunctions(session, schema) {
161
+ const rows = await session.rows(`SELECT p.proname AS name, pg_catalog.pg_get_function_identity_arguments(p.oid) AS args,
162
+ p.prokind AS kind,
163
+ CASE WHEN p.prokind = 'p' THEN NULL ELSE pg_catalog.pg_get_function_result(p.oid) END AS returns,
164
+ l.lanname AS language, pg_catalog.obj_description(p.oid, 'pg_proc') AS comment
165
+ FROM pg_catalog.pg_proc p
166
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
167
+ JOIN pg_catalog.pg_language l ON l.oid = p.prolang
168
+ WHERE n.nspname = $1
169
+ ORDER BY p.proname, args`, [schema]);
170
+ return rows.map((row) => ({
171
+ schema,
172
+ name: row.name ?? '',
173
+ args: row.args ?? '',
174
+ kind: functionKinds[row.kind ?? ''] ?? 'function',
175
+ returns: row.returns,
176
+ language: row.language ?? '',
177
+ comment: row.comment,
178
+ }));
179
+ }
180
+ export async function listSequences(session, schema) {
181
+ const rows = await session.rows(`SELECT s.sequencename AS name, s.data_type::text AS data_type,
182
+ (SELECT r.relname || '.' || a.attname
183
+ FROM pg_catalog.pg_depend d
184
+ JOIN pg_catalog.pg_class r ON r.oid = d.refobjid
185
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
186
+ WHERE d.objid = c.oid AND d.classid = 'pg_catalog.pg_class'::regclass
187
+ AND d.refclassid = 'pg_catalog.pg_class'::regclass AND d.deptype IN ('a', 'i')
188
+ LIMIT 1) AS owned_by
189
+ FROM pg_catalog.pg_sequences s
190
+ JOIN pg_catalog.pg_namespace n ON n.nspname = s.schemaname
191
+ JOIN pg_catalog.pg_class c ON c.relname = s.sequencename AND c.relnamespace = n.oid
192
+ WHERE s.schemaname = $1
193
+ ORDER BY s.sequencename`, [schema]);
194
+ return rows.map((row) => ({
195
+ schema,
196
+ name: row.name ?? '',
197
+ dataType: row.data_type ?? '',
198
+ ownedBy: row.owned_by,
199
+ }));
200
+ }
201
+ /** Enums, composite types, domains and ranges; not the row types that tables and views bring. */
202
+ export async function listTypes(session, schema) {
203
+ const rows = await session.rows(`SELECT t.typname AS name, t.typtype AS kind, pg_catalog.obj_description(t.oid, 'pg_type') AS comment
204
+ FROM pg_catalog.pg_type t
205
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
206
+ WHERE n.nspname = $1 AND t.typtype IN ('e', 'c', 'd', 'r', 'm')
207
+ AND (t.typtype <> 'c' OR EXISTS (
208
+ SELECT 1 FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid AND c.relkind = 'c'))
209
+ ORDER BY t.typname`, [schema]);
210
+ return rows.map((row) => ({
211
+ schema,
212
+ name: row.name ?? '',
213
+ kind: typeKinds[row.kind ?? ''] ?? 'composite',
214
+ comment: row.comment,
215
+ }));
216
+ }
217
+ /** Tables, functions, sequences and types of one schema. */
218
+ export async function listObjects(session, schema) {
219
+ const tables = await listTables(session, schema);
220
+ const functions = await listFunctions(session, schema);
221
+ const sequences = await listSequences(session, schema);
222
+ const types = await listTypes(session, schema);
223
+ return { tables, functions, sequences, types };
224
+ }
225
+ /** The tables of a schema with their columns, whose foreign keys are the relations of an ERD. */
226
+ export async function describeErd(session, schema) {
227
+ const tables = (await listTables(session, schema)).filter((table) => table.kind === 'table' || table.kind === 'partitioned');
228
+ const erd = [];
229
+ for (const table of tables)
230
+ erd.push({ ...table, columns: await listColumns(session, table) });
231
+ return erd;
232
+ }
233
+ /** Every table, view, ... of every schema with its columns, for completing and explaining SQL. */
234
+ export async function describeCatalog(session) {
235
+ const catalog = [];
236
+ for (const schema of await listSchemas(session)) {
237
+ for (const table of await listTables(session, schema.name)) {
238
+ catalog.push({ ...table, columns: await listColumns(session, table) });
239
+ }
240
+ }
241
+ return catalog;
242
+ }
243
+ /** Every function of every user schema with its declared arguments, for explaining calls in SQL. */
244
+ export async function describeFunctionCatalog(session) {
245
+ const rows = await session.rows(`SELECT n.nspname AS schema, p.proname AS name,
246
+ pg_catalog.pg_get_function_identity_arguments(p.oid) AS args,
247
+ pg_catalog.pg_get_function_arguments(p.oid) AS signature, p.prokind AS kind,
248
+ CASE WHEN p.prokind = 'p' THEN NULL ELSE pg_catalog.pg_get_function_result(p.oid) END AS returns,
249
+ l.lanname AS language, pg_catalog.obj_description(p.oid, 'pg_proc') AS comment
250
+ FROM pg_catalog.pg_proc p
251
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
252
+ JOIN pg_catalog.pg_language l ON l.oid = p.prolang
253
+ WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'
254
+ OR n.nspname = 'pg_catalog' AND p.prokind IN ('f', 'a', 'w') AND p.proname !~ '^_'
255
+ ORDER BY n.nspname, p.proname, args`);
256
+ return rows.map((row) => ({
257
+ schema: row.schema ?? '',
258
+ name: row.name ?? '',
259
+ args: row.args ?? '',
260
+ signature: row.signature ?? '',
261
+ kind: functionKinds[row.kind ?? ''] ?? 'function',
262
+ returns: row.returns,
263
+ language: row.language ?? '',
264
+ comment: row.comment,
265
+ }));
266
+ }
267
+ /** A table, view, ... with its columns, indexes and constraints. Null when there is no such one. */
268
+ export async function describeRelation(session, table) {
269
+ const [info] = await listTables(session, table.schema, table.name);
270
+ if (!info)
271
+ return null;
272
+ const details = await describeTable(session, table);
273
+ let definition = null;
274
+ if (info.kind === 'view' || info.kind === 'materialized') {
275
+ const [row] = await session.rows('SELECT pg_catalog.pg_get_viewdef($1::regclass, true) AS definition', [qualified(table)]);
276
+ definition = row?.definition ?? null;
277
+ }
278
+ return { ...info, ...details, definition };
279
+ }
280
+ /** Null when there is no function of that name with those argument types. */
281
+ export async function describeFunction(session, ref) {
282
+ const [row] = await session.rows(`SELECT p.prokind AS kind,
283
+ CASE WHEN p.prokind = 'p' THEN NULL ELSE pg_catalog.pg_get_function_result(p.oid) END AS returns,
284
+ l.lanname AS language, pg_catalog.obj_description(p.oid, 'pg_proc') AS comment,
285
+ pg_catalog.pg_get_userbyid(p.proowner) AS owner, p.provolatile AS volatility,
286
+ p.proisstrict AS strict, p.prosecdef AS security_definer,
287
+ CASE WHEN p.prokind = 'a' THEN NULL ELSE pg_catalog.pg_get_functiondef(p.oid) END AS definition
288
+ FROM pg_catalog.pg_proc p
289
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
290
+ JOIN pg_catalog.pg_language l ON l.oid = p.prolang
291
+ WHERE n.nspname = $1 AND p.proname = $2
292
+ AND pg_catalog.pg_get_function_identity_arguments(p.oid) = $3`, [ref.schema, ref.name, ref.args]);
293
+ if (!row)
294
+ return null;
295
+ return {
296
+ ...ref,
297
+ kind: functionKinds[row.kind ?? ''] ?? 'function',
298
+ returns: row.returns,
299
+ language: row.language ?? '',
300
+ comment: row.comment,
301
+ owner: row.owner ?? '',
302
+ volatility: row.volatility === 'i' ? 'immutable' : row.volatility === 's' ? 'stable' : 'volatile',
303
+ strict: flag(row.strict),
304
+ securityDefiner: flag(row.security_definer),
305
+ definition: row.definition,
306
+ };
307
+ }
308
+ export async function describeSequence(session, ref) {
309
+ const [info] = (await listSequences(session, ref.schema)).filter((s) => s.name === ref.name);
310
+ if (!info)
311
+ return null;
312
+ const [row] = await session.rows(`SELECT sequenceowner AS owner, start_value::text AS start, min_value::text AS min,
313
+ max_value::text AS max, increment_by::text AS increment, cache_size::text AS cache,
314
+ cycle, last_value::text AS last_value
315
+ FROM pg_catalog.pg_sequences WHERE schemaname = $1 AND sequencename = $2`, [ref.schema, ref.name]);
316
+ return {
317
+ ...info,
318
+ owner: row?.owner ?? '',
319
+ start: row?.start ?? '',
320
+ min: row?.min ?? '',
321
+ max: row?.max ?? '',
322
+ increment: row?.increment ?? '',
323
+ cache: row?.cache ?? '',
324
+ cycle: flag(row?.cycle ?? null),
325
+ lastValue: row?.last_value ?? null,
326
+ };
327
+ }
328
+ export async function describeType(session, ref) {
329
+ const [row] = await session.rows(`SELECT t.oid::text AS oid, t.typtype AS kind, pg_catalog.obj_description(t.oid, 'pg_type') AS comment,
330
+ pg_catalog.pg_get_userbyid(t.typowner) AS owner,
331
+ CASE t.typtype
332
+ WHEN 'd' THEN pg_catalog.format_type(t.typbasetype, t.typtypmod)
333
+ WHEN 'r' THEN (SELECT pg_catalog.format_type(r.rngsubtype, NULL)
334
+ FROM pg_catalog.pg_range r WHERE r.rngtypid = t.oid)
335
+ END AS base_type,
336
+ t.typdefault AS default, t.typnotnull AS not_null
337
+ FROM pg_catalog.pg_type t
338
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
339
+ WHERE n.nspname = $1 AND t.typname = $2 AND t.typtype IN ('e', 'c', 'd', 'r', 'm')
340
+ AND (t.typtype <> 'c' OR EXISTS (
341
+ SELECT 1 FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid AND c.relkind = 'c'))`, [ref.schema, ref.name]);
342
+ if (!row)
343
+ return null;
344
+ const oid = row.oid ?? '0';
345
+ const kind = typeKinds[row.kind ?? ''] ?? 'composite';
346
+ const labels = kind === 'enum'
347
+ ? (await session.rows('SELECT enumlabel AS label FROM pg_catalog.pg_enum WHERE enumtypid = $1::oid ORDER BY enumsortorder', [oid])).map((r) => r.label ?? '')
348
+ : [];
349
+ const attributes = kind === 'composite'
350
+ ? (await session.rows(`SELECT a.attname AS name, pg_catalog.format_type(a.atttypid, a.atttypmod) AS type
351
+ FROM pg_catalog.pg_type t
352
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = t.typrelid
353
+ WHERE t.oid = $1::oid AND a.attnum > 0 AND NOT a.attisdropped
354
+ ORDER BY a.attnum`, [oid])).map((r) => ({ name: r.name ?? '', type: r.type ?? '' }))
355
+ : [];
356
+ const constraints = kind === 'domain'
357
+ ? (await session.rows(`SELECT conname AS name, pg_catalog.pg_get_constraintdef(oid, true) AS definition
358
+ FROM pg_catalog.pg_constraint WHERE contypid = $1::oid ORDER BY conname`, [oid])).map((r) => ({
359
+ name: r.name ?? '',
360
+ kind: 'check',
361
+ definition: r.definition ?? '',
362
+ }))
363
+ : [];
364
+ return {
365
+ ...ref,
366
+ kind,
367
+ comment: row.comment,
368
+ owner: row.owner ?? '',
369
+ labels,
370
+ attributes,
371
+ baseType: row.base_type,
372
+ default: row.default,
373
+ notNull: flag(row.not_null),
374
+ constraints,
375
+ };
376
+ }
377
+ export async function serverInfo(session) {
378
+ const [row] = await session.rows(`SELECT pg_catalog.version() AS version, current_database() AS database, current_user AS user,
379
+ pg_catalog.pg_database_size(current_database()) AS "databaseBytes",
380
+ pg_catalog.pg_postmaster_start_time()::text AS "startedAt",
381
+ current_setting('default_transaction_read_only') AS "readOnly",
382
+ current_setting('TimeZone') AS "timeZone",
383
+ current_setting('search_path') AS "searchPath",
384
+ current_setting('server_encoding') AS encoding`);
385
+ return {
386
+ version: row?.version ?? '',
387
+ database: row?.database ?? '',
388
+ user: row?.user ?? '',
389
+ databaseBytes: Number(row?.databaseBytes ?? 0),
390
+ startedAt: row?.startedAt ?? '',
391
+ readOnly: row?.readOnly === 'on',
392
+ timeZone: row?.timeZone ?? '',
393
+ searchPath: row?.searchPath ?? '',
394
+ encoding: row?.encoding ?? '',
395
+ };
396
+ }
397
+ /** The other connections to this database, longest running first. */
398
+ export async function listActivity(session) {
399
+ const rows = await session.rows(`SELECT pid, usename AS user, application_name AS application,
400
+ host(client_addr) AS client, state,
401
+ (now() - query_start)::text AS duration, left(query, 500) AS query
402
+ FROM pg_catalog.pg_stat_activity
403
+ WHERE datname = current_database() AND pid <> pg_backend_pid()
404
+ ORDER BY query_start NULLS LAST`);
405
+ return rows.map((row) => ({
406
+ pid: Number(row.pid),
407
+ user: row.user,
408
+ application: row.application,
409
+ client: row.client,
410
+ state: row.state,
411
+ duration: row.duration,
412
+ query: row.query,
413
+ }));
414
+ }
@@ -0,0 +1,42 @@
1
+ import { DEFAULT_TIMEOUT_MS } from '../db/connect.js';
2
+ import { describeError } from '../db/tester.js';
3
+ import { resolveEntry } from '../environments.js';
4
+ /** The driver refused the login, so what was typed for it was probably wrong. */
5
+ export class LoginError extends Error {
6
+ name = 'LoginError';
7
+ }
8
+ /**
9
+ * Looks up everything an environment needs (from Vault, a prompt, ...) once and opens up to `count`
10
+ * connections with it, read only when the environment says so, so a batch of queries can run side
11
+ * by side without asking for anything twice. Connections beyond the first that the server refuses
12
+ * (too many clients, say) are left out; the first failing is an Error with a plain-English message.
13
+ */
14
+ export async function connectEntries(entry, services, count, options = {}) {
15
+ const resolved = await resolveEntry(entry, services, { signal: options.signal });
16
+ const open = () => services.openSession(resolved, {
17
+ readOnly: options.readOnly ?? resolved.readOnly,
18
+ ...(options.statementTimeoutMs === undefined
19
+ ? {}
20
+ : { statementTimeoutMs: options.statementTimeoutMs }),
21
+ });
22
+ const attempts = await Promise.allSettled(Array.from({ length: Math.max(count, 1) }, open));
23
+ const sessions = attempts.flatMap((attempt) => attempt.status === 'fulfilled' ? [attempt.value] : []);
24
+ if (sessions.length > 0)
25
+ return sessions;
26
+ const error = attempts[0].reason;
27
+ const failure = describeError(error, resolved, DEFAULT_TIMEOUT_MS);
28
+ if (failure.ok)
29
+ throw error;
30
+ const message = failure.detail && failure.detail !== failure.reason
31
+ ? `${failure.reason} (${failure.detail})`
32
+ : failure.reason;
33
+ throw failure.kind === 'auth' ? new LoginError(message) : new Error(message);
34
+ }
35
+ /**
36
+ * Looks up everything an environment needs (from Vault, a prompt, ...) and connects, read only when
37
+ * the environment says so. A connection failure is an Error with a plain-English message.
38
+ */
39
+ export async function connectEntry(entry, services, options = {}) {
40
+ const [session] = await connectEntries(entry, services, 1, options);
41
+ return session;
42
+ }