jsql-neo 5.2.0 → 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.
@@ -24,9 +24,11 @@ const TYPE_SIGNATURES = {
24
24
  PING: 0, ECHO: 1, SET: 2, SETNX: 2, GET: 1, DEL: -1, EXISTS: -1, KEYS: 1,
25
25
  TYPE: 1, EXPIRE: 2, TTL: 1, PERSIST: 1,
26
26
  INCR: 1, DECR: 1, INCRBY: 2, DECRBY: 2, APPEND: 2, STRLEN: 1,
27
+ MSET: -2, MGET: -1,
27
28
  HSET: -3, HGET: 2, HGETALL: 1, HDEL: -2, HEXISTS: 2, HLEN: 1, HKEYS: 1, HVALS: 1,
28
29
  LPUSH: -2, RPUSH: -2, LPOP: 1, RPOP: 1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
29
30
  SADD: -2, SREM: -2, SMEMBERS: 1, SISMEMBER: 2, SCARD: 1,
31
+ ZADD: -3, ZRANGE: -3, ZREVRANGE: -3, ZSCORE: 2, ZCARD: 1, ZREM: -2, ZINCRBY: 3,
30
32
  DBSIZE: 0, FLUSHALL: 0, FLUSHDB: 0, SELECT: 1, INFO: 0, AUTH: 1, QUIT: 0,
31
33
  };
32
34
 
@@ -333,6 +335,109 @@ class RedisServer {
333
335
  const v = this._get(args[0]);
334
336
  return v && v.type === 'set' ? v.val.length : 0;
335
337
  }
338
+ case 'ZADD': {
339
+ const key = args[0];
340
+ const v = this._get(key);
341
+ let zset;
342
+ if (v && v.type === 'zset') zset = v.val;
343
+ else if (v) throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
344
+ else { this.db.set(key, { type: 'zset', val: {} }); zset = this.db.get(key).val; }
345
+ const rest = args.slice(1);
346
+ if (rest.length % 2 !== 0) throw new Error('ERR syntax error');
347
+ let added = 0;
348
+ for (let i = 0; i < rest.length; i += 2) {
349
+ const score = parseFloat(rest[i]);
350
+ const member = rest[i + 1];
351
+ if (isNaN(score)) throw new Error('ERR value is not a valid float');
352
+ if (!(member in zset)) added++;
353
+ zset[member] = score;
354
+ }
355
+ this._scheduleSnapshot();
356
+ return added;
357
+ }
358
+ case 'ZRANGE': {
359
+ const v = this._get(args[0]);
360
+ if (!v) return [];
361
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
362
+ const start = parseInt(args[1], 10);
363
+ const stop = parseInt(args[2], 10);
364
+ const withScores = args[3] && args[3].toUpperCase() === 'WITHSCORES';
365
+ const entries = Object.entries(v.val).sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1));
366
+ const from = start < 0 ? Math.max(0, entries.length + start) : start;
367
+ const to = stop < 0 ? entries.length + stop : stop;
368
+ const out = [];
369
+ for (let i = from; i <= to && i < entries.length; i++) {
370
+ out.push(entries[i][0]);
371
+ if (withScores) out.push(entries[i][1]);
372
+ }
373
+ return out;
374
+ }
375
+ case 'ZREVRANGE': {
376
+ const v = this._get(args[0]);
377
+ if (!v) return [];
378
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
379
+ const start = parseInt(args[1], 10);
380
+ const stop = parseInt(args[2], 10);
381
+ const withScores = args[3] && args[3].toUpperCase() === 'WITHSCORES';
382
+ const entries = Object.entries(v.val).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? 1 : -1));
383
+ const from = start < 0 ? Math.max(0, entries.length + start) : start;
384
+ const to = stop < 0 ? entries.length + stop : stop;
385
+ const out = [];
386
+ for (let i = from; i <= to && i < entries.length; i++) {
387
+ out.push(entries[i][0]);
388
+ if (withScores) out.push(entries[i][1]);
389
+ }
390
+ return out;
391
+ }
392
+ case 'ZSCORE': {
393
+ const v = this._get(args[0]);
394
+ if (!v || v.type !== 'zset' || !(args[1] in v.val)) return null;
395
+ return v.val[args[1]];
396
+ }
397
+ case 'ZCARD': {
398
+ const v = this._get(args[0]);
399
+ if (!v) return 0;
400
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
401
+ return Object.keys(v.val).length;
402
+ }
403
+ case 'ZREM': {
404
+ const v = this._get(args[0]);
405
+ if (!v) return 0;
406
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
407
+ let n = 0;
408
+ for (const m of args.slice(1)) {
409
+ if (m in v.val) { delete v.val[m]; n++; }
410
+ }
411
+ this._scheduleSnapshot();
412
+ return n;
413
+ }
414
+ case 'ZINCRBY': {
415
+ const key = args[0];
416
+ const inc = parseFloat(args[1]);
417
+ if (isNaN(inc)) throw new Error('ERR value is not a valid float');
418
+ const v = this._get(key);
419
+ let zset;
420
+ if (v && v.type === 'zset') zset = v.val;
421
+ else if (v) throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
422
+ else { this.db.set(key, { type: 'zset', val: {} }); zset = this.db.get(key).val; }
423
+ const member = args[2];
424
+ zset[member] = (zset[member] || 0) + inc;
425
+ this._scheduleSnapshot();
426
+ return zset[member];
427
+ }
428
+ case 'MSET': {
429
+ const rest = args;
430
+ if (rest.length % 2 !== 0) throw new Error('ERR wrong number of arguments');
431
+ for (let i = 0; i < rest.length; i += 2) this.db.set(rest[i], { type: 'string', val: rest[i + 1], ttl: null });
432
+ this._scheduleSnapshot();
433
+ return 'OK';
434
+ }
435
+ case 'MGET': {
436
+ return args.map((k) => {
437
+ const v = this._get(k);
438
+ return v && v.type === 'string' ? v.val : null;
439
+ });
440
+ }
336
441
  case 'DBSIZE': return this.db.size;
