@dbsp/cli 1.0.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/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/chunk-AQC34IO5.js +107 -0
- package/dist/chunk-AQC34IO5.js.map +1 -0
- package/dist/chunk-U5DSGBS2.js +123 -0
- package/dist/chunk-U5DSGBS2.js.map +1 -0
- package/dist/chunk-UZEMCTNH.js +243 -0
- package/dist/chunk-UZEMCTNH.js.map +1 -0
- package/dist/chunk-ZSGVJFWG.js +2304 -0
- package/dist/chunk-ZSGVJFWG.js.map +1 -0
- package/dist/generators/schema-codegen.d.ts +39 -0
- package/dist/generators/schema-codegen.js +7 -0
- package/dist/generators/schema-codegen.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1138 -0
- package/dist/index.js.map +1 -0
- package/dist/repl/batch.d.ts +343 -0
- package/dist/repl/batch.js +407 -0
- package/dist/repl/batch.js.map +1 -0
- package/dist/repl-4OFERLKZ.js +1454 -0
- package/dist/repl-4OFERLKZ.js.map +1 -0
- package/dist/utils/schema-loader.d.ts +47 -0
- package/dist/utils/schema-loader.js +15 -0
- package/dist/utils/schema-loader.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,2304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TABLE_OPTIONS,
|
|
3
|
+
config,
|
|
4
|
+
isValidTableOption
|
|
5
|
+
} from "./chunk-U5DSGBS2.js";
|
|
6
|
+
|
|
7
|
+
// src/repl/db-connection.ts
|
|
8
|
+
import pg from "pg";
|
|
9
|
+
var { Pool } = pg;
|
|
10
|
+
var MAX_ROWS = 100;
|
|
11
|
+
async function createDbConnection(connectionString) {
|
|
12
|
+
if (!connectionString.startsWith("postgres://") && !connectionString.startsWith("postgresql://")) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Invalid connection URL: must start with postgres:// or postgresql://`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const pool = new Pool({
|
|
18
|
+
connectionString,
|
|
19
|
+
max: 1,
|
|
20
|
+
// Single connection for REPL
|
|
21
|
+
connectionTimeoutMillis: 1e4,
|
|
22
|
+
idleTimeoutMillis: 3e4
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
await pool.query("SELECT 1");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
await pool.end();
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
throw new Error(`Failed to connect to database: ${message}`);
|
|
30
|
+
}
|
|
31
|
+
let txClient = null;
|
|
32
|
+
async function runTransactionControl(sql) {
|
|
33
|
+
if (!txClient) {
|
|
34
|
+
throw new Error("No active transaction. Use .begin first.");
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await txClient.query(sql);
|
|
38
|
+
} finally {
|
|
39
|
+
txClient.release();
|
|
40
|
+
txClient = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function executeRaw(query, params = []) {
|
|
44
|
+
const startTime = performance.now();
|
|
45
|
+
const target = txClient ?? pool;
|
|
46
|
+
try {
|
|
47
|
+
const poolResult = await target.query(query, [...params]);
|
|
48
|
+
const endTime = performance.now();
|
|
49
|
+
const executionTimeMs = Math.round(endTime - startTime);
|
|
50
|
+
const rows = poolResult.rows ?? [];
|
|
51
|
+
const columns = poolResult.fields?.map((f) => f.name) ?? [];
|
|
52
|
+
const rowCount = poolResult.rowCount ?? rows?.length ?? 0;
|
|
53
|
+
const truncated = rows.length > MAX_ROWS;
|
|
54
|
+
const limitedRows = truncated ? rows.slice(0, MAX_ROWS) : rows;
|
|
55
|
+
return {
|
|
56
|
+
rows: limitedRows,
|
|
57
|
+
columns,
|
|
58
|
+
rowCount,
|
|
59
|
+
executionTimeMs,
|
|
60
|
+
truncated
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const endTime = performance.now();
|
|
64
|
+
const executionTimeMs = Math.round(endTime - startTime);
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return {
|
|
67
|
+
rows: [],
|
|
68
|
+
columns: [],
|
|
69
|
+
rowCount: 0,
|
|
70
|
+
executionTimeMs,
|
|
71
|
+
error: message
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
executeRaw,
|
|
77
|
+
async ping() {
|
|
78
|
+
try {
|
|
79
|
+
await pool.query("SELECT 1");
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async close() {
|
|
86
|
+
if (txClient) {
|
|
87
|
+
try {
|
|
88
|
+
await txClient.query("ROLLBACK");
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
txClient.release();
|
|
92
|
+
txClient = null;
|
|
93
|
+
}
|
|
94
|
+
await pool.end();
|
|
95
|
+
},
|
|
96
|
+
getPool() {
|
|
97
|
+
return pool;
|
|
98
|
+
},
|
|
99
|
+
async beginTransaction() {
|
|
100
|
+
if (txClient) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Transaction already active. Use .commit or .rollback first."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
txClient = await pool.connect();
|
|
106
|
+
try {
|
|
107
|
+
await txClient.query("BEGIN");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
txClient.release();
|
|
110
|
+
txClient = null;
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async commitTransaction() {
|
|
115
|
+
return runTransactionControl("COMMIT");
|
|
116
|
+
},
|
|
117
|
+
async rollbackTransaction() {
|
|
118
|
+
return runTransactionControl("ROLLBACK");
|
|
119
|
+
},
|
|
120
|
+
get inTransaction() {
|
|
121
|
+
return txClient !== null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getDatabaseName(connectionString) {
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(connectionString);
|
|
128
|
+
return url.pathname.slice(1) || url.hostname;
|
|
129
|
+
} catch {
|
|
130
|
+
return "database";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/repl/dot-commands.ts
|
|
135
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
136
|
+
|
|
137
|
+
// src/utils/identifier-validation.ts
|
|
138
|
+
var InvalidIdentifierError = class extends Error {
|
|
139
|
+
constructor(value, identifierType, reason) {
|
|
140
|
+
super(
|
|
141
|
+
`Invalid ${identifierType} identifier ${JSON.stringify(value)}: ${reason}`
|
|
142
|
+
);
|
|
143
|
+
this.value = value;
|
|
144
|
+
this.identifierType = identifierType;
|
|
145
|
+
this.name = "InvalidIdentifierError";
|
|
146
|
+
}
|
|
147
|
+
value;
|
|
148
|
+
identifierType;
|
|
149
|
+
};
|
|
150
|
+
var VALID_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_$]*$/;
|
|
151
|
+
function validateIdentifier(value, identifierType) {
|
|
152
|
+
if (!value || value.length === 0) {
|
|
153
|
+
throw new InvalidIdentifierError(value, identifierType, "cannot be empty");
|
|
154
|
+
}
|
|
155
|
+
if (/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
156
|
+
throw new InvalidIdentifierError(
|
|
157
|
+
value,
|
|
158
|
+
identifierType,
|
|
159
|
+
"contains control characters or NUL byte"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const byteLen = Buffer.byteLength(value, "utf8");
|
|
163
|
+
if (byteLen > 63) {
|
|
164
|
+
throw new InvalidIdentifierError(
|
|
165
|
+
value,
|
|
166
|
+
identifierType,
|
|
167
|
+
`exceeds maximum length of 63 bytes (got ${byteLen})`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (!VALID_IDENTIFIER_RE.test(value)) {
|
|
171
|
+
if (/^[0-9]/.test(value)) {
|
|
172
|
+
throw new InvalidIdentifierError(
|
|
173
|
+
value,
|
|
174
|
+
identifierType,
|
|
175
|
+
"cannot start with a digit"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
throw new InvalidIdentifierError(
|
|
179
|
+
value,
|
|
180
|
+
identifierType,
|
|
181
|
+
"contains invalid characters (only letters, digits, underscore, and $ allowed)"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/utils/path-containment.ts
|
|
187
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
188
|
+
var PathEscapeError = class extends Error {
|
|
189
|
+
constructor(originalArg, resolvedPath, baseDir) {
|
|
190
|
+
super(
|
|
191
|
+
`Path escapes working directory: "${originalArg}" resolves to "${resolvedPath}" which is outside "${baseDir}"`
|
|
192
|
+
);
|
|
193
|
+
this.originalArg = originalArg;
|
|
194
|
+
this.resolvedPath = resolvedPath;
|
|
195
|
+
this.baseDir = baseDir;
|
|
196
|
+
this.name = "PathEscapeError";
|
|
197
|
+
}
|
|
198
|
+
originalArg;
|
|
199
|
+
resolvedPath;
|
|
200
|
+
baseDir;
|
|
201
|
+
};
|
|
202
|
+
function validatePathInCwd(arg, cwd = process.cwd()) {
|
|
203
|
+
const sanitised = arg.replace(/\0/g, "");
|
|
204
|
+
const resolved = resolve(cwd, sanitised);
|
|
205
|
+
const base = resolve(cwd);
|
|
206
|
+
const isRelative = !isAbsolute(sanitised);
|
|
207
|
+
if (isRelative) {
|
|
208
|
+
const rel = relative(base, resolved);
|
|
209
|
+
if (rel === ".." || rel.startsWith("../") || rel.startsWith(`..${sep}`)) {
|
|
210
|
+
throw new PathEscapeError(arg, resolved, base);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return resolved;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/repl/csv.ts
|
|
217
|
+
import { readFile } from "fs/promises";
|
|
218
|
+
var CsvParseError = class _CsvParseError extends Error {
|
|
219
|
+
/** 1-based physical line number where the error was detected. */
|
|
220
|
+
line;
|
|
221
|
+
constructor(line, message) {
|
|
222
|
+
super(`CSV parse error at line ${line}: ${message}`);
|
|
223
|
+
this.name = "CsvParseError";
|
|
224
|
+
this.line = line;
|
|
225
|
+
Object.setPrototypeOf(this, _CsvParseError.prototype);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var CANDIDATE_SEPARATORS = [",", ";", " ", "|"];
|
|
229
|
+
var CANDIDATE_QUOTES = ['"', "'"];
|
|
230
|
+
function sniffCsvFormat(lines, schemaColumns) {
|
|
231
|
+
if (lines.length === 0) {
|
|
232
|
+
return { separator: ",", quote: '"', hasHeader: false, columns: [] };
|
|
233
|
+
}
|
|
234
|
+
const quote = detectQuoteChar(lines);
|
|
235
|
+
const separator = detectSeparator(lines, quote);
|
|
236
|
+
const firstRowFields = parseCsvLine(lines[0], separator, quote);
|
|
237
|
+
const hasHeader = detectHeader(firstRowFields, schemaColumns);
|
|
238
|
+
const columns = hasHeader ? firstRowFields.map((f) => f.trim()) : firstRowFields.map((_, i) => `col_${i}`);
|
|
239
|
+
return { separator, quote, hasHeader, columns };
|
|
240
|
+
}
|
|
241
|
+
function detectQuoteChar(lines) {
|
|
242
|
+
const joined = lines.join("");
|
|
243
|
+
let bestQuote = '"';
|
|
244
|
+
let bestCount = 0;
|
|
245
|
+
for (const q of CANDIDATE_QUOTES) {
|
|
246
|
+
const count = joined.split(q).length - 1;
|
|
247
|
+
if (count > bestCount) {
|
|
248
|
+
bestCount = count;
|
|
249
|
+
bestQuote = q;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return bestQuote;
|
|
253
|
+
}
|
|
254
|
+
function detectSeparator(lines, quote) {
|
|
255
|
+
let bestSep = ",";
|
|
256
|
+
let bestScore = -1;
|
|
257
|
+
for (const sep2 of CANDIDATE_SEPARATORS) {
|
|
258
|
+
const counts = lines.map((line) => parseCsvLine(line, sep2, quote).length);
|
|
259
|
+
const firstCount = counts[0];
|
|
260
|
+
if (firstCount <= 1) continue;
|
|
261
|
+
const consistent = counts.every((c) => c === firstCount);
|
|
262
|
+
const score = consistent ? firstCount * 10 : firstCount;
|
|
263
|
+
if (score > bestScore) {
|
|
264
|
+
bestScore = score;
|
|
265
|
+
bestSep = sep2;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return bestSep;
|
|
269
|
+
}
|
|
270
|
+
function detectHeader(firstRowFields, schemaColumns) {
|
|
271
|
+
if (firstRowFields.length === 0) return false;
|
|
272
|
+
if (schemaColumns && schemaColumns.length > 0) {
|
|
273
|
+
const schemaSet = new Set(schemaColumns.map((c) => c.toLowerCase().trim()));
|
|
274
|
+
const matchCount = firstRowFields.filter(
|
|
275
|
+
(f) => schemaSet.has(f.toLowerCase().trim())
|
|
276
|
+
).length;
|
|
277
|
+
if (matchCount > firstRowFields.length / 2) return true;
|
|
278
|
+
}
|
|
279
|
+
return firstRowFields.every((field) => {
|
|
280
|
+
const trimmed = field.trim();
|
|
281
|
+
if (trimmed === "") return false;
|
|
282
|
+
return /^[a-z][a-z0-9_]*$/.test(trimmed);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function parseCsvLine(line, separator, quote, physicalLine) {
|
|
286
|
+
const fields = [];
|
|
287
|
+
let current = "";
|
|
288
|
+
let inQuotes = false;
|
|
289
|
+
let i = 0;
|
|
290
|
+
while (i < line.length) {
|
|
291
|
+
const char = line[i];
|
|
292
|
+
if (inQuotes) {
|
|
293
|
+
if (char === quote) {
|
|
294
|
+
if (i + 1 < line.length && line[i + 1] === quote) {
|
|
295
|
+
current += quote;
|
|
296
|
+
i += 2;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (physicalLine !== void 0 && i + 1 < line.length && line[i + 1] !== separator) {
|
|
300
|
+
throw new CsvParseError(
|
|
301
|
+
physicalLine,
|
|
302
|
+
`unexpected character '${line[i + 1]}' after closing quote`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
inQuotes = false;
|
|
306
|
+
i++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
current += char;
|
|
310
|
+
i++;
|
|
311
|
+
} else {
|
|
312
|
+
if (char === quote && current === "") {
|
|
313
|
+
inQuotes = true;
|
|
314
|
+
i++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (char === separator) {
|
|
318
|
+
fields.push(current);
|
|
319
|
+
current = "";
|
|
320
|
+
i++;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
current += char;
|
|
324
|
+
i++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (inQuotes && physicalLine !== void 0) {
|
|
328
|
+
throw new CsvParseError(
|
|
329
|
+
physicalLine,
|
|
330
|
+
"unterminated quoted field (EOF in quote)"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
fields.push(current);
|
|
334
|
+
return fields;
|
|
335
|
+
}
|
|
336
|
+
function tokeniseCsvContent(content, quoteChar) {
|
|
337
|
+
const rows = [];
|
|
338
|
+
const q = quoteChar;
|
|
339
|
+
let inQuote = false;
|
|
340
|
+
let rowStart = 0;
|
|
341
|
+
let physicalLine = 1;
|
|
342
|
+
let rowStartPhysicalLine = 1;
|
|
343
|
+
let firstLineOfRowEnd = -1;
|
|
344
|
+
for (let i = 0; i < content.length; i++) {
|
|
345
|
+
const ch = content[i];
|
|
346
|
+
if (ch === q) {
|
|
347
|
+
if (!inQuote) {
|
|
348
|
+
inQuote = true;
|
|
349
|
+
} else {
|
|
350
|
+
if (i + 1 < content.length && content[i + 1] === q) {
|
|
351
|
+
i++;
|
|
352
|
+
} else {
|
|
353
|
+
inQuote = false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} else if (ch === "\n") {
|
|
357
|
+
if (firstLineOfRowEnd === -1) {
|
|
358
|
+
firstLineOfRowEnd = i > 0 && content[i - 1] === "\r" ? i - 1 : i;
|
|
359
|
+
}
|
|
360
|
+
physicalLine++;
|
|
361
|
+
if (!inQuote) {
|
|
362
|
+
const rawEnd = i > 0 && content[i - 1] === "\r" ? i - 1 : i;
|
|
363
|
+
const rawText = content.slice(rowStart, rawEnd);
|
|
364
|
+
const rawFirstLine = firstLineOfRowEnd === rawEnd ? rawText : content.slice(rowStart, firstLineOfRowEnd);
|
|
365
|
+
rows.push({
|
|
366
|
+
rawText,
|
|
367
|
+
rawFirstLine,
|
|
368
|
+
startPhysicalLine: rowStartPhysicalLine
|
|
369
|
+
});
|
|
370
|
+
rowStart = i + 1;
|
|
371
|
+
rowStartPhysicalLine = physicalLine;
|
|
372
|
+
firstLineOfRowEnd = -1;
|
|
373
|
+
}
|
|
374
|
+
} else if (ch === "\r") {
|
|
375
|
+
if (i + 1 >= content.length || content[i + 1] !== "\n") {
|
|
376
|
+
if (firstLineOfRowEnd === -1) {
|
|
377
|
+
firstLineOfRowEnd = i;
|
|
378
|
+
}
|
|
379
|
+
physicalLine++;
|
|
380
|
+
if (!inQuote) {
|
|
381
|
+
const rawText = content.slice(rowStart, i);
|
|
382
|
+
const rawFirstLine = firstLineOfRowEnd === i ? rawText : content.slice(rowStart, firstLineOfRowEnd);
|
|
383
|
+
rows.push({
|
|
384
|
+
rawText,
|
|
385
|
+
rawFirstLine,
|
|
386
|
+
startPhysicalLine: rowStartPhysicalLine
|
|
387
|
+
});
|
|
388
|
+
rowStart = i + 1;
|
|
389
|
+
rowStartPhysicalLine = physicalLine;
|
|
390
|
+
firstLineOfRowEnd = -1;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (rowStart < content.length) {
|
|
396
|
+
if (inQuote) {
|
|
397
|
+
throw new CsvParseError(
|
|
398
|
+
rowStartPhysicalLine,
|
|
399
|
+
"unterminated quoted field at end of file"
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const rawText = content.slice(rowStart).replace(/\r$/, "");
|
|
403
|
+
if (rawText.length > 0) {
|
|
404
|
+
const rawFirstLine = firstLineOfRowEnd === -1 ? rawText : content.slice(rowStart, firstLineOfRowEnd);
|
|
405
|
+
rows.push({
|
|
406
|
+
rawText,
|
|
407
|
+
rawFirstLine,
|
|
408
|
+
startPhysicalLine: rowStartPhysicalLine
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
} else if (inQuote) {
|
|
412
|
+
throw new CsvParseError(
|
|
413
|
+
rowStartPhysicalLine,
|
|
414
|
+
"unterminated quoted field at end of file"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return rows;
|
|
418
|
+
}
|
|
419
|
+
function parseLogicalRow(row, separator, quote) {
|
|
420
|
+
const text = row.rawText;
|
|
421
|
+
const fields = [];
|
|
422
|
+
let current = "";
|
|
423
|
+
let inQuotes = false;
|
|
424
|
+
let i = 0;
|
|
425
|
+
while (i < text.length) {
|
|
426
|
+
const ch = text[i];
|
|
427
|
+
if (inQuotes) {
|
|
428
|
+
if (ch === quote) {
|
|
429
|
+
if (i + 1 < text.length && text[i + 1] === quote) {
|
|
430
|
+
current += quote;
|
|
431
|
+
i += 2;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const next = text[i + 1];
|
|
435
|
+
if (next !== void 0 && next !== separator && next !== "\r" && next !== "\n") {
|
|
436
|
+
throw new CsvParseError(
|
|
437
|
+
row.startPhysicalLine,
|
|
438
|
+
`unexpected character '${next}' after closing quote`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
inQuotes = false;
|
|
442
|
+
i++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
current += ch;
|
|
446
|
+
i++;
|
|
447
|
+
} else {
|
|
448
|
+
if (ch === quote && current === "") {
|
|
449
|
+
inQuotes = true;
|
|
450
|
+
i++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (ch === separator) {
|
|
454
|
+
fields.push(current);
|
|
455
|
+
current = "";
|
|
456
|
+
i++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
current += ch;
|
|
460
|
+
i++;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
fields.push(current);
|
|
464
|
+
return fields;
|
|
465
|
+
}
|
|
466
|
+
async function parseCsvFile(filePath, schemaColumns) {
|
|
467
|
+
const raw = await readFile(filePath, "utf-8");
|
|
468
|
+
if (raw.length === 0) {
|
|
469
|
+
return {
|
|
470
|
+
format: { separator: ",", quote: '"', hasHeader: false, columns: [] },
|
|
471
|
+
rows: []
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const SNIFF_LINE_COUNT = 10;
|
|
475
|
+
const physicalLines = raw.split("\n");
|
|
476
|
+
const sampleLines = physicalLines.slice(0, SNIFF_LINE_COUNT * 3).map((l) => l.replace(/\r$/, "")).filter((l) => l.trim() !== "").slice(0, SNIFF_LINE_COUNT);
|
|
477
|
+
if (sampleLines.length === 0) {
|
|
478
|
+
return {
|
|
479
|
+
format: { separator: ",", quote: '"', hasHeader: false, columns: [] },
|
|
480
|
+
rows: []
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const format = sniffCsvFormat(sampleLines, schemaColumns);
|
|
484
|
+
const logicalRows = tokeniseCsvContent(raw, format.quote);
|
|
485
|
+
if (logicalRows.length === 0) {
|
|
486
|
+
return { format, rows: [] };
|
|
487
|
+
}
|
|
488
|
+
const rows = [];
|
|
489
|
+
let rowIndex = 0;
|
|
490
|
+
for (const logicalRow of logicalRows) {
|
|
491
|
+
if (logicalRow.rawText.trim() === "") {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (rowIndex === 0 && format.hasHeader) {
|
|
495
|
+
rowIndex++;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const fields = parseLogicalRow(logicalRow, format.separator, format.quote);
|
|
499
|
+
if (fields.length !== format.columns.length) {
|
|
500
|
+
throw new CsvParseError(
|
|
501
|
+
logicalRow.startPhysicalLine,
|
|
502
|
+
`expected ${format.columns.length} fields, got ${fields.length}`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const row = {};
|
|
506
|
+
for (let i = 0; i < format.columns.length; i++) {
|
|
507
|
+
row[format.columns[i]] = fields[i] ?? "";
|
|
508
|
+
}
|
|
509
|
+
rows.push(row);
|
|
510
|
+
rowIndex++;
|
|
511
|
+
}
|
|
512
|
+
return { format, rows };
|
|
513
|
+
}
|
|
514
|
+
function formatCsv(rows, columns) {
|
|
515
|
+
if (columns.length === 0) return "";
|
|
516
|
+
const header = columns.map((c) => escapeCsvField(c)).join(",");
|
|
517
|
+
const dataLines = rows.map(
|
|
518
|
+
(row) => columns.map((col) => escapeCsvField(formatFieldValue(row[col]))).join(",")
|
|
519
|
+
);
|
|
520
|
+
return [header, ...dataLines].join("\n");
|
|
521
|
+
}
|
|
522
|
+
function escapeCsvField(value) {
|
|
523
|
+
if (value.includes(",") || value.includes("\n") || value.includes("\r") || value.includes('"')) {
|
|
524
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
525
|
+
}
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
function formatFieldValue(value) {
|
|
529
|
+
if (value === null || value === void 0) return "";
|
|
530
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
531
|
+
return String(value);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/repl/dot-commands.ts
|
|
535
|
+
function formatTables(schema) {
|
|
536
|
+
const tables = schema.tableNames;
|
|
537
|
+
return `Tables (${tables.length}):
|
|
538
|
+
${tables.map((t) => ` - ${t}`).join("\n")}`;
|
|
539
|
+
}
|
|
540
|
+
function formatTableSchema(schema, tableName) {
|
|
541
|
+
const table = schema.model.tables.get(tableName);
|
|
542
|
+
if (!table) {
|
|
543
|
+
return `\u274C Table not found: ${tableName}`;
|
|
544
|
+
}
|
|
545
|
+
const lines = [`Table: ${tableName}`, "Columns:"];
|
|
546
|
+
for (const col of table.columns) {
|
|
547
|
+
const nullable = col.nullable ? "" : " (NOT NULL)";
|
|
548
|
+
lines.push(` - ${col.name}: ${col.type}${nullable}`);
|
|
549
|
+
}
|
|
550
|
+
return lines.join("\n");
|
|
551
|
+
}
|
|
552
|
+
function getRelationDescription(rel) {
|
|
553
|
+
return `${rel.type} \u2192 ${rel.target}`;
|
|
554
|
+
}
|
|
555
|
+
function formatRelations(schema, tableName) {
|
|
556
|
+
const relations = Array.from(schema.model.relations.entries());
|
|
557
|
+
if (tableName) {
|
|
558
|
+
const tableRelations = relations.filter(
|
|
559
|
+
([, rel]) => rel.target === tableName || rel.source === tableName
|
|
560
|
+
);
|
|
561
|
+
if (tableRelations.length === 0) {
|
|
562
|
+
return `No relations found for table: ${tableName}`;
|
|
563
|
+
}
|
|
564
|
+
const lines2 = [`Relations for ${tableName}:`];
|
|
565
|
+
for (const [name, rel] of tableRelations) {
|
|
566
|
+
lines2.push(` - ${name}: ${getRelationDescription(rel)}`);
|
|
567
|
+
}
|
|
568
|
+
return lines2.join("\n");
|
|
569
|
+
}
|
|
570
|
+
const lines = [`Relations (${relations.length}):`];
|
|
571
|
+
for (const [name, rel] of relations) {
|
|
572
|
+
lines.push(` - ${name}: ${getRelationDescription(rel)}`);
|
|
573
|
+
}
|
|
574
|
+
return lines.join("\n");
|
|
575
|
+
}
|
|
576
|
+
function handleBooleanToggle(arg, key, label, state) {
|
|
577
|
+
const requiresDb = key === "execEnabled";
|
|
578
|
+
if (arg === "on") {
|
|
579
|
+
if (requiresDb && !state.dbConnection) {
|
|
580
|
+
return { output: "\u274C No database connection. Use --db option." };
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
output: `\u2713 ${label}: ON`,
|
|
584
|
+
stateChange: { [key]: true }
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (arg === "off") {
|
|
588
|
+
return {
|
|
589
|
+
output: `\u2713 ${label}: OFF`,
|
|
590
|
+
stateChange: { [key]: false }
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (requiresDb && !state.dbConnection) {
|
|
594
|
+
return { output: "\u274C No database connection. Use --db option." };
|
|
595
|
+
}
|
|
596
|
+
const newValue = !state[key];
|
|
597
|
+
return {
|
|
598
|
+
output: `\u2713 ${label}: ${newValue ? "ON" : "OFF"}`,
|
|
599
|
+
stateChange: { [key]: newValue }
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async function runTransactionAction(state, action) {
|
|
603
|
+
if (!state.dbConnection) {
|
|
604
|
+
return { output: "\u274C No database connection. Use --db option." };
|
|
605
|
+
}
|
|
606
|
+
const methodMap = {
|
|
607
|
+
begin: {
|
|
608
|
+
method: "beginTransaction",
|
|
609
|
+
expectInTx: false,
|
|
610
|
+
errorMsg: "Transaction already active. Use .commit or .rollback first.",
|
|
611
|
+
okOutput: "\u2713 Transaction started (BEGIN)",
|
|
612
|
+
failPrefix: "Failed to begin transaction",
|
|
613
|
+
stateChange: { inTransaction: true }
|
|
614
|
+
},
|
|
615
|
+
commit: {
|
|
616
|
+
method: "commitTransaction",
|
|
617
|
+
expectInTx: true,
|
|
618
|
+
errorMsg: "No active transaction. Use .begin first.",
|
|
619
|
+
okOutput: "\u2713 Transaction committed (COMMIT)",
|
|
620
|
+
failPrefix: "Commit failed",
|
|
621
|
+
stateChange: { inTransaction: false }
|
|
622
|
+
},
|
|
623
|
+
rollback: {
|
|
624
|
+
method: "rollbackTransaction",
|
|
625
|
+
expectInTx: true,
|
|
626
|
+
errorMsg: "No active transaction. Use .begin first.",
|
|
627
|
+
okOutput: "\u2713 Transaction rolled back (ROLLBACK)",
|
|
628
|
+
failPrefix: "Rollback failed",
|
|
629
|
+
stateChange: { inTransaction: false }
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
const cfg = methodMap[action];
|
|
633
|
+
if (state.dbConnection.inTransaction !== cfg.expectInTx) {
|
|
634
|
+
return { output: `\u274C ${cfg.errorMsg}` };
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
await state.dbConnection[cfg.method]();
|
|
638
|
+
return { output: cfg.okOutput, stateChange: cfg.stateChange };
|
|
639
|
+
} catch (err) {
|
|
640
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
641
|
+
return { output: `\u274C ${cfg.failPrefix}: ${message}` };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function handleHelp() {
|
|
645
|
+
return {
|
|
646
|
+
output: `Available commands:
|
|
647
|
+
.tables - List all tables
|
|
648
|
+
.schema <table> - Show table schema
|
|
649
|
+
.relations [table]- Show relations (optionally for a specific table)
|
|
650
|
+
.use [schema] - Set/clear PostgreSQL schema for multi-tenant
|
|
651
|
+
.exec [on|off] - Toggle or set execution mode (requires --db)
|
|
652
|
+
.explain [on|off] - Toggle EXPLAIN output for queries
|
|
653
|
+
.parse [on|off] - Toggle parse tree (AST) output for queries
|
|
654
|
+
.natural - Switch to natural query mode
|
|
655
|
+
.sql - Switch to SQL mode
|
|
656
|
+
.output [mode] - Set output format (json|table|csv)
|
|
657
|
+
.import <file> - Execute SQL file (DDL, seed data)
|
|
658
|
+
.load <table> <f> - Import CSV file into table
|
|
659
|
+
.dump <table> <f> - Export table to CSV file
|
|
660
|
+
.begin - Start a transaction
|
|
661
|
+
.commit - Commit the active transaction
|
|
662
|
+
.rollback - Rollback the active transaction
|
|
663
|
+
.help - Show this help`
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function handleTables(_arg, schema) {
|
|
667
|
+
return { output: formatTables(schema) };
|
|
668
|
+
}
|
|
669
|
+
function handleSchema(arg, schema) {
|
|
670
|
+
if (!arg) {
|
|
671
|
+
const tableCount = schema.tableNames.length;
|
|
672
|
+
const relationCount = schema.model.relations.size;
|
|
673
|
+
return {
|
|
674
|
+
output: `Schema Summary:
|
|
675
|
+
- Tables: ${tableCount}
|
|
676
|
+
- Relations: ${relationCount}
|
|
677
|
+
Use .schema <table> for details`
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return { output: formatTableSchema(schema, arg) };
|
|
681
|
+
}
|
|
682
|
+
function handleRelations(arg, schema) {
|
|
683
|
+
return { output: formatRelations(schema, arg || void 0) };
|
|
684
|
+
}
|
|
685
|
+
function handleUse(arg) {
|
|
686
|
+
if (!arg) {
|
|
687
|
+
return {
|
|
688
|
+
output: "Cleared schema scope. Queries now use default schema.",
|
|
689
|
+
stateChange: { schemaName: void 0 }
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
validateIdentifier(arg, "schema");
|
|
694
|
+
} catch (err) {
|
|
695
|
+
const reason = err instanceof InvalidIdentifierError ? err.message : String(err);
|
|
696
|
+
return { output: `\u274C ${reason}` };
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
output: `Using schema: ${arg}`,
|
|
700
|
+
stateChange: { schemaName: arg }
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function handleExec(arg, _schema, state) {
|
|
704
|
+
return handleBooleanToggle(arg, "execEnabled", "Execution mode", state);
|
|
705
|
+
}
|
|
706
|
+
function handleNatural() {
|
|
707
|
+
return {
|
|
708
|
+
output: "Switched to natural query mode",
|
|
709
|
+
stateChange: { mode: "natural" }
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function handleSql() {
|
|
713
|
+
return {
|
|
714
|
+
output: "Switched to SQL mode",
|
|
715
|
+
stateChange: { mode: "sql" }
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function handleExplain(arg, _schema, state) {
|
|
719
|
+
return handleBooleanToggle(arg, "explainMode", "EXPLAIN mode", state);
|
|
720
|
+
}
|
|
721
|
+
function handleParse(arg, _schema, state) {
|
|
722
|
+
return handleBooleanToggle(arg, "parseMode", "Parse mode", state);
|
|
723
|
+
}
|
|
724
|
+
function handleOutput(arg, _schema, state) {
|
|
725
|
+
const validModes = ["json", "table", "csv"];
|
|
726
|
+
if (!arg) {
|
|
727
|
+
return {
|
|
728
|
+
output: `Current output mode: ${state.outputMode}`
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
const requestedMode = arg.toLowerCase();
|
|
732
|
+
if (!validModes.includes(requestedMode)) {
|
|
733
|
+
return {
|
|
734
|
+
output: `\u274C Invalid output mode: ${arg}. Use: json, table, csv`
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
output: `\u2713 Output mode: ${requestedMode}`,
|
|
739
|
+
stateChange: { outputMode: requestedMode }
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
var handleBegin = (_arg, _schema, state) => runTransactionAction(state, "begin");
|
|
743
|
+
var handleCommit = (_arg, _schema, state) => runTransactionAction(state, "commit");
|
|
744
|
+
var handleRollback = (_arg, _schema, state) => runTransactionAction(state, "rollback");
|
|
745
|
+
async function handleImport(arg, _schema, state) {
|
|
746
|
+
if (!arg) {
|
|
747
|
+
return { output: "\u274C Usage: .import <file.sql>" };
|
|
748
|
+
}
|
|
749
|
+
if (!state.dbConnection) {
|
|
750
|
+
return { output: "\u274C .import requires database connection (--db)" };
|
|
751
|
+
}
|
|
752
|
+
let resolvedPath;
|
|
753
|
+
try {
|
|
754
|
+
resolvedPath = validatePathInCwd(arg);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
const reason = err instanceof PathEscapeError ? err.message : String(err);
|
|
757
|
+
return { output: `\u274C ${reason}` };
|
|
758
|
+
}
|
|
759
|
+
if (!existsSync(resolvedPath)) {
|
|
760
|
+
return { output: `\u274C File not found: ${arg}` };
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
let sqlContent = readFileSync(resolvedPath, "utf-8");
|
|
764
|
+
if (state.schemaName) {
|
|
765
|
+
sqlContent = `SET search_path TO "${state.schemaName}", public;
|
|
766
|
+
${sqlContent}`;
|
|
767
|
+
}
|
|
768
|
+
const result = await state.dbConnection.executeRaw(sqlContent, []);
|
|
769
|
+
if (result.error) {
|
|
770
|
+
return {
|
|
771
|
+
output: `\u274C Import failed: ${result.error}`,
|
|
772
|
+
success: false,
|
|
773
|
+
error: result.error
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
const rowInfo = result.rowCount !== void 0 ? ` (${result.rowCount} rows affected)` : "";
|
|
777
|
+
const schemaInfo = state.schemaName ? ` (schema: ${state.schemaName})` : "";
|
|
778
|
+
return {
|
|
779
|
+
output: `\u2705 Imported: ${arg}${rowInfo}${schemaInfo}`,
|
|
780
|
+
success: true
|
|
781
|
+
};
|
|
782
|
+
} catch (err) {
|
|
783
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
784
|
+
return {
|
|
785
|
+
output: `\u274C Import failed: ${message}`,
|
|
786
|
+
success: false,
|
|
787
|
+
error: message
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async function handleLoad(arg, schema, state) {
|
|
792
|
+
const loadParts = arg.split(/\s+/);
|
|
793
|
+
const tableName = loadParts[0];
|
|
794
|
+
const filePath = loadParts.slice(1).join(" ");
|
|
795
|
+
if (!tableName || !filePath) {
|
|
796
|
+
return { output: "\u274C Usage: .load <table> <file.csv>" };
|
|
797
|
+
}
|
|
798
|
+
if (!state.dbConnection) {
|
|
799
|
+
return { output: "\u274C .load requires database connection (--db)" };
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
validateIdentifier(tableName, "table");
|
|
803
|
+
} catch (err) {
|
|
804
|
+
const reason = err instanceof InvalidIdentifierError ? err.message : String(err);
|
|
805
|
+
return { output: `\u274C ${reason}` };
|
|
806
|
+
}
|
|
807
|
+
const loadTable = schema.model.tables.get(tableName);
|
|
808
|
+
const schemaColumns = loadTable ? loadTable.columns.map((c) => c.name) : void 0;
|
|
809
|
+
let loadFilePath;
|
|
810
|
+
try {
|
|
811
|
+
loadFilePath = validatePathInCwd(filePath);
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const reason = err instanceof PathEscapeError ? err.message : String(err);
|
|
814
|
+
return { output: `\u274C ${reason}` };
|
|
815
|
+
}
|
|
816
|
+
if (!existsSync(loadFilePath)) {
|
|
817
|
+
return { output: `\u274C File not found: ${filePath}` };
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const csvData = await parseCsvFile(loadFilePath, schemaColumns);
|
|
821
|
+
if (csvData.rows.length === 0) {
|
|
822
|
+
return { output: "\u26A0\uFE0F CSV file is empty \u2014 no rows to import" };
|
|
823
|
+
}
|
|
824
|
+
const csvColumns = [...csvData.format.columns];
|
|
825
|
+
for (const col of csvColumns) {
|
|
826
|
+
try {
|
|
827
|
+
validateIdentifier(col, "column");
|
|
828
|
+
} catch (err) {
|
|
829
|
+
const reason = err instanceof InvalidIdentifierError ? err.message : String(err);
|
|
830
|
+
return {
|
|
831
|
+
output: `\u274C CSV header contains invalid column: ${reason}`
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const validColumns = schemaColumns ? csvColumns.filter((c) => schemaColumns.includes(c)) : csvColumns;
|
|
836
|
+
if (validColumns.length === 0) {
|
|
837
|
+
return {
|
|
838
|
+
output: `\u274C No matching columns found in CSV: ${csvColumns.join(", ")}`
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
const BATCH_SIZE = 100;
|
|
842
|
+
let totalInserted = 0;
|
|
843
|
+
for (let i = 0; i < csvData.rows.length; i += BATCH_SIZE) {
|
|
844
|
+
const batch = csvData.rows.slice(i, i + BATCH_SIZE);
|
|
845
|
+
const params = [];
|
|
846
|
+
const valueRows = [];
|
|
847
|
+
for (const row of batch) {
|
|
848
|
+
const placeholders = [];
|
|
849
|
+
for (const col of validColumns) {
|
|
850
|
+
params.push(row[col] ?? null);
|
|
851
|
+
placeholders.push(`$${params.length}`);
|
|
852
|
+
}
|
|
853
|
+
valueRows.push(`(${placeholders.join(", ")})`);
|
|
854
|
+
}
|
|
855
|
+
const quotedCols = validColumns.map((c) => `"${c}"`).join(", ");
|
|
856
|
+
const schemaPrefix = state.schemaName ? `"${state.schemaName}".` : "";
|
|
857
|
+
const sql = `INSERT INTO ${schemaPrefix}"${tableName}" (${quotedCols}) VALUES ${valueRows.join(", ")}`;
|
|
858
|
+
const result = await state.dbConnection.executeRaw(sql, params);
|
|
859
|
+
if (result.error) {
|
|
860
|
+
return {
|
|
861
|
+
output: `\u274C Insert failed at row ${totalInserted + 1}: ${result.error}`,
|
|
862
|
+
success: false,
|
|
863
|
+
error: result.error
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
totalInserted += batch.length;
|
|
867
|
+
}
|
|
868
|
+
const formatInfo = `separator: '${csvData.format.separator === " " ? "\\t" : csvData.format.separator}', header: ${csvData.format.hasHeader ? "yes" : "no"}`;
|
|
869
|
+
return {
|
|
870
|
+
output: `\u2705 Loaded ${totalInserted} rows into ${tableName} (${formatInfo})`,
|
|
871
|
+
success: true
|
|
872
|
+
};
|
|
873
|
+
} catch (err) {
|
|
874
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
875
|
+
return {
|
|
876
|
+
output: `\u274C Load failed: ${message}`,
|
|
877
|
+
success: false,
|
|
878
|
+
error: message
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function handleDump(arg, schema, state) {
|
|
883
|
+
const dumpParts = arg.split(/\s+/);
|
|
884
|
+
const dumpTableName = dumpParts[0];
|
|
885
|
+
const dumpFilePath = dumpParts.slice(1).join(" ");
|
|
886
|
+
if (!dumpTableName || !dumpFilePath) {
|
|
887
|
+
return { output: "\u274C Usage: .dump <table> <file.csv>" };
|
|
888
|
+
}
|
|
889
|
+
if (!state.dbConnection) {
|
|
890
|
+
return { output: "\u274C .dump requires database connection (--db)" };
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
validateIdentifier(dumpTableName, "table");
|
|
894
|
+
} catch (err) {
|
|
895
|
+
const reason = err instanceof InvalidIdentifierError ? err.message : String(err);
|
|
896
|
+
return { output: `\u274C ${reason}` };
|
|
897
|
+
}
|
|
898
|
+
const dumpTable = schema.model.tables.get(dumpTableName);
|
|
899
|
+
if (!dumpTable) {
|
|
900
|
+
return { output: `\u274C Table not found: ${dumpTableName}` };
|
|
901
|
+
}
|
|
902
|
+
let resolvedDumpPath;
|
|
903
|
+
try {
|
|
904
|
+
resolvedDumpPath = validatePathInCwd(dumpFilePath);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
const reason = err instanceof PathEscapeError ? err.message : String(err);
|
|
907
|
+
return { output: `\u274C ${reason}` };
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
const schemaPrefix = state.schemaName ? `"${state.schemaName}".` : "";
|
|
911
|
+
const result = await state.dbConnection.executeRaw(
|
|
912
|
+
`SELECT * FROM ${schemaPrefix}"${dumpTableName}"`
|
|
913
|
+
);
|
|
914
|
+
if (result.error) {
|
|
915
|
+
return {
|
|
916
|
+
output: `\u274C Query failed: ${result.error}`,
|
|
917
|
+
success: false,
|
|
918
|
+
error: result.error
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const columns = result.columns.length > 0 ? result.columns : dumpTable.columns.map((c) => c.name);
|
|
922
|
+
const csv = formatCsv(result.rows, columns);
|
|
923
|
+
writeFileSync(resolvedDumpPath, `${csv}
|
|
924
|
+
`, "utf-8");
|
|
925
|
+
return {
|
|
926
|
+
output: `\u2705 Dumped ${result.rows.length} rows from ${dumpTableName} to ${dumpFilePath}`,
|
|
927
|
+
success: true
|
|
928
|
+
};
|
|
929
|
+
} catch (err) {
|
|
930
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
931
|
+
return {
|
|
932
|
+
output: `\u274C Dump failed: ${message}`,
|
|
933
|
+
success: false,
|
|
934
|
+
error: message
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function handleExit() {
|
|
939
|
+
return { output: "Exiting..." };
|
|
940
|
+
}
|
|
941
|
+
var DOT_COMMAND_HANDLERS = /* @__PURE__ */ new Map([
|
|
942
|
+
[".help", handleHelp],
|
|
943
|
+
[".tables", handleTables],
|
|
944
|
+
[".schema", handleSchema],
|
|
945
|
+
[".relations", handleRelations],
|
|
946
|
+
[".use", handleUse],
|
|
947
|
+
[".exec", handleExec],
|
|
948
|
+
[".natural", handleNatural],
|
|
949
|
+
[".sql", handleSql],
|
|
950
|
+
[".explain", handleExplain],
|
|
951
|
+
[".parse", handleParse],
|
|
952
|
+
[".output", handleOutput],
|
|
953
|
+
[".begin", handleBegin],
|
|
954
|
+
[".commit", handleCommit],
|
|
955
|
+
[".rollback", handleRollback],
|
|
956
|
+
[".import", handleImport],
|
|
957
|
+
[".load", handleLoad],
|
|
958
|
+
[".dump", handleDump],
|
|
959
|
+
[".exit", handleExit],
|
|
960
|
+
[".quit", handleExit]
|
|
961
|
+
// alias — same handler instance
|
|
962
|
+
]);
|
|
963
|
+
async function processDotCommand(input, schema, state) {
|
|
964
|
+
const parts = input.split(/\s+/);
|
|
965
|
+
const command = (parts[0] ?? "").toLowerCase();
|
|
966
|
+
const arg = parts.slice(1).join(" ").trim().replace(/\0/g, "");
|
|
967
|
+
const handler = DOT_COMMAND_HANDLERS.get(command);
|
|
968
|
+
if (!handler) {
|
|
969
|
+
return { output: `\u274C Unknown command: ${command}` };
|
|
970
|
+
}
|
|
971
|
+
return handler(arg, schema, state);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/repl/completion.ts
|
|
975
|
+
function levenshtein(a, b) {
|
|
976
|
+
const aLower = a.toLowerCase();
|
|
977
|
+
const bLower = b.toLowerCase();
|
|
978
|
+
if (aLower === bLower) return 0;
|
|
979
|
+
if (aLower.length === 0) return bLower.length;
|
|
980
|
+
if (bLower.length === 0) return aLower.length;
|
|
981
|
+
const matrix = Array.from(
|
|
982
|
+
{ length: aLower.length + 1 },
|
|
983
|
+
(_, i) => Array.from(
|
|
984
|
+
{ length: bLower.length + 1 },
|
|
985
|
+
(_2, j) => i === 0 ? j : j === 0 ? i : 0
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
for (let i = 1; i <= aLower.length; i++) {
|
|
989
|
+
for (let j = 1; j <= bLower.length; j++) {
|
|
990
|
+
const cost = aLower[i - 1] === bLower[j - 1] ? 0 : 1;
|
|
991
|
+
const deletion = matrix[i - 1]?.[j] ?? 0;
|
|
992
|
+
const insertion = matrix[i]?.[j - 1] ?? 0;
|
|
993
|
+
const substitution = matrix[i - 1]?.[j - 1] ?? 0;
|
|
994
|
+
matrix[i][j] = Math.min(
|
|
995
|
+
deletion + 1,
|
|
996
|
+
insertion + 1,
|
|
997
|
+
substitution + cost
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return matrix[aLower.length]?.[bLower.length] ?? 0;
|
|
1002
|
+
}
|
|
1003
|
+
function suggestClosestMatch(input, candidates, maxDistance = 3) {
|
|
1004
|
+
if (!input || candidates.length === 0) return null;
|
|
1005
|
+
const matches = candidates.map((c) => ({ name: c, distance: levenshtein(input, c) })).filter((m) => m.distance <= maxDistance && m.distance > 0).sort((a, b) => a.distance - b.distance);
|
|
1006
|
+
return matches[0]?.name ?? null;
|
|
1007
|
+
}
|
|
1008
|
+
function enhanceErrorWithSuggestion(error, tableNames, columnNames = []) {
|
|
1009
|
+
const tableMatch = error.match(/Unknown table:\s*(\w+)/i);
|
|
1010
|
+
if (tableMatch?.[1]) {
|
|
1011
|
+
const unknownTable = tableMatch[1];
|
|
1012
|
+
const suggestion = suggestClosestMatch(unknownTable, tableNames);
|
|
1013
|
+
if (suggestion) {
|
|
1014
|
+
return `${error}
|
|
1015
|
+
|
|
1016
|
+
Did you mean '${suggestion}'?`;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (columnNames.length > 0) {
|
|
1020
|
+
const columnMatch = error.match(/Unknown column:\s*(\w+)/i);
|
|
1021
|
+
if (columnMatch?.[1]) {
|
|
1022
|
+
const unknownColumn = columnMatch[1];
|
|
1023
|
+
const suggestion = suggestClosestMatch(unknownColumn, columnNames);
|
|
1024
|
+
if (suggestion) {
|
|
1025
|
+
return `${error}
|
|
1026
|
+
|
|
1027
|
+
Did you mean '${suggestion}'?`;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return error;
|
|
1032
|
+
}
|
|
1033
|
+
var DOT_COMMANDS = [
|
|
1034
|
+
{ text: ".help", label: ".help", type: "command", description: "Show help" },
|
|
1035
|
+
{
|
|
1036
|
+
text: ".tables",
|
|
1037
|
+
label: ".tables",
|
|
1038
|
+
type: "command",
|
|
1039
|
+
description: "List tables"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
text: ".schema",
|
|
1043
|
+
label: ".schema",
|
|
1044
|
+
type: "command",
|
|
1045
|
+
description: "Show schema"
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
text: ".relations",
|
|
1049
|
+
label: ".relations",
|
|
1050
|
+
type: "command",
|
|
1051
|
+
description: "List relations"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
text: ".history",
|
|
1055
|
+
label: ".history",
|
|
1056
|
+
type: "command",
|
|
1057
|
+
description: "Show history"
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
text: ".sql",
|
|
1061
|
+
label: ".sql",
|
|
1062
|
+
type: "command",
|
|
1063
|
+
description: "Switch to SQL mode"
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
text: ".natural",
|
|
1067
|
+
label: ".natural",
|
|
1068
|
+
type: "command",
|
|
1069
|
+
description: "Switch to natural mode"
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
text: ".split",
|
|
1073
|
+
label: ".split",
|
|
1074
|
+
type: "command",
|
|
1075
|
+
description: "Toggle split view"
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
text: ".clear",
|
|
1079
|
+
label: ".clear",
|
|
1080
|
+
type: "command",
|
|
1081
|
+
description: "Clear screen"
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
text: ".exit",
|
|
1085
|
+
label: ".exit",
|
|
1086
|
+
type: "command",
|
|
1087
|
+
description: "Exit REPL"
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
text: ".quit",
|
|
1091
|
+
label: ".quit",
|
|
1092
|
+
type: "command",
|
|
1093
|
+
description: "Exit REPL (alias)"
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
text: ".aliasing",
|
|
1097
|
+
label: ".aliasing",
|
|
1098
|
+
type: "command",
|
|
1099
|
+
description: "Toggle column aliasing mode"
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
text: ".strategy",
|
|
1103
|
+
label: ".strategy",
|
|
1104
|
+
type: "command",
|
|
1105
|
+
description: "Show/set include strategy"
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
text: ".dialect",
|
|
1109
|
+
label: ".dialect",
|
|
1110
|
+
type: "command",
|
|
1111
|
+
description: "Show/set SQL dialect"
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
text: ".use",
|
|
1115
|
+
label: ".use",
|
|
1116
|
+
type: "command",
|
|
1117
|
+
description: "Set schema for queries"
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
text: ".exec",
|
|
1121
|
+
label: ".exec",
|
|
1122
|
+
type: "command",
|
|
1123
|
+
description: "Toggle execution mode"
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
text: ".import",
|
|
1127
|
+
label: ".import",
|
|
1128
|
+
type: "command",
|
|
1129
|
+
description: "Execute SQL file"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
text: ".parse",
|
|
1133
|
+
label: ".parse",
|
|
1134
|
+
type: "command",
|
|
1135
|
+
description: "Toggle parse tree (AST) output"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
text: ".explain",
|
|
1139
|
+
label: ".explain",
|
|
1140
|
+
type: "command",
|
|
1141
|
+
description: "Toggle EXPLAIN prefix for queries"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
text: ".table",
|
|
1145
|
+
label: ".table",
|
|
1146
|
+
type: "command",
|
|
1147
|
+
description: "Configure table display options"
|
|
1148
|
+
}
|
|
1149
|
+
];
|
|
1150
|
+
var KEYWORDS = [
|
|
1151
|
+
// Pipe operator (NQL syntax)
|
|
1152
|
+
{
|
|
1153
|
+
text: "|",
|
|
1154
|
+
label: "|",
|
|
1155
|
+
type: "keyword",
|
|
1156
|
+
description: "Pipe to next clause"
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
text: "where",
|
|
1160
|
+
label: "where",
|
|
1161
|
+
type: "keyword",
|
|
1162
|
+
description: "Filter results"
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
text: "with",
|
|
1166
|
+
label: "with",
|
|
1167
|
+
type: "keyword",
|
|
1168
|
+
description: "Include related data"
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
text: "select",
|
|
1172
|
+
label: "select",
|
|
1173
|
+
type: "keyword",
|
|
1174
|
+
description: "Select columns"
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
text: "group",
|
|
1178
|
+
label: "group",
|
|
1179
|
+
type: "keyword",
|
|
1180
|
+
description: "Group by columns"
|
|
1181
|
+
},
|
|
1182
|
+
{ text: "limit", label: "limit", type: "keyword", description: "Limit rows" },
|
|
1183
|
+
{
|
|
1184
|
+
text: "offset",
|
|
1185
|
+
label: "offset",
|
|
1186
|
+
type: "keyword",
|
|
1187
|
+
description: "Skip rows"
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
text: "order",
|
|
1191
|
+
label: "order",
|
|
1192
|
+
type: "keyword",
|
|
1193
|
+
description: "Sort results"
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
text: "asc",
|
|
1197
|
+
label: "asc",
|
|
1198
|
+
type: "keyword",
|
|
1199
|
+
description: "Ascending order"
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
text: "desc",
|
|
1203
|
+
label: "desc",
|
|
1204
|
+
type: "keyword",
|
|
1205
|
+
description: "Descending order"
|
|
1206
|
+
},
|
|
1207
|
+
{ text: "and", label: "and", type: "keyword", description: "Logical AND" },
|
|
1208
|
+
{ text: "or", label: "or", type: "keyword", description: "Logical OR" },
|
|
1209
|
+
{
|
|
1210
|
+
text: "true",
|
|
1211
|
+
label: "true",
|
|
1212
|
+
type: "keyword",
|
|
1213
|
+
description: "Boolean true"
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
text: "false",
|
|
1217
|
+
label: "false",
|
|
1218
|
+
type: "keyword",
|
|
1219
|
+
description: "Boolean false"
|
|
1220
|
+
},
|
|
1221
|
+
{ text: "null", label: "null", type: "keyword", description: "Null value" },
|
|
1222
|
+
// Range operators (PostgreSQL)
|
|
1223
|
+
{
|
|
1224
|
+
text: "overlaps",
|
|
1225
|
+
label: "overlaps",
|
|
1226
|
+
type: "keyword",
|
|
1227
|
+
description: "Range overlap operator (&&)"
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
text: "contains",
|
|
1231
|
+
label: "contains",
|
|
1232
|
+
type: "keyword",
|
|
1233
|
+
description: "Range contains element (@>)"
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
text: "containedBy",
|
|
1237
|
+
label: "containedBy",
|
|
1238
|
+
type: "keyword",
|
|
1239
|
+
description: "Range within another (<@)"
|
|
1240
|
+
},
|
|
1241
|
+
// String operators
|
|
1242
|
+
{
|
|
1243
|
+
text: "like",
|
|
1244
|
+
label: "like",
|
|
1245
|
+
type: "keyword",
|
|
1246
|
+
description: "Pattern matching (LIKE)"
|
|
1247
|
+
},
|
|
1248
|
+
// Set operators
|
|
1249
|
+
{
|
|
1250
|
+
text: "in",
|
|
1251
|
+
label: "in",
|
|
1252
|
+
type: "keyword",
|
|
1253
|
+
description: "Value in set (IN)"
|
|
1254
|
+
}
|
|
1255
|
+
];
|
|
1256
|
+
var CompletionProvider = class {
|
|
1257
|
+
tables = [];
|
|
1258
|
+
columns = /* @__PURE__ */ new Map();
|
|
1259
|
+
relations = [];
|
|
1260
|
+
// Relations per table - key is table name, value is relations for that table
|
|
1261
|
+
tableRelations = /* @__PURE__ */ new Map();
|
|
1262
|
+
constructor(schema) {
|
|
1263
|
+
this.initializeFromSchema(schema);
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Initialize completions from schema
|
|
1267
|
+
* ARCH-005: Uses schema.model (ModelIR) directly
|
|
1268
|
+
*/
|
|
1269
|
+
initializeFromSchema(schema) {
|
|
1270
|
+
for (const [tableName, table] of schema.model.tables) {
|
|
1271
|
+
this.tables.push({
|
|
1272
|
+
text: tableName,
|
|
1273
|
+
label: tableName,
|
|
1274
|
+
type: "table",
|
|
1275
|
+
description: "Table"
|
|
1276
|
+
});
|
|
1277
|
+
const tableColumns = [];
|
|
1278
|
+
for (const col of table.columns) {
|
|
1279
|
+
tableColumns.push({
|
|
1280
|
+
text: col.name,
|
|
1281
|
+
label: col.name,
|
|
1282
|
+
type: "column",
|
|
1283
|
+
description: col.type
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
this.columns.set(tableName, tableColumns);
|
|
1287
|
+
}
|
|
1288
|
+
for (const [relName] of schema.model.relations) {
|
|
1289
|
+
this.relations.push({
|
|
1290
|
+
text: relName,
|
|
1291
|
+
label: relName,
|
|
1292
|
+
type: "relation",
|
|
1293
|
+
description: "Relation"
|
|
1294
|
+
});
|
|
1295
|
+
if (relName.includes(".")) {
|
|
1296
|
+
const [tableName, ...relParts] = relName.split(".");
|
|
1297
|
+
if (!tableName) continue;
|
|
1298
|
+
const simpleRelName = relParts.join(".");
|
|
1299
|
+
if (!this.tableRelations.has(tableName)) {
|
|
1300
|
+
this.tableRelations.set(tableName, []);
|
|
1301
|
+
}
|
|
1302
|
+
this.tableRelations.get(tableName)?.push({
|
|
1303
|
+
text: simpleRelName,
|
|
1304
|
+
label: simpleRelName,
|
|
1305
|
+
type: "relation",
|
|
1306
|
+
description: `Relation from ${tableName}`
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Get completions for the given input
|
|
1313
|
+
*/
|
|
1314
|
+
complete(input) {
|
|
1315
|
+
const trimmed = input.trim();
|
|
1316
|
+
if (!trimmed) {
|
|
1317
|
+
return [...this.tables, ...DOT_COMMANDS.slice(0, 5)];
|
|
1318
|
+
}
|
|
1319
|
+
if (trimmed.startsWith(".")) {
|
|
1320
|
+
return this.filterSuggestions(DOT_COMMANDS, trimmed);
|
|
1321
|
+
}
|
|
1322
|
+
const context = this.parseContext(input);
|
|
1323
|
+
switch (context.expecting) {
|
|
1324
|
+
case "table":
|
|
1325
|
+
return this.filterSuggestions(this.tables, context.partial);
|
|
1326
|
+
case "keyword":
|
|
1327
|
+
return this.filterSuggestions(KEYWORDS, context.partial);
|
|
1328
|
+
case "column": {
|
|
1329
|
+
const tableCols = this.columns.get(context.table ?? "") ?? [];
|
|
1330
|
+
return this.filterSuggestions(
|
|
1331
|
+
[...tableCols, ...KEYWORDS],
|
|
1332
|
+
context.partial
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
case "relation": {
|
|
1336
|
+
const tableRels = context.table ? this.tableRelations.get(context.table) ?? [] : [];
|
|
1337
|
+
const relSuggestions = tableRels.length > 0 ? tableRels : this.relations;
|
|
1338
|
+
return this.filterSuggestions(relSuggestions, context.partial);
|
|
1339
|
+
}
|
|
1340
|
+
case "value":
|
|
1341
|
+
return this.filterSuggestions(
|
|
1342
|
+
KEYWORDS.filter((k) => ["true", "false", "null"].includes(k.text)),
|
|
1343
|
+
context.partial
|
|
1344
|
+
);
|
|
1345
|
+
default:
|
|
1346
|
+
return this.filterSuggestions(
|
|
1347
|
+
[...this.tables, ...KEYWORDS],
|
|
1348
|
+
context.partial
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Parse input to determine completion context
|
|
1354
|
+
*/
|
|
1355
|
+
parseContext(input) {
|
|
1356
|
+
const endsWithSpace = input.endsWith(" ");
|
|
1357
|
+
const words = input.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
|
|
1358
|
+
if (words.length === 0) {
|
|
1359
|
+
return { expecting: "table", partial: "" };
|
|
1360
|
+
}
|
|
1361
|
+
const partial = endsWithSpace ? "" : words[words.length - 1] ?? "";
|
|
1362
|
+
const contextWords = endsWithSpace ? words : words.slice(0, -1);
|
|
1363
|
+
const lastContextWord = contextWords[contextWords.length - 1];
|
|
1364
|
+
if (lastContextWord === "with") {
|
|
1365
|
+
const table = this.findTableInInput(words);
|
|
1366
|
+
return { expecting: "relation", partial, table };
|
|
1367
|
+
}
|
|
1368
|
+
if (lastContextWord === "where") {
|
|
1369
|
+
const table = this.findTableInInput(words);
|
|
1370
|
+
return { expecting: "column", partial, table };
|
|
1371
|
+
}
|
|
1372
|
+
if (lastContextWord === "and" || lastContextWord === "or") {
|
|
1373
|
+
const table = this.findTableInInput(words);
|
|
1374
|
+
return { expecting: "column", partial, table };
|
|
1375
|
+
}
|
|
1376
|
+
if (lastContextWord && /^[=!<>]+$/.test(lastContextWord)) {
|
|
1377
|
+
return { expecting: "value", partial };
|
|
1378
|
+
}
|
|
1379
|
+
if (contextWords.length === 0) {
|
|
1380
|
+
return { expecting: "table", partial };
|
|
1381
|
+
}
|
|
1382
|
+
const firstWordIsTable = this.tables.some(
|
|
1383
|
+
(t) => t.text.toLowerCase() === contextWords[0]
|
|
1384
|
+
);
|
|
1385
|
+
if (firstWordIsTable) {
|
|
1386
|
+
return {
|
|
1387
|
+
expecting: "keyword",
|
|
1388
|
+
partial,
|
|
1389
|
+
table: contextWords[0]
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
return { expecting: "any", partial };
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Find table name in the input words
|
|
1396
|
+
*/
|
|
1397
|
+
findTableInInput(words) {
|
|
1398
|
+
for (const word of words) {
|
|
1399
|
+
if (this.tables.some((t) => t.text.toLowerCase() === word)) {
|
|
1400
|
+
return word;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return void 0;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Filter suggestions by prefix
|
|
1407
|
+
*/
|
|
1408
|
+
filterSuggestions(suggestions, prefix) {
|
|
1409
|
+
if (!prefix) return suggestions;
|
|
1410
|
+
const lower = prefix.toLowerCase();
|
|
1411
|
+
return suggestions.filter(
|
|
1412
|
+
(s) => s.text.toLowerCase().startsWith(lower) || s.label.toLowerCase().includes(lower)
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Get table names for direct access
|
|
1417
|
+
*/
|
|
1418
|
+
getTableNames() {
|
|
1419
|
+
return this.tables.map((t) => t.text);
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Get column names for a table
|
|
1423
|
+
*/
|
|
1424
|
+
getColumnNames(tableName) {
|
|
1425
|
+
return (this.columns.get(tableName) ?? []).map((c) => c.text);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Get relation names
|
|
1429
|
+
*/
|
|
1430
|
+
getRelationNames() {
|
|
1431
|
+
return this.relations.map((r) => r.text);
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Apply a completion to the current input.
|
|
1435
|
+
* Returns the new input text with the partial word replaced by the completion.
|
|
1436
|
+
*
|
|
1437
|
+
* @param input - Current input text
|
|
1438
|
+
* @param completionText - The completion text to insert
|
|
1439
|
+
* @returns New input text with completion applied
|
|
1440
|
+
*/
|
|
1441
|
+
applyCompletion(input, completionText) {
|
|
1442
|
+
const endsWithSpace = input.endsWith(" ");
|
|
1443
|
+
if (endsWithSpace) {
|
|
1444
|
+
return input + completionText;
|
|
1445
|
+
}
|
|
1446
|
+
const words = input.split(/\s+/);
|
|
1447
|
+
if (words.length === 0) {
|
|
1448
|
+
return completionText;
|
|
1449
|
+
}
|
|
1450
|
+
words[words.length - 1] = completionText;
|
|
1451
|
+
return words.join(" ");
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
// src/repl/mode-escape.ts
|
|
1456
|
+
function parseInputMode(input, mode) {
|
|
1457
|
+
const trimmed = input.trim();
|
|
1458
|
+
const escaped = trimmed.startsWith("!");
|
|
1459
|
+
const content = escaped ? trimmed.slice(1).trim() : trimmed;
|
|
1460
|
+
const isRawSql = mode === "natural" && escaped || mode === "sql" && !escaped;
|
|
1461
|
+
return { content, isRawSql, escaped };
|
|
1462
|
+
}
|
|
1463
|
+
function getModeWarning(mode, escaped) {
|
|
1464
|
+
if (mode === "sql" && !escaped) {
|
|
1465
|
+
return "SQL mode: direct SQL";
|
|
1466
|
+
}
|
|
1467
|
+
if (mode === "natural" && escaped) {
|
|
1468
|
+
return "Escaped to raw SQL with !";
|
|
1469
|
+
}
|
|
1470
|
+
if (mode === "sql" && escaped) {
|
|
1471
|
+
return "Escaped to natural query with !";
|
|
1472
|
+
}
|
|
1473
|
+
return "Natural query mode";
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/repl/nql-executor.ts
|
|
1477
|
+
import {
|
|
1478
|
+
extractPseudoColumnKeywords,
|
|
1479
|
+
plan
|
|
1480
|
+
} from "@dbsp/core";
|
|
1481
|
+
import { compile as compileNql } from "@dbsp/nql";
|
|
1482
|
+
function extractIntentSummary(compiled, intentType) {
|
|
1483
|
+
if (compiled.query) {
|
|
1484
|
+
const q = compiled.query;
|
|
1485
|
+
return {
|
|
1486
|
+
type: "query",
|
|
1487
|
+
table: q.from,
|
|
1488
|
+
with: (q.include ?? []).map((i) => i.relation),
|
|
1489
|
+
hasWhere: !!q.where,
|
|
1490
|
+
hasGroupBy: !!(q.groupBy && q.groupBy.length > 0),
|
|
1491
|
+
hasOrderBy: !!(q.orderBy && q.orderBy.length > 0),
|
|
1492
|
+
ctes: []
|
|
1493
|
+
// CTEs are at program level, not in CompileResult
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
if (compiled.mutation) {
|
|
1497
|
+
const m = compiled.mutation;
|
|
1498
|
+
return {
|
|
1499
|
+
type: intentType,
|
|
1500
|
+
table: m.table,
|
|
1501
|
+
with: [],
|
|
1502
|
+
hasWhere: "where" in m && !!m.where,
|
|
1503
|
+
hasGroupBy: false,
|
|
1504
|
+
hasOrderBy: false,
|
|
1505
|
+
ctes: []
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
if (compiled.setOperation) {
|
|
1509
|
+
const setOp = compiled.setOperation;
|
|
1510
|
+
const leftLeaf = "from" in setOp.left ? setOp.left : null;
|
|
1511
|
+
return {
|
|
1512
|
+
type: "setOperation",
|
|
1513
|
+
table: leftLeaf?.from ?? "",
|
|
1514
|
+
with: [],
|
|
1515
|
+
hasWhere: leftLeaf != null ? !!leftLeaf.where : false,
|
|
1516
|
+
hasGroupBy: false,
|
|
1517
|
+
hasOrderBy: false,
|
|
1518
|
+
ctes: []
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
return {
|
|
1522
|
+
type: intentType,
|
|
1523
|
+
table: "",
|
|
1524
|
+
with: [],
|
|
1525
|
+
hasWhere: false,
|
|
1526
|
+
hasGroupBy: false,
|
|
1527
|
+
hasOrderBy: false,
|
|
1528
|
+
ctes: []
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
var NqlParseError = class extends Error {
|
|
1532
|
+
constructor(errors) {
|
|
1533
|
+
const messages = errors.map((e) => e.message).join("\n");
|
|
1534
|
+
super(`NQL parse error:
|
|
1535
|
+
${messages}`);
|
|
1536
|
+
this.errors = errors;
|
|
1537
|
+
this.name = "NqlParseError";
|
|
1538
|
+
}
|
|
1539
|
+
errors;
|
|
1540
|
+
};
|
|
1541
|
+
var NqlCompileError = class extends Error {
|
|
1542
|
+
constructor(message) {
|
|
1543
|
+
super(`NQL compile error: ${message}`);
|
|
1544
|
+
this.name = "NqlCompileError";
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
async function compileNqlToSql(nql, model, options) {
|
|
1548
|
+
const {
|
|
1549
|
+
createPgsqlCompileOnlyAdapter,
|
|
1550
|
+
compileSetOperation,
|
|
1551
|
+
createLeafCompileFn
|
|
1552
|
+
} = await import("@dbsp/adapter-pgsql");
|
|
1553
|
+
const adapter = createPgsqlCompileOnlyAdapter({
|
|
1554
|
+
...options?.schemaName !== void 0 && {
|
|
1555
|
+
schemaName: options.schemaName
|
|
1556
|
+
},
|
|
1557
|
+
...options?.dbCasing !== void 0 && {
|
|
1558
|
+
dbCasing: options.dbCasing
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
const compiled = compileNqlToIntent(nql, model);
|
|
1562
|
+
if (compiled.query) {
|
|
1563
|
+
const queryIntent = compiled.query;
|
|
1564
|
+
const planReport = plan(queryIntent, model, {
|
|
1565
|
+
dialectCapabilities: adapter.dialectCapabilities
|
|
1566
|
+
});
|
|
1567
|
+
const compiledQuery = adapter.compile(planReport, { model });
|
|
1568
|
+
return {
|
|
1569
|
+
sql: compiledQuery.sql,
|
|
1570
|
+
params: compiledQuery.parameters,
|
|
1571
|
+
intentType: "query",
|
|
1572
|
+
intent: extractIntentSummary(compiled, "query"),
|
|
1573
|
+
planReport
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
if (compiled.setOperation) {
|
|
1577
|
+
const compileFn = createLeafCompileFn(adapter, model, plan);
|
|
1578
|
+
const result = compileSetOperation(compiled.setOperation, compileFn);
|
|
1579
|
+
return {
|
|
1580
|
+
sql: result.sql,
|
|
1581
|
+
params: result.parameters,
|
|
1582
|
+
intentType: "setOperation",
|
|
1583
|
+
intent: extractIntentSummary(compiled, "setOperation")
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
if (compiled.mutation) {
|
|
1587
|
+
const mutation = compiled.mutation;
|
|
1588
|
+
switch (mutation.type) {
|
|
1589
|
+
case "insert": {
|
|
1590
|
+
const compiledQuery = adapter.compileInsert(mutation);
|
|
1591
|
+
return {
|
|
1592
|
+
sql: compiledQuery.sql,
|
|
1593
|
+
params: compiledQuery.parameters,
|
|
1594
|
+
intentType: "insert",
|
|
1595
|
+
intent: extractIntentSummary(compiled, "insert")
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
case "update": {
|
|
1599
|
+
const compiledQuery = adapter.compileUpdate(mutation);
|
|
1600
|
+
return {
|
|
1601
|
+
sql: compiledQuery.sql,
|
|
1602
|
+
params: compiledQuery.parameters,
|
|
1603
|
+
intentType: "update",
|
|
1604
|
+
intent: extractIntentSummary(compiled, "update")
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
case "delete": {
|
|
1608
|
+
const compiledQuery = adapter.compileDelete(mutation);
|
|
1609
|
+
return {
|
|
1610
|
+
sql: compiledQuery.sql,
|
|
1611
|
+
params: compiledQuery.parameters,
|
|
1612
|
+
intentType: "delete",
|
|
1613
|
+
intent: extractIntentSummary(compiled, "delete")
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
case "upsert": {
|
|
1617
|
+
const compiledQuery = adapter.compileUpsert(mutation);
|
|
1618
|
+
return {
|
|
1619
|
+
sql: compiledQuery.sql,
|
|
1620
|
+
params: compiledQuery.parameters,
|
|
1621
|
+
intentType: "upsert",
|
|
1622
|
+
intent: extractIntentSummary(compiled, "upsert")
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
default:
|
|
1626
|
+
throw new NqlCompileError(
|
|
1627
|
+
`Unknown mutation type: ${JSON.stringify(mutation)}`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
throw new NqlCompileError(
|
|
1632
|
+
"NQL compiled to neither query, mutation, nor set operation"
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
function compileNqlToIntent(nql, _model) {
|
|
1636
|
+
const compilerOptions = extractPseudoColumnKeywords(_model);
|
|
1637
|
+
const result = compileNql(nql, _model, void 0, compilerOptions);
|
|
1638
|
+
if (!result.success) {
|
|
1639
|
+
throw new NqlParseError(result.errors);
|
|
1640
|
+
}
|
|
1641
|
+
if (!result.ast) {
|
|
1642
|
+
throw new NqlCompileError("Compilation succeeded but no AST produced");
|
|
1643
|
+
}
|
|
1644
|
+
return result.ast;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/repl/engine/repl-engine.ts
|
|
1648
|
+
var DIALECT_STRATEGIES = {
|
|
1649
|
+
postgresql: ["auto", "join", "subquery", "cte", "lateral", "json_agg"],
|
|
1650
|
+
mysql: ["auto", "join", "subquery", "cte", "json_agg"],
|
|
1651
|
+
sqlite: ["auto", "join", "subquery", "cte"],
|
|
1652
|
+
mssql: ["auto", "join", "subquery", "cte"],
|
|
1653
|
+
duckdb: ["auto", "join", "subquery", "cte", "json_agg"]
|
|
1654
|
+
};
|
|
1655
|
+
function isInsideStringLiteral(input) {
|
|
1656
|
+
let inString = false;
|
|
1657
|
+
for (let i = 0; i < input.length - 1; i++) {
|
|
1658
|
+
if (input[i] === "'") {
|
|
1659
|
+
if (inString && i + 1 < input.length - 1 && input[i + 1] === "'") {
|
|
1660
|
+
i++;
|
|
1661
|
+
} else {
|
|
1662
|
+
inString = !inString;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return inString;
|
|
1667
|
+
}
|
|
1668
|
+
var borderHandler = {
|
|
1669
|
+
field: "borderStyle",
|
|
1670
|
+
label: "borders",
|
|
1671
|
+
// Border style values (none / outline / rounded / etc.) are all-lowercase —
|
|
1672
|
+
// normalize input so e.g. `.table borders NONE` matches.
|
|
1673
|
+
parse: (s) => s.toLowerCase()
|
|
1674
|
+
};
|
|
1675
|
+
var headerHandler = {
|
|
1676
|
+
field: "headerFormatter",
|
|
1677
|
+
label: "headers",
|
|
1678
|
+
// Header-formatter values include camelCase (capitalCase / snakeCase / camelCase) —
|
|
1679
|
+
// preserve original case so the user-typed value matches TABLE_OPTIONS exactly.
|
|
1680
|
+
parse: (s) => s
|
|
1681
|
+
};
|
|
1682
|
+
var TABLE_OPTION_HANDLERS = {
|
|
1683
|
+
borders: borderHandler,
|
|
1684
|
+
border: borderHandler,
|
|
1685
|
+
overflow: {
|
|
1686
|
+
field: "overflow",
|
|
1687
|
+
label: "overflow",
|
|
1688
|
+
// Overflow values (truncate / wrap) are all-lowercase — normalize input.
|
|
1689
|
+
parse: (s) => s.toLowerCase()
|
|
1690
|
+
},
|
|
1691
|
+
headers: headerHandler,
|
|
1692
|
+
header: headerHandler,
|
|
1693
|
+
padding: {
|
|
1694
|
+
field: "padding",
|
|
1695
|
+
label: "padding",
|
|
1696
|
+
parse: (s) => Number.parseInt(s, 10)
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
var ReplEngine = class {
|
|
1700
|
+
state;
|
|
1701
|
+
listeners = [];
|
|
1702
|
+
schema;
|
|
1703
|
+
schemaPath;
|
|
1704
|
+
model;
|
|
1705
|
+
dbConnection = null;
|
|
1706
|
+
completionProvider;
|
|
1707
|
+
databaseUrl;
|
|
1708
|
+
continuationBuffer = "";
|
|
1709
|
+
engineDotHandlers;
|
|
1710
|
+
constructor(config2) {
|
|
1711
|
+
this.schema = config2.schema;
|
|
1712
|
+
this.schemaPath = config2.schemaPath;
|
|
1713
|
+
this.model = config2.schema.model;
|
|
1714
|
+
this.databaseUrl = config2.databaseUrl;
|
|
1715
|
+
this.completionProvider = new CompletionProvider(config2.schema);
|
|
1716
|
+
this.state = {
|
|
1717
|
+
mode: "natural",
|
|
1718
|
+
execMode: config2.initialExecMode ?? false,
|
|
1719
|
+
connected: false,
|
|
1720
|
+
explainMode: false,
|
|
1721
|
+
parseMode: config2.initialParseMode ?? false,
|
|
1722
|
+
aliasingMode: "always",
|
|
1723
|
+
includeStrategy: "auto",
|
|
1724
|
+
dialect: "postgresql",
|
|
1725
|
+
...config2.initialSchemaName !== void 0 && {
|
|
1726
|
+
schemaName: config2.initialSchemaName
|
|
1727
|
+
},
|
|
1728
|
+
...config2.dbCasing !== void 0 && { dbCasing: config2.dbCasing },
|
|
1729
|
+
outputMode: "json",
|
|
1730
|
+
outputLayout: "full",
|
|
1731
|
+
planVerbosity: "normal",
|
|
1732
|
+
inTransaction: false
|
|
1733
|
+
};
|
|
1734
|
+
const exitHandler = this.handleExitCommand.bind(this);
|
|
1735
|
+
this.engineDotHandlers = /* @__PURE__ */ new Map([
|
|
1736
|
+
[".exit", exitHandler],
|
|
1737
|
+
[".quit", exitHandler],
|
|
1738
|
+
[".clear", this.handleClearCommand.bind(this)],
|
|
1739
|
+
[".help", this.handleHelpCommand.bind(this)],
|
|
1740
|
+
[".history", this.handleHistoryCommand.bind(this)],
|
|
1741
|
+
[".aliasing", this.handleAliasingCommand.bind(this)],
|
|
1742
|
+
[".strategy", this.handleStrategyCommand.bind(this)],
|
|
1743
|
+
[".dialect", this.handleDialectCommand.bind(this)],
|
|
1744
|
+
[".table", this.handleTableCommand.bind(this)],
|
|
1745
|
+
[".show", this.handleShowCommand.bind(this)],
|
|
1746
|
+
[".close", this.handleCloseCommand.bind(this)],
|
|
1747
|
+
[".layout", this.handleLayoutCommand.bind(this)],
|
|
1748
|
+
[".plan", this.handlePlanCommand.bind(this)]
|
|
1749
|
+
]);
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Initialize database connection if configured.
|
|
1753
|
+
* Must be called after construction for async init.
|
|
1754
|
+
*/
|
|
1755
|
+
async init() {
|
|
1756
|
+
if (!this.databaseUrl) return;
|
|
1757
|
+
try {
|
|
1758
|
+
this.dbConnection = await createDbConnection(this.databaseUrl);
|
|
1759
|
+
this.state.connected = true;
|
|
1760
|
+
const dbName = getDatabaseName(this.databaseUrl);
|
|
1761
|
+
this.emit({
|
|
1762
|
+
type: "info",
|
|
1763
|
+
message: `\u2713 Connected to database: ${dbName}`
|
|
1764
|
+
});
|
|
1765
|
+
this.emitStateChange();
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
this.state.connected = false;
|
|
1768
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1769
|
+
this.emit({
|
|
1770
|
+
type: "init-error",
|
|
1771
|
+
message: `Connection failed: ${message}`
|
|
1772
|
+
});
|
|
1773
|
+
this.emitStateChange();
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/** Subscribe to engine events. Returns unsubscribe function. */
|
|
1777
|
+
on(handler) {
|
|
1778
|
+
this.listeners.push(handler);
|
|
1779
|
+
return () => {
|
|
1780
|
+
const idx = this.listeners.indexOf(handler);
|
|
1781
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
/** Get current state (readonly snapshot). */
|
|
1785
|
+
getState() {
|
|
1786
|
+
return { ...this.state };
|
|
1787
|
+
}
|
|
1788
|
+
/** Get the completion provider for the UI. */
|
|
1789
|
+
getCompletionProvider() {
|
|
1790
|
+
return this.completionProvider;
|
|
1791
|
+
}
|
|
1792
|
+
/** Get schema for UI display. */
|
|
1793
|
+
getSchema() {
|
|
1794
|
+
return this.schema;
|
|
1795
|
+
}
|
|
1796
|
+
/** Get schema path for header display. */
|
|
1797
|
+
getSchemaPath() {
|
|
1798
|
+
return this.schemaPath;
|
|
1799
|
+
}
|
|
1800
|
+
/** Get database name for header display. */
|
|
1801
|
+
getDatabaseName() {
|
|
1802
|
+
return this.databaseUrl ? getDatabaseName(this.databaseUrl) : void 0;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Main entry point: process user input.
|
|
1806
|
+
* Handles dot commands, raw SQL, and NQL queries.
|
|
1807
|
+
*/
|
|
1808
|
+
async submit(input) {
|
|
1809
|
+
const trimmed = input.trim();
|
|
1810
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1811
|
+
this.continuationBuffer = "";
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (trimmed.endsWith("\\")) {
|
|
1815
|
+
this.continuationBuffer += (this.continuationBuffer ? "\n" : "") + trimmed.slice(0, -1);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const merged = this.continuationBuffer ? `${this.continuationBuffer}
|
|
1819
|
+
${trimmed}` : trimmed;
|
|
1820
|
+
this.continuationBuffer = "";
|
|
1821
|
+
if (merged.startsWith(".")) {
|
|
1822
|
+
await this.processDotCommand(merged);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const { content, isRawSql, escaped } = parseInputMode(
|
|
1826
|
+
merged,
|
|
1827
|
+
this.state.mode
|
|
1828
|
+
);
|
|
1829
|
+
if (!content) {
|
|
1830
|
+
this.emit({
|
|
1831
|
+
type: "error",
|
|
1832
|
+
message: this.state.mode === "sql" ? "Empty query. Enter SQL or use ! for natural query" : "Empty query. Enter query or use ! for raw SQL"
|
|
1833
|
+
});
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
if (isRawSql) {
|
|
1837
|
+
await this.handleRawSql(content, escaped);
|
|
1838
|
+
} else {
|
|
1839
|
+
await this.handleNql(content);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
/** Cleanup resources. */
|
|
1843
|
+
async destroy() {
|
|
1844
|
+
if (this.dbConnection) {
|
|
1845
|
+
await this.dbConnection.close();
|
|
1846
|
+
this.dbConnection = null;
|
|
1847
|
+
this.state.connected = false;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
// ========================================================================
|
|
1851
|
+
// Private: event emission
|
|
1852
|
+
// ========================================================================
|
|
1853
|
+
emit(event) {
|
|
1854
|
+
for (const listener of this.listeners) {
|
|
1855
|
+
listener(event);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
emitStateChange() {
|
|
1859
|
+
this.emit({ type: "state-change", state: { ...this.state } });
|
|
1860
|
+
}
|
|
1861
|
+
// ========================================================================
|
|
1862
|
+
// Private: dot command processing
|
|
1863
|
+
// ========================================================================
|
|
1864
|
+
async processDotCommand(input) {
|
|
1865
|
+
const [rawCmd = "", ...args] = input.trim().split(/\s+/);
|
|
1866
|
+
const cmd = rawCmd.toLowerCase();
|
|
1867
|
+
const arg = args.join(" ").trim();
|
|
1868
|
+
const engineHandler = this.engineDotHandlers.get(cmd);
|
|
1869
|
+
if (engineHandler) {
|
|
1870
|
+
engineHandler(arg);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const batchState = {
|
|
1874
|
+
mode: this.state.mode,
|
|
1875
|
+
execEnabled: this.state.execMode,
|
|
1876
|
+
schemaName: this.state.schemaName,
|
|
1877
|
+
dbConnection: this.dbConnection ?? void 0,
|
|
1878
|
+
explainMode: this.state.explainMode,
|
|
1879
|
+
parseMode: this.state.parseMode,
|
|
1880
|
+
model: this.model,
|
|
1881
|
+
outputMode: this.state.outputMode,
|
|
1882
|
+
inTransaction: this.state.inTransaction,
|
|
1883
|
+
...this.state.dbCasing !== void 0 && {
|
|
1884
|
+
dbCasing: this.state.dbCasing
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
const result = await processDotCommand(input, this.schema, batchState);
|
|
1888
|
+
this.applyDotCommandStateChange(result.stateChange);
|
|
1889
|
+
if (result.error) {
|
|
1890
|
+
this.emit({ type: "error", message: result.output });
|
|
1891
|
+
} else {
|
|
1892
|
+
this.emit({ type: "info", message: result.output });
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
// ========================================================================
|
|
1896
|
+
// Private: engine-level dot-command handlers
|
|
1897
|
+
// ========================================================================
|
|
1898
|
+
handleExitCommand(_arg) {
|
|
1899
|
+
this.emit({ type: "exit" });
|
|
1900
|
+
}
|
|
1901
|
+
handleClearCommand(_arg) {
|
|
1902
|
+
this.emit({ type: "clear" });
|
|
1903
|
+
}
|
|
1904
|
+
handleHelpCommand(_arg) {
|
|
1905
|
+
this.emit({ type: "info", message: "SHOW_HELP" });
|
|
1906
|
+
}
|
|
1907
|
+
handleHistoryCommand(_arg) {
|
|
1908
|
+
this.emit({ type: "show-history" });
|
|
1909
|
+
}
|
|
1910
|
+
handleAliasingCommand(_arg) {
|
|
1911
|
+
const newMode = this.state.aliasingMode === "always" ? "onCollision" : "always";
|
|
1912
|
+
this.state.aliasingMode = newMode;
|
|
1913
|
+
this.emitStateChange();
|
|
1914
|
+
this.emit({
|
|
1915
|
+
type: "info",
|
|
1916
|
+
message: `\u{1F3F7}\uFE0F Column aliasing mode: ${newMode}${newMode === "always" ? " (all included columns prefixed)" : " (only colliding columns prefixed)"}`
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
handleStrategyCommand(arg) {
|
|
1920
|
+
const strategyArg = arg?.toLowerCase();
|
|
1921
|
+
const validStrategies = DIALECT_STRATEGIES[this.state.dialect] ?? [];
|
|
1922
|
+
if (!strategyArg) {
|
|
1923
|
+
const lines = [
|
|
1924
|
+
`\u{1F517} Include Strategy: ${this.state.includeStrategy.toUpperCase()}`,
|
|
1925
|
+
`Dialect: ${this.state.dialect}`,
|
|
1926
|
+
`Available: ${validStrategies.join(", ")}`,
|
|
1927
|
+
`Usage: .strategy ${validStrategies.join(" | ")}`
|
|
1928
|
+
];
|
|
1929
|
+
this.emit({ type: "info", message: lines.join("\n") });
|
|
1930
|
+
} else if (validStrategies.includes(strategyArg)) {
|
|
1931
|
+
this.state.includeStrategy = strategyArg;
|
|
1932
|
+
this.emitStateChange();
|
|
1933
|
+
this.emit({
|
|
1934
|
+
type: "info",
|
|
1935
|
+
message: `\u2713 Include strategy: ${strategyArg.toUpperCase()}`
|
|
1936
|
+
});
|
|
1937
|
+
} else {
|
|
1938
|
+
this.emit({
|
|
1939
|
+
type: "error",
|
|
1940
|
+
message: `\u274C Unknown or unavailable strategy: ${strategyArg}. Available: ${validStrategies.join(", ")}`
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
handleDialectCommand(arg) {
|
|
1945
|
+
const dialectArg = arg?.toLowerCase();
|
|
1946
|
+
const validDialects = Object.keys(DIALECT_STRATEGIES);
|
|
1947
|
+
if (!dialectArg) {
|
|
1948
|
+
const lines = [
|
|
1949
|
+
`\u{1F5C4}\uFE0F SQL Dialect: ${this.state.dialect}`,
|
|
1950
|
+
`Available: ${validDialects.join(", ")}`,
|
|
1951
|
+
`Usage: .dialect ${validDialects.join(" | ")}`
|
|
1952
|
+
];
|
|
1953
|
+
this.emit({ type: "info", message: lines.join("\n") });
|
|
1954
|
+
} else if (validDialects.includes(dialectArg)) {
|
|
1955
|
+
const strategies = DIALECT_STRATEGIES[dialectArg];
|
|
1956
|
+
this.state.dialect = dialectArg;
|
|
1957
|
+
if (strategies && !strategies.includes(this.state.includeStrategy)) {
|
|
1958
|
+
this.state.includeStrategy = "join";
|
|
1959
|
+
this.emitStateChange();
|
|
1960
|
+
this.emit({
|
|
1961
|
+
type: "info",
|
|
1962
|
+
message: `\u2713 Dialect: ${dialectArg}
|
|
1963
|
+
\u26A0 Strategy reset to 'join' (previous not available for ${dialectArg})`
|
|
1964
|
+
});
|
|
1965
|
+
} else {
|
|
1966
|
+
this.emitStateChange();
|
|
1967
|
+
this.emit({ type: "info", message: `\u2713 Dialect: ${dialectArg}` });
|
|
1968
|
+
}
|
|
1969
|
+
} else {
|
|
1970
|
+
this.emit({
|
|
1971
|
+
type: "error",
|
|
1972
|
+
message: `\u274C Unknown dialect: ${dialectArg}. Available: ${validDialects.join(", ")}`
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
handleTableCommand(arg) {
|
|
1977
|
+
this.handleTableConfig(arg);
|
|
1978
|
+
}
|
|
1979
|
+
handleShowCommand(arg) {
|
|
1980
|
+
const validViews = [
|
|
1981
|
+
"sql",
|
|
1982
|
+
"plan",
|
|
1983
|
+
"results",
|
|
1984
|
+
"params",
|
|
1985
|
+
"dump"
|
|
1986
|
+
];
|
|
1987
|
+
const viewArg = arg?.toLowerCase();
|
|
1988
|
+
if (!viewArg) {
|
|
1989
|
+
this.emit({
|
|
1990
|
+
type: "info",
|
|
1991
|
+
message: `\u{1F4CB} Inspection panel views: ${validViews.join(", ")}
|
|
1992
|
+
Usage: .show ${validViews.join(" | ")}`
|
|
1993
|
+
});
|
|
1994
|
+
} else if (validViews.includes(viewArg)) {
|
|
1995
|
+
this.emit({ type: "show-panel", view: viewArg });
|
|
1996
|
+
} else {
|
|
1997
|
+
this.emit({
|
|
1998
|
+
type: "error",
|
|
1999
|
+
message: `\u274C Unknown panel view: ${viewArg}. Available: ${validViews.join(", ")}`
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
handleCloseCommand(_arg) {
|
|
2004
|
+
this.emit({ type: "close-panel" });
|
|
2005
|
+
}
|
|
2006
|
+
handleLayoutCommand(arg) {
|
|
2007
|
+
const validLayouts = ["compact", "results", "sql", "full"];
|
|
2008
|
+
const layoutArg = arg?.toLowerCase();
|
|
2009
|
+
if (!layoutArg) {
|
|
2010
|
+
this.emit({
|
|
2011
|
+
type: "info",
|
|
2012
|
+
message: `\u{1F4D0} Output layout: ${this.state.outputLayout}
|
|
2013
|
+
Available: ${validLayouts.join(", ")}
|
|
2014
|
+
Usage: .layout ${validLayouts.join(" | ")}`
|
|
2015
|
+
});
|
|
2016
|
+
} else if (validLayouts.includes(layoutArg)) {
|
|
2017
|
+
this.state.outputLayout = layoutArg;
|
|
2018
|
+
this.emitStateChange();
|
|
2019
|
+
this.emit({ type: "layout-change", layout: this.state.outputLayout });
|
|
2020
|
+
this.emit({ type: "info", message: `\u2713 Output layout: ${layoutArg}` });
|
|
2021
|
+
} else {
|
|
2022
|
+
this.emit({
|
|
2023
|
+
type: "error",
|
|
2024
|
+
message: `\u274C Unknown layout: ${layoutArg}. Available: ${validLayouts.join(", ")}`
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
handlePlanCommand(arg) {
|
|
2029
|
+
const validLevels = ["compact", "normal", "verbose"];
|
|
2030
|
+
const level = arg?.toLowerCase();
|
|
2031
|
+
if (!level) {
|
|
2032
|
+
this.emit({
|
|
2033
|
+
type: "info",
|
|
2034
|
+
message: `\u{1F4CB} Plan verbosity: ${this.state.planVerbosity}
|
|
2035
|
+
Available: ${validLevels.join(", ")}
|
|
2036
|
+
Usage: .plan ${validLevels.join(" | ")}`
|
|
2037
|
+
});
|
|
2038
|
+
} else if (validLevels.includes(level)) {
|
|
2039
|
+
this.state.planVerbosity = level;
|
|
2040
|
+
this.emitStateChange();
|
|
2041
|
+
this.emit({ type: "info", message: `\u2713 Plan verbosity: ${level}` });
|
|
2042
|
+
} else {
|
|
2043
|
+
this.emit({
|
|
2044
|
+
type: "error",
|
|
2045
|
+
message: `\u274C Invalid plan verbosity: ${level}. Use: ${validLevels.join(", ")}`
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
applyDotCommandStateChange(stateChange) {
|
|
2050
|
+
if (!stateChange) return;
|
|
2051
|
+
if (stateChange.mode !== void 0) this.state.mode = stateChange.mode;
|
|
2052
|
+
if (stateChange.execEnabled !== void 0) {
|
|
2053
|
+
this.state.execMode = stateChange.execEnabled;
|
|
2054
|
+
}
|
|
2055
|
+
if ("schemaName" in stateChange) {
|
|
2056
|
+
if (stateChange.schemaName !== void 0) {
|
|
2057
|
+
this.state.schemaName = stateChange.schemaName;
|
|
2058
|
+
} else {
|
|
2059
|
+
delete this.state.schemaName;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (stateChange.explainMode !== void 0) {
|
|
2063
|
+
this.state.explainMode = stateChange.explainMode;
|
|
2064
|
+
}
|
|
2065
|
+
if (stateChange.parseMode !== void 0) {
|
|
2066
|
+
this.state.parseMode = stateChange.parseMode;
|
|
2067
|
+
}
|
|
2068
|
+
if (stateChange.outputMode !== void 0) {
|
|
2069
|
+
this.state.outputMode = stateChange.outputMode;
|
|
2070
|
+
}
|
|
2071
|
+
if (stateChange.inTransaction !== void 0) {
|
|
2072
|
+
this.state.inTransaction = stateChange.inTransaction;
|
|
2073
|
+
}
|
|
2074
|
+
this.emitStateChange();
|
|
2075
|
+
}
|
|
2076
|
+
// ========================================================================
|
|
2077
|
+
// Private: .table config
|
|
2078
|
+
// ========================================================================
|
|
2079
|
+
handleTableConfig(arg) {
|
|
2080
|
+
const tableConfig = config.getTable();
|
|
2081
|
+
const parts = arg.split(/\s+/);
|
|
2082
|
+
const option = parts[0]?.toLowerCase() ?? "";
|
|
2083
|
+
const value = parts[1] ?? "";
|
|
2084
|
+
if (!option) {
|
|
2085
|
+
this.emit({
|
|
2086
|
+
type: "info",
|
|
2087
|
+
message: `Table Configuration:
|
|
2088
|
+
borders: ${tableConfig.borderStyle}
|
|
2089
|
+
overflow: ${tableConfig.overflow}
|
|
2090
|
+
headers: ${tableConfig.headerFormatter}
|
|
2091
|
+
padding: ${tableConfig.padding}`
|
|
2092
|
+
});
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
if (option === "reset") {
|
|
2096
|
+
config.resetTable();
|
|
2097
|
+
this.emit({
|
|
2098
|
+
type: "info",
|
|
2099
|
+
message: "\u2713 Table configuration reset to defaults"
|
|
2100
|
+
});
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const handler = TABLE_OPTION_HANDLERS[option];
|
|
2104
|
+
if (!handler) {
|
|
2105
|
+
this.emit({
|
|
2106
|
+
type: "error",
|
|
2107
|
+
message: `Unknown option: ${option}. Options: borders, overflow, headers, padding, reset`
|
|
2108
|
+
});
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
if (!value) {
|
|
2112
|
+
this.emit({
|
|
2113
|
+
type: "info",
|
|
2114
|
+
message: `Current: ${tableConfig[handler.field]}
|
|
2115
|
+
Options: ${TABLE_OPTIONS[handler.field].join(", ")}`
|
|
2116
|
+
});
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
const parsed = handler.parse(value);
|
|
2120
|
+
if (isValidTableOption(handler.field, parsed)) {
|
|
2121
|
+
config.updateTable({ [handler.field]: parsed });
|
|
2122
|
+
this.emit({ type: "info", message: `\u2713 ${handler.label} = ${parsed}` });
|
|
2123
|
+
} else {
|
|
2124
|
+
this.emit({
|
|
2125
|
+
type: "error",
|
|
2126
|
+
message: `Invalid value. Options: ${TABLE_OPTIONS[handler.field].join(", ")}`
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
// ========================================================================
|
|
2131
|
+
// Private: raw SQL handling
|
|
2132
|
+
// ========================================================================
|
|
2133
|
+
async handleRawSql(content, escaped) {
|
|
2134
|
+
const queryResult = {
|
|
2135
|
+
sql: content,
|
|
2136
|
+
params: [],
|
|
2137
|
+
plan: {
|
|
2138
|
+
strategy: "RAW_SQL",
|
|
2139
|
+
rootTable: "",
|
|
2140
|
+
tables: [],
|
|
2141
|
+
decisions: [],
|
|
2142
|
+
warnings: [
|
|
2143
|
+
{ message: getModeWarning(this.state.mode, escaped) },
|
|
2144
|
+
...!this.state.execMode || !this.state.connected ? [{ message: "(compile-only, use .exec on to execute)" }] : []
|
|
2145
|
+
].filter((w) => w.message),
|
|
2146
|
+
cteCount: 0,
|
|
2147
|
+
planningTimeMs: 0
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
this.emit({ type: "query-result", result: queryResult });
|
|
2151
|
+
if (this.state.execMode && this.state.connected && this.dbConnection) {
|
|
2152
|
+
try {
|
|
2153
|
+
const execResult = await this.dbConnection.executeRaw(content, []);
|
|
2154
|
+
this.emit({
|
|
2155
|
+
type: "execution-result",
|
|
2156
|
+
result: execResult,
|
|
2157
|
+
query: queryResult
|
|
2158
|
+
});
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2161
|
+
this.emit({
|
|
2162
|
+
type: "query-result",
|
|
2163
|
+
result: { sql: content, params: [], error: message }
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
// ========================================================================
|
|
2169
|
+
// Private: NQL handling
|
|
2170
|
+
// ========================================================================
|
|
2171
|
+
async handleNql(content) {
|
|
2172
|
+
if (!this.model) {
|
|
2173
|
+
this.emit({
|
|
2174
|
+
type: "error",
|
|
2175
|
+
message: "No schema model available for NQL compilation"
|
|
2176
|
+
});
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
const trimmed = content.trim();
|
|
2181
|
+
const hasBangSuffix = trimmed.endsWith("!") && !isInsideStringLiteral(trimmed);
|
|
2182
|
+
const nqlContent = hasBangSuffix ? trimmed.slice(0, -1).trim() : content;
|
|
2183
|
+
const result = await compileNqlToSql(nqlContent, this.model, {
|
|
2184
|
+
...this.state.schemaName ? { schemaName: this.state.schemaName } : {},
|
|
2185
|
+
...this.state.dbCasing ? { dbCasing: this.state.dbCasing } : {}
|
|
2186
|
+
});
|
|
2187
|
+
const isMutation = result.intentType !== "query" && result.intentType !== "setOperation";
|
|
2188
|
+
const isDryRun = isMutation && !hasBangSuffix;
|
|
2189
|
+
const finalSql = !isMutation && this.state.explainMode ? `EXPLAIN ${result.sql}` : result.sql;
|
|
2190
|
+
const planInfo = isMutation ? isDryRun ? "DRY-RUN (add ! to execute)" : "EXECUTED" : "";
|
|
2191
|
+
const queryResult = this.buildQueryResult(
|
|
2192
|
+
result,
|
|
2193
|
+
finalSql,
|
|
2194
|
+
isMutation,
|
|
2195
|
+
isDryRun,
|
|
2196
|
+
planInfo
|
|
2197
|
+
);
|
|
2198
|
+
this.emit({ type: "query-result", result: queryResult });
|
|
2199
|
+
if (this.shouldExecuteQuery(isMutation, isDryRun) && this.dbConnection) {
|
|
2200
|
+
const execResult = await this.dbConnection.executeRaw(
|
|
2201
|
+
finalSql,
|
|
2202
|
+
result.params
|
|
2203
|
+
);
|
|
2204
|
+
this.emit({
|
|
2205
|
+
type: "execution-result",
|
|
2206
|
+
result: execResult,
|
|
2207
|
+
query: queryResult
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
const tableNames = this.schema.tableNames;
|
|
2212
|
+
const rawError = err instanceof Error ? err.message : String(err);
|
|
2213
|
+
const enhancedError = enhanceErrorWithSuggestion(rawError, tableNames);
|
|
2214
|
+
this.emit({
|
|
2215
|
+
type: "query-result",
|
|
2216
|
+
result: { sql: "", params: [], error: enhancedError }
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
// ========================================================================
|
|
2221
|
+
// Private: NQL helpers
|
|
2222
|
+
// ========================================================================
|
|
2223
|
+
buildQueryResult(nqlResult, finalSql, isMutation, isDryRun, planInfo) {
|
|
2224
|
+
const pr = nqlResult.planReport;
|
|
2225
|
+
return {
|
|
2226
|
+
sql: finalSql,
|
|
2227
|
+
params: nqlResult.params,
|
|
2228
|
+
intent: nqlResult.intent,
|
|
2229
|
+
plan: {
|
|
2230
|
+
strategy: isMutation ? `${nqlResult.intentType.toUpperCase()} - ${planInfo}` : "NQL v2",
|
|
2231
|
+
rootTable: pr?.rootTable ?? "",
|
|
2232
|
+
tables: [
|
|
2233
|
+
...new Set(
|
|
2234
|
+
pr?.decisions.map((d) => d.context.sourceTable).filter(Boolean) ?? []
|
|
2235
|
+
)
|
|
2236
|
+
],
|
|
2237
|
+
decisions: pr?.decisions.map((d) => ({
|
|
2238
|
+
type: d.type,
|
|
2239
|
+
context: [d.context.sourceTable, d.context.target].filter(Boolean).join(" \u2192 "),
|
|
2240
|
+
choice: d.choice,
|
|
2241
|
+
reasoning: d.reasoning,
|
|
2242
|
+
...d.alternatives.length > 0 && {
|
|
2243
|
+
alternatives: [...d.alternatives]
|
|
2244
|
+
},
|
|
2245
|
+
...d.context.foreignKey !== void 0 && {
|
|
2246
|
+
foreignKey: typeof d.context.foreignKey === "string" ? d.context.foreignKey : [...d.context.foreignKey]
|
|
2247
|
+
},
|
|
2248
|
+
...d.context.relationType !== void 0 && {
|
|
2249
|
+
relationType: d.context.relationType
|
|
2250
|
+
},
|
|
2251
|
+
...d.context.intentPath !== void 0 && {
|
|
2252
|
+
intentPath: d.context.intentPath
|
|
2253
|
+
},
|
|
2254
|
+
...d.context.relationPath !== void 0 && {
|
|
2255
|
+
relationPath: d.context.relationPath
|
|
2256
|
+
},
|
|
2257
|
+
...d.id !== void 0 && { decisionId: d.id }
|
|
2258
|
+
})) ?? [],
|
|
2259
|
+
warnings: [
|
|
2260
|
+
...isDryRun ? [{ message: "This is a dry-run. Add ! suffix to execute." }] : [],
|
|
2261
|
+
...pr?.warnings.map((w) => ({
|
|
2262
|
+
message: w.message,
|
|
2263
|
+
...w.suggestion !== void 0 && { suggestion: w.suggestion },
|
|
2264
|
+
...w.code !== void 0 && { code: w.code },
|
|
2265
|
+
...w.relatedDecision !== void 0 && {
|
|
2266
|
+
relatedDecision: w.relatedDecision
|
|
2267
|
+
}
|
|
2268
|
+
})) ?? []
|
|
2269
|
+
],
|
|
2270
|
+
cteCount: pr?.ctes.length ?? 0,
|
|
2271
|
+
planningTimeMs: pr?.metadata.planningTimeMs ?? 0,
|
|
2272
|
+
...pr?.ctes && pr.ctes.length > 0 ? {
|
|
2273
|
+
ctes: pr.ctes.map((c) => ({
|
|
2274
|
+
name: c.name,
|
|
2275
|
+
purpose: c.purpose,
|
|
2276
|
+
...c.recursive && { recursive: c.recursive },
|
|
2277
|
+
...c.referencedBy.length > 0 && {
|
|
2278
|
+
referencedBy: [...c.referencedBy]
|
|
2279
|
+
}
|
|
2280
|
+
}))
|
|
2281
|
+
} : {},
|
|
2282
|
+
...pr?.metadata ? {
|
|
2283
|
+
metadata: {
|
|
2284
|
+
relationsAnalyzed: pr.metadata.relationsAnalyzed,
|
|
2285
|
+
isAmbiguous: pr.metadata.isAmbiguous,
|
|
2286
|
+
...pr.metadata.ambiguousOptions && pr.metadata.ambiguousOptions.length > 0 && {
|
|
2287
|
+
ambiguousOptions: [...pr.metadata.ambiguousOptions]
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
} : {}
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
shouldExecuteQuery(isMutation, isDryRun) {
|
|
2295
|
+
return isMutation ? !isDryRun && this.state.execMode && this.state.connected : this.state.execMode && this.state.connected;
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
|
|
2299
|
+
export {
|
|
2300
|
+
getDatabaseName,
|
|
2301
|
+
processDotCommand,
|
|
2302
|
+
ReplEngine
|
|
2303
|
+
};
|
|
2304
|
+
//# sourceMappingURL=chunk-ZSGVJFWG.js.map
|