@indiekitai/pg-complete 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1187 @@
1
+ // src/completer.ts
2
+ import { Pool } from "pg";
3
+
4
+ // src/keywords.ts
5
+ var KEYWORDS = [
6
+ // DML
7
+ "SELECT",
8
+ "FROM",
9
+ "WHERE",
10
+ "AND",
11
+ "OR",
12
+ "NOT",
13
+ "IN",
14
+ "EXISTS",
15
+ "BETWEEN",
16
+ "LIKE",
17
+ "ILIKE",
18
+ "IS",
19
+ "NULL",
20
+ "TRUE",
21
+ "FALSE",
22
+ "AS",
23
+ "ON",
24
+ "USING",
25
+ "JOIN",
26
+ "INNER",
27
+ "LEFT",
28
+ "RIGHT",
29
+ "FULL",
30
+ "OUTER",
31
+ "CROSS",
32
+ "NATURAL",
33
+ "ORDER",
34
+ "BY",
35
+ "ASC",
36
+ "DESC",
37
+ "NULLS",
38
+ "FIRST",
39
+ "LAST",
40
+ "GROUP",
41
+ "HAVING",
42
+ "LIMIT",
43
+ "OFFSET",
44
+ "UNION",
45
+ "ALL",
46
+ "INTERSECT",
47
+ "EXCEPT",
48
+ "DISTINCT",
49
+ "INSERT",
50
+ "INTO",
51
+ "VALUES",
52
+ "DEFAULT",
53
+ "RETURNING",
54
+ "UPDATE",
55
+ "SET",
56
+ "DELETE",
57
+ "WITH",
58
+ "RECURSIVE",
59
+ "CASE",
60
+ "WHEN",
61
+ "THEN",
62
+ "ELSE",
63
+ "END",
64
+ "CAST",
65
+ "COALESCE",
66
+ "NULLIF",
67
+ "GREATEST",
68
+ "LEAST",
69
+ // DDL
70
+ "CREATE",
71
+ "ALTER",
72
+ "DROP",
73
+ "TRUNCATE",
74
+ "TABLE",
75
+ "VIEW",
76
+ "INDEX",
77
+ "SEQUENCE",
78
+ "SCHEMA",
79
+ "DATABASE",
80
+ "FUNCTION",
81
+ "PROCEDURE",
82
+ "TRIGGER",
83
+ "TYPE",
84
+ "EXTENSION",
85
+ "MATERIALIZED",
86
+ "TEMPORARY",
87
+ "TEMP",
88
+ "UNLOGGED",
89
+ "IF",
90
+ "CASCADE",
91
+ "RESTRICT",
92
+ "ADD",
93
+ "COLUMN",
94
+ "RENAME",
95
+ "TO",
96
+ "PRIMARY",
97
+ "KEY",
98
+ "FOREIGN",
99
+ "REFERENCES",
100
+ "UNIQUE",
101
+ "CHECK",
102
+ "CONSTRAINT",
103
+ "NOT NULL",
104
+ // Data types
105
+ "INTEGER",
106
+ "INT",
107
+ "BIGINT",
108
+ "SMALLINT",
109
+ "SERIAL",
110
+ "BIGSERIAL",
111
+ "REAL",
112
+ "DOUBLE",
113
+ "PRECISION",
114
+ "NUMERIC",
115
+ "DECIMAL",
116
+ "TEXT",
117
+ "VARCHAR",
118
+ "CHAR",
119
+ "CHARACTER",
120
+ "VARYING",
121
+ "BOOLEAN",
122
+ "BOOL",
123
+ "DATE",
124
+ "TIME",
125
+ "TIMESTAMP",
126
+ "TIMESTAMPTZ",
127
+ "INTERVAL",
128
+ "JSON",
129
+ "JSONB",
130
+ "UUID",
131
+ "BYTEA",
132
+ "ARRAY",
133
+ // Transaction
134
+ "BEGIN",
135
+ "COMMIT",
136
+ "ROLLBACK",
137
+ "SAVEPOINT",
138
+ // Other
139
+ "GRANT",
140
+ "REVOKE",
141
+ "EXPLAIN",
142
+ "ANALYZE",
143
+ "VERBOSE",
144
+ "COPY",
145
+ "VACUUM",
146
+ "REINDEX",
147
+ "CLUSTER",
148
+ "LATERAL",
149
+ "WINDOW",
150
+ "OVER",
151
+ "PARTITION",
152
+ "ROWS",
153
+ "RANGE",
154
+ "FETCH",
155
+ "NEXT",
156
+ "PRIOR",
157
+ "ABSOLUTE",
158
+ "RELATIVE",
159
+ "FOR",
160
+ "SHARE",
161
+ "NOWAIT",
162
+ "SKIP",
163
+ "LOCKED",
164
+ "ANY",
165
+ "SOME"
166
+ ];
167
+ var KEYWORD_TREE = {
168
+ "SELECT": ["DISTINCT", "ALL", "*"],
169
+ "FROM": [],
170
+ "WHERE": [],
171
+ "ORDER": ["BY"],
172
+ "GROUP": ["BY"],
173
+ "INSERT": ["INTO"],
174
+ "UPDATE": ["SET"],
175
+ "DELETE": ["FROM"],
176
+ "CREATE": [
177
+ "TABLE",
178
+ "VIEW",
179
+ "INDEX",
180
+ "SCHEMA",
181
+ "DATABASE",
182
+ "FUNCTION",
183
+ "PROCEDURE",
184
+ "TRIGGER",
185
+ "TYPE",
186
+ "EXTENSION",
187
+ "MATERIALIZED",
188
+ "TEMPORARY",
189
+ "TEMP",
190
+ "UNLOGGED",
191
+ "UNIQUE",
192
+ "OR"
193
+ ],
194
+ "ALTER": [
195
+ "TABLE",
196
+ "VIEW",
197
+ "INDEX",
198
+ "SCHEMA",
199
+ "DATABASE",
200
+ "FUNCTION",
201
+ "PROCEDURE",
202
+ "TRIGGER",
203
+ "TYPE",
204
+ "EXTENSION",
205
+ "SEQUENCE"
206
+ ],
207
+ "DROP": [
208
+ "TABLE",
209
+ "VIEW",
210
+ "INDEX",
211
+ "SCHEMA",
212
+ "DATABASE",
213
+ "FUNCTION",
214
+ "PROCEDURE",
215
+ "TRIGGER",
216
+ "TYPE",
217
+ "EXTENSION",
218
+ "SEQUENCE",
219
+ "IF"
220
+ ],
221
+ "JOIN": [],
222
+ "INNER": ["JOIN"],
223
+ "LEFT": ["JOIN", "OUTER"],
224
+ "RIGHT": ["JOIN", "OUTER"],
225
+ "FULL": ["JOIN", "OUTER"],
226
+ "CROSS": ["JOIN"],
227
+ "NATURAL": ["JOIN", "LEFT", "RIGHT", "FULL", "INNER"],
228
+ "OUTER": ["JOIN"],
229
+ "IF": ["EXISTS", "NOT"],
230
+ "NOT": ["NULL", "EXISTS", "IN", "BETWEEN", "LIKE", "ILIKE"],
231
+ "IS": ["NULL", "NOT", "TRUE", "FALSE", "DISTINCT"],
232
+ "OR": ["REPLACE"],
233
+ "UNION": ["ALL"],
234
+ "LIMIT": [],
235
+ "OFFSET": [],
236
+ "SET": [],
237
+ "VALUES": [],
238
+ "ON": [],
239
+ "RETURNING": [],
240
+ "INTO": [],
241
+ "GRANT": ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE", "USAGE"],
242
+ "REVOKE": ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE", "USAGE"],
243
+ "WITH": ["RECURSIVE"]
244
+ };
245
+ var BUILTIN_FUNCTIONS = [
246
+ // Aggregate
247
+ "avg",
248
+ "count",
249
+ "max",
250
+ "min",
251
+ "sum",
252
+ "array_agg",
253
+ "string_agg",
254
+ "bool_and",
255
+ "bool_or",
256
+ "json_agg",
257
+ "jsonb_agg",
258
+ "json_object_agg",
259
+ "jsonb_object_agg",
260
+ // String
261
+ "length",
262
+ "lower",
263
+ "upper",
264
+ "trim",
265
+ "ltrim",
266
+ "rtrim",
267
+ "substring",
268
+ "replace",
269
+ "concat",
270
+ "concat_ws",
271
+ "left",
272
+ "right",
273
+ "repeat",
274
+ "reverse",
275
+ "split_part",
276
+ "starts_with",
277
+ "format",
278
+ "regexp_replace",
279
+ "regexp_matches",
280
+ // Numeric
281
+ "abs",
282
+ "ceil",
283
+ "floor",
284
+ "round",
285
+ "trunc",
286
+ "mod",
287
+ "power",
288
+ "sqrt",
289
+ "random",
290
+ "generate_series",
291
+ // Date/Time
292
+ "now",
293
+ "current_date",
294
+ "current_time",
295
+ "current_timestamp",
296
+ "date_trunc",
297
+ "date_part",
298
+ "extract",
299
+ "age",
300
+ "to_char",
301
+ "to_date",
302
+ "to_timestamp",
303
+ "to_number",
304
+ "make_date",
305
+ "make_time",
306
+ "make_timestamp",
307
+ // JSON
308
+ "json_build_object",
309
+ "jsonb_build_object",
310
+ "json_build_array",
311
+ "jsonb_build_array",
312
+ "json_extract_path",
313
+ "jsonb_extract_path",
314
+ "json_extract_path_text",
315
+ "jsonb_extract_path_text",
316
+ "json_array_length",
317
+ "jsonb_array_length",
318
+ "json_each",
319
+ "jsonb_each",
320
+ "json_each_text",
321
+ "jsonb_each_text",
322
+ "json_typeof",
323
+ "jsonb_typeof",
324
+ "row_to_json",
325
+ "to_json",
326
+ "to_jsonb",
327
+ // Array
328
+ "array_length",
329
+ "array_dims",
330
+ "array_lower",
331
+ "array_upper",
332
+ "array_append",
333
+ "array_prepend",
334
+ "array_cat",
335
+ "array_remove",
336
+ "array_position",
337
+ "array_positions",
338
+ "unnest",
339
+ // Type casting
340
+ "cast",
341
+ // Window
342
+ "row_number",
343
+ "rank",
344
+ "dense_rank",
345
+ "percent_rank",
346
+ "cume_dist",
347
+ "ntile",
348
+ "lag",
349
+ "lead",
350
+ "first_value",
351
+ "last_value",
352
+ "nth_value",
353
+ // System
354
+ "current_database",
355
+ "current_schema",
356
+ "current_user",
357
+ "session_user",
358
+ "pg_typeof",
359
+ "version",
360
+ // Conditional
361
+ "coalesce",
362
+ "nullif",
363
+ "greatest",
364
+ "least"
365
+ ];
366
+
367
+ // src/parser.ts
368
+ function suggestType(fullText, textBeforeCursor) {
369
+ const trimmed = textBeforeCursor.trimEnd();
370
+ const wordMatch = textBeforeCursor.match(/([A-Za-z0-9_."$]+)$/);
371
+ const wordBeforeCursor = wordMatch ? wordMatch[1] : "";
372
+ const textForAnalysis = wordBeforeCursor ? textBeforeCursor.slice(0, -wordBeforeCursor.length) : textBeforeCursor;
373
+ const dotMatch = wordBeforeCursor.match(/^([A-Za-z_][A-Za-z0-9_]*)\.(.*)$/);
374
+ const tableRefs = extractTables(fullText);
375
+ const lastKeyword = findLastKeyword(textForAnalysis);
376
+ const lastKw = lastKeyword?.toUpperCase() || "";
377
+ if (dotMatch) {
378
+ const parent = dotMatch[1];
379
+ const matchingTables = tableRefs.filter(
380
+ (t) => t.alias === parent || t.name === parent || t.schema && `${t.schema}.${t.name}` === parent
381
+ );
382
+ const suggestions = [];
383
+ if (matchingTables.length > 0) {
384
+ suggestions.push({ type: "column", tableRefs: matchingTables, qualifiable: false, parent });
385
+ }
386
+ suggestions.push(
387
+ { type: "table", schema: parent },
388
+ { type: "view", schema: parent },
389
+ { type: "function", schema: parent }
390
+ );
391
+ return suggestions;
392
+ }
393
+ if (!lastKw || lastKw === "") {
394
+ return [{ type: "keyword" }];
395
+ }
396
+ if (lastKw === "SELECT" || lastKw === "WHERE" || lastKw === "HAVING" || lastKw === "ORDER BY" || lastKw === "DISTINCT") {
397
+ return [
398
+ { type: "column", tableRefs, qualifiable: tableRefs.length > 1 },
399
+ { type: "function", schema: null },
400
+ { type: "keyword", lastToken: lastKw }
401
+ ];
402
+ }
403
+ if (lastKw === "FROM" || lastKw === "COPY" || lastKw === "UPDATE" || lastKw === "INTO" || lastKw === "INSERT INTO" || lastKw === "TRUNCATE") {
404
+ const isFrom = lastKw === "FROM";
405
+ const suggestions = [{ type: "schema" }];
406
+ if (isFrom) {
407
+ suggestions.push({ type: "from_clause", schema: null, tableRefs });
408
+ } else {
409
+ suggestions.push({ type: "table", schema: null });
410
+ suggestions.push({ type: "view", schema: null });
411
+ }
412
+ return suggestions;
413
+ }
414
+ if (lastKw === "JOIN" || lastKw.endsWith(" JOIN")) {
415
+ const suggestions = [
416
+ { type: "schema" },
417
+ { type: "from_clause", schema: null, tableRefs }
418
+ ];
419
+ if (tableRefs.length > 0) {
420
+ suggestions.push({ type: "join", tableRefs, schema: null });
421
+ }
422
+ return suggestions;
423
+ }
424
+ if (lastKw === "ON") {
425
+ const aliases = tableRefs.map((t) => t.alias || t.name);
426
+ const suggestions = [
427
+ { type: "alias", aliases }
428
+ ];
429
+ if (tableRefs.length >= 2) {
430
+ suggestions.push({ type: "join_condition", tableRefs });
431
+ }
432
+ return suggestions;
433
+ }
434
+ if (lastKw === "SET") {
435
+ return [{ type: "column", tableRefs, qualifiable: false }];
436
+ }
437
+ if (lastKw === "TABLE" || lastKw === "VIEW") {
438
+ return [{ type: "schema" }, { type: lastKw === "TABLE" ? "table" : "view", schema: null }];
439
+ }
440
+ if (lastKw === "FUNCTION") {
441
+ return [{ type: "schema" }, { type: "function", schema: null }];
442
+ }
443
+ if (lastKw === "SCHEMA") {
444
+ return [{ type: "schema" }];
445
+ }
446
+ if (lastKw === "DATABASE") {
447
+ return [{ type: "database" }];
448
+ }
449
+ if (lastKw === "TYPE" || lastKw === "::") {
450
+ return [
451
+ { type: "datatype", schema: null },
452
+ { type: "table", schema: null },
453
+ { type: "schema" }
454
+ ];
455
+ }
456
+ if (["CREATE", "ALTER", "DROP"].includes(lastKw)) {
457
+ return [{ type: "keyword", lastToken: lastKw }];
458
+ }
459
+ if (lastKw === "AND" || lastKw === "OR" || lastKw === "," || lastKw === "=") {
460
+ const prevKw = findPrevSignificantKeyword(textForAnalysis);
461
+ if (prevKw) {
462
+ return suggestType(fullText, textForAnalysis);
463
+ }
464
+ }
465
+ if (lastKw === "AS") {
466
+ return [];
467
+ }
468
+ if (lastKw === "COLUMN") {
469
+ return [{ type: "column", tableRefs, qualifiable: false }];
470
+ }
471
+ return [{ type: "keyword", lastToken: lastKw }];
472
+ }
473
+ function extractTables(sql) {
474
+ const tables = [];
475
+ const normalized = sql.replace(/\s+/g, " ").trim();
476
+ const tableContextRegex = /\b(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+/gi;
477
+ let match;
478
+ while ((match = tableContextRegex.exec(normalized)) !== null) {
479
+ const afterKeyword = normalized.slice(match.index + match[0].length);
480
+ const refs = parseTableList(afterKeyword);
481
+ tables.push(...refs);
482
+ }
483
+ return dedup(tables);
484
+ }
485
+ function parseTableList(text) {
486
+ const tables = [];
487
+ const parts = splitOnCommas(text);
488
+ for (const part of parts) {
489
+ const ref = parseOneTableRef(part.trim());
490
+ if (ref) tables.push(ref);
491
+ }
492
+ return tables;
493
+ }
494
+ function splitOnCommas(text) {
495
+ const parts = [];
496
+ let depth = 0;
497
+ let current = "";
498
+ for (const ch of text) {
499
+ if (ch === "(") depth++;
500
+ else if (ch === ")") depth--;
501
+ if (ch === "," && depth === 0) {
502
+ parts.push(current);
503
+ current = "";
504
+ } else if (depth < 0) {
505
+ parts.push(current);
506
+ break;
507
+ } else if (/[;]/.test(ch) && depth === 0) {
508
+ parts.push(current);
509
+ break;
510
+ } else {
511
+ current += ch;
512
+ }
513
+ }
514
+ if (current.trim()) parts.push(current);
515
+ return parts;
516
+ }
517
+ function parseOneTableRef(text) {
518
+ const stopKeywords = /\b(?:WHERE|SET|ORDER|GROUP|HAVING|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|ON|USING|RETURNING|VALUES|LEFT|RIGHT|INNER|OUTER|FULL|CROSS|NATURAL|JOIN)\b/i;
519
+ const stopMatch = text.match(stopKeywords);
520
+ if (stopMatch) {
521
+ text = text.slice(0, stopMatch.index).trim();
522
+ }
523
+ if (!text) return null;
524
+ const m = text.match(
525
+ /^(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_$]*))(?:\.(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_$]*)))?(?:\s+(?:AS\s+)?(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_$]*)))?/i
526
+ );
527
+ if (!m) return null;
528
+ const part1 = m[1] || m[2];
529
+ const part2 = m[3] || m[4];
530
+ const alias = m[5] || m[6];
531
+ const aliasUpper = alias?.toUpperCase();
532
+ const keywordAlias = aliasUpper && /^(WHERE|SET|ORDER|GROUP|HAVING|LIMIT|OFFSET|ON|USING|JOIN|LEFT|RIGHT|INNER|OUTER|FULL|CROSS|NATURAL|AS|AND|OR|NOT|RETURNING|VALUES|INTO|FROM|SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)$/.test(aliasUpper);
533
+ if (part2) {
534
+ return {
535
+ schema: part1.toLowerCase(),
536
+ name: part2.toLowerCase(),
537
+ alias: keywordAlias ? null : alias?.toLowerCase() || null,
538
+ isFunction: false
539
+ };
540
+ }
541
+ return {
542
+ schema: null,
543
+ name: part1.toLowerCase(),
544
+ alias: keywordAlias ? null : alias?.toLowerCase() || null,
545
+ isFunction: false
546
+ };
547
+ }
548
+ function dedup(tables) {
549
+ const seen = /* @__PURE__ */ new Set();
550
+ return tables.filter((t) => {
551
+ const key = `${t.schema || ""}.${t.name}.${t.alias || ""}`;
552
+ if (seen.has(key)) return false;
553
+ seen.add(key);
554
+ return true;
555
+ });
556
+ }
557
+ function findLastKeyword(text) {
558
+ const trimmed = text.trimEnd();
559
+ const compoundMatch = trimmed.match(/\b(ORDER\s+BY|GROUP\s+BY|INSERT\s+INTO|NOT\s+IN|LEFT\s+(?:OUTER\s+)?JOIN|RIGHT\s+(?:OUTER\s+)?JOIN|FULL\s+(?:OUTER\s+)?JOIN|INNER\s+JOIN|CROSS\s+JOIN|NATURAL\s+JOIN|DISTINCT\s+ON)\s*$/i);
560
+ if (compoundMatch) {
561
+ return compoundMatch[1].replace(/\s+/g, " ").toUpperCase();
562
+ }
563
+ const simpleMatch = trimmed.match(/\b([A-Za-z]+)\s*$/);
564
+ if (simpleMatch) {
565
+ const kw = simpleMatch[1].toUpperCase();
566
+ const knownKeywords = /* @__PURE__ */ new Set([
567
+ "SELECT",
568
+ "FROM",
569
+ "WHERE",
570
+ "JOIN",
571
+ "ON",
572
+ "AND",
573
+ "OR",
574
+ "SET",
575
+ "INTO",
576
+ "UPDATE",
577
+ "INSERT",
578
+ "DELETE",
579
+ "CREATE",
580
+ "ALTER",
581
+ "DROP",
582
+ "TABLE",
583
+ "VIEW",
584
+ "FUNCTION",
585
+ "SCHEMA",
586
+ "DATABASE",
587
+ "AS",
588
+ "HAVING",
589
+ "ORDER",
590
+ "GROUP",
591
+ "DISTINCT",
592
+ "LIMIT",
593
+ "OFFSET",
594
+ "RETURNING",
595
+ "TRUNCATE",
596
+ "COPY",
597
+ "TYPE",
598
+ "COLUMN",
599
+ "VALUES"
600
+ ]);
601
+ if (knownKeywords.has(kw)) return kw;
602
+ }
603
+ if (trimmed.endsWith(",")) return ",";
604
+ if (trimmed.endsWith("=")) return "=";
605
+ if (trimmed.endsWith("::")) return "::";
606
+ return null;
607
+ }
608
+ function findPrevSignificantKeyword(text) {
609
+ const withoutLast = text.replace(/\S+\s*$/, "").trim();
610
+ return findLastKeyword(withoutLast);
611
+ }
612
+ function tableRef(t) {
613
+ return t.alias || t.name;
614
+ }
615
+
616
+ // src/metadata.ts
617
+ async function fetchMetadata(pool) {
618
+ const meta = {
619
+ tables: {},
620
+ views: {},
621
+ functions: {},
622
+ datatypes: {}
623
+ };
624
+ const [schemas, tables, views, tableCols, viewCols, functions, fks, datatypes] = await Promise.all([
625
+ fetchSchemas(pool),
626
+ fetchTables(pool),
627
+ fetchViews(pool),
628
+ fetchColumns(pool, ["r", "p", "f"]),
629
+ fetchColumns(pool, ["v", "m"]),
630
+ fetchFunctions(pool),
631
+ fetchForeignKeys(pool),
632
+ fetchDatatypes(pool)
633
+ ]);
634
+ for (const schema of schemas) {
635
+ meta.tables[schema] = meta.tables[schema] || {};
636
+ meta.views[schema] = meta.views[schema] || {};
637
+ meta.functions[schema] = meta.functions[schema] || {};
638
+ meta.datatypes[schema] = meta.datatypes[schema] || {};
639
+ }
640
+ for (const { schema, name } of tables) {
641
+ if (!meta.tables[schema]) meta.tables[schema] = {};
642
+ meta.tables[schema][name] = [];
643
+ }
644
+ for (const { schema, name } of views) {
645
+ if (!meta.views[schema]) meta.views[schema] = {};
646
+ meta.views[schema][name] = [];
647
+ }
648
+ for (const col of tableCols) {
649
+ const tbl = meta.tables[col.schema]?.[col.table];
650
+ if (tbl) {
651
+ tbl.push({
652
+ name: col.name,
653
+ datatype: col.datatype,
654
+ hasDefault: col.hasDefault,
655
+ default_: col.default_,
656
+ foreignKeys: []
657
+ });
658
+ }
659
+ }
660
+ for (const col of viewCols) {
661
+ const v = meta.views[col.schema]?.[col.view];
662
+ if (v) {
663
+ v.push({
664
+ name: col.name,
665
+ datatype: col.datatype,
666
+ hasDefault: col.hasDefault,
667
+ default_: col.default_,
668
+ foreignKeys: []
669
+ });
670
+ }
671
+ }
672
+ for (const f of functions) {
673
+ if (!meta.functions[f.schemaName]) meta.functions[f.schemaName] = {};
674
+ if (!meta.functions[f.schemaName][f.funcName]) {
675
+ meta.functions[f.schemaName][f.funcName] = [];
676
+ }
677
+ meta.functions[f.schemaName][f.funcName].push(f);
678
+ }
679
+ for (const fk of fks) {
680
+ const childCols = meta.tables[fk.childSchema]?.[fk.childTable];
681
+ if (childCols) {
682
+ const col = childCols.find((c) => c.name === fk.childColumn);
683
+ if (col) col.foreignKeys.push(fk);
684
+ }
685
+ const parentCols = meta.tables[fk.parentSchema]?.[fk.parentTable];
686
+ if (parentCols) {
687
+ const col = parentCols.find((c) => c.name === fk.parentColumn);
688
+ if (col) col.foreignKeys.push(fk);
689
+ }
690
+ }
691
+ for (const dt of datatypes) {
692
+ if (!meta.datatypes[dt.schema]) meta.datatypes[dt.schema] = {};
693
+ meta.datatypes[dt.schema][dt.name] = null;
694
+ }
695
+ return meta;
696
+ }
697
+ async function fetchSchemas(pool) {
698
+ const { rows } = await pool.query(
699
+ `SELECT nspname FROM pg_catalog.pg_namespace ORDER BY 1`
700
+ );
701
+ return rows.map((r) => r.nspname);
702
+ }
703
+ async function fetchTables(pool) {
704
+ const { rows } = await pool.query(
705
+ `SELECT n.nspname AS schema, c.relname AS name
706
+ FROM pg_catalog.pg_class c
707
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
708
+ WHERE c.relkind = ANY($1)
709
+ ORDER BY 1, 2`,
710
+ [["r", "p", "f"]]
711
+ );
712
+ return rows.map((r) => ({ schema: r.schema, name: r.name }));
713
+ }
714
+ async function fetchViews(pool) {
715
+ const { rows } = await pool.query(
716
+ `SELECT n.nspname AS schema, c.relname AS name
717
+ FROM pg_catalog.pg_class c
718
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
719
+ WHERE c.relkind = ANY($1)
720
+ ORDER BY 1, 2`,
721
+ [["v", "m"]]
722
+ );
723
+ return rows.map((r) => ({ schema: r.schema, name: r.name }));
724
+ }
725
+ async function fetchColumns(pool, kinds) {
726
+ const { rows } = await pool.query(
727
+ `SELECT nsp.nspname AS schema,
728
+ cls.relname AS table,
729
+ att.attname AS name,
730
+ att.atttypid::regtype::text AS datatype,
731
+ att.atthasdef AS "hasDefault",
732
+ pg_catalog.pg_get_expr(def.adbin, def.adrelid, true) AS "default_"
733
+ FROM pg_catalog.pg_attribute att
734
+ INNER JOIN pg_catalog.pg_class cls ON att.attrelid = cls.oid
735
+ INNER JOIN pg_catalog.pg_namespace nsp ON cls.relnamespace = nsp.oid
736
+ LEFT OUTER JOIN pg_attrdef def ON def.adrelid = att.attrelid AND def.adnum = att.attnum
737
+ WHERE cls.relkind = ANY($1)
738
+ AND NOT att.attisdropped
739
+ AND att.attnum > 0
740
+ ORDER BY 1, 2, att.attnum`,
741
+ [kinds]
742
+ );
743
+ return rows.map((r) => ({
744
+ schema: r.schema,
745
+ table: r.table,
746
+ view: r.table,
747
+ name: r.name,
748
+ datatype: r.datatype,
749
+ hasDefault: r.hasDefault,
750
+ default_: r.default_
751
+ }));
752
+ }
753
+ async function fetchFunctions(pool) {
754
+ const { rows } = await pool.query(
755
+ `SELECT n.nspname AS "schemaName",
756
+ p.proname AS "funcName",
757
+ p.proargnames AS "argNames",
758
+ COALESCE(proallargtypes::regtype[], proargtypes::regtype[])::text[] AS "argTypes",
759
+ p.proargmodes AS "argModes",
760
+ prorettype::regtype::text AS "returnType",
761
+ p.prokind = 'a' AS "isAggregate",
762
+ p.prokind = 'w' AS "isWindow",
763
+ p.proretset AS "isSetReturning",
764
+ d.deptype = 'e' AS "isExtension"
765
+ FROM pg_catalog.pg_proc p
766
+ INNER JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
767
+ LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e'
768
+ WHERE p.prorettype::regtype != 'trigger'::regtype
769
+ ORDER BY 1, 2`
770
+ );
771
+ return rows.map((r) => ({
772
+ ...r,
773
+ isPublic: r.schemaName === "public",
774
+ isExtension: !!r.isExtension
775
+ }));
776
+ }
777
+ async function fetchForeignKeys(pool) {
778
+ const { rows } = await pool.query(
779
+ `SELECT s_p.nspname AS "parentSchema",
780
+ t_p.relname AS "parentTable",
781
+ unnest((
782
+ SELECT array_agg(attname ORDER BY i)
783
+ FROM (SELECT unnest(confkey) AS attnum, generate_subscripts(confkey, 1) AS i) x
784
+ JOIN pg_catalog.pg_attribute c USING(attnum)
785
+ WHERE c.attrelid = fk.confrelid
786
+ )) AS "parentColumn",
787
+ s_c.nspname AS "childSchema",
788
+ t_c.relname AS "childTable",
789
+ unnest((
790
+ SELECT array_agg(attname ORDER BY i)
791
+ FROM (SELECT unnest(conkey) AS attnum, generate_subscripts(conkey, 1) AS i) x
792
+ JOIN pg_catalog.pg_attribute c USING(attnum)
793
+ WHERE c.attrelid = fk.conrelid
794
+ )) AS "childColumn"
795
+ FROM pg_catalog.pg_constraint fk
796
+ JOIN pg_catalog.pg_class t_p ON t_p.oid = fk.confrelid
797
+ JOIN pg_catalog.pg_namespace s_p ON s_p.oid = t_p.relnamespace
798
+ JOIN pg_catalog.pg_class t_c ON t_c.oid = fk.conrelid
799
+ JOIN pg_catalog.pg_namespace s_c ON s_c.oid = t_c.relnamespace
800
+ WHERE fk.contype = 'f'`
801
+ );
802
+ return rows;
803
+ }
804
+ async function fetchDatatypes(pool) {
805
+ const { rows } = await pool.query(
806
+ `SELECT n.nspname AS schema, t.typname AS name
807
+ FROM pg_catalog.pg_type t
808
+ INNER JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
809
+ WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))
810
+ AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)
811
+ AND n.nspname <> 'pg_catalog'
812
+ AND n.nspname <> 'information_schema'
813
+ ORDER BY 1, 2`
814
+ );
815
+ return rows;
816
+ }
817
+ async function fetchSearchPath(pool) {
818
+ const { rows } = await pool.query(
819
+ `SELECT * FROM unnest(current_schemas(true))`
820
+ );
821
+ return rows.map((r) => r.unnest);
822
+ }
823
+
824
+ // src/completer.ts
825
+ var PgCompleter = class {
826
+ pool = null;
827
+ metadata;
828
+ searchPath;
829
+ qualifyColumns;
830
+ connectionString;
831
+ constructor(connectionStringOrOpts) {
832
+ const opts = typeof connectionStringOrOpts === "string" ? { connectionString: connectionStringOrOpts } : connectionStringOrOpts || {};
833
+ this.connectionString = opts.connectionString;
834
+ this.metadata = opts.metadata || { tables: {}, views: {}, functions: {}, datatypes: {} };
835
+ this.searchPath = opts.searchPath || ["pg_catalog", "public"];
836
+ this.qualifyColumns = opts.qualifyColumns || "if_more_than_one_table";
837
+ }
838
+ /** Connect to the database and load schema metadata */
839
+ async refresh() {
840
+ if (!this.connectionString) {
841
+ throw new Error("No connection string provided");
842
+ }
843
+ this.pool = new Pool({ connectionString: this.connectionString, max: 2 });
844
+ try {
845
+ const [meta, sp] = await Promise.all([
846
+ fetchMetadata(this.pool),
847
+ fetchSearchPath(this.pool)
848
+ ]);
849
+ this.metadata = meta;
850
+ this.searchPath = sp;
851
+ } finally {
852
+ await this.pool.end();
853
+ this.pool = null;
854
+ }
855
+ }
856
+ /** Close any open connections */
857
+ async close() {
858
+ if (this.pool) {
859
+ await this.pool.end();
860
+ this.pool = null;
861
+ }
862
+ }
863
+ /** Set metadata directly (for testing or pre-loaded scenarios) */
864
+ setMetadata(meta, searchPath) {
865
+ this.metadata = meta;
866
+ if (searchPath) this.searchPath = searchPath;
867
+ }
868
+ /**
869
+ * Get completions for the given SQL text.
870
+ * @param text - The full SQL text typed so far
871
+ * @param cursorPos - Cursor position (defaults to end of text)
872
+ * @returns Array of completion suggestions
873
+ */
874
+ complete(text, cursorPos) {
875
+ const pos = cursorPos ?? text.length;
876
+ const textBeforeCursor = text.slice(0, pos);
877
+ const fullText = text;
878
+ const suggestions = suggestType(fullText, textBeforeCursor);
879
+ const completions = [];
880
+ for (const suggestion of suggestions) {
881
+ switch (suggestion.type) {
882
+ case "keyword":
883
+ completions.push(...this.getKeywordCompletions(suggestion.lastToken));
884
+ break;
885
+ case "table":
886
+ completions.push(...this.getTableCompletions(suggestion.schema));
887
+ break;
888
+ case "view":
889
+ completions.push(...this.getViewCompletions(suggestion.schema));
890
+ break;
891
+ case "column":
892
+ completions.push(...this.getColumnCompletions(
893
+ suggestion.tableRefs,
894
+ suggestion.qualifiable,
895
+ suggestion.parent
896
+ ));
897
+ break;
898
+ case "function":
899
+ completions.push(...this.getFunctionCompletions(suggestion.schema));
900
+ break;
901
+ case "schema":
902
+ completions.push(...this.getSchemaCompletions());
903
+ break;
904
+ case "from_clause":
905
+ completions.push(...this.getTableCompletions(suggestion.schema));
906
+ completions.push(...this.getViewCompletions(suggestion.schema));
907
+ completions.push(...this.getFunctionCompletions(suggestion.schema));
908
+ break;
909
+ case "join":
910
+ completions.push(...this.getJoinCompletions(suggestion.tableRefs, suggestion.schema));
911
+ break;
912
+ case "join_condition":
913
+ completions.push(...this.getJoinConditionCompletions(suggestion.tableRefs, suggestion.parent));
914
+ break;
915
+ case "alias":
916
+ completions.push(...suggestion.aliases.map((a) => ({
917
+ text: a,
918
+ type: "alias"
919
+ })));
920
+ break;
921
+ case "database":
922
+ break;
923
+ case "datatype":
924
+ completions.push(...this.getDatatypeCompletions(suggestion.schema));
925
+ break;
926
+ }
927
+ }
928
+ const wordMatch = textBeforeCursor.match(/([A-Za-z0-9_."$]+)$/);
929
+ const word = wordMatch ? wordMatch[1] : "";
930
+ const dotIdx = word.lastIndexOf(".");
931
+ const filterWord = dotIdx >= 0 ? word.slice(dotIdx + 1) : word;
932
+ if (!filterWord) return this.dedup(completions);
933
+ const lower = filterWord.toLowerCase().replace(/^"/, "");
934
+ const filtered = completions.filter((c) => {
935
+ const ct = c.text.toLowerCase();
936
+ return ct.startsWith(lower) || fuzzyMatch(lower, ct);
937
+ });
938
+ filtered.sort((a, b) => {
939
+ const aExact = a.text.toLowerCase().startsWith(lower) ? 0 : 1;
940
+ const bExact = b.text.toLowerCase().startsWith(lower) ? 0 : 1;
941
+ if (aExact !== bExact) return aExact - bExact;
942
+ return a.text.localeCompare(b.text);
943
+ });
944
+ return this.dedup(filtered);
945
+ }
946
+ dedup(completions) {
947
+ const seen = /* @__PURE__ */ new Set();
948
+ return completions.filter((c) => {
949
+ const key = `${c.type}:${c.text}`;
950
+ if (seen.has(key)) return false;
951
+ seen.add(key);
952
+ return true;
953
+ });
954
+ }
955
+ getKeywordCompletions(lastToken) {
956
+ let kws;
957
+ if (lastToken && KEYWORD_TREE[lastToken]) {
958
+ kws = KEYWORD_TREE[lastToken];
959
+ if (kws.length === 0) kws = KEYWORDS;
960
+ } else {
961
+ kws = KEYWORDS;
962
+ }
963
+ return kws.map((k) => ({ text: k, type: "keyword" }));
964
+ }
965
+ getSchemaCompletions() {
966
+ const schemas = /* @__PURE__ */ new Set();
967
+ for (const kind of ["tables", "views", "functions", "datatypes"]) {
968
+ for (const s of Object.keys(this.metadata[kind])) {
969
+ if (!s.startsWith("pg_")) schemas.add(s);
970
+ }
971
+ }
972
+ return [...schemas].map((s) => ({ text: s, type: "schema" }));
973
+ }
974
+ getTableCompletions(schema) {
975
+ const results = [];
976
+ const schemas = schema ? [schema] : this.getVisibleSchemas();
977
+ for (const s of schemas) {
978
+ const tables = this.metadata.tables[s];
979
+ if (!tables) continue;
980
+ for (const name of Object.keys(tables)) {
981
+ if (!schema && name.startsWith("pg_")) continue;
982
+ const prefix = !schema && !this.searchPath.includes(s) ? `${s}.` : "";
983
+ results.push({ text: prefix + name, type: "table" });
984
+ }
985
+ }
986
+ return results;
987
+ }
988
+ getViewCompletions(schema) {
989
+ const results = [];
990
+ const schemas = schema ? [schema] : this.getVisibleSchemas();
991
+ for (const s of schemas) {
992
+ const views = this.metadata.views[s];
993
+ if (!views) continue;
994
+ for (const name of Object.keys(views)) {
995
+ if (!schema && name.startsWith("pg_")) continue;
996
+ const prefix = !schema && !this.searchPath.includes(s) ? `${s}.` : "";
997
+ results.push({ text: prefix + name, type: "view" });
998
+ }
999
+ }
1000
+ return results;
1001
+ }
1002
+ getColumnCompletions(tableRefs, qualifiable, parent) {
1003
+ const results = [];
1004
+ const doQualify = qualifiable && {
1005
+ always: true,
1006
+ never: false,
1007
+ if_more_than_one_table: tableRefs.length > 1
1008
+ }[this.qualifyColumns];
1009
+ for (const tref of tableRefs) {
1010
+ const cols = this.findColumns(tref);
1011
+ const ref = tableRef(tref);
1012
+ for (const col of cols) {
1013
+ const text = doQualify ? `${ref}.${col.name}` : col.name;
1014
+ results.push({
1015
+ text,
1016
+ type: "column",
1017
+ meta: col.datatype
1018
+ });
1019
+ }
1020
+ }
1021
+ return results;
1022
+ }
1023
+ getFunctionCompletions(schema) {
1024
+ const results = [];
1025
+ const schemas = schema ? [schema] : this.getVisibleSchemas();
1026
+ for (const s of schemas) {
1027
+ const funcs = this.metadata.functions[s];
1028
+ if (!funcs) continue;
1029
+ for (const name of Object.keys(funcs)) {
1030
+ const prefix = schema || !this.searchPath.includes(s) ? `${s}.` : "";
1031
+ results.push({ text: prefix + name, type: "function" });
1032
+ }
1033
+ }
1034
+ if (!schema) {
1035
+ for (const f of BUILTIN_FUNCTIONS) {
1036
+ results.push({ text: f, type: "function" });
1037
+ }
1038
+ }
1039
+ return results;
1040
+ }
1041
+ getJoinCompletions(tableRefs, schema) {
1042
+ const results = [];
1043
+ for (const tref of tableRefs) {
1044
+ const cols = this.findColumns(tref);
1045
+ for (const col of cols) {
1046
+ for (const fk of col.foreignKeys) {
1047
+ const isChild = col.name === fk.childColumn && this.matchesTable(tref, fk.childSchema, fk.childTable);
1048
+ const otherSchema = isChild ? fk.parentSchema : fk.childSchema;
1049
+ const otherTable = isChild ? fk.parentTable : fk.childTable;
1050
+ const otherCol = isChild ? fk.parentColumn : fk.childColumn;
1051
+ const thisCol = isChild ? fk.childColumn : fk.parentColumn;
1052
+ if (schema && otherSchema !== schema) continue;
1053
+ const ref = tableRef(tref);
1054
+ const joinText = `${otherTable} ON ${otherTable}.${otherCol} = ${ref}.${thisCol}`;
1055
+ results.push({ text: joinText, type: "join", meta: "fk join" });
1056
+ }
1057
+ }
1058
+ }
1059
+ return results;
1060
+ }
1061
+ getJoinConditionCompletions(tableRefs, parent) {
1062
+ const results = [];
1063
+ if (tableRefs.length < 2) return results;
1064
+ const lastRef = tableRefs[tableRefs.length - 1];
1065
+ const lastCols = this.findColumns(lastRef);
1066
+ const lastRefName = tableRef(lastRef);
1067
+ for (const col of lastCols) {
1068
+ for (const fk of col.foreignKeys) {
1069
+ for (const otherRef of tableRefs.slice(0, -1)) {
1070
+ const otherRefName = tableRef(otherRef);
1071
+ const isChild = col.name === fk.childColumn && this.matchesTable(lastRef, fk.childSchema, fk.childTable);
1072
+ if (isChild) {
1073
+ if (this.matchesTable(otherRef, fk.parentSchema, fk.parentTable)) {
1074
+ results.push({
1075
+ text: `${lastRefName}.${fk.childColumn} = ${otherRefName}.${fk.parentColumn}`,
1076
+ type: "join",
1077
+ meta: "fk join"
1078
+ });
1079
+ }
1080
+ } else {
1081
+ if (this.matchesTable(otherRef, fk.childSchema, fk.childTable)) {
1082
+ results.push({
1083
+ text: `${lastRefName}.${fk.parentColumn} = ${otherRefName}.${fk.childColumn}`,
1084
+ type: "join",
1085
+ meta: "fk join"
1086
+ });
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+ }
1092
+ for (const col of lastCols) {
1093
+ for (const otherRef of tableRefs.slice(0, -1)) {
1094
+ const otherCols = this.findColumns(otherRef);
1095
+ const match = otherCols.find((c) => c.name === col.name);
1096
+ if (match) {
1097
+ const otherRefName = tableRef(otherRef);
1098
+ const text = `${lastRefName}.${col.name} = ${otherRefName}.${col.name}`;
1099
+ if (!results.some((r) => r.text === text)) {
1100
+ results.push({ text, type: "join", meta: "name join" });
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ return results;
1106
+ }
1107
+ getDatatypeCompletions(schema) {
1108
+ const results = [];
1109
+ const schemas = schema ? [schema] : this.getVisibleSchemas();
1110
+ for (const s of schemas) {
1111
+ const types = this.metadata.datatypes[s];
1112
+ if (!types) continue;
1113
+ for (const name of Object.keys(types)) {
1114
+ results.push({ text: name, type: "datatype" });
1115
+ }
1116
+ }
1117
+ if (!schema) {
1118
+ for (const t of [
1119
+ "integer",
1120
+ "bigint",
1121
+ "smallint",
1122
+ "serial",
1123
+ "bigserial",
1124
+ "real",
1125
+ "double precision",
1126
+ "numeric",
1127
+ "decimal",
1128
+ "text",
1129
+ "varchar",
1130
+ "char",
1131
+ "boolean",
1132
+ "bool",
1133
+ "date",
1134
+ "time",
1135
+ "timestamp",
1136
+ "timestamptz",
1137
+ "interval",
1138
+ "json",
1139
+ "jsonb",
1140
+ "uuid",
1141
+ "bytea",
1142
+ "xml"
1143
+ ]) {
1144
+ results.push({ text: t, type: "datatype" });
1145
+ }
1146
+ }
1147
+ return results;
1148
+ }
1149
+ /** Find columns for a table reference, searching through schemas */
1150
+ findColumns(tref) {
1151
+ const schemas = tref.schema ? [tref.schema] : this.searchPath;
1152
+ for (const s of schemas) {
1153
+ const cols = this.metadata.tables[s]?.[tref.name] || this.metadata.views[s]?.[tref.name];
1154
+ if (cols) return cols;
1155
+ }
1156
+ return [];
1157
+ }
1158
+ matchesTable(tref, schema, table) {
1159
+ if (tref.schema) return tref.schema === schema && tref.name === table;
1160
+ return tref.name === table && this.searchPath.includes(schema);
1161
+ }
1162
+ getVisibleSchemas() {
1163
+ const all = new Set(this.searchPath);
1164
+ for (const kind of ["tables", "views", "functions", "datatypes"]) {
1165
+ for (const s of Object.keys(this.metadata[kind])) {
1166
+ all.add(s);
1167
+ }
1168
+ }
1169
+ return [...all];
1170
+ }
1171
+ };
1172
+ function fuzzyMatch(pattern, text) {
1173
+ let pi = 0;
1174
+ for (let ti = 0; ti < text.length && pi < pattern.length; ti++) {
1175
+ if (text[ti] === pattern[pi]) pi++;
1176
+ }
1177
+ return pi === pattern.length;
1178
+ }
1179
+ export {
1180
+ BUILTIN_FUNCTIONS,
1181
+ KEYWORDS,
1182
+ KEYWORD_TREE,
1183
+ PgCompleter,
1184
+ extractTables,
1185
+ suggestType
1186
+ };
1187
+ //# sourceMappingURL=index.mjs.map