neon 2.47.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +395 -0
  5. package/dist/_shared/env-core/reuse-secrets.js +179 -0
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +575 -658
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +642 -681
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -341
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -64
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +133 -147
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +9 -7
@@ -1,948 +1,776 @@
1
- /**
2
- * Formatting backslash commands.
3
- *
4
- * TypeScript port of the `exec_command_a/C/f/H/t/T/x/pset/encoding`
5
- * implementations in upstream PostgreSQL's `src/bin/psql/command.c` and
6
- * their backing `do_pset()` / `printPsetInfo()` helpers in the same file.
7
- *
8
- * All commands mutate `settings.popt.topt` (a {@link PrintTableOpts}) in
9
- * place. Several are thin wrappers over `\pset <option>` — for instance
10
- * `\a` is equivalent to `\pset format aligned|unaligned`. We keep the
11
- * separate exports so the registry can advertise them under their public
12
- * names without aliasing oddities.
13
- *
14
- * Encoding & connection coupling: `\encoding NAME` propagates the client
15
- * encoding to the live connection via {@link Connection.setClientEncoding}
16
- * (libpq's `PQsetClientEncoding`). Following upstream `do_encoding`, we
17
- * validate the name client-side first — an unrecognised name prints
18
- * `invalid encoding name "<name>"` and leaves the encoding unchanged,
19
- * never touching the connection.
20
- *
21
- * Error format: upstream uses `\\<cmd>: <message>` for diagnostics. We
22
- * mirror that exactly, writing to stderr and returning
23
- * `{ status: 'error' }`.
24
- */
25
- import { scanSlashArgs } from "../scanner/slash.js";
26
1
  import { parseBool, parseTriple, writeErr, writeOut } from "./shared.js";
2
+ import { scanSlashArgs } from "../scanner/slash.js";
3
+ //#region src/psql/command/cmd_format.ts
4
+ /**
5
+ * Formatting backslash commands.
6
+ *
7
+ * TypeScript port of the `exec_command_a/C/f/H/t/T/x/pset/encoding`
8
+ * implementations in upstream PostgreSQL's `src/bin/psql/command.c` and
9
+ * their backing `do_pset()` / `printPsetInfo()` helpers in the same file.
10
+ *
11
+ * All commands mutate `settings.popt.topt` (a {@link PrintTableOpts}) in
12
+ * place. Several are thin wrappers over `\pset <option>` — for instance
13
+ * `\a` is equivalent to `\pset format aligned|unaligned`. We keep the
14
+ * separate exports so the registry can advertise them under their public
15
+ * names without aliasing oddities.
16
+ *
17
+ * Encoding & connection coupling: `\encoding NAME` propagates the client
18
+ * encoding to the live connection via {@link Connection.setClientEncoding}
19
+ * (libpq's `PQsetClientEncoding`). Following upstream `do_encoding`, we
20
+ * validate the name client-side first — an unrecognised name prints
21
+ * `invalid encoding name "<name>"` and leaves the encoding unchanged,
22
+ * never touching the connection.
23
+ *
24
+ * Error format: upstream uses `\\<cmd>: <message>` for diagnostics. We
25
+ * mirror that exactly, writing to stderr and returning
26
+ * `{ status: 'error' }`.
27
+ */
27
28
  /** Recognised output-format names accepted by `\pset format`. */
28
29
  const OUTPUT_FORMATS = [
29
- "aligned",
30
- "unaligned",
31
- "wrapped",
32
- "html",
33
- "asciidoc",
34
- "latex",
35
- "latex-longtable",
36
- "troff-ms",
37
- "csv",
38
- "json",
30
+ "aligned",
31
+ "unaligned",
32
+ "wrapped",
33
+ "html",
34
+ "asciidoc",
35
+ "latex",
36
+ "latex-longtable",
37
+ "troff-ms",
38
+ "csv",
39
+ "json"
39
40
  ];
40
41
  /** Convert OutputFormat to its human-readable display string. */
41
42
  const formatName = (f) => f;
42
43
  /**
43
- * Stringify a triple-state for status lines (`\x`, `\pset expanded`).
44
- * Matches upstream psql phrasing:
45
- * on → "Expanded display is on."
46
- * off → "Expanded display is off."
47
- * auto → "Expanded display is used automatically."
48
- */
44
+ * Stringify a triple-state for status lines (`\x`, `\pset expanded`).
45
+ * Matches upstream psql phrasing:
46
+ * on → "Expanded display is on."
47
+ * off → "Expanded display is off."
48
+ * auto → "Expanded display is used automatically."
49
+ */
49
50
  const tripleLabel = (value) => value === "auto" ? "used automatically" : value;
