neonctl 2.22.2 → 2.23.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 (113) hide show
  1. package/README.md +84 -0
  2. package/analytics.js +5 -2
  3. package/commands/branches.js +9 -1
  4. package/commands/connection_string.js +9 -1
  5. package/commands/functions.js +277 -0
  6. package/commands/index.js +4 -0
  7. package/commands/neon_auth.js +1013 -0
  8. package/commands/projects.js +9 -1
  9. package/commands/psql.js +6 -1
  10. package/functions_api.js +44 -0
  11. package/package.json +15 -5
  12. package/psql/cli.js +51 -0
  13. package/psql/command/cmd_cond.js +437 -0
  14. package/psql/command/cmd_connect.js +815 -0
  15. package/psql/command/cmd_copy.js +1025 -0
  16. package/psql/command/cmd_describe.js +1810 -0
  17. package/psql/command/cmd_format.js +909 -0
  18. package/psql/command/cmd_io.js +2187 -0
  19. package/psql/command/cmd_lo.js +385 -0
  20. package/psql/command/cmd_meta.js +970 -0
  21. package/psql/command/cmd_misc.js +187 -0
  22. package/psql/command/cmd_pipeline.js +1141 -0
  23. package/psql/command/cmd_restrict.js +171 -0
  24. package/psql/command/cmd_show.js +751 -0
  25. package/psql/command/dispatch.js +343 -0
  26. package/psql/command/inputQueue.js +42 -0
  27. package/psql/command/shared.js +71 -0
  28. package/psql/complete/filenames.js +139 -0
  29. package/psql/complete/index.js +104 -0
  30. package/psql/complete/matcher.js +314 -0
  31. package/psql/complete/psqlVars.js +247 -0
  32. package/psql/complete/queries.js +491 -0
  33. package/psql/complete/rules.js +2387 -0
  34. package/psql/core/common.js +1250 -0
  35. package/psql/core/help.js +576 -0
  36. package/psql/core/mainloop.js +1353 -0
  37. package/psql/core/prompt.js +437 -0
  38. package/psql/core/settings.js +684 -0
  39. package/psql/core/sqlHelp.js +1066 -0
  40. package/psql/core/startup.js +840 -0
  41. package/psql/core/syncVars.js +116 -0
  42. package/psql/core/variables.js +287 -0
  43. package/psql/describe/formatters.js +1277 -0
  44. package/psql/describe/processNamePattern.js +270 -0
  45. package/psql/describe/queries.js +2373 -0
  46. package/psql/describe/versionGate.js +43 -0
  47. package/psql/index.js +2005 -0
  48. package/psql/io/history.js +299 -0
  49. package/psql/io/input.js +120 -0
  50. package/psql/io/lineEditor/buffer.js +323 -0
  51. package/psql/io/lineEditor/complete.js +227 -0
  52. package/psql/io/lineEditor/filename.js +159 -0
  53. package/psql/io/lineEditor/index.js +891 -0
  54. package/psql/io/lineEditor/keymap.js +738 -0
  55. package/psql/io/lineEditor/vt100.js +363 -0
  56. package/psql/io/pgpass.js +202 -0
  57. package/psql/io/pgservice.js +194 -0
  58. package/psql/io/psqlrc.js +422 -0
  59. package/psql/print/aligned.js +1756 -0
  60. package/psql/print/asciidoc.js +248 -0
  61. package/psql/print/crosstab.js +460 -0
  62. package/psql/print/csv.js +92 -0
  63. package/psql/print/html.js +258 -0
  64. package/psql/print/json.js +96 -0
  65. package/psql/print/latex.js +396 -0
  66. package/psql/print/pager.js +265 -0
  67. package/psql/print/troff.js +258 -0
  68. package/psql/print/unaligned.js +118 -0
  69. package/psql/print/units.js +135 -0
  70. package/psql/scanner/slash.js +513 -0
  71. package/psql/scanner/sql.js +910 -0
  72. package/psql/scanner/stringutils.js +390 -0
  73. package/psql/types/backslash.js +1 -0
  74. package/psql/types/connection.js +1 -0
  75. package/psql/types/index.js +7 -0
  76. package/psql/types/printer.js +1 -0
  77. package/psql/types/repl.js +1 -0
  78. package/psql/types/scanner.js +24 -0
  79. package/psql/types/settings.js +1 -0
  80. package/psql/types/variables.js +1 -0
  81. package/psql/wire/connection.js +2844 -0
  82. package/psql/wire/copy.js +108 -0
  83. package/psql/wire/notify.js +59 -0
  84. package/psql/wire/pipeline.js +519 -0
  85. package/psql/wire/protocol.js +466 -0
  86. package/psql/wire/sasl.js +296 -0
  87. package/psql/wire/tls.js +596 -0
  88. package/test_utils/fixtures.js +1 -0
  89. package/utils/esbuild.js +147 -0
  90. package/utils/psql.js +107 -11
  91. package/utils/zip.js +4 -0
  92. package/writer.js +1 -1
  93. package/commands/auth.test.js +0 -211
  94. package/commands/branches.test.js +0 -460
  95. package/commands/checkout.test.js +0 -170
  96. package/commands/connection_string.test.js +0 -196
  97. package/commands/data_api.test.js +0 -169
  98. package/commands/databases.test.js +0 -39
  99. package/commands/help.test.js +0 -9
  100. package/commands/init.test.js +0 -56
  101. package/commands/ip_allow.test.js +0 -59
  102. package/commands/link.test.js +0 -381
  103. package/commands/operations.test.js +0 -7
  104. package/commands/orgs.test.js +0 -7
  105. package/commands/projects.test.js +0 -144
  106. package/commands/psql.test.js +0 -49
  107. package/commands/roles.test.js +0 -37
  108. package/commands/set_context.test.js +0 -159
  109. package/commands/vpc_endpoints.test.js +0 -69
  110. package/context.test.js +0 -119
  111. package/env.test.js +0 -55
  112. package/utils/formats.test.js +0 -32
  113. package/writer.test.js +0 -104