337
442
  case 'FLUSHALL': case 'FLUSHDB':
338
443
  this.db.clear();
@@ -358,25 +463,34 @@ class RedisServer {
358
463
 
359
464
  /* ---------- RESP protocol ---------- */
360
465
  listen() {
361
- this.server = net.createServer((socket) => {
362
- let buf = Buffer.alloc(0);
363
- this._authedSockets.set(socket, false);
364
- socket.setEncoding('utf8');
365
- socket.on('data', (chunk) => {
366
- buf += chunk;
367
- for (;;) {
368
- const msg = this._parse(buf);
369
- if (!msg) break;
370
- buf = buf.slice(msg.consumed);
371
- this._handle(socket, msg.cmd, msg.args);
372
- }
373
- });
374
- socket.on('error', () => {});
375
- });
466
+ this.server = net.createServer((socket) => this._handleSocket(socket));
376
467
  this.server.listen(this.port, this.host);
377
468
  return this;
378
469
  }
379
470
 
471
+ /** 供多协议嗅探复用:把 socket 交给 Redis 处理,可携带已缓冲的首包 */
472
+ _handleSocket(socket, existing = '') {
473
+ let buf = String(existing || '');
474
+ this._authedSockets.set(socket, false);
475
+ socket.setEncoding('utf8');
476
+ socket.on('data', (chunk) => {
477
+ buf += chunk;
478
+ for (;;) {
479
+ const msg = this._parse(buf);
480
+ if (!msg) break;
481
+ buf = buf.slice(msg.consumed);
482
+ this._handle(socket, msg.cmd, msg.args);
483
+ }
484
+ });
485
+ socket.on('error', () => {});
486
+ for (;;) {
487
+ const msg = this._parse(buf);
488
+ if (!msg) break;
489
+ buf = buf.slice(msg.consumed);
490
+ this._handle(socket, msg.cmd, msg.args);
491
+ }
492
+ }
493
+
380
494
  _parse(buf) {
381
495
  if (buf[0] !== '*') {
382
496
  const i = buf.indexOf('\r\n');
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",