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