@@ -0,0 +1,2387 @@
1
+ /**
2
+ * Tab-completion rule body.
3
+ *
4
+ * Port (selective — see "Coverage" below) of psql's `psql_completion()`
5
+ * from `src/bin/psql/tab-complete.in.c`. Given the tokenized "previous
6
+ * words" and the in-progress current word, we walk a chain of
7
+ * Matches/TailMatches/HeadMatches guards and return:
8
+ *
9
+ * - a STATIC list (for keyword / enum-value completion), filtered by the
10
+ * current word's prefix, OR
11
+ * - a CATALOG query (for table/view/role/schema/etc.) executed against
12
+ * the live Connection.
13
+ *
14
+ * Coverage (the parts of upstream we ship):
15
+ *
16
+ * - Backslash command name completion: `\` → list of commands.
17
+ * - Backslash arg completion for the high-traffic commands: `\c[onnect]`,
18
+ * `\dt`/`\d`/`\dv`/`\dm`/`\di`/`\ds`, `\df`, `\dn`, `\du`/`\dg`,
19
+ * `\dx`, `\dL`, `\dT`, `\do` (operators), `\dC` (casts), `\encoding`,
20
+ * `\pset`, `\set`.
21
+ * - Top-level SQL keyword set (SELECT/INSERT/UPDATE/DELETE/etc).
22
+ * - Mid-statement object completion in the most common contexts:
23
+ * FROM → tables/views/matviews/foreign tables.
24
+ * INTO (INSERT) → tables.
25
+ * JOIN → tables/views/matviews.
26
+ * UPDATE → tables.
27
+ * DELETE FROM → tables.
28
+ * ALTER TABLE → tables, then sub-action (ADD/DROP/…),
29
+ * then sub-action continuation (COLUMN/CONSTRAINT/…).
30
+ * ALTER VIEW → views + sub-actions.
31
+ * ALTER MATERIALIZED VIEW → mat-views + sub-actions.
32
+ * ALTER INDEX → indexes + sub-actions.
33
+ * ALTER SEQUENCE → sequences + sub-actions.
34
+ * ALTER FUNCTION / PROCEDURE / ROUTINE → functions + sub-actions.
35
+ * ALTER TYPE → types + sub-actions.
36
+ * ALTER ROLE/USER → roles + sub-actions.
37
+ * ALTER DATABASE → databases + sub-actions.
38
+ * ALTER SCHEMA → schemas + sub-actions.
39
+ * ALTER EXTENSION → extensions + sub-actions.
40
+ * ALTER POLICY → ON <table>, rename/owner.
41
+ * ALTER PUBLICATION / SUBSCRIPTION → sub-actions.
42
+ * DROP TABLE → tables.
43
+ * DROP VIEW → views.
44
+ * DROP INDEX → indexes.
45
+ * DROP MATERIALIZED VIEW → mat views.
46
+ * DROP SEQUENCE → sequences.
47
+ * DROP TYPE → types.
48
+ * DROP SCHEMA → schemas.
49
+ * DROP EXTENSION → extensions.
50
+ * DROP ROLE / USER → roles.
51
+ * DROP DATABASE → databases.
52
+ * DROP FUNCTION → functions.
53
+ * CREATE INDEX → CONCURRENTLY / IF NOT EXISTS / ON / USING (access methods).
54
+ * GRANT / REVOKE … ON … → tables.
55
+ * TRUNCATE [TABLE] → tables.
56
+ * LOCK TABLE → tables.
57
+ * COPY → tables.
58
+ * ANALYZE / VACUUM → tables.
59
+ * REINDEX → indexes/tables/databases (limited).
60
+ * SET <guc> → list of GUC names from pg_settings.
61
+ * SET ROLE → roles.
62
+ * SET SCHEMA → schemas.
63
+ * SHOW <guc> → list of GUC names from pg_settings.
64
+ * RESET <guc> → list of GUC names from pg_settings.
65
+ *
66
+ * - Window-function clauses: `OVER (` → PARTITION BY / ORDER BY / RANGE /
67
+ * ROWS / GROUPS.
68
+ * - Generic post-FROM/JOIN tail keywords: JOIN, WHERE, GROUP BY, ORDER BY,
69
+ * LIMIT, OFFSET, UNION, INTERSECT, EXCEPT, etc.
70
+ * - WHERE-expression continuations: AND / OR / IS / IN / NOT / BETWEEN.
71
+ * - `\set`/`\unset`: completion of psql variable names.
72
+ * - Variable expansion `:NAME` completion (inside any line).
73
+ *
74
+ * Upstream coverage notes (psql `tab-complete.in.c`):
75
+ * - We ported the ALTER-OBJECT arms around lines 2050-2700 (sub-actions
76
+ * for TABLE/VIEW/MV/INDEX/SEQUENCE/FUNCTION/TYPE/ROLE/DB/SCHEMA/
77
+ * EXTENSION/POLICY/PUBLICATION/SUBSCRIPTION) but elided the deep
78
+ * option-value continuations (e.g. ALTER TABLE … ADD CONSTRAINT …
79
+ * CHECK (…)) and partition-bound clauses.
80
+ * - We ported the post-FROM / JOIN tail-keyword set from lines ~4500-4700.
81
+ * - We ported the SET/SHOW/RESET GUC lookup from lines ~5500-5700, using
82
+ * a live pg_settings query rather than a static list.
83
+ * - We ported the CREATE INDEX block at ~3000-3100.
84
+ * - Skipped: ALTER-system fine-grained options, COMMENT ON full grammar,
85
+ * CREATE STATISTICS, CREATE EVENT TRIGGER bodies, FDW/USER MAPPING
86
+ * argument grammar, and most GRANT/REVOKE class continuations beyond
87
+ * the table object form.
88
+ *
89
+ * What's intentionally still stubbed:
90
+ *
91
+ * - psql `\h` SQL help index — exists in psql proper, would need the
92
+ * help index data.
93
+ * - Column-name completion after `SELECT … FROM t WHERE` (we don't carry
94
+ * a parsed alias→relation map; upstream parses the FROM clause).
95
+ *
96
+ * The shape mirrors the C source closely enough that adding new rules is
97
+ * mechanical: drop a new `if (TailMatches(...))` arm in the right region.
98
+ */
99
+ import { completeFilenames, isCopyFromOrTo } from './filenames.js';
100
+ import { HeadMatches, MatchAny, TailMatches, tokenize } from './matcher.js';
101
+ import { Query_for_constraint_of_table, Query_for_constraint_of_table_in_schema, Query_for_list_of_casts, Query_for_list_of_databases, Query_for_list_of_datatypes, Query_for_list_of_enum_values_quoted, Query_for_list_of_extensions, Query_for_list_of_functions, Query_for_list_of_index_access_methods, Query_for_list_of_indexes, Query_for_list_of_languages, Query_for_list_of_matviews, Query_for_list_of_operators, Query_for_list_of_publications, Query_for_list_of_relations_in_schema, Query_for_list_of_roles, Query_for_list_of_schemas, Query_for_list_of_sequences, Query_for_list_of_set_vars, Query_for_list_of_subscriptions, Query_for_list_of_tables, Query_for_list_of_tables_for_constraint, Query_for_list_of_tables_for_constraint_in_schema, Query_for_list_of_tables_views, Query_for_list_of_tablespaces, Query_for_list_of_timezone_names_quoted_in, Query_for_list_of_timezone_names_quoted_out, Query_for_list_of_types, Query_for_list_of_views, Query_for_values_of_enum_GUC, runCatalogQuery, } from './queries.js';
102
+ import { ENCODINGS, PSET_OPTIONS, SPECIAL_VARIABLES, psetValuesFor, variableValuesFor, } from './psqlVars.js';
103
+ /** Backslash command names psql tab-completes (mirrors backslash_commands[]). */
104
+ export const BACKSLASH_COMMANDS = [
105
+ '\\a',
106
+ '\\bind',
107
+ '\\bind_named',
108
+ '\\c',
109
+ '\\C',
110
+ '\\cd',
111
+ '\\close_prepared',
112
+ '\\conninfo',
113
+ '\\connect',
114
+ '\\copy',
115
+ '\\copyright',
116
+ '\\crosstabview',
117
+ '\\d',
118
+ '\\dA',
119
+ '\\dAc',
120
+ '\\dAf',
121
+ '\\dAo',
122
+ '\\dAp',
123
+ '\\da',
124
+ '\\db',
125
+ '\\dC',
126
+ '\\dc',
127
+ '\\dconfig',
128
+ '\\dD',
129
+ '\\dd',
130
+ '\\ddp',
131
+ '\\dE',
132
+ '\\des',
133
+ '\\det',
134
+ '\\deu',
135
+ '\\dew',
136
+ '\\df',
137
+ '\\dF',
138
+ '\\dFd',
139
+ '\\dFp',
140
+ '\\dFt',
141
+ '\\dg',
142
+ '\\di',
143
+ '\\dl',
144
+ '\\dL',
145
+ '\\dm',
146
+ '\\dn',
147
+ '\\do',
148
+ '\\dO',
149
+ '\\dp',
150
+ '\\dP',
151
+ '\\dPi',
152
+ '\\dPt',
153
+ '\\drds',
154
+ '\\drg',
155
+ '\\dRs',
156
+ '\\dRp',
157
+ '\\ds',
158
+ '\\dt',
159
+ '\\dT',
160
+ '\\dv',
161
+ '\\du',
162
+ '\\dx',
163
+ '\\dX',
164
+ '\\dy',
165
+ '\\echo',
166
+ '\\edit',
167
+ '\\ef',
168
+ '\\elif',
169
+ '\\else',
170
+ '\\encoding',
171
+ '\\endif',
172
+ '\\endpipeline',
173
+ '\\errverbose',
174
+ '\\ev',
175
+ '\\f',
176
+ '\\flush',
177
+ '\\flushrequest',
178
+ '\\g',
179
+ '\\gdesc',
180
+ '\\getenv',
181
+ '\\getresults',
182
+ '\\gexec',
183
+ '\\gset',
184
+ '\\gx',
185
+ '\\help',
186
+ '\\html',
187
+ '\\if',
188
+ '\\include',
189
+ '\\include_relative',
190
+ '\\ir',
191
+ '\\l',
192
+ '\\list',
193
+ '\\lo_export',
194
+ '\\lo_import',
195
+ '\\lo_list',
196
+ '\\lo_unlink',
197
+ '\\o',
198
+ '\\out',
199
+ '\\parse',
200
+ '\\password',
201
+ '\\print',
202
+ '\\prompt',
203
+ '\\pset',
204
+ '\\q',
205
+ '\\qecho',
206
+ '\\quit',
207
+ '\\reset',
208
+ '\\restrict',
209
+ '\\s',
210
+ '\\sendpipeline',
211
+ '\\set',
212
+ '\\setenv',
213
+ '\\sf',
214
+ '\\startpipeline',
215
+ '\\sv',
216
+ '\\syncpipeline',
217
+ '\\t',
218
+ '\\T',
219
+ '\\timing',
220
+ '\\unrestrict',
221
+ '\\unset',
222
+ '\\w',
223
+ '\\warn',
224
+ '\\watch',
225
+ '\\write',
226
+ '\\x',
227
+ '\\z',
228
+ '\\!',
229
+ '\\?',
230
+ ];
231
+ /** Top-level SQL statement keywords. */
232
+ export const SQL_TOP_KEYWORDS = [
233
+ 'ABORT',
234
+ 'ALTER',
235
+ 'ANALYZE',
236
+ 'BEGIN',
237
+ 'CALL',
238
+ 'CHECKPOINT',
239
+ 'CLOSE',
240
+ 'CLUSTER',
241
+ 'COMMENT',
242
+ 'COMMIT',
243
+ 'COPY',
244
+ 'CREATE',
245
+ 'DEALLOCATE',
246
+ 'DECLARE',
247
+ 'DELETE FROM',
248
+ 'DISCARD',
249
+ 'DO',
250
+ 'DROP',
251
+ 'END',
252
+ 'EXECUTE',
253
+ 'EXPLAIN',
254
+ 'FETCH',
255
+ 'GRANT',
256
+ 'IMPORT',
257
+ 'INSERT INTO',
258
+ 'LISTEN',
259
+ 'LOAD',
260
+ 'LOCK',
261
+ 'MERGE',
262
+ 'MOVE',
263
+ 'NOTIFY',
264
+ 'PREPARE',
265
+ 'REASSIGN',
266
+ 'REFRESH MATERIALIZED VIEW',
267
+ 'REINDEX',
268
+ 'RELEASE',
269
+ 'RESET',
270
+ 'REVOKE',
271
+ 'ROLLBACK',
272
+ 'SAVEPOINT',
273
+ 'SECURITY LABEL',
274
+ 'SELECT',
275
+ 'SET',
276
+ 'SHOW',
277
+ 'START TRANSACTION',
278
+ 'TABLE',
279
+ 'TRUNCATE',
280
+ 'UNLISTEN',
281
+ 'UPDATE',
282
+ 'VACUUM',
283
+ 'VALUES',
284
+ 'WITH',
285
+ ];
286
+ /** Keywords accepted after CREATE. */
287
+ export const CREATE_OBJECTS = [
288
+ 'ACCESS METHOD',
289
+ 'AGGREGATE',
290
+ 'CAST',
291
+ 'COLLATION',
292
+ 'CONVERSION',
293
+ 'DATABASE',
294
+ 'DEFAULT PRIVILEGES',
295
+ 'DOMAIN',
296
+ 'EVENT TRIGGER',
297
+ 'EXTENSION',
298
+ 'FOREIGN DATA WRAPPER',
299
+ 'FOREIGN TABLE',
300
+ 'FUNCTION',
301
+ 'GLOBAL',
302
+ 'GROUP',
303
+ 'INDEX',
304
+ 'LANGUAGE',
305
+ 'LOCAL',
306
+ 'MATERIALIZED VIEW',
307
+ 'OPERATOR',
308
+ 'OR REPLACE',
309
+ 'POLICY',
310
+ 'PROCEDURE',
311
+ 'PUBLICATION',
312
+ 'ROLE',
313
+ 'RULE',
314
+ 'SCHEMA',
315
+ 'SEQUENCE',
316
+ 'SERVER',
317
+ 'STATISTICS',
318
+ 'SUBSCRIPTION',
319
+ 'TABLE',
320
+ 'TABLESPACE',
321
+ 'TEMP',
322
+ 'TEMPORARY',
323
+ 'TEXT SEARCH',
324
+ 'TRANSFORM',
325
+ 'TRIGGER',
326
+ 'TYPE',
327
+ 'UNIQUE',
328
+ 'UNLOGGED',
329
+ 'USER',
330
+ 'VIEW',
331
+ ];
332
+ /** Keywords accepted after DROP. */
333
+ export const DROP_OBJECTS = [
334
+ 'ACCESS METHOD',
335
+ 'AGGREGATE',
336
+ 'CAST',
337
+ 'COLLATION',
338
+ 'CONVERSION',
339
+ 'DATABASE',
340
+ 'DOMAIN',
341
+ 'EVENT TRIGGER',
342
+ 'EXTENSION',
343
+ 'FOREIGN DATA WRAPPER',
344
+ 'FOREIGN TABLE',
345
+ 'FUNCTION',
346
+ 'GROUP',
347
+ 'INDEX',
348
+ 'LANGUAGE',
349
+ 'MATERIALIZED VIEW',
350
+ 'OPERATOR',
351
+ 'OWNED',
352
+ 'POLICY',
353
+ 'PROCEDURE',
354
+ 'PUBLICATION',
355
+ 'ROLE',
356
+ 'RULE',
357
+ 'SCHEMA',
358
+ 'SEQUENCE',
359
+ 'SERVER',
360
+ 'STATISTICS',
361
+ 'SUBSCRIPTION',
362
+ 'TABLE',
363
+ 'TABLESPACE',
364
+ 'TEXT SEARCH',
365
+ 'TRANSFORM',
366
+ 'TRIGGER',
367
+ 'TYPE',
368
+ 'USER',
369
+ 'VIEW',
370
+ ];
371
+ /** Keywords accepted after ALTER. */
372
+ export const ALTER_OBJECTS = [
373
+ 'AGGREGATE',
374
+ 'COLLATION',
375
+ 'CONVERSION',
376
+ 'DATABASE',
377
+ 'DEFAULT PRIVILEGES',
378
+ 'DOMAIN',
379
+ 'EVENT TRIGGER',
380
+ 'EXTENSION',
381
+ 'FOREIGN DATA WRAPPER',
382
+ 'FOREIGN TABLE',
383
+ 'FUNCTION',
384
+ 'GROUP',
385
+ 'INDEX',
386
+ 'LANGUAGE',
387
+ 'LARGE OBJECT',
388
+ 'MATERIALIZED VIEW',
389
+ 'OPERATOR',
390
+ 'POLICY',
391
+ 'PROCEDURE',
392
+ 'PUBLICATION',
393
+ 'ROLE',
394
+ 'RULE',
395
+ 'SCHEMA',
396
+ 'SEQUENCE',
397
+ 'SERVER',
398
+ 'STATISTICS',
399
+ 'SUBSCRIPTION',
400
+ 'SYSTEM',
401
+ 'TABLE',
402
+ 'TABLESPACE',
403
+ 'TEXT SEARCH',
404
+ 'TRIGGER',
405
+ 'TYPE',
406
+ 'USER',
407
+ 'VIEW',
408
+ ];
409
+ /** Sub-actions for ALTER TABLE. */
410
+ export const ALTER_TABLE_ACTIONS = [
411
+ 'ADD',
412
+ 'ALTER',
413
+ 'ATTACH PARTITION',
414
+ 'CLUSTER ON',
415
+ 'DETACH PARTITION',
416
+ 'DISABLE',
417
+ 'DROP',
418
+ 'ENABLE',
419
+ 'INHERIT',
420
+ 'NO INHERIT',
421
+ 'OF',
422
+ 'NOT OF',
423
+ 'OWNER TO',
424
+ 'RENAME',
425
+ 'REPLICA IDENTITY',
426
+ 'RESET',
427
+ 'SET',
428
+ 'VALIDATE CONSTRAINT',
429
+ ];
430
+ /** Continuation after `ALTER TABLE x ADD`. */
431
+ export const ALTER_TABLE_ADD = [
432
+ 'COLUMN',
433
+ 'CONSTRAINT',
434
+ 'CHECK',
435
+ 'FOREIGN KEY',
436
+ 'PRIMARY KEY',
437
+ 'UNIQUE',
438
+ 'EXCLUDE',
439
+ ];
440
+ /** Continuation after `ALTER TABLE x ALTER [COLUMN] y`. */
441
+ export const ALTER_TABLE_ALTER_COLUMN = [
442
+ 'ADD GENERATED',
443
+ 'DROP DEFAULT',
444
+ 'DROP EXPRESSION',
445
+ 'DROP IDENTITY',
446
+ 'DROP NOT NULL',
447
+ 'RESET',
448
+ 'RESTART',
449
+ 'SET',
450
+ 'SET DATA TYPE',
451
+ 'SET DEFAULT',
452
+ 'SET EXPRESSION',
453
+ 'SET GENERATED',
454
+ 'SET NOT NULL',
455
+ 'SET STATISTICS',
456
+ 'SET STORAGE',
457
+ 'TYPE',
458
+ ];
459
+ /** Continuation after `ALTER TABLE x DROP`. */
460
+ export const ALTER_TABLE_DROP = [
461
+ 'COLUMN',
462
+ 'CONSTRAINT',
463
+ 'IF EXISTS',
464
+ ];
465
+ /** Continuation after `ALTER TABLE x RENAME`. */
466
+ export const ALTER_TABLE_RENAME = [
467
+ 'COLUMN',
468
+ 'CONSTRAINT',
469
+ 'TO',
470
+ ];
471
+ /** Continuation after `ALTER TABLE x SET`. */
472
+ export const ALTER_TABLE_SET = [
473
+ '(',
474
+ 'LOGGED',
475
+ 'SCHEMA',
476
+ 'TABLESPACE',
477
+ 'UNLOGGED',
478
+ 'WITHOUT CLUSTER',
479
+ 'WITHOUT OIDS',
480
+ ];
481
+ /** Continuation after `ALTER TABLE x ENABLE`. */
482
+ export const ALTER_TABLE_ENABLE = [
483
+ 'ALWAYS',
484
+ 'REPLICA',
485
+ 'ROW LEVEL SECURITY',
486
+ 'RULE',
487
+ 'TRIGGER',
488
+ ];
489
+ /** Continuation after `ALTER TABLE x DISABLE`. */
490
+ export const ALTER_TABLE_DISABLE = [
491
+ 'ROW LEVEL SECURITY',
492
+ 'RULE',
493
+ 'TRIGGER',
494
+ ];
495
+ /** Continuation after `ALTER TABLE x REPLICA IDENTITY`. */
496
+ export const ALTER_TABLE_REPLICA_IDENTITY = [
497
+ 'DEFAULT',
498
+ 'FULL',
499
+ 'NOTHING',
500
+ 'USING INDEX',
501
+ ];
502
+ /** Sub-actions for ALTER VIEW. */
503
+ export const ALTER_VIEW_ACTIONS = [
504
+ 'ALTER',
505
+ 'OWNER TO',
506
+ 'RENAME',
507
+ 'RESET',
508
+ 'SET',
509
+ ];
510
+ /** Sub-actions for ALTER MATERIALIZED VIEW. */
511
+ export const ALTER_MATVIEW_ACTIONS = [
512
+ 'ALTER',
513
+ 'CLUSTER ON',
514
+ 'DEPENDS ON EXTENSION',
515
+ 'NO DEPENDS ON EXTENSION',
516
+ 'OWNER TO',
517
+ 'RENAME',
518
+ 'RESET',
519
+ 'SET',
520
+ ];
521
+ /** Sub-actions for ALTER INDEX. */
522
+ export const ALTER_INDEX_ACTIONS = [
523
+ 'ALTER COLUMN',
524
+ 'ATTACH PARTITION',
525
+ 'DEPENDS ON EXTENSION',
526
+ 'NO DEPENDS ON EXTENSION',
527
+ 'OWNER TO',
528
+ 'RENAME',
529
+ 'RESET',
530
+ 'SET',
531
+ ];
532
+ /** Sub-actions for ALTER SEQUENCE. */
533
+ export const ALTER_SEQUENCE_ACTIONS = [
534
+ 'AS',
535
+ 'CACHE',
536
+ 'CYCLE',
537
+ 'INCREMENT BY',
538
+ 'MAXVALUE',
539
+ 'MINVALUE',
540
+ 'NO CYCLE',
541
+ 'NO MAXVALUE',
542
+ 'NO MINVALUE',
543
+ 'OWNED BY',
544
+ 'OWNER TO',
545
+ 'RENAME TO',
546
+ 'RESTART',
547
+ 'SET SCHEMA',
548
+ 'START WITH',
549
+ ];
550
+ /** Sub-actions for ALTER FUNCTION / PROCEDURE / ROUTINE. */
551
+ export const ALTER_FUNCTION_ACTIONS = [
552
+ 'CALLED ON NULL INPUT',
553
+ 'COST',
554
+ 'DEPENDS ON EXTENSION',
555
+ 'IMMUTABLE',
556
+ 'LEAKPROOF',
557
+ 'NO DEPENDS ON EXTENSION',
558
+ 'NOT LEAKPROOF',
559
+ 'OWNER TO',
560
+ 'PARALLEL',
561
+ 'RENAME TO',
562
+ 'RESET',
563
+ 'RETURNS NULL ON NULL INPUT',
564
+ 'ROWS',
565
+ 'SECURITY DEFINER',
566
+ 'SECURITY INVOKER',
567
+ 'SET',
568
+ 'SET SCHEMA',
569
+ 'STABLE',
570
+ 'STRICT',
571
+ 'SUPPORT',
572
+ 'VOLATILE',
573
+ ];
574
+ /** Sub-actions for ALTER TYPE. */
575
+ export const ALTER_TYPE_ACTIONS = [
576
+ 'ADD ATTRIBUTE',
577
+ 'ADD VALUE',
578
+ 'ALTER ATTRIBUTE',
579
+ 'DROP ATTRIBUTE',
580
+ 'OWNER TO',
581
+ 'RENAME',
582
+ 'RENAME ATTRIBUTE',
583
+ 'RENAME VALUE',
584
+ 'SET SCHEMA',
585
+ 'SET',
586
+ ];
587
+ /** Sub-actions for ALTER ROLE / USER. */
588
+ export const ALTER_ROLE_ACTIONS = [
589
+ 'BYPASSRLS',
590
+ 'CONNECTION LIMIT',
591
+ 'CREATEDB',
592
+ 'CREATEROLE',
593
+ 'ENCRYPTED PASSWORD',
594
+ 'IN DATABASE',
595
+ 'INHERIT',
596
+ 'LOGIN',
597
+ 'NOBYPASSRLS',
598
+ 'NOCREATEDB',
599
+ 'NOCREATEROLE',
600
+ 'NOINHERIT',
601
+ 'NOLOGIN',
602
+ 'NOREPLICATION',
603
+ 'NOSUPERUSER',
604
+ 'PASSWORD',
605
+ 'RENAME TO',
606
+ 'REPLICATION',
607
+ 'RESET',
608
+ 'SET',
609
+ 'SUPERUSER',
610
+ 'VALID UNTIL',
611
+ 'WITH',
612
+ ];
613
+ /** Sub-actions for ALTER DATABASE. */
614
+ export const ALTER_DATABASE_ACTIONS = [
615
+ 'ALLOW_CONNECTIONS',
616
+ 'CONNECTION LIMIT',
617
+ 'IS_TEMPLATE',
618
+ 'OWNER TO',
619
+ 'REFRESH COLLATION VERSION',
620
+ 'RENAME TO',
621
+ 'RESET',
622
+ 'SET',
623
+ 'SET TABLESPACE',
624
+ 'WITH',
625
+ ];
626
+ /** Sub-actions for ALTER SCHEMA. */
627
+ export const ALTER_SCHEMA_ACTIONS = [
628
+ 'OWNER TO',
629
+ 'RENAME TO',
630
+ ];
631
+ /** Sub-actions for ALTER EXTENSION. */
632
+ export const ALTER_EXTENSION_ACTIONS = [
633
+ 'ADD',
634
+ 'DROP',
635
+ 'SET SCHEMA',
636
+ 'UPDATE',
637
+ ];
638
+ /** Sub-actions for ALTER POLICY. */
639
+ export const ALTER_POLICY_ACTIONS = ['ON', 'RENAME TO'];
640
+ /** Sub-actions for ALTER PUBLICATION. */
641
+ export const ALTER_PUBLICATION_ACTIONS = [
642
+ 'ADD',
643
+ 'DROP',
644
+ 'OWNER TO',
645
+ 'RENAME TO',
646
+ 'SET',
647
+ ];
648
+ /** Sub-actions for ALTER SUBSCRIPTION. */
649
+ export const ALTER_SUBSCRIPTION_ACTIONS = [
650
+ 'ADD PUBLICATION',
651
+ 'CONNECTION',
652
+ 'DISABLE',
653
+ 'DROP PUBLICATION',
654
+ 'ENABLE',
655
+ 'OWNER TO',
656
+ 'REFRESH PUBLICATION',
657
+ 'RENAME TO',
658
+ 'SET',
659
+ 'SET PUBLICATION',
660
+ 'SKIP',
661
+ ];
662
+ /** CREATE INDEX top-level options. */
663
+ export const CREATE_INDEX_OPTIONS = [
664
+ 'CONCURRENTLY',
665
+ 'IF NOT EXISTS',
666
+ 'ON',
667
+ 'UNIQUE',
668
+ ];
669
+ /** Window frame clauses after `OVER (`. */
670
+ export const WINDOW_FRAME_KEYWORDS = [
671
+ 'GROUPS',
672
+ 'ORDER BY',
673
+ 'PARTITION BY',
674
+ 'RANGE',
675
+ 'ROWS',
676
+ ];
677
+ /** Tail keywords that follow a `FROM <table>` clause in a query. */
678
+ export const POST_FROM_KEYWORDS = [
679
+ 'AS',
680
+ 'CROSS JOIN',
681
+ 'EXCEPT',
682
+ 'FETCH',
683
+ 'FOR',
684
+ 'FULL JOIN',
685
+ 'FULL OUTER JOIN',
686
+ 'GROUP BY',
687
+ 'HAVING',
688
+ 'INNER JOIN',
689
+ 'INTERSECT',
690
+ 'JOIN',
691
+ 'LATERAL',
692
+ 'LEFT JOIN',
693
+ 'LEFT OUTER JOIN',
694
+ 'LIMIT',
695
+ 'NATURAL JOIN',
696
+ 'OFFSET',
697
+ 'ON',
698
+ 'ORDER BY',
699
+ 'RIGHT JOIN',
700
+ 'RIGHT OUTER JOIN',
701
+ 'TABLESAMPLE',
702
+ 'UNION',
703
+ 'USING',
704
+ 'WHERE',
705
+ 'WINDOW',
706
+ ];
707
+ /** Continuations within a WHERE expression. */
708
+ export const WHERE_CONTINUATIONS = [
709
+ 'AND',
710
+ 'BETWEEN',
711
+ 'IN',
712
+ 'IS',
713
+ 'LIKE',
714
+ 'NOT',
715
+ 'OR',
716
+ ];
717
+ /** Boolean-style values used with `\set` for AUTOCOMMIT etc. (extends ON_OFF). */
718
+ export const DATESTYLE_VALUES = [
719
+ 'GERMAN',
720
+ 'ISO',
721
+ 'POSTGRES',
722
+ 'SQL',
723
+ ];
724
+ /** GRANT / REVOKE privileges. */
725
+ export const PRIVILEGE_KEYWORDS = [
726
+ 'ALL',
727
+ 'CREATE',
728
+ 'CONNECT',
729
+ 'DELETE',
730
+ 'EXECUTE',
731
+ 'INSERT',
732
+ 'REFERENCES',
733
+ 'SELECT',
734
+ 'TEMPORARY',
735
+ 'TRIGGER',
736
+ 'TRUNCATE',
737
+ 'UPDATE',
738
+ 'USAGE',
739
+ ];
740
+ /** Common transaction/savepoint keywords. */
741
+ export const TRANSACTION_KEYWORDS = [
742
+ 'ISOLATION LEVEL',
743
+ 'READ ONLY',
744
+ 'READ WRITE',
745
+ 'TRANSACTION',
746
+ ];
747
+ /**
748
+ * Built-in scalar type keywords that psql tab-completion mixes in
749
+ * wherever a type name is expected. Mirrors upstream's
750
+ * `Keywords_for_list_of_datatypes` (tab-complete.in.c). The multi-word
751
+ * names disabled under `#ifdef NOT_USED` upstream are intentionally
752
+ * omitted here too — tab completion can't disambiguate across word
753
+ * boundaries.
754
+ */
755
+ export const BUILTIN_DATATYPE_KEYWORDS = [
756
+ 'bigint',
757
+ 'boolean',
758
+ 'character',
759
+ 'double precision',
760
+ 'integer',
761
+ 'real',
762
+ 'smallint',
763
+ ];
764
+ /**
765
+ * COPY ... FROM ... WITH ( ... ) option keywords. Mirrors upstream's
766
+ * `Copy_from_options` macro = `Copy_common_options` + the FROM-specific
767
+ * extras (DEFAULT, FORCE_NOT_NULL, FORCE_NULL, FREEZE, LOG_VERBOSITY,
768
+ * ON_ERROR, REJECT_LIMIT).
769
+ */
770
+ export const COPY_FROM_OPTIONS = [
771
+ 'DELIMITER',
772
+ 'ENCODING',
773
+ 'ESCAPE',
774
+ 'FORMAT',
775
+ 'HEADER',
776
+ 'NULL',
777
+ 'QUOTE',
778
+ 'DEFAULT',
779
+ 'FORCE_NOT_NULL',
780
+ 'FORCE_NULL',
781
+ 'FREEZE',
782
+ 'LOG_VERBOSITY',
783
+ 'ON_ERROR',
784
+ 'REJECT_LIMIT',
785
+ ];
786
+ /**
787
+ * COPY ... TO ... WITH ( ... ) option keywords. Mirrors upstream's
788
+ * `Copy_to_options` macro = `Copy_common_options` + `FORCE_QUOTE`.
789
+ */
790
+ export const COPY_TO_OPTIONS = [
791
+ 'DELIMITER',
792
+ 'ENCODING',
793
+ 'ESCAPE',
794
+ 'FORMAT',
795
+ 'HEADER',
796
+ 'NULL',
797
+ 'QUOTE',
798
+ 'FORCE_QUOTE',
799
+ ];
800
+ /**
801
+ * Apply a case-insensitive prefix filter. Empty `prefix` returns the whole
802
+ * list. The match honours `compCase` — uppercase the candidates when the
803
+ * user is typing in uppercase, etc.
804
+ */
805
+ const filterAndCase = (candidates, prefix, settings) => {
806
+ const lowPrefix = prefix.toLowerCase();
807
+ const result = [];
808
+ for (const c of candidates) {
809
+ if (c.toLowerCase().startsWith(lowPrefix)) {
810
+ result.push(applyCase(c, prefix, settings));
811
+ }
812
+ }
813
+ return result;
814
+ };
815
+ /**
816
+ * Render a candidate with the case psql would use, based on `COMP_KEYWORD_CASE`:
817
+ *
818
+ * - lower → always lowercase, regardless of input.
819
+ * - upper → always uppercase, regardless of input.
820
+ * - preserve-lower → lowercase by default; uppercase if the user typed
821
+ * a fragment containing an UPPERCASE letter.
822
+ * - preserve-upper → (default) uppercase by default; lowercase if the
823
+ * user typed a fragment containing a lowercase letter.
824
+ *
825
+ * Per psql docs: "preserve-upper, the default, returns the keyword in upper
826
+ * case unless the partial word entered is in lower case". The dichotomy is
827
+ * really "did the user type ANY lowercase letter" (for preserve-upper) and
828
+ * "did the user type ANY uppercase letter" (for preserve-lower) — matching
829
+ * upstream's `pg_str_endswith` / `pg_str_islower` heuristics.
830
+ */
831
+ const applyCase = (candidate, typed, settings) => {
832
+ // Identifiers (already quoted, starts with ", or already lowercase) are
833
+ // never re-cased.
834
+ if (candidate.startsWith('"') || candidate.startsWith("'"))
835
+ return candidate;
836
+ // Catalog query results are always quoted-or-lowercase by virtue of
837
+ // quote_ident; pass them through unchanged.
838
+ if (containsNonKeywordChar(candidate))
839
+ return candidate;
840
+ const mode = settings.compCase;
841
+ // Mirror upstream `pg_strdup_keyword_case` (tab-complete.c): the case
842
+ // decision keys off the FIRST character of the user's input, not the
843
+ // presence of any case anywhere. Empty input (`first` = 0) is treated as
844
+ // neither lowercase nor alpha, so:
845
+ // - preserve-upper → UPPERCASE (default mode; matches vanilla psql 18
846
+ // for the `set <name> <TAB><TAB>` → `TO` case, upstream test
847
+ // 010_tab_completion.pl line 366).
848
+ // - preserve-lower → lowercase.
849
+ // The same rule applies when the first char is a non-letter (digit, `_`,
850
+ // punctuation) — those preserve the mode's default direction.
851
+ const first = typed[0] ?? '';
852
+ const firstIsLower = /[a-z]/.test(first);
853
+ const firstIsAlpha = /[A-Za-z]/.test(first);
854
+ const lowerCaseIt = mode === 'lower' ||
855
+ ((mode === 'preserve-lower' || mode === 'preserve-upper') &&
856
+ firstIsLower) ||
857
+ (mode === 'preserve-lower' && !firstIsAlpha);
858
+ if (mode === 'upper')
859
+ return candidate.toUpperCase();
860
+ return lowerCaseIt ? candidate.toLowerCase() : candidate.toUpperCase();
861
+ };
862
+ const containsNonKeywordChar = (s) => /[^A-Za-z0-9 ]/.test(s);
863
+ /**
864
+ * Split a candidate like `pg_catalog.tab_` into [schema, prefix]. Returns
865
+ * `null` if there's no schema qualifier. `schemaWasQuoted` records whether
866
+ * the user wrote the schema in `"..."` form so the caller can decide whether
867
+ * to fold its case in the rendered output.
868
+ */
869
+ const splitSchemaPrefix = (word) => {
870
+ const dot = word.indexOf('.');
871
+ if (dot < 0)
872
+ return null;
873
+ // Reject if anything after the dot looks like another dot (we only handle
874
+ // schema.relation, not catalog.schema.relation).
875
+ const after = word.slice(dot + 1);
876
+ if (after.includes('.'))
877
+ return null;
878
+ // Strip optional quoting on the schema.
879
+ let schema = word.slice(0, dot);
880
+ let schemaWasQuoted = false;
881
+ if (schema.startsWith('"') && schema.endsWith('"')) {
882
+ schemaWasQuoted = true;
883
+ schema = schema.slice(1, -1).replace(/""/g, '"');
884
+ }
885
+ return { schema, prefix: after, schemaWasQuoted };
886
+ };
887
+ /**
888
+ * Parse a table reference (the `<ref>` slot in
889
+ * `ALTER TABLE <ref> DROP CONSTRAINT y`) from the already-tokenized
890
+ * `prevWords` slice between `ALTER TABLE` and the action keyword.
891
+ *
892
+ * The scanner can produce the reference as 1 or 2 tokens depending on
893
+ * quoting:
894
+ *
895
+ * - `tab1` → `["tab1"]` (bare, case-folded)
896
+ * - `"tab1"` → `["\"tab1\""]` (quoted, exact-case)
897
+ * - `public.tab1` → `["public.tab1"]` (single token, dotted)
898
+ * - `public."tab1"` → `["public.\"tab1\""]` (single token, dotted+quoted)
899
+ *
900
+ * Returns the parsed parts with case-folding applied to UNQUOTED
901
+ * identifiers (matching `pg_strcasecmp` semantics) so the caller can
902
+ * pass them straight to a `WHERE relname = $N` catalog query.
903
+ *
904
+ * Returns `null` when the tokens don't look like a valid reference.
905
+ */
906
+ const parseTableRef = (refTokens) => {
907
+ const stripQuote = (s) => {
908
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
909
+ return { v: s.slice(1, -1).replace(/""/g, '"'), quoted: true };
910
+ }
911
+ return { v: s, quoted: false };
912
+ };
913
+ // Two-token form: `public.` + `"tab1"` (our scanner ends the first
914
+ // token on the dot when the relation half is quoted).
915
+ if (refTokens.length === 2) {
916
+ const first = refTokens[0];
917
+ if (!first.endsWith('.'))
918
+ return null;
919
+ const s = stripQuote(first.slice(0, -1));
920
+ const t = stripQuote(refTokens[1]);
921
+ if (t.v.length === 0)
922
+ return null;
923
+ return {
924
+ schema: s.quoted ? s.v : s.v.toLowerCase(),
925
+ table: t.quoted ? t.v : t.v.toLowerCase(),
926
+ };
927
+ }
928
+ if (refTokens.length !== 1)
929
+ return null;
930
+ const tok = refTokens[0];
931
+ // Single-token form. Schema-qualified? Find the FIRST dot that isn't
932
+ // inside `"..."`.
933
+ let inQuote = false;
934
+ let dot = -1;
935
+ for (let i = 0; i < tok.length; i++) {
936
+ const ch = tok[i];
937
+ if (ch === '"') {
938
+ inQuote = !inQuote;
939
+ }
940
+ else if (ch === '.' && !inQuote) {
941
+ dot = i;
942
+ break;
943
+ }
944
+ }
945
+ if (dot >= 0) {
946
+ const s = stripQuote(tok.slice(0, dot));
947
+ const t = stripQuote(tok.slice(dot + 1));
948
+ if (t.v.length === 0)
949
+ return null;
950
+ return {
951
+ schema: s.quoted ? s.v : s.v.toLowerCase(),
952
+ table: t.quoted ? t.v : t.v.toLowerCase(),
953
+ };
954
+ }
955
+ const t = stripQuote(tok);
956
+ if (t.v.length === 0)
957
+ return null;
958
+ return { schema: null, table: t.quoted ? t.v : t.v.toLowerCase() };
959
+ };
960
+ // ---------------------------------------------------------------------------
961
+ // Entry point.
962
+ // ---------------------------------------------------------------------------
963
+ /**
964
+ * Top-level rule dispatch.
965
+ *
966
+ * `prevWords` are the tokens BEFORE the current (in-progress) word; the
967
+ * current word's leading characters are what we're filtering candidates
968
+ * against.
969
+ */
970
+ export const findCompletions = async (prevWords, currentWord, ctx) => {
971
+ // Re-read the connection on every call so `\c` is picked up immediately.
972
+ const conn = ctx.settings.db ?? ctx.conn ?? null;
973
+ // ----- Variable expansion (`:NAME`, `:'NAME'`, `:"NAME"`, `:{?NAME}`)
974
+ // takes priority over anything else. The interpolation forms are valid
975
+ // both inside SQL and inside backslash-command args (`\echo :VERB`),
976
+ // so this branch fires regardless of `prevWords`.
977
+ if (currentWord.startsWith(':') && !currentWord.startsWith('::')) {
978
+ const names = listVarNames(ctx.settings);
979
+ const lc = (s) => s.toLowerCase();
980
+ // `:{?NAME}` — test-form (psqlscan_test_variable upstream). The
981
+ // candidate must close the `}` so the user's literal `:{?VERB`
982
+ // expands to `:{?VERBOSITY}` in one Tab.
983
+ if (currentWord.startsWith(':{?')) {
984
+ const prefix = currentWord.slice(3);
985
+ const lp = lc(prefix);
986
+ const cands = names
987
+ .filter((n) => lc(n).startsWith(lp))
988
+ .map((n) => ':{?' + n + '}');
989
+ return { candidates: cands };
990
+ }
991
+ // `:'NAME'` / `:"NAME"` — quoted-substitution forms (psqlscan emits
992
+ // a quoted literal / identifier). Close the matching quote so the
993
+ // unique-match path appends a trailing space cleanly.
994
+ if (currentWord.startsWith(":'")) {
995
+ const prefix = currentWord.slice(2);
996
+ const lp = lc(prefix);
997
+ const cands = names
998
+ .filter((n) => lc(n).startsWith(lp))
999
+ .map((n) => ":'" + n + "'");
1000
+ return { candidates: cands };
1001
+ }
1002
+ if (currentWord.startsWith(':"')) {
1003
+ const prefix = currentWord.slice(2);
1004
+ const lp = lc(prefix);
1005
+ const cands = names
1006
+ .filter((n) => lc(n).startsWith(lp))
1007
+ .map((n) => ':"' + n + '"');
1008
+ return { candidates: cands };
1009
+ }
1010
+ // Plain `:NAME` — bare substitution.
1011
+ const prefix = currentWord.slice(1);
1012
+ const lp = lc(prefix);
1013
+ const filt = names.filter((n) => lc(n).startsWith(lp)).map((n) => ':' + n);
1014
+ return { candidates: filt };
1015
+ }
1016
+ // ----- Backslash-command name completion.
1017
+ // Trigger: the user is mid-token and the token starts with '\'.
1018
+ if (currentWord.startsWith('\\') && prevWords.length === 0) {
1019
+ return {
1020
+ candidates: BACKSLASH_COMMANDS.filter((c) => c.toLowerCase().startsWith(currentWord.toLowerCase())),
1021
+ };
1022
+ }
1023
+ // ----- Backslash-command argument completion.
1024
+ if (prevWords.length > 0 && prevWords[0].startsWith('\\')) {
1025
+ return await backslashArgRules(prevWords, currentWord, ctx, conn);
1026
+ }
1027
+ // ----- Multi-line SQL: rules that need to see across the line boundary
1028
+ // re-tokenize `queryBuf` and consult the COMBINED token sequence. The
1029
+ // standard single-line `prevWords` view stays untouched so the bulk of
1030
+ // the rule grammar (which is happy to match on the current line alone)
1031
+ // isn't disturbed.
1032
+ //
1033
+ // Upstream's `get_previous_words` pastes `tab_completion_query_buf` in
1034
+ // front of `rl_line_buffer` with a `\n` separator and tokenizes the
1035
+ // whole thing — see tab-complete.in.c ~line 6670. We mirror that, but
1036
+ // gate the cross-line view to specific rules so the existing tail-match
1037
+ // arms keep their familiar (line-local) semantics.
1038
+ const combined = combinedPrevWords(ctx.queryBuf, prevWords);
1039
+ const multiLine = await multiLineSqlRules(combined, prevWords, currentWord, ctx, conn);
1040
+ if (multiLine !== null)
1041
+ return multiLine;
1042
+ // ----- SQL: top-of-statement keyword completion.
1043
+ if (prevWords.length === 0) {
1044
+ return {
1045
+ candidates: filterAndCase(SQL_TOP_KEYWORDS, currentWord, ctx.settings),
1046
+ };
1047
+ }
1048
+ return await sqlRules(prevWords, currentWord, ctx, conn);
1049
+ };
1050
+ /**
1051
+ * Tokenize `queryBuf` and concatenate with the current line's `prevWords`.
1052
+ * The result matches what upstream's `get_previous_words` produces for a
1053
+ * multi-line statement: tokens from prior lines first, then tokens from the
1054
+ * current line up to the cursor.
1055
+ *
1056
+ * Tokens cross the line boundary harmlessly because our tokenizer treats
1057
+ * `\n` as whitespace (a separator). Newlines INSIDE a `'...'`/`"..."`
1058
+ * literal are absorbed by the quoted-string handling and don't split the
1059
+ * literal into two tokens.
1060
+ */
1061
+ const combinedPrevWords = (queryBuf, prevWords) => {
1062
+ if (queryBuf === undefined || queryBuf.length === 0)
1063
+ return prevWords;
1064
+ const bufTokens = tokenize(queryBuf).map((t) => t.text);
1065
+ return [...bufTokens, ...prevWords];
1066
+ };
1067
+ /**
1068
+ * Rules that fire only when the user is in the middle of a multi-line
1069
+ * statement (`queryBuf` non-empty / the combined-token view is longer than
1070
+ * the current-line view). Today this covers:
1071
+ *
1072
+ * - `ANALYZE (` opened on a previous line — emit the option list.
1073
+ * - `COMMENT ON CONSTRAINT <name> ON <schema>.` continuation —
1074
+ * resolve to the table holding that constraint in `<schema>`.
1075
+ *
1076
+ * Returns the rule's `RuleResult` when an arm matches, or `null` when the
1077
+ * combined-token view doesn't add anything (so the caller continues with
1078
+ * the line-local rule grammar).
1079
+ */
1080
+ const multiLineSqlRules = async (combined, prevWords, currentWord, ctx, conn) => {
1081
+ // Skip if there's no cross-line context to add.
1082
+ if (combined.length === prevWords.length)
1083
+ return null;
1084
+ // ----- ANALYZE ( <prefix> — option list inside the parenthesized form.
1085
+ // Mirrors upstream:
1086
+ //
1087
+ // else if (HeadMatches("ANALYZE", "(*") &&
1088
+ // !HeadMatches("ANALYZE", "(*)"))
1089
+ // {
1090
+ // if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
1091
+ // COMPLETE_WITH("VERBOSE", "SKIP_LOCKED", "BUFFER_USAGE_LIMIT");
1092
+ // else if (TailMatches("VERBOSE|SKIP_LOCKED"))
1093
+ // COMPLETE_WITH("ON", "OFF");
1094
+ // }
1095
+ //
1096
+ // Our scanner splits `(` and `,` as their own tokens (the C tokenizer
1097
+ // keeps `(verbose` as one word), so `ends_with(prev_wd, '(')` becomes
1098
+ // "previous token IS `(`" and similarly for `,`.
1099
+ if (HeadMatches(combined, ['ANALYZE']) &&
1100
+ isInsideOpenParen(combined.slice(1))) {
1101
+ const lastTok = combined[combined.length - 1];
1102
+ if (lastTok === '(' || lastTok === ',') {
1103
+ return {
1104
+ candidates: filterAndCase(['VERBOSE', 'SKIP_LOCKED', 'BUFFER_USAGE_LIMIT'], currentWord, ctx.settings),
1105
+ };
1106
+ }
1107
+ // Inside the option list with a partial option name in the current
1108
+ // word — filter the option list by the prefix.
1109
+ if (currentWord.length > 0 &&
1110
+ (lastTok === undefined || /^[A-Za-z_]+$/.test(currentWord))) {
1111
+ return {
1112
+ candidates: filterAndCase(['VERBOSE', 'SKIP_LOCKED', 'BUFFER_USAGE_LIMIT'], currentWord, ctx.settings),
1113
+ };
1114
+ }
1115
+ // `VERBOSE` / `SKIP_LOCKED` boolean continuation — ON / OFF.
1116
+ if (TailMatches(combined, ['VERBOSE|SKIP_LOCKED'])) {
1117
+ return {
1118
+ candidates: filterAndCase(['ON', 'OFF'], currentWord, ctx.settings),
1119
+ };
1120
+ }
1121
+ }
1122
+ // ----- COMMENT ON CONSTRAINT <name> ON <schema>.<prefix> — resolve to the
1123
+ // table that has `<name>` as a constraint within `<schema>`. Upstream
1124
+ // calls `set_completion_reference(prev2_wd)` (the constraint name) and
1125
+ // runs `Query_for_list_of_tables_for_constraint` with the schema match
1126
+ // baked into the SchemaQuery shape (tab-complete.in.c ~line 3204).
1127
+ //
1128
+ // We allow the rule to fire even when the line-local `prevWords` only
1129
+ // sees the trailing `ON` — the COMBINED tokens carry the
1130
+ // `COMMENT ON CONSTRAINT <name>` prefix from the previous line.
1131
+ if (TailMatches(combined, ['COMMENT', 'ON', 'CONSTRAINT', MatchAny, 'ON'])) {
1132
+ if (!conn)
1133
+ return { candidates: [] };
1134
+ // `combined` ends in `ON`. The constraint name is the token just
1135
+ // before that trailing `ON`. Indexing from the right avoids hard-coding
1136
+ // the queryBuf token count.
1137
+ const constraintNameRaw = combined[combined.length - 2];
1138
+ const constraintName = stripIdentifierQuote(constraintNameRaw);
1139
+ const split = splitSchemaPrefix(currentWord);
1140
+ if (split) {
1141
+ // Schema-qualified: emit tables in `<schema>` that have the named
1142
+ // constraint, with the canonical schema prefix re-attached so the
1143
+ // line ends up looking like `public.tab1`.
1144
+ const rows = await runCatalogQuery(conn, Query_for_list_of_tables_for_constraint_in_schema, split.prefix, [constraintName, split.schema]);
1145
+ const canonicalSchema = split.schemaWasQuoted
1146
+ ? split.schema
1147
+ : split.schema.toLowerCase();
1148
+ return {
1149
+ candidates: rows.map((r) => canonicalSchema + '.' + r),
1150
+ };
1151
+ }
1152
+ // Unqualified: list tables that have the constraint, plus the schemas
1153
+ // they live in (with trailing `.` so the user can drill into a schema).
1154
+ const [tables, schemas] = await Promise.all([
1155
+ runCatalogQuery(conn, Query_for_list_of_tables_for_constraint, currentWord, [constraintName]),
1156
+ runCatalogQuery(conn, Query_for_list_of_schemas, currentWord),
1157
+ ]);
1158
+ return { candidates: [...tables, ...schemas.map((s) => s + '.')] };
1159
+ }
1160
+ void conn;
1161
+ return null;
1162
+ };
1163
+ /**
1164
+ * Return true when the tokens (after dropping a leading `ANALYZE` keyword)
1165
+ * are inside an unclosed parenthesized form — i.e. there's a `(` somewhere
1166
+ * and no matching `)` at the trailing position. Mirrors upstream's
1167
+ * `HeadMatches("ANALYZE", "(*") && !HeadMatches("ANALYZE", "(*)")`
1168
+ * pattern under our split-punctuation tokenizer.
1169
+ */
1170
+ const isInsideOpenParen = (tokens) => {
1171
+ let depth = 0;
1172
+ let sawOpen = false;
1173
+ for (const t of tokens) {
1174
+ if (t === '(') {
1175
+ depth++;
1176
+ sawOpen = true;
1177
+ }
1178
+ else if (t === ')') {
1179
+ depth--;
1180
+ }
1181
+ }
1182
+ return sawOpen && depth > 0;
1183
+ };
1184
+ /** Strip surrounding `"..."` quoting from a constraint/identifier token. */
1185
+ const stripIdentifierQuote = (raw) => {
1186
+ if (raw === undefined)
1187
+ return '';
1188
+ if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
1189
+ return raw.slice(1, -1).replace(/""/g, '"');
1190
+ }
1191
+ return raw;
1192
+ };
1193
+ // ---------------------------------------------------------------------------
1194
+ // Backslash arg rules.
1195
+ // ---------------------------------------------------------------------------
1196
+ const backslashArgRules = async (prevWords, currentWord, ctx, conn) => {
1197
+ const cmd = prevWords[0]; // e.g. '\dt'
1198
+ // \c [DBNAME], \connect [DBNAME]: complete database names.
1199
+ if (cmd === '\\c' || cmd === '\\connect') {
1200
+ if (prevWords.length === 1 && conn) {
1201
+ const rows = await runCatalogQuery(conn, Query_for_list_of_databases, currentWord);
1202
+ return { candidates: rows };
1203
+ }
1204
+ return { candidates: [] };
1205
+ }
1206
+ // \dn[+] [schema]
1207
+ if (cmd === '\\dn' || cmd === '\\dn+') {
1208
+ if (prevWords.length === 1 && conn) {
1209
+ const rows = await runCatalogQuery(conn, Query_for_list_of_schemas, currentWord);
1210
+ return { candidates: rows };
1211
+ }
1212
+ return { candidates: [] };
1213
+ }
1214
+ // \df, \dfa, \dfn, \dfp, \dft, \dfw, \ef, \sf
1215
+ if (cmd === '\\df' ||
1216
+ cmd === '\\df+' ||
1217
+ cmd === '\\dfa' ||
1218
+ cmd === '\\dfn' ||
1219
+ cmd === '\\dfp' ||
1220
+ cmd === '\\dft' ||
1221
+ cmd === '\\dfw' ||
1222
+ cmd === '\\ef' ||
1223
+ cmd === '\\sf' ||
1224
+ cmd === '\\sf+') {
1225
+ if (prevWords.length === 1 && conn) {
1226
+ const rows = await runCatalogQuery(conn, Query_for_list_of_functions, currentWord);
1227
+ return { candidates: rows };
1228
+ }
1229
+ return { candidates: [] };
1230
+ }
1231
+ // \du, \dg → roles.
1232
+ if (cmd === '\\du' || cmd === '\\dg' || cmd === '\\du+' || cmd === '\\dg+') {
1233
+ if (prevWords.length === 1 && conn) {
1234
+ const rows = await runCatalogQuery(conn, Query_for_list_of_roles, currentWord);
1235
+ return { candidates: rows };
1236
+ }
1237
+ return { candidates: [] };
1238
+ }
1239
+ // \dx → extensions.
1240
+ if (cmd === '\\dx' || cmd === '\\dx+') {
1241
+ if (prevWords.length === 1 && conn) {
1242
+ const rows = await runCatalogQuery(conn, Query_for_list_of_extensions, currentWord);
1243
+ return { candidates: rows };
1244
+ }
1245
+ return { candidates: [] };
1246
+ }
1247
+ // \dL → languages.
1248
+ if (cmd === '\\dL' || cmd === '\\dL+') {
1249
+ if (prevWords.length === 1 && conn) {
1250
+ const rows = await runCatalogQuery(conn, Query_for_list_of_languages, currentWord);
1251
+ return { candidates: rows };
1252
+ }
1253
+ return { candidates: [] };
1254
+ }
1255
+ // \dT → types.
1256
+ if (cmd === '\\dT' || cmd === '\\dT+') {
1257
+ if (prevWords.length === 1 && conn) {
1258
+ const rows = await runCatalogQuery(conn, Query_for_list_of_types, currentWord);
1259
+ return { candidates: rows };
1260
+ }
1261
+ return { candidates: [] };
1262
+ }
1263
+ // \do → operators.
1264
+ if (cmd === '\\do' ||
1265
+ cmd === '\\do+' ||
1266
+ cmd === '\\doS' ||
1267
+ cmd === '\\doS+') {
1268
+ if (prevWords.length === 1 && conn) {
1269
+ const rows = await runCatalogQuery(conn, Query_for_list_of_operators, currentWord);
1270
+ return { candidates: rows };
1271
+ }
1272
+ return { candidates: [] };
1273
+ }
1274
+ // \dC → casts (free-form pattern of "src AS tgt").
1275
+ if (cmd === '\\dC' || cmd === '\\dC+') {
1276
+ if (prevWords.length === 1 && conn) {
1277
+ const rows = await runCatalogQuery(conn, Query_for_list_of_casts, currentWord);
1278
+ return { candidates: rows };
1279
+ }
1280
+ return { candidates: [] };
1281
+ }
1282
+ // \dt / \dtv / \d / \dv / \dm / \di / \ds → relations of various kinds.
1283
+ if (cmd === '\\dt' ||
1284
+ cmd === '\\dt+' ||
1285
+ cmd === '\\dtv' ||
1286
+ cmd === '\\d' ||
1287
+ cmd === '\\d+' ||
1288
+ cmd === '\\dE' ||
1289
+ cmd === '\\dE+') {
1290
+ if (prevWords.length === 1 && conn) {
1291
+ return {
1292
+ candidates: await completeSchemaOrRelations(conn, currentWord, Query_for_list_of_tables),
1293
+ };
1294
+ }
1295
+ return { candidates: [] };
1296
+ }
1297
+ if (cmd === '\\dv' || cmd === '\\dv+') {
1298
+ if (prevWords.length === 1 && conn) {
1299
+ return {
1300
+ candidates: await completeSchemaOrRelations(conn, currentWord, Query_for_list_of_views),
1301
+ };
1302
+ }
1303
+ return { candidates: [] };
1304
+ }
1305
+ if (cmd === '\\dm' || cmd === '\\dm+') {
1306
+ if (prevWords.length === 1 && conn) {
1307
+ return {
1308
+ candidates: await completeSchemaOrRelations(conn, currentWord, Query_for_list_of_matviews),
1309
+ };
1310
+ }
1311
+ return { candidates: [] };
1312
+ }
1313
+ if (cmd === '\\di' || cmd === '\\di+') {
1314
+ if (prevWords.length === 1 && conn) {
1315
+ return {
1316
+ candidates: await completeSchemaOrRelations(conn, currentWord, Query_for_list_of_indexes),
1317
+ };
1318
+ }
1319
+ return { candidates: [] };
1320
+ }
1321
+ if (cmd === '\\ds' || cmd === '\\ds+') {
1322
+ if (prevWords.length === 1 && conn) {
1323
+ return {
1324
+ candidates: await completeSchemaOrRelations(conn, currentWord, Query_for_list_of_sequences),
1325
+ };
1326
+ }
1327
+ return { candidates: [] };
1328
+ }
1329
+ // \l[+] / \list → databases.
1330
+ if (cmd === '\\l' ||
1331
+ cmd === '\\l+' ||
1332
+ cmd === '\\list' ||
1333
+ cmd === '\\list+') {
1334
+ if (prevWords.length === 1 && conn) {
1335
+ const rows = await runCatalogQuery(conn, Query_for_list_of_databases, currentWord);
1336
+ return { candidates: rows };
1337
+ }
1338
+ return { candidates: [] };
1339
+ }
1340
+ // \encoding NAME
1341
+ if (cmd === '\\encoding') {
1342
+ if (prevWords.length === 1) {
1343
+ return { candidates: filterCi(ENCODINGS, currentWord) };
1344
+ }
1345
+ return { candidates: [] };
1346
+ }
1347
+ // \pset OPT [value]
1348
+ if (cmd === '\\pset') {
1349
+ if (prevWords.length === 1) {
1350
+ return { candidates: filterCi(PSET_OPTIONS, currentWord) };
1351
+ }
1352
+ if (prevWords.length === 2) {
1353
+ const values = psetValuesFor(prevWords[1].toLowerCase());
1354
+ if (values)
1355
+ return { candidates: filterCi(values, currentWord) };
1356
+ return { candidates: [] };
1357
+ }
1358
+ return { candidates: [] };
1359
+ }
1360
+ // \set NAME [VALUE]
1361
+ if (cmd === '\\set') {
1362
+ if (prevWords.length === 1) {
1363
+ return {
1364
+ candidates: filterCi(listAllVarNames(ctx.settings), currentWord),
1365
+ };
1366
+ }
1367
+ if (prevWords.length === 2) {
1368
+ const values = variableValuesFor(prevWords[1].toUpperCase());
1369
+ if (values)
1370
+ return { candidates: filterCi(values, currentWord) };
1371
+ return { candidates: [] };
1372
+ }
1373
+ return { candidates: [] };
1374
+ }
1375
+ // \unset NAME — only existing variables.
1376
+ if (cmd === '\\unset') {
1377
+ if (prevWords.length === 1) {
1378
+ return { candidates: filterCi(listVarNames(ctx.settings), currentWord) };
1379
+ }
1380
+ return { candidates: [] };
1381
+ }
1382
+ // \echo / \warn / \qecho — variable expansion only (handled above).
1383
+ // \prompt — variable name argument is the 1st positional.
1384
+ if (cmd === '\\prompt') {
1385
+ if (prevWords.length === 2) {
1386
+ return {
1387
+ candidates: filterCi(listAllVarNames(ctx.settings), currentWord),
1388
+ };
1389
+ }
1390
+ return { candidates: [] };
1391
+ }
1392
+ // \lo_import / \lo_export → filesystem-driven filename completion.
1393
+ // psql parses the path as a backslash-argument literal — no SQL string
1394
+ // quoting required, so we use the unquoted candidate form.
1395
+ if (cmd === '\\lo_import' || cmd === '\\lo_export') {
1396
+ if (prevWords.length === 1) {
1397
+ return { candidates: completeFilenames(currentWord, 'none') };
1398
+ }
1399
+ return { candidates: [] };
1400
+ }
1401
+ // \copy <table> [FROM|TO] <path> — once the FROM/TO keyword has been
1402
+ // typed, the next token is a filename (backslash-context, so bare paths
1403
+ // are fine).
1404
+ if (cmd === '\\copy') {
1405
+ // First arg: a table name.
1406
+ if (prevWords.length === 1 && conn) {
1407
+ return {
1408
+ candidates: await completeSchemaOrRelations(conn, currentWord, Query_for_list_of_tables),
1409
+ };
1410
+ }
1411
+ // Second arg: the FROM/TO keyword.
1412
+ if (prevWords.length === 2) {
1413
+ return {
1414
+ candidates: filterAndCase(['FROM', 'TO'], currentWord, ctx.settings),
1415
+ };
1416
+ }
1417
+ // Third arg (after FROM/TO): the filename.
1418
+ if (prevWords.length >= 3) {
1419
+ const last = prevWords[prevWords.length - 1].toUpperCase();
1420
+ if (last === 'FROM' || last === 'TO') {
1421
+ return { candidates: completeFilenames(currentWord, 'none') };
1422
+ }
1423
+ }
1424
+ return { candidates: [] };
1425
+ }
1426
+ // \drds, \drg → roles (for the first arg).
1427
+ if (cmd === '\\drds' || cmd === '\\drg') {
1428
+ if (prevWords.length === 1 && conn) {
1429
+ const rows = await runCatalogQuery(conn, Query_for_list_of_roles, currentWord);
1430
+ return { candidates: rows };
1431
+ }
1432
+ return { candidates: [] };
1433
+ }
1434
+ // Anything else: no completion.
1435
+ return { candidates: [] };
1436
+ };
1437
+ // ---------------------------------------------------------------------------
1438
+ // SQL rules.
1439
+ // ---------------------------------------------------------------------------
1440
+ const sqlRules = async (prevWords, currentWord, ctx, conn) => {
1441
+ // Convenience: most rules want to fall back to a "tables" lookup. We
1442
+ // factor that into a single helper.
1443
+ const completeTables = async (query = Query_for_list_of_tables) => {
1444
+ if (!conn)
1445
+ return { candidates: [] };
1446
+ return {
1447
+ candidates: await completeSchemaOrRelations(conn, currentWord, query),
1448
+ };
1449
+ };
1450
+ // COPY <table> [FROM|TO] <'path'> — filename completion in the
1451
+ // string-literal context. MUST come before the generic `FROM <prefix>`
1452
+ // rule below, which would otherwise treat the path as a table name.
1453
+ if (isCopyFromOrTo(prevWords)) {
1454
+ return { candidates: completeFilenames(currentWord, 'sql') };
1455
+ }
1456
+ // FROM <prefix>: tables/views/matviews. EXCLUDE `REVOKE … FROM <role>`,
1457
+ // which must complete role names — otherwise this generic rule shadows the
1458
+ // REVOKE-roles arm below, making it dead code (review item #26).
1459
+ if (TailMatches(prevWords, ['FROM']) && !HeadMatches(prevWords, ['REVOKE'])) {
1460
+ return completeTables(Query_for_list_of_tables_views);
1461
+ }
1462
+ // After FROM x, suggest JOIN/WHERE/etc — handled below.
1463
+ // UPDATE <prefix>: tables.
1464
+ if (TailMatches(prevWords, ['UPDATE']))
1465
+ return completeTables();
1466
+ // DELETE FROM <prefix>: tables.
1467
+ if (TailMatches(prevWords, ['DELETE', 'FROM']))
1468
+ return completeTables();
1469
+ // INSERT INTO <prefix>: tables. The tokenizer might give us
1470
+ // ['INSERT'] [INTO] or ['INSERT', 'INTO'] depending on whether the user
1471
+ // typed an extra space.
1472
+ if (TailMatches(prevWords, ['INTO']))
1473
+ return completeTables();
1474
+ // JOIN <prefix>: tables. Most common: SELECT … FROM x JOIN <prefix>.
1475
+ if (TailMatches(prevWords, ['JOIN'])) {
1476
+ return completeTables(Query_for_list_of_tables_views);
1477
+ }
1478
+ // After JOIN x, suggest ON or USING.
1479
+ if (TailMatches(prevWords, ['JOIN', MatchAny])) {
1480
+ return {
1481
+ candidates: filterAndCase(['ON', 'USING'], currentWord, ctx.settings),
1482
+ };
1483
+ }
1484
+ // After `FROM table_name <TAB>` (or `FROM table AS alias <TAB>`) — offer
1485
+ // the post-FROM tail keywords. Only fire when the statement starts with
1486
+ // SELECT/INSERT/UPDATE/DELETE (i.e. a SELECT-list-friendly context), so
1487
+ // we don't trample more specific rules like `ALTER TABLE x` or
1488
+ // `INSERT INTO x` which look "FROM-like" structurally.
1489
+ const inSelectContext = HeadMatches(prevWords, ['SELECT']) ||
1490
+ HeadMatches(prevWords, ['INSERT']) ||
1491
+ HeadMatches(prevWords, ['UPDATE']) ||
1492
+ HeadMatches(prevWords, ['DELETE']) ||
1493
+ HeadMatches(prevWords, ['WITH']) ||
1494
+ HeadMatches(prevWords, ['EXPLAIN']);
1495
+ if (inSelectContext &&
1496
+ (TailMatches(prevWords, ['FROM', MatchAny]) ||
1497
+ TailMatches(prevWords, ['FROM', MatchAny, MatchAny]))) {
1498
+ // `FROM x` or `FROM x alias` → post-FROM continuations.
1499
+ return {
1500
+ candidates: filterAndCase(POST_FROM_KEYWORDS, currentWord, ctx.settings),
1501
+ };
1502
+ }
1503
+ // Window-function frame clause: `OVER (` then `<TAB>`. The tokenizer
1504
+ // emits the `(` as its own token, so prevWords ends with '('.
1505
+ if (TailMatches(prevWords, ['OVER', '('])) {
1506
+ return {
1507
+ candidates: filterAndCase(WINDOW_FRAME_KEYWORDS, currentWord, ctx.settings),
1508
+ };
1509
+ }
1510
+ // `OVER <name>` (named window reference) is free-form; no completion.
1511
+ // After a WHERE clause has been started (any `WHERE … <expr> <TAB>`),
1512
+ // suggest boolean continuations. We trigger on the simple case
1513
+ // `WHERE <ident>` and `WHERE … AND/OR <ident>`.
1514
+ if (inSelectContext &&
1515
+ TailMatches(prevWords, ['WHERE', MatchAny]) &&
1516
+ !TailMatches(prevWords, ['WHERE'])) {
1517
+ return {
1518
+ candidates: filterAndCase(WHERE_CONTINUATIONS, currentWord, ctx.settings),
1519
+ };
1520
+ }
1521
+ // ---- ALTER TABLE block ----
1522
+ // Deep ALTER TABLE sub-action continuations must be checked BEFORE the
1523
+ // 3-token fallback `ALTER TABLE x` (which lists generic sub-actions).
1524
+ // The deeper rules use HeadMatches so they survive an arbitrary trailing
1525
+ // option list like `ALTER TABLE foo ADD CONSTRAINT bar CHECK (...)`.
1526
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1527
+ TailMatches(prevWords, ['ADD'])) {
1528
+ return {
1529
+ candidates: filterAndCase(ALTER_TABLE_ADD, currentWord, ctx.settings),
1530
+ };
1531
+ }
1532
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1533
+ TailMatches(prevWords, ['DROP'])) {
1534
+ return {
1535
+ candidates: filterAndCase(ALTER_TABLE_DROP, currentWord, ctx.settings),
1536
+ };
1537
+ }
1538
+ // `ALTER TABLE <ref> DROP CONSTRAINT <prefix>` — constraint names on
1539
+ // the referenced table. Mirrors upstream tab-complete.in.c ~line 1280
1540
+ // (`COMPLETE_WITH_QUERY(Query_for_constraint_of_table)`).
1541
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1542
+ TailMatches(prevWords, ['DROP', 'CONSTRAINT'])) {
1543
+ if (!conn)
1544
+ return { candidates: [] };
1545
+ const refTokens = prevWords.slice(2, prevWords.length - 2);
1546
+ const ref = parseTableRef(refTokens);
1547
+ if (!ref)
1548
+ return { candidates: [] };
1549
+ const cands = ref.schema === null
1550
+ ? await runCatalogQuery(conn, Query_for_constraint_of_table, currentWord, [ref.table])
1551
+ : await runCatalogQuery(conn, Query_for_constraint_of_table_in_schema, currentWord, [ref.schema, ref.table]);
1552
+ return { candidates: cands };
1553
+ }
1554
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1555
+ TailMatches(prevWords, ['RENAME'])) {
1556
+ return {
1557
+ candidates: filterAndCase(ALTER_TABLE_RENAME, currentWord, ctx.settings),
1558
+ };
1559
+ }
1560
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1561
+ (TailMatches(prevWords, ['ALTER']) ||
1562
+ TailMatches(prevWords, ['ALTER', 'COLUMN', MatchAny]))) {
1563
+ return {
1564
+ candidates: filterAndCase(ALTER_TABLE_ALTER_COLUMN, currentWord, ctx.settings),
1565
+ };
1566
+ }
1567
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1568
+ TailMatches(prevWords, ['SET'])) {
1569
+ return {
1570
+ candidates: filterAndCase(ALTER_TABLE_SET, currentWord, ctx.settings),
1571
+ };
1572
+ }
1573
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1574
+ TailMatches(prevWords, ['ENABLE'])) {
1575
+ return {
1576
+ candidates: filterAndCase(ALTER_TABLE_ENABLE, currentWord, ctx.settings),
1577
+ };
1578
+ }
1579
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1580
+ TailMatches(prevWords, ['DISABLE'])) {
1581
+ return {
1582
+ candidates: filterAndCase(ALTER_TABLE_DISABLE, currentWord, ctx.settings),
1583
+ };
1584
+ }
1585
+ if (HeadMatches(prevWords, ['ALTER', 'TABLE']) &&
1586
+ TailMatches(prevWords, ['REPLICA', 'IDENTITY'])) {
1587
+ return {
1588
+ candidates: filterAndCase(ALTER_TABLE_REPLICA_IDENTITY, currentWord, ctx.settings),
1589
+ };
1590
+ }
1591
+ // ALTER TABLE x — sub-actions (must come AFTER the deep continuations above).
1592
+ if (TailMatches(prevWords, ['ALTER', 'TABLE', MatchAny])) {
1593
+ return {
1594
+ candidates: filterAndCase(ALTER_TABLE_ACTIONS, currentWord, ctx.settings),
1595
+ };
1596
+ }
1597
+ // ALTER TABLE — table name.
1598
+ if (TailMatches(prevWords, ['ALTER', 'TABLE']))
1599
+ return completeTables();
1600
+ // ---- ALTER VIEW / MATERIALIZED VIEW ----
1601
+ if (TailMatches(prevWords, ['ALTER', 'VIEW', MatchAny])) {
1602
+ return {
1603
+ candidates: filterAndCase(ALTER_VIEW_ACTIONS, currentWord, ctx.settings),
1604
+ };
1605
+ }
1606
+ if (TailMatches(prevWords, ['ALTER', 'VIEW'])) {
1607
+ return completeTables(Query_for_list_of_views);
1608
+ }
1609
+ if (TailMatches(prevWords, ['ALTER', 'MATERIALIZED', 'VIEW', MatchAny])) {
1610
+ return {
1611
+ candidates: filterAndCase(ALTER_MATVIEW_ACTIONS, currentWord, ctx.settings),
1612
+ };
1613
+ }
1614
+ if (TailMatches(prevWords, ['ALTER', 'MATERIALIZED', 'VIEW'])) {
1615
+ return completeTables(Query_for_list_of_matviews);
1616
+ }
1617
+ // ---- ALTER INDEX ----
1618
+ if (TailMatches(prevWords, ['ALTER', 'INDEX', MatchAny])) {
1619
+ return {
1620
+ candidates: filterAndCase(ALTER_INDEX_ACTIONS, currentWord, ctx.settings),
1621
+ };
1622
+ }
1623
+ if (TailMatches(prevWords, ['ALTER', 'INDEX'])) {
1624
+ return completeTables(Query_for_list_of_indexes);
1625
+ }
1626
+ // ---- ALTER SEQUENCE ----
1627
+ if (TailMatches(prevWords, ['ALTER', 'SEQUENCE', MatchAny])) {
1628
+ return {
1629
+ candidates: filterAndCase(ALTER_SEQUENCE_ACTIONS, currentWord, ctx.settings),
1630
+ };
1631
+ }
1632
+ if (TailMatches(prevWords, ['ALTER', 'SEQUENCE'])) {
1633
+ return completeTables(Query_for_list_of_sequences);
1634
+ }
1635
+ // ---- ALTER FUNCTION / PROCEDURE / ROUTINE ----
1636
+ if (TailMatches(prevWords, ['ALTER', 'FUNCTION|PROCEDURE|ROUTINE', MatchAny])) {
1637
+ return {
1638
+ candidates: filterAndCase(ALTER_FUNCTION_ACTIONS, currentWord, ctx.settings),
1639
+ };
1640
+ }
1641
+ if (TailMatches(prevWords, ['ALTER', 'FUNCTION|PROCEDURE|ROUTINE'])) {
1642
+ if (!conn)
1643
+ return { candidates: [] };
1644
+ return {
1645
+ candidates: await runCatalogQuery(conn, Query_for_list_of_functions, currentWord),
1646
+ };
1647
+ }
1648
+ // ---- ALTER TYPE ----
1649
+ // `ALTER TYPE <enum> RENAME VALUE 'X<TAB>` — enum labels of the named
1650
+ // type, wrapped in single quotes since the user is mid-string-literal.
1651
+ // Mirrors upstream tab-complete.in.c ~line 1480.
1652
+ if (TailMatches(prevWords, ['ALTER', 'TYPE', MatchAny, 'RENAME', 'VALUE']) &&
1653
+ currentWord.startsWith("'")) {
1654
+ if (!conn)
1655
+ return { candidates: [] };
1656
+ const typeName = prevWords[prevWords.length - 3].toLowerCase();
1657
+ // Strip the leading quote so the LIKE pattern matches `bar`/`BLACK`
1658
+ // (enumlabel column) rather than `'bar`/`'BLACK`.
1659
+ const labelPrefix = currentWord.slice(1);
1660
+ const cands = await runCatalogQuery(conn, Query_for_list_of_enum_values_quoted, labelPrefix, [typeName]);
1661
+ return { candidates: cands };
1662
+ }
1663
+ if (TailMatches(prevWords, ['ALTER', 'TYPE', MatchAny])) {
1664
+ return {
1665
+ candidates: filterAndCase(ALTER_TYPE_ACTIONS, currentWord, ctx.settings),
1666
+ };
1667
+ }
1668
+ if (TailMatches(prevWords, ['ALTER', 'TYPE'])) {
1669
+ if (!conn)
1670
+ return { candidates: [] };
1671
+ return {
1672
+ candidates: await runCatalogQuery(conn, Query_for_list_of_types, currentWord),
1673
+ };
1674
+ }
1675
+ // ---- ALTER ROLE / USER / GROUP ----
1676
+ if (TailMatches(prevWords, ['ALTER', 'ROLE|USER|GROUP', MatchAny])) {
1677
+ return {
1678
+ candidates: filterAndCase(ALTER_ROLE_ACTIONS, currentWord, ctx.settings),
1679
+ };
1680
+ }
1681
+ if (TailMatches(prevWords, ['ALTER', 'ROLE|USER|GROUP'])) {
1682
+ if (!conn)
1683
+ return { candidates: [] };
1684
+ return {
1685
+ candidates: await runCatalogQuery(conn, Query_for_list_of_roles, currentWord),
1686
+ };
1687
+ }
1688
+ // ---- ALTER DATABASE ----
1689
+ if (TailMatches(prevWords, ['ALTER', 'DATABASE', MatchAny])) {
1690
+ return {
1691
+ candidates: filterAndCase(ALTER_DATABASE_ACTIONS, currentWord, ctx.settings),
1692
+ };
1693
+ }
1694
+ if (TailMatches(prevWords, ['ALTER', 'DATABASE'])) {
1695
+ if (!conn)
1696
+ return { candidates: [] };
1697
+ return {
1698
+ candidates: await runCatalogQuery(conn, Query_for_list_of_databases, currentWord),
1699
+ };
1700
+ }
1701
+ // ---- ALTER SCHEMA ----
1702
+ if (TailMatches(prevWords, ['ALTER', 'SCHEMA', MatchAny])) {
1703
+ return {
1704
+ candidates: filterAndCase(ALTER_SCHEMA_ACTIONS, currentWord, ctx.settings),
1705
+ };
1706
+ }
1707
+ if (TailMatches(prevWords, ['ALTER', 'SCHEMA'])) {
1708
+ if (!conn)
1709
+ return { candidates: [] };
1710
+ return {
1711
+ candidates: await runCatalogQuery(conn, Query_for_list_of_schemas, currentWord),
1712
+ };
1713
+ }
1714
+ // ---- ALTER EXTENSION ----
1715
+ if (TailMatches(prevWords, ['ALTER', 'EXTENSION', MatchAny])) {
1716
+ return {
1717
+ candidates: filterAndCase(ALTER_EXTENSION_ACTIONS, currentWord, ctx.settings),
1718
+ };
1719
+ }
1720
+ if (TailMatches(prevWords, ['ALTER', 'EXTENSION'])) {
1721
+ if (!conn)
1722
+ return { candidates: [] };
1723
+ return {
1724
+ candidates: await runCatalogQuery(conn, Query_for_list_of_extensions, currentWord),
1725
+ };
1726
+ }
1727
+ // ---- ALTER POLICY <name> ON <table> ----
1728
+ if (TailMatches(prevWords, ['ALTER', 'POLICY', MatchAny])) {
1729
+ return {
1730
+ candidates: filterAndCase(ALTER_POLICY_ACTIONS, currentWord, ctx.settings),
1731
+ };
1732
+ }
1733
+ if (HeadMatches(prevWords, ['ALTER', 'POLICY']) &&
1734
+ TailMatches(prevWords, ['ON'])) {
1735
+ return completeTables();
1736
+ }
1737
+ if (TailMatches(prevWords, ['ALTER', 'POLICY'])) {
1738
+ // Free-form policy name.
1739
+ return { candidates: [] };
1740
+ }
1741
+ // ---- ALTER PUBLICATION ----
1742
+ if (TailMatches(prevWords, ['ALTER', 'PUBLICATION', MatchAny])) {
1743
+ return {
1744
+ candidates: filterAndCase(ALTER_PUBLICATION_ACTIONS, currentWord, ctx.settings),
1745
+ };
1746
+ }
1747
+ if (TailMatches(prevWords, ['ALTER', 'PUBLICATION'])) {
1748
+ if (!conn)
1749
+ return { candidates: [] };
1750
+ return {
1751
+ candidates: await runCatalogQuery(conn, Query_for_list_of_publications, currentWord),
1752
+ };
1753
+ }
1754
+ // ---- ALTER SUBSCRIPTION ----
1755
+ if (TailMatches(prevWords, ['ALTER', 'SUBSCRIPTION', MatchAny])) {
1756
+ return {
1757
+ candidates: filterAndCase(ALTER_SUBSCRIPTION_ACTIONS, currentWord, ctx.settings),
1758
+ };
1759
+ }
1760
+ if (TailMatches(prevWords, ['ALTER', 'SUBSCRIPTION'])) {
1761
+ if (!conn)
1762
+ return { candidates: [] };
1763
+ return {
1764
+ candidates: await runCatalogQuery(conn, Query_for_list_of_subscriptions, currentWord),
1765
+ };
1766
+ }
1767
+ // Bare ALTER — sub-object keywords.
1768
+ if (TailMatches(prevWords, ['ALTER'])) {
1769
+ return {
1770
+ candidates: filterAndCase(ALTER_OBJECTS, currentWord, ctx.settings),
1771
+ };
1772
+ }
1773
+ // DROP TABLE, DROP VIEW, DROP INDEX, ...
1774
+ if (TailMatches(prevWords, ['DROP', 'TABLE']))
1775
+ return completeTables();
1776
+ if (TailMatches(prevWords, ['DROP', 'VIEW'])) {
1777
+ return completeTables(Query_for_list_of_views);
1778
+ }
1779
+ if (TailMatches(prevWords, ['DROP', 'MATERIALIZED', 'VIEW'])) {
1780
+ return completeTables(Query_for_list_of_matviews);
1781
+ }
1782
+ if (TailMatches(prevWords, ['DROP', 'INDEX'])) {
1783
+ return completeTables(Query_for_list_of_indexes);
1784
+ }
1785
+ if (TailMatches(prevWords, ['DROP', 'SEQUENCE'])) {
1786
+ return completeTables(Query_for_list_of_sequences);
1787
+ }
1788
+ if (TailMatches(prevWords, ['DROP', 'TYPE'])) {
1789
+ // Built-in scalar type keywords mirror upstream's
1790
+ // `Keywords_for_list_of_datatypes` attached to the SchemaQuery; psql
1791
+ // mixes them with user-defined types so `DROP TYPE big<TAB>` resolves
1792
+ // to `bigint` even without a matching catalog row.
1793
+ const keywords = filterAndCase(BUILTIN_DATATYPE_KEYWORDS, currentWord, ctx.settings);
1794
+ if (!conn)
1795
+ return { candidates: keywords };
1796
+ const types = await runCatalogQuery(conn, Query_for_list_of_types, currentWord);
1797
+ return { candidates: [...keywords, ...types] };
1798
+ }
1799
+ if (TailMatches(prevWords, ['DROP', 'SCHEMA'])) {
1800
+ if (!conn)
1801
+ return { candidates: [] };
1802
+ return {
1803
+ candidates: await runCatalogQuery(conn, Query_for_list_of_schemas, currentWord),
1804
+ };
1805
+ }
1806
+ if (TailMatches(prevWords, ['DROP', 'EXTENSION'])) {
1807
+ if (!conn)
1808
+ return { candidates: [] };
1809
+ return {
1810
+ candidates: await runCatalogQuery(conn, Query_for_list_of_extensions, currentWord),
1811
+ };
1812
+ }
1813
+ if (TailMatches(prevWords, ['DROP', 'ROLE|USER|GROUP'])) {
1814
+ if (!conn)
1815
+ return { candidates: [] };
1816
+ return {
1817
+ candidates: await runCatalogQuery(conn, Query_for_list_of_roles, currentWord),
1818
+ };
1819
+ }
1820
+ if (TailMatches(prevWords, ['DROP', 'DATABASE'])) {
1821
+ if (!conn)
1822
+ return { candidates: [] };
1823
+ return {
1824
+ candidates: await runCatalogQuery(conn, Query_for_list_of_databases, currentWord),
1825
+ };
1826
+ }
1827
+ if (TailMatches(prevWords, ['DROP', 'FUNCTION'])) {
1828
+ if (!conn)
1829
+ return { candidates: [] };
1830
+ return {
1831
+ candidates: await runCatalogQuery(conn, Query_for_list_of_functions, currentWord),
1832
+ };
1833
+ }
1834
+ if (TailMatches(prevWords, ['DROP', 'LANGUAGE'])) {
1835
+ if (!conn)
1836
+ return { candidates: [] };
1837
+ return {
1838
+ candidates: await runCatalogQuery(conn, Query_for_list_of_languages, currentWord),
1839
+ };
1840
+ }
1841
+ if (TailMatches(prevWords, ['DROP', 'TABLESPACE'])) {
1842
+ if (!conn)
1843
+ return { candidates: [] };
1844
+ return {
1845
+ candidates: await runCatalogQuery(conn, Query_for_list_of_tablespaces, currentWord),
1846
+ };
1847
+ }
1848
+ if (TailMatches(prevWords, ['DROP', 'PUBLICATION'])) {
1849
+ // Mirrors upstream's `words_after_create` PUBLICATION entry
1850
+ // (VersionedQuery on `Query_for_list_of_publications`). Two-step
1851
+ // completion: `DROP PUBLIC<TAB>` first resolves to `PUBLICATION`
1852
+ // via the static DROP_OBJECTS list (handled below by the bare
1853
+ // `DROP` arm), then `DROP PUBLICATION <TAB>` lists publications.
1854
+ if (!conn)
1855
+ return { candidates: [] };
1856
+ return {
1857
+ candidates: await runCatalogQuery(conn, Query_for_list_of_publications, currentWord),
1858
+ };
1859
+ }
1860
+ if (TailMatches(prevWords, ['DROP', 'SUBSCRIPTION'])) {
1861
+ // Same shape as DROP PUBLICATION — paired here for parity with the
1862
+ // ALTER block above.
1863
+ if (!conn)
1864
+ return { candidates: [] };
1865
+ return {
1866
+ candidates: await runCatalogQuery(conn, Query_for_list_of_subscriptions, currentWord),
1867
+ };
1868
+ }
1869
+ // Bare DROP — sub-object keywords.
1870
+ if (TailMatches(prevWords, ['DROP'])) {
1871
+ return {
1872
+ candidates: filterAndCase(DROP_OBJECTS, currentWord, ctx.settings),
1873
+ };
1874
+ }
1875
+ // ---- CREATE INDEX deep handling (specific arms BEFORE generic CREATE). ----
1876
+ // CREATE INDEX <TAB> → CONCURRENTLY, IF NOT EXISTS, ON (no name = use ON
1877
+ // directly), or a free-form index name.
1878
+ if (TailMatches(prevWords, ['CREATE', 'INDEX'])) {
1879
+ return {
1880
+ candidates: filterAndCase(CREATE_INDEX_OPTIONS, currentWord, ctx.settings),
1881
+ };
1882
+ }
1883
+ if (TailMatches(prevWords, ['CREATE', 'UNIQUE', 'INDEX'])) {
1884
+ return {
1885
+ candidates: filterAndCase(CREATE_INDEX_OPTIONS, currentWord, ctx.settings),
1886
+ };
1887
+ }
1888
+ // CREATE INDEX <name> <TAB> → ON.
1889
+ if (TailMatches(prevWords, ['CREATE', 'INDEX', MatchAny]) ||
1890
+ TailMatches(prevWords, ['CREATE', 'UNIQUE', 'INDEX', MatchAny])) {
1891
+ return { candidates: filterAndCase(['ON'], currentWord, ctx.settings) };
1892
+ }
1893
+ // CREATE INDEX ... ON <TAB> → tables.
1894
+ if (TailMatches(prevWords, ['CREATE', 'INDEX', MatchAny, 'ON']) ||
1895
+ TailMatches(prevWords, ['CREATE', 'INDEX', 'ON']) ||
1896
+ TailMatches(prevWords, ['CREATE', 'UNIQUE', 'INDEX', MatchAny, 'ON']) ||
1897
+ TailMatches(prevWords, ['CREATE', 'UNIQUE', 'INDEX', 'ON'])) {
1898
+ return completeTables();
1899
+ }
1900
+ // CREATE INDEX ... ON <table> <TAB> → USING / (.
1901
+ if ((HeadMatches(prevWords, ['CREATE', 'INDEX']) ||
1902
+ HeadMatches(prevWords, ['CREATE', 'UNIQUE', 'INDEX'])) &&
1903
+ TailMatches(prevWords, ['ON', MatchAny])) {
1904
+ return {
1905
+ candidates: filterAndCase(['USING', '('], currentWord, ctx.settings),
1906
+ };
1907
+ }
1908
+ // CREATE INDEX ... USING <TAB> → access methods.
1909
+ if ((HeadMatches(prevWords, ['CREATE', 'INDEX']) ||
1910
+ HeadMatches(prevWords, ['CREATE', 'UNIQUE', 'INDEX'])) &&
1911
+ TailMatches(prevWords, ['USING'])) {
1912
+ if (!conn) {
1913
+ // Even without a connection, offer the built-in AMs as a fallback.
1914
+ return {
1915
+ candidates: filterAndCase(['btree', 'hash', 'gist', 'gin', 'spgist', 'brin'], currentWord, ctx.settings),
1916
+ };
1917
+ }
1918
+ return {
1919
+ candidates: await runCatalogQuery(conn, Query_for_list_of_index_access_methods, currentWord),
1920
+ };
1921
+ }
1922
+ // CREATE ... — first sub-object keyword.
1923
+ if (TailMatches(prevWords, ['CREATE'])) {
1924
+ return {
1925
+ candidates: filterAndCase(CREATE_OBJECTS, currentWord, ctx.settings),
1926
+ };
1927
+ }
1928
+ if (TailMatches(prevWords, ['CREATE', 'TABLE'])) {
1929
+ // Upstream's `words_after_create` fallback (tab-complete.in.c
1930
+ // ~lines 2062-2082) runs `Query_for_list_of_tables` when the
1931
+ // word immediately before the cursor is `TABLE`. The intent is to
1932
+ // surface existing table names as a HINT — the user can pick a
1933
+ // similar name as a starting point. The completion is non-binding;
1934
+ // psql doesn't actually insert one of these on a single Tab if
1935
+ // the prefix is ambiguous, but the listing on Tab-Tab matches
1936
+ // upstream's `qr/mytab123 +mytab246/`.
1937
+ return completeTables();
1938
+ }
1939
+ // Inside CREATE TABLE column-list parens: `<col_name> <TAB>` → types.
1940
+ if (HeadMatches(prevWords, ['CREATE', 'TABLE']) &&
1941
+ TailMatches(prevWords, ['(|,', MatchAny])) {
1942
+ if (!conn)
1943
+ return { candidates: [] };
1944
+ return {
1945
+ candidates: await runCatalogQuery(conn, Query_for_list_of_datatypes, currentWord),
1946
+ };
1947
+ }
1948
+ if (TailMatches(prevWords, ['CREATE', 'OR', 'REPLACE'])) {
1949
+ return {
1950
+ candidates: filterAndCase(['FUNCTION', 'PROCEDURE', 'VIEW', 'TRIGGER', 'AGGREGATE', 'TRANSFORM'], currentWord, ctx.settings),
1951
+ };
1952
+ }
1953
+ // TRUNCATE [TABLE] x
1954
+ if (TailMatches(prevWords, ['TRUNCATE'])) {
1955
+ return {
1956
+ candidates: [
1957
+ ...filterAndCase(['TABLE', 'ONLY'], currentWord, ctx.settings),
1958
+ ...(await tableCandidates(conn, currentWord)),
1959
+ ],
1960
+ };
1961
+ }
1962
+ if (TailMatches(prevWords, ['TRUNCATE', 'TABLE']))
1963
+ return completeTables();
1964
+ if (TailMatches(prevWords, ['TRUNCATE', 'ONLY']))
1965
+ return completeTables();
1966
+ // COPY ... FROM <sth> WITH ( — option keywords inside the WITH
1967
+ // parenthesised list. The tokenizer splits `(` and `,` into their
1968
+ // own tokens so the tail looks like `[... WITH (]` for the first
1969
+ // option and `[..., WITH, (, opt, ,]` for subsequent ones. Two
1970
+ // arms: one for FROM (which adds DEFAULT, FORCE_NOT_NULL, etc.)
1971
+ // and one for TO (which adds FORCE_QUOTE only). Mirrors upstream
1972
+ // tab-complete.in.c ~3309-3315 (`Copy_from_options` /
1973
+ // `Copy_to_options`). The `HeadMatches` guard restricts the rule
1974
+ // to a real COPY statement so the generic `(`/`,` pattern doesn't
1975
+ // fire inside CREATE TABLE / SELECT lists.
1976
+ if (HeadMatches(prevWords, ['COPY|\\copy']) &&
1977
+ HeadMatches(prevWords, [MatchAny, MatchAny, 'FROM']) &&
1978
+ (TailMatches(prevWords, ['(']) || TailMatches(prevWords, [',']))) {
1979
+ return {
1980
+ candidates: filterAndCase(COPY_FROM_OPTIONS, currentWord, ctx.settings),
1981
+ };
1982
+ }
1983
+ if (HeadMatches(prevWords, ['COPY|\\copy']) &&
1984
+ HeadMatches(prevWords, [MatchAny, MatchAny, 'TO']) &&
1985
+ (TailMatches(prevWords, ['(']) || TailMatches(prevWords, [',']))) {
1986
+ return {
1987
+ candidates: filterAndCase(COPY_TO_OPTIONS, currentWord, ctx.settings),
1988
+ };
1989
+ }
1990
+ // COPY x → tables. (The `COPY x FROM/TO <path>` filename completion is
1991
+ // handled by the early `isCopyFromOrTo` check at the top of this
1992
+ // function so it wins over the generic `FROM <prefix>` table rule.)
1993
+ if (TailMatches(prevWords, ['COPY']))
1994
+ return completeTables();
1995
+ if (TailMatches(prevWords, ['COPY', MatchAny])) {
1996
+ return {
1997
+ candidates: filterAndCase(['FROM', 'TO'], currentWord, ctx.settings),
1998
+ };
1999
+ }
2000
+ // ANALYZE x → tables (optional VERBOSE first).
2001
+ if (TailMatches(prevWords, ['ANALYZE']) ||
2002
+ TailMatches(prevWords, ['ANALYZE', 'VERBOSE'])) {
2003
+ return completeTables();
2004
+ }
2005
+ // VACUUM x → tables.
2006
+ if (TailMatches(prevWords, ['VACUUM']) ||
2007
+ TailMatches(prevWords, ['VACUUM', 'VERBOSE'])) {
2008
+ return completeTables();
2009
+ }
2010
+ if (TailMatches(prevWords, ['VACUUM', 'FULL']))
2011
+ return completeTables();
2012
+ if (TailMatches(prevWords, ['VACUUM', 'ANALYZE']))
2013
+ return completeTables();
2014
+ // REINDEX [TABLE|INDEX|SCHEMA|DATABASE] x
2015
+ if (TailMatches(prevWords, ['REINDEX'])) {
2016
+ return {
2017
+ candidates: filterAndCase(['TABLE', 'INDEX', 'SCHEMA', 'DATABASE', 'SYSTEM', 'CONCURRENTLY'], currentWord, ctx.settings),
2018
+ };
2019
+ }
2020
+ if (TailMatches(prevWords, ['REINDEX', 'TABLE']))
2021
+ return completeTables();
2022
+ if (TailMatches(prevWords, ['REINDEX', 'INDEX'])) {
2023
+ return completeTables(Query_for_list_of_indexes);
2024
+ }
2025
+ if (TailMatches(prevWords, ['REINDEX', 'SCHEMA'])) {
2026
+ if (!conn)
2027
+ return { candidates: [] };
2028
+ return {
2029
+ candidates: await runCatalogQuery(conn, Query_for_list_of_schemas, currentWord),
2030
+ };
2031
+ }
2032
+ if (TailMatches(prevWords, ['REINDEX', 'DATABASE'])) {
2033
+ if (!conn)
2034
+ return { candidates: [] };
2035
+ return {
2036
+ candidates: await runCatalogQuery(conn, Query_for_list_of_databases, currentWord),
2037
+ };
2038
+ }
2039
+ // GRANT … ON … TO …
2040
+ if (TailMatches(prevWords, ['GRANT']) || TailMatches(prevWords, ['REVOKE'])) {
2041
+ return {
2042
+ candidates: filterAndCase(PRIVILEGE_KEYWORDS, currentWord, ctx.settings),
2043
+ };
2044
+ }
2045
+ if (TailMatches(prevWords, ['ON'])) {
2046
+ // Could be either GRANT/REVOKE … ON or CREATE INDEX … ON. We try
2047
+ // tables; if the prior keyword set is CREATE INDEX the rule above
2048
+ // already short-circuited.
2049
+ return completeTables();
2050
+ }
2051
+ if (TailMatches(prevWords, ['TO'])) {
2052
+ if (HeadMatches(prevWords, ['GRANT']) && conn) {
2053
+ return {
2054
+ candidates: await runCatalogQuery(conn, Query_for_list_of_roles, currentWord),
2055
+ };
2056
+ }
2057
+ }
2058
+ if (TailMatches(prevWords, ['FROM']) && HeadMatches(prevWords, ['REVOKE'])) {
2059
+ if (!conn)
2060
+ return { candidates: [] };
2061
+ return {
2062
+ candidates: await runCatalogQuery(conn, Query_for_list_of_roles, currentWord),
2063
+ };
2064
+ }
2065
+ // LOCK [TABLE] x
2066
+ if (TailMatches(prevWords, ['LOCK'])) {
2067
+ return {
2068
+ candidates: [
2069
+ ...filterAndCase(['TABLE'], currentWord, ctx.settings),
2070
+ ...(await tableCandidates(conn, currentWord)),
2071
+ ],
2072
+ };
2073
+ }
2074
+ if (TailMatches(prevWords, ['LOCK', 'TABLE']))
2075
+ return completeTables();
2076
+ // SET search_path, SET ROLE, SET SCHEMA, etc.
2077
+ if (TailMatches(prevWords, ['SET', 'ROLE'])) {
2078
+ if (!conn)
2079
+ return { candidates: [] };
2080
+ return {
2081
+ candidates: await runCatalogQuery(conn, Query_for_list_of_roles, currentWord),
2082
+ };
2083
+ }
2084
+ if (TailMatches(prevWords, ['SET', 'SCHEMA'])) {
2085
+ if (!conn)
2086
+ return { candidates: [] };
2087
+ return {
2088
+ candidates: await runCatalogQuery(conn, Query_for_list_of_schemas, currentWord),
2089
+ };
2090
+ }
2091
+ if (TailMatches(prevWords, ['SET', 'SESSION'])) {
2092
+ return {
2093
+ candidates: filterAndCase(['AUTHORIZATION', 'CHARACTERISTICS AS TRANSACTION'], currentWord, ctx.settings),
2094
+ };
2095
+ }
2096
+ if (TailMatches(prevWords, ['SET', 'TRANSACTION'])) {
2097
+ return {
2098
+ candidates: filterAndCase(['ISOLATION LEVEL', 'READ ONLY', 'READ WRITE', 'DEFERRABLE'], currentWord, ctx.settings),
2099
+ };
2100
+ }
2101
+ // `SET <name> TO|= <TAB>` — for DateStyle we can suggest the formats.
2102
+ if (TailMatches(prevWords, ['SET', 'DateStyle', 'TO|=']) ||
2103
+ TailMatches(prevWords, ['SET', 'DATESTYLE', 'TO|=']) ||
2104
+ TailMatches(prevWords, ['SET', 'datestyle', 'TO|='])) {
2105
+ return {
2106
+ candidates: filterAndCase(DATESTYLE_VALUES, currentWord, ctx.settings),
2107
+ };
2108
+ }
2109
+ // `SET timezone TO <prefix>` — names from pg_timezone_names. Two
2110
+ // variants depending on whether the user has opened a single quote:
2111
+ // - `SET timezone TO am<TAB>` → user typed an unquoted prefix; we
2112
+ // respond with `'America/'`-style quoted candidates so the next
2113
+ // keystroke continues inside the string literal. The
2114
+ // `Query_for_list_of_timezone_names_quoted_out` template matches
2115
+ // LIKE against the unquoted name and returns `quote_literal(name)`.
2116
+ // - `SET timezone TO 'America/New_<TAB>` → user is mid-literal;
2117
+ // `Query_for_list_of_timezone_names_quoted_in` matches the LIKE
2118
+ // pattern against the quoted form so the partial `'America/New_`
2119
+ // resolves correctly.
2120
+ // Mirrors upstream tab-complete.in.c ~line 4530.
2121
+ if (TailMatches(prevWords, ['SET', 'timezone', 'TO|=']) ||
2122
+ TailMatches(prevWords, ['SET', 'TIMEZONE', 'TO|=']) ||
2123
+ TailMatches(prevWords, ['SET', 'TimeZone', 'TO|='])) {
2124
+ if (!conn)
2125
+ return { candidates: [] };
2126
+ const query = currentWord.startsWith("'")
2127
+ ? Query_for_list_of_timezone_names_quoted_in
2128
+ : Query_for_list_of_timezone_names_quoted_out;
2129
+ const cands = await runCatalogQuery(conn, query, currentWord);
2130
+ return { candidates: cands };
2131
+ }
2132
+ // `SET <name> TO|= <prefix>` for any enum-typed GUC — emit the legal
2133
+ // values from `pg_settings.enumvals`. Mirrors upstream `tab-complete.in.c`
2134
+ // `Query_for_values_of_enum_GUC`. Enum-typed GUCs (intervalstyle,
2135
+ // bytea_output, synchronous_commit, plpgsql.variable_conflict, …) get
2136
+ // value completion; non-enum GUCs return an empty rowset (no candidates)
2137
+ // — matching upstream, which also emits nothing for unknown GUCs once a
2138
+ // value position is reached.
2139
+ if (TailMatches(prevWords, ['SET', MatchAny, 'TO|='])) {
2140
+ if (!conn)
2141
+ return { candidates: [] };
2142
+ // Walk back to the GUC name (the word before TO/=).
2143
+ const gucName = prevWords[prevWords.length - 2];
2144
+ const cands = await runCatalogQuery(conn, Query_for_values_of_enum_GUC, currentWord, [gucName]);
2145
+ return { candidates: cands };
2146
+ }
2147
+ // `SET <name> <TAB>` (no operator yet) → TO.
2148
+ // Upstream tab-complete.in.c uses `COMPLETE_WITH("TO")` here even
2149
+ // though `SET <name> = <value>` is valid syntax — the goal is a
2150
+ // single unique completion so `set foo<tab><tab>` resolves to
2151
+ // `set foo TO ` rather than listing two near-synonymous separators.
2152
+ // Verified against vanilla psql 18 + upstream test line 366.
2153
+ if (TailMatches(prevWords, ['SET', MatchAny])) {
2154
+ return {
2155
+ candidates: filterAndCase(['TO'], currentWord, ctx.settings),
2156
+ };
2157
+ }
2158
+ // Bare SET <TAB>: GUC name OR top-level SET sub-keywords (ROLE, SCHEMA, …).
2159
+ if (TailMatches(prevWords, ['SET'])) {
2160
+ const staticKw = filterAndCase([
2161
+ 'CONSTRAINTS',
2162
+ 'LOCAL',
2163
+ 'ROLE',
2164
+ 'SCHEMA',
2165
+ 'SESSION',
2166
+ 'TIME ZONE',
2167
+ 'TRANSACTION',
2168
+ ], currentWord, ctx.settings);
2169
+ if (!conn)
2170
+ return { candidates: staticKw };
2171
+ const guc = await runCatalogQuery(conn, Query_for_list_of_set_vars, currentWord);
2172
+ return { candidates: [...staticKw, ...guc] };
2173
+ }
2174
+ // SHOW <TAB> — ALL keyword plus the live GUC list.
2175
+ if (TailMatches(prevWords, ['SHOW'])) {
2176
+ const staticKw = filterAndCase(['ALL'], currentWord, ctx.settings);
2177
+ if (!conn) {
2178
+ return {
2179
+ candidates: [
2180
+ ...staticKw,
2181
+ ...filterAndCase([
2182
+ 'search_path',
2183
+ 'role',
2184
+ 'session_authorization',
2185
+ 'transaction_isolation',
2186
+ 'client_encoding',
2187
+ 'server_encoding',
2188
+ 'server_version',
2189
+ 'timezone',
2190
+ ], currentWord, ctx.settings),
2191
+ ],
2192
+ };
2193
+ }
2194
+ const guc = await runCatalogQuery(conn, Query_for_list_of_set_vars, currentWord);
2195
+ return { candidates: [...staticKw, ...guc] };
2196
+ }
2197
+ // RESET <TAB> — ALL or the live GUC list.
2198
+ if (TailMatches(prevWords, ['RESET'])) {
2199
+ const staticKw = filterAndCase(['ALL', 'SESSION AUTHORIZATION', 'ROLE'], currentWord, ctx.settings);
2200
+ if (!conn)
2201
+ return { candidates: staticKw };
2202
+ const guc = await runCatalogQuery(conn, Query_for_list_of_set_vars, currentWord);
2203
+ return { candidates: [...staticKw, ...guc] };
2204
+ }
2205
+ // START / BEGIN [TRANSACTION] …
2206
+ if (TailMatches(prevWords, ['BEGIN']) || TailMatches(prevWords, ['START'])) {
2207
+ return {
2208
+ candidates: filterAndCase(TRANSACTION_KEYWORDS, currentWord, ctx.settings),
2209
+ };
2210
+ }
2211
+ // COMMIT / ROLLBACK / RELEASE / SAVEPOINT - small completions
2212
+ if (TailMatches(prevWords, ['ROLLBACK'])) {
2213
+ return {
2214
+ candidates: filterAndCase(['TO SAVEPOINT', 'TRANSACTION', 'AND CHAIN', 'AND NO CHAIN'], currentWord, ctx.settings),
2215
+ };
2216
+ }
2217
+ // EXPLAIN … — pass through to SELECT-/INSERT-/UPDATE-style rules by
2218
+ // letting subsequent words drive completion. For the first word after
2219
+ // EXPLAIN, offer the options + statement keywords.
2220
+ if (TailMatches(prevWords, ['EXPLAIN'])) {
2221
+ return {
2222
+ candidates: filterAndCase([
2223
+ 'ANALYZE',
2224
+ 'VERBOSE',
2225
+ 'SELECT',
2226
+ 'INSERT INTO',
2227
+ 'UPDATE',
2228
+ 'DELETE FROM',
2229
+ '(',
2230
+ ], currentWord, ctx.settings),
2231
+ };
2232
+ }
2233
+ // DECLARE … CURSOR FOR …
2234
+ if (TailMatches(prevWords, ['DECLARE'])) {
2235
+ return { candidates: [] };
2236
+ }
2237
+ // CALL / DO — no completion beyond keywords (procedure args / language).
2238
+ if (TailMatches(prevWords, ['CALL'])) {
2239
+ if (!conn)
2240
+ return { candidates: [] };
2241
+ return {
2242
+ candidates: await runCatalogQuery(conn, Query_for_list_of_functions, currentWord),
2243
+ };
2244
+ }
2245
+ // After SELECT — offer FROM / common functions.
2246
+ if (TailMatches(prevWords, ['SELECT'])) {
2247
+ // The next word can be anything (column list); offer DISTINCT/ALL/* as a
2248
+ // small hint set.
2249
+ return {
2250
+ candidates: filterAndCase([
2251
+ 'DISTINCT',
2252
+ 'ALL',
2253
+ '*',
2254
+ 'CURRENT_DATE',
2255
+ 'CURRENT_TIME',
2256
+ 'CURRENT_TIMESTAMP',
2257
+ 'CURRENT_USER',
2258
+ 'NULL',
2259
+ 'TRUE',
2260
+ 'FALSE',
2261
+ ], currentWord, ctx.settings),
2262
+ };
2263
+ }
2264
+ // Trailing-keyword fallthrough: WHERE, ORDER BY, GROUP BY, LIMIT etc.
2265
+ if (TailMatches(prevWords, ['SELECT', '*'])) {
2266
+ return { candidates: filterAndCase(['FROM'], currentWord, ctx.settings) };
2267
+ }
2268
+ // No specific rule fired.
2269
+ return { candidates: [] };
2270
+ };
2271
+ // ---------------------------------------------------------------------------
2272
+ // Helpers.
2273
+ // ---------------------------------------------------------------------------
2274
+ /** Candidates for table-name completion, no schema-qualifier handling. */
2275
+ const tableCandidates = async (conn, word) => {
2276
+ if (!conn)
2277
+ return [];
2278
+ return runCatalogQuery(conn, Query_for_list_of_tables, word);
2279
+ };
2280
+ /**
2281
+ * Schema-aware relation completion. If the user typed `schema.x`, fetch
2282
+ * relations in `schema` matching `x`. Otherwise fetch unqualified relations
2283
+ * AND a list of schemas (so the user can dot through to relations in any
2284
+ * schema, mirroring upstream's `complete_from_schema_query`).
2285
+ *
2286
+ * Schema names are returned with a trailing `.` so the completion engine
2287
+ * recognizes them as "in progress" and refrains from appending a space.
2288
+ * When a unique candidate is a schema (e.g. `pub<tab>` → `public.`) the
2289
+ * user can immediately Tab again to list relations within that schema.
2290
+ *
2291
+ * Quoted identifier handling: if the user's input starts with `"`, we
2292
+ * search the catalog case-sensitively (stripping the opening quote) and
2293
+ * return candidates pre-quoted with `"..."` so the line ends up in a
2294
+ * valid quoted form — matching upstream readline's behaviour for
2295
+ * `select * from "my<tab>` → `"mytab123 "mytab246`.
2296
+ */
2297
+ const completeSchemaOrRelations = async (conn, word, query) => {
2298
+ if (word.startsWith('"')) {
2299
+ return completeQuotedRelations(conn, word, query);
2300
+ }
2301
+ const split = splitSchemaPrefix(word);
2302
+ if (split) {
2303
+ const rows = await runCatalogQuery(conn, Query_for_list_of_relations_in_schema, split.prefix, [split.schema]);
2304
+ // Re-prefix with the catalog's canonical schema name. Upstream folds
2305
+ // unquoted schema identifiers to lowercase in the output (mirroring
2306
+ // the way Postgres downcases unquoted names), so `PUBLIC.t<tab>`
2307
+ // completes to `public.tab1` instead of preserving `PUBLIC` in the
2308
+ // returned candidate.
2309
+ const canonicalSchema = split.schemaWasQuoted
2310
+ ? split.schema
2311
+ : split.schema.toLowerCase();
2312
+ return rows.map((r) => canonicalSchema + '.' + r);
2313
+ }
2314
+ // Unqualified: combine relations + schemas (schemas suffixed with `.`).
2315
+ const [rels, schemas] = await Promise.all([
2316
+ runCatalogQuery(conn, query, word),
2317
+ runCatalogQuery(conn, Query_for_list_of_schemas, word),
2318
+ ]);
2319
+ return [...rels, ...schemas.map((s) => s + '.')];
2320
+ };
2321
+ /**
2322
+ * Search the catalog for relations matching a user-supplied prefix that
2323
+ * begins with `"`. We strip the opening quote, search the RAW relname
2324
+ * case-sensitively (so `"mi` → `mixedName` rather than `mytab123`), and
2325
+ * emit results wrapped in `"..."` with a closing quote so the editor's
2326
+ * unique-completion handler appends the trailing space outside the
2327
+ * quoted region (e.g. `"mixedName" `).
2328
+ */
2329
+ const completeQuotedRelations = async (conn, word, query) => {
2330
+ // Strip leading `"` (and any trailing `"` the user pre-typed).
2331
+ const inside = word.slice(1).replace(/"$/, '');
2332
+ // Use a case-sensitive raw-relname LIKE — the inside-the-quote portion
2333
+ // is taken verbatim.
2334
+ const sql = caseSensitiveRelnameVariant(query);
2335
+ if (!sql)
2336
+ return [];
2337
+ const rows = await runCatalogQuery(conn, sql, inside);
2338
+ return rows.map((name) => '"' + name + '"');
2339
+ };
2340
+ /**
2341
+ * Build a case-sensitive variant of a relation-list query for quoted
2342
+ * input. The standard queries use `ILIKE` on `quote_ident(c.relname)`
2343
+ * (which obscures the original case for identifiers like `mixedName`);
2344
+ * we swap that for `LIKE` on the raw `c.relname` and return the raw
2345
+ * relname so we control the quoting.
2346
+ *
2347
+ * Returns null if the query isn't a recognised relation query
2348
+ * (defensive — keeps the quoted-completion code path bounded).
2349
+ */
2350
+ const caseSensitiveRelnameVariant = (sql) => {
2351
+ // We rewrite the SELECT/WHERE clauses on the standard relation queries.
2352
+ // The only shape we need to support is `c.relname ILIKE $1` against
2353
+ // `pg_catalog.pg_class c`, with various `c.relkind IN (...)` filters.
2354
+ if (!sql.includes('pg_catalog.pg_class c'))
2355
+ return null;
2356
+ if (!sql.includes('c.relname ILIKE $1'))
2357
+ return null;
2358
+ return sql
2359
+ .replace('SELECT pg_catalog.quote_ident(c.relname)', 'SELECT c.relname')
2360
+ .replace('c.relname ILIKE $1', 'c.relname LIKE $1');
2361
+ };
2362
+ /** Case-insensitive prefix filter; preserves the candidate's original casing. */
2363
+ const filterCi = (candidates, prefix) => {
2364
+ if (prefix.length === 0)
2365
+ return candidates.slice();
2366
+ const lp = prefix.toLowerCase();
2367
+ return candidates.filter((c) => c.toLowerCase().startsWith(lp));
2368
+ };
2369
+ /** Names of variables currently SET on the settings (for `\unset` etc). */
2370
+ const listVarNames = (settings) => {
2371
+ const out = [];
2372
+ for (const [name] of settings.vars.entries())
2373
+ out.push(name);
2374
+ return out.sort();
2375
+ };
2376
+ /** Both currently-set variables AND special-variable names. */
2377
+ const listAllVarNames = (settings) => {
2378
+ const set = new Set();
2379
+ for (const [name] of settings.vars.entries())
2380
+ set.add(name);
2381
+ for (const n of SPECIAL_VARIABLES)
2382
+ set.add(n);
2383
+ return [...set].sort();
2384
+ };
2385
+ // Re-exported so the index entrypoint can implement its own splitting helper
2386
+ // using the same prefix logic.
2387
+ export const _internals = { splitSchemaPrefix };