jsql-neo 5.2.1 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7615 -210
- package/bin/jsql +13 -0
- package/index.js +16 -0
- package/lib/mongo_server.js +135 -10
- package/lib/redis_server.js +105 -0
- package/lib/tui.js +503 -0
- package/package.json +1 -1
package/lib/tui.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* jsql-neo TUI — zero-dependency interactive SQL terminal.
|
|
3
|
+
*
|
|
4
|
+
* Raw-mode keyboard handling, line editing with history & Tab completion,
|
|
5
|
+
* statement continuation (quote/paren balance + trailing ";"), table
|
|
6
|
+
* rendering with CJK-aware column widths, meta commands, and a status bar.
|
|
7
|
+
*
|
|
8
|
+
* When stdin is NOT a TTY it degrades to batch mode (execute each line).
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const Database = require('./database');
|
|
14
|
+
const { executeSQL } = require('./sql');
|
|
15
|
+
|
|
16
|
+
const HISTORY_FILE = path.join(os.homedir(), '.jsql-history');
|
|
17
|
+
const MAX_HISTORY = 500;
|
|
18
|
+
|
|
19
|
+
/* ---------- ANSI helpers ---------- */
|
|
20
|
+
|
|
21
|
+
const RESET = '\x1b[0m';
|
|
22
|
+
const BOLD = '\x1b[1m';
|
|
23
|
+
const DIM = '\x1b[2m';
|
|
24
|
+
const RED = '\x1b[31m';
|
|
25
|
+
const GREEN = '\x1b[32m';
|
|
26
|
+
const YELLOW = '\x1b[33m';
|
|
27
|
+
const CYAN = '\x1b[36m';
|
|
28
|
+
const MAGENTA = '\x1b[35m';
|
|
29
|
+
const GRAY = '\x1b[90m';
|
|
30
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
31
|
+
const CLEAR_SCREEN = '\x1b[2J';
|
|
32
|
+
const HOME = '\x1b[H';
|
|
33
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
34
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
35
|
+
|
|
36
|
+
/* ---------- display width (CJK-aware) ---------- */
|
|
37
|
+
|
|
38
|
+
function wswidth(str) {
|
|
39
|
+
let w = 0;
|
|
40
|
+
for (const ch of String(str)) {
|
|
41
|
+
const cp = ch.codePointAt(0);
|
|
42
|
+
if (cp >= 0x1100 && (cp <= 0x115f || cp === 0x2329 || cp === 0x232a ||
|
|
43
|
+
(cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
|
|
44
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || (cp >= 0xf900 && cp <= 0xfaff) ||
|
|
45
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || (cp >= 0xff00 && cp <= 0xff60) ||
|
|
46
|
+
(cp >= 0xffe0 && cp <= 0xffe6) || (cp >= 0x20000 && cp <= 0x2fffd))) {
|
|
47
|
+
w += 2;
|
|
48
|
+
} else w += 1;
|
|
49
|
+
}
|
|
50
|
+
return w;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pad(str, width) {
|
|
54
|
+
const s = String(str);
|
|
55
|
+
return s + ' '.repeat(Math.max(0, width - wswidth(s)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ---------- table rendering ---------- */
|
|
59
|
+
|
|
60
|
+
function renderTable(headers, rows, maxWidth) {
|
|
61
|
+
if (!headers || headers.length === 0) return '(no columns)';
|
|
62
|
+
const widths = headers.map((h) => wswidth(h));
|
|
63
|
+
const lines = [];
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
headers.forEach((_, i) => {
|
|
66
|
+
const v = row[i] == null ? 'NULL' : String(row[i]);
|
|
67
|
+
if (wswidth(v) > widths[i]) widths[i] = wswidth(v);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const maxCol = maxWidth || 200;
|
|
71
|
+
let total = 1;
|
|
72
|
+
for (const w of widths) total += w + 3;
|
|
73
|
+
if (total > maxCol && widths.length > 1) {
|
|
74
|
+
let overshoot = total - maxCol;
|
|
75
|
+
let i = widths.indexOf(Math.max(...widths));
|
|
76
|
+
while (overshoot > 0) {
|
|
77
|
+
if (i === -1 || widths[i] <= 3) break;
|
|
78
|
+
const cut = Math.min(widths[i] - 3, overshoot);
|
|
79
|
+
widths[i] -= cut;
|
|
80
|
+
overshoot -= cut;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const border = '+' + widths.map((w) => '-'.repeat(w + 2)).join('+') + '+';
|
|
84
|
+
lines.push(border);
|
|
85
|
+
lines.push('| ' + headers.map((h, i) => pad(h, widths[i])).join(' | ') + ' |');
|
|
86
|
+
lines.push(border);
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
const cells = headers.map((_, i) => {
|
|
89
|
+
const v = row[i] == null ? 'NULL' : String(row[i]);
|
|
90
|
+
return v.length > widths[i] + 0 && widths[i] >= 3 && wswidth(v) > widths[i]
|
|
91
|
+
? v.slice(0, Math.max(1, widths[i] - 1)) + '…'
|
|
92
|
+
: v;
|
|
93
|
+
});
|
|
94
|
+
lines.push('| ' + cells.map((c, i) => pad(c, widths[i])).join(' | ') + ' |');
|
|
95
|
+
}
|
|
96
|
+
lines.push(border);
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---------- meta commands ---------- */
|
|
101
|
+
|
|
102
|
+
const META_HELP = [
|
|
103
|
+
['\\q, \\quit, exit', '退出'],
|
|
104
|
+
['\\c', '清屏'],
|
|
105
|
+
['\\db', '显示当前数据库'],
|
|
106
|
+
['\\use <name>', '切换数据库'],
|
|
107
|
+
['\\tables', '列出所有表'],
|
|
108
|
+
['\\desc <table>', '查看表结构'],
|
|
109
|
+
['\\help', '显示帮助'],
|
|
110
|
+
['Ctrl+L 清屏 · Ctrl+C 取消当前行(再按退出) · Ctrl+D 退出', '快捷键'],
|
|
111
|
+
['Tab', '自动补全关键字'],
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/* ---------- TUI shell ---------- */
|
|
115
|
+
|
|
116
|
+
class TUIShell {
|
|
117
|
+
constructor(opts = {}) {
|
|
118
|
+
this.dataDir = opts.dataDir || null;
|
|
119
|
+
this.dbName = opts.db || 'default';
|
|
120
|
+
this.engine = this._openEngine(this.dbName);
|
|
121
|
+
this._engines = new Map();
|
|
122
|
+
this._engines.set(this.dbName, this.engine);
|
|
123
|
+
this.history = [];
|
|
124
|
+
this._loadHistory();
|
|
125
|
+
this.line = '';
|
|
126
|
+
this.cursor = 0;
|
|
127
|
+
this.histIdx = -1;
|
|
128
|
+
this.pending = ''; // 续行 buffer
|
|
129
|
+
this.dialect = opts.dialect || 'mysql';
|
|
130
|
+
this.batch = !process.stdin.isTTY;
|
|
131
|
+
this._exiting = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_openEngine(name) {
|
|
135
|
+
if (this.dataDir && this.dataDir !== ':memory:') {
|
|
136
|
+
return new Database(path.join(this.dataDir, name), { autoSave: true });
|
|
137
|
+
}
|
|
138
|
+
return new Database(':memory:', { autoSave: false });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_loadHistory() {
|
|
142
|
+
try {
|
|
143
|
+
const raw = fs.readFileSync(HISTORY_FILE, 'utf8').split('\n').filter(Boolean);
|
|
144
|
+
this.history = raw.slice(-MAX_HISTORY);
|
|
145
|
+
} catch (e) { this.history = []; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_saveHistory() {
|
|
149
|
+
try {
|
|
150
|
+
const head = this.history.slice(-MAX_HISTORY);
|
|
151
|
+
fs.writeFileSync(HISTORY_FILE, head.join('\n') + '\n');
|
|
152
|
+
} catch (e) { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
listen() { /* compat no-op (TUI owns the process) */ }
|
|
156
|
+
|
|
157
|
+
async run() {
|
|
158
|
+
if (this.batch) {
|
|
159
|
+
return this._runBatch();
|
|
160
|
+
}
|
|
161
|
+
this._statusBar();
|
|
162
|
+
console.log(`${CYAN}Welcome to jsql-neo ${BOLD}v${require('../package.json').version}${RESET} ${GRAY}(type \\help for commands)${RESET}\n`);
|
|
163
|
+
process.stdin.setRawMode(true);
|
|
164
|
+
process.stdin.resume();
|
|
165
|
+
process.stdin.setEncoding('utf8');
|
|
166
|
+
process.stdin.on('data', (chunk) => this._onKeys(chunk));
|
|
167
|
+
process.stdout.write(HIDE_CURSOR);
|
|
168
|
+
this._redrawPrompt();
|
|
169
|
+
this._term = process.stdin;
|
|
170
|
+
this._term.on('end', () => { this._saveHistory(); });
|
|
171
|
+
return new Promise(() => {});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ---------- batch mode ---------- */
|
|
175
|
+
|
|
176
|
+
async _runBatch() {
|
|
177
|
+
let data = '';
|
|
178
|
+
try { data = fs.readFileSync(0, 'utf8'); } catch (e) {}
|
|
179
|
+
const lines = data.split('\n');
|
|
180
|
+
let combined = '';
|
|
181
|
+
let results = 0;
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const trimmed = line.trim();
|
|
184
|
+
if (combined === '' && (trimmed.startsWith('\\q') || trimmed.startsWith('exit') || trimmed.startsWith('quit'))) break;
|
|
185
|
+
if (combined === '' && trimmed.startsWith('\\')) {
|
|
186
|
+
const r = this._meta(trimmed, true);
|
|
187
|
+
if (r === 'quit') break;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (trimmed === '') {
|
|
191
|
+
if (combined !== '') { await this._exec(combined); results++; combined = ''; }
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
combined += (combined ? '\n' : '') + line;
|
|
195
|
+
if (this._statementComplete(combined)) { await this._exec(combined); results++; combined = ''; }
|
|
196
|
+
}
|
|
197
|
+
if (combined) { await this._exec(combined); results++; }
|
|
198
|
+
if (results === 0) {
|
|
199
|
+
this._err('(no SQL statements — use interactive mode for the TUI, e.g. `jsql tui`)');
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ---------- keyboard ---------- */
|
|
205
|
+
|
|
206
|
+
_onKeys(chunk) {
|
|
207
|
+
const seq = Buffer.from(chunk, 'utf8');
|
|
208
|
+
let i = 0;
|
|
209
|
+
while (i < seq.length) {
|
|
210
|
+
const b = seq[i];
|
|
211
|
+
if (b === 0x1b) {
|
|
212
|
+
if (seq[i + 1] === 0x5b) {
|
|
213
|
+
const c = seq[i + 2];
|
|
214
|
+
if (c === 0x41) this._histPrev();
|
|
215
|
+
else if (c === 0x42) this._histNext();
|
|
216
|
+
else if (c === 0x43) this._moveRight();
|
|
217
|
+
else if (c === 0x44) this._moveLeft();
|
|
218
|
+
else if (c === 0x48 || c === 0x31) this.cursor = 0, this._redrawPrompt();
|
|
219
|
+
else if (c === 0x46 || c === 0x34) this.cursor = this.line.length, this._redrawPrompt();
|
|
220
|
+
else if (c === 0x33 && seq[i + 3] === 0x7e) { this._deleteAt(); i += 4; continue; }
|
|
221
|
+
i += 3;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (seq[i + 1] === 0x4f) { // ESC O x
|
|
225
|
+
const c = seq[i + 2];
|
|
226
|
+
if (c === 0x48) this.cursor = 0;
|
|
227
|
+
else if (c === 0x46) this.cursor = this.line.length;
|
|
228
|
+
this._redrawPrompt();
|
|
229
|
+
i += 3;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
i += 1; // lone ESC: ignore
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (b === 0x0d || b === 0x0a) { this._enter(); i++; continue; }
|
|
236
|
+
if (b === 0x7f || b === 0x08) { this._backspace(); i++; continue; }
|
|
237
|
+
if (b === 0x01) { this.cursor = 0; this._redrawPrompt(); i++; continue; } // Ctrl+A
|
|
238
|
+
if (b === 0x05) { this.cursor = this.line.length; this._redrawPrompt(); i++; continue; } // Ctrl+E
|
|
239
|
+
if (b === 0x0c) { this._clearScreen(); i++; continue; } // Ctrl+L
|
|
240
|
+
if (b === 0x03) { if (this.line || this.pending) this._cancelLine(); else this._quit(); i++; continue; } // Ctrl+C
|
|
241
|
+
if (b === 0x04) { if (!this.line && !this.pending) this._quit(); i++; continue; } // Ctrl+D
|
|
242
|
+
if (b === 0x09) { this._complete(); i++; continue; } // Tab
|
|
243
|
+
if (b === 0x15) { this.line = ''; this.cursor = 0; this._redrawPrompt(); i++; continue; } // Ctrl+U
|
|
244
|
+
if (b >= 0x20) {
|
|
245
|
+
let ch, step;
|
|
246
|
+
if (b < 0x80) { ch = String.fromCharCode(b); step = 1; }
|
|
247
|
+
else {
|
|
248
|
+
let n = 1;
|
|
249
|
+
if (b >= 0xf0) n = 4;
|
|
250
|
+
else if (b >= 0xe0) n = 3;
|
|
251
|
+
else if (b >= 0xc0) n = 2;
|
|
252
|
+
ch = seq.toString('utf8', i, i + n);
|
|
253
|
+
step = ch.length || n;
|
|
254
|
+
}
|
|
255
|
+
this.line = this.line.slice(0, this.cursor) + ch + this.line.slice(this.cursor);
|
|
256
|
+
this.cursor += ch.length;
|
|
257
|
+
this._redrawPrompt();
|
|
258
|
+
i += step;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
i++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_isMultibyte(seq, i) {
|
|
266
|
+
const b = seq[i];
|
|
267
|
+
let n = 1;
|
|
268
|
+
if (b >= 0xf0) n = 4;
|
|
269
|
+
else if (b >= 0xe0) n = 3;
|
|
270
|
+
else if (b >= 0xc0) n = 2;
|
|
271
|
+
return n > 1 && i + n <= seq.length;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_enter() {
|
|
275
|
+
this._pushHistory(this.line);
|
|
276
|
+
const text = this.line;
|
|
277
|
+
this.line = '';
|
|
278
|
+
this.cursor = 0;
|
|
279
|
+
this.histIdx = -1;
|
|
280
|
+
if (this.pending === '' && text.trim() === '') { this._redrawPrompt(); return; }
|
|
281
|
+
this.pending += (this.pending ? '\n' : '') + text;
|
|
282
|
+
if (this._statementComplete(this.pending)) {
|
|
283
|
+
const stmt = this.pending;
|
|
284
|
+
this.pending = '';
|
|
285
|
+
this._exec(stmt);
|
|
286
|
+
} else {
|
|
287
|
+
this._redrawPrompt();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
_statementComplete(sql) {
|
|
292
|
+
let inSQ = false, inDQ = false, parens = 0;
|
|
293
|
+
for (let i = 0; i < sql.length; i++) {
|
|
294
|
+
const c = sql[i];
|
|
295
|
+
if (c === "'" && !inDQ) inSQ = !inSQ;
|
|
296
|
+
else if (c === '"' && !inSQ) inDQ = !inDQ;
|
|
297
|
+
else if (!inSQ && !inDQ) {
|
|
298
|
+
if (c === '(') parens++;
|
|
299
|
+
else if (c === ')') parens--;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const trimmed = sql.trimEnd();
|
|
303
|
+
return !inSQ && !inDQ && parens <= 0 && (trimmed.endsWith(';') || trimmed.endsWith('\\g'));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_backspace() {
|
|
307
|
+
if (this.cursor <= 0) return;
|
|
308
|
+
this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
|
|
309
|
+
this.cursor--;
|
|
310
|
+
this._redrawPrompt();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_deleteAt() {
|
|
314
|
+
if (this.cursor >= this.line.length) return;
|
|
315
|
+
this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1);
|
|
316
|
+
this._redrawPrompt();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_moveLeft() { if (this.cursor > 0) { this.cursor--; this._redrawPrompt(); } }
|
|
320
|
+
_moveRight() { if (this.cursor < this.line.length) { this.cursor++; this._redrawPrompt(); } }
|
|
321
|
+
|
|
322
|
+
_histPrev() {
|
|
323
|
+
if (this.history.length === 0) return;
|
|
324
|
+
if (this.histIdx === -1) this.histIdx = this.history.length - 1;
|
|
325
|
+
else if (this.histIdx > 0) this.histIdx--;
|
|
326
|
+
this.line = this.history[this.histIdx];
|
|
327
|
+
this.cursor = this.line.length;
|
|
328
|
+
this._redrawPrompt();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_histNext() {
|
|
332
|
+
if (this.histIdx === -1) return;
|
|
333
|
+
this.histIdx++;
|
|
334
|
+
if (this.histIdx >= this.history.length) { this.histIdx = -1; this.line = ''; }
|
|
335
|
+
else this.line = this.history[this.histIdx];
|
|
336
|
+
this.cursor = this.line.length;
|
|
337
|
+
this._redrawPrompt();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_pushHistory(line) {
|
|
341
|
+
const t = line.trim();
|
|
342
|
+
if (!t || t.startsWith('\\')) return;
|
|
343
|
+
if (this.history[this.history.length - 1] === t) return;
|
|
344
|
+
this.history.push(t);
|
|
345
|
+
if (this.history.length > MAX_HISTORY) this.history = this.history.slice(-MAX_HISTORY);
|
|
346
|
+
this._saveHistory();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_cancelLine() {
|
|
350
|
+
this.line = '';
|
|
351
|
+
this.cursor = 0;
|
|
352
|
+
if (this.pending) {
|
|
353
|
+
this.pending = '';
|
|
354
|
+
process.stdout.write('\r' + CLEAR_LINE + (this.pending ? '' : ''));
|
|
355
|
+
}
|
|
356
|
+
this._redrawPrompt();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
_quit() {
|
|
360
|
+
this._exiting = true;
|
|
361
|
+
process.stdout.write('\r' + CLEAR_LINE + SHOW_CURSOR + '\n');
|
|
362
|
+
this._saveHistory();
|
|
363
|
+
try { process.stdin.setRawMode(false); } catch (e) {}
|
|
364
|
+
process.stdin.pause();
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
_redrawPrompt() {
|
|
369
|
+
const prompt = this.pending ? `${GREEN} ->${RESET} ` : `${GREEN}jsql>${RESET} `;
|
|
370
|
+
const display = this.line.slice(0, this.cursor) + this.line.slice(this.cursor);
|
|
371
|
+
process.stdout.write('\r' + CLEAR_LINE + prompt + display);
|
|
372
|
+
const left = wswidth(prompt + this.line.slice(0, this.cursor));
|
|
373
|
+
if (left > 0) process.stdout.write(`\x1b[${left}D`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_clearScreen() {
|
|
377
|
+
process.stdout.write(CLEAR_SCREEN + HOME);
|
|
378
|
+
this._statusBar();
|
|
379
|
+
process.stdout.write('\n');
|
|
380
|
+
this._redrawPrompt();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_statusBar() {
|
|
384
|
+
const mode = this.dataDir ? `data: ${this.dataDir}` : 'in-memory';
|
|
385
|
+
const bar = `${BOLD}${GREEN} jsql-neo ${RESET}${GRAY}v${require('../package.json').version}${RESET} | db: ${CYAN}${this.dbName}${RESET} | ${GRAY}${mode}${RESET} | dialect: ${this.dialect}`;
|
|
386
|
+
process.stdout.write('\x1b[7m' + bar + RESET + '\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* ---------- completion ---------- */
|
|
390
|
+
|
|
391
|
+
_complete() {
|
|
392
|
+
const keywords = ['SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
|
|
393
|
+
'CREATE', 'TABLE', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'PRIMARY', 'KEY', 'INDEX', 'AND', 'OR', 'NOT',
|
|
394
|
+
'NULL', 'LIKE', 'ILIKE', 'IN', 'BETWEEN', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET',
|
|
395
|
+
'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'ON', 'AS', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MIN',
|
|
396
|
+
'MAX', 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'USE', 'EXPLAIN', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END'];
|
|
397
|
+
const lastWord = this.line.slice(0, this.cursor).match(/[A-Za-z_][A-Za-z0-9_]*$/);
|
|
398
|
+
if (!lastWord) { process.stdout.write('\a'); return; }
|
|
399
|
+
const w = lastWord[0];
|
|
400
|
+
const rest = this.line.slice(this.cursor);
|
|
401
|
+
const match = keywords.find((k) => k.startsWith(w.toUpperCase()));
|
|
402
|
+
if (!match) { process.stdout.write('\a'); return; }
|
|
403
|
+
this.line = this.line.slice(0, this.cursor - w.length) + match + rest;
|
|
404
|
+
this.cursor += match.length - w.length;
|
|
405
|
+
this._redrawPrompt();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* ---------- execution ---------- */
|
|
409
|
+
|
|
410
|
+
async _exec(sql) {
|
|
411
|
+
const start = Date.now();
|
|
412
|
+
try {
|
|
413
|
+
const result = await executeSQL(this.engine, sql, { dialect: this.dialect, safety: false });
|
|
414
|
+
const elapsed = Date.now() - start;
|
|
415
|
+
this._showResult(result, elapsed);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
this._err(e.message || String(e));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_showResult(result, elapsed) {
|
|
422
|
+
if (result == null) { this._out(`(no result) ${GRAY}${elapsed} ms${RESET}`); return; }
|
|
423
|
+
if (Array.isArray(result)) {
|
|
424
|
+
result.forEach((r) => this._showResult(r, elapsed));
|
|
425
|
+
this._redrawPrompt();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (result.ok === true && result.columns && result.type === 'select') {
|
|
429
|
+
const maxW = (process.stdout.columns || 120) - 4;
|
|
430
|
+
this._out(renderTable(result.columns, result.rows, maxW));
|
|
431
|
+
const n = result.rows.length;
|
|
432
|
+
this._out(`${n} row${n === 1 ? '' : 's'} in set ${GRAY}(${elapsed} ms)${RESET}`);
|
|
433
|
+
} else if (result.ok === true && (result.affectedRows !== undefined || result.rows !== undefined)) {
|
|
434
|
+
const n = result.affectedRows !== undefined ? result.affectedRows : (Array.isArray(result.rows) ? result.rows.length : 0);
|
|
435
|
+
this._out(`${GREEN}Query OK${RESET}, ${n} row${n === 1 ? '' : 's'} affected ${GRAY}(${elapsed} ms)${RESET}`);
|
|
436
|
+
} else if (result.ok === true && result.message) {
|
|
437
|
+
this._out(GREEN + result.message + RESET);
|
|
438
|
+
} else if (result.ok === true && result.rows && Array.isArray(result.rows)) {
|
|
439
|
+
this._out(renderTable(result.columns || Object.keys(result.rows[0] || {}), result.rows, (process.stdout.columns || 120) - 4));
|
|
440
|
+
} else {
|
|
441
|
+
this._out(JSON.stringify(result, null, 2));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_out(text) {
|
|
446
|
+
if (this.batch) { console.log(text.replace(/\x1b\[[0-9;]*m/g, '')); return; }
|
|
447
|
+
process.stdout.write('\r' + CLEAR_LINE + text + '\n');
|
|
448
|
+
this._redrawPrompt();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
_err(msg) {
|
|
452
|
+
if (this.batch) { console.error(msg.replace(/\x1b\[[0-9;]*m/g, '')); return; }
|
|
453
|
+
process.stdout.write('\r' + CLEAR_LINE + `${RED}ERROR ${RESET}${msg}\n`);
|
|
454
|
+
this._redrawPrompt();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* ---------- meta ---------- */
|
|
458
|
+
|
|
459
|
+
_meta(line, batch) {
|
|
460
|
+
const out = (t) => (batch ? console.log(String(t).replace(/\x1b\[[0-9;]*m/g, '')) : this._out(t));
|
|
461
|
+
const [head, ...rest] = line.trim().split(/\s+/);
|
|
462
|
+
const arg = rest.join(' ');
|
|
463
|
+
switch (head.toLowerCase()) {
|
|
464
|
+
case '\\q': case '\\quit': case '\\exit': case 'exit': case 'quit':
|
|
465
|
+
if (batch) return 'quit';
|
|
466
|
+
this._quit(); break;
|
|
467
|
+
case '\\c': case '\\clear':
|
|
468
|
+
if (!batch) this._clearScreen(); break;
|
|
469
|
+
case '\\db': return out(`current database: ${CYAN}${this.dbName}${RESET}`);
|
|
470
|
+
case '\\use': {
|
|
471
|
+
if (!arg) return out('usage: \\use <name>');
|
|
472
|
+
if (!this._engines.has(arg)) this._engines.set(arg, this._openEngine(arg));
|
|
473
|
+
this.engine = this._engines.get(arg);
|
|
474
|
+
this.dbName = arg;
|
|
475
|
+
return out(`switched to database: ${CYAN}${arg}${RESET}`);
|
|
476
|
+
}
|
|
477
|
+
case '\\tables': {
|
|
478
|
+
const meta = this.engine._meta && this.engine._meta.tables ? Object.keys(this.engine._meta.tables) : [];
|
|
479
|
+
const runtime = Object.keys(this.engine._tables || {});
|
|
480
|
+
const tables = [...new Set([...meta, ...runtime])];
|
|
481
|
+
if (tables.length === 0) return out('(no tables)');
|
|
482
|
+
return out(renderTable(['Table'], tables.map((t) => [t]), (process.stdout.columns || 120) - 4));
|
|
483
|
+
}
|
|
484
|
+
case '\\desc': {
|
|
485
|
+
if (!arg) return out('usage: \\desc <table>');
|
|
486
|
+
const schema = this.engine.getTableSchema(arg);
|
|
487
|
+
if (!schema) return out(`${RED}table not found:${RESET} ${arg}`);
|
|
488
|
+
return out(renderTable(['Field', 'Type'], Object.entries(schema || {}).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]), (process.stdout.columns || 120) - 4));
|
|
489
|
+
}
|
|
490
|
+
case '\\help': case '\\?':
|
|
491
|
+
return out(renderTable(['Command', 'Description'], META_HELP, (process.stdout.columns || 120) - 4));
|
|
492
|
+
default:
|
|
493
|
+
return out(`${RED}unknown meta command:${RESET} ${head} (try \\help)`);
|
|
494
|
+
}
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function createTUI(options) {
|
|
500
|
+
return new TUIShell(options || {});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
module.exports = { TUIShell, createTUI, renderTable, wswidth, pad };
|