neonctl 2.22.0 → 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.
- package/README.md +242 -16
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/checkout.js +249 -0
- package/commands/connection_string.js +15 -2
- package/commands/data_api.js +286 -0
- package/commands/functions.js +277 -0
- package/commands/index.js +12 -0
- package/commands/link.js +667 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +62 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/functions_api.js +44 -0
- package/index.js +3 -0
- package/package.json +60 -51
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/enrichers.js +18 -1
- package/utils/esbuild.js +147 -0
- package/utils/middlewares.js +1 -1
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/connection_string.test.js +0 -196
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- 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 };
|