50
51
  /**
51
- * `\a` — toggle aligned/unaligned.
52
- *
53
- * Upstream `exec_command_a` flips the format via `do_pset("format", …)`
54
- * and, when not quiet, prints `Output format is <aligned|unaligned>.` —
55
- * the same `printPsetInfo("format")` line `\pset format` emits. We mirror
56
- * that here, gating the status write on QUIET like the sibling toggles.
57
- */
58
- export const cmdA = {
59
- name: "a",
60
- helpKey: "a",
61
- run: (ctx) => {
62
- const topt = ctx.settings.popt.topt;
63
- topt.format = topt.format === "aligned" ? "unaligned" : "aligned";
64
- if (!ctx.settings.quiet) {
65
- writeOut(`Output format is ${formatName(topt.format)}.\n`);
66
- }
67
- return Promise.resolve({ status: "ok" });
68
- },
52
+ * `\a` — toggle aligned/unaligned.
53
+ *
54
+ * Upstream `exec_command_a` flips the format via `do_pset("format", …)`
55
+ * and, when not quiet, prints `Output format is <aligned|unaligned>.` —
56
+ * the same `printPsetInfo("format")` line `\pset format` emits. We mirror
57
+ * that here, gating the status write on QUIET like the sibling toggles.
58
+ */
59
+ const cmdA = {
60
+ name: "a",
61
+ helpKey: "a",
62
+ run: (ctx) => {
63
+ const topt = ctx.settings.popt.topt;
64
+ topt.format = topt.format === "aligned" ? "unaligned" : "aligned";
65
+ if (!ctx.settings.quiet) writeOut(`Output format is ${formatName(topt.format)}.\n`);
66
+ return Promise.resolve({ status: "ok" });
67
+ }
69
68
  };
70
69
  /**
71
- * `\C [title]` — set or clear `topt.title`. No arg clears, any arg sets to
72
- * that string verbatim. Equivalent to `\pset title [value]`; upstream
73
- * `exec_command_C` dispatches via `do_pset("title", value, …)` so the
74
- * status line (`Title is "…".` / `Title is unset.`) is emitted by
75
- * `printPsetInfo`.
76
- */
77
- export const cmdC = {
78
- name: "C",
79
- helpKey: "C",
80
- run: (ctx) => {
81
- const arg = ctx.nextArg("normal");
82
- return Promise.resolve(applyPset(ctx.settings.popt.topt, "title", arg, ctx.cmdName, ctx.settings.quiet));
83
- },
70
+ * `\C [title]` — set or clear `topt.title`. No arg clears, any arg sets to
71
+ * that string verbatim. Equivalent to `\pset title [value]`; upstream
72
+ * `exec_command_C` dispatches via `do_pset("title", value, …)` so the
73
+ * status line (`Title is "…".` / `Title is unset.`) is emitted by
74
+ * `printPsetInfo`.
75
+ */
76
+ const cmdC = {
77
+ name: "C",
78
+ helpKey: "C",
79
+ run: (ctx) => {
80
+ const arg = ctx.nextArg("normal");
81
+ return Promise.resolve(applyPset(ctx.settings.popt.topt, "title", arg, ctx.cmdName, ctx.settings.quiet));
82
+ }
84
83
  };
85
84
  /**
86
- * `\f [sep]` — set or show the unaligned field separator. With no arg, we
87
- * print the current value (upstream prints `Field separator is "%s".`).
88
- */
89
- export const cmdF = {
90
- name: "f",
91
- helpKey: "f",
92
- run: (ctx) => {
93
- const arg = ctx.nextArg("normal");
94
- if (arg === null) {
95
- writeOut(`Field separator is "${ctx.settings.popt.topt.fieldSep}".\n`);
96
- return Promise.resolve({ status: "ok" });
97
- }
98
- ctx.settings.popt.topt.fieldSep = arg;
99
- // Upstream confirms the change (quiet-gated), like the other \pset-style
100
- // setters (review: `\f SEP` is silent).
101
- if (!ctx.settings.quiet) {
102
- writeOut(`Field separator is "${arg}".\n`);
103
- }
104
- return Promise.resolve({ status: "ok" });
105
- },
85
+ * `\f [sep]` — set or show the unaligned field separator. With no arg, we
86
+ * print the current value (upstream prints `Field separator is "%s".`).
87
+ */
88
+ const cmdF = {
89
+ name: "f",
90
+ helpKey: "f",
91
+ run: (ctx) => {
92
+ const arg = ctx.nextArg("normal");
93
+ if (arg === null) {
94
+ writeOut(`Field separator is "${ctx.settings.popt.topt.fieldSep}".\n`);
95
+ return Promise.resolve({ status: "ok" });
96
+ }
97
+ ctx.settings.popt.topt.fieldSep = arg;
98
+ if (!ctx.settings.quiet) writeOut(`Field separator is "${arg}".\n`);
99
+ return Promise.resolve({ status: "ok" });
100
+ }
106
101
  };
107
102
  /**
108
- * `\H` — toggle html on/off. If currently `html`, flip back to `aligned`;
109
- * otherwise flip to `html` (upstream remembers the prior format only
110
- * loosely — we always restore `aligned` to match the documented behaviour).
111
- */
112
- export const cmdH = {
113
- name: "H",
114
- helpKey: "H",
115
- run: (ctx) => {
116
- const topt = ctx.settings.popt.topt;
117
- topt.format = topt.format === "html" ? "aligned" : "html";
118
- return Promise.resolve({ status: "ok" });
119
- },
103
+ * `\H` — toggle html on/off. If currently `html`, flip back to `aligned`;
104
+ * otherwise flip to `html` (upstream remembers the prior format only
105
+ * loosely — we always restore `aligned` to match the documented behaviour).
106
+ */
107
+ const cmdH = {
108
+ name: "H",
109
+ helpKey: "H",
110
+ run: (ctx) => {
111
+ const topt = ctx.settings.popt.topt;
112
+ topt.format = topt.format === "html" ? "aligned" : "html";
113
+ return Promise.resolve({ status: "ok" });
114
+ }
120
115
  };
121
116
  /**
122
- * `\t [on|off|toggle]` — tuples-only. No arg → toggle.
123
- *
124
- * Equivalent to `\pset tuples_only [value]`; upstream `exec_command_t`
125
- * dispatches via `do_pset("tuples_only", opt, …)`. The
126
- * `printPsetInfo("tuples_only")` confirmation line is only emitted when
127
- * `opt` is NULL (the toggle path) — when a value is supplied,
128
- * `do_pset` returns early via `ParseVariableBool` and skips
129
- * `printPsetInfo`, so the status line is suppressed.
130
- */
131
- export const cmdT = {
132
- name: "t",
133
- helpKey: "t",
134
- run: (ctx) => {
135
- const arg = ctx.nextArg("normal");
136
- return Promise.resolve(applyPset(ctx.settings.popt.topt, "tuples_only", arg, ctx.cmdName, ctx.settings.quiet));
137
- },
117
+ * `\t [on|off|toggle]` — tuples-only. No arg → toggle.
118
+ *
119
+ * Equivalent to `\pset tuples_only [value]`; upstream `exec_command_t`
120
+ * dispatches via `do_pset("tuples_only", opt, …)`. The
121
+ * `printPsetInfo("tuples_only")` confirmation line is only emitted when
122
+ * `opt` is NULL (the toggle path) — when a value is supplied,
123
+ * `do_pset` returns early via `ParseVariableBool` and skips
124
+ * `printPsetInfo`, so the status line is suppressed.
125
+ */
126
+ const cmdT = {
127
+ name: "t",
128
+ helpKey: "t",
129
+ run: (ctx) => {
130
+ const arg = ctx.nextArg("normal");
131
+ return Promise.resolve(applyPset(ctx.settings.popt.topt, "tuples_only", arg, ctx.cmdName, ctx.settings.quiet));
132
+ }
138
133
  };
139
134
  /**
140
- * `\T [attr]` — set HTML table attributes. No arg clears. Equivalent to
141
- * `\pset tableattr [value]`; upstream `exec_command_T` dispatches via
142
- * `do_pset("tableattr", value, …)` so the status line
143
- * (`Table attributes are "…".` / `Table attributes unset.`) is emitted
144
- * by `printPsetInfo`.
145
- */
146
- export const cmdTitleAttr = {
147
- name: "T",
148
- helpKey: "T",
149
- run: (ctx) => {
150
- const arg = ctx.nextArg("normal");
151
- return Promise.resolve(applyPset(ctx.settings.popt.topt, "tableattr", arg, ctx.cmdName, ctx.settings.quiet));
152
- },
135
+ * `\T [attr]` — set HTML table attributes. No arg clears. Equivalent to
136
+ * `\pset tableattr [value]`; upstream `exec_command_T` dispatches via
137
+ * `do_pset("tableattr", value, …)` so the status line
138
+ * (`Table attributes are "…".` / `Table attributes unset.`) is emitted
139
+ * by `printPsetInfo`.
140
+ */
141
+ const cmdTitleAttr = {
142
+ name: "T",
143
+ helpKey: "T",
144
+ run: (ctx) => {
145
+ const arg = ctx.nextArg("normal");
146
+ return Promise.resolve(applyPset(ctx.settings.popt.topt, "tableattr", arg, ctx.cmdName, ctx.settings.quiet));
147
+ }
153
148
  };
154
149
  /** `\x [on|off|auto|toggle]` — expanded output. No arg → toggle. */
155
- export const cmdX = {
156
- name: "x",
157
- helpKey: "x",
158
- run: (ctx) => {
159
- const arg = ctx.nextArg("normal");
160
- const topt = ctx.settings.popt.topt;
161
- let next;
162
- if (arg === null) {
163
- next = topt.expanded === "on" ? "off" : "on";
164
- }
165
- else {
166
- const parsed = parseTriple(arg);
167
- if (parsed === null) {
168
- writeErr(`\\${ctx.cmdName}: unrecognized value "${arg}": Boolean expected\n`);
169
- return Promise.resolve({ status: "error", errorWritten: true });
170
- }
171
- if (parsed === "toggle") {
172
- next = topt.expanded === "on" ? "off" : "on";
173
- }
174
- else {
175
- next = parsed;
176
- }
177
- }
178
- topt.expanded = next;
179
- writeOut(`Expanded display is ${tripleLabel(next)}.\n`);
180
- return Promise.resolve({ status: "ok" });
181
- },
150
+ const cmdX = {
151
+ name: "x",
152
+ helpKey: "x",
153
+ run: (ctx) => {
154
+ const arg = ctx.nextArg("normal");
155
+ const topt = ctx.settings.popt.topt;
156
+ let next;
157
+ if (arg === null) next = topt.expanded === "on" ? "off" : "on";
158
+ else {
159
+ const parsed = parseTriple(arg);
160
+ if (parsed === null) {
161
+ writeErr(`\\${ctx.cmdName}: unrecognized value "${arg}": Boolean expected\n`);
162
+ return Promise.resolve({
163
+ status: "error",
164
+ errorWritten: true
165
+ });
166
+ }
167
+ if (parsed === "toggle") next = topt.expanded === "on" ? "off" : "on";
168
+ else next = parsed;
169
+ }
170
+ topt.expanded = next;
171
+ writeOut(`Expanded display is ${tripleLabel(next)}.\n`);
172
+ return Promise.resolve({ status: "ok" });
173
+ }
182
174
  };
183
175
  /**
184
- * Canonical PostgreSQL encoding names, normalised the way upstream
185
- * `pg_char_to_encoding` (`src/common/encnames.c`) normalises before
186
- * comparing: lowercased with every `-` and `_` removed. The set covers the
187
- * full `pg_enc2name` table — both the backend-usable encodings and the
188
- * client-only ones (SJIS, BIG5, …) — because upstream `\encoding` validates
189
- * via `pg_char_to_encoding`, which accepts any recognised name and lets the
190
- * server reject genuinely unusable ones.
191
- */
192
- const NORMALISED_ENCODINGS = new Set([
193
- "sqlascii",
194
- "eucjp",
195
- "euccn",
196
- "euckr",
197
- "euctw",
198
- "eucjis2004",
199
- "utf8",
200
- "muleinternal",
201
- "latin1",
202
- "latin2",
203
- "latin3",
204
- "latin4",
205
- "latin5",
206
- "latin6",
207
- "latin7",
208
- "latin8",
209
- "latin9",
210
- "latin10",
211
- "win1256",
212
- "win1258",
213
- "win866",
214
- "win874",
215
- "koi8r",
216
- "win1251",
217
- "win1252",
218
- "iso88595",
219
- "iso88596",
220
- "iso88597",
221
- "iso88598",
222
- "win1250",
223
- "win1253",
224
- "win1254",
225
- "win1255",
226
- "win1257",
227
- "koi8u",
228
- "sjis",
229
- "big5",
230
- "gbk",
231
- "uhc",
232
- "gb18030",
233
- "johab",
234
- "shiftjis2004",
235
- // Aliases upstream's encoding_match_list accepts as recognised names.
236
- "unicode", // → UTF8
237
- "mskanji", // → SJIS
238
- "shiftjis", // → SJIS
239
- "windows949", // → UHC
240
- "windows950", // → BIG5
241
- "windows936", // → GBK
242
- "tcvn", // → WIN1258
243
- "tcvn5712", // → WIN1258
244
- "vscii", // → WIN1258
245
- "alt", // → WIN866
246
- "win", // → WIN1251
247
- "koi8", // → KOI8R
248
- "abc", // → WIN1258
176
+ * Canonical PostgreSQL encoding names, normalised the way upstream
177
+ * `pg_char_to_encoding` (`src/common/encnames.c`) normalises before
178
+ * comparing: lowercased with every `-` and `_` removed. The set covers the
179
+ * full `pg_enc2name` table — both the backend-usable encodings and the
180
+ * client-only ones (SJIS, BIG5, …) — because upstream `\encoding` validates
181
+ * via `pg_char_to_encoding`, which accepts any recognised name and lets the
182
+ * server reject genuinely unusable ones.
183
+ */
184
+ const NORMALISED_ENCODINGS = /* @__PURE__ */ new Set([
185
+ "sqlascii",
186
+ "eucjp",
187
+ "euccn",
188
+ "euckr",
189
+ "euctw",
190
+ "eucjis2004",
191
+ "utf8",
192
+ "muleinternal",
193
+ "latin1",
194
+ "latin2",
195
+ "latin3",
196
+ "latin4",
197
+ "latin5",
198
+ "latin6",
199
+ "latin7",
200
+ "latin8",
201
+ "latin9",
202
+ "latin10",
203
+ "win1256",
204
+ "win1258",
205
+ "win866",
206
+ "win874",
207
+ "koi8r",
208
+ "win1251",
209
+ "win1252",
210
+ "iso88595",
211
+ "iso88596",
212
+ "iso88597",
213
+ "iso88598",
214
+ "win1250",
215
+ "win1253",
216
+ "win1254",
217
+ "win1255",
218
+ "win1257",
219
+ "koi8u",
220
+ "sjis",
221
+ "big5",
222
+ "gbk",
223
+ "uhc",
224
+ "gb18030",
225
+ "johab",
226
+ "shiftjis2004",
227
+ "unicode",
228
+ "mskanji",
229
+ "shiftjis",
230
+ "windows949",
231
+ "windows950",
232
+ "windows936",
233
+ "tcvn",
234
+ "tcvn5712",
235
+ "vscii",
236
+ "alt",
237
+ "win",
238
+ "koi8",
239
+ "abc"
249
240
  ]);
250
241
  /**
251
- * Mirror upstream `pg_char_to_encoding`'s name normalisation: drop every
252
- * `-` and `_`, lowercase, and look the result up in the canonical set.
253
- * Used to reproduce `\encoding`'s `invalid encoding name` guard without a
254
- * server round-trip.
255
- */
242
+ * Mirror upstream `pg_char_to_encoding`'s name normalisation: drop every
243
+ * `-` and `_`, lowercase, and look the result up in the canonical set.
244
+ * Used to reproduce `\encoding`'s `invalid encoding name` guard without a
245
+ * server round-trip.
246
+ */
256
247
  const isValidEncodingName = (name) => NORMALISED_ENCODINGS.has(name.replace(/[-_]/g, "").toLowerCase());
257
248
  /**
258
- * `\encoding [name]` — show or set the client encoding.
259
- *
260
- * No arg: print the current `topt.encoding`. With an arg: validate the name
261
- * the way upstream `do_encoding` does (`pg_char_to_encoding` >= 0) — an
262
- * unrecognised name prints `\encoding: invalid encoding name "<name>"`,
263
- * leaves `topt.encoding` untouched, and never calls the connection.
264
- * Otherwise push it to the live connection via
265
- * {@link Connection.setClientEncoding} (libpq `PQsetClientEncoding`) and
266
- * mirror it into `topt.encoding` so prompts/printer see the new value.
267
- */
268
- export const cmdEncoding = {
269
- name: "encoding",
270
- helpKey: "encoding",
271
- run: async (ctx) => {
272
- const arg = ctx.nextArg("normal");
273
- if (arg === null) {
274
- writeOut(`${ctx.settings.popt.topt.encoding}\n`);
275
- return { status: "ok" };
276
- }
277
- if (!isValidEncodingName(arg)) {
278
- // Upstream `do_encoding` reports the rejected name verbatim and leaves
279
- // the current encoding in place.
280
- writeErr(`\\${ctx.cmdName}: invalid encoding name "${arg}"\n`);
281
- return { status: "error", errorWritten: true };
282
- }
283
- const { db } = ctx.settings;
284
- // `PgConnection` always implements setClientEncoding; the optional call
285
- // only short-circuits when there's no live connection (or a partial mock
286
- // in tests), in which case we still mirror the encoding into topt below.
287
- if (db?.setClientEncoding) {
288
- try {
289
- await db.setClientEncoding(arg);
290
- }
291
- catch (err) {
292
- // Server refused the SET (or the connection failed mid-flight);
293
- // surface the diagnostic and leave topt.encoding unchanged, matching
294
- // upstream's "leave the encoding as it was on failure" behaviour.
295
- const msg = err instanceof Error ? err.message : String(err);
296
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
297
- return { status: "error", errorWritten: true };
298
- }
299
- }
300
- ctx.settings.popt.topt.encoding = arg;
301
- return { status: "ok" };
302
- },
249
+ * `\encoding [name]` — show or set the client encoding.
250
+ *
251
+ * No arg: print the current `topt.encoding`. With an arg: validate the name
252
+ * the way upstream `do_encoding` does (`pg_char_to_encoding` >= 0) — an
253
+ * unrecognised name prints `\encoding: invalid encoding name "<name>"`,
254
+ * leaves `topt.encoding` untouched, and never calls the connection.
255
+ * Otherwise push it to the live connection via
256
+ * {@link Connection.setClientEncoding} (libpq `PQsetClientEncoding`) and
257
+ * mirror it into `topt.encoding` so prompts/printer see the new value.
258
+ */
259
+ const cmdEncoding = {
260
+ name: "encoding",
261
+ helpKey: "encoding",
262
+ run: async (ctx) => {
263
+ const arg = ctx.nextArg("normal");
264
+ if (arg === null) {
265
+ writeOut(`${ctx.settings.popt.topt.encoding}\n`);
266
+ return { status: "ok" };
267
+ }
268
+ if (!isValidEncodingName(arg)) {
269
+ writeErr(`\\${ctx.cmdName}: invalid encoding name "${arg}"\n`);
270
+ return {
271
+ status: "error",
272
+ errorWritten: true
273
+ };
274
+ }
275
+ const { db } = ctx.settings;
276
+ if (db?.setClientEncoding) try {
277
+ await db.setClientEncoding(arg);
278
+ } catch (err) {
279
+ const msg = err instanceof Error ? err.message : String(err);
280
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
281
+ return {
282
+ status: "error",
283
+ errorWritten: true
284
+ };
285
+ }
286
+ ctx.settings.popt.topt.encoding = arg;
287
+ return { status: "ok" };
288
+ }
303
289
  };
304
290
  /**
305
- * The heart of `\pset`. Given a parsed `option` and optional `value`,
306
- * mutates `topt` in place and emits the upstream-style status line.
307
- *
308
- * Returns `{ status: 'error' }` and writes an error if the value is
309
- * unrecognised; `{ status: 'ok' }` otherwise.
310
- *
311
- * Wording reference: every status line is byte-matched against the
312
- * `printPsetInfo` table in upstream `src/bin/psql/command.c`. Notable
313
- * subtleties:
314
- *
315
- * - `tuples_only`, `footer`, and `numericlocale` are silenced when a
316
- * value is supplied — upstream `do_pset` returns directly out of
317
- * `ParseVariableBool`, never reaching `printPsetInfo`. The toggle
318
- * paths still print.
319
- * - `recordsep` renders the literal `\n` as the `<newline>` sentinel.
320
- * - `columns` reports `0` as `Target width is unset.`.
321
- * - `unicode_*_linestyle` uses the multi-word "line style" phrasing
322
- * even though the option name itself is a single token.
323
- * - `pager_min_lines` pluralizes via `ngettext` — singular at 1, plural
324
- * everywhere else (including 0).
325
- * - `csv_fieldsep` reports as `Field separator for CSV is "…".`.
326
- * - `xheader_width` quotes the named enum values (`"full"`/`"column"`/
327
- * `"page"`) and prints the numeric form unquoted.
328
- */
329
- export const applyPset = (topt, option, value, cmdName,
330
- // When `silent` is true, suppress the "X is now Y." status lines that
331
- // `\pset` emits on a successful set. Errors (invalid option / bad
332
- // value) still go to stderr. Used by `\g (option=value ...)` —
333
- // upstream applies the temporary overrides silently.
334
- silent = false) => {
335
- const writeOutMaybe = silent ? () => undefined : writeOut;
336
- const opt = option.toLowerCase();
337
- switch (opt) {
338
- case "format": {
339
- if (value === null) {
340
- writeOutMaybe(`Output format is ${formatName(topt.format)}.\n`);
341
- return { status: "ok" };
342
- }
343
- const v = value.toLowerCase();
344
- // Upstream `do_pset` accepts unambiguous prefix matches for the
345
- // format name via a hand-rolled cascade of `pg_strncasecmp` checks:
346
- //
347
- // 1. Special ambiguity guard for "aligned" vs "asciidoc" — any
348
- // input that is a prefix of BOTH is rejected with the
349
- // "ambiguous abbreviation" diagnostic (the only pair where
350
- // upstream cares).
351
- // 2. Otherwise pick the first OUTPUT_FORMATS entry that starts
352
- // with `v` (the order in OUTPUT_FORMATS — `latex` before
353
- // `latex-longtable` etc. — encodes which canonical name wins
354
- // when one is a prefix of another).
355
- const startsWithBoth = "aligned".startsWith(v) && "asciidoc".startsWith(v);
356
- if (startsWithBoth) {
357
- writeErr(`\\pset: ambiguous abbreviation "${value}" matches both "aligned" and "asciidoc"\n`);
358
- return { status: "error", errorWritten: true };
359
- }
360
- const match = OUTPUT_FORMATS.find((f) => f.startsWith(v));
361
- if (!match) {
362
- writeErr(`\\pset: allowed formats are aligned, asciidoc, csv, html, json, latex, latex-longtable, troff-ms, unaligned, wrapped\n`);
363
- return { status: "error", errorWritten: true };
364
- }
365
- topt.format = match;
366
- writeOutMaybe(`Output format is ${formatName(match)}.\n`);
367
- return { status: "ok" };
368
- }
369
- case "border": {
370
- if (value === null) {
371
- writeOutMaybe(`Border style is ${topt.border}.\n`);
372
- return { status: "ok" };
373
- }
374
- const n = parseInt(value, 10);
375
- if (!Number.isFinite(n) || n < 0 || n > 3) {
376
- writeErr(`\\pset: invalid border "${value}"\n`);
377
- return { status: "error", errorWritten: true };
378
- }
379
- topt.border = n;
380
- writeOutMaybe(`Border style is ${topt.border}.\n`);
381
- return { status: "ok" };
382
- }
383
- case "expanded":
384
- case "x": {
385
- if (value === null) {
386
- topt.expanded = topt.expanded === "on" ? "off" : "on";
387
- }
388
- else {
389
- const p = parseTriple(value);
390
- if (p === null) {
391
- writeErr(`\\pset: unrecognized value "${value}" for "expanded": Boolean expected\n`);
392
- return { status: "error", errorWritten: true };
393
- }
394
- topt.expanded =
395
- p === "toggle"
396
- ? topt.expanded === "on"
397
- ? "off"
398
- : "on"
399
- : p;
400
- }
401
- writeOutMaybe(`Expanded display is ${tripleLabel(topt.expanded)}.\n`);
402
- return { status: "ok" };
403
- }
404
- case "fieldsep": {
405
- if (value === null) {
406
- writeOutMaybe(`Field separator is "${topt.fieldSep}".\n`);
407
- return { status: "ok" };
408
- }
409
- topt.fieldSep = value;
410
- writeOutMaybe(`Field separator is "${topt.fieldSep}".\n`);
411
- return { status: "ok" };
412
- }
413
- case "fieldsep_zero": {
414
- // Upstream: any value (or none) forces fieldSep to the NUL byte.
415
- // The bulk-view's `fieldsep_zero` line is derived from fieldSep
416
- // (on iff fieldSep === '\0').
417
- topt.fieldSep = "\0";
418
- writeOutMaybe("Field separator is zero byte.\n");
419
- return { status: "ok" };
420
- }
421
- case "footer": {
422
- if (value !== null) {
423
- // Upstream `do_pset` returns directly from `ParseVariableBool`
424
- // for `footer`, bypassing the `printPsetInfo` call entirely
425
- // — `\pset footer on` is silent, while `\pset footer`
426
- // (toggle) still prints the new state.
427
- const b = parseBool(value);
428
- if (b === null) {
429
- writeErr(`\\pset: unrecognized value "${value}" for "footer": Boolean expected\n`);
430
- return { status: "error", errorWritten: true };
431
- }
432
- topt.defaultFooter = b;
433
- return { status: "ok" };
434
- }
435
- topt.defaultFooter = !topt.defaultFooter;
436
- writeOutMaybe(topt.defaultFooter
437
- ? "Default footer is on.\n"
438
- : "Default footer is off.\n");
439
- return { status: "ok" };
440
- }
441
- case "recordsep": {
442
- if (value !== null) {
443
- topt.recordSep = value;
444
- }
445
- // Upstream `printPsetInfo` has three branches: the separator-zero
446
- // path (handled by the dedicated `recordsep_zero` case), the
447
- // "<newline>" sentinel for the literal `\n` byte, and the quoted
448
- // verbatim form for everything else.
449
- if (topt.recordSep === "\n") {
450
- writeOutMaybe("Record separator is <newline>.\n");
451
- }
452
- else {
453
- writeOutMaybe(`Record separator is "${topt.recordSep}".\n`);
454
- }
455
- return { status: "ok" };
456
- }
457
- case "recordsep_zero": {
458
- topt.recordSep = "\0";
459
- writeOutMaybe("Record separator is zero byte.\n");
460
- return { status: "ok" };
461
- }
462
- case "tuples_only":
463
- case "t": {
464
- if (value !== null) {
465
- // Upstream `do_pset` returns directly from `ParseVariableBool`
466
- // for `tuples_only`, bypassing `printPsetInfo` — so
467
- // `\pset tuples_only on` (and the equivalent `\t on`) is
468
- // silent. The toggle path (no value) still prints.
469
- const b = parseBool(value);
470
- if (b === null) {
471
- writeErr(`\\pset: unrecognized value "${value}": Boolean expected\n`);
472
- return { status: "error", errorWritten: true };
473
- }
474
- topt.tuplesOnly = b;
475
- return { status: "ok" };
476
- }
477
- topt.tuplesOnly = !topt.tuplesOnly;
478
- writeOutMaybe(topt.tuplesOnly
479
- ? "Tuples only is on.\n"
480
- : "Tuples only is off.\n");
481
- return { status: "ok" };
482
- }
483
- case "title": {
484
- topt.title = value;
485
- if (value === null) {
486
- writeOutMaybe("Title is unset.\n");
487
- }
488
- else {
489
- writeOutMaybe(`Title is "${value}".\n`);
490
- }
491
- return { status: "ok" };
492
- }
493
- case "tableattr":
494
- case "t_a": {
495
- topt.tableAttr = value;
496
- if (value === null) {
497
- writeOutMaybe("Table attributes unset.\n");
498
- }
499
- else {
500
- writeOutMaybe(`Table attributes are "${value}".\n`);
501
- }
502
- return { status: "ok" };
503
- }
504
- case "pager": {
505
- if (value === null) {
506
- topt.pager = topt.pager === "off" ? "on" : "off";
507
- }
508
- else {
509
- const lower = value.toLowerCase();
510
- if (lower === "always") {
511
- topt.pager = "always";
512
- }
513
- else if (lower === "on" || lower === "off") {
514
- topt.pager = lower;
515
- }
516
- else {
517
- const b = parseBool(value);
518
- if (b === null) {
519
- writeErr(`\\pset: unrecognized value "${value}" for "pager"\n`);
520
- return { status: "error", errorWritten: true };
521
- }
522
- topt.pager = b ? "on" : "off";
523
- }
524
- }
525
- writeOutMaybe(topt.pager === "always"
526
- ? "Pager is always used.\n"
527
- : topt.pager === "on"
528
- ? "Pager is used for long output.\n"
529
- : "Pager usage is off.\n");
530
- return { status: "ok" };
531
- }
532
- case "pager_min_lines": {
533
- if (value !== null) {
534
- const n = parseInt(value, 10);
535
- if (!Number.isFinite(n) || n < 0) {
536
- writeErr(`\\pset: invalid pager_min_lines "${value}"\n`);
537
- return { status: "error", errorWritten: true };
538
- }
539
- topt.pagerMinLines = n;
540
- }
541
- // Upstream uses `ngettext` so singular ("line") fires only for
542
- // n == 1; 0 and 2+ render as "lines".
543
- const lines = topt.pagerMinLines;
544
- const unit = lines === 1 ? "line" : "lines";
545
- writeOutMaybe(`Pager won't be used for less than ${lines} ${unit}.\n`);
546
- return { status: "ok" };
547
- }
548
- case "null": {
549
- // Upstream guards the assignment with `if (value)`: the bare form
550
- // (`\pset null`, value === null) prints the current setting WITHOUT
551
- // resetting it. Only an explicit value (including the empty string,
552
- // `\pset null ''`) changes nullPrint.
553
- if (value !== null) {
554
- topt.nullPrint = value;
555
- }
556
- writeOutMaybe(`Null display is "${topt.nullPrint}".\n`);
557
- return { status: "ok" };
558
- }
559
- // `\pset display_true` / `\pset display_false` (PG 19+). Upstream guards
560
- // the assignment with `if (value)` — the bare form (no value) prints the
561
- // current setting WITHOUT resetting it, unlike `null`. The default when
562
- // unset is the literal 't' / 'f'.
563
- case "display_true": {
564
- if (value !== null) {
565
- topt.truePrint = value;
566
- }
567
- writeOutMaybe(`Boolean true display is "${topt.truePrint ?? "t"}".\n`);
568
- return { status: "ok" };
569
- }
570
- case "display_false": {
571
- if (value !== null) {
572
- topt.falsePrint = value;
573
- }
574
- writeOutMaybe(`Boolean false display is "${topt.falsePrint ?? "f"}".\n`);
575
- return { status: "ok" };
576
- }
577
- case "csv_fieldsep": {
578
- if (value !== null) {
579
- // Upstream `do_pset` splits the validation in two: length-based
580
- // ("must be a single one-byte character") fires for empty / multi-
581
- // char inputs *and* for the NUL byte (because in C the string is
582
- // NUL-terminated, so `'\0'` decodes to an empty C string). The
583
- // "cannot be a double quote, a newline, or a carriage return"
584
- // path is reserved for the three forbidden single-byte values.
585
- if (value.length !== 1 || value === "\0") {
586
- writeErr(`\\pset: csv_fieldsep must be a single one-byte character\n`);
587
- return { status: "error", errorWritten: true };
588
- }
589
- if (value === '"' || value === "\n" || value === "\r") {
590
- writeErr(`\\pset: csv_fieldsep cannot be a double quote, a newline, or a carriage return\n`);
591
- return { status: "error", errorWritten: true };
592
- }
593
- topt.csvFieldSep = value;
594
- }
595
- // Upstream wording: "Field separator for CSV is "%s".".
596
- writeOutMaybe(`Field separator for CSV is "${topt.csvFieldSep}".\n`);
597
- return { status: "ok" };
598
- }
599
- case "numericlocale": {
600
- if (value !== null) {
601
- // Upstream `do_pset` returns directly from `ParseVariableBool`
602
- // for `numericlocale`, bypassing `printPsetInfo`. The toggle
603
- // path (no value) still prints the new state.
604
- const p = parseTriple(value);
605
- if (p === null || p === "auto") {
606
- writeErr(`\\pset: unrecognized value "${value}" for "numericlocale": Boolean expected\n`);
607
- return { status: "error", errorWritten: true };
608
- }
609
- topt.numericLocale =
610
- p === "toggle" ? !topt.numericLocale : p === "on";
611
- return { status: "ok" };
612
- }
613
- topt.numericLocale = !topt.numericLocale;
614
- writeOutMaybe(topt.numericLocale
615
- ? "Locale-adjusted numeric output is on.\n"
616
- : "Locale-adjusted numeric output is off.\n");
617
- return { status: "ok" };
618
- }
619
- case "linestyle": {
620
- if (value === null) {
621
- writeOutMaybe(`Line style is ${topt.unicodeBorderLineStyle}.\n`);
622
- return { status: "ok" };
623
- }
624
- const lower = value.toLowerCase();
625
- // Preserve 'old-ascii' verbatim so the bulk-view (`\pset` with no
626
- // args) round-trips and the printer can pick the matching glyph
627
- // table. Upstream `do_pset("linestyle", "old-ascii", …)` flips
628
- // `popt.topt.line_style = &pg_asciiformat_old`; we carry the same
629
- // three-way distinction on `unicodeBorderLineStyle`.
630
- if (lower === "ascii" ||
631
- lower === "old-ascii" ||
632
- lower === "unicode") {
633
- const ls = lower;
634
- topt.unicodeBorderLineStyle = ls;
635
- topt.unicodeColumnLineStyle = ls;
636
- topt.unicodeHeaderLineStyle = ls;
637
- writeOutMaybe(`Line style is ${ls}.\n`);
638
- return { status: "ok" };
639
- }
640
- writeErr(`\\pset: allowed line styles are ascii, old-ascii, unicode\n`);
641
- return { status: "error", errorWritten: true };
642
- }
643
- case "columns": {
644
- if (value !== null) {
645
- const n = parseInt(value, 10);
646
- if (!Number.isFinite(n) || n < 0) {
647
- writeErr(`\\pset: invalid columns "${value}"\n`);
648
- return { status: "error", errorWritten: true };
649
- }
650
- topt.columns = n;
651
- }
652
- // Upstream `printPsetInfo` reports `0` as the special "unset"
653
- // sentinel — see `command.c:5433`.
654
- if (topt.columns === 0) {
655
- writeOutMaybe("Target width is unset.\n");
656
- }
657
- else {
658
- writeOutMaybe(`Target width is ${topt.columns}.\n`);
659
- }
660
- return { status: "ok" };
661
- }
662
- case "xheader_width": {
663
- if (value !== null) {
664
- const lower = value.toLowerCase();
665
- if (lower === "full" ||
666
- lower === "column" ||
667
- lower === "page") {
668
- topt.xheaderWidth = lower;
669
- }
670
- else {
671
- const n = parseInt(value, 10);
672
- if (!Number.isFinite(n) ||
673
- n <= 0 ||
674
- !/^[+]?\d+$/.test(value.trim())) {
675
- writeErr(`\\pset: allowed xheader_width values are "full" (default), "column", "page", or a number specifying the exact width\n`);
676
- return { status: "error", errorWritten: true };
677
- }
678
- topt.xheaderWidth = n;
679
- }
680
- }
681
- // Upstream `printPsetInfo` quotes the three named widths
682
- // ("full" / "column" / "page") but renders the numeric form
683
- // unquoted as `Expanded header width is 33.`.
684
- const current = topt.xheaderWidth ?? "full";
685
- if (typeof current === "number") {
686
- writeOutMaybe(`Expanded header width is ${current}.\n`);
687
- }
688
- else {
689
- writeOutMaybe(`Expanded header width is "${current}".\n`);
690
- }
691
- return { status: "ok" };
692
- }
693
- case "unicode_border_linestyle":
694
- case "unicode_column_linestyle":
695
- case "unicode_header_linestyle": {
696
- // Upstream `printPsetInfo` renders these as
697
- // `Unicode border line style is "single".` etc. — note the space
698
- // between "line" and "style" in the message (the option name
699
- // itself is one token, `linestyle`).
700
- const which = opt === "unicode_border_linestyle"
701
- ? "border"
702
- : opt === "unicode_column_linestyle"
703
- ? "column"
704
- : "header";
705
- if (value !== null) {
706
- const lower = value.toLowerCase();
707
- if (lower !== "single" && lower !== "double") {
708
- writeErr(`\\pset: ${opt} must be single or double\n`);
709
- return { status: "error", errorWritten: true };
710
- }
711
- const style = lower;
712
- if (opt === "unicode_border_linestyle") {
713
- topt.unicodeBorderStyle = style;
714
- }
715
- else if (opt === "unicode_column_linestyle") {
716
- topt.unicodeColumnStyle = style;
717
- }
718
- else {
719
- topt.unicodeHeaderStyle = style;
720
- }
721
- }
722
- const current = opt === "unicode_border_linestyle"
723
- ? (topt.unicodeBorderStyle ?? "single")
724
- : opt === "unicode_column_linestyle"
725
- ? (topt.unicodeColumnStyle ?? "single")
726
- : (topt.unicodeHeaderStyle ?? "single");
727
- writeOutMaybe(`Unicode ${which} line style is "${current}".\n`);
728
- return { status: "ok" };
729
- }
730
- default: {
731
- writeErr(`\\pset: unknown option "${option}"\n`);
732
- return { status: "error", errorWritten: true };
733
- }
734
- }
291
+ * The heart of `\pset`. Given a parsed `option` and optional `value`,
292
+ * mutates `topt` in place and emits the upstream-style status line.
293
+ *
294
+ * Returns `{ status: 'error' }` and writes an error if the value is
295
+ * unrecognised; `{ status: 'ok' }` otherwise.
296
+ *
297
+ * Wording reference: every status line is byte-matched against the
298
+ * `printPsetInfo` table in upstream `src/bin/psql/command.c`. Notable
299
+ * subtleties:
300
+ *
301
+ * - `tuples_only`, `footer`, and `numericlocale` are silenced when a
302
+ * value is supplied — upstream `do_pset` returns directly out of
303
+ * `ParseVariableBool`, never reaching `printPsetInfo`. The toggle
304
+ * paths still print.
305
+ * - `recordsep` renders the literal `\n` as the `<newline>` sentinel.
306
+ * - `columns` reports `0` as `Target width is unset.`.
307
+ * - `unicode_*_linestyle` uses the multi-word "line style" phrasing
308
+ * even though the option name itself is a single token.
309
+ * - `pager_min_lines` pluralizes via `ngettext` — singular at 1, plural
310
+ * everywhere else (including 0).
311
+ * - `csv_fieldsep` reports as `Field separator for CSV is "…".`.
312
+ * - `xheader_width` quotes the named enum values (`"full"`/`"column"`/
313
+ * `"page"`) and prints the numeric form unquoted.
314
+ */
315
+ const applyPset = (topt, option, value, cmdName, silent = false) => {
316
+ const writeOutMaybe = silent ? () => void 0 : writeOut;
317
+ const opt = option.toLowerCase();
318
+ switch (opt) {
319
+ case "format": {
320
+ if (value === null) {
321
+ writeOutMaybe(`Output format is ${formatName(topt.format)}.\n`);
322
+ return { status: "ok" };
323
+ }
324
+ const v = value.toLowerCase();
325
+ if ("aligned".startsWith(v) && "asciidoc".startsWith(v)) {
326
+ writeErr(`\\pset: ambiguous abbreviation "${value}" matches both "aligned" and "asciidoc"\n`);
327
+ return {
328
+ status: "error",
329
+ errorWritten: true
330
+ };
331
+ }
332
+ const match = OUTPUT_FORMATS.find((f) => f.startsWith(v));
333
+ if (!match) {
334
+ writeErr(`\\pset: allowed formats are aligned, asciidoc, csv, html, json, latex, latex-longtable, troff-ms, unaligned, wrapped\n`);
335
+ return {
336
+ status: "error",
337
+ errorWritten: true
338
+ };
339
+ }
340
+ topt.format = match;
341
+ writeOutMaybe(`Output format is ${formatName(match)}.\n`);
342
+ return { status: "ok" };
343
+ }
344
+ case "border": {
345
+ if (value === null) {
346
+ writeOutMaybe(`Border style is ${topt.border}.\n`);
347
+ return { status: "ok" };
348
+ }
349
+ const n = parseInt(value, 10);
350
+ if (!Number.isFinite(n) || n < 0 || n > 3) {
351
+ writeErr(`\\pset: invalid border "${value}"\n`);
352
+ return {
353
+ status: "error",
354
+ errorWritten: true
355
+ };
356
+ }
357
+ topt.border = n;
358
+ writeOutMaybe(`Border style is ${topt.border}.\n`);
359
+ return { status: "ok" };
360
+ }
361
+ case "expanded":
362
+ case "x":
363
+ if (value === null) topt.expanded = topt.expanded === "on" ? "off" : "on";
364
+ else {
365
+ const p = parseTriple(value);
366
+ if (p === null) {
367
+ writeErr(`\\pset: unrecognized value "${value}" for "expanded": Boolean expected\n`);
368
+ return {
369
+ status: "error",
370
+ errorWritten: true
371
+ };
372
+ }
373
+ topt.expanded = p === "toggle" ? topt.expanded === "on" ? "off" : "on" : p;
374
+ }
375
+ writeOutMaybe(`Expanded display is ${tripleLabel(topt.expanded)}.\n`);
376
+ return { status: "ok" };
377
+ case "fieldsep":
378
+ if (value === null) {
379
+ writeOutMaybe(`Field separator is "${topt.fieldSep}".\n`);
380
+ return { status: "ok" };
381
+ }
382
+ topt.fieldSep = value;
383
+ writeOutMaybe(`Field separator is "${topt.fieldSep}".\n`);
384
+ return { status: "ok" };
385
+ case "fieldsep_zero":
386
+ topt.fieldSep = "\0";
387
+ writeOutMaybe("Field separator is zero byte.\n");
388
+ return { status: "ok" };
389
+ case "footer":
390
+ if (value !== null) {
391
+ const b = parseBool(value);
392
+ if (b === null) {
393
+ writeErr(`\\pset: unrecognized value "${value}" for "footer": Boolean expected\n`);
394
+ return {
395
+ status: "error",
396
+ errorWritten: true
397
+ };
398
+ }
399
+ topt.defaultFooter = b;
400
+ return { status: "ok" };
401
+ }
402
+ topt.defaultFooter = !topt.defaultFooter;
403
+ writeOutMaybe(topt.defaultFooter ? "Default footer is on.\n" : "Default footer is off.\n");
404
+ return { status: "ok" };
405
+ case "recordsep":
406
+ if (value !== null) topt.recordSep = value;
407
+ if (topt.recordSep === "\n") writeOutMaybe("Record separator is <newline>.\n");
408
+ else writeOutMaybe(`Record separator is "${topt.recordSep}".\n`);
409
+ return { status: "ok" };
410
+ case "recordsep_zero":
411
+ topt.recordSep = "\0";
412
+ writeOutMaybe("Record separator is zero byte.\n");
413
+ return { status: "ok" };
414
+ case "tuples_only":
415
+ case "t":
416
+ if (value !== null) {
417
+ const b = parseBool(value);
418
+ if (b === null) {
419
+ writeErr(`\\pset: unrecognized value "${value}": Boolean expected\n`);
420
+ return {
421
+ status: "error",
422
+ errorWritten: true
423
+ };
424
+ }
425
+ topt.tuplesOnly = b;
426
+ return { status: "ok" };
427
+ }
428
+ topt.tuplesOnly = !topt.tuplesOnly;
429
+ writeOutMaybe(topt.tuplesOnly ? "Tuples only is on.\n" : "Tuples only is off.\n");
430
+ return { status: "ok" };
431
+ case "title":
432
+ topt.title = value;
433
+ if (value === null) writeOutMaybe("Title is unset.\n");
434
+ else writeOutMaybe(`Title is "${value}".\n`);
435
+ return { status: "ok" };
436
+ case "tableattr":
437
+ case "t_a":
438
+ topt.tableAttr = value;
439
+ if (value === null) writeOutMaybe("Table attributes unset.\n");
440
+ else writeOutMaybe(`Table attributes are "${value}".\n`);
441
+ return { status: "ok" };
442
+ case "pager":
443
+ if (value === null) topt.pager = topt.pager === "off" ? "on" : "off";
444
+ else {
445
+ const lower = value.toLowerCase();
446
+ if (lower === "always") topt.pager = "always";
447
+ else if (lower === "on" || lower === "off") topt.pager = lower;
448
+ else {
449
+ const b = parseBool(value);
450
+ if (b === null) {
451
+ writeErr(`\\pset: unrecognized value "${value}" for "pager"\n`);
452
+ return {
453
+ status: "error",
454
+ errorWritten: true
455
+ };
456
+ }
457
+ topt.pager = b ? "on" : "off";
458
+ }
459
+ }
460
+ writeOutMaybe(topt.pager === "always" ? "Pager is always used.\n" : topt.pager === "on" ? "Pager is used for long output.\n" : "Pager usage is off.\n");
461
+ return { status: "ok" };
462
+ case "pager_min_lines": {
463
+ if (value !== null) {
464
+ const n = parseInt(value, 10);
465
+ if (!Number.isFinite(n) || n < 0) {
466
+ writeErr(`\\pset: invalid pager_min_lines "${value}"\n`);
467
+ return {
468
+ status: "error",
469
+ errorWritten: true
470
+ };
471
+ }
472
+ topt.pagerMinLines = n;
473
+ }
474
+ const lines = topt.pagerMinLines;
475
+ writeOutMaybe(`Pager won't be used for less than ${lines} ${lines === 1 ? "line" : "lines"}.\n`);
476
+ return { status: "ok" };
477
+ }
478
+ case "null":
479
+ if (value !== null) topt.nullPrint = value;
480
+ writeOutMaybe(`Null display is "${topt.nullPrint}".\n`);
481
+ return { status: "ok" };
482
+ case "display_true":
483
+ if (value !== null) topt.truePrint = value;
484
+ writeOutMaybe(`Boolean true display is "${topt.truePrint ?? "t"}".\n`);
485
+ return { status: "ok" };
486
+ case "display_false":
487
+ if (value !== null) topt.falsePrint = value;
488
+ writeOutMaybe(`Boolean false display is "${topt.falsePrint ?? "f"}".\n`);
489
+ return { status: "ok" };
490
+ case "csv_fieldsep":
491
+ if (value !== null) {
492
+ if (value.length !== 1 || value === "\0") {
493
+ writeErr(`\\pset: csv_fieldsep must be a single one-byte character\n`);
494
+ return {
495
+ status: "error",
496
+ errorWritten: true
497
+ };
498
+ }
499
+ if (value === "\"" || value === "\n" || value === "\r") {
500
+ writeErr(`\\pset: csv_fieldsep cannot be a double quote, a newline, or a carriage return\n`);
501
+ return {
502
+ status: "error",
503
+ errorWritten: true
504
+ };
505
+ }
506
+ topt.csvFieldSep = value;
507
+ }
508
+ writeOutMaybe(`Field separator for CSV is "${topt.csvFieldSep}".\n`);
509
+ return { status: "ok" };
510
+ case "numericlocale":
511
+ if (value !== null) {
512
+ const p = parseTriple(value);
513
+ if (p === null || p === "auto") {
514
+ writeErr(`\\pset: unrecognized value "${value}" for "numericlocale": Boolean expected\n`);
515
+ return {
516
+ status: "error",
517
+ errorWritten: true
518
+ };
519
+ }
520
+ topt.numericLocale = p === "toggle" ? !topt.numericLocale : p === "on";
521
+ return { status: "ok" };
522
+ }
523
+ topt.numericLocale = !topt.numericLocale;
524
+ writeOutMaybe(topt.numericLocale ? "Locale-adjusted numeric output is on.\n" : "Locale-adjusted numeric output is off.\n");
525
+ return { status: "ok" };
526
+ case "linestyle": {
527
+ if (value === null) {
528
+ writeOutMaybe(`Line style is ${topt.unicodeBorderLineStyle}.\n`);
529
+ return { status: "ok" };
530
+ }
531
+ const lower = value.toLowerCase();
532
+ if (lower === "ascii" || lower === "old-ascii" || lower === "unicode") {
533
+ const ls = lower;
534
+ topt.unicodeBorderLineStyle = ls;
535
+ topt.unicodeColumnLineStyle = ls;
536
+ topt.unicodeHeaderLineStyle = ls;
537
+ writeOutMaybe(`Line style is ${ls}.\n`);
538
+ return { status: "ok" };
539
+ }
540
+ writeErr(`\\pset: allowed line styles are ascii, old-ascii, unicode\n`);
541
+ return {
542
+ status: "error",
543
+ errorWritten: true
544
+ };
545
+ }
546
+ case "columns":
547
+ if (value !== null) {
548
+ const n = parseInt(value, 10);
549
+ if (!Number.isFinite(n) || n < 0) {
550
+ writeErr(`\\pset: invalid columns "${value}"\n`);
551
+ return {
552
+ status: "error",
553
+ errorWritten: true
554
+ };
555
+ }
556
+ topt.columns = n;
557
+ }
558
+ if (topt.columns === 0) writeOutMaybe("Target width is unset.\n");
559
+ else writeOutMaybe(`Target width is ${topt.columns}.\n`);
560
+ return { status: "ok" };
561
+ case "xheader_width": {
562
+ if (value !== null) {
563
+ const lower = value.toLowerCase();
564
+ if (lower === "full" || lower === "column" || lower === "page") topt.xheaderWidth = lower;
565
+ else {
566
+ const n = parseInt(value, 10);
567
+ if (!Number.isFinite(n) || n <= 0 || !/^[+]?\d+$/.test(value.trim())) {
568
+ writeErr(`\\pset: allowed xheader_width values are "full" (default), "column", "page", or a number specifying the exact width\n`);
569
+ return {
570
+ status: "error",
571
+ errorWritten: true
572
+ };
573
+ }
574
+ topt.xheaderWidth = n;
575
+ }
576
+ }
577
+ const current = topt.xheaderWidth ?? "full";
578
+ if (typeof current === "number") writeOutMaybe(`Expanded header width is ${current}.\n`);
579
+ else writeOutMaybe(`Expanded header width is "${current}".\n`);
580
+ return { status: "ok" };
581
+ }
582
+ case "unicode_border_linestyle":
583
+ case "unicode_column_linestyle":
584
+ case "unicode_header_linestyle": {
585
+ const which = opt === "unicode_border_linestyle" ? "border" : opt === "unicode_column_linestyle" ? "column" : "header";
586
+ if (value !== null) {
587
+ const lower = value.toLowerCase();
588
+ if (lower !== "single" && lower !== "double") {
589
+ writeErr(`\\pset: ${opt} must be single or double\n`);
590
+ return {
591
+ status: "error",
592
+ errorWritten: true
593
+ };
594
+ }
595
+ const style = lower;
596
+ if (opt === "unicode_border_linestyle") topt.unicodeBorderStyle = style;
597
+ else if (opt === "unicode_column_linestyle") topt.unicodeColumnStyle = style;
598
+ else topt.unicodeHeaderStyle = style;
599
+ }
600
+ writeOutMaybe(`Unicode ${which} line style is "${opt === "unicode_border_linestyle" ? topt.unicodeBorderStyle ?? "single" : opt === "unicode_column_linestyle" ? topt.unicodeColumnStyle ?? "single" : topt.unicodeHeaderStyle ?? "single"}".\n`);
601
+ return { status: "ok" };
602
+ }
603
+ default:
604
+ writeErr(`\\pset: unknown option "${option}"\n`);
605
+ return {
606
+ status: "error",
607
+ errorWritten: true
608
+ };
609
+ }
735
610
  };
736
611
  /**
737
- * Wrap a string value in single quotes, escaping embedded newlines and
738
- * single quotes. Mirrors upstream `pset_quoted_string` in
739
- * `src/bin/psql/command.c` — used by the bulk-view formatter so the
740
- * emitted line can be fed back into `\pset NAME VALUE`.
741
- */
612
+ * Wrap a string value in single quotes, escaping embedded newlines and
613
+ * single quotes. Mirrors upstream `pset_quoted_string` in
614
+ * `src/bin/psql/command.c` — used by the bulk-view formatter so the
615
+ * emitted line can be fed back into `\pset NAME VALUE`.
616
+ */
742
617
  const psetQuotedString = (str) => {
743
- let out = "'";
744
- for (const ch of str) {
745
- if (ch === "\n")
746
- out += "\\n";
747
- else if (ch === "'")
748
- out += "\\'";
749
- else
750
- out += ch;
751
- }
752
- out += "'";
753
- return out;
618
+ let out = "'";
619
+ for (const ch of str) if (ch === "\n") out += "\\n";
620
+ else if (ch === "'") out += "\\'";
621
+ else out += ch;
622
+ out += "'";
623
+ return out;
754
624
  };
755
625
  /**
756
- * Render the numeric pager encoding upstream uses in `printPsetInfo`:
757
- * 0 = never, 1 = "if needed" (our `'on'`), 2 = always. We keep
758
- * `topt.pager` as the upstream-style triple ('off'|'on'|'always') for
759
- * `applyPset`'s state machine; this is only the bulk-view conversion.
760
- */
626
+ * Render the numeric pager encoding upstream uses in `printPsetInfo`:
627
+ * 0 = never, 1 = "if needed" (our `'on'`), 2 = always. We keep
628
+ * `topt.pager` as the upstream-style triple ('off'|'on'|'always') for
629
+ * `applyPset`'s state machine; this is only the bulk-view conversion.
630
+ */
761
631
  const pagerNumeric = (pager) => pager === "off" ? 0 : pager === "on" ? 1 : 2;
762
632
  /**
763
- * Render `xheader_width` for the bulk view. Enum values print verbatim;
764
- * numeric values print as the integer.
765
- */
766
- const xheaderWidthDisplay = (w) => (typeof w === "number" ? String(w) : w);
633
+ * Render `xheader_width` for the bulk view. Enum values print verbatim;
634
+ * numeric values print as the integer.
635
+ */
636
+ const xheaderWidthDisplay = (w) => typeof w === "number" ? String(w) : w;
767
637
  /**
768
- * Print the full current `\pset` state, one option per line, to stdout.
769
- * Used when `\pset` is invoked with no arguments. String-valued settings
770
- * are single-quoted (matching upstream `pset_value_string`); `tableattr`
771
- * and `title` are unquoted-empty when unset. The set, ordering, and
772
- * column-spacing mirror `printPsetInfo` in `src/bin/psql/command.c`.
773
- */
638
+ * Print the full current `\pset` state, one option per line, to stdout.
639
+ * Used when `\pset` is invoked with no arguments. String-valued settings
640
+ * are single-quoted (matching upstream `pset_value_string`); `tableattr`
641
+ * and `title` are unquoted-empty when unset. The set, ordering, and
642
+ * column-spacing mirror `printPsetInfo` in `src/bin/psql/command.c`.
643
+ */
774
644
  const printAllPset = (topt) => {
775
- writeOut(`border ${topt.border}\n`);
776
- writeOut(`columns ${topt.columns}\n`);
777
- writeOut(`csv_fieldsep ${psetQuotedString(topt.csvFieldSep)}\n`);
778
- writeOut(`display_false ${psetQuotedString(topt.falsePrint ?? "f")}\n`);
779
- writeOut(`display_true ${psetQuotedString(topt.truePrint ?? "t")}\n`);
780
- writeOut(`expanded ${topt.expanded}\n`);
781
- writeOut(`fieldsep ${psetQuotedString(topt.fieldSep)}\n`);
782
- // fieldsep_zero / recordsep_zero are derived: upstream emits "on" iff
783
- // the corresponding separator is the NUL byte.
784
- writeOut(`fieldsep_zero ${topt.fieldSep === "\0" ? "on" : "off"}\n`);
785
- writeOut(`footer ${topt.defaultFooter ? "on" : "off"}\n`);
786
- writeOut(`format ${topt.format}\n`);
787
- writeOut(`linestyle ${topt.unicodeBorderLineStyle}\n`);
788
- writeOut(`null ${psetQuotedString(topt.nullPrint)}\n`);
789
- writeOut(`numericlocale ${topt.numericLocale ? "on" : "off"}\n`);
790
- // pager is emitted numerically (0/1/2) — upstream uses %d in printPsetInfo.
791
- writeOut(`pager ${pagerNumeric(topt.pager)}\n`);
792
- writeOut(`pager_min_lines ${topt.pagerMinLines}\n`);
793
- writeOut(`recordsep ${psetQuotedString(topt.recordSep)}\n`);
794
- writeOut(`recordsep_zero ${topt.recordSep === "\0" ? "on" : "off"}\n`);
795
- writeOut(`tableattr ${topt.tableAttr === null ? "" : psetQuotedString(topt.tableAttr)}\n`);
796
- writeOut(`title ${topt.title === null ? "" : psetQuotedString(topt.title)}\n`);
797
- writeOut(`tuples_only ${topt.tuplesOnly ? "on" : "off"}\n`);
798
- writeOut(`unicode_border_linestyle ${topt.unicodeBorderStyle ?? "single"}\n`);
799
- writeOut(`unicode_column_linestyle ${topt.unicodeColumnStyle ?? "single"}\n`);
800
- writeOut(`unicode_header_linestyle ${topt.unicodeHeaderStyle ?? "single"}\n`);
801
- writeOut(`xheader_width ${xheaderWidthDisplay(topt.xheaderWidth ?? "full")}\n`);
645
+ writeOut(`border ${topt.border}\n`);
646
+ writeOut(`columns ${topt.columns}\n`);
647
+ writeOut(`csv_fieldsep ${psetQuotedString(topt.csvFieldSep)}\n`);
648
+ writeOut(`display_false ${psetQuotedString(topt.falsePrint ?? "f")}\n`);
649
+ writeOut(`display_true ${psetQuotedString(topt.truePrint ?? "t")}\n`);
650
+ writeOut(`expanded ${topt.expanded}\n`);
651
+ writeOut(`fieldsep ${psetQuotedString(topt.fieldSep)}\n`);
652
+ writeOut(`fieldsep_zero ${topt.fieldSep === "\0" ? "on" : "off"}\n`);
653
+ writeOut(`footer ${topt.defaultFooter ? "on" : "off"}\n`);
654
+ writeOut(`format ${topt.format}\n`);
655
+ writeOut(`linestyle ${topt.unicodeBorderLineStyle}\n`);
656
+ writeOut(`null ${psetQuotedString(topt.nullPrint)}\n`);
657
+ writeOut(`numericlocale ${topt.numericLocale ? "on" : "off"}\n`);
658
+ writeOut(`pager ${pagerNumeric(topt.pager)}\n`);
659
+ writeOut(`pager_min_lines ${topt.pagerMinLines}\n`);
660
+ writeOut(`recordsep ${psetQuotedString(topt.recordSep)}\n`);
661
+ writeOut(`recordsep_zero ${topt.recordSep === "\0" ? "on" : "off"}\n`);
662
+ writeOut(`tableattr ${topt.tableAttr === null ? "" : psetQuotedString(topt.tableAttr)}\n`);
663
+ writeOut(`title ${topt.title === null ? "" : psetQuotedString(topt.title)}\n`);
664
+ writeOut(`tuples_only ${topt.tuplesOnly ? "on" : "off"}\n`);
665
+ writeOut(`unicode_border_linestyle ${topt.unicodeBorderStyle ?? "single"}\n`);
666
+ writeOut(`unicode_column_linestyle ${topt.unicodeColumnStyle ?? "single"}\n`);
667
+ writeOut(`unicode_header_linestyle ${topt.unicodeHeaderStyle ?? "single"}\n`);
668
+ writeOut(`xheader_width ${xheaderWidthDisplay(topt.xheaderWidth ?? "full")}\n`);
802
669
  };
803
670
  const lexRawArgs = (tail) => {
804
- const out = [];
805
- let i = 0;
806
- const isSpace = (c) => c === " " ||
807
- c === "\t" ||
808
- c === "\n" ||
809
- c === "\r" ||
810
- c === "\f" ||
811
- c === "\v";
812
- while (i < tail.length) {
813
- while (i < tail.length && isSpace(tail[i]))
814
- i++;
815
- if (i >= tail.length)
816
- break;
817
- if (tail[i] === "\\")
818
- break;
819
- let arg = "";
820
- while (i < tail.length && !isSpace(tail[i]) && tail[i] !== "\\") {
821
- const c = tail[i];
822
- if (c === "'") {
823
- // Single-quoted: keep delimiters in the warning text so the user
824
- // sees the literal token.
825
- let j = i + 1;
826
- let inner = "";
827
- while (j < tail.length && tail[j] !== "'") {
828
- if (tail[j] === "\\" && j + 1 < tail.length) {
829
- inner += tail[j] + tail[j + 1];
830
- j += 2;
831
- continue;
832
- }
833
- inner += tail[j];
834
- j++;
835
- }
836
- arg += `'${inner}'`;
837
- i = j < tail.length ? j + 1 : j;
838
- continue;
839
- }
840
- if (c === '"') {
841
- let j = i + 1;
842
- let inner = "";
843
- while (j < tail.length && tail[j] !== '"') {
844
- inner += tail[j];
845
- j++;
846
- }
847
- arg += `"${inner}"`;
848
- i = j < tail.length ? j + 1 : j;
849
- continue;
850
- }
851
- if (c === "`") {
852
- // Drop the backtick delimiters; don't run the command (OT_NO_EVAL).
853
- let j = i + 1;
854
- let inner = "";
855
- while (j < tail.length && tail[j] !== "`") {
856
- inner += tail[j];
857
- j++;
858
- }
859
- arg += inner;
860
- i = j < tail.length ? j + 1 : j;
861
- continue;
862
- }
863
- arg += c;
864
- i++;
865
- }
866
- out.push({ arg, endIdx: i });
867
- }
868
- return out;
671
+ const out = [];
672
+ let i = 0;
673
+ const isSpace = (c) => c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === "\v";
674
+ while (i < tail.length) {
675
+ while (i < tail.length && isSpace(tail[i])) i++;
676
+ if (i >= tail.length) break;
677
+ if (tail[i] === "\\") break;
678
+ let arg = "";
679
+ while (i < tail.length && !isSpace(tail[i]) && tail[i] !== "\\") {
680
+ const c = tail[i];
681
+ if (c === "'") {
682
+ let j = i + 1;
683
+ let inner = "";
684
+ while (j < tail.length && tail[j] !== "'") {
685
+ if (tail[j] === "\\" && j + 1 < tail.length) {
686
+ inner += tail[j] + tail[j + 1];
687
+ j += 2;
688
+ continue;
689
+ }
690
+ inner += tail[j];
691
+ j++;
692
+ }
693
+ arg += `'${inner}'`;
694
+ i = j < tail.length ? j + 1 : j;
695
+ continue;
696
+ }
697
+ if (c === "\"") {
698
+ let j = i + 1;
699
+ let inner = "";
700
+ while (j < tail.length && tail[j] !== "\"") {
701
+ inner += tail[j];
702
+ j++;
703
+ }
704
+ arg += `"${inner}"`;
705
+ i = j < tail.length ? j + 1 : j;
706
+ continue;
707
+ }
708
+ if (c === "`") {
709
+ let j = i + 1;
710
+ let inner = "";
711
+ while (j < tail.length && tail[j] !== "`") {
712
+ inner += tail[j];
713
+ j++;
714
+ }
715
+ arg += inner;
716
+ i = j < tail.length ? j + 1 : j;
717
+ continue;
718
+ }
719
+ arg += c;
720
+ i++;
721
+ }
722
+ out.push({
723
+ arg,
724
+ endIdx: i
725
+ });
726
+ }
727
+ return out;
869
728
  };
870
729
  /**
871
- * `\pset [option [value]]` — the master print-options setter.
872
- *
873
- * - No args: print all options.
874
- * - Option only: toggle (for booleans) or show current value.
875
- * - Option + value: set.
876
- * - Option + value + extra: set, then emit one `extra argument "X" ignored`
877
- * per extra arg (matches upstream `exec_command_pset`'s post-`do_pset`
878
- * call to `ignore_slash_options`).
879
- *
880
- * Implementation note: the surrounding REPL's `BackslashContext` (built
881
- * in `core/mainloop.ts::makeBackslashContext`) returns the full
882
- * `rawArgs` verbatim from `restOfLine()` — there's no cursor we can
883
- * read to find the unconsumed tail. We therefore lex `rawArgs` a second
884
- * time with {@link lexExtraArgs} to recover the *raw* (unexpanded)
885
- * token text, while still calling `ctx.nextArg('normal')` for
886
- * option/value so variable substitution and backtick execution behave
887
- * exactly like the rest of the slash-command layer. The drain pulls
888
- * the raw token text from index 2+ so the warning preserves the
889
- * upstream `OT_NO_EVAL` semantics — `:foo` stays as `:foo`, `` `cmd` ``
890
- * collapses to `cmd` without running.
891
- */
892
- export const cmdPset = {
893
- name: "pset",
894
- helpKey: "pset",
895
- run: (ctx) => {
896
- // Upstream `exec_command_pset` calls `psql_scan_slash_option` twice
897
- // (with `OT_NORMAL`, `evaluate=true`) for option + value, then loops
898
- // `psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false)` with
899
- // `evaluate=false` over the remainder to emit the "extra argument …
900
- // ignored" warnings. Our `BackslashContext.nextArg('normal')` route
901
- // pre-parses the entire `rawArgs` through `scanSlashArgs` on the
902
- // first call, which evaluates EVERY backtick — so an invocation
903
- // like `\pset fieldsep | \`nosuchcommand\` :foo` would spawn the
904
- // shell for `nosuchcommand` even though upstream never runs it.
905
- //
906
- // To match upstream's no-eval semantics we split the lex in two:
907
- // 1. Walk `rawArgs` no-eval with {@link lexRawArgs} to recover the
908
- // byte boundary that ends arg #2 (option + value).
909
- // 2. Re-parse the truncated prefix with `scanSlashArgs('normal')`
910
- // so option/value get their proper `:var` / `` ` ` `` expansion.
911
- // 3. Walk the tail with `lexRawArgs` no-eval and emit one
912
- // "extra argument … ignored" warning per token, using the raw
913
- // (unexpanded) text so `:foo` stays `:foo` and `` `cmd` ``
914
- // collapses to `cmd` without running.
915
- const rawEntries = lexRawArgs(ctx.rawArgs);
916
- if (rawEntries.length === 0) {
917
- printAllPset(ctx.settings.popt.topt);
918
- return Promise.resolve({ status: "ok" });
919
- }
920
- // Slice rawArgs up to the end of arg #2 (or arg #1 if only one was
921
- // provided) and feed THAT to the eval-mode scanner. Anything past
922
- // that boundary lives in the no-eval extras zone.
923
- const headEndIdx = rawEntries.length >= 2
924
- ? rawEntries[1].endIdx
925
- : rawEntries[0].endIdx;
926
- const headSlice = ctx.rawArgs.slice(0, headEndIdx);
927
- const varLookup = (name) => ctx.settings.vars.get(name);
928
- const headArgs = scanSlashArgs(headSlice, "normal", varLookup);
929
- const option = headArgs[0] ?? null;
930
- if (option === null) {
931
- printAllPset(ctx.settings.popt.topt);
932
- return Promise.resolve({ status: "ok" });
933
- }
934
- const value = headArgs[1] ?? null;
935
- // Under `--quiet` / `\set QUIET on`, upstream `exec_command_pset`
936
- // (and the printPsetInfo helper it delegates to) suppresses the
937
- // confirmation lines like `Null display is "…".` and `Tuples only
938
- // is on.`. Pass `silent=true` so applyPset skips the writes —
939
- // errors (invalid option / bad value) still go to stderr.
940
- const result = applyPset(ctx.settings.popt.topt, option, value, ctx.cmdName, ctx.settings.quiet);
941
- // Drain extras. Index 2+ in the no-eval lex are the tokens past
942
- // option+value; emit one warning per raw token.
943
- for (let i = 2; i < rawEntries.length; i++) {
944
- writeErr(`\\${ctx.cmdName}: extra argument "${rawEntries[i].arg}" ignored\n`);
945
- }
946
- return Promise.resolve(result);
947
- },
730
+ * `\pset [option [value]]` — the master print-options setter.
731
+ *
732
+ * - No args: print all options.
733
+ * - Option only: toggle (for booleans) or show current value.
734
+ * - Option + value: set.
735
+ * - Option + value + extra: set, then emit one `extra argument "X" ignored`
736
+ * per extra arg (matches upstream `exec_command_pset`'s post-`do_pset`
737
+ * call to `ignore_slash_options`).
738
+ *
739
+ * Implementation note: the surrounding REPL's `BackslashContext` (built
740
+ * in `core/mainloop.ts::makeBackslashContext`) returns the full
741
+ * `rawArgs` verbatim from `restOfLine()` — there's no cursor we can
742
+ * read to find the unconsumed tail. We therefore lex `rawArgs` a second
743
+ * time with {@link lexExtraArgs} to recover the *raw* (unexpanded)
744
+ * token text, while still calling `ctx.nextArg('normal')` for
745
+ * option/value so variable substitution and backtick execution behave
746
+ * exactly like the rest of the slash-command layer. The drain pulls
747
+ * the raw token text from index 2+ so the warning preserves the
748
+ * upstream `OT_NO_EVAL` semantics — `:foo` stays as `:foo`, `` `cmd` ``
749
+ * collapses to `cmd` without running.
750
+ */
751
+ const cmdPset = {
752
+ name: "pset",
753
+ helpKey: "pset",
754
+ run: (ctx) => {
755
+ const rawEntries = lexRawArgs(ctx.rawArgs);
756
+ if (rawEntries.length === 0) {
757
+ printAllPset(ctx.settings.popt.topt);
758
+ return Promise.resolve({ status: "ok" });
759
+ }
760
+ const headEndIdx = rawEntries.length >= 2 ? rawEntries[1].endIdx : rawEntries[0].endIdx;
761
+ const headSlice = ctx.rawArgs.slice(0, headEndIdx);
762
+ const varLookup = (name) => ctx.settings.vars.get(name);
763
+ const headArgs = scanSlashArgs(headSlice, "normal", varLookup);
764
+ const option = headArgs[0] ?? null;
765
+ if (option === null) {
766
+ printAllPset(ctx.settings.popt.topt);
767
+ return Promise.resolve({ status: "ok" });
768
+ }
769
+ const value = headArgs[1] ?? null;
770
+ const result = applyPset(ctx.settings.popt.topt, option, value, ctx.cmdName, ctx.settings.quiet);
771
+ for (let i = 2; i < rawEntries.length; i++) writeErr(`\\${ctx.cmdName}: extra argument "${rawEntries[i].arg}" ignored\n`);
772
+ return Promise.resolve(result);
773
+ }
948
774
  };
775
+ //#endregion
776
+ export { applyPset, cmdA, cmdC, cmdEncoding, cmdF, cmdH, cmdPset, cmdT, cmdTitleAttr, cmdX };