midql-cli 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.
@@ -0,0 +1,1643 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConnectionManager
4
+ } from "./chunk-3CIK5I4M.js";
5
+ import {
6
+ formatResultSet
7
+ } from "./chunk-7WNCISNV.js";
8
+ import {
9
+ historyFilePath,
10
+ loadConfig
11
+ } from "./chunk-3QAR6XBK.js";
12
+ import {
13
+ AmbiguityError,
14
+ MidqlError,
15
+ ParseError
16
+ } from "./chunk-VFC3HWTF.js";
17
+
18
+ // src/nl/clauses.ts
19
+ var limitWords = /* @__PURE__ */ new Set(["first", "top", "limit", "last"]);
20
+ var sortWords = /* @__PURE__ */ new Set(["sorted", "ordered", "sort", "order"]);
21
+ var conditionIntros = /* @__PURE__ */ new Set([
22
+ "where",
23
+ "which",
24
+ "that",
25
+ "who",
26
+ "whose",
27
+ "having",
28
+ "has",
29
+ "have",
30
+ "with"
31
+ ]);
32
+ function extractClauses(tokens) {
33
+ const subject = [];
34
+ const conditionTokens = [];
35
+ const sortTokens = [];
36
+ let limit = null;
37
+ let reverse = false;
38
+ let mode = "subject";
39
+ let index = 0;
40
+ while (index < tokens.length) {
41
+ const token = tokens[index];
42
+ const next = tokens[index + 1];
43
+ if (token.type === "word" && limitWords.has(token.value) && next?.type === "number") {
44
+ limit = Number(next.value);
45
+ if (token.value === "last") {
46
+ reverse = true;
47
+ }
48
+ index += 2;
49
+ continue;
50
+ }
51
+ if (token.type === "word" && sortWords.has(token.value) && next?.type === "word" && next.value === "by") {
52
+ mode = "sort";
53
+ index += 2;
54
+ continue;
55
+ }
56
+ if (mode === "subject" && token.type === "word" && conditionIntros.has(token.value)) {
57
+ mode = "conditions";
58
+ index += 1;
59
+ continue;
60
+ }
61
+ if (mode === "subject") {
62
+ subject.push(token);
63
+ } else if (mode === "conditions") {
64
+ conditionTokens.push(token);
65
+ } else {
66
+ sortTokens.push(token);
67
+ }
68
+ index += 1;
69
+ }
70
+ return { subject, conditionTokens, sortTokens, limit, reverse };
71
+ }
72
+
73
+ // src/nl/fuzzy.ts
74
+ import pluralize from "pluralize";
75
+ var ACCEPT_THRESHOLD = 0.72;
76
+ var AMBIGUITY_GAP = 0.08;
77
+ function normalizeIdentifier(value) {
78
+ return value.toLowerCase().replace(/[_\s-]/g, "");
79
+ }
80
+ function damerauLevenshtein(a, b) {
81
+ if (a === b) {
82
+ return 0;
83
+ }
84
+ const rows = a.length + 1;
85
+ const cols = b.length + 1;
86
+ const matrix = Array.from({ length: rows }, () => new Array(cols).fill(0));
87
+ for (let i = 0; i < rows; i++) {
88
+ matrix[i][0] = i;
89
+ }
90
+ for (let j = 0; j < cols; j++) {
91
+ matrix[0][j] = j;
92
+ }
93
+ for (let i = 1; i < rows; i++) {
94
+ for (let j = 1; j < cols; j++) {
95
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
96
+ let value = Math.min(
97
+ matrix[i - 1][j] + 1,
98
+ matrix[i][j - 1] + 1,
99
+ matrix[i - 1][j - 1] + cost
100
+ );
101
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
102
+ value = Math.min(value, matrix[i - 2][j - 2] + 1);
103
+ }
104
+ matrix[i][j] = value;
105
+ }
106
+ }
107
+ return matrix[a.length][b.length];
108
+ }
109
+ function similarity(a, b) {
110
+ const longest = Math.max(a.length, b.length);
111
+ if (longest === 0) {
112
+ return 1;
113
+ }
114
+ return 1 - damerauLevenshtein(a, b) / longest;
115
+ }
116
+ function scoreName(input, candidate) {
117
+ const inputLower = input.toLowerCase();
118
+ const candidateLower = candidate.toLowerCase();
119
+ if (inputLower === candidateLower) {
120
+ return 1;
121
+ }
122
+ if (pluralize.singular(inputLower) === pluralize.singular(candidateLower) || pluralize.plural(inputLower) === candidateLower || pluralize.singular(inputLower) === candidateLower) {
123
+ return 0.95;
124
+ }
125
+ const inputNorm = normalizeIdentifier(input);
126
+ const candidateNorm = normalizeIdentifier(candidate);
127
+ if (inputNorm === candidateNorm) {
128
+ return 0.9;
129
+ }
130
+ if (pluralize.singular(inputNorm) === pluralize.singular(candidateNorm) && inputNorm.length > 2) {
131
+ return 0.88;
132
+ }
133
+ if (candidateNorm.startsWith(inputNorm) && inputNorm.length >= 3) {
134
+ return 0.8;
135
+ }
136
+ const base = similarity(pluralize.singular(inputNorm), pluralize.singular(candidateNorm));
137
+ return Math.min(base, 0.79);
138
+ }
139
+ function rankCandidates(input, candidates, nameOf) {
140
+ return candidates.map((item) => ({ item, score: scoreName(input, nameOf(item)) })).sort((a, b) => b.score - a.score);
141
+ }
142
+
143
+ // src/nl/entities.ts
144
+ function findTable(name, schema) {
145
+ const ranked = rankCandidates(name, schema.tables, (table) => table.name);
146
+ const best = ranked[0];
147
+ if (!best || best.score < ACCEPT_THRESHOLD) {
148
+ return null;
149
+ }
150
+ return best.item;
151
+ }
152
+ function resolveTable(name, schema) {
153
+ const ranked = rankCandidates(name, schema.tables, (table) => table.name);
154
+ const best = ranked[0];
155
+ if (!best || best.score < ACCEPT_THRESHOLD) {
156
+ const nearest = ranked.slice(0, 3).filter((match) => match.score > 0.4);
157
+ throw new ParseError(`No table matching "${name}"`, {
158
+ suggestions: nearest.map((match) => match.item.name)
159
+ });
160
+ }
161
+ const second = ranked[1];
162
+ if (second && second.score >= ACCEPT_THRESHOLD && best.score - second.score < AMBIGUITY_GAP) {
163
+ throw new AmbiguityError(
164
+ name,
165
+ ranked.filter((match) => best.score - match.score < AMBIGUITY_GAP).map((match) => ({
166
+ value: match.item.name,
167
+ label: match.item.name,
168
+ detail: `table with ${match.item.columns.length} columns`
169
+ }))
170
+ );
171
+ }
172
+ return best.item;
173
+ }
174
+ function resolveTableFromTokens(tokens, startIndex, schema) {
175
+ const first = tokens[startIndex];
176
+ if (!first || first.type !== "word") {
177
+ throw new ParseError("Expected a table name", {
178
+ suggestions: schema.tables.slice(0, 3).map((table) => table.name)
179
+ });
180
+ }
181
+ const second = tokens[startIndex + 1];
182
+ if (second && second.type === "word") {
183
+ const combined = `${first.value}_${second.value}`;
184
+ const table = findTable(combined, schema);
185
+ if (table) {
186
+ return { table, consumed: 2 };
187
+ }
188
+ }
189
+ return { table: resolveTable(first.value, schema), consumed: 1 };
190
+ }
191
+ function resolveColumn(name, baseTable, schema) {
192
+ if (name.includes(".")) {
193
+ const [tablePart, columnPart] = name.split(".", 2);
194
+ const table = resolveTable(tablePart, schema);
195
+ const ranked2 = rankCandidates(columnPart, table.columns, (column) => column.name);
196
+ const best2 = ranked2[0];
197
+ if (!best2 || best2.score < ACCEPT_THRESHOLD) {
198
+ throw new ParseError(`No column matching "${columnPart}" in ${table.name}`, {
199
+ suggestions: ranked2.slice(0, 3).map((match) => match.item.name)
200
+ });
201
+ }
202
+ return {
203
+ tableName: table.name,
204
+ column: best2.item,
205
+ requiresJoin: table.name !== baseTable.name
206
+ };
207
+ }
208
+ const candidates = [];
209
+ for (const table of schema.tables) {
210
+ for (const column of table.columns) {
211
+ candidates.push({ tableName: table.name, column, base: table.name === baseTable.name });
212
+ }
213
+ }
214
+ const ranked = rankCandidates(name, candidates, (candidate) => candidate.column.name).map(
215
+ (match) => ({
216
+ ...match,
217
+ score: match.score + (match.item.base ? 0.05 : 0)
218
+ })
219
+ );
220
+ ranked.sort((a, b) => b.score - a.score);
221
+ const best = ranked[0];
222
+ if (!best || best.score < ACCEPT_THRESHOLD) {
223
+ const nearest = ranked.slice(0, 3).filter((match) => match.score > 0.4);
224
+ throw new ParseError(`No column matching "${name}"`, {
225
+ suggestions: nearest.map((match) => `${match.item.tableName}.${match.item.column.name}`)
226
+ });
227
+ }
228
+ if (!best.item.base) {
229
+ const contenders = ranked.filter(
230
+ (match) => match !== best && best.score - match.score < AMBIGUITY_GAP && match.score >= ACCEPT_THRESHOLD && match.item.tableName !== best.item.tableName
231
+ );
232
+ if (contenders.length > 0) {
233
+ throw new AmbiguityError(name, [
234
+ {
235
+ value: `${best.item.tableName}.${best.item.column.name}`,
236
+ label: `${best.item.tableName}.${best.item.column.name}`,
237
+ detail: best.item.column.dataType
238
+ },
239
+ ...contenders.map((match) => ({
240
+ value: `${match.item.tableName}.${match.item.column.name}`,
241
+ label: `${match.item.tableName}.${match.item.column.name}`,
242
+ detail: match.item.column.dataType
243
+ }))
244
+ ]);
245
+ }
246
+ }
247
+ return {
248
+ tableName: best.item.tableName,
249
+ column: best.item.column,
250
+ requiresJoin: best.item.tableName !== baseTable.name
251
+ };
252
+ }
253
+ function resolveColumnInTable(name, table) {
254
+ const ranked = rankCandidates(name, table.columns, (column) => column.name);
255
+ const best = ranked[0];
256
+ if (!best || best.score < ACCEPT_THRESHOLD) {
257
+ throw new ParseError(`No column matching "${name}" in ${table.name}`, {
258
+ suggestions: ranked.slice(0, 3).map((match) => match.item.name)
259
+ });
260
+ }
261
+ return best.item;
262
+ }
263
+
264
+ // src/nl/conditions.ts
265
+ var operatorRules = [
266
+ { words: ["is", "not", "null"], operator: "isNotNull" },
267
+ { words: ["is", "not", "empty"], operator: "isNotNull" },
268
+ { words: ["is", "null"], operator: "isNull" },
269
+ { words: ["is", "empty"], operator: "isNull" },
270
+ { words: ["is", "set"], operator: "isNotNull" },
271
+ { words: ["exists"], operator: "isNotNull" },
272
+ { words: ["greater", "than", "or", "equal", "to"], operator: ">=" },
273
+ { words: ["less", "than", "or", "equal", "to"], operator: "<=" },
274
+ { words: ["greater", "than"], operator: ">" },
275
+ { words: ["more", "than"], operator: ">" },
276
+ { words: ["bigger", "than"], operator: ">" },
277
+ { words: ["higher", "than"], operator: ">" },
278
+ { words: ["less", "than"], operator: "<" },
279
+ { words: ["fewer", "than"], operator: "<" },
280
+ { words: ["lower", "than"], operator: "<" },
281
+ { words: ["smaller", "than"], operator: "<" },
282
+ { words: ["at", "least"], operator: ">=" },
283
+ { words: ["at", "most"], operator: "<=" },
284
+ { words: ["over"], operator: ">" },
285
+ { words: ["above"], operator: ">" },
286
+ { words: ["under"], operator: "<" },
287
+ { words: ["below"], operator: "<" },
288
+ { words: ["is", "not"], operator: "!=" },
289
+ { words: ["not"], operator: "!=" },
290
+ { words: ["equal", "to"], operator: "=" },
291
+ { words: ["equals"], operator: "=" },
292
+ { words: ["is"], operator: "=" },
293
+ { words: ["contains"], operator: "ilike", pattern: "contains" },
294
+ { words: ["containing"], operator: "ilike", pattern: "contains" },
295
+ { words: ["includes"], operator: "ilike", pattern: "contains" },
296
+ { words: ["including"], operator: "ilike", pattern: "contains" },
297
+ { words: ["like"], operator: "ilike", pattern: "contains" },
298
+ { words: ["starts", "with"], operator: "ilike", pattern: "startsWith" },
299
+ { words: ["begins", "with"], operator: "ilike", pattern: "startsWith" },
300
+ { words: ["starting", "with"], operator: "ilike", pattern: "startsWith" },
301
+ { words: ["ends", "with"], operator: "ilike", pattern: "endsWith" },
302
+ { words: ["ending", "with"], operator: "ilike", pattern: "endsWith" },
303
+ { words: ["between"], operator: "between" },
304
+ { words: ["in"], operator: "in" }
305
+ ];
306
+ var skippableWords = /* @__PURE__ */ new Set(["has", "have", "had", "that", "who", "whose", "with", "value"]);
307
+ function isValueToken(token) {
308
+ return token !== void 0 && (token.type === "number" || token.type === "string" || token.type === "word");
309
+ }
310
+ function coerceValue(token, kind) {
311
+ if (token.type === "number") {
312
+ return Number(token.value);
313
+ }
314
+ if (token.type === "string") {
315
+ return token.value;
316
+ }
317
+ const word = token.value;
318
+ if (kind === "boolean" || word === "true" || word === "false") {
319
+ if (["true", "yes", "on"].includes(word)) {
320
+ return true;
321
+ }
322
+ if (["false", "no", "off"].includes(word)) {
323
+ return false;
324
+ }
325
+ }
326
+ if (word === "null" || word === "nothing" || word === "none") {
327
+ return null;
328
+ }
329
+ if (kind === "number" && /^-?\d+(\.\d+)?$/.test(word)) {
330
+ return Number(word);
331
+ }
332
+ return token.raw;
333
+ }
334
+ function applyPattern(value, pattern) {
335
+ if (typeof value !== "string" || !pattern) {
336
+ return value;
337
+ }
338
+ if (pattern === "contains") {
339
+ return `%${value}%`;
340
+ }
341
+ if (pattern === "startsWith") {
342
+ return `${value}%`;
343
+ }
344
+ return `%${value}`;
345
+ }
346
+ function matchOperator(tokens, index) {
347
+ const first = tokens[index];
348
+ if (!first) {
349
+ return null;
350
+ }
351
+ if (first.type === "operator") {
352
+ return {
353
+ rule: { words: [], operator: first.value },
354
+ consumed: 1
355
+ };
356
+ }
357
+ for (const rule of operatorRules) {
358
+ const matches = rule.words.every((word, offset) => {
359
+ const token = tokens[index + offset];
360
+ return token?.type === "word" && token.value === word;
361
+ });
362
+ if (matches && rule.words.length > 0) {
363
+ return { rule, consumed: rule.words.length };
364
+ }
365
+ }
366
+ return null;
367
+ }
368
+ function parseConditions(tokens, baseTable, schema) {
369
+ if (tokens.length === 0) {
370
+ return { where: null, foreignTables: [] };
371
+ }
372
+ const foreignTables = /* @__PURE__ */ new Set();
373
+ const conditions = [];
374
+ const connectors = [];
375
+ let index = 0;
376
+ while (index < tokens.length) {
377
+ while (tokens[index]?.type === "word" && skippableWords.has(tokens[index].value)) {
378
+ index += 1;
379
+ }
380
+ const columnToken = tokens[index];
381
+ if (!columnToken || columnToken.type !== "word") {
382
+ throw new ParseError("Expected a column name in the condition", {
383
+ position: columnToken?.position ?? tokens[tokens.length - 1]?.position
384
+ });
385
+ }
386
+ const resolved = resolveColumn(columnToken.value, baseTable, schema);
387
+ if (resolved.requiresJoin) {
388
+ foreignTables.add(resolved.tableName);
389
+ }
390
+ index += 1;
391
+ const operatorMatch = matchOperator(tokens, index);
392
+ let operator = "=";
393
+ let pattern;
394
+ if (operatorMatch) {
395
+ operator = operatorMatch.rule.operator;
396
+ pattern = operatorMatch.rule.pattern;
397
+ index += operatorMatch.consumed;
398
+ }
399
+ const columnRef = { table: resolved.tableName, column: resolved.column.name };
400
+ if (operator === "isNull" || operator === "isNotNull") {
401
+ conditions.push({ type: "condition", column: columnRef, operator, values: [] });
402
+ } else if (operator === "between") {
403
+ const low = tokens[index];
404
+ const connector = tokens[index + 1];
405
+ const high = tokens[index + 2];
406
+ if (!isValueToken(low) || connector?.value !== "and" || !isValueToken(high)) {
407
+ throw new ParseError(`"between" needs two values joined by "and"`, {
408
+ position: low?.position
409
+ });
410
+ }
411
+ conditions.push({
412
+ type: "condition",
413
+ column: columnRef,
414
+ operator,
415
+ values: [coerceValue(low, resolved.column.kind), coerceValue(high, resolved.column.kind)]
416
+ });
417
+ index += 3;
418
+ } else if (operator === "in") {
419
+ const values = [];
420
+ while (isValueToken(tokens[index])) {
421
+ values.push(coerceValue(tokens[index], resolved.column.kind));
422
+ index += 1;
423
+ const separator = tokens[index];
424
+ if (separator?.type === "comma" || separator?.type === "word" && separator.value === "or") {
425
+ index += 1;
426
+ } else {
427
+ break;
428
+ }
429
+ }
430
+ if (values.length === 0) {
431
+ throw new ParseError(`"in" needs at least one value`, {
432
+ position: tokens[index]?.position
433
+ });
434
+ }
435
+ conditions.push({ type: "condition", column: columnRef, operator, values });
436
+ } else {
437
+ const valueToken = tokens[index];
438
+ if (!isValueToken(valueToken)) {
439
+ throw new ParseError(`Expected a value for "${resolved.column.name}"`, {
440
+ position: valueToken?.position ?? columnToken.position
441
+ });
442
+ }
443
+ const value = coerceValue(valueToken, resolved.column.kind);
444
+ if (value === null) {
445
+ conditions.push({
446
+ type: "condition",
447
+ column: columnRef,
448
+ operator: operator === "!=" ? "isNotNull" : "isNull",
449
+ values: []
450
+ });
451
+ } else {
452
+ conditions.push({
453
+ type: "condition",
454
+ column: columnRef,
455
+ operator,
456
+ values: [applyPattern(value, pattern)]
457
+ });
458
+ }
459
+ index += 1;
460
+ }
461
+ const connectorToken = tokens[index];
462
+ if (connectorToken?.type === "word" && (connectorToken.value === "and" || connectorToken.value === "or")) {
463
+ connectors.push(connectorToken.value);
464
+ index += 1;
465
+ continue;
466
+ }
467
+ if (connectorToken !== void 0 && connectorToken.type === "comma") {
468
+ connectors.push("and");
469
+ index += 1;
470
+ continue;
471
+ }
472
+ if (connectorToken !== void 0) {
473
+ throw new ParseError(`Unexpected "${connectorToken.raw}" in condition`, {
474
+ position: connectorToken.position
475
+ });
476
+ }
477
+ }
478
+ return { where: groupConditions(conditions, connectors), foreignTables: [...foreignTables] };
479
+ }
480
+ function groupConditions(conditions, connectors) {
481
+ if (conditions.length === 0) {
482
+ return null;
483
+ }
484
+ if (conditions.length === 1) {
485
+ return conditions[0];
486
+ }
487
+ const orGroups = [[conditions[0]]];
488
+ connectors.forEach((connector, index) => {
489
+ const condition = conditions[index + 1];
490
+ if (!condition) {
491
+ return;
492
+ }
493
+ if (connector === "or") {
494
+ orGroups.push([condition]);
495
+ } else {
496
+ orGroups[orGroups.length - 1].push(condition);
497
+ }
498
+ });
499
+ const andNodes = orGroups.map(
500
+ (group) => group.length === 1 ? group[0] : { type: "group", combinator: "and", items: group }
501
+ );
502
+ if (andNodes.length === 1) {
503
+ return andNodes[0];
504
+ }
505
+ return { type: "group", combinator: "or", items: andNodes };
506
+ }
507
+
508
+ // src/nl/intent.ts
509
+ var selectVerbs = /* @__PURE__ */ new Set([
510
+ "show",
511
+ "list",
512
+ "get",
513
+ "select",
514
+ "find",
515
+ "search",
516
+ "display",
517
+ "fetch",
518
+ "give",
519
+ "view"
520
+ ]);
521
+ var insertVerbs = /* @__PURE__ */ new Set(["add", "insert", "register"]);
522
+ var updateVerbs = /* @__PURE__ */ new Set(["update", "change", "modify", "set"]);
523
+ var deleteVerbs = /* @__PURE__ */ new Set(["delete", "remove", "erase"]);
524
+ var describeVerbs = /* @__PURE__ */ new Set(["describe", "desc", "inspect"]);
525
+ var aggregateWords = /* @__PURE__ */ new Map([
526
+ ["average", "avg"],
527
+ ["avg", "avg"],
528
+ ["mean", "avg"],
529
+ ["sum", "sum"],
530
+ ["total", "sum"],
531
+ ["min", "min"],
532
+ ["minimum", "min"],
533
+ ["lowest", "min"],
534
+ ["smallest", "min"],
535
+ ["max", "max"],
536
+ ["maximum", "max"],
537
+ ["highest", "max"],
538
+ ["largest", "max"],
539
+ ["biggest", "max"]
540
+ ]);
541
+ var allVerbs = [
542
+ ...selectVerbs,
543
+ ...insertVerbs,
544
+ ...updateVerbs,
545
+ ...deleteVerbs,
546
+ ...describeVerbs,
547
+ ...aggregateWords.keys(),
548
+ "count",
549
+ "create",
550
+ "drop",
551
+ "how",
552
+ "what"
553
+ ];
554
+ function isTablesWord(token) {
555
+ return token?.type === "word" && (token.value === "tables" || token.value === "table");
556
+ }
557
+ function classifyIntent(tokens) {
558
+ const first = tokens[0];
559
+ if (!first || first.type !== "word") {
560
+ throw new ParseError("Could not understand the input", {
561
+ suggestions: ["show all users", "count orders", "\\help"]
562
+ });
563
+ }
564
+ const second = tokens[1];
565
+ const rest = tokens.slice(1);
566
+ if ((selectVerbs.has(first.value) || first.value === "what") && isTablesWord(second) && tokens.length <= 3) {
567
+ return { intent: "showTables", tokens: [], aggregate: null };
568
+ }
569
+ if (first.value === "tables" && tokens.length === 1) {
570
+ return { intent: "showTables", tokens: [], aggregate: null };
571
+ }
572
+ if (describeVerbs.has(first.value)) {
573
+ return { intent: "describe", tokens: rest, aggregate: null };
574
+ }
575
+ if (first.value === "what") {
576
+ const inIndex = tokens.findIndex((token) => token.type === "word" && token.value === "in");
577
+ if (inIndex >= 0) {
578
+ return { intent: "describe", tokens: tokens.slice(inIndex + 1), aggregate: null };
579
+ }
580
+ throw new ParseError(`Could not understand "what ..." \u2014 try "what is in <table>"`);
581
+ }
582
+ if (selectVerbs.has(first.value) && second?.type === "word" && second.value === "columns") {
583
+ const ofIndex = tokens.findIndex(
584
+ (token, index) => index > 1 && token.type === "word" && (token.value === "of" || token.value === "in" || token.value === "from")
585
+ );
586
+ return {
587
+ intent: "describe",
588
+ tokens: ofIndex >= 0 ? tokens.slice(ofIndex + 1) : tokens.slice(2),
589
+ aggregate: null
590
+ };
591
+ }
592
+ if (first.value === "count") {
593
+ return { intent: "count", tokens: rest, aggregate: null };
594
+ }
595
+ if (first.value === "how" && second?.type === "word" && second.value === "many") {
596
+ return { intent: "count", tokens: tokens.slice(2), aggregate: null };
597
+ }
598
+ const aggregate = aggregateWords.get(first.value);
599
+ if (aggregate) {
600
+ return { intent: "aggregate", tokens: rest, aggregate };
601
+ }
602
+ if (first.value === "create") {
603
+ if (isTablesWord(second)) {
604
+ return { intent: "createTable", tokens: tokens.slice(2), aggregate: null };
605
+ }
606
+ return { intent: "insert", tokens: rest, aggregate: null };
607
+ }
608
+ if (first.value === "drop") {
609
+ return {
610
+ intent: "dropTable",
611
+ tokens: isTablesWord(second) ? tokens.slice(2) : rest,
612
+ aggregate: null
613
+ };
614
+ }
615
+ if (deleteVerbs.has(first.value)) {
616
+ if (isTablesWord(second) && tokens.length === 3) {
617
+ return { intent: "dropTable", tokens: tokens.slice(2), aggregate: null };
618
+ }
619
+ return { intent: "delete", tokens: rest, aggregate: null };
620
+ }
621
+ if (insertVerbs.has(first.value)) {
622
+ return { intent: "insert", tokens: rest, aggregate: null };
623
+ }
624
+ if (updateVerbs.has(first.value)) {
625
+ return { intent: "update", tokens: rest, aggregate: null };
626
+ }
627
+ if (selectVerbs.has(first.value)) {
628
+ return { intent: "select", tokens: rest, aggregate: null };
629
+ }
630
+ const nearest = rankCandidates(first.value, allVerbs, (verb) => verb).slice(0, 3).filter((match) => match.score > 0.5).map((match) => match.item);
631
+ throw new ParseError(`Don't know how to "${first.raw}"`, {
632
+ position: first.position,
633
+ suggestions: nearest.length > 0 ? nearest : ["show", "count", "add", "update", "delete"]
634
+ });
635
+ }
636
+
637
+ // src/nl/joins.ts
638
+ function buildFkGraph(schema) {
639
+ const graph = /* @__PURE__ */ new Map();
640
+ const add = (edge) => {
641
+ const edges = graph.get(edge.from) ?? [];
642
+ edges.push(edge);
643
+ graph.set(edge.from, edges);
644
+ };
645
+ for (const table of schema.tables) {
646
+ for (const fk of table.foreignKeys) {
647
+ const fromColumn = fk.columns[0];
648
+ const toColumn = fk.referencedColumns[0];
649
+ if (!fromColumn || !toColumn) {
650
+ continue;
651
+ }
652
+ add({ from: table.name, to: fk.referencedTable, fromColumn, toColumn });
653
+ add({ from: fk.referencedTable, to: table.name, fromColumn: toColumn, toColumn: fromColumn });
654
+ }
655
+ }
656
+ return graph;
657
+ }
658
+ function describePath(base, path) {
659
+ return [base, ...path.map((edge) => `${edge.to} (on ${edge.from}.${edge.fromColumn})`)].join(
660
+ " \u2192 "
661
+ );
662
+ }
663
+ function findJoinPath(baseTable, targetTable, schema, maxDepth) {
664
+ if (baseTable === targetTable) {
665
+ return [];
666
+ }
667
+ const graph = buildFkGraph(schema);
668
+ let frontier = [{ table: baseTable, path: [] }];
669
+ const visitedDepth = /* @__PURE__ */ new Map([[baseTable, 0]]);
670
+ for (let depth = 1; depth <= maxDepth; depth++) {
671
+ const next = [];
672
+ const arrivals = [];
673
+ for (const state of frontier) {
674
+ for (const edge of graph.get(state.table) ?? []) {
675
+ const known = visitedDepth.get(edge.to);
676
+ if (known !== void 0 && known < depth) {
677
+ continue;
678
+ }
679
+ visitedDepth.set(edge.to, depth);
680
+ const extended = { table: edge.to, path: [...state.path, edge] };
681
+ if (edge.to === targetTable) {
682
+ arrivals.push(extended);
683
+ } else {
684
+ next.push(extended);
685
+ }
686
+ }
687
+ }
688
+ if (arrivals.length > 0) {
689
+ const unique = new Map(arrivals.map((state) => [describePath(baseTable, state.path), state]));
690
+ if (unique.size > 1) {
691
+ throw new AmbiguityError(
692
+ targetTable,
693
+ [...unique.values()].map((state) => ({
694
+ value: describePath(baseTable, state.path),
695
+ label: describePath(baseTable, state.path),
696
+ detail: `${state.path.length} join${state.path.length === 1 ? "" : "s"}`
697
+ }))
698
+ );
699
+ }
700
+ return pathToJoins(arrivals[0].path);
701
+ }
702
+ frontier = next;
703
+ if (frontier.length === 0) {
704
+ break;
705
+ }
706
+ }
707
+ throw new ParseError(
708
+ `No relationship found between "${baseTable}" and "${targetTable}" within ${maxDepth} joins`,
709
+ { suggestions: [`\\d ${baseTable}`, `\\d ${targetTable}`, "write the join in raw SQL"] }
710
+ );
711
+ }
712
+ function pathToJoins(path) {
713
+ return path.map((edge) => ({
714
+ table: edge.to,
715
+ leftColumn: { table: edge.from, column: edge.fromColumn },
716
+ rightColumn: { table: edge.to, column: edge.toColumn }
717
+ }));
718
+ }
719
+ function resolveJoins(baseTable, foreignTables, schema, maxDepth) {
720
+ const joins = [];
721
+ const joined = /* @__PURE__ */ new Set([baseTable]);
722
+ for (const target of foreignTables) {
723
+ if (joined.has(target)) {
724
+ continue;
725
+ }
726
+ for (const join of findJoinPath(baseTable, target, schema, maxDepth)) {
727
+ if (!joined.has(join.table)) {
728
+ joins.push(join);
729
+ joined.add(join.table);
730
+ }
731
+ }
732
+ }
733
+ return joins;
734
+ }
735
+
736
+ // src/nl/subject.ts
737
+ function splitSubject(subject) {
738
+ const connectorIndex = subject.findIndex(
739
+ (token) => token.type === "word" && (token.value === "of" || token.value === "from")
740
+ );
741
+ if (connectorIndex < 0) {
742
+ return { columnTokens: [], tableTokens: subject };
743
+ }
744
+ return {
745
+ columnTokens: subject.slice(0, connectorIndex),
746
+ tableTokens: subject.slice(connectorIndex + 1)
747
+ };
748
+ }
749
+ function parseColumnList(tokens, table) {
750
+ const refs = [];
751
+ for (const token of tokens) {
752
+ if (token.type === "comma" || token.type === "word" && token.value === "and") {
753
+ continue;
754
+ }
755
+ if (token.type !== "word") {
756
+ throw new ParseError(`Expected a column name, got "${token.raw}"`, {
757
+ position: token.position
758
+ });
759
+ }
760
+ const column = resolveColumnInTable(token.value, table);
761
+ refs.push({ table: table.name, column: column.name });
762
+ }
763
+ return refs;
764
+ }
765
+ var nameLikeColumns = ["name", "title", "username", "email", "label", "slug"];
766
+ function nameLikeCondition(table, token) {
767
+ if (token.type === "number") {
768
+ const pk = table.primaryKey[0];
769
+ if (!pk) {
770
+ throw new ParseError(
771
+ `"${table.name}" has no primary key; use an explicit condition like "where id ${token.value}"`
772
+ );
773
+ }
774
+ return {
775
+ type: "condition",
776
+ column: { table: table.name, column: pk },
777
+ operator: "=",
778
+ values: [Number(token.value)]
779
+ };
780
+ }
781
+ const column = table.columns.find((candidate) => nameLikeColumns.includes(candidate.name)) ?? table.columns.find((candidate) => candidate.kind === "text" && !candidate.isPrimaryKey);
782
+ if (!column) {
783
+ throw new ParseError(
784
+ `Can't guess which column "${token.raw}" refers to in ${table.name}; use "where <column> is ${token.raw}"`
785
+ );
786
+ }
787
+ const value = token.type === "string" ? token.value : token.raw;
788
+ return {
789
+ type: "condition",
790
+ column: { table: table.name, column: column.name },
791
+ operator: "=",
792
+ values: [value]
793
+ };
794
+ }
795
+ function mergeWhere(existing, extra) {
796
+ if (!existing) {
797
+ return extra;
798
+ }
799
+ if (!extra) {
800
+ return existing;
801
+ }
802
+ return { type: "group", combinator: "and", items: [existing, extra] };
803
+ }
804
+
805
+ // src/nl/mutations.ts
806
+ var assignmentConnectors = /* @__PURE__ */ new Set(["is", "as", "to", "of"]);
807
+ var conditionIntros2 = /* @__PURE__ */ new Set(["where", "which", "that", "whose", "having"]);
808
+ function isValueToken2(token) {
809
+ return token !== void 0 && (token.type === "number" || token.type === "string" || token.type === "word");
810
+ }
811
+ function parseAssignments(tokens, table) {
812
+ const values = {};
813
+ let index = 0;
814
+ while (index < tokens.length) {
815
+ const token = tokens[index];
816
+ if (token.type === "comma" || token.type === "word" && token.value === "and") {
817
+ index += 1;
818
+ continue;
819
+ }
820
+ if (token.type === "word" && conditionIntros2.has(token.value)) {
821
+ break;
822
+ }
823
+ if (token.type !== "word") {
824
+ throw new ParseError(`Expected a column name, got "${token.raw}"`, {
825
+ position: token.position
826
+ });
827
+ }
828
+ const columnName = token.value === "named" ? "name" : token.value;
829
+ const column = resolveColumnInTable(columnName, table);
830
+ index += 1;
831
+ const connector = tokens[index];
832
+ if (connector && (connector.type === "operator" && connector.value === "=" || connector.type === "word" && assignmentConnectors.has(connector.value))) {
833
+ index += 1;
834
+ }
835
+ const valueToken = tokens[index];
836
+ if (!isValueToken2(valueToken)) {
837
+ throw new ParseError(`Expected a value for "${column.name}"`, {
838
+ position: token.position
839
+ });
840
+ }
841
+ values[column.name] = coerceValue(valueToken, column.kind);
842
+ index += 1;
843
+ }
844
+ if (Object.keys(values).length === 0) {
845
+ throw new ParseError(`Expected column/value pairs, e.g. "name john and rank 5"`);
846
+ }
847
+ return { values, consumed: index };
848
+ }
849
+ function parseInsert(tokens, schema) {
850
+ let index = 0;
851
+ if (tokens[index]?.type === "word" && tokens[index].value === "into") {
852
+ index += 1;
853
+ }
854
+ const { table, consumed } = resolveTableFromTokens(tokens, index, schema);
855
+ index += consumed;
856
+ const introducer = tokens[index];
857
+ if (introducer?.type === "word" && (introducer.value === "with" || introducer.value === "values" || introducer.value === "set")) {
858
+ index += 1;
859
+ }
860
+ const { values } = parseAssignments(tokens.slice(index), table);
861
+ return { kind: "insert", table: table.name, values };
862
+ }
863
+ function parseUpdate(tokens, schema, maxJoinDepth) {
864
+ const ofIndex = tokens.findIndex((token) => token.type === "word" && token.value === "of");
865
+ const setIndex = tokens.findIndex((token) => token.type === "word" && token.value === "set");
866
+ if (ofIndex > 0 && (setIndex < 0 || ofIndex < setIndex)) {
867
+ return parseColumnFirstUpdate(tokens, ofIndex, schema, maxJoinDepth);
868
+ }
869
+ const { table, consumed } = resolveTableFromTokens(tokens, 0, schema);
870
+ let index = consumed;
871
+ if (tokens[index]?.type === "word" && tokens[index].value === "set") {
872
+ index += 1;
873
+ }
874
+ const rest = tokens.slice(index);
875
+ const { values, consumed: assignmentConsumed } = parseAssignments(rest, table);
876
+ const conditionTokens = stripConditionIntro(rest.slice(assignmentConsumed));
877
+ const parsed = parseConditions(conditionTokens, table, schema);
878
+ return {
879
+ kind: "update",
880
+ table: table.name,
881
+ set: values,
882
+ joins: resolveJoins(table.name, parsed.foreignTables, schema, maxJoinDepth),
883
+ where: parsed.where,
884
+ keyColumn: table.primaryKey[0] ?? null
885
+ };
886
+ }
887
+ function parseColumnFirstUpdate(tokens, ofIndex, schema, maxJoinDepth) {
888
+ const columnToken = tokens[ofIndex - 1];
889
+ if (!columnToken || columnToken.type !== "word") {
890
+ throw new ParseError(`Expected a column before "of"`, { position: tokens[ofIndex]?.position });
891
+ }
892
+ const { table, consumed } = resolveTableFromTokens(tokens, ofIndex + 1, schema);
893
+ const column = resolveColumnInTable(columnToken.value, table);
894
+ let index = ofIndex + 1 + consumed;
895
+ let bareCondition = null;
896
+ const maybeBare = tokens[index];
897
+ if (isValueToken2(maybeBare) && !(maybeBare.type === "word" && maybeBare.value === "to")) {
898
+ bareCondition = nameLikeCondition(table, maybeBare);
899
+ index += 1;
900
+ }
901
+ const toToken = tokens[index];
902
+ if (!(toToken?.type === "word" && toToken.value === "to") && !(toToken?.type === "operator" && toToken.value === "=")) {
903
+ throw new ParseError(`Expected "to <value>" after the column`, {
904
+ position: toToken?.position ?? columnToken.position
905
+ });
906
+ }
907
+ index += 1;
908
+ const valueToken = tokens[index];
909
+ if (!isValueToken2(valueToken)) {
910
+ throw new ParseError(`Expected a value after "to"`, { position: toToken.position });
911
+ }
912
+ index += 1;
913
+ const conditionTokens = stripConditionIntro(tokens.slice(index));
914
+ const parsed = parseConditions(conditionTokens, table, schema);
915
+ return {
916
+ kind: "update",
917
+ table: table.name,
918
+ set: { [column.name]: coerceValue(valueToken, column.kind) },
919
+ joins: resolveJoins(table.name, parsed.foreignTables, schema, maxJoinDepth),
920
+ where: mergeWhere(bareCondition, parsed.where),
921
+ keyColumn: table.primaryKey[0] ?? null
922
+ };
923
+ }
924
+ function stripConditionIntro(tokens) {
925
+ const first = tokens[0];
926
+ if (first?.type === "word" && (conditionIntros2.has(first.value) || first.value === "with")) {
927
+ return tokens.slice(1);
928
+ }
929
+ return tokens;
930
+ }
931
+ function parseDelete(tokens, schema, maxJoinDepth) {
932
+ const clauses = extractClauses(tokens);
933
+ const { table, consumed } = resolveTableFromTokens(clauses.subject, 0, schema);
934
+ const leftover = clauses.subject[consumed];
935
+ const bareCondition = isValueToken2(leftover) ? nameLikeCondition(table, leftover) : null;
936
+ const parsed = parseConditions(clauses.conditionTokens, table, schema);
937
+ return {
938
+ kind: "delete",
939
+ table: table.name,
940
+ joins: resolveJoins(table.name, parsed.foreignTables, schema, maxJoinDepth),
941
+ where: mergeWhere(bareCondition, parsed.where),
942
+ keyColumn: table.primaryKey[0] ?? null
943
+ };
944
+ }
945
+ var typeWords = /* @__PURE__ */ new Map([
946
+ ["text", "text"],
947
+ ["string", "text"],
948
+ ["varchar", "text"],
949
+ ["number", "number"],
950
+ ["int", "number"],
951
+ ["integer", "number"],
952
+ ["decimal", "decimal"],
953
+ ["float", "decimal"],
954
+ ["double", "decimal"],
955
+ ["money", "decimal"],
956
+ ["numeric", "decimal"],
957
+ ["bool", "boolean"],
958
+ ["boolean", "boolean"],
959
+ ["date", "date"],
960
+ ["timestamp", "timestamp"],
961
+ ["datetime", "timestamp"],
962
+ ["time", "timestamp"],
963
+ ["json", "json"],
964
+ ["uuid", "uuid"],
965
+ ["serial", "serial"]
966
+ ]);
967
+ function referencedType(kind) {
968
+ if (kind === "uuid") {
969
+ return "uuid";
970
+ }
971
+ if (kind === "text") {
972
+ return "text";
973
+ }
974
+ return "number";
975
+ }
976
+ function parseCreateTable(tokens, schema) {
977
+ const nameToken = tokens[0];
978
+ if (!nameToken || nameToken.type !== "word" || !/^[a-z_][\w]*$/i.test(nameToken.raw)) {
979
+ throw new ParseError('Expected a valid table name after "create table"');
980
+ }
981
+ let index = 1;
982
+ if (tokens[index]?.type === "word" && tokens[index].value === "with") {
983
+ index += 1;
984
+ }
985
+ const columns = [];
986
+ while (index < tokens.length) {
987
+ const token = tokens[index];
988
+ if (token.type === "comma" || token.type === "word" && token.value === "and") {
989
+ index += 1;
990
+ continue;
991
+ }
992
+ if (token.type !== "word") {
993
+ throw new ParseError(`Expected a column name, got "${token.raw}"`, {
994
+ position: token.position
995
+ });
996
+ }
997
+ const columnName = token.value;
998
+ index += 1;
999
+ const specifier = tokens[index];
1000
+ if (specifier?.type === "word" && specifier.value === "references") {
1001
+ const targetToken = tokens[index + 1];
1002
+ if (!targetToken || targetToken.type !== "word") {
1003
+ throw new ParseError(`Expected a table after "references"`, {
1004
+ position: specifier.position
1005
+ });
1006
+ }
1007
+ const target = resolveTable(targetToken.value, schema);
1008
+ const referencedColumn = target.primaryKey[0] ?? "id";
1009
+ const referenced = target.columns.find((column) => column.name === referencedColumn);
1010
+ columns.push({
1011
+ name: columnName,
1012
+ type: referenced ? referencedType(referenced.kind) : "number",
1013
+ nullable: true,
1014
+ references: { table: target.name, column: referencedColumn }
1015
+ });
1016
+ index += 2;
1017
+ continue;
1018
+ }
1019
+ if (specifier?.type === "word" && typeWords.has(specifier.value)) {
1020
+ columns.push({
1021
+ name: columnName,
1022
+ type: typeWords.get(specifier.value),
1023
+ nullable: true,
1024
+ references: null
1025
+ });
1026
+ index += 1;
1027
+ continue;
1028
+ }
1029
+ throw new ParseError(
1030
+ `Expected a type for "${columnName}" (text, number, decimal, boolean, date, timestamp, json, uuid) or "references <table>"`,
1031
+ { position: specifier?.position ?? token.position }
1032
+ );
1033
+ }
1034
+ if (columns.length === 0) {
1035
+ throw new ParseError(
1036
+ `Expected column definitions, e.g. "create table pets with name text, age number, owner references users"`
1037
+ );
1038
+ }
1039
+ if (!columns.some((column) => column.name === "id")) {
1040
+ columns.unshift({ name: "id", type: "serial", nullable: false, references: null });
1041
+ }
1042
+ return { kind: "createTable", table: nameToken.raw, columns };
1043
+ }
1044
+ function parseDropTable(tokens, schema) {
1045
+ const nameToken = tokens[0];
1046
+ if (!nameToken || nameToken.type !== "word") {
1047
+ throw new ParseError(`Expected a table name after "drop table"`);
1048
+ }
1049
+ const table = resolveTable(nameToken.value, schema);
1050
+ return { kind: "dropTable", table: table.name };
1051
+ }
1052
+
1053
+ // src/nl/tokenizer.ts
1054
+ var operatorPattern = /^(>=|<=|!=|<>|=|>|<)/;
1055
+ var datePattern = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?)?/;
1056
+ var numberPattern = /^-?\d+(?:\.\d+)?(?![\w-])/;
1057
+ var wordPattern = /^[\w.]+/;
1058
+ function tokenize(input) {
1059
+ const tokens = [];
1060
+ let index = 0;
1061
+ while (index < input.length) {
1062
+ const char = input[index];
1063
+ if (/\s/.test(char)) {
1064
+ index += 1;
1065
+ continue;
1066
+ }
1067
+ const rest = input.slice(index);
1068
+ if (char === "'" || char === '"') {
1069
+ const closing = input.indexOf(char, index + 1);
1070
+ const end = closing === -1 ? input.length : closing;
1071
+ const value = input.slice(index + 1, end);
1072
+ tokens.push({ type: "string", value, raw: input.slice(index, end + 1), position: index });
1073
+ index = end + 1;
1074
+ continue;
1075
+ }
1076
+ if (char === ",") {
1077
+ tokens.push({ type: "comma", value: ",", raw: ",", position: index });
1078
+ index += 1;
1079
+ continue;
1080
+ }
1081
+ const operatorMatch = operatorPattern.exec(rest);
1082
+ if (operatorMatch) {
1083
+ const value = operatorMatch[0] === "<>" ? "!=" : operatorMatch[0];
1084
+ tokens.push({ type: "operator", value, raw: operatorMatch[0], position: index });
1085
+ index += operatorMatch[0].length;
1086
+ continue;
1087
+ }
1088
+ const dateMatch = datePattern.exec(rest);
1089
+ if (dateMatch) {
1090
+ tokens.push({ type: "string", value: dateMatch[0], raw: dateMatch[0], position: index });
1091
+ index += dateMatch[0].length;
1092
+ continue;
1093
+ }
1094
+ const numberMatch = numberPattern.exec(rest);
1095
+ if (numberMatch) {
1096
+ tokens.push({ type: "number", value: numberMatch[0], raw: numberMatch[0], position: index });
1097
+ index += numberMatch[0].length;
1098
+ continue;
1099
+ }
1100
+ const wordMatch = wordPattern.exec(rest);
1101
+ if (wordMatch) {
1102
+ tokens.push({
1103
+ type: "word",
1104
+ value: wordMatch[0].toLowerCase(),
1105
+ raw: wordMatch[0],
1106
+ position: index
1107
+ });
1108
+ index += wordMatch[0].length;
1109
+ continue;
1110
+ }
1111
+ index += 1;
1112
+ }
1113
+ return tokens;
1114
+ }
1115
+ var fillerWords = /* @__PURE__ */ new Set(["the", "a", "an", "me", "please", "all", "every", "new"]);
1116
+ function dropFillers(tokens) {
1117
+ return tokens.filter((token) => token.type !== "word" || !fillerWords.has(token.value));
1118
+ }
1119
+
1120
+ // src/nl/translate.ts
1121
+ var defaultOptions = { maxJoinDepth: 3 };
1122
+ function translate(input, schema, options) {
1123
+ const { maxJoinDepth } = { ...defaultOptions, ...options };
1124
+ const tokens = dropFillers(tokenize(input));
1125
+ if (tokens.length === 0) {
1126
+ throw new ParseError("Empty input");
1127
+ }
1128
+ const classified = classifyIntent(tokens);
1129
+ switch (classified.intent) {
1130
+ case "showTables":
1131
+ return { kind: "showTables" };
1132
+ case "describe":
1133
+ return parseDescribe(classified.tokens, schema);
1134
+ case "select":
1135
+ return parseSelect(classified.tokens, schema, maxJoinDepth, null);
1136
+ case "count":
1137
+ return parseSelect(classified.tokens, schema, maxJoinDepth, {
1138
+ fn: "count",
1139
+ column: null
1140
+ });
1141
+ case "aggregate":
1142
+ return parseAggregate(classified.tokens, classified.aggregate, schema, maxJoinDepth);
1143
+ case "insert":
1144
+ return parseInsert(classified.tokens, schema);
1145
+ case "update":
1146
+ return parseUpdate(classified.tokens, schema, maxJoinDepth);
1147
+ case "delete":
1148
+ return parseDelete(classified.tokens, schema, maxJoinDepth);
1149
+ case "createTable":
1150
+ return parseCreateTable(classified.tokens, schema);
1151
+ case "dropTable":
1152
+ return parseDropTable(classified.tokens, schema);
1153
+ }
1154
+ }
1155
+ function parseDescribe(tokens, schema) {
1156
+ const meaningful = tokens.filter(
1157
+ (token) => !(token.type === "word" && (token.value === "is" || token.value === "in"))
1158
+ );
1159
+ const { table } = resolveTableFromTokens(meaningful, 0, schema);
1160
+ return { kind: "describe", table: table.name };
1161
+ }
1162
+ function parseSelect(tokens, schema, maxJoinDepth, aggregate) {
1163
+ const clauses = extractClauses(tokens);
1164
+ const { columnTokens, tableTokens } = splitSubject(clauses.subject);
1165
+ const { table, consumed } = resolveTableFromTokens(tableTokens, 0, schema);
1166
+ const leftover = tableTokens[consumed];
1167
+ const columns = columnTokens.length > 0 && !aggregate ? parseColumnList(columnTokens, table) : "*";
1168
+ const parsed = parseConditions(clauses.conditionTokens, table, schema);
1169
+ const foreignTables = new Set(parsed.foreignTables);
1170
+ let where = parsed.where;
1171
+ if (leftover && (leftover.type !== "word" || !isConnectorWord(leftover.value))) {
1172
+ where = mergeWhere(nameLikeCondition(table, leftover), where);
1173
+ }
1174
+ const orderBy = parseSort(clauses, table, schema, foreignTables);
1175
+ const joins = resolveJoins(table.name, [...foreignTables], schema, maxJoinDepth);
1176
+ return {
1177
+ kind: "select",
1178
+ table: table.name,
1179
+ columns,
1180
+ aggregate,
1181
+ joins,
1182
+ where,
1183
+ orderBy,
1184
+ limit: clauses.limit,
1185
+ offset: null
1186
+ };
1187
+ }
1188
+ function isConnectorWord(value) {
1189
+ return value === "and" || value === "or";
1190
+ }
1191
+ var descendingWords = /* @__PURE__ */ new Set(["descending", "desc", "reverse", "reversed", "highest"]);
1192
+ var ascendingWords = /* @__PURE__ */ new Set(["ascending", "asc", "lowest"]);
1193
+ function parseSort(clauses, table, schema, foreignTables) {
1194
+ const orderBy = [];
1195
+ let current = null;
1196
+ for (const token of clauses.sortTokens) {
1197
+ if (token.type === "comma" || token.type === "word" && token.value === "and") {
1198
+ continue;
1199
+ }
1200
+ if (token.type === "word" && descendingWords.has(token.value)) {
1201
+ if (current) {
1202
+ current.direction = "desc";
1203
+ }
1204
+ continue;
1205
+ }
1206
+ if (token.type === "word" && ascendingWords.has(token.value)) {
1207
+ continue;
1208
+ }
1209
+ if (token.type !== "word") {
1210
+ throw new ParseError(`Expected a column to sort by, got "${token.raw}"`, {
1211
+ position: token.position
1212
+ });
1213
+ }
1214
+ const resolved = resolveColumn(token.value, table, schema);
1215
+ if (resolved.requiresJoin) {
1216
+ foreignTables.add(resolved.tableName);
1217
+ }
1218
+ current = {
1219
+ column: { table: resolved.tableName, column: resolved.column.name },
1220
+ direction: "asc"
1221
+ };
1222
+ orderBy.push(current);
1223
+ }
1224
+ if (orderBy.length === 0 && clauses.reverse) {
1225
+ const pk = table.primaryKey[0];
1226
+ if (pk) {
1227
+ orderBy.push({ column: { table: table.name, column: pk }, direction: "desc" });
1228
+ }
1229
+ }
1230
+ return orderBy;
1231
+ }
1232
+ function parseAggregate(tokens, fn, schema, maxJoinDepth) {
1233
+ const clauses = extractClauses(tokens);
1234
+ let { columnTokens, tableTokens } = splitSubject(clauses.subject);
1235
+ if (columnTokens.length === 0) {
1236
+ const inIndex = clauses.subject.findIndex(
1237
+ (token) => token.type === "word" && token.value === "in"
1238
+ );
1239
+ if (inIndex > 0) {
1240
+ columnTokens = clauses.subject.slice(0, inIndex);
1241
+ tableTokens = clauses.subject.slice(inIndex + 1);
1242
+ } else if (clauses.subject.length >= 2) {
1243
+ columnTokens = clauses.subject.slice(0, -1);
1244
+ tableTokens = clauses.subject.slice(-1);
1245
+ }
1246
+ }
1247
+ const columnToken = columnTokens[0];
1248
+ if (!columnToken || columnToken.type !== "word") {
1249
+ throw new ParseError(`Expected "<${fn}> <column> of <table>"`, {
1250
+ position: tokens[0]?.position
1251
+ });
1252
+ }
1253
+ const { table } = resolveTableFromTokens(tableTokens, 0, schema);
1254
+ const resolved = resolveColumn(columnToken.value, table, schema);
1255
+ const foreignTables = /* @__PURE__ */ new Set();
1256
+ if (resolved.requiresJoin) {
1257
+ foreignTables.add(resolved.tableName);
1258
+ }
1259
+ const parsed = parseConditions(clauses.conditionTokens, table, schema);
1260
+ for (const foreign of parsed.foreignTables) {
1261
+ foreignTables.add(foreign);
1262
+ }
1263
+ return {
1264
+ kind: "select",
1265
+ table: table.name,
1266
+ columns: "*",
1267
+ aggregate: { fn, column: { table: resolved.tableName, column: resolved.column.name } },
1268
+ joins: resolveJoins(table.name, [...foreignTables], schema, maxJoinDepth),
1269
+ where: parsed.where,
1270
+ orderBy: [],
1271
+ limit: clauses.limit,
1272
+ offset: null
1273
+ };
1274
+ }
1275
+
1276
+ // src/safety/guard.ts
1277
+ function classifyAst(ast) {
1278
+ switch (ast.kind) {
1279
+ case "select":
1280
+ case "insert":
1281
+ case "createTable":
1282
+ return "safe";
1283
+ case "update":
1284
+ case "delete":
1285
+ return ast.where ? "destructive" : "catastrophic";
1286
+ case "dropTable":
1287
+ return "catastrophic";
1288
+ }
1289
+ }
1290
+ function astIsWrite(ast) {
1291
+ return ast.kind !== "select";
1292
+ }
1293
+ var readOnlyStarters = /* @__PURE__ */ new Set([
1294
+ "select",
1295
+ "show",
1296
+ "explain",
1297
+ "describe",
1298
+ "desc",
1299
+ "table",
1300
+ "values",
1301
+ "begin",
1302
+ "start",
1303
+ "commit",
1304
+ "rollback",
1305
+ "end",
1306
+ "set"
1307
+ ]);
1308
+ var writeKeywords = /\b(insert|update|delete|drop|truncate|alter|create|grant|revoke|vacuum)\b/i;
1309
+ function rawSqlIsWrite(sql) {
1310
+ const first = sql.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
1311
+ if (first === "with") {
1312
+ return writeKeywords.test(sql.replace(/'[^']*'/g, ""));
1313
+ }
1314
+ return !readOnlyStarters.has(first);
1315
+ }
1316
+ function classifyRawSql(sql) {
1317
+ const stripped = sql.replace(/'[^']*'/g, "").toLowerCase();
1318
+ const first = stripped.trim().split(/\s+/)[0] ?? "";
1319
+ if (first === "drop" || first === "truncate") {
1320
+ return "catastrophic";
1321
+ }
1322
+ if (first === "delete" || first === "update") {
1323
+ return /\bwhere\b/.test(stripped) ? "destructive" : "catastrophic";
1324
+ }
1325
+ return "safe";
1326
+ }
1327
+ function extractRawTargetTable(sql) {
1328
+ const stripped = sql.trim();
1329
+ const deleteMatch = /^delete\s+from\s+([\w".`]+)/i.exec(stripped);
1330
+ if (deleteMatch?.[1]) {
1331
+ return deleteMatch[1].replace(/["`]/g, "");
1332
+ }
1333
+ const updateMatch = /^update\s+([\w".`]+)/i.exec(stripped);
1334
+ if (updateMatch?.[1]) {
1335
+ return updateMatch[1].replace(/["`]/g, "");
1336
+ }
1337
+ const dropMatch = /^(?:drop|truncate)\s+table\s+(?:if\s+exists\s+)?([\w".`]+)/i.exec(stripped);
1338
+ if (dropMatch?.[1]) {
1339
+ return dropMatch[1].replace(/["`]/g, "");
1340
+ }
1341
+ const truncateMatch = /^truncate\s+([\w".`]+)/i.exec(stripped);
1342
+ if (truncateMatch?.[1]) {
1343
+ return truncateMatch[1].replace(/["`]/g, "");
1344
+ }
1345
+ return null;
1346
+ }
1347
+
1348
+ // src/repl/history.ts
1349
+ import { appendFileSync, existsSync, readFileSync } from "fs";
1350
+ var MAX_LOADED_ENTRIES = 1e3;
1351
+ function loadHistory() {
1352
+ const path = historyFilePath();
1353
+ if (!existsSync(path)) {
1354
+ return [];
1355
+ }
1356
+ const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
1357
+ const entries = [];
1358
+ for (const line of lines.slice(-MAX_LOADED_ENTRIES)) {
1359
+ try {
1360
+ entries.push(JSON.parse(line));
1361
+ } catch {
1362
+ }
1363
+ }
1364
+ return entries;
1365
+ }
1366
+ function appendHistory(entry) {
1367
+ try {
1368
+ appendFileSync(historyFilePath(), `${JSON.stringify(entry)}
1369
+ `, "utf8");
1370
+ } catch {
1371
+ }
1372
+ }
1373
+ function searchHistory(entries, term) {
1374
+ const needle = term.toLowerCase();
1375
+ return entries.filter((entry) => entry.input.toLowerCase().includes(needle));
1376
+ }
1377
+
1378
+ // src/repl/controller.ts
1379
+ function block(kind, text) {
1380
+ return { kind, text };
1381
+ }
1382
+ var sqlStarters = /* @__PURE__ */ new Set([
1383
+ "select",
1384
+ "insert",
1385
+ "update",
1386
+ "delete",
1387
+ "create",
1388
+ "alter",
1389
+ "drop",
1390
+ "truncate",
1391
+ "with",
1392
+ "begin",
1393
+ "start",
1394
+ "commit",
1395
+ "rollback",
1396
+ "end",
1397
+ "explain",
1398
+ "show",
1399
+ "set",
1400
+ "grant",
1401
+ "revoke",
1402
+ "vacuum",
1403
+ "analyze",
1404
+ "values",
1405
+ "table",
1406
+ "desc",
1407
+ "describe"
1408
+ ]);
1409
+ var sqlShapeMarkers = /(\bfrom\b|\binto\b|\bvalues\s*\(|\bjoin\b|\*|;|=|\breturning\b)/i;
1410
+ function startsWithSqlKeyword(input) {
1411
+ const first = input.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
1412
+ return sqlStarters.has(first);
1413
+ }
1414
+ function hasSqlShape(input) {
1415
+ return sqlShapeMarkers.test(input);
1416
+ }
1417
+ var ddlPattern = /^\s*(create|drop|alter|truncate)\b/i;
1418
+ var ReplController = class {
1419
+ manager = new ConnectionManager();
1420
+ config;
1421
+ safety;
1422
+ sqlLock = false;
1423
+ lastResult = null;
1424
+ lastSql = null;
1425
+ lastParams = [];
1426
+ inputHistory;
1427
+ pending = null;
1428
+ constructor() {
1429
+ this.config = loadConfig();
1430
+ this.safety = this.config.safety;
1431
+ this.inputHistory = loadHistory().map((entry) => entry.input);
1432
+ }
1433
+ promptInfo() {
1434
+ const connection = this.manager.active();
1435
+ if (!connection) {
1436
+ return null;
1437
+ }
1438
+ return {
1439
+ alias: connection.alias,
1440
+ database: connection.database,
1441
+ dialect: connection.dialect,
1442
+ env: connection.env,
1443
+ readonly: connection.readonly,
1444
+ inTransaction: connection.adapter.inTransaction(),
1445
+ sqlLock: this.sqlLock
1446
+ };
1447
+ }
1448
+ pendingConfirmation() {
1449
+ return this.pending;
1450
+ }
1451
+ setPending(confirmation) {
1452
+ this.pending = confirmation;
1453
+ }
1454
+ async handleInput(rawInput) {
1455
+ const input = rawInput.trim();
1456
+ if (!input) {
1457
+ return { blocks: [] };
1458
+ }
1459
+ if (this.pending) {
1460
+ return this.respondToPending(input);
1461
+ }
1462
+ try {
1463
+ if (input.startsWith("\\")) {
1464
+ const { dispatchMeta } = await import("./meta-NZ5Z6NYN.js");
1465
+ return await dispatchMeta(this, input);
1466
+ }
1467
+ return await this.handleQuery(input);
1468
+ } catch (error) {
1469
+ return { blocks: [this.errorBlock(error)] };
1470
+ }
1471
+ }
1472
+ errorBlock(error) {
1473
+ if (error instanceof ParseError) {
1474
+ const lines = [error.message];
1475
+ if (error.suggestions.length > 0) {
1476
+ lines.push(`Try: ${error.suggestions.join(" \xB7 ")}`);
1477
+ }
1478
+ return block("error", lines.join("\n"));
1479
+ }
1480
+ if (error instanceof AmbiguityError) {
1481
+ const lines = [
1482
+ error.message,
1483
+ ...error.candidates.map((candidate) => ` - ${candidate.label} (${candidate.detail})`)
1484
+ ];
1485
+ return block("warn", lines.join("\n"));
1486
+ }
1487
+ if (error instanceof MidqlError) {
1488
+ return block("error", error.message);
1489
+ }
1490
+ return block("error", error.message ?? String(error));
1491
+ }
1492
+ async respondToPending(input) {
1493
+ const pending = this.pending;
1494
+ this.pending = null;
1495
+ const normalized = input.trim();
1496
+ const confirmed = pending.expectedText ? normalized === pending.expectedText : /^y(es)?$/i.test(normalized);
1497
+ if (!confirmed) {
1498
+ return { blocks: [block("info", "Cancelled.")] };
1499
+ }
1500
+ try {
1501
+ return { blocks: await pending.execute() };
1502
+ } catch (error) {
1503
+ return { blocks: [this.errorBlock(error)] };
1504
+ }
1505
+ }
1506
+ safetyEnforced(connection) {
1507
+ return this.safety === "on" || connection.env === "prod";
1508
+ }
1509
+ async handleQuery(input) {
1510
+ const connection = this.manager.active();
1511
+ if (!connection) {
1512
+ return {
1513
+ blocks: [
1514
+ block("error", "Not connected. Use \\c <profile|url> to connect."),
1515
+ block("info", "Example: \\c postgres://user:pass@localhost:5432/mydb")
1516
+ ]
1517
+ };
1518
+ }
1519
+ if (this.sqlLock) {
1520
+ return this.runRawSql(connection, input);
1521
+ }
1522
+ if (startsWithSqlKeyword(input) && hasSqlShape(input)) {
1523
+ return this.runRawSql(connection, input);
1524
+ }
1525
+ try {
1526
+ const ast = translate(input, connection.schema, {
1527
+ maxJoinDepth: this.config.maxJoinDepth
1528
+ });
1529
+ return await this.runAst(connection, input, ast);
1530
+ } catch (error) {
1531
+ if (error instanceof ParseError && startsWithSqlKeyword(input)) {
1532
+ return this.runRawSql(connection, input);
1533
+ }
1534
+ throw error;
1535
+ }
1536
+ }
1537
+ async runAst(connection, input, ast) {
1538
+ const { executeAst } = await import("./execute-E45HSHNE.js");
1539
+ return executeAst(this, connection, input, ast);
1540
+ }
1541
+ async runRawSql(connection, sql) {
1542
+ if (connection.readonly && rawSqlIsWrite(sql)) {
1543
+ return {
1544
+ blocks: [
1545
+ block("error", "Connection is read-only; write statements are blocked."),
1546
+ block("info", "Use \\readonly off to allow writes on this connection.")
1547
+ ]
1548
+ };
1549
+ }
1550
+ const danger = classifyRawSql(sql);
1551
+ if (danger !== "safe" && this.safetyEnforced(connection)) {
1552
+ return this.confirmRawSql(connection, sql, danger);
1553
+ }
1554
+ return { blocks: await this.executeRaw(connection, sql) };
1555
+ }
1556
+ confirmRawSql(connection, sql, danger) {
1557
+ const table = extractRawTargetTable(sql);
1558
+ const verb = sql.trim().split(/\s+/)[0]?.toUpperCase() ?? "STATEMENT";
1559
+ const prodBanner = connection.env === "prod" ? " (PRODUCTION)" : "";
1560
+ const blocks = [];
1561
+ if (danger === "catastrophic") {
1562
+ const expected = table ?? "yes";
1563
+ blocks.push(
1564
+ block(
1565
+ "error",
1566
+ `\u26A0${prodBanner} ${verb} on ${table ?? "this database"}${/\bwhere\b/i.test(sql) ? "" : " affects every row"}.`
1567
+ )
1568
+ );
1569
+ blocks.push(block("warn", `Type '${expected}' to confirm:`));
1570
+ this.pending = {
1571
+ prompt: `confirm ${expected}`,
1572
+ expectedText: expected,
1573
+ execute: () => this.executeRaw(connection, sql)
1574
+ };
1575
+ } else {
1576
+ blocks.push(block("warn", `\u26A0${prodBanner} ${verb} will modify ${table ?? "data"}.`));
1577
+ blocks.push(block("warn", "Proceed? [y/N]"));
1578
+ this.pending = {
1579
+ prompt: "y/N",
1580
+ expectedText: null,
1581
+ execute: () => this.executeRaw(connection, sql)
1582
+ };
1583
+ }
1584
+ return { blocks };
1585
+ }
1586
+ async executeRaw(connection, sql) {
1587
+ let result;
1588
+ try {
1589
+ result = await connection.adapter.sessionQuery(sql);
1590
+ } catch (error) {
1591
+ this.record(connection.alias, sql, sql, null, false);
1592
+ throw error;
1593
+ }
1594
+ this.lastResult = result;
1595
+ this.lastSql = sql;
1596
+ this.lastParams = [];
1597
+ this.record(connection.alias, sql, sql, result.rowCount, true);
1598
+ if (ddlPattern.test(sql)) {
1599
+ await this.manager.refreshSchema(connection.alias).catch(() => connection);
1600
+ }
1601
+ return [block("table", formatResultSet(result))];
1602
+ }
1603
+ record(connectionAlias, input, sql, rowCount, ok) {
1604
+ this.inputHistory.push(input);
1605
+ appendHistory({
1606
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1607
+ connection: connectionAlias,
1608
+ input,
1609
+ sql,
1610
+ rowCount,
1611
+ ok
1612
+ });
1613
+ }
1614
+ async shutdown() {
1615
+ const openTx = this.manager.list().filter((connection) => connection.adapter.inTransaction()).map((connection) => connection.alias);
1616
+ await this.manager.closeAll();
1617
+ if (openTx.length > 0) {
1618
+ return [
1619
+ block("warn", `Open transactions on ${openTx.join(", ")} were rolled back on disconnect.`)
1620
+ ];
1621
+ }
1622
+ return [];
1623
+ }
1624
+ };
1625
+
1626
+ export {
1627
+ extractClauses,
1628
+ findTable,
1629
+ buildFkGraph,
1630
+ tokenize,
1631
+ dropFillers,
1632
+ translate,
1633
+ classifyAst,
1634
+ astIsWrite,
1635
+ rawSqlIsWrite,
1636
+ classifyRawSql,
1637
+ loadHistory,
1638
+ searchHistory,
1639
+ block,
1640
+ startsWithSqlKeyword,
1641
+ hasSqlShape,
1642
+ ReplController
1643
+ };