@coldsmirk/inkstone-sql 0.13.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 +68 -0
- package/dist/assist-gLTZKSD3.d.ts +401 -0
- package/dist/index.d.ts +380 -0
- package/dist/index.js +1531 -0
- package/dist/monaco.d.ts +31 -0
- package/dist/monaco.js +128 -0
- package/dist/react.d.ts +158 -0
- package/dist/react.js +135 -0
- package/package.json +81 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
//#region src/dialect.ts
|
|
2
|
+
function fn(name, signature, summary) {
|
|
3
|
+
return {
|
|
4
|
+
name,
|
|
5
|
+
signature,
|
|
6
|
+
summary
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
const CORE_KEYWORDS = [
|
|
10
|
+
"select",
|
|
11
|
+
"from",
|
|
12
|
+
"where",
|
|
13
|
+
"join",
|
|
14
|
+
"inner",
|
|
15
|
+
"left",
|
|
16
|
+
"right",
|
|
17
|
+
"full",
|
|
18
|
+
"outer",
|
|
19
|
+
"cross",
|
|
20
|
+
"on",
|
|
21
|
+
"as",
|
|
22
|
+
"and",
|
|
23
|
+
"or",
|
|
24
|
+
"not",
|
|
25
|
+
"in",
|
|
26
|
+
"exists",
|
|
27
|
+
"between",
|
|
28
|
+
"like",
|
|
29
|
+
"is",
|
|
30
|
+
"null",
|
|
31
|
+
"distinct",
|
|
32
|
+
"group",
|
|
33
|
+
"by",
|
|
34
|
+
"having",
|
|
35
|
+
"order",
|
|
36
|
+
"asc",
|
|
37
|
+
"desc",
|
|
38
|
+
"union",
|
|
39
|
+
"all",
|
|
40
|
+
"insert",
|
|
41
|
+
"into",
|
|
42
|
+
"values",
|
|
43
|
+
"update",
|
|
44
|
+
"set",
|
|
45
|
+
"delete",
|
|
46
|
+
"case",
|
|
47
|
+
"when",
|
|
48
|
+
"then",
|
|
49
|
+
"else",
|
|
50
|
+
"end",
|
|
51
|
+
"cast",
|
|
52
|
+
"with",
|
|
53
|
+
"using"
|
|
54
|
+
];
|
|
55
|
+
const CORE_FUNCTIONS = [
|
|
56
|
+
fn("count", "count(expr)", "Rows in the group; `count(*)` counts every row, `count(column)` skips NULLs."),
|
|
57
|
+
fn("sum", "sum(expr)", "Adds the values, ignoring NULLs."),
|
|
58
|
+
fn("avg", "avg(expr)", "Mean of the values, ignoring NULLs."),
|
|
59
|
+
fn("min", "min(expr)", "Smallest value in the group."),
|
|
60
|
+
fn("max", "max(expr)", "Largest value in the group."),
|
|
61
|
+
fn("coalesce", "coalesce(value, fallback, …)", "The first argument that is not NULL."),
|
|
62
|
+
fn("nullif", "nullif(value, equal_to)", "NULL when the two are equal, otherwise the first argument."),
|
|
63
|
+
fn("abs", "abs(number)", "Absolute value."),
|
|
64
|
+
fn("round", "round(number, digits)", "Rounds to the given number of decimal places."),
|
|
65
|
+
fn("upper", "upper(text)", "Upper case."),
|
|
66
|
+
fn("lower", "lower(text)", "Lower case."),
|
|
67
|
+
fn("trim", "trim(text)", "Strips leading and trailing whitespace."),
|
|
68
|
+
fn("row_number", "row_number()", "Window: a running number over `over (partition by … order by …)`."),
|
|
69
|
+
fn("rank", "rank()", "Window: ties share a rank, and the next rank skips."),
|
|
70
|
+
fn("dense_rank", "dense_rank()", "Window: ties share a rank, and the next rank does not skip.")
|
|
71
|
+
];
|
|
72
|
+
const GREATEST = fn("greatest", "greatest(value, …)", "The largest of the arguments.");
|
|
73
|
+
const LEAST = fn("least", "least(value, …)", "The smallest of the arguments.");
|
|
74
|
+
const SUBSTRING = fn("substring", "substring(text, start, count)", "`count` characters from position `start`, counting from 1.");
|
|
75
|
+
const TO_CHAR = fn("to_char", "to_char(value, format)", "Formats a timestamp or a number through a format string.");
|
|
76
|
+
const TO_DATE = fn("to_date", "to_date(text, format)", "Parses text into a date through a format string.");
|
|
77
|
+
const EXTRACT = fn("extract", "extract(field from source)", "One field out of a temporal value, as in `extract(year from created_at)`.");
|
|
78
|
+
function dialect(keywords, functions) {
|
|
79
|
+
return {
|
|
80
|
+
keywords: [...CORE_KEYWORDS, ...keywords],
|
|
81
|
+
functions: [...CORE_FUNCTIONS, ...functions]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const ORACLE$1 = dialect([
|
|
85
|
+
"dual",
|
|
86
|
+
"rownum",
|
|
87
|
+
"minus",
|
|
88
|
+
"connect",
|
|
89
|
+
"start",
|
|
90
|
+
"prior",
|
|
91
|
+
"fetch",
|
|
92
|
+
"first",
|
|
93
|
+
"next",
|
|
94
|
+
"rows",
|
|
95
|
+
"only",
|
|
96
|
+
"merge",
|
|
97
|
+
"matched",
|
|
98
|
+
"returning",
|
|
99
|
+
"sysdate",
|
|
100
|
+
"systimestamp"
|
|
101
|
+
], [
|
|
102
|
+
fn("nvl", "nvl(value, fallback)", "The second argument when the first is NULL."),
|
|
103
|
+
fn("nvl2", "nvl2(value, if_not_null, if_null)", "Chooses by whether the first argument is NULL."),
|
|
104
|
+
fn("decode", "decode(expr, search, result, …, default)", "Compares pair by pair — a `case` in shorthand."),
|
|
105
|
+
TO_CHAR,
|
|
106
|
+
TO_DATE,
|
|
107
|
+
fn("to_number", "to_number(text, format)", "Parses text into a number through a format string."),
|
|
108
|
+
fn("trunc", "trunc(value, unit)", "Truncates: a date to a precision, a number to decimal places."),
|
|
109
|
+
EXTRACT,
|
|
110
|
+
fn("substr", "substr(text, start, count)", "`count` characters from position `start`; a negative `start` counts from the end."),
|
|
111
|
+
fn("instr", "instr(text, search)", "Position of a substring, or 0 when it is absent."),
|
|
112
|
+
fn("length", "length(text)", "Characters in the string."),
|
|
113
|
+
fn("concat", "concat(text, text)", "Joins two strings; use `||` for more."),
|
|
114
|
+
GREATEST,
|
|
115
|
+
LEAST,
|
|
116
|
+
fn("listagg", "listagg(expr, delimiter)", "Aggregate: joins with a delimiter; needs `within group (order by …)`.")
|
|
117
|
+
]);
|
|
118
|
+
const SQL_DIALECTS = {
|
|
119
|
+
postgres: dialect([
|
|
120
|
+
"limit",
|
|
121
|
+
"offset",
|
|
122
|
+
"returning",
|
|
123
|
+
"ilike",
|
|
124
|
+
"lateral",
|
|
125
|
+
"conflict",
|
|
126
|
+
"do",
|
|
127
|
+
"nothing",
|
|
128
|
+
"recursive",
|
|
129
|
+
"fetch",
|
|
130
|
+
"first",
|
|
131
|
+
"next",
|
|
132
|
+
"rows",
|
|
133
|
+
"only",
|
|
134
|
+
"true",
|
|
135
|
+
"false",
|
|
136
|
+
"current_date",
|
|
137
|
+
"current_timestamp"
|
|
138
|
+
], [
|
|
139
|
+
fn("now", "now()", "Current date and time."),
|
|
140
|
+
TO_CHAR,
|
|
141
|
+
TO_DATE,
|
|
142
|
+
fn("to_timestamp", "to_timestamp(text, format)", "Parses text into a timestamp through a format string."),
|
|
143
|
+
EXTRACT,
|
|
144
|
+
fn("date_trunc", "date_trunc(field, source)", "Truncates a timestamp to a precision, as in `date_trunc('day', created_at)`."),
|
|
145
|
+
fn("concat", "concat(value, …)", "Joins the arguments as text, treating NULL as an empty string."),
|
|
146
|
+
SUBSTRING,
|
|
147
|
+
fn("length", "length(text)", "Characters in the string."),
|
|
148
|
+
GREATEST,
|
|
149
|
+
LEAST,
|
|
150
|
+
fn("string_agg", "string_agg(expr, delimiter)", "Aggregate: joins each row's value with a delimiter."),
|
|
151
|
+
fn("array_agg", "array_agg(expr)", "Aggregate: collects each row's value into an array."),
|
|
152
|
+
fn("jsonb_agg", "jsonb_agg(expr)", "Aggregate: collects each row's value into a JSON array."),
|
|
153
|
+
fn("jsonb_build_object", "jsonb_build_object(key, value, …)", "Builds a JSON object from alternating key and value arguments.")
|
|
154
|
+
]),
|
|
155
|
+
mysql: dialect([
|
|
156
|
+
"limit",
|
|
157
|
+
"offset",
|
|
158
|
+
"ignore",
|
|
159
|
+
"replace",
|
|
160
|
+
"duplicate",
|
|
161
|
+
"key",
|
|
162
|
+
"interval",
|
|
163
|
+
"straight_join",
|
|
164
|
+
"true",
|
|
165
|
+
"false"
|
|
166
|
+
], [
|
|
167
|
+
fn("now", "now()", "Current date and time."),
|
|
168
|
+
fn("curdate", "curdate()", "Current date."),
|
|
169
|
+
fn("curtime", "curtime()", "Current time."),
|
|
170
|
+
fn("ifnull", "ifnull(value, fallback)", "The second argument when the first is NULL."),
|
|
171
|
+
fn("if", "if(condition, then, else)", "Ternary choice."),
|
|
172
|
+
fn("date_format", "date_format(date, format)", "Formats a date through a format string."),
|
|
173
|
+
fn("str_to_date", "str_to_date(text, format)", "Parses text into a date through a format string."),
|
|
174
|
+
fn("date_add", "date_add(date, interval n unit)", "Adds an interval, as in `date_add(d, interval 7 day)`."),
|
|
175
|
+
fn("date_sub", "date_sub(date, interval n unit)", "Subtracts an interval."),
|
|
176
|
+
fn("concat", "concat(value, …)", "Joins the arguments as text; NULL in any of them makes the whole result NULL."),
|
|
177
|
+
fn("concat_ws", "concat_ws(separator, value, …)", "Joins with a separator, skipping NULLs."),
|
|
178
|
+
SUBSTRING,
|
|
179
|
+
fn("length", "length(text)", "Bytes in the string; `char_length` counts characters."),
|
|
180
|
+
GREATEST,
|
|
181
|
+
LEAST,
|
|
182
|
+
fn("group_concat", "group_concat(expr)", "Aggregate: joins each row's value into one string; takes `separator '…'`."),
|
|
183
|
+
fn("json_extract", "json_extract(json, path)", "Reads a JSON path, as in `'$.id'`.")
|
|
184
|
+
]),
|
|
185
|
+
oracle: ORACLE$1,
|
|
186
|
+
sqlserver: dialect([
|
|
187
|
+
"top",
|
|
188
|
+
"offset",
|
|
189
|
+
"fetch",
|
|
190
|
+
"next",
|
|
191
|
+
"rows",
|
|
192
|
+
"only",
|
|
193
|
+
"apply",
|
|
194
|
+
"output",
|
|
195
|
+
"inserted",
|
|
196
|
+
"deleted",
|
|
197
|
+
"merge",
|
|
198
|
+
"matched",
|
|
199
|
+
"pivot",
|
|
200
|
+
"unpivot"
|
|
201
|
+
], [
|
|
202
|
+
fn("isnull", "isnull(value, fallback)", "The second argument when the first is NULL."),
|
|
203
|
+
fn("iif", "iif(condition, then, else)", "Ternary choice."),
|
|
204
|
+
fn("getdate", "getdate()", "Current date and time."),
|
|
205
|
+
fn("sysdatetime", "sysdatetime()", "Current date and time, at higher precision."),
|
|
206
|
+
fn("dateadd", "dateadd(datepart, number, date)", "Adds to a date part, as in `dateadd(day, 7, d)`."),
|
|
207
|
+
fn("datediff", "datediff(datepart, start, end)", "Whole date parts between two times."),
|
|
208
|
+
fn("datepart", "datepart(datepart, date)", "One part of a date, as an integer."),
|
|
209
|
+
fn("datename", "datename(datepart, date)", "One part of a date, as text."),
|
|
210
|
+
fn("convert", "convert(type, value, style)", "Converts a type through a style number."),
|
|
211
|
+
fn("try_convert", "try_convert(type, value, style)", "Like `convert`, but NULL instead of an error."),
|
|
212
|
+
fn("format", "format(value, format, culture)", "Formats through a .NET format string."),
|
|
213
|
+
fn("concat", "concat(value, …)", "Joins the arguments as text, treating NULL as an empty string."),
|
|
214
|
+
SUBSTRING,
|
|
215
|
+
fn("charindex", "charindex(search, text, start)", "Position of a substring, or 0 when it is absent."),
|
|
216
|
+
fn("len", "len(text)", "Characters in the string, trailing spaces excluded."),
|
|
217
|
+
fn("ltrim", "ltrim(text)", "Strips leading whitespace."),
|
|
218
|
+
fn("rtrim", "rtrim(text)", "Strips trailing whitespace."),
|
|
219
|
+
fn("string_agg", "string_agg(expr, delimiter)", "Aggregate: joins each row's value with a delimiter.")
|
|
220
|
+
]),
|
|
221
|
+
dm: ORACLE$1
|
|
222
|
+
};
|
|
223
|
+
function signatureParameters(signature) {
|
|
224
|
+
const open = signature.indexOf("(");
|
|
225
|
+
const close = signature.lastIndexOf(")");
|
|
226
|
+
if (open === -1 || close < open) return [];
|
|
227
|
+
const spans = [];
|
|
228
|
+
let depth = 0;
|
|
229
|
+
let start = open + 1;
|
|
230
|
+
const push = (end) => {
|
|
231
|
+
const text = signature.slice(start, end);
|
|
232
|
+
const lead = text.length - text.trimStart().length;
|
|
233
|
+
const tail = text.length - text.trimEnd().length;
|
|
234
|
+
if (text.trim() !== "") spans.push([start + lead, end - tail]);
|
|
235
|
+
};
|
|
236
|
+
for (let at = open + 1; at < close; at += 1) {
|
|
237
|
+
const ch = signature[at];
|
|
238
|
+
if (ch === "(") depth += 1;
|
|
239
|
+
else if (ch === ")") depth -= 1;
|
|
240
|
+
else if (ch === "," && depth === 0) {
|
|
241
|
+
push(at);
|
|
242
|
+
start = at + 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
push(close);
|
|
246
|
+
return spans;
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/spelling.ts
|
|
250
|
+
function reservedWords(list) {
|
|
251
|
+
return new Set(list.trim().split(/\s+/u));
|
|
252
|
+
}
|
|
253
|
+
const POSTGRES_RESERVED = reservedWords(`
|
|
254
|
+
all analyse analyze and any array as asc asymmetric authorization between bigint binary bit
|
|
255
|
+
boolean both case cast char character check coalesce collate collation column concurrently
|
|
256
|
+
constraint create cross current_catalog current_date current_role current_schema current_time
|
|
257
|
+
current_timestamp current_user dec decimal default deferrable desc distinct do else end except
|
|
258
|
+
exists extract false fetch float for foreign freeze from full grant greatest group grouping
|
|
259
|
+
having ilike in initially inner inout int integer intersect interval into is isnull join
|
|
260
|
+
lateral leading least left like limit localtime localtimestamp national natural nchar none
|
|
261
|
+
normalize not notnull null nullif numeric offset on only or order out outer overlaps overlay
|
|
262
|
+
placing position precision primary real references returning right row select session_user
|
|
263
|
+
setof similar smallint some substring symmetric system_user table tablesample then time
|
|
264
|
+
timestamp to trailing treat trim true union unique user using values varchar variadic verbose
|
|
265
|
+
when where window with xmlattributes xmlconcat xmlelement xmlexists xmlforest xmlnamespaces
|
|
266
|
+
xmlparse xmlpi xmlroot xmlserialize xmltable
|
|
267
|
+
`);
|
|
268
|
+
const MYSQL_RESERVED = reservedWords(`
|
|
269
|
+
accessible add all alter analyze and as asc asensitive before between bigint binary blob both
|
|
270
|
+
by call cascade case change char character check collate column condition constraint continue
|
|
271
|
+
convert create cross cube cume_dist current_date current_time current_timestamp current_user
|
|
272
|
+
cursor database databases day_hour day_microsecond day_minute day_second dec decimal declare
|
|
273
|
+
default delayed delete dense_rank desc describe deterministic distinct distinctrow div double
|
|
274
|
+
drop dual each else elseif empty enclosed escaped except exists exit explain false fetch
|
|
275
|
+
first_value float float4 float8 for force foreign from fulltext function generated get grant
|
|
276
|
+
group grouping groups having high_priority hour_microsecond hour_minute hour_second if ignore
|
|
277
|
+
in index infile inner inout insensitive insert int int1 int2 int3 int4 int8 integer intersect
|
|
278
|
+
interval into io_after_gtids io_before_gtids is iterate join json_table key keys kill lag
|
|
279
|
+
last_value lateral lead leading leave left like limit linear lines load localtime
|
|
280
|
+
localtimestamp lock long longblob longtext loop low_priority master_bind match maxvalue
|
|
281
|
+
mediumblob mediumint mediumtext middleint minute_microsecond minute_second mod modifies
|
|
282
|
+
natural no_write_to_binlog not nth_value ntile null numeric of on optimize optimizer_costs
|
|
283
|
+
option optionally or order out outer outfile over partition percent_rank precision primary
|
|
284
|
+
procedure purge qualify range rank read read_write reads real recursive references regexp
|
|
285
|
+
release rename repeat
|
|
286
|
+
replace require resignal restrict return revoke right rlike row row_number rows schema
|
|
287
|
+
schemas second_microsecond select sensitive separator set show signal smallint spatial
|
|
288
|
+
specific sql sql_big_result sql_calc_found_rows sql_small_result sqlexception sqlstate
|
|
289
|
+
sqlwarning ssl starting stored straight_join system table tablesample terminated then tinyblob tinyint
|
|
290
|
+
tinytext to trailing trigger true undo union unique unlock unsigned update usage use using
|
|
291
|
+
utc_date utc_time utc_timestamp values varbinary varchar varcharacter varying virtual when
|
|
292
|
+
where while window with write xor year_month zerofill
|
|
293
|
+
`);
|
|
294
|
+
const ORACLE_RESERVED = reservedWords(`
|
|
295
|
+
access add all alter and any as asc audit between by char check cluster column comment
|
|
296
|
+
compress connect create current date decimal default delete desc distinct drop else exclusive
|
|
297
|
+
exists file float for from grant group having identified immediate in increment index initial
|
|
298
|
+
insert integer intersect into is level like lock long maxextents minus mlslabel mode modify
|
|
299
|
+
noaudit nocompress not nowait null number of offline on online option or order pctfree prior
|
|
300
|
+
privileges public raw rename resource revoke row rowid rownum rows select session set share
|
|
301
|
+
size smallint start successful synonym sysdate table then to trigger uid union unique update
|
|
302
|
+
user validate values varchar varchar2 view whenever where with
|
|
303
|
+
`);
|
|
304
|
+
const TSQL_RESERVED = reservedWords(`
|
|
305
|
+
add all alter and any as asc authorization backup begin between break browse bulk by cascade
|
|
306
|
+
case check checkpoint close clustered coalesce collate column commit compute constraint
|
|
307
|
+
contains containstable continue convert create cross current current_date current_time
|
|
308
|
+
current_timestamp current_user cursor database dbcc deallocate declare default delete deny
|
|
309
|
+
desc disk distinct distributed double drop dump else end errlvl escape except exec execute
|
|
310
|
+
exists exit external fetch file fillfactor for foreign freetext freetexttable from full
|
|
311
|
+
function goto grant group having holdlock identity identity_insert identitycol if image in
|
|
312
|
+
index inner insert intersect into is join key kill left like lineno load merge national
|
|
313
|
+
nocheck nonclustered not ntext null nullif of off offsets on open opendatasource openquery
|
|
314
|
+
openrowset openxml option or order outer over percent pivot plan precision primary print proc
|
|
315
|
+
procedure public raiserror read readtext reconfigure references replication restore restrict
|
|
316
|
+
return revert revoke right rollback rowcount rowguidcol rule save schema securityaudit select
|
|
317
|
+
semantickeyphrasetable semanticsimilaritydetailstable semanticsimilaritytable session_user
|
|
318
|
+
set setuser shutdown some statistics system_user table tablesample text textsize then to top
|
|
319
|
+
tran transaction trigger truncate try_convert tsequal union unique unpivot update updatetext
|
|
320
|
+
use user values varying view waitfor when where while with within writetext
|
|
321
|
+
`);
|
|
322
|
+
const ORACLE = {
|
|
323
|
+
open: "\"",
|
|
324
|
+
close: "\"",
|
|
325
|
+
regular: /^[A-Z][A-Z0-9_$#]*$/,
|
|
326
|
+
reserved: ORACLE_RESERVED
|
|
327
|
+
};
|
|
328
|
+
const SPELLINGS = {
|
|
329
|
+
postgres: {
|
|
330
|
+
open: "\"",
|
|
331
|
+
close: "\"",
|
|
332
|
+
regular: /^[a-z_][a-z0-9_$]*$/,
|
|
333
|
+
reserved: POSTGRES_RESERVED
|
|
334
|
+
},
|
|
335
|
+
mysql: {
|
|
336
|
+
open: "`",
|
|
337
|
+
close: "`",
|
|
338
|
+
regular: /^(?!\d+$)[\w$\u{0080}-\u{FFFF}]+$/u,
|
|
339
|
+
reserved: MYSQL_RESERVED
|
|
340
|
+
},
|
|
341
|
+
oracle: ORACLE,
|
|
342
|
+
sqlserver: {
|
|
343
|
+
open: "[",
|
|
344
|
+
close: "]",
|
|
345
|
+
regular: /^[a-z_@#][\w@#$]*$/i,
|
|
346
|
+
reserved: TSQL_RESERVED
|
|
347
|
+
},
|
|
348
|
+
dm: ORACLE
|
|
349
|
+
};
|
|
350
|
+
function delimited(spelling, name) {
|
|
351
|
+
return `${spelling.open}${name.replaceAll(spelling.close, () => spelling.close + spelling.close)}${spelling.close}`;
|
|
352
|
+
}
|
|
353
|
+
function spellIdentifier(kind, name) {
|
|
354
|
+
const spelling = SPELLINGS[kind];
|
|
355
|
+
if (spelling.regular.test(name) && !spelling.reserved.has(name.toLowerCase())) return name;
|
|
356
|
+
return delimited(spelling, name);
|
|
357
|
+
}
|
|
358
|
+
function spellCallable(kind, name) {
|
|
359
|
+
const spelling = SPELLINGS[kind];
|
|
360
|
+
return spelling.regular.test(name) ? name : delimited(spelling, name);
|
|
361
|
+
}
|
|
362
|
+
function spellQualifier(kind, source) {
|
|
363
|
+
return source.aliasWritten === "" ? spellIdentifier(kind, source.table.name) : source.aliasWritten;
|
|
364
|
+
}
|
|
365
|
+
function spellQualifiedName(kind, table) {
|
|
366
|
+
const name = spellIdentifier(kind, table.name);
|
|
367
|
+
return table.schema === "" ? name : `${spellIdentifier(kind, table.schema)}.${name}`;
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/joins.ts
|
|
371
|
+
const BY_NAMED_KEY = "named-foreign-key";
|
|
372
|
+
const BY_SHARED_KEY = "shared-key";
|
|
373
|
+
function singular(name) {
|
|
374
|
+
if (name.endsWith("ies")) return `${name.slice(0, -3)}y`;
|
|
375
|
+
return name.endsWith("s") && !name.endsWith("ss") ? name.slice(0, -1) : name;
|
|
376
|
+
}
|
|
377
|
+
function pointsAt(from, to) {
|
|
378
|
+
const base = to.table.name.toLowerCase();
|
|
379
|
+
const prefixes = [.../* @__PURE__ */ new Set([`${singular(base)}_`, `${base}_`])];
|
|
380
|
+
return from.columns.flatMap((column) => {
|
|
381
|
+
const name = column.name.toLowerCase();
|
|
382
|
+
const prefix = prefixes.find((one) => name.startsWith(one));
|
|
383
|
+
if (prefix === void 0) return [];
|
|
384
|
+
const rest = name.slice(prefix.length);
|
|
385
|
+
const key = to.columns.find((entry) => entry.name.toLowerCase() === rest && (entry.primaryKey || entry.unique));
|
|
386
|
+
return key === void 0 ? [] : [[column, key]];
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function sharedKey(here, there) {
|
|
390
|
+
return (here.primaryKey || here.unique) && !there.primaryKey || (there.primaryKey || there.unique) && !here.primaryKey || here.name.toLowerCase().endsWith("_id");
|
|
391
|
+
}
|
|
392
|
+
function joinGuesses(kind, target, others) {
|
|
393
|
+
const found = /* @__PURE__ */ new Map();
|
|
394
|
+
const add = (left, leftColumn, right, rightColumn, why) => {
|
|
395
|
+
const text = `${spellQualifier(kind, left)}.${spellIdentifier(kind, leftColumn)} = ${spellQualifier(kind, right)}.${spellIdentifier(kind, rightColumn)}`;
|
|
396
|
+
if (!found.has(text)) found.set(text, {
|
|
397
|
+
text,
|
|
398
|
+
why
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
for (const other of others) {
|
|
402
|
+
for (const [column, key] of pointsAt(target, other)) add(target, column.name, other, key.name, BY_NAMED_KEY);
|
|
403
|
+
for (const [column, key] of pointsAt(other, target)) add(other, column.name, target, key.name, BY_NAMED_KEY);
|
|
404
|
+
}
|
|
405
|
+
for (const other of others) for (const column of target.columns) {
|
|
406
|
+
const twin = other.columns.find((entry) => entry.name.toLowerCase() === column.name.toLowerCase());
|
|
407
|
+
if (twin !== void 0 && sharedKey(column, twin)) add(target, column.name, other, twin.name, BY_SHARED_KEY);
|
|
408
|
+
}
|
|
409
|
+
return [...found.values()];
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/noise.ts
|
|
413
|
+
const BASE = {
|
|
414
|
+
backslashEscapes: false,
|
|
415
|
+
doubleQuotedStrings: false,
|
|
416
|
+
hashLineComments: false,
|
|
417
|
+
lineCommentNeedsSpace: false,
|
|
418
|
+
backtickIdentifiers: false,
|
|
419
|
+
bracketIdentifiers: false,
|
|
420
|
+
dollarQuoting: false,
|
|
421
|
+
escapeStrings: false,
|
|
422
|
+
nestedBlockComments: false,
|
|
423
|
+
alternativeQuoting: false
|
|
424
|
+
};
|
|
425
|
+
const NOISE_PROFILES = {
|
|
426
|
+
postgres: {
|
|
427
|
+
...BASE,
|
|
428
|
+
escapeStrings: true,
|
|
429
|
+
nestedBlockComments: true,
|
|
430
|
+
dollarQuoting: true
|
|
431
|
+
},
|
|
432
|
+
mysql: {
|
|
433
|
+
...BASE,
|
|
434
|
+
backslashEscapes: true,
|
|
435
|
+
doubleQuotedStrings: true,
|
|
436
|
+
hashLineComments: true,
|
|
437
|
+
lineCommentNeedsSpace: true,
|
|
438
|
+
backtickIdentifiers: true
|
|
439
|
+
},
|
|
440
|
+
oracle: {
|
|
441
|
+
...BASE,
|
|
442
|
+
alternativeQuoting: true
|
|
443
|
+
},
|
|
444
|
+
sqlserver: {
|
|
445
|
+
...BASE,
|
|
446
|
+
bracketIdentifiers: true
|
|
447
|
+
},
|
|
448
|
+
dm: BASE
|
|
449
|
+
};
|
|
450
|
+
const WORD_START = /[a-z_\u{0080}-\u{FFFF}]/iu;
|
|
451
|
+
const WORD_BYTE = /[\w$\u{0080}-\u{FFFF}]/u;
|
|
452
|
+
const NAME_BYTE = /\w/;
|
|
453
|
+
const ALTERNATIVE_QUOTE_CLOSE = {
|
|
454
|
+
"[": "]",
|
|
455
|
+
"{": "}",
|
|
456
|
+
"(": ")",
|
|
457
|
+
"<": ">"
|
|
458
|
+
};
|
|
459
|
+
function closeDelimited(text, from, delimiter, backslash) {
|
|
460
|
+
let at = from;
|
|
461
|
+
while (at < text.length) if (backslash && text[at] === "\\" && at + 1 < text.length) at += 2;
|
|
462
|
+
else if (text[at] === delimiter) if (text[at + 1] === delimiter) at += 2;
|
|
463
|
+
else return {
|
|
464
|
+
end: at + 1,
|
|
465
|
+
sealed: true
|
|
466
|
+
};
|
|
467
|
+
else at += 1;
|
|
468
|
+
return {
|
|
469
|
+
end: text.length,
|
|
470
|
+
sealed: false
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function closeBracket(text, from) {
|
|
474
|
+
let at = from;
|
|
475
|
+
while (at < text.length) if (text[at] === "]") if (text[at + 1] === "]") at += 2;
|
|
476
|
+
else return {
|
|
477
|
+
end: at + 1,
|
|
478
|
+
sealed: true
|
|
479
|
+
};
|
|
480
|
+
else at += 1;
|
|
481
|
+
return {
|
|
482
|
+
end: text.length,
|
|
483
|
+
sealed: false
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function closeLine(text, from) {
|
|
487
|
+
const newline = text.indexOf("\n", from);
|
|
488
|
+
return {
|
|
489
|
+
end: newline === -1 ? text.length : newline,
|
|
490
|
+
sealed: false
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function closeBlock(text, from, nested) {
|
|
494
|
+
let depth = 1;
|
|
495
|
+
let at = from;
|
|
496
|
+
while (at + 1 < text.length) if (text[at] === "*" && text[at + 1] === "/") {
|
|
497
|
+
depth -= 1;
|
|
498
|
+
at += 2;
|
|
499
|
+
if (depth === 0) return {
|
|
500
|
+
end: at,
|
|
501
|
+
sealed: true
|
|
502
|
+
};
|
|
503
|
+
} else if (nested && text[at] === "/" && text[at + 1] === "*") {
|
|
504
|
+
depth += 1;
|
|
505
|
+
at += 2;
|
|
506
|
+
} else at += 1;
|
|
507
|
+
return {
|
|
508
|
+
end: text.length,
|
|
509
|
+
sealed: false
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function closeDollarQuoted(text, from) {
|
|
513
|
+
let tagEnd = from + 1;
|
|
514
|
+
if (tagEnd < text.length && WORD_START.test(text[tagEnd])) {
|
|
515
|
+
tagEnd += 1;
|
|
516
|
+
while (tagEnd < text.length && NAME_BYTE.test(text[tagEnd])) tagEnd += 1;
|
|
517
|
+
}
|
|
518
|
+
if (text[tagEnd] !== "$") return null;
|
|
519
|
+
const tag = text.slice(from, tagEnd + 1);
|
|
520
|
+
const close = text.indexOf(tag, tagEnd + 1);
|
|
521
|
+
return close === -1 ? {
|
|
522
|
+
end: text.length,
|
|
523
|
+
sealed: false
|
|
524
|
+
} : {
|
|
525
|
+
end: close + tag.length,
|
|
526
|
+
sealed: true
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function closeAlternativeQuoted(text, quote) {
|
|
530
|
+
const open = text[quote + 1];
|
|
531
|
+
if (open === void 0) return {
|
|
532
|
+
end: text.length,
|
|
533
|
+
sealed: false
|
|
534
|
+
};
|
|
535
|
+
const close = text.indexOf(`${ALTERNATIVE_QUOTE_CLOSE[open] ?? open}'`, quote + 2);
|
|
536
|
+
return close === -1 ? {
|
|
537
|
+
end: text.length,
|
|
538
|
+
sealed: false
|
|
539
|
+
} : {
|
|
540
|
+
end: close + 2,
|
|
541
|
+
sealed: true
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function opensLineComment(text, from, profile) {
|
|
545
|
+
if (text[from] === "#") return profile.hashLineComments;
|
|
546
|
+
if (text[from] !== "-" || text[from + 1] !== "-") return false;
|
|
547
|
+
const after = text[from + 2];
|
|
548
|
+
return !profile.lineCommentNeedsSpace || after === void 0 || after <= " ";
|
|
549
|
+
}
|
|
550
|
+
let lastBlanked = null;
|
|
551
|
+
function blankNoise(text, kind) {
|
|
552
|
+
if (lastBlanked !== null && lastBlanked.text === text && lastBlanked.kind === kind) return lastBlanked.result;
|
|
553
|
+
const profile = NOISE_PROFILES[kind];
|
|
554
|
+
const withNames = text.split("");
|
|
555
|
+
const blanked = text.split("");
|
|
556
|
+
const spans = [];
|
|
557
|
+
let at = 0;
|
|
558
|
+
const skip = (run, name) => {
|
|
559
|
+
for (let index = at; index < run.end; index += 1) {
|
|
560
|
+
blanked[index] = " ";
|
|
561
|
+
if (!name) withNames[index] = " ";
|
|
562
|
+
}
|
|
563
|
+
spans.push({
|
|
564
|
+
start: at,
|
|
565
|
+
end: run.end,
|
|
566
|
+
sealed: run.sealed,
|
|
567
|
+
name
|
|
568
|
+
});
|
|
569
|
+
at = run.end;
|
|
570
|
+
};
|
|
571
|
+
while (at < text.length) {
|
|
572
|
+
const ch = text[at];
|
|
573
|
+
if (ch === "'") skip(closeDelimited(text, at + 1, "'", profile.backslashEscapes), false);
|
|
574
|
+
else if (ch === "\"") skip(closeDelimited(text, at + 1, "\"", profile.backslashEscapes), !profile.doubleQuotedStrings);
|
|
575
|
+
else if (ch === "`" && profile.backtickIdentifiers) skip(closeDelimited(text, at + 1, "`", false), true);
|
|
576
|
+
else if (ch === "[" && profile.bracketIdentifiers) skip(closeBracket(text, at + 1), true);
|
|
577
|
+
else if (opensLineComment(text, at, profile)) skip(closeLine(text, at), false);
|
|
578
|
+
else if (ch === "/" && text[at + 1] === "*") skip(closeBlock(text, at + 2, profile.nestedBlockComments), false);
|
|
579
|
+
else if (ch === "$" && profile.dollarQuoting) {
|
|
580
|
+
const quoted = closeDollarQuoted(text, at);
|
|
581
|
+
if (quoted === null) at += 1;
|
|
582
|
+
else skip(quoted, false);
|
|
583
|
+
} else if (WORD_START.test(ch)) {
|
|
584
|
+
let end = at + 1;
|
|
585
|
+
while (end < text.length && WORD_BYTE.test(text[end])) end += 1;
|
|
586
|
+
const word = text.slice(at, end).toLowerCase();
|
|
587
|
+
if (text[end] === "'" && profile.escapeStrings && word === "e") skip(closeDelimited(text, end + 1, "'", true), false);
|
|
588
|
+
else if (text[end] === "'" && profile.alternativeQuoting && (word === "q" || word === "nq")) skip(closeAlternativeQuoted(text, end), false);
|
|
589
|
+
else at = end;
|
|
590
|
+
} else at += 1;
|
|
591
|
+
}
|
|
592
|
+
const result = {
|
|
593
|
+
withNames: withNames.join(""),
|
|
594
|
+
blanked: blanked.join(""),
|
|
595
|
+
spans
|
|
596
|
+
};
|
|
597
|
+
lastBlanked = {
|
|
598
|
+
text,
|
|
599
|
+
kind,
|
|
600
|
+
result
|
|
601
|
+
};
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
function insideNoise(spans, cursor) {
|
|
605
|
+
return spans.some((span) => !span.name && cursor > span.start && (span.sealed ? cursor < span.end : cursor <= span.end));
|
|
606
|
+
}
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region src/reader.ts
|
|
609
|
+
const TOKENS = /[\w$\u{0080}-\u{FFFF}]+|"(?:[^"]|"")*"|`(?:[^`]|``)*`|\[(?:[^\]]|\]\])*\]|\S/gu;
|
|
610
|
+
const TOKENS_TSQL = /[\w$#\u{0080}-\u{FFFF}]+|"(?:[^"]|"")*"|`(?:[^`]|``)*`|\[(?:[^\]]|\]\])*\]|\S/gu;
|
|
611
|
+
function statementBounds(blanked, cursor) {
|
|
612
|
+
const start = cursor === 0 ? 0 : blanked.lastIndexOf(";", cursor - 1) + 1;
|
|
613
|
+
const close = blanked.indexOf(";", cursor);
|
|
614
|
+
return {
|
|
615
|
+
start,
|
|
616
|
+
end: close === -1 ? blanked.length : close
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function tokenize(blanked, bounds, kind) {
|
|
620
|
+
const tokens = [];
|
|
621
|
+
const tokensFor = kind === "sqlserver" ? TOKENS_TSQL : TOKENS;
|
|
622
|
+
for (const match of blanked.slice(bounds.start, bounds.end).matchAll(tokensFor)) {
|
|
623
|
+
const value = match[0];
|
|
624
|
+
tokens.push({
|
|
625
|
+
value,
|
|
626
|
+
kind: /^[\w$#"`[\u{0080}-\u{FFFF}]/u.test(value) ? "word" : "punct",
|
|
627
|
+
start: bounds.start + match.index,
|
|
628
|
+
end: bounds.start + match.index + value.length
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return tokens;
|
|
632
|
+
}
|
|
633
|
+
function unquote(value) {
|
|
634
|
+
const quoted = /^"(?<d>.*)"$|^`(?<b>.*)`$|^\[(?<s>.*)\]$/s.exec(value);
|
|
635
|
+
if (quoted?.groups?.d !== void 0) return quoted.groups.d.replaceAll("\"\"", "\"");
|
|
636
|
+
if (quoted?.groups?.b !== void 0) return quoted.groups.b.replaceAll("``", "`");
|
|
637
|
+
if (quoted?.groups?.s !== void 0) return quoted.groups.s.replaceAll("]]", "]");
|
|
638
|
+
return value;
|
|
639
|
+
}
|
|
640
|
+
function word(token) {
|
|
641
|
+
return token !== void 0 && token.kind === "word" ? token.value.toLowerCase() : "";
|
|
642
|
+
}
|
|
643
|
+
function scan(text, cursor, kind) {
|
|
644
|
+
const noise = blankNoise(text, kind);
|
|
645
|
+
if (insideNoise(noise.spans, cursor)) return null;
|
|
646
|
+
return tokenize(noise.withNames, statementBounds(noise.blanked, cursor), kind);
|
|
647
|
+
}
|
|
648
|
+
const TABLE_ANCHORS = /* @__PURE__ */ new Set([
|
|
649
|
+
"from",
|
|
650
|
+
"join",
|
|
651
|
+
"update",
|
|
652
|
+
"into",
|
|
653
|
+
"table"
|
|
654
|
+
]);
|
|
655
|
+
const NOT_AN_ALIAS = /* @__PURE__ */ new Set([
|
|
656
|
+
"as",
|
|
657
|
+
"where",
|
|
658
|
+
"on",
|
|
659
|
+
"join",
|
|
660
|
+
"inner",
|
|
661
|
+
"left",
|
|
662
|
+
"right",
|
|
663
|
+
"full",
|
|
664
|
+
"cross",
|
|
665
|
+
"outer",
|
|
666
|
+
"natural",
|
|
667
|
+
"group",
|
|
668
|
+
"order",
|
|
669
|
+
"having",
|
|
670
|
+
"limit",
|
|
671
|
+
"offset",
|
|
672
|
+
"fetch",
|
|
673
|
+
"union",
|
|
674
|
+
"intersect",
|
|
675
|
+
"except",
|
|
676
|
+
"minus",
|
|
677
|
+
"set",
|
|
678
|
+
"values",
|
|
679
|
+
"returning",
|
|
680
|
+
"when",
|
|
681
|
+
"then",
|
|
682
|
+
"using",
|
|
683
|
+
"and",
|
|
684
|
+
"or",
|
|
685
|
+
"not",
|
|
686
|
+
"for",
|
|
687
|
+
"into",
|
|
688
|
+
"from",
|
|
689
|
+
"select",
|
|
690
|
+
"with",
|
|
691
|
+
"start",
|
|
692
|
+
"connect",
|
|
693
|
+
"straight_join",
|
|
694
|
+
"output"
|
|
695
|
+
]);
|
|
696
|
+
function readName(tokens, at) {
|
|
697
|
+
const parts = [];
|
|
698
|
+
let dangling = false;
|
|
699
|
+
let i = at;
|
|
700
|
+
for (;;) {
|
|
701
|
+
const part = tokens[i];
|
|
702
|
+
if (part === void 0 || part.kind !== "word") break;
|
|
703
|
+
parts.push(unquote(part.value));
|
|
704
|
+
dangling = false;
|
|
705
|
+
if (tokens[i + 1]?.value === ".") {
|
|
706
|
+
i += 2;
|
|
707
|
+
dangling = true;
|
|
708
|
+
} else {
|
|
709
|
+
i += 1;
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
parts,
|
|
715
|
+
dangling,
|
|
716
|
+
next: i
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function readAnchored(tokens, start, anchor) {
|
|
720
|
+
const refs = [];
|
|
721
|
+
let i = start;
|
|
722
|
+
for (;;) {
|
|
723
|
+
const first = tokens[i];
|
|
724
|
+
const name = readName(tokens, i);
|
|
725
|
+
i = name.next;
|
|
726
|
+
if (first === void 0 || name.parts.length === 0 || name.dangling) break;
|
|
727
|
+
let alias = "";
|
|
728
|
+
let aliasWritten = "";
|
|
729
|
+
const next = tokens[i];
|
|
730
|
+
if (next !== void 0 && next.kind === "word") {
|
|
731
|
+
if (word(next) === "as") {
|
|
732
|
+
i += 1;
|
|
733
|
+
const named = tokens[i];
|
|
734
|
+
if (named !== void 0 && named.kind === "word") {
|
|
735
|
+
alias = unquote(named.value);
|
|
736
|
+
aliasWritten = named.value;
|
|
737
|
+
i += 1;
|
|
738
|
+
}
|
|
739
|
+
} else if (!NOT_AN_ALIAS.has(word(next))) {
|
|
740
|
+
alias = unquote(next.value);
|
|
741
|
+
aliasWritten = next.value;
|
|
742
|
+
i += 1;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
refs.push({
|
|
746
|
+
schema: name.parts.length > 1 ? name.parts.at(-2) : "",
|
|
747
|
+
table: name.parts.at(-1),
|
|
748
|
+
alias,
|
|
749
|
+
aliasWritten,
|
|
750
|
+
anchor,
|
|
751
|
+
from: first.start,
|
|
752
|
+
to: tokens[i - 1]?.end ?? first.end
|
|
753
|
+
});
|
|
754
|
+
if (anchor === "from" && tokens[i]?.value === ",") {
|
|
755
|
+
i += 1;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
refs,
|
|
762
|
+
next: i
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function tableRefs(tokens) {
|
|
766
|
+
const refs = [];
|
|
767
|
+
let i = 0;
|
|
768
|
+
while (i < tokens.length) {
|
|
769
|
+
const head = tokens[i];
|
|
770
|
+
i += 1;
|
|
771
|
+
if (!TABLE_ANCHORS.has(word(head))) continue;
|
|
772
|
+
const anchored = readAnchored(tokens, i, word(head));
|
|
773
|
+
refs.push(...anchored.refs);
|
|
774
|
+
i = anchored.next;
|
|
775
|
+
}
|
|
776
|
+
return refs;
|
|
777
|
+
}
|
|
778
|
+
const CLAUSE_HEADS = /* @__PURE__ */ new Set([
|
|
779
|
+
"on",
|
|
780
|
+
"where",
|
|
781
|
+
"having",
|
|
782
|
+
"set",
|
|
783
|
+
"values",
|
|
784
|
+
"select",
|
|
785
|
+
"from",
|
|
786
|
+
"join",
|
|
787
|
+
"group",
|
|
788
|
+
"order",
|
|
789
|
+
"when",
|
|
790
|
+
"then",
|
|
791
|
+
"using",
|
|
792
|
+
"update",
|
|
793
|
+
"insert",
|
|
794
|
+
"delete",
|
|
795
|
+
"returning"
|
|
796
|
+
]);
|
|
797
|
+
function joinContext(clauseHead, refs) {
|
|
798
|
+
if (clauseHead === void 0 || word(clauseHead) !== "on") return null;
|
|
799
|
+
const joined = refs.findLast((ref) => ref.anchor === "join" && ref.from < clauseHead.start);
|
|
800
|
+
if (joined === void 0) return null;
|
|
801
|
+
return {
|
|
802
|
+
kind: "joinCondition",
|
|
803
|
+
joined,
|
|
804
|
+
earlier: refs.filter((ref) => ref.from < joined.from)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function insertContext(verb, prior, refs) {
|
|
808
|
+
if (verb !== "insert") return null;
|
|
809
|
+
const into = refs.find((ref) => ref.anchor === "into");
|
|
810
|
+
if (into === void 0 || (prior.at(-1)?.end ?? 0) < into.to) return null;
|
|
811
|
+
return prior.some((token) => token.start >= into.to && (token.value === "(" || word(token) === "values" || word(token) === "select")) ? null : {
|
|
812
|
+
kind: "insertColumns",
|
|
813
|
+
into
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function memberParts(prior) {
|
|
817
|
+
const parts = [];
|
|
818
|
+
let at = prior.length - 1;
|
|
819
|
+
while (at >= 1) {
|
|
820
|
+
const dot = prior[at];
|
|
821
|
+
const name = prior[at - 1];
|
|
822
|
+
if (dot.value !== "." || name.kind !== "word") break;
|
|
823
|
+
parts.unshift(unquote(name.value));
|
|
824
|
+
at -= 2;
|
|
825
|
+
}
|
|
826
|
+
return parts;
|
|
827
|
+
}
|
|
828
|
+
function readStatement(text, cursor, kind) {
|
|
829
|
+
const tokens = scan(text, cursor, kind);
|
|
830
|
+
if (tokens === null) return null;
|
|
831
|
+
const refs = tableRefs(tokens);
|
|
832
|
+
let prior = tokens.filter((token) => token.end <= cursor);
|
|
833
|
+
const typing = prior.at(-1);
|
|
834
|
+
const typed = typing !== void 0 && typing.kind === "word" && typing.end === cursor ? typing.value : "";
|
|
835
|
+
if (typed !== "") prior = prior.slice(0, -1);
|
|
836
|
+
const clauseHead = prior.findLast((token) => CLAUSE_HEADS.has(word(token)));
|
|
837
|
+
const previous = prior.at(-1);
|
|
838
|
+
const read = (context) => {
|
|
839
|
+
return {
|
|
840
|
+
context,
|
|
841
|
+
refs,
|
|
842
|
+
typed,
|
|
843
|
+
previous: previous?.value ?? "",
|
|
844
|
+
onStar: previous?.value === "*" && previous.end === cursor,
|
|
845
|
+
clause: word(clauseHead)
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
if (previous === void 0) return read({ kind: "expression" });
|
|
849
|
+
if (previous.value === ":") return read({ kind: "none" });
|
|
850
|
+
if (previous.value === ".") {
|
|
851
|
+
const parts = memberParts(prior);
|
|
852
|
+
return read(parts.length === 0 ? { kind: "none" } : {
|
|
853
|
+
kind: "member",
|
|
854
|
+
parts
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
if (TABLE_ANCHORS.has(word(previous))) return read({ kind: "table" });
|
|
858
|
+
return read(joinContext(clauseHead, refs) ?? insertContext(word(tokens[0]), prior, refs) ?? { kind: "expression" });
|
|
859
|
+
}
|
|
860
|
+
function trimmedSpan(text, blanked, from, to) {
|
|
861
|
+
let start = from;
|
|
862
|
+
let end = to;
|
|
863
|
+
while (start < end && /\s/.test(text[start])) start += 1;
|
|
864
|
+
while (end > start && /\s/.test(text[end - 1])) end -= 1;
|
|
865
|
+
return start === end || blanked.slice(start, end).trim() === "" ? null : {
|
|
866
|
+
from: start,
|
|
867
|
+
to: end
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function statementSpans(text, blanked, from, to) {
|
|
871
|
+
const spans = [];
|
|
872
|
+
let start = from;
|
|
873
|
+
for (let at = from; at <= to; at += 1) {
|
|
874
|
+
if (!(at === to || blanked[at] === ";")) continue;
|
|
875
|
+
const span = trimmedSpan(text, blanked, start, at);
|
|
876
|
+
if (span !== null) spans.push(span);
|
|
877
|
+
start = at + 1;
|
|
878
|
+
}
|
|
879
|
+
return spans;
|
|
880
|
+
}
|
|
881
|
+
function runnableStatement(text, from, to, kind) {
|
|
882
|
+
const { blanked } = blankNoise(text, kind);
|
|
883
|
+
const selected = from < to;
|
|
884
|
+
const around = statementBounds(blanked, from);
|
|
885
|
+
const spans = selected ? statementSpans(text, blanked, from, to) : statementSpans(text, blanked, around.start, around.end);
|
|
886
|
+
if (spans.length === 0) return { kind: "none" };
|
|
887
|
+
if (selected && spans.length > 1) return { kind: "several" };
|
|
888
|
+
const span = spans[0];
|
|
889
|
+
return {
|
|
890
|
+
kind: "one",
|
|
891
|
+
text: text.slice(span.from, span.to),
|
|
892
|
+
blanked: blanked.slice(span.from, span.to),
|
|
893
|
+
from: span.from,
|
|
894
|
+
to: span.to,
|
|
895
|
+
alone: statementSpans(text, blanked, 0, text.length).length <= 1
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function readIdentifier(text, offset, kind) {
|
|
899
|
+
const tokens = scan(text, offset, kind);
|
|
900
|
+
if (tokens === null) return null;
|
|
901
|
+
const under = tokens.findIndex((token) => token.start <= offset && offset < token.end);
|
|
902
|
+
const index = under !== -1 && tokens[under].kind === "word" ? under : tokens.findIndex((token) => token.end === offset);
|
|
903
|
+
const hovered = tokens[index];
|
|
904
|
+
if (hovered === void 0 || hovered.kind !== "word") return null;
|
|
905
|
+
const parts = [unquote(hovered.value)];
|
|
906
|
+
let at = index;
|
|
907
|
+
while (at >= 2 && tokens[at - 1].value === "." && tokens[at - 2].kind === "word") {
|
|
908
|
+
parts.unshift(unquote(tokens[at - 2].value));
|
|
909
|
+
at -= 2;
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
parts,
|
|
913
|
+
from: tokens[at].start,
|
|
914
|
+
to: hovered.end,
|
|
915
|
+
refs: tableRefs(tokens)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function readCall(text, cursor, kind) {
|
|
919
|
+
const scanned = scan(text, cursor, kind);
|
|
920
|
+
if (scanned === null) return null;
|
|
921
|
+
const tokens = scanned.filter((token) => token.end <= cursor);
|
|
922
|
+
const open = [];
|
|
923
|
+
for (const [index, token] of tokens.entries()) if (token.value === "(") open.push({
|
|
924
|
+
name: word(tokens[index - 1]),
|
|
925
|
+
argument: 0
|
|
926
|
+
});
|
|
927
|
+
else if (token.value === ")") open.pop();
|
|
928
|
+
else if (token.value === "," && open.length > 0) open.at(-1).argument += 1;
|
|
929
|
+
const call = open.at(-1);
|
|
930
|
+
return call === void 0 || call.name === "" ? null : call;
|
|
931
|
+
}
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/completion.ts
|
|
934
|
+
const RANK_ACTION = 0;
|
|
935
|
+
const RANK_PRIMARY = 1;
|
|
936
|
+
const RANK_KEYWORD = 2;
|
|
937
|
+
const RANK_FUNCTION = 3;
|
|
938
|
+
const RANK_TABLE = 4;
|
|
939
|
+
const RANK_VENDOR = 5;
|
|
940
|
+
const EXPANSION_PREVIEW = 200;
|
|
941
|
+
function columnCandidate(kind, column, origin) {
|
|
942
|
+
const tag = `${column.dataType}${column.nullable ? "?" : ""}`;
|
|
943
|
+
const qualifier = origin?.qualifier ?? "";
|
|
944
|
+
const label = origin?.label ?? "";
|
|
945
|
+
const spelled = spellIdentifier(kind, column.name);
|
|
946
|
+
return {
|
|
947
|
+
label: column.name,
|
|
948
|
+
kind: "column",
|
|
949
|
+
insertText: qualifier === "" ? spelled : `${qualifier}.${spelled}`,
|
|
950
|
+
detail: label === "" ? tag : `${label} · ${tag}`,
|
|
951
|
+
documentation: column.comment === "" ? void 0 : column.comment,
|
|
952
|
+
sortGroup: RANK_PRIMARY
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function tableCandidate(table, insertText, sortGroup) {
|
|
956
|
+
return {
|
|
957
|
+
label: table.name,
|
|
958
|
+
kind: table.kind === "view" ? "view" : "table",
|
|
959
|
+
insertText,
|
|
960
|
+
detail: table.schema === "" ? void 0 : table.schema,
|
|
961
|
+
documentation: table.comment === "" ? void 0 : table.comment,
|
|
962
|
+
sortGroup
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function ambiguousNames(sources) {
|
|
966
|
+
const seen = /* @__PURE__ */ new Set();
|
|
967
|
+
const twice = /* @__PURE__ */ new Set();
|
|
968
|
+
for (const source of sources) for (const column of source.columns) {
|
|
969
|
+
const name = column.name.toLowerCase();
|
|
970
|
+
if (seen.has(name)) twice.add(name);
|
|
971
|
+
seen.add(name);
|
|
972
|
+
}
|
|
973
|
+
return twice;
|
|
974
|
+
}
|
|
975
|
+
function expansionCandidate(kind, sources, replacesStar) {
|
|
976
|
+
const many = sources.length > 1;
|
|
977
|
+
const names = sources.flatMap((source) => source.columns.map((column) => {
|
|
978
|
+
const spelled = spellIdentifier(kind, column.name);
|
|
979
|
+
return many ? `${spellQualifier(kind, source)}.${spelled}` : spelled;
|
|
980
|
+
}));
|
|
981
|
+
if (names.length === 0) return null;
|
|
982
|
+
const list = names.join(", ");
|
|
983
|
+
return {
|
|
984
|
+
label: "*",
|
|
985
|
+
kind: "snippet",
|
|
986
|
+
insertText: list,
|
|
987
|
+
replace: replacesStar ? 1 : void 0,
|
|
988
|
+
detail: `expand to ${names.length} columns`,
|
|
989
|
+
documentation: list.length > EXPANSION_PREVIEW ? `${list.slice(0, EXPANSION_PREVIEW)}…` : list,
|
|
990
|
+
sortGroup: RANK_ACTION
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function createSqlCompletion(kind, schema, functions) {
|
|
994
|
+
const { keywords } = SQL_DIALECTS[kind];
|
|
995
|
+
const spelled = /* @__PURE__ */ new Map();
|
|
996
|
+
async function wordCandidates(typed) {
|
|
997
|
+
const upper = typed !== "" && typed === typed.toUpperCase() && typed !== typed.toLowerCase();
|
|
998
|
+
const known = await functions.all();
|
|
999
|
+
const built = spelled.get(upper);
|
|
1000
|
+
if (built?.of === known) return built.candidates;
|
|
1001
|
+
const spell = (word) => upper ? word.toUpperCase() : word;
|
|
1002
|
+
const candidates = [...keywords.map((word) => {
|
|
1003
|
+
return {
|
|
1004
|
+
label: spell(word),
|
|
1005
|
+
kind: "keyword",
|
|
1006
|
+
insertText: spell(word),
|
|
1007
|
+
sortGroup: RANK_KEYWORD
|
|
1008
|
+
};
|
|
1009
|
+
}), ...known.map((entry) => {
|
|
1010
|
+
const name = entry.origin === "curated" ? spell(entry.name) : entry.name;
|
|
1011
|
+
const written = entry.origin === "curated" ? name : spellCallable(kind, name);
|
|
1012
|
+
return {
|
|
1013
|
+
label: name,
|
|
1014
|
+
kind: "function",
|
|
1015
|
+
insertText: `${entry.qualifier === "" ? written : `${spellIdentifier(kind, entry.qualifier)}.${written}`}($0)`,
|
|
1016
|
+
snippet: true,
|
|
1017
|
+
detail: entry.signature,
|
|
1018
|
+
documentation: entry.summary === "" ? void 0 : entry.summary,
|
|
1019
|
+
sortGroup: entry.origin === "builtin" ? RANK_VENDOR : RANK_FUNCTION
|
|
1020
|
+
};
|
|
1021
|
+
})];
|
|
1022
|
+
spelled.set(upper, {
|
|
1023
|
+
of: known,
|
|
1024
|
+
candidates
|
|
1025
|
+
});
|
|
1026
|
+
return candidates;
|
|
1027
|
+
}
|
|
1028
|
+
async function memberCandidates(parts, refs) {
|
|
1029
|
+
if (parts.length === 1) {
|
|
1030
|
+
const name = parts[0];
|
|
1031
|
+
const table = await schema.resolve(name, refs);
|
|
1032
|
+
if (table !== null) return (await schema.columns(table)).map((column) => columnCandidate(kind, column));
|
|
1033
|
+
return (await schema.tables()).filter((entry) => entry.schema.toLowerCase() === name.toLowerCase()).map((entry) => tableCandidate(entry, spellIdentifier(kind, entry.name), RANK_PRIMARY));
|
|
1034
|
+
}
|
|
1035
|
+
if (parts.length === 2) {
|
|
1036
|
+
const table = await schema.find({
|
|
1037
|
+
schema: parts[0],
|
|
1038
|
+
table: parts[1]
|
|
1039
|
+
});
|
|
1040
|
+
if (table !== null) return (await schema.columns(table)).map((column) => columnCandidate(kind, column));
|
|
1041
|
+
}
|
|
1042
|
+
return [];
|
|
1043
|
+
}
|
|
1044
|
+
async function tableCandidates() {
|
|
1045
|
+
return (await schema.tables()).map((table) => tableCandidate(table, spellQualifiedName(kind, table), RANK_PRIMARY));
|
|
1046
|
+
}
|
|
1047
|
+
async function insertCandidates(into, typed) {
|
|
1048
|
+
const words = await wordCandidates(typed);
|
|
1049
|
+
const table = await schema.find(into);
|
|
1050
|
+
if (table === null) return words;
|
|
1051
|
+
const columns = await schema.columns(table);
|
|
1052
|
+
if (columns.length === 0) return words;
|
|
1053
|
+
const names = columns.map((column) => spellIdentifier(kind, column.name)).join(", ");
|
|
1054
|
+
const marks = columns.map(() => "?").join(", ");
|
|
1055
|
+
return [{
|
|
1056
|
+
label: "(…) values (…)",
|
|
1057
|
+
kind: "snippet",
|
|
1058
|
+
insertText: `(${names}) values (${marks})`,
|
|
1059
|
+
detail: `${columns.length} columns of ${table.name}`,
|
|
1060
|
+
documentation: `(${names})\nvalues (${marks})`,
|
|
1061
|
+
sortGroup: RANK_ACTION
|
|
1062
|
+
}, ...words];
|
|
1063
|
+
}
|
|
1064
|
+
async function expressionCandidates(read) {
|
|
1065
|
+
const words = await wordCandidates(read.typed);
|
|
1066
|
+
try {
|
|
1067
|
+
const tables = await schema.tables();
|
|
1068
|
+
const sources = await schema.sources(read.refs);
|
|
1069
|
+
const twice = ambiguousNames(sources);
|
|
1070
|
+
const columns = sources.flatMap((source) => source.columns.map((column) => columnCandidate(kind, column, {
|
|
1071
|
+
label: sources.length > 1 ? source.table.name : "",
|
|
1072
|
+
qualifier: twice.has(column.name.toLowerCase()) ? spellQualifier(kind, source) : ""
|
|
1073
|
+
})));
|
|
1074
|
+
const opens = read.previous.toLowerCase() === "select" || read.previous === "," || read.onStar;
|
|
1075
|
+
const expansion = read.clause === "select" && opens ? expansionCandidate(kind, sources, read.onStar) : null;
|
|
1076
|
+
return [
|
|
1077
|
+
...expansion === null ? [] : [expansion],
|
|
1078
|
+
...columns,
|
|
1079
|
+
...words,
|
|
1080
|
+
...tables.map((table) => tableCandidate(table, spellQualifiedName(kind, table), RANK_TABLE))
|
|
1081
|
+
];
|
|
1082
|
+
} catch {
|
|
1083
|
+
return words;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async function joinCandidates(joined, earlier, read) {
|
|
1087
|
+
const rest = await expressionCandidates(read);
|
|
1088
|
+
try {
|
|
1089
|
+
const [target] = await schema.sources([joined]);
|
|
1090
|
+
const others = await schema.sources(earlier);
|
|
1091
|
+
if (target === void 0 || others.length === 0) return rest;
|
|
1092
|
+
return [...joinGuesses(kind, target, others).map((guess) => {
|
|
1093
|
+
return {
|
|
1094
|
+
label: guess.text,
|
|
1095
|
+
kind: "snippet",
|
|
1096
|
+
insertText: guess.text,
|
|
1097
|
+
detail: guess.why === "named-foreign-key" ? "named foreign key" : "shared key column",
|
|
1098
|
+
sortGroup: RANK_ACTION
|
|
1099
|
+
};
|
|
1100
|
+
}), ...rest];
|
|
1101
|
+
} catch {
|
|
1102
|
+
return rest;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return async (text, cursor) => {
|
|
1106
|
+
const read = readStatement(text, cursor, kind);
|
|
1107
|
+
if (read === null) return [];
|
|
1108
|
+
try {
|
|
1109
|
+
switch (read.context.kind) {
|
|
1110
|
+
case "none": return [];
|
|
1111
|
+
case "member": return await memberCandidates(read.context.parts, read.refs);
|
|
1112
|
+
case "table": return await tableCandidates();
|
|
1113
|
+
case "insertColumns": return await insertCandidates(read.context.into, read.typed);
|
|
1114
|
+
case "joinCondition": return await joinCandidates(read.context.joined, read.context.earlier, read);
|
|
1115
|
+
case "expression": return await expressionCandidates(read);
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
return [];
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
//#endregion
|
|
1123
|
+
//#region src/format.ts
|
|
1124
|
+
const DIALECTS = {
|
|
1125
|
+
postgres: "postgresql",
|
|
1126
|
+
mysql: "mysql",
|
|
1127
|
+
oracle: "plsql",
|
|
1128
|
+
dm: "plsql",
|
|
1129
|
+
sqlserver: "transactsql"
|
|
1130
|
+
};
|
|
1131
|
+
const PLACEHOLDERS = String.raw`\?|:[A-Za-z_]\w*`;
|
|
1132
|
+
function sentinelFor(text) {
|
|
1133
|
+
let sentinel = ":inkqq";
|
|
1134
|
+
while (text.includes(sentinel)) sentinel += "q";
|
|
1135
|
+
return sentinel;
|
|
1136
|
+
}
|
|
1137
|
+
function createSqlFormat(kind) {
|
|
1138
|
+
return async (text) => {
|
|
1139
|
+
const sentinel = sentinelFor(text);
|
|
1140
|
+
const formatter = await import("sql-formatter");
|
|
1141
|
+
try {
|
|
1142
|
+
return formatter.formatDialect(text.replaceAll("??", () => sentinel), {
|
|
1143
|
+
dialect: formatter[DIALECTS[kind]],
|
|
1144
|
+
tabWidth: 2,
|
|
1145
|
+
keywordCase: "preserve",
|
|
1146
|
+
paramTypes: { custom: [{ regex: PLACEHOLDERS }] }
|
|
1147
|
+
}).replaceAll(sentinel, "??");
|
|
1148
|
+
} catch {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/functions.ts
|
|
1155
|
+
function createSqlFunctions(kind, schema) {
|
|
1156
|
+
const curated = SQL_DIALECTS[kind].functions.map((entry) => {
|
|
1157
|
+
return {
|
|
1158
|
+
name: entry.name,
|
|
1159
|
+
signature: entry.signature,
|
|
1160
|
+
summary: entry.summary,
|
|
1161
|
+
returns: "",
|
|
1162
|
+
qualifier: "",
|
|
1163
|
+
origin: "curated"
|
|
1164
|
+
};
|
|
1165
|
+
});
|
|
1166
|
+
let merged = null;
|
|
1167
|
+
function merge(reported) {
|
|
1168
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1169
|
+
const routines = reported.filter((entry) => !entry.builtin);
|
|
1170
|
+
const builtins = reported.filter((entry) => entry.builtin);
|
|
1171
|
+
for (const entry of routines) byName.set(entry.name.toLowerCase(), {
|
|
1172
|
+
name: entry.name,
|
|
1173
|
+
signature: entry.signature,
|
|
1174
|
+
summary: entry.comment,
|
|
1175
|
+
returns: entry.returns,
|
|
1176
|
+
qualifier: entry.schema,
|
|
1177
|
+
origin: "routine"
|
|
1178
|
+
});
|
|
1179
|
+
for (const entry of curated) if (!byName.has(entry.name.toLowerCase())) byName.set(entry.name.toLowerCase(), entry);
|
|
1180
|
+
for (const entry of builtins) if (!byName.has(entry.name.toLowerCase())) byName.set(entry.name.toLowerCase(), {
|
|
1181
|
+
name: entry.name,
|
|
1182
|
+
signature: entry.signature,
|
|
1183
|
+
summary: entry.comment,
|
|
1184
|
+
returns: entry.returns,
|
|
1185
|
+
qualifier: "",
|
|
1186
|
+
origin: "builtin"
|
|
1187
|
+
});
|
|
1188
|
+
return [...byName.values()];
|
|
1189
|
+
}
|
|
1190
|
+
async function all() {
|
|
1191
|
+
const reported = await schema.functions().catch(() => null);
|
|
1192
|
+
if (reported === null) return curated;
|
|
1193
|
+
if (merged?.of !== reported) merged = {
|
|
1194
|
+
of: reported,
|
|
1195
|
+
known: merge(reported)
|
|
1196
|
+
};
|
|
1197
|
+
return merged.known;
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
all,
|
|
1201
|
+
async find(name) {
|
|
1202
|
+
const wanted = name.toLowerCase();
|
|
1203
|
+
return (await all()).find((entry) => entry.name.toLowerCase() === wanted) ?? null;
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
//#endregion
|
|
1208
|
+
//#region src/schema.ts
|
|
1209
|
+
function qualifiedName(table) {
|
|
1210
|
+
return table.schema === "" ? table.name : `${table.schema}.${table.name}`;
|
|
1211
|
+
}
|
|
1212
|
+
function identity(table) {
|
|
1213
|
+
return `${table.schema}\u{0}${table.name}`;
|
|
1214
|
+
}
|
|
1215
|
+
function matches(table, ref) {
|
|
1216
|
+
return table.name.toLowerCase() === ref.table.toLowerCase() && (ref.schema === "" || table.schema.toLowerCase() === ref.schema.toLowerCase());
|
|
1217
|
+
}
|
|
1218
|
+
function createSqlSchema(catalog) {
|
|
1219
|
+
function tables() {
|
|
1220
|
+
return catalog.tables();
|
|
1221
|
+
}
|
|
1222
|
+
function functions() {
|
|
1223
|
+
return catalog.functions();
|
|
1224
|
+
}
|
|
1225
|
+
function columns(table) {
|
|
1226
|
+
return catalog.columns(table.schema, table.name);
|
|
1227
|
+
}
|
|
1228
|
+
async function find(ref) {
|
|
1229
|
+
return (await tables()).find((table) => matches(table, ref)) ?? null;
|
|
1230
|
+
}
|
|
1231
|
+
function resolve(name, refs) {
|
|
1232
|
+
const wanted = name.toLowerCase();
|
|
1233
|
+
return find(refs.find((entry) => entry.alias.toLowerCase() === wanted) ?? refs.find((entry) => entry.table.toLowerCase() === wanted) ?? {
|
|
1234
|
+
schema: "",
|
|
1235
|
+
table: name
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
async function sources(refs) {
|
|
1239
|
+
const known = await tables();
|
|
1240
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const ref of refs) {
|
|
1242
|
+
const table = known.find((entry) => matches(entry, ref));
|
|
1243
|
+
if (table === void 0) continue;
|
|
1244
|
+
const as = ref.alias === "" ? ref.table : ref.alias;
|
|
1245
|
+
wanted.set(`${identity(table)}\u{0}${as.toLowerCase()}`, {
|
|
1246
|
+
table,
|
|
1247
|
+
as,
|
|
1248
|
+
aliasWritten: ref.aliasWritten
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
return (await Promise.allSettled(Array.from(wanted.values(), async (entry) => {
|
|
1252
|
+
return {
|
|
1253
|
+
...entry,
|
|
1254
|
+
columns: await columns(entry.table)
|
|
1255
|
+
};
|
|
1256
|
+
}))).filter((one) => one.status === "fulfilled").map((one) => one.value);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
tables,
|
|
1260
|
+
columns,
|
|
1261
|
+
functions,
|
|
1262
|
+
find,
|
|
1263
|
+
resolve,
|
|
1264
|
+
sources
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
//#endregion
|
|
1268
|
+
//#region src/hover.ts
|
|
1269
|
+
const COLUMN_PREVIEW = 12;
|
|
1270
|
+
function typeTag(column) {
|
|
1271
|
+
return `${column.dataType}${column.nullable ? "?" : ""}`;
|
|
1272
|
+
}
|
|
1273
|
+
function plain(text) {
|
|
1274
|
+
return text.replaceAll(/[\\`*_[\]<>]/g, (one) => `\\${one}`);
|
|
1275
|
+
}
|
|
1276
|
+
function keyMarks(column) {
|
|
1277
|
+
const marks = [];
|
|
1278
|
+
if (column.primaryKey) marks.push("primary key");
|
|
1279
|
+
if (column.foreignKey) marks.push("foreign key");
|
|
1280
|
+
if (column.unique) marks.push("unique");
|
|
1281
|
+
if (column.indexed) marks.push("indexed");
|
|
1282
|
+
return marks.join(" · ");
|
|
1283
|
+
}
|
|
1284
|
+
function tableCard(table, columns) {
|
|
1285
|
+
const blocks = [`**${plain(qualifiedName(table))}** · ${table.kind}`];
|
|
1286
|
+
if (table.comment !== "") blocks.push(plain(table.comment));
|
|
1287
|
+
if (columns.length > 0) {
|
|
1288
|
+
const shown = columns.slice(0, COLUMN_PREVIEW);
|
|
1289
|
+
const width = Math.max(...shown.map((column) => column.name.length));
|
|
1290
|
+
const lines = shown.map((column) => {
|
|
1291
|
+
const marks = keyMarks(column);
|
|
1292
|
+
return `${column.name.padEnd(width)} ${typeTag(column)}${marks === "" ? "" : ` ${marks}`}`;
|
|
1293
|
+
});
|
|
1294
|
+
if (columns.length > shown.length) lines.push(`… ${columns.length - shown.length} more columns`);
|
|
1295
|
+
blocks.push([
|
|
1296
|
+
"```",
|
|
1297
|
+
...lines,
|
|
1298
|
+
"```"
|
|
1299
|
+
].join("\n"));
|
|
1300
|
+
}
|
|
1301
|
+
return blocks;
|
|
1302
|
+
}
|
|
1303
|
+
function columnCard(column, table) {
|
|
1304
|
+
const blocks = [`**${plain(column.name)}** · ${plain(typeTag(column))}`];
|
|
1305
|
+
const marks = keyMarks(column);
|
|
1306
|
+
if (marks !== "") blocks.push(marks);
|
|
1307
|
+
if (column.comment !== "") blocks.push(plain(column.comment));
|
|
1308
|
+
blocks.push(`in ${plain(qualifiedName(table))}`);
|
|
1309
|
+
return blocks;
|
|
1310
|
+
}
|
|
1311
|
+
function functionCard(entry) {
|
|
1312
|
+
const call = entry.qualifier === "" ? entry.signature : `${entry.qualifier}.${entry.signature}`;
|
|
1313
|
+
const blocks = [[
|
|
1314
|
+
"```sql",
|
|
1315
|
+
entry.returns === "" ? call : `${call} → ${entry.returns}`,
|
|
1316
|
+
"```"
|
|
1317
|
+
].join("\n")];
|
|
1318
|
+
if (entry.origin === "routine") blocks.push("Defined by this schema.");
|
|
1319
|
+
if (entry.summary !== "") blocks.push(plain(entry.summary));
|
|
1320
|
+
return blocks;
|
|
1321
|
+
}
|
|
1322
|
+
function createSqlHover(kind, schema, functions) {
|
|
1323
|
+
return async (text, offset) => {
|
|
1324
|
+
const read = readIdentifier(text, offset, kind);
|
|
1325
|
+
if (read === null) return null;
|
|
1326
|
+
const name = read.parts.at(-1).toLowerCase();
|
|
1327
|
+
const card = (markdown) => {
|
|
1328
|
+
return {
|
|
1329
|
+
from: read.from,
|
|
1330
|
+
to: read.to,
|
|
1331
|
+
markdown
|
|
1332
|
+
};
|
|
1333
|
+
};
|
|
1334
|
+
try {
|
|
1335
|
+
if (read.parts.length >= 2) {
|
|
1336
|
+
const owner = read.parts.at(-2);
|
|
1337
|
+
const table = read.parts.length >= 3 ? await schema.find({
|
|
1338
|
+
schema: read.parts.at(-3),
|
|
1339
|
+
table: owner
|
|
1340
|
+
}) : await schema.resolve(owner, read.refs);
|
|
1341
|
+
const column = (table === null ? [] : await schema.columns(table)).find((entry) => entry.name.toLowerCase() === name);
|
|
1342
|
+
if (table !== null && column !== void 0) return card(columnCard(column, table));
|
|
1343
|
+
const qualified = read.parts.length === 2 ? await schema.find({
|
|
1344
|
+
schema: owner,
|
|
1345
|
+
table: read.parts.at(-1)
|
|
1346
|
+
}) : null;
|
|
1347
|
+
return qualified === null ? null : card(tableCard(qualified, await schema.columns(qualified)));
|
|
1348
|
+
}
|
|
1349
|
+
const sources = await schema.sources(read.refs);
|
|
1350
|
+
for (const source of sources) {
|
|
1351
|
+
const column = source.columns.find((entry) => entry.name.toLowerCase() === name);
|
|
1352
|
+
if (column !== void 0) return card(columnCard(column, source.table));
|
|
1353
|
+
}
|
|
1354
|
+
const table = await schema.resolve(name, read.refs);
|
|
1355
|
+
if (table !== null) return card(tableCard(table, await schema.columns(table)));
|
|
1356
|
+
const entry = await functions.find(name);
|
|
1357
|
+
return entry === null ? null : card(functionCard(entry));
|
|
1358
|
+
} catch {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
//#endregion
|
|
1364
|
+
//#region src/signature.ts
|
|
1365
|
+
function createSqlSignature(kind, functions) {
|
|
1366
|
+
return async (text, cursor) => {
|
|
1367
|
+
const call = readCall(text, cursor, kind);
|
|
1368
|
+
if (call === null) return null;
|
|
1369
|
+
const entry = await functions.find(call.name);
|
|
1370
|
+
if (entry === null) return null;
|
|
1371
|
+
const parameters = signatureParameters(entry.signature);
|
|
1372
|
+
return {
|
|
1373
|
+
label: entry.signature,
|
|
1374
|
+
parameters,
|
|
1375
|
+
active: Math.min(call.argument, Math.max(0, parameters.length - 1)),
|
|
1376
|
+
documentation: entry.summary === "" ? void 0 : entry.summary
|
|
1377
|
+
};
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
//#endregion
|
|
1381
|
+
//#region src/assist.ts
|
|
1382
|
+
function createSqlAssist(kind, catalog) {
|
|
1383
|
+
const schema = createSqlSchema(catalog);
|
|
1384
|
+
const functions = createSqlFunctions(kind, schema);
|
|
1385
|
+
return {
|
|
1386
|
+
complete: createSqlCompletion(kind, schema, functions),
|
|
1387
|
+
hover: createSqlHover(kind, schema, functions),
|
|
1388
|
+
signature: createSqlSignature(kind, functions),
|
|
1389
|
+
format: createSqlFormat(kind)
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
//#endregion
|
|
1393
|
+
//#region src/markers.ts
|
|
1394
|
+
const SCAN = /[A-Z_\u0080-\uFFFF][\w$\u0080-\uFFFF]*|(?<qq>\?\?)|::|(?<q>\?)|:(?<name>[A-Z_]\w*)|(?<colonDigits>:\d+)|(?<dollarDigits>\$\d+)|(?<atDigits>@P\d+)/gi;
|
|
1395
|
+
const QUESTION_IS_NATIVE = /* @__PURE__ */ new Set(["mysql", "dm"]);
|
|
1396
|
+
const NATIVE_GROUP = {
|
|
1397
|
+
postgres: "dollarDigits",
|
|
1398
|
+
mysql: null,
|
|
1399
|
+
oracle: "colonDigits",
|
|
1400
|
+
sqlserver: "atDigits",
|
|
1401
|
+
dm: null
|
|
1402
|
+
};
|
|
1403
|
+
function statementMarkers(statement, kind) {
|
|
1404
|
+
const names = [];
|
|
1405
|
+
const nativeGroup = NATIVE_GROUP[kind];
|
|
1406
|
+
let count = 0;
|
|
1407
|
+
let native = false;
|
|
1408
|
+
let refusal = null;
|
|
1409
|
+
const refuse = (fault) => {
|
|
1410
|
+
refusal ??= fault;
|
|
1411
|
+
};
|
|
1412
|
+
for (const match of statement.matchAll(SCAN)) {
|
|
1413
|
+
const groups = match.groups;
|
|
1414
|
+
if (groups.qq !== void 0 && QUESTION_IS_NATIVE.has(kind)) refuse("question-escape-unsupported");
|
|
1415
|
+
else if (groups.q !== void 0) {
|
|
1416
|
+
if (names.length > 0) refuse("mixed-forms");
|
|
1417
|
+
count += 1;
|
|
1418
|
+
} else if (groups.name !== void 0) {
|
|
1419
|
+
if (count > 0) refuse("mixed-forms");
|
|
1420
|
+
if (!names.includes(groups.name)) names.push(groups.name);
|
|
1421
|
+
} else if (nativeGroup !== null && groups[nativeGroup] !== void 0) native = true;
|
|
1422
|
+
}
|
|
1423
|
+
if (refusal === null && native && (count > 0 || names.length > 0)) refusal = "native-marker-collision";
|
|
1424
|
+
return {
|
|
1425
|
+
form: names.length > 0 ? "named" : count > 0 ? "positional" : "none",
|
|
1426
|
+
names,
|
|
1427
|
+
count,
|
|
1428
|
+
refusal
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
//#endregion
|
|
1432
|
+
//#region src/origin.ts
|
|
1433
|
+
const SINGLE_TABLE = /^\s*select\s+\*\s+from\s+(?<qualified>[^\s;,()]+)(?:[\s;]*$|\s+(?:where|order|group|having|limit|offset|fetch|for)\b)/i;
|
|
1434
|
+
function statementOrigin(statement) {
|
|
1435
|
+
const qualified = SINGLE_TABLE.exec(statement)?.groups?.qualified;
|
|
1436
|
+
if (qualified === void 0) return null;
|
|
1437
|
+
const segments = qualified.split(".").map((segment) => segment.replace(/^"(?<inner>.*)"$/s, "$<inner>"));
|
|
1438
|
+
if (segments.length > 3 || segments.some((segment) => segment === "" || segment.includes("\""))) return null;
|
|
1439
|
+
const table = segments.at(-1);
|
|
1440
|
+
const schema = segments.length > 1 ? segments.at(-2) : "";
|
|
1441
|
+
if (table === void 0 || schema === void 0) return null;
|
|
1442
|
+
return {
|
|
1443
|
+
schema,
|
|
1444
|
+
table
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
//#endregion
|
|
1448
|
+
//#region src/params.ts
|
|
1449
|
+
const TEXT_PARAM = Object.freeze({
|
|
1450
|
+
type: "text",
|
|
1451
|
+
text: ""
|
|
1452
|
+
});
|
|
1453
|
+
function untouched(rows) {
|
|
1454
|
+
const all = Object.values(rows);
|
|
1455
|
+
return all.length > 0 && all.every((row) => row.type === TEXT_PARAM.type && row.text === TEXT_PARAM.text);
|
|
1456
|
+
}
|
|
1457
|
+
const SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
|
1458
|
+
const DECIMAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
1459
|
+
const INTEGER = /^[+-]?\d+$/;
|
|
1460
|
+
function boundValue(param) {
|
|
1461
|
+
switch (param.type) {
|
|
1462
|
+
case "text": return {
|
|
1463
|
+
ok: true,
|
|
1464
|
+
value: param.text
|
|
1465
|
+
};
|
|
1466
|
+
case "null": return {
|
|
1467
|
+
ok: true,
|
|
1468
|
+
value: null
|
|
1469
|
+
};
|
|
1470
|
+
case "boolean":
|
|
1471
|
+
if (param.text === "true" || param.text === "false") return {
|
|
1472
|
+
ok: true,
|
|
1473
|
+
value: param.text === "true"
|
|
1474
|
+
};
|
|
1475
|
+
return {
|
|
1476
|
+
ok: false,
|
|
1477
|
+
refusal: "not-a-boolean"
|
|
1478
|
+
};
|
|
1479
|
+
case "number": {
|
|
1480
|
+
const text = param.text.trim();
|
|
1481
|
+
if (text === "") return {
|
|
1482
|
+
ok: false,
|
|
1483
|
+
refusal: "empty"
|
|
1484
|
+
};
|
|
1485
|
+
if (!DECIMAL.test(text)) return {
|
|
1486
|
+
ok: false,
|
|
1487
|
+
refusal: "not-a-number"
|
|
1488
|
+
};
|
|
1489
|
+
const value = Number(text);
|
|
1490
|
+
if (!Number.isFinite(value)) return {
|
|
1491
|
+
ok: false,
|
|
1492
|
+
refusal: "not-finite"
|
|
1493
|
+
};
|
|
1494
|
+
if (INTEGER.test(text)) {
|
|
1495
|
+
const exact = BigInt(text);
|
|
1496
|
+
if (exact > SAFE_INTEGER || exact < -SAFE_INTEGER) return {
|
|
1497
|
+
ok: false,
|
|
1498
|
+
refusal: "unsafe-integer"
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
ok: true,
|
|
1503
|
+
value
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function boundParams(markers, rows) {
|
|
1509
|
+
if (markers.form === "none") return {
|
|
1510
|
+
ok: true,
|
|
1511
|
+
params: void 0
|
|
1512
|
+
};
|
|
1513
|
+
const bound = {};
|
|
1514
|
+
const keys = markers.form === "positional" ? Array.from({ length: markers.count }, (_, index) => String(index + 1)) : markers.names;
|
|
1515
|
+
for (const key of keys) {
|
|
1516
|
+
const one = boundValue(rows[key] ?? TEXT_PARAM);
|
|
1517
|
+
if (!one.ok) return {
|
|
1518
|
+
ok: false,
|
|
1519
|
+
form: markers.form,
|
|
1520
|
+
key,
|
|
1521
|
+
refusal: one.refusal
|
|
1522
|
+
};
|
|
1523
|
+
bound[key] = one.value;
|
|
1524
|
+
}
|
|
1525
|
+
return {
|
|
1526
|
+
ok: true,
|
|
1527
|
+
params: markers.form === "positional" ? keys.map((key) => bound[key]) : bound
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
//#endregion
|
|
1531
|
+
export { SQL_DIALECTS, TEXT_PARAM, blankNoise, boundParams, boundValue, createSqlAssist, createSqlCompletion, createSqlFormat, createSqlFunctions, createSqlHover, createSqlSchema, createSqlSignature, insideNoise, joinGuesses, qualifiedName, readCall, readIdentifier, readStatement, runnableStatement, signatureParameters, spellCallable, spellIdentifier, spellQualifiedName, spellQualifier, statementMarkers, statementOrigin, untouched };
|