neonctl 2.22.2 → 2.23.1
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 +84 -0
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/connection_string.js +9 -1
- package/commands/functions.js +268 -0
- package/commands/index.js +4 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +6 -1
- package/functions_api.js +43 -0
- package/package.json +15 -5
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/esbuild.js +147 -0
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/checkout.test.js +0 -170
- package/commands/connection_string.test.js +0 -196
- package/commands/data_api.test.js +0 -169
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/link.test.js +0 -381
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/psql.test.js +0 -49
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/context.test.js +0 -119
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering for psql's `\d*` describe commands.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the runtime side of WP-20: take the SQL templates
|
|
5
|
+
* from {@link './queries.js'} (WP-19), run them against a real
|
|
6
|
+
* {@link Connection}, and render the result via the aligned printer
|
|
7
|
+
* for tabular sections plus free-form text for "footer" sections
|
|
8
|
+
* (`Indexes:`, `Foreign-key constraints:`, etc.) the way upstream
|
|
9
|
+
* `describe.c` does.
|
|
10
|
+
*
|
|
11
|
+
* Scope of the initial implementation:
|
|
12
|
+
*
|
|
13
|
+
* - {@link runListQuery} runs an arbitrary {@link DescribeQuery}
|
|
14
|
+
* (typically one of `listTables`, `describeFunctions`, etc.) and
|
|
15
|
+
* prints its result with the aligned printer. Title is taken from
|
|
16
|
+
* the query's `description`. This covers every `\d*` *list* command.
|
|
17
|
+
*
|
|
18
|
+
* - {@link describeOneTableDetails} fans out from the lookup query
|
|
19
|
+
* `describeTableDetails` into the per-relation detail render: a
|
|
20
|
+
* columns table at the top, followed by index / constraint / trigger
|
|
21
|
+
* sections as appropriate for the relkind. This is the bulk of
|
|
22
|
+
* upstream's `describeOneTableDetails()` from `describe.c`. We
|
|
23
|
+
* implement the *common* table layout (regular tables, views,
|
|
24
|
+
* materialized views, partitioned tables and indexes) — exotic
|
|
25
|
+
* sections (foreign-table options, replica identity, RLS policies,
|
|
26
|
+
* inheritance pretty-printing) are stubbed with the SQL queries in
|
|
27
|
+
* place but only minimal rendering. The output is sufficient for
|
|
28
|
+
* real-world `\d <name>` usage; gaps are flagged with TODO comments.
|
|
29
|
+
*
|
|
30
|
+
* - {@link describeOneSequence}, {@link describeOneFunctionDetails}
|
|
31
|
+
* and {@link describeOneViewDetails} are thinner: a single query +
|
|
32
|
+
* one section of output each.
|
|
33
|
+
*
|
|
34
|
+
* Pattern conditions: each list query has an `AND true /<!---->* TODO(WP-20)…`
|
|
35
|
+
* placeholder we replace via {@link applyPattern} before sending the
|
|
36
|
+
* query down the wire. See {@link processSQLNamePattern} for the
|
|
37
|
+
* pattern parser.
|
|
38
|
+
*/
|
|
39
|
+
import { processSQLNamePattern } from './processNamePattern.js';
|
|
40
|
+
import { alignedPrinter } from '../print/aligned.js';
|
|
41
|
+
import { asciidocPrinter } from '../print/asciidoc.js';
|
|
42
|
+
import { csvPrinter } from '../print/csv.js';
|
|
43
|
+
import { htmlPrinter } from '../print/html.js';
|
|
44
|
+
import { jsonPrinter } from '../print/json.js';
|
|
45
|
+
import { latexLongtablePrinter, latexPrinter } from '../print/latex.js';
|
|
46
|
+
import { troffMsPrinter } from '../print/troff.js';
|
|
47
|
+
import { unalignedPrinter } from '../print/unaligned.js';
|
|
48
|
+
import { fetchForeignTableInfo, fetchInheritedBy, fetchInherits, fetchPartitionKey, fetchPartitionOf, fetchPerColumnFdwOptions, fetchPolicies, fetchStatisticsObjects, fetchTableInfo, fetchTablePublications, fetchTableSubscriptions, } from './queries.js';
|
|
49
|
+
import { applyPattern } from './processNamePattern.js';
|
|
50
|
+
import { fetchNotNullConstraints } from './queries.js';
|
|
51
|
+
import { serverAtLeast, PG_14 } from './versionGate.js';
|
|
52
|
+
/**
|
|
53
|
+
* Pick the printer for the active output format. Mirrors `pickPrinter`
|
|
54
|
+
* in `core/common.ts`, but operates off `PrintQueryOpts.topt.format`
|
|
55
|
+
* since formatters don't have access to the full `PsqlSettings`. The
|
|
56
|
+
* aligned printer covers both `aligned` and `wrapped`; everything else
|
|
57
|
+
* routes to its dedicated module so `\d <obj>` honours the user's
|
|
58
|
+
* `\pset format` choice (asciidoc/csv/html/latex/etc.) the same way
|
|
59
|
+
* regular SELECTs do.
|
|
60
|
+
*/
|
|
61
|
+
const pickPrinterForFormat = (opts) => {
|
|
62
|
+
switch (opts.topt.format) {
|
|
63
|
+
case 'aligned':
|
|
64
|
+
case 'wrapped':
|
|
65
|
+
return alignedPrinter;
|
|
66
|
+
case 'unaligned':
|
|
67
|
+
return unalignedPrinter;
|
|
68
|
+
case 'csv':
|
|
69
|
+
return csvPrinter;
|
|
70
|
+
case 'json':
|
|
71
|
+
return jsonPrinter;
|
|
72
|
+
case 'html':
|
|
73
|
+
return htmlPrinter;
|
|
74
|
+
case 'asciidoc':
|
|
75
|
+
return asciidocPrinter;
|
|
76
|
+
case 'latex':
|
|
77
|
+
return latexPrinter;
|
|
78
|
+
case 'latex-longtable':
|
|
79
|
+
return latexLongtablePrinter;
|
|
80
|
+
case 'troff-ms':
|
|
81
|
+
return troffMsPrinter;
|
|
82
|
+
default:
|
|
83
|
+
return alignedPrinter;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Format a cell value coming back from the protocol layer. Connection
|
|
88
|
+
* decoded values arrive as strings (text mode) or null. We coerce
|
|
89
|
+
* everything to string for the printer.
|
|
90
|
+
*/
|
|
91
|
+
const cellToString = (v) => {
|
|
92
|
+
if (v === null || v === undefined)
|
|
93
|
+
return '';
|
|
94
|
+
if (typeof v === 'string')
|
|
95
|
+
return v;
|
|
96
|
+
if (Buffer.isBuffer(v))
|
|
97
|
+
return v.toString('utf-8');
|
|
98
|
+
if (typeof v === 'number' ||
|
|
99
|
+
typeof v === 'boolean' ||
|
|
100
|
+
typeof v === 'bigint') {
|
|
101
|
+
return String(v);
|
|
102
|
+
}
|
|
103
|
+
// Non-primitive fallback: encode JSON. This branch shouldn't be hit
|
|
104
|
+
// under the protocol layer (which decodes to strings) but we guard
|
|
105
|
+
// against future shape changes.
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(v);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Materialize a {@link ResultSet} into something the aligned printer
|
|
115
|
+
* can render. The printer expects `rows: unknown[][]`; we keep the
|
|
116
|
+
* shape but ensure cells are strings or null for the null-print logic.
|
|
117
|
+
*/
|
|
118
|
+
const coerceResultSet = (rs) => ({
|
|
119
|
+
...rs,
|
|
120
|
+
rows: rs.rows.map((row) => row.map((c) => (c === null || c === undefined ? null : cellToString(c)))),
|
|
121
|
+
});
|
|
122
|
+
const makeSectionBuffer = () => {
|
|
123
|
+
let buf = '';
|
|
124
|
+
const write = (chunk) => {
|
|
125
|
+
if (typeof chunk === 'string') {
|
|
126
|
+
buf += chunk;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
buf += chunk.toString('utf-8');
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
};
|
|
133
|
+
// We only need `.write(chunk)` from the renderers; everything else on
|
|
134
|
+
// WritableStream is stubbed so the type checks pass.
|
|
135
|
+
const stub = {
|
|
136
|
+
write,
|
|
137
|
+
end: () => true,
|
|
138
|
+
on: () => stub,
|
|
139
|
+
once: () => stub,
|
|
140
|
+
emit: () => true,
|
|
141
|
+
removeListener: () => stub,
|
|
142
|
+
addListener: () => stub,
|
|
143
|
+
setDefaultEncoding: () => stub,
|
|
144
|
+
cork: () => undefined,
|
|
145
|
+
uncork: () => undefined,
|
|
146
|
+
destroy: () => stub,
|
|
147
|
+
writable: true,
|
|
148
|
+
writableEnded: false,
|
|
149
|
+
writableFinished: false,
|
|
150
|
+
toString: () => buf,
|
|
151
|
+
};
|
|
152
|
+
return stub;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Capture the output of a single render-section helper into a string
|
|
156
|
+
* suitable for `opts.footers`. Returns `null` when the section emitted
|
|
157
|
+
* nothing (so callers can skip pushing an empty entry). Trailing
|
|
158
|
+
* newlines are stripped: the printer re-appends a single `\n` per
|
|
159
|
+
* footer, and trailing blank lines (between sections / before next
|
|
160
|
+
* command) are emitted once by the printer's own footer-terminator.
|
|
161
|
+
*/
|
|
162
|
+
const captureSection = async (fn) => {
|
|
163
|
+
const buf = makeSectionBuffer();
|
|
164
|
+
await fn(buf);
|
|
165
|
+
const text = buf.toString().replace(/\n+$/, '');
|
|
166
|
+
return text === '' ? null : text;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Run a list-style describe query and write its result via the aligned
|
|
170
|
+
* printer. Returns the {@link ResultSet} for callers that want to
|
|
171
|
+
* inspect or post-process. Used by `\dt`, `\df`, `\dn`, etc.
|
|
172
|
+
*/
|
|
173
|
+
export const runListQuery = async (conn, query, patternResult, out, popt) => {
|
|
174
|
+
const { sql, params } = applyPattern(query.sql, patternResult, query.params);
|
|
175
|
+
const rs = await conn.query(sql, params);
|
|
176
|
+
const coerced = coerceResultSet(rs);
|
|
177
|
+
const titleOverride = query.description ?? popt.title;
|
|
178
|
+
const opts = {
|
|
179
|
+
...popt,
|
|
180
|
+
title: titleOverride,
|
|
181
|
+
topt: { ...popt.topt, title: titleOverride ?? popt.topt.title },
|
|
182
|
+
footers: rs.rows.length === 0
|
|
183
|
+
? popt.footers
|
|
184
|
+
: popt.footers !== null
|
|
185
|
+
? popt.footers
|
|
186
|
+
: null,
|
|
187
|
+
};
|
|
188
|
+
await pickPrinterForFormat(opts).printQuery(coerced, opts, out);
|
|
189
|
+
return rs;
|
|
190
|
+
};
|
|
191
|
+
export const lookupRelations = async (conn, query, patternResult) => {
|
|
192
|
+
const { sql, params } = applyPattern(query.sql, patternResult, query.params);
|
|
193
|
+
const rs = await conn.query(sql, params);
|
|
194
|
+
return rs.rows.map((row) => ({
|
|
195
|
+
oid: Number(cellToString(row[0])),
|
|
196
|
+
nspname: cellToString(row[1]),
|
|
197
|
+
relname: cellToString(row[2]),
|
|
198
|
+
relkind: cellToString(row[3] ?? ''),
|
|
199
|
+
}));
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Lookup of one specific relation by `schema.name` for the `\d <name>`
|
|
203
|
+
* dispatch. Returns the row we need to choose the right `describeOne*`
|
|
204
|
+
* renderer — including `relkind` which the upstream code reads from
|
|
205
|
+
* a separate SELECT.
|
|
206
|
+
*/
|
|
207
|
+
export const lookupOneRelation = async (conn, pattern) => {
|
|
208
|
+
// Route the bare-name lookup through processSQLNamePattern so the name is
|
|
209
|
+
// case-folded (unquoted → lower) and dequoted exactly like the list views:
|
|
210
|
+
// `\d Foo` matches catalog relation `foo`, `\d "MyTable"` matches the
|
|
211
|
+
// mixed-case `MyTable`, and `schema.name` splits correctly. The old raw
|
|
212
|
+
// `^(name)$` interpolation matched neither (review item #22).
|
|
213
|
+
const np = processSQLNamePattern({
|
|
214
|
+
pattern,
|
|
215
|
+
namevar: 'c.relname',
|
|
216
|
+
schemavar: 'n.nspname',
|
|
217
|
+
visibilityrule: 'pg_catalog.pg_table_is_visible(c.oid)',
|
|
218
|
+
});
|
|
219
|
+
// A db-qualified pattern (3+ dotted components → dotCount > 1) is a
|
|
220
|
+
// cross-database reference that this single-DB detail short-circuit cannot
|
|
221
|
+
// honour. Return null so the caller falls through to the LIST path, which
|
|
222
|
+
// emits upstream's "cross-database references are not implemented" /
|
|
223
|
+
// "improper qualified name (too many dotted names)" diagnostic. Without
|
|
224
|
+
// this, the detail lookup ignored the db literal and wrongly matched
|
|
225
|
+
// (e.g. `\d nonesuch.pg_catalog.pg_class` rendered the table).
|
|
226
|
+
if (np.dotCount > 1)
|
|
227
|
+
return null;
|
|
228
|
+
const conds = [
|
|
229
|
+
...np.schemaConditions,
|
|
230
|
+
...np.nameConditions,
|
|
231
|
+
...np.visibilityConditions,
|
|
232
|
+
];
|
|
233
|
+
let sql = 'SELECT c.oid, n.nspname, c.relname, c.relkind\n' +
|
|
234
|
+
'FROM pg_catalog.pg_class c\n' +
|
|
235
|
+
' LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n';
|
|
236
|
+
if (conds.length > 0)
|
|
237
|
+
sql += `WHERE ${conds.join('\n AND ')}\n`;
|
|
238
|
+
sql += 'ORDER BY 2, 3 LIMIT 1;';
|
|
239
|
+
const rs = await conn.query(sql, np.params);
|
|
240
|
+
if (rs.rows.length === 0)
|
|
241
|
+
return null;
|
|
242
|
+
const row = rs.rows[0];
|
|
243
|
+
return {
|
|
244
|
+
oid: Number(cellToString(row[0])),
|
|
245
|
+
nspname: cellToString(row[1]),
|
|
246
|
+
relname: cellToString(row[2]),
|
|
247
|
+
relkind: cellToString(row[3]),
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Render `Table "schema.name"` (or the relkind-specific header) plus the
|
|
252
|
+
* column listing, followed by per-relkind sections (Indexes, Check
|
|
253
|
+
* constraints, Foreign-key constraints, Referenced-by, Triggers).
|
|
254
|
+
*
|
|
255
|
+
* Upstream `describeOneTableDetails()` is ~1500 LOC in `describe.c`;
|
|
256
|
+
* this implementation focuses on the headline experience and leaves
|
|
257
|
+
* exotic sections (RLS, replica identity, partition bounds rendering,
|
|
258
|
+
* pretty-printed inheritance) as TODOs. The query layer fetches the
|
|
259
|
+
* raw data so a follow-up WP can extend rendering without re-running
|
|
260
|
+
* queries.
|
|
261
|
+
*/
|
|
262
|
+
export const describeOneTableDetails = async (conn, oid, schema, name, relkind, verbose, out, popt, hideTableam = false, hideCompression = false) => {
|
|
263
|
+
// ----- One-shot relation info (RLS flags, replica identity,
|
|
264
|
+
// partition flag, tablespace, access method). Fetched before
|
|
265
|
+
// columns so the matview header can carry an "Access method:"
|
|
266
|
+
// line and the per-column FDW options can be merged inline.
|
|
267
|
+
const relInfo = await fetchRelationInfo(conn, oid);
|
|
268
|
+
// Compose the title. Matviews with a non-default access method get
|
|
269
|
+
// a second line ("Access method: <amname>") between the header and
|
|
270
|
+
// the column table — see upstream `describeOneTableDetails`. The
|
|
271
|
+
// matview-inline form is also gated by `HIDE_TABLEAM` so the user can
|
|
272
|
+
// opt out of access-method noise.
|
|
273
|
+
const baseTitle = headerForRelkind(relkind, schema, name);
|
|
274
|
+
const title = !hideTableam && relkind === 'm' && relInfo.relam !== 0 && relInfo.amname
|
|
275
|
+
? `${baseTitle}\nAccess method: ${relInfo.amname}`
|
|
276
|
+
: baseTitle;
|
|
277
|
+
// ----- Pre-fetch per-column FDW options (foreign tables only) so we
|
|
278
|
+
// can fold them into each column row. Upstream renders these
|
|
279
|
+
// inline as a trailing "FDW options: (k 'v', ...)" annotation
|
|
280
|
+
// rather than a separate footer section.
|
|
281
|
+
const fdwOptionsByColumn = relkind === 'f'
|
|
282
|
+
? await fetchPerColumnFdwOptionsMap(conn, oid)
|
|
283
|
+
: new Map();
|
|
284
|
+
// ----- Columns -----
|
|
285
|
+
// Verbose mode adds Storage / Stats target / Description columns to
|
|
286
|
+
// mirror upstream's `\d+`. These apply to every relkind that carries
|
|
287
|
+
// a column listing, including views and materialized views (upstream
|
|
288
|
+
// `describeOneTableDetails` gates the verbose column block on `verbose`
|
|
289
|
+
// alone, not on relkind).
|
|
290
|
+
const verboseCols = verbose &&
|
|
291
|
+
(relkind === 'r' ||
|
|
292
|
+
relkind === 'm' ||
|
|
293
|
+
relkind === 'p' ||
|
|
294
|
+
relkind === 'f' ||
|
|
295
|
+
relkind === 'v' ||
|
|
296
|
+
relkind === 'I' ||
|
|
297
|
+
relkind === 'i');
|
|
298
|
+
// Compression column (upstream `\d+`): present when the server is
|
|
299
|
+
// PG 14+, the `HIDE_TOAST_COMPRESSION` var is off, and the relkind is
|
|
300
|
+
// a regular table / partitioned table / materialized view (describe.c
|
|
301
|
+
// ~1953: `sversion >= 140000 && !hide_compression && relkind in
|
|
302
|
+
// (RELATION, PARTITIONED_TABLE, MATVIEW)`). When suppressed the column
|
|
303
|
+
// is dropped entirely — matching the conformance regress which runs
|
|
304
|
+
// with HIDE_TOAST_COMPRESSION=on.
|
|
305
|
+
const includeCompression = verboseCols &&
|
|
306
|
+
serverAtLeast(conn.serverVersion, PG_14) &&
|
|
307
|
+
!hideCompression &&
|
|
308
|
+
(relkind === 'r' || relkind === 'p' || relkind === 'm');
|
|
309
|
+
// Stats target column (upstream `\d+`): every verbose relkind EXCEPT a
|
|
310
|
+
// plain view — views have no per-column statistics targets (describe.c
|
|
311
|
+
// ~1964: RELATION, INDEX, PARTITIONED_INDEX, MATVIEW, FOREIGN_TABLE,
|
|
312
|
+
// PARTITIONED_TABLE).
|
|
313
|
+
const includeStatsTarget = verboseCols &&
|
|
314
|
+
(relkind === 'r' ||
|
|
315
|
+
relkind === 'i' ||
|
|
316
|
+
relkind === 'I' ||
|
|
317
|
+
relkind === 'm' ||
|
|
318
|
+
relkind === 'f' ||
|
|
319
|
+
relkind === 'p');
|
|
320
|
+
const colSql = 'SELECT a.attname,\n' +
|
|
321
|
+
' pg_catalog.format_type(a.atttypid, a.atttypmod),\n' +
|
|
322
|
+
' (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid, true)\n' +
|
|
323
|
+
' FROM pg_catalog.pg_attrdef d\n' +
|
|
324
|
+
' WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef),\n' +
|
|
325
|
+
' a.attnotnull,\n' +
|
|
326
|
+
' (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n' +
|
|
327
|
+
' WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation,\n' +
|
|
328
|
+
' a.attidentity,\n' +
|
|
329
|
+
' a.attgenerated' +
|
|
330
|
+
(verboseCols
|
|
331
|
+
? ',\n CASE a.attstorage' +
|
|
332
|
+
" WHEN 'p' THEN 'plain'" +
|
|
333
|
+
" WHEN 'e' THEN 'external'" +
|
|
334
|
+
" WHEN 'm' THEN 'main'" +
|
|
335
|
+
" WHEN 'x' THEN 'extended'" +
|
|
336
|
+
" ELSE '???'" +
|
|
337
|
+
' END AS attstorage' +
|
|
338
|
+
(includeCompression
|
|
339
|
+
? ',\n CASE a.attcompression' +
|
|
340
|
+
" WHEN 'p' THEN 'pglz'" +
|
|
341
|
+
" WHEN 'l' THEN 'lz4'" +
|
|
342
|
+
" WHEN '' THEN ''" +
|
|
343
|
+
" ELSE '???'" +
|
|
344
|
+
' END AS attcompression'
|
|
345
|
+
: '') +
|
|
346
|
+
(includeStatsTarget
|
|
347
|
+
? ',\n CASE WHEN a.attstattarget = -1 THEN NULL ELSE a.attstattarget::text END AS attstattarget'
|
|
348
|
+
: '') +
|
|
349
|
+
',\n pg_catalog.col_description(a.attrelid, a.attnum)'
|
|
350
|
+
: '') +
|
|
351
|
+
'\nFROM pg_catalog.pg_attribute a\n' +
|
|
352
|
+
`WHERE a.attrelid = '${oid}' AND a.attnum > 0 AND NOT a.attisdropped\n` +
|
|
353
|
+
'ORDER BY a.attnum;';
|
|
354
|
+
const colsRs = await conn.query(colSql, []);
|
|
355
|
+
// Foreign tables get an extra "FDW options" column when at least one
|
|
356
|
+
// attribute actually has options set (matches upstream — the column
|
|
357
|
+
// slot is conditional on the row data, not just the relkind).
|
|
358
|
+
const hasAnyFdwOptions = fdwOptionsByColumn.size > 0;
|
|
359
|
+
// TOAST tables show a slimmer column listing: Column + Type only, no
|
|
360
|
+
// Collation/Nullable/Default (those are uniformly empty for the three
|
|
361
|
+
// fixed columns chunk_id/chunk_seq/chunk_data). Matches upstream's
|
|
362
|
+
// `\d <toast>` output.
|
|
363
|
+
const isToast = relkind === 't';
|
|
364
|
+
// Synthesize a printable result set: Column, Type[, Collation, Nullable,
|
|
365
|
+
// Default[, Storage[, Compression], Stats target, Description]][, FDW options].
|
|
366
|
+
const fields = [fakeField('Column'), fakeField('Type')];
|
|
367
|
+
if (!isToast) {
|
|
368
|
+
fields.push(fakeField('Collation'));
|
|
369
|
+
fields.push(fakeField('Nullable'));
|
|
370
|
+
fields.push(fakeField('Default'));
|
|
371
|
+
}
|
|
372
|
+
if (verboseCols) {
|
|
373
|
+
fields.push(fakeField('Storage'));
|
|
374
|
+
if (includeCompression)
|
|
375
|
+
fields.push(fakeField('Compression'));
|
|
376
|
+
if (includeStatsTarget)
|
|
377
|
+
fields.push(fakeField('Stats target'));
|
|
378
|
+
fields.push(fakeField('Description'));
|
|
379
|
+
}
|
|
380
|
+
if (hasAnyFdwOptions)
|
|
381
|
+
fields.push(fakeField('FDW options'));
|
|
382
|
+
const rows = colsRs.rows.map((r) => {
|
|
383
|
+
const colName = cellToString(r[0]);
|
|
384
|
+
const colType = cellToString(r[1]);
|
|
385
|
+
const colDefault = r[2] === null ? null : cellToString(r[2]);
|
|
386
|
+
const notnull = String(r[3]) === 't' || r[3] === true;
|
|
387
|
+
const collation = r[4] === null ? null : cellToString(r[4]);
|
|
388
|
+
const identity = cellToString(r[5] ?? '');
|
|
389
|
+
const generated = cellToString(r[6] ?? '');
|
|
390
|
+
const nullable = notnull ? 'not null' : '';
|
|
391
|
+
let dflt = colDefault ?? '';
|
|
392
|
+
if (identity === 'a') {
|
|
393
|
+
dflt = 'generated always as identity';
|
|
394
|
+
}
|
|
395
|
+
else if (identity === 'd') {
|
|
396
|
+
dflt = 'generated by default as identity';
|
|
397
|
+
}
|
|
398
|
+
else if (generated === 's') {
|
|
399
|
+
// STORED generated column (PG 12+).
|
|
400
|
+
dflt = dflt ? `generated always as (${dflt}) stored` : '';
|
|
401
|
+
}
|
|
402
|
+
else if (generated === 'v') {
|
|
403
|
+
// VIRTUAL generated column (PG 18+). Same expression rendering as
|
|
404
|
+
// STORED but without the trailing keyword.
|
|
405
|
+
dflt = dflt ? `generated always as (${dflt})` : '';
|
|
406
|
+
}
|
|
407
|
+
const row = isToast
|
|
408
|
+
? [colName, colType]
|
|
409
|
+
: [colName, colType, collation ?? '', nullable, dflt];
|
|
410
|
+
if (verboseCols) {
|
|
411
|
+
// Slot offsets: 7 = storage, [8 = compression if PG14+], stats, desc.
|
|
412
|
+
let idx = 7;
|
|
413
|
+
const storage = cellToString(r[idx++] ?? '');
|
|
414
|
+
row.push(storage);
|
|
415
|
+
if (includeCompression) {
|
|
416
|
+
const compression = cellToString(r[idx++] ?? '');
|
|
417
|
+
row.push(compression);
|
|
418
|
+
}
|
|
419
|
+
if (includeStatsTarget) {
|
|
420
|
+
const statsTarget = r[idx] === null ? '' : cellToString(r[idx] ?? '');
|
|
421
|
+
idx++;
|
|
422
|
+
row.push(statsTarget);
|
|
423
|
+
}
|
|
424
|
+
const description = r[idx] === null ? '' : cellToString(r[idx] ?? '');
|
|
425
|
+
row.push(description);
|
|
426
|
+
}
|
|
427
|
+
if (hasAnyFdwOptions) {
|
|
428
|
+
const opts = fdwOptionsByColumn.get(colName);
|
|
429
|
+
row.push(opts ? `(${opts})` : '');
|
|
430
|
+
}
|
|
431
|
+
return row;
|
|
432
|
+
});
|
|
433
|
+
const colsResult = {
|
|
434
|
+
command: 'SELECT',
|
|
435
|
+
rowCount: rows.length,
|
|
436
|
+
oid: null,
|
|
437
|
+
fields,
|
|
438
|
+
rows,
|
|
439
|
+
notices: [],
|
|
440
|
+
};
|
|
441
|
+
// ----- Per-section footers, accumulated *before* the column table is
|
|
442
|
+
// printed. Upstream `describeOneTableDetails` attaches each
|
|
443
|
+
// relkind-specific footer to the columns table via
|
|
444
|
+
// `printTableAddFooter()`, then `printTable()` emits them flush
|
|
445
|
+
// against the data rows with a single trailing blank line at the
|
|
446
|
+
// end of the whole block. Routing every section through
|
|
447
|
+
// `opts.footers` mirrors that layout — single-line annotations
|
|
448
|
+
// (`Access method:`, `Tablespace:`, …) sit immediately under the
|
|
449
|
+
// last data row, multi-line group footers (`Indexes:`,
|
|
450
|
+
// `Foreign-key constraints:`, …) follow, and the trailing blank
|
|
451
|
+
// only fires after the last footer rather than between data and
|
|
452
|
+
// the first footer.
|
|
453
|
+
const footers = [];
|
|
454
|
+
const push = (s) => {
|
|
455
|
+
if (s !== null)
|
|
456
|
+
footers.push(s);
|
|
457
|
+
};
|
|
458
|
+
// ----- View definition (views / matviews, verbose only) -----
|
|
459
|
+
// Upstream attaches this as a table FOOTER (describe.c ~3175), so it
|
|
460
|
+
// renders flush against the column rows with the single trailing
|
|
461
|
+
// blank line the footer machinery adds — not as a separate block.
|
|
462
|
+
if ((relkind === 'v' || relkind === 'm') && verbose) {
|
|
463
|
+
const vrs = await conn.query(`SELECT pg_catalog.pg_get_viewdef('${oid}'::pg_catalog.oid, true);`, []);
|
|
464
|
+
if (vrs.rows.length > 0) {
|
|
465
|
+
push(`View definition:\n${cellToString(vrs.rows[0][0])}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// ----- Partition-key (partitioned-table parent only) -----
|
|
469
|
+
if (relkind === 'p') {
|
|
470
|
+
push(await captureSection((b) => renderPartitionKeySection(conn, oid, b)));
|
|
471
|
+
}
|
|
472
|
+
// ----- Partition-of (child partition only) -----
|
|
473
|
+
if (relInfo.relispartition) {
|
|
474
|
+
push(await captureSection((b) => renderPartitionOfSection(conn, oid, verbose, b)));
|
|
475
|
+
}
|
|
476
|
+
// ----- Owning table (TOAST tables only — printed before Indexes).
|
|
477
|
+
// Upstream `describeOneTableDetails` adds the owning-table footer
|
|
478
|
+
// prior to attaching the indexes footer for `RELKIND_TOASTVALUE`.
|
|
479
|
+
if (relkind === 't') {
|
|
480
|
+
push(await captureSection((b) => renderToastOwningTableFooter(conn, oid, b)));
|
|
481
|
+
}
|
|
482
|
+
// ----- Indexes (tables / matviews / partitioned tables / TOAST) -----
|
|
483
|
+
if (relkind === 'r' ||
|
|
484
|
+
relkind === 'm' ||
|
|
485
|
+
relkind === 'p' ||
|
|
486
|
+
relkind === 't') {
|
|
487
|
+
push(await captureSection((b) => renderIndexesSection(conn, oid, b)));
|
|
488
|
+
}
|
|
489
|
+
// ----- Check constraints -----
|
|
490
|
+
if (relkind === 'r' || relkind === 'p' || relkind === 'f') {
|
|
491
|
+
push(await captureSection((b) => renderCheckConstraintsSection(conn, oid, b)));
|
|
492
|
+
}
|
|
493
|
+
// ----- Not-null constraints (PG 18+ named NOT NULL constraints) -----
|
|
494
|
+
// Upstream renders this footer in verbose mode between Check
|
|
495
|
+
// constraints and Foreign-key constraints (describe.c ~3104).
|
|
496
|
+
// The query returns empty on pre-PG-18 servers (no contype = 'n'
|
|
497
|
+
// rows), so the section is naturally absent there.
|
|
498
|
+
if (verbose && (relkind === 'r' || relkind === 'p' || relkind === 'f')) {
|
|
499
|
+
push(await captureSection((b) => renderNotNullConstraintsSection(conn, oid, b)));
|
|
500
|
+
}
|
|
501
|
+
// ----- Foreign-key constraints -----
|
|
502
|
+
if (relkind === 'r' || relkind === 'p') {
|
|
503
|
+
push(await captureSection((b) => renderForeignKeyConstraintsSection(conn, oid, b)));
|
|
504
|
+
push(await captureSection((b) => renderReferencedBySection(conn, oid, b)));
|
|
505
|
+
}
|
|
506
|
+
// ----- Triggers -----
|
|
507
|
+
if (relkind === 'r' || relkind === 'p' || relkind === 'v') {
|
|
508
|
+
push(await captureSection((b) => renderTriggersSection(conn, oid, b)));
|
|
509
|
+
}
|
|
510
|
+
// ----- RLS policies (regular + partitioned tables) -----
|
|
511
|
+
if (relkind === 'r' || relkind === 'p') {
|
|
512
|
+
push(await captureSection((b) => renderPoliciesSection(conn, oid, relInfo, b)));
|
|
513
|
+
}
|
|
514
|
+
// ----- Foreign-table footer: Server + FDW options -----
|
|
515
|
+
// Per-column FDW options are rendered inline within the columns
|
|
516
|
+
// table (see fdwOptionsByColumn above); no separate footer here.
|
|
517
|
+
if (relkind === 'f') {
|
|
518
|
+
push(await captureSection((b) => renderForeignTableFooter(conn, oid, b)));
|
|
519
|
+
}
|
|
520
|
+
// ----- Inherits: (parents) — for tables, partitioned tables, foreign -----
|
|
521
|
+
if (relkind === 'r' || relkind === 'p' || relkind === 'f') {
|
|
522
|
+
push(await captureSection((b) => renderInheritsSection(conn, oid, b)));
|
|
523
|
+
}
|
|
524
|
+
// ----- Inherited by / Partitions / Number of [child tables|partitions] -----
|
|
525
|
+
if (relkind === 'r' || relkind === 'p' || relkind === 'f') {
|
|
526
|
+
push(await captureSection((b) => renderInheritedBySection(conn, oid, relkind, verbose, b)));
|
|
527
|
+
}
|
|
528
|
+
// ----- Publications (any publishable relkind) -----
|
|
529
|
+
if (relkind === 'r' ||
|
|
530
|
+
relkind === 'p' ||
|
|
531
|
+
relkind === 'm' ||
|
|
532
|
+
relkind === 'f') {
|
|
533
|
+
push(await captureSection((b) => renderPublicationsSection(conn, oid, b)));
|
|
534
|
+
}
|
|
535
|
+
// ----- Subscriptions (any publishable relkind; permission-denied silent) -----
|
|
536
|
+
if (relkind === 'r' ||
|
|
537
|
+
relkind === 'p' ||
|
|
538
|
+
relkind === 'm' ||
|
|
539
|
+
relkind === 'f') {
|
|
540
|
+
push(await captureSection((b) => renderSubscriptionsSection(conn, oid, b)));
|
|
541
|
+
}
|
|
542
|
+
// ----- Statistics objects (verbose; r/m/p/f) -----
|
|
543
|
+
if (verbose &&
|
|
544
|
+
(relkind === 'r' || relkind === 'm' || relkind === 'p' || relkind === 'f')) {
|
|
545
|
+
push(await captureSection((b) => renderStatisticsObjectsSection(conn, oid, b)));
|
|
546
|
+
}
|
|
547
|
+
// ----- Replica Identity (verbose, non-default, regular & matview).
|
|
548
|
+
// INDEX mode is rendered inline within Indexes:, so the footer
|
|
549
|
+
// is only emitted for FULL / NOTHING.
|
|
550
|
+
if (verbose && (relkind === 'r' || relkind === 'm')) {
|
|
551
|
+
push(await captureSection((b) => {
|
|
552
|
+
renderReplicaIdentitySection(schema, relInfo, b);
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
// ----- Tablespace footer (verbose: explicit tablespace only) -----
|
|
556
|
+
if (verbose) {
|
|
557
|
+
push(await captureSection((b) => {
|
|
558
|
+
renderTablespaceFooter(relkind, relInfo, b);
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
// ----- Access method footer (verbose: relkind r/p with relam set).
|
|
562
|
+
// Matviews ('m') show their access method inline in the header,
|
|
563
|
+
// so we don't double up here. Gated by `HIDE_TABLEAM` to mirror
|
|
564
|
+
// upstream — the per-test psql.sql toggles the variable to
|
|
565
|
+
// suppress access-method noise.
|
|
566
|
+
if (!hideTableam && verbose && (relkind === 'r' || relkind === 'p')) {
|
|
567
|
+
push(await captureSection((b) => {
|
|
568
|
+
renderAccessMethodFooter(relInfo, b);
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
// Upstream's `printTable` is invoked with `default_footer = false`
|
|
572
|
+
// for the column listing: the row-count footer ("(N rows)") is
|
|
573
|
+
// suppressed so the relkind-specific footers we just collected drive
|
|
574
|
+
// the post-table layout. Pass them via `opts.footers` so the printer
|
|
575
|
+
// emits each one flush against the data rows and ends the block with
|
|
576
|
+
// a single trailing blank line.
|
|
577
|
+
const colOpts = {
|
|
578
|
+
...popt,
|
|
579
|
+
title,
|
|
580
|
+
topt: { ...popt.topt, title, defaultFooter: false },
|
|
581
|
+
footers: footers.length > 0 ? footers : null,
|
|
582
|
+
};
|
|
583
|
+
await pickPrinterForFormat(colOpts).printQuery(coerceResultSet(colsResult), colOpts, out);
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
586
|
+
* Helper that runs {@link fetchTableInfo} and parses the resulting row
|
|
587
|
+
* into a {@link RelationInfo}. Returns sensible falsy defaults when the
|
|
588
|
+
* row is missing (shouldn't happen given the caller already looked up
|
|
589
|
+
* the relation, but we don't want to throw mid-render).
|
|
590
|
+
*/
|
|
591
|
+
const fetchRelationInfo = async (conn, oid) => {
|
|
592
|
+
const q = fetchTableInfo({ oid, serverVersion: conn.serverVersion });
|
|
593
|
+
const rs = await conn.query(q.sql, q.params);
|
|
594
|
+
if (rs.rows.length === 0) {
|
|
595
|
+
return {
|
|
596
|
+
rowsecurity: false,
|
|
597
|
+
forcerowsecurity: false,
|
|
598
|
+
relreplident: 'd',
|
|
599
|
+
relispartition: false,
|
|
600
|
+
reltablespace: 0,
|
|
601
|
+
relam: 0,
|
|
602
|
+
spcname: null,
|
|
603
|
+
amname: null,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const r = rs.rows[0];
|
|
607
|
+
return {
|
|
608
|
+
rowsecurity: parseBool(r[0]),
|
|
609
|
+
forcerowsecurity: parseBool(r[1]),
|
|
610
|
+
relreplident: cellToString(r[2] ?? 'd') || 'd',
|
|
611
|
+
relispartition: parseBool(r[3]),
|
|
612
|
+
reltablespace: Number(cellToString(r[4] ?? '0')) || 0,
|
|
613
|
+
relam: Number(cellToString(r[5] ?? '0')) || 0,
|
|
614
|
+
spcname: r[6] === null || r[6] === undefined ? null : cellToString(r[6]),
|
|
615
|
+
amname: r[7] === null || r[7] === undefined ? null : cellToString(r[7]),
|
|
616
|
+
};
|
|
617
|
+
};
|
|
618
|
+
/** Coerce a Postgres "t"/"f" text-mode boolean (or a real bool) to JS. */
|
|
619
|
+
const parseBool = (v) => v === true || (typeof v === 'string' && (v === 't' || v === 'true'));
|
|
620
|
+
/**
|
|
621
|
+
* Render `Partition key: <partkeydef>` for partitioned-table parents.
|
|
622
|
+
*/
|
|
623
|
+
const renderPartitionKeySection = async (conn, oid, out) => {
|
|
624
|
+
const q = fetchPartitionKey({ oid });
|
|
625
|
+
const rs = await conn.query(q.sql, q.params);
|
|
626
|
+
if (rs.rows.length === 0)
|
|
627
|
+
return;
|
|
628
|
+
const def = cellToString(rs.rows[0][0] ?? '');
|
|
629
|
+
if (def === '')
|
|
630
|
+
return;
|
|
631
|
+
out.write(`Partition key: ${def}\n`);
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* Render the "Partition of: <parent> <bound>[ DETACH PENDING]" line and
|
|
635
|
+
* the verbose-only "Partition constraint:" follow-up for a child
|
|
636
|
+
* partition (`relispartition = true`).
|
|
637
|
+
*/
|
|
638
|
+
const renderPartitionOfSection = async (conn, oid, verbose, out) => {
|
|
639
|
+
const q = fetchPartitionOf({
|
|
640
|
+
oid,
|
|
641
|
+
serverVersion: conn.serverVersion,
|
|
642
|
+
withConstraint: verbose,
|
|
643
|
+
});
|
|
644
|
+
const rs = await conn.query(q.sql, q.params);
|
|
645
|
+
if (rs.rows.length === 0)
|
|
646
|
+
return;
|
|
647
|
+
const row = rs.rows[0];
|
|
648
|
+
const parent = cellToString(row[0] ?? '');
|
|
649
|
+
const bound = cellToString(row[1] ?? '');
|
|
650
|
+
const detached = parseBool(row[2]);
|
|
651
|
+
const tail = detached ? ' DETACH PENDING' : '';
|
|
652
|
+
out.write(`Partition of: ${parent} ${bound}${tail}\n`);
|
|
653
|
+
if (verbose) {
|
|
654
|
+
const constraintdef = row[3] === null || row[3] === undefined ? '' : cellToString(row[3]);
|
|
655
|
+
if (constraintdef === '') {
|
|
656
|
+
out.write('No partition constraint\n');
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
out.write(`Partition constraint: ${constraintdef}\n`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
/**
|
|
664
|
+
* Render the `Policies[...]:` header + one POLICY line per row. The
|
|
665
|
+
* exact header text encodes (rowsecurity, forcerowsecurity, has-policies)
|
|
666
|
+
* the same way upstream does, including the "(none)" tail for the
|
|
667
|
+
* enabled-but-no-policies cases.
|
|
668
|
+
*/
|
|
669
|
+
const renderPoliciesSection = async (conn, oid, relInfo, out) => {
|
|
670
|
+
const q = fetchPolicies({ oid, serverVersion: conn.serverVersion });
|
|
671
|
+
const rs = await conn.query(q.sql, q.params);
|
|
672
|
+
const tuples = rs.rows.length;
|
|
673
|
+
const { rowsecurity, forcerowsecurity } = relInfo;
|
|
674
|
+
let header = null;
|
|
675
|
+
if (rowsecurity && !forcerowsecurity && tuples > 0) {
|
|
676
|
+
header = 'Policies:';
|
|
677
|
+
}
|
|
678
|
+
else if (rowsecurity && forcerowsecurity && tuples > 0) {
|
|
679
|
+
header = 'Policies (forced row security enabled):';
|
|
680
|
+
}
|
|
681
|
+
else if (rowsecurity && !forcerowsecurity && tuples === 0) {
|
|
682
|
+
header = 'Policies (row security enabled): (none)';
|
|
683
|
+
}
|
|
684
|
+
else if (rowsecurity && forcerowsecurity && tuples === 0) {
|
|
685
|
+
header = 'Policies (forced row security enabled): (none)';
|
|
686
|
+
}
|
|
687
|
+
else if (!rowsecurity && tuples > 0) {
|
|
688
|
+
header = 'Policies (row security disabled):';
|
|
689
|
+
}
|
|
690
|
+
if (header === null)
|
|
691
|
+
return;
|
|
692
|
+
out.write(`${header}\n`);
|
|
693
|
+
for (const r of rs.rows) {
|
|
694
|
+
const polname = cellToString(r[0]);
|
|
695
|
+
const permissive = parseBool(r[1]);
|
|
696
|
+
const roles = r[2] === null || r[2] === undefined ? null : cellToString(r[2]);
|
|
697
|
+
const qual = r[3] === null || r[3] === undefined ? null : cellToString(r[3]);
|
|
698
|
+
const withcheck = r[4] === null || r[4] === undefined ? null : cellToString(r[4]);
|
|
699
|
+
const cmd = r[5] === null || r[5] === undefined ? null : cellToString(r[5]);
|
|
700
|
+
let line = ` POLICY "${polname}"`;
|
|
701
|
+
if (!permissive)
|
|
702
|
+
line += ' AS RESTRICTIVE';
|
|
703
|
+
if (cmd !== null && cmd !== '')
|
|
704
|
+
line += ` FOR ${cmd}`;
|
|
705
|
+
if (roles !== null)
|
|
706
|
+
line += `\n TO ${roles}`;
|
|
707
|
+
if (qual !== null)
|
|
708
|
+
line += `\n USING (${qual})`;
|
|
709
|
+
if (withcheck !== null)
|
|
710
|
+
line += `\n WITH CHECK (${withcheck})`;
|
|
711
|
+
out.write(`${line}\n`);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
/**
|
|
715
|
+
* Render the foreign-table footer: `Server: <name>` + optional
|
|
716
|
+
* `FDW options: (key 'val', key 'val')`. Upstream pulls these in a
|
|
717
|
+
* single follow-up query; we mirror that shape via
|
|
718
|
+
* {@link fetchForeignTableInfo}.
|
|
719
|
+
*/
|
|
720
|
+
const renderForeignTableFooter = async (conn, oid, out) => {
|
|
721
|
+
const q = fetchForeignTableInfo({ oid });
|
|
722
|
+
const rs = await conn.query(q.sql, q.params);
|
|
723
|
+
if (rs.rows.length === 0)
|
|
724
|
+
return;
|
|
725
|
+
const row = rs.rows[0];
|
|
726
|
+
const server = cellToString(row[0] ?? '');
|
|
727
|
+
const ftoptions = row[1] === null || row[1] === undefined ? '' : cellToString(row[1]);
|
|
728
|
+
if (server !== '')
|
|
729
|
+
out.write(`Server: ${server}\n`);
|
|
730
|
+
if (ftoptions !== '')
|
|
731
|
+
out.write(`FDW options: (${ftoptions})\n`);
|
|
732
|
+
};
|
|
733
|
+
/**
|
|
734
|
+
* Render `Inherits: <parent>[, ...]` for relations with parents in
|
|
735
|
+
* `pg_inherits`. Partition parents are excluded (they're rendered via
|
|
736
|
+
* `Partition of:` instead) inside the query builder.
|
|
737
|
+
*/
|
|
738
|
+
const renderInheritsSection = async (conn, oid, out) => {
|
|
739
|
+
const q = fetchInherits({ oid });
|
|
740
|
+
const rs = await conn.query(q.sql, q.params);
|
|
741
|
+
if (rs.rows.length === 0)
|
|
742
|
+
return;
|
|
743
|
+
const label = 'Inherits';
|
|
744
|
+
const indent = ' '.repeat(label.length);
|
|
745
|
+
rs.rows.forEach((r, idx) => {
|
|
746
|
+
const parent = cellToString(r[0]);
|
|
747
|
+
const prefix = idx === 0 ? `${label}: ` : `${indent} `;
|
|
748
|
+
const trailing = idx < rs.rows.length - 1 ? ',' : '';
|
|
749
|
+
out.write(`${prefix}${parent}${trailing}\n`);
|
|
750
|
+
});
|
|
751
|
+
};
|
|
752
|
+
/**
|
|
753
|
+
* Render the child-relation footer for inheritance / partition parents.
|
|
754
|
+
*
|
|
755
|
+
* - Partitioned parents always emit a `Number of partitions: N` footer
|
|
756
|
+
* (even when zero, even in verbose mode); when verbose=false and N>0
|
|
757
|
+
* the footer adds the `(Use \d+ to list them.)` hint. Verbose mode
|
|
758
|
+
* replaces the count with a full `Partitions:` list including bounds.
|
|
759
|
+
* - Non-partition parents (regular tables) emit `Number of child
|
|
760
|
+
* tables: N (Use \d+ to list them.)` (non-verbose) or `Child tables:`
|
|
761
|
+
* list (verbose).
|
|
762
|
+
*/
|
|
763
|
+
const renderInheritedBySection = async (conn, oid, relkind, verbose, out) => {
|
|
764
|
+
const isPartitioned = relkind === 'p' || relkind === 'I';
|
|
765
|
+
const q = fetchInheritedBy({ oid, serverVersion: conn.serverVersion });
|
|
766
|
+
const rs = await conn.query(q.sql, q.params);
|
|
767
|
+
const tuples = rs.rows.length;
|
|
768
|
+
if (isPartitioned && tuples === 0) {
|
|
769
|
+
out.write('Number of partitions: 0\n');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (!verbose) {
|
|
773
|
+
if (tuples === 0)
|
|
774
|
+
return;
|
|
775
|
+
if (isPartitioned) {
|
|
776
|
+
out.write(`Number of partitions: ${tuples} (Use \\d+ to list them.)\n`);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
out.write(`Number of child tables: ${tuples} (Use \\d+ to list them.)\n`);
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// Verbose mode: list each child with its bound (for partitions) and
|
|
784
|
+
// child-relkind annotations.
|
|
785
|
+
const label = isPartitioned ? 'Partitions' : 'Child tables';
|
|
786
|
+
const indent = ' '.repeat(label.length);
|
|
787
|
+
rs.rows.forEach((r, idx) => {
|
|
788
|
+
const relname = cellToString(r[0]);
|
|
789
|
+
const childKind = cellToString(r[1] ?? '');
|
|
790
|
+
const detached = parseBool(r[2]);
|
|
791
|
+
const bound = r[3] === null || r[3] === undefined ? '' : cellToString(r[3]);
|
|
792
|
+
const prefix = idx === 0 ? `${label}: ` : `${indent} `;
|
|
793
|
+
let line = `${prefix}${relname}`;
|
|
794
|
+
if (bound !== '')
|
|
795
|
+
line += ` ${bound}`;
|
|
796
|
+
if (childKind === 'p' || childKind === 'I')
|
|
797
|
+
line += ', PARTITIONED';
|
|
798
|
+
else if (childKind === 'f')
|
|
799
|
+
line += ', FOREIGN';
|
|
800
|
+
if (detached)
|
|
801
|
+
line += ' (DETACH PENDING)';
|
|
802
|
+
if (idx < rs.rows.length - 1)
|
|
803
|
+
line += ',';
|
|
804
|
+
out.write(`${line}\n`);
|
|
805
|
+
});
|
|
806
|
+
};
|
|
807
|
+
/**
|
|
808
|
+
* Render `Replica Identity: <value>` when the relation's `relreplident`
|
|
809
|
+
* is non-default and non-INDEX. Upstream skips this footer entirely for
|
|
810
|
+
* the default value ('d' in user schemas, 'n' for pg_catalog relations);
|
|
811
|
+
* INDEX-mode (relreplident = 'i') is surfaced inline on the matching
|
|
812
|
+
* index line in the Indexes: section, so no footer is emitted there
|
|
813
|
+
* either.
|
|
814
|
+
*/
|
|
815
|
+
const renderReplicaIdentitySection = (schema, relInfo, out) => {
|
|
816
|
+
const ri = relInfo.relreplident;
|
|
817
|
+
// INDEX mode is rendered inline on the matching index — no footer.
|
|
818
|
+
if (ri === 'i')
|
|
819
|
+
return;
|
|
820
|
+
// pg_catalog relations default to 'n', user relations to 'd' — both
|
|
821
|
+
// suppress the footer when the value matches the schema default.
|
|
822
|
+
const isCatalog = schema === 'pg_catalog';
|
|
823
|
+
if (!isCatalog && ri === 'd')
|
|
824
|
+
return;
|
|
825
|
+
if (isCatalog && ri === 'n')
|
|
826
|
+
return;
|
|
827
|
+
const label = ri === 'f'
|
|
828
|
+
? 'FULL'
|
|
829
|
+
: ri === 'd'
|
|
830
|
+
? 'NOTHING'
|
|
831
|
+
: ri === 'n'
|
|
832
|
+
? 'NOTHING'
|
|
833
|
+
: '???';
|
|
834
|
+
out.write(`Replica Identity: ${label}\n`);
|
|
835
|
+
};
|
|
836
|
+
/**
|
|
837
|
+
* Emit `Tablespace: "<name>"` when the relation has an explicit
|
|
838
|
+
* (non-default) tablespace. Only meaningful for relkinds that support
|
|
839
|
+
* tablespaces — caller enforces the relkind filter.
|
|
840
|
+
*/
|
|
841
|
+
const renderTablespaceFooter = (relkind, relInfo, out) => {
|
|
842
|
+
const tsSupported = relkind === 'r' ||
|
|
843
|
+
relkind === 'm' ||
|
|
844
|
+
relkind === 'i' ||
|
|
845
|
+
relkind === 'I' ||
|
|
846
|
+
relkind === 'p' ||
|
|
847
|
+
relkind === 't';
|
|
848
|
+
if (!tsSupported)
|
|
849
|
+
return;
|
|
850
|
+
if (relInfo.reltablespace === 0 || !relInfo.spcname)
|
|
851
|
+
return;
|
|
852
|
+
out.write(`Tablespace: "${relInfo.spcname}"\n`);
|
|
853
|
+
};
|
|
854
|
+
/**
|
|
855
|
+
* Emit `Access method: <name>` when the relation has an explicit table
|
|
856
|
+
* access method (PG 12+). Indexes have their AM rendered inline within
|
|
857
|
+
* the index definition string, so this footer covers only
|
|
858
|
+
* tables / materialized views / partitioned tables.
|
|
859
|
+
*/
|
|
860
|
+
const renderAccessMethodFooter = (relInfo, out) => {
|
|
861
|
+
if (relInfo.relam === 0 || !relInfo.amname)
|
|
862
|
+
return;
|
|
863
|
+
out.write(`Access method: ${relInfo.amname}\n`);
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* Render `Indexes:\n "name" PRIMARY KEY, btree (col)` for each index
|
|
867
|
+
* on `oid`. Free-form section — not a table.
|
|
868
|
+
*
|
|
869
|
+
* When the relation has INDEX-mode replica identity (relreplident = 'i'),
|
|
870
|
+
* the corresponding index gets a trailing " REPLICA IDENTITY" marker on
|
|
871
|
+
* its line, matching upstream `\d` output. The marker comes from each
|
|
872
|
+
* index's own `pg_index.indisreplident` flag — only one index can carry
|
|
873
|
+
* it, so no follow-up footer is needed for INDEX-mode RI.
|
|
874
|
+
*/
|
|
875
|
+
const renderIndexesSection = async (conn, oid, out) => {
|
|
876
|
+
const sql = 'SELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered,\n' +
|
|
877
|
+
' i.indisvalid,\n' +
|
|
878
|
+
' pg_catalog.pg_get_indexdef(i.indexrelid, 0, true),\n' +
|
|
879
|
+
' pg_catalog.pg_get_constraintdef(con.oid, true),\n' +
|
|
880
|
+
' contype, condeferrable, condeferred,\n' +
|
|
881
|
+
' i.indisreplident,\n' +
|
|
882
|
+
' c2.reltablespace\n' +
|
|
883
|
+
'FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n' +
|
|
884
|
+
` LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))\n` +
|
|
885
|
+
`WHERE c.oid = '${oid}' AND c.oid = i.indrelid AND i.indexrelid = c2.oid\n` +
|
|
886
|
+
'ORDER BY i.indisprimary DESC, c2.relname;';
|
|
887
|
+
const rs = await conn.query(sql, []);
|
|
888
|
+
if (rs.rows.length === 0)
|
|
889
|
+
return;
|
|
890
|
+
out.write('Indexes:\n');
|
|
891
|
+
for (const r of rs.rows) {
|
|
892
|
+
const idxName = cellToString(r[0]);
|
|
893
|
+
const isPrimary = String(r[1]) === 't' || r[1] === true;
|
|
894
|
+
const isUnique = String(r[2]) === 't' || r[2] === true;
|
|
895
|
+
const isClustered = String(r[3]) === 't' || r[3] === true;
|
|
896
|
+
const isValid = String(r[4]) === 't' || r[4] === true;
|
|
897
|
+
const indexdef = cellToString(r[5]);
|
|
898
|
+
const constrDef = r[6] !== null ? cellToString(r[6]) : '';
|
|
899
|
+
const contype = r[7] === null ? '' : cellToString(r[7]);
|
|
900
|
+
const condeferrable = String(r[8]) === 't' || r[8] === true;
|
|
901
|
+
const condeferred = String(r[9]) === 't' || r[9] === true;
|
|
902
|
+
const isReplIdent = String(r[10]) === 't' || r[10] === true;
|
|
903
|
+
let line = ` "${idxName}"`;
|
|
904
|
+
// Strip everything up through " USING " from the indexdef so we get
|
|
905
|
+
// the trailing `btree (...)` clause.
|
|
906
|
+
const usingPos = indexdef.indexOf(' USING ');
|
|
907
|
+
const tail = usingPos >= 0 ? indexdef.slice(usingPos + 7) : indexdef;
|
|
908
|
+
if (contype === 'x') {
|
|
909
|
+
// Exclusion constraint: emit constraintdef verbatim, no tail.
|
|
910
|
+
line += ` ${constrDef}`;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
// Prefix label per upstream describe.c:
|
|
914
|
+
// indisprimary -> " PRIMARY KEY,"
|
|
915
|
+
// indisunique && contype=='u' -> " UNIQUE CONSTRAINT,"
|
|
916
|
+
// indisunique -> " UNIQUE,"
|
|
917
|
+
// No prefix for plain non-unique indexes.
|
|
918
|
+
if (isPrimary) {
|
|
919
|
+
line += ' PRIMARY KEY,';
|
|
920
|
+
}
|
|
921
|
+
else if (isUnique) {
|
|
922
|
+
line += contype === 'u' ? ' UNIQUE CONSTRAINT,' : ' UNIQUE,';
|
|
923
|
+
}
|
|
924
|
+
line += ` ${tail}`;
|
|
925
|
+
if (condeferrable)
|
|
926
|
+
line += ' DEFERRABLE';
|
|
927
|
+
if (condeferred)
|
|
928
|
+
line += ' INITIALLY DEFERRED';
|
|
929
|
+
}
|
|
930
|
+
if (isClustered)
|
|
931
|
+
line += ' CLUSTER';
|
|
932
|
+
if (!isValid)
|
|
933
|
+
line += ' INVALID';
|
|
934
|
+
if (isReplIdent)
|
|
935
|
+
line += ' REPLICA IDENTITY';
|
|
936
|
+
out.write(`${line}\n`);
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
/**
|
|
940
|
+
* Render `Check constraints:\n "name" CHECK (expr)` list.
|
|
941
|
+
*/
|
|
942
|
+
const renderCheckConstraintsSection = async (conn, oid, out) => {
|
|
943
|
+
const sql = 'SELECT r.conname, pg_catalog.pg_get_constraintdef(r.oid, true)\n' +
|
|
944
|
+
'FROM pg_catalog.pg_constraint r\n' +
|
|
945
|
+
`WHERE r.conrelid = '${oid}' AND r.contype = 'c'\n` +
|
|
946
|
+
'ORDER BY 1;';
|
|
947
|
+
const rs = await conn.query(sql, []);
|
|
948
|
+
if (rs.rows.length === 0)
|
|
949
|
+
return;
|
|
950
|
+
out.write('Check constraints:\n');
|
|
951
|
+
for (const r of rs.rows) {
|
|
952
|
+
out.write(` "${cellToString(r[0])}" ${cellToString(r[1])}\n`);
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
/**
|
|
956
|
+
* Render `Not-null constraints:\n "name" NOT NULL "col"[ NO INHERIT]`
|
|
957
|
+
* list (PG 18+ named NOT NULL constraints, `pg_constraint.contype = 'n'`).
|
|
958
|
+
*
|
|
959
|
+
* Upstream `describeOneTableDetails` (describe.c ~3104) emits one line per
|
|
960
|
+
* constraint in `attnum` order. `connoinherit` adds a trailing
|
|
961
|
+
* ` NO INHERIT`; inherited-only constraints (`conislocal = false`) are
|
|
962
|
+
* tagged ` (inherited)` to match vanilla `\d+`. On pre-PG-18 servers the
|
|
963
|
+
* query returns no rows, so the whole section is suppressed.
|
|
964
|
+
*/
|
|
965
|
+
const renderNotNullConstraintsSection = async (conn, oid, out) => {
|
|
966
|
+
const q = fetchNotNullConstraints({ oid, serverVersion: conn.serverVersion });
|
|
967
|
+
const rs = await conn.query(q.sql, q.params);
|
|
968
|
+
if (rs.rows.length === 0)
|
|
969
|
+
return;
|
|
970
|
+
out.write('Not-null constraints:\n');
|
|
971
|
+
for (const r of rs.rows) {
|
|
972
|
+
const conname = cellToString(r[0]);
|
|
973
|
+
const attname = cellToString(r[1]);
|
|
974
|
+
const noInherit = parseBool(r[2]);
|
|
975
|
+
const isLocal = parseBool(r[3]);
|
|
976
|
+
let line = ` "${conname}" NOT NULL "${attname}"`;
|
|
977
|
+
if (noInherit)
|
|
978
|
+
line += ' NO INHERIT';
|
|
979
|
+
else if (!isLocal)
|
|
980
|
+
line += ' (inherited)';
|
|
981
|
+
out.write(`${line}\n`);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
/**
|
|
985
|
+
* Render `Foreign-key constraints:\n "name" FOREIGN KEY ...` list.
|
|
986
|
+
*/
|
|
987
|
+
const renderForeignKeyConstraintsSection = async (conn, oid, out) => {
|
|
988
|
+
const sql = 'SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) AS condef\n' +
|
|
989
|
+
'FROM pg_catalog.pg_constraint\n' +
|
|
990
|
+
`WHERE conrelid = '${oid}' AND contype = 'f'\n` +
|
|
991
|
+
'ORDER BY conname;';
|
|
992
|
+
const rs = await conn.query(sql, []);
|
|
993
|
+
if (rs.rows.length === 0)
|
|
994
|
+
return;
|
|
995
|
+
out.write('Foreign-key constraints:\n');
|
|
996
|
+
for (const r of rs.rows) {
|
|
997
|
+
out.write(` "${cellToString(r[0])}" ${cellToString(r[1])}\n`);
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
/**
|
|
1001
|
+
* Render `Referenced by:\n TABLE "..." CONSTRAINT "..." FOREIGN KEY ...`
|
|
1002
|
+
* (incoming FKs from other tables).
|
|
1003
|
+
*/
|
|
1004
|
+
const renderReferencedBySection = async (conn, oid, out) => {
|
|
1005
|
+
const sql = 'SELECT conname, conrelid::pg_catalog.regclass,\n' +
|
|
1006
|
+
' pg_catalog.pg_get_constraintdef(oid, true) AS condef\n' +
|
|
1007
|
+
'FROM pg_catalog.pg_constraint\n' +
|
|
1008
|
+
`WHERE confrelid = '${oid}' AND contype = 'f'\n` +
|
|
1009
|
+
'ORDER BY conname;';
|
|
1010
|
+
const rs = await conn.query(sql, []);
|
|
1011
|
+
if (rs.rows.length === 0)
|
|
1012
|
+
return;
|
|
1013
|
+
out.write('Referenced by:\n');
|
|
1014
|
+
for (const r of rs.rows) {
|
|
1015
|
+
out.write(` TABLE "${cellToString(r[1])}" CONSTRAINT "${cellToString(r[0])}" ${cellToString(r[2])}\n`);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
/**
|
|
1019
|
+
* Render `Triggers:\n name AFTER ... EXECUTE ...` list.
|
|
1020
|
+
*/
|
|
1021
|
+
const renderTriggersSection = async (conn, oid, out) => {
|
|
1022
|
+
const sql = 'SELECT t.tgname, pg_catalog.pg_get_triggerdef(t.oid, true) AS tgdef, t.tgenabled\n' +
|
|
1023
|
+
'FROM pg_catalog.pg_trigger t\n' +
|
|
1024
|
+
`WHERE t.tgrelid = '${oid}' AND NOT t.tgisinternal\n` +
|
|
1025
|
+
'ORDER BY 1;';
|
|
1026
|
+
const rs = await conn.query(sql, []);
|
|
1027
|
+
if (rs.rows.length === 0)
|
|
1028
|
+
return;
|
|
1029
|
+
out.write('Triggers:\n');
|
|
1030
|
+
for (const r of rs.rows) {
|
|
1031
|
+
out.write(` ${cellToString(r[1])}\n`);
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
/**
|
|
1035
|
+
* Render `Statistics objects:\n "schema"."name" (kinds) ON cols FROM tbl`
|
|
1036
|
+
* for each `pg_statistic_ext` row on the relation. Verbose-only.
|
|
1037
|
+
*
|
|
1038
|
+
* Upstream concatenates the active "kinds" (ndistinct / dependencies / mcv)
|
|
1039
|
+
* inside parentheses; we preserve insertion order matching upstream.
|
|
1040
|
+
*/
|
|
1041
|
+
const renderStatisticsObjectsSection = async (conn, oid, out) => {
|
|
1042
|
+
const q = fetchStatisticsObjects({ oid, serverVersion: conn.serverVersion });
|
|
1043
|
+
const rs = await conn.query(q.sql, q.params);
|
|
1044
|
+
if (rs.rows.length === 0)
|
|
1045
|
+
return;
|
|
1046
|
+
out.write('Statistics objects:\n');
|
|
1047
|
+
for (const r of rs.rows) {
|
|
1048
|
+
const nsp = cellToString(r[0] ?? '');
|
|
1049
|
+
const name = cellToString(r[1] ?? '');
|
|
1050
|
+
const ndist = parseBool(r[2]);
|
|
1051
|
+
const deps = parseBool(r[3]);
|
|
1052
|
+
const mcv = parseBool(r[4]);
|
|
1053
|
+
const columns = cellToString(r[5] ?? '');
|
|
1054
|
+
const relname = cellToString(r[6] ?? '');
|
|
1055
|
+
const kinds = [];
|
|
1056
|
+
if (ndist)
|
|
1057
|
+
kinds.push('ndistinct');
|
|
1058
|
+
if (deps)
|
|
1059
|
+
kinds.push('dependencies');
|
|
1060
|
+
if (mcv)
|
|
1061
|
+
kinds.push('mcv');
|
|
1062
|
+
const kindStr = kinds.length > 0 ? ` (${kinds.join(', ')})` : '';
|
|
1063
|
+
out.write(` "${nsp}"."${name}"${kindStr} ON ${columns} FROM ${relname}\n`);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
/**
|
|
1067
|
+
* Render `Publications:\n "name"` (one per row) for any publication
|
|
1068
|
+
* the relation belongs to (explicit, FOR ALL TABLES, or FOR ALL TABLES
|
|
1069
|
+
* IN SCHEMA). No-op when the result set is empty.
|
|
1070
|
+
*/
|
|
1071
|
+
const renderPublicationsSection = async (conn, oid, out) => {
|
|
1072
|
+
const q = fetchTablePublications({ oid, serverVersion: conn.serverVersion });
|
|
1073
|
+
const rs = await conn.query(q.sql, q.params);
|
|
1074
|
+
if (rs.rows.length === 0)
|
|
1075
|
+
return;
|
|
1076
|
+
out.write('Publications:\n');
|
|
1077
|
+
for (const r of rs.rows) {
|
|
1078
|
+
out.write(` "${cellToString(r[0] ?? '')}"\n`);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
/**
|
|
1082
|
+
* Render `Subscriptions:\n "name"` (one per row). Requires superuser
|
|
1083
|
+
* access to `pg_subscription` — when the query fails with a permission
|
|
1084
|
+
* error, the section is silently omitted (mirroring upstream behaviour).
|
|
1085
|
+
*/
|
|
1086
|
+
const renderSubscriptionsSection = async (conn, oid, out) => {
|
|
1087
|
+
const q = fetchTableSubscriptions({ oid, serverVersion: conn.serverVersion });
|
|
1088
|
+
let rs;
|
|
1089
|
+
try {
|
|
1090
|
+
rs = await conn.query(q.sql, q.params);
|
|
1091
|
+
}
|
|
1092
|
+
catch (err) {
|
|
1093
|
+
if (isPermissionDeniedError(err))
|
|
1094
|
+
return;
|
|
1095
|
+
throw err;
|
|
1096
|
+
}
|
|
1097
|
+
if (rs.rows.length === 0)
|
|
1098
|
+
return;
|
|
1099
|
+
out.write('Subscriptions:\n');
|
|
1100
|
+
for (const r of rs.rows) {
|
|
1101
|
+
out.write(` "${cellToString(r[0] ?? '')}"\n`);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
/**
|
|
1105
|
+
* Pre-fetch per-column FDW options for a foreign table and index them by
|
|
1106
|
+
* column name so the column-table renderer can fold them in inline.
|
|
1107
|
+
* Upstream renders these as a trailing "FDW options: (k 'v', ...)" cell
|
|
1108
|
+
* on each affected column row, not as a separate footer.
|
|
1109
|
+
*/
|
|
1110
|
+
const fetchPerColumnFdwOptionsMap = async (conn, oid) => {
|
|
1111
|
+
const q = fetchPerColumnFdwOptions({ oid });
|
|
1112
|
+
const rs = await conn.query(q.sql, q.params);
|
|
1113
|
+
const m = new Map();
|
|
1114
|
+
for (const r of rs.rows) {
|
|
1115
|
+
const attname = cellToString(r[0] ?? '');
|
|
1116
|
+
const opts = cellToString(r[1] ?? '');
|
|
1117
|
+
if (attname !== '' && opts !== '')
|
|
1118
|
+
m.set(attname, opts);
|
|
1119
|
+
}
|
|
1120
|
+
return m;
|
|
1121
|
+
};
|
|
1122
|
+
/**
|
|
1123
|
+
* Render `Owning table: "schema.name"` for a TOAST relation. Matches
|
|
1124
|
+
* upstream's `\d <toast>` footer — upstream always emits the qualified
|
|
1125
|
+
* `"schema.name"` form (even for `pg_catalog` parents that would
|
|
1126
|
+
* otherwise be elided by search_path), so we look up the nsp+rel pair
|
|
1127
|
+
* directly rather than relying on regclass-cast text.
|
|
1128
|
+
*/
|
|
1129
|
+
const renderToastOwningTableFooter = async (conn, oid, out) => {
|
|
1130
|
+
// Side-step the regclass-cast query (which honours search_path and
|
|
1131
|
+
// would drop the `pg_catalog.` prefix for pg_catalog parents). Look
|
|
1132
|
+
// up the parent's schema + relname directly so we can render the
|
|
1133
|
+
// schema-qualified form unconditionally.
|
|
1134
|
+
const sql = 'SELECT n.nspname, c.relname\n' +
|
|
1135
|
+
'FROM pg_catalog.pg_class c\n' +
|
|
1136
|
+
'JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n' +
|
|
1137
|
+
`WHERE c.reltoastrelid = '${oid}'\n` +
|
|
1138
|
+
'LIMIT 1;';
|
|
1139
|
+
const rs = await conn.query(sql, []);
|
|
1140
|
+
if (rs.rows.length === 0)
|
|
1141
|
+
return;
|
|
1142
|
+
const nspname = cellToString(rs.rows[0][0] ?? '');
|
|
1143
|
+
const relname = cellToString(rs.rows[0][1] ?? '');
|
|
1144
|
+
if (relname === '')
|
|
1145
|
+
return;
|
|
1146
|
+
out.write(`Owning table: "${nspname}.${relname}"\n`);
|
|
1147
|
+
};
|
|
1148
|
+
/**
|
|
1149
|
+
* Detect a "permission denied" PostgresError (SQLSTATE 42501) on a
|
|
1150
|
+
* thrown value. We look at both `code` (SQLSTATE) and the message text
|
|
1151
|
+
* because not every transport layer surfaces the code. The check is
|
|
1152
|
+
* intentionally conservative — we only swallow genuine privilege
|
|
1153
|
+
* errors, not arbitrary failures.
|
|
1154
|
+
*/
|
|
1155
|
+
const isPermissionDeniedError = (err) => {
|
|
1156
|
+
if (err === null || typeof err !== 'object')
|
|
1157
|
+
return false;
|
|
1158
|
+
const code = err.code;
|
|
1159
|
+
if (typeof code === 'string' && code === '42501')
|
|
1160
|
+
return true;
|
|
1161
|
+
const message = err.message;
|
|
1162
|
+
if (typeof message === 'string' && /permission denied/i.test(message)) {
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
1166
|
+
};
|
|
1167
|
+
/**
|
|
1168
|
+
* `\ds <name>` — sequence details. Renders the columns of pg_sequence
|
|
1169
|
+
* plus the `Owned by:` footer if applicable.
|
|
1170
|
+
*/
|
|
1171
|
+
export const describeOneSequence = async (conn, oid, schema, name, out, popt) => {
|
|
1172
|
+
const sql = 'SELECT pg_catalog.format_type(seqtypid, NULL) AS "Type",\n' +
|
|
1173
|
+
' seqstart AS "Start", seqmin AS "Minimum", seqmax AS "Maximum",\n' +
|
|
1174
|
+
' seqincrement AS "Increment",\n' +
|
|
1175
|
+
" CASE WHEN seqcycle THEN 'yes' ELSE 'no' END AS \"Cycles?\",\n" +
|
|
1176
|
+
' seqcache AS "Cache"\n' +
|
|
1177
|
+
`FROM pg_catalog.pg_sequence WHERE seqrelid = '${oid}';`;
|
|
1178
|
+
const rs = await conn.query(sql, []);
|
|
1179
|
+
const title = `Sequence "${schema}.${name}"`;
|
|
1180
|
+
// Owned-by footer text is collected up-front so the printer can place
|
|
1181
|
+
// it inside the body of the result (between the data row and the
|
|
1182
|
+
// trailing blank line), matching upstream where `\d <seq>` renders
|
|
1183
|
+
// `Owned by:` AS a footer of the printed table — not as a separate
|
|
1184
|
+
// post-table line.
|
|
1185
|
+
const ownedSql = "SELECT pg_catalog.quote_ident(nspname) || '.' || pg_catalog.quote_ident(relname) || '.' || pg_catalog.quote_ident(attname)\n" +
|
|
1186
|
+
'FROM pg_catalog.pg_class c\n' +
|
|
1187
|
+
'JOIN pg_catalog.pg_depend d ON c.oid = d.refobjid\n' +
|
|
1188
|
+
'JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n' +
|
|
1189
|
+
'JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.refobjsubid\n' +
|
|
1190
|
+
`WHERE d.classid = 'pg_catalog.pg_class'::regclass AND d.refclassid = 'pg_catalog.pg_class'::regclass\n` +
|
|
1191
|
+
` AND d.objid = '${oid}' AND d.deptype IN ('a', 'i');`;
|
|
1192
|
+
const ownRs = await conn.query(ownedSql, []);
|
|
1193
|
+
const footers = [];
|
|
1194
|
+
if (ownRs.rows.length > 0) {
|
|
1195
|
+
footers.push(`Owned by: ${cellToString(ownRs.rows[0][0])}`);
|
|
1196
|
+
}
|
|
1197
|
+
// Suppress the row-count footer — upstream's sequence detail output is
|
|
1198
|
+
// a single row with no `(1 row)` line. Pass the Owned-by line as a
|
|
1199
|
+
// user footer so the printer places it before the trailing blank.
|
|
1200
|
+
const seqOpts = {
|
|
1201
|
+
...popt,
|
|
1202
|
+
title,
|
|
1203
|
+
topt: { ...popt.topt, title, defaultFooter: false },
|
|
1204
|
+
footers: footers.length > 0 ? footers : null,
|
|
1205
|
+
};
|
|
1206
|
+
await pickPrinterForFormat(seqOpts).printQuery(coerceResultSet(rs), seqOpts, out);
|
|
1207
|
+
};
|
|
1208
|
+
/**
|
|
1209
|
+
* `\sf <name>` — show function definition (full CREATE FUNCTION).
|
|
1210
|
+
* Renders the single-column result as raw text.
|
|
1211
|
+
*/
|
|
1212
|
+
export const describeOneFunctionDetails = async (conn, oid, out) => {
|
|
1213
|
+
const sql = `SELECT pg_catalog.pg_get_functiondef('${oid}'::pg_catalog.oid) AS def;`;
|
|
1214
|
+
const rs = await conn.query(sql, []);
|
|
1215
|
+
if (rs.rows.length > 0) {
|
|
1216
|
+
out.write(cellToString(rs.rows[0][0]));
|
|
1217
|
+
out.write('\n');
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
/**
|
|
1221
|
+
* `\sv <name>` — show view definition.
|
|
1222
|
+
*/
|
|
1223
|
+
export const describeOneViewDetails = async (conn, oid, schema, name, out, popt, verbose = false, hideCompression = false) => {
|
|
1224
|
+
// Use the table renderer for columns first (views have columns). In
|
|
1225
|
+
// verbose mode this also adds the Storage / Stats target / Description
|
|
1226
|
+
// columns, matching upstream `\d+ <view>`.
|
|
1227
|
+
await describeOneTableDetails(conn, oid, schema, name, 'v', verbose, out, popt, false, hideCompression);
|
|
1228
|
+
// The "View definition:" footer (verbose-only) is emitted by
|
|
1229
|
+
// describeOneTableDetails as a table footer so it renders flush with the
|
|
1230
|
+
// column rows and gets the single trailing blank — matching upstream
|
|
1231
|
+
// `describeOneTableDetails` (describe.c ~3151/3175). Nothing more to do.
|
|
1232
|
+
};
|
|
1233
|
+
/**
|
|
1234
|
+
* Translate a relkind char into the canonical header psql uses for
|
|
1235
|
+
* `\d <name>`. Examples: 'r' → `Table "schema.name"`; 'v' → `View "..."`.
|
|
1236
|
+
*/
|
|
1237
|
+
const headerForRelkind = (relkind, schema, name) => {
|
|
1238
|
+
switch (relkind) {
|
|
1239
|
+
case 'r':
|
|
1240
|
+
return `Table "${schema}.${name}"`;
|
|
1241
|
+
case 'v':
|
|
1242
|
+
return `View "${schema}.${name}"`;
|
|
1243
|
+
case 'm':
|
|
1244
|
+
return `Materialized view "${schema}.${name}"`;
|
|
1245
|
+
case 'S':
|
|
1246
|
+
return `Sequence "${schema}.${name}"`;
|
|
1247
|
+
case 'i':
|
|
1248
|
+
return `Index "${schema}.${name}"`;
|
|
1249
|
+
case 'I':
|
|
1250
|
+
return `Partitioned index "${schema}.${name}"`;
|
|
1251
|
+
case 'p':
|
|
1252
|
+
return `Partitioned table "${schema}.${name}"`;
|
|
1253
|
+
case 'f':
|
|
1254
|
+
return `Foreign table "${schema}.${name}"`;
|
|
1255
|
+
case 't':
|
|
1256
|
+
return `TOAST table "${schema}.${name}"`;
|
|
1257
|
+
case 'c':
|
|
1258
|
+
return `Composite type "${schema}.${name}"`;
|
|
1259
|
+
default:
|
|
1260
|
+
return `Relation "${schema}.${name}"`;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
/**
|
|
1264
|
+
* Build a minimal {@link FieldDescription} for synthesized rows where
|
|
1265
|
+
* we don't actually have a wire-level row description. Used by the
|
|
1266
|
+
* columns table in `describeOneTableDetails` because we synthesize
|
|
1267
|
+
* the layout from pg_attribute data.
|
|
1268
|
+
*/
|
|
1269
|
+
const fakeField = (name) => ({
|
|
1270
|
+
name,
|
|
1271
|
+
tableID: 0,
|
|
1272
|
+
columnID: 0,
|
|
1273
|
+
dataTypeID: 25,
|
|
1274
|
+
dataTypeSize: -1,
|
|
1275
|
+
dataTypeModifier: -1,
|
|
1276
|
+
format: 0,
|
|
1277
|
+
});
|