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/bin/jsql CHANGED
@@ -215,6 +215,19 @@ const cli = yaggs({ pkg: require('../package.json') })
215
215
  setInterval(() => {}, 1 << 30);
216
216
  } catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
217
217
  })
218
+ .command('tui', 'Interactive SQL terminal (zero-dependency TUI)', (sub) => {
219
+ sub.option('data-dir', { alias: ['d'], type: 'string', description: 'Data directory (default: in-memory)' });
220
+ sub.option('db', { type: 'string', description: 'Database name (default: default)' });
221
+ sub.option('dialect', { alias: ['t'], type: 'string', description: 'SQL dialect: mysql|pg (default: mysql)' });
222
+ }, (argv) => {
223
+ const { createTUI } = require('../lib/tui');
224
+ const tui = createTUI({
225
+ dataDir: argv['data-dir'],
226
+ db: argv.db || 'default',
227
+ dialect: argv.dialect || 'mysql',
228
+ });
229
+ tui.run().catch((e) => { console.error(`Error: ${e.message}`); process.exit(1); });
230
+ })
218
231
  .command('redis', 'Run the Redis-compatible server', (sub) => {
219
232
  sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 6379)' });
220
233
  sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
package/index.js CHANGED
@@ -22,6 +22,10 @@ const { createMysqlServer, MysqlServer } = require('./lib/mysql_server');
22
22
  const migrate = require('./lib/migrate');
23
23
  const { WebUI } = require('./lib/web_ui');
24
24
  const { RedisServer, createRedisServer } = require('./lib/redis_server');
25
+ const { PgServer, createPgServer } = require('./lib/pg_server');
26
+ const { MongoServer, createMongoServer } = require('./lib/mongo_server');
27
+ const { MultiServer, createMultiServer } = require('./lib/multiserver');
28
+ const { TUIShell, createTUI } = require('./lib/tui');
25
29
 
26
30
  /**
27
31
  * 全局注入:把项目内 `require('mysql2')` 全部替换为 jsql-neo 内存引擎兼容层。
@@ -99,4 +103,16 @@ module.exports = {
99
103
  // Redis 兼容服务器
100
104
  RedisServer,
101
105
  createRedisServer,
106
+ // PostgreSQL wire protocol 服务器
107
+ PgServer,
108
+ createPgServer,
109
+ // MongoDB wire protocol 服务器
110
+ MongoServer,
111
+ createMongoServer,
112
+ // 多协议嗅探服务器(MySQL + PG + Redis + Mongo 同端口)
113
+ MultiServer,
114
+ createMultiServer,
115
+ // 交互式 TUI
116
+ TUIShell,
117
+ createTUI,
102
118
  };
@@ -322,7 +322,7 @@ class MongoServer {
322
322
  localTime: new Date(), logicalSessionTimeoutMinutes: 30, connectionId: 1,
323
323
  };
324
324
  case 'ping': return { ok: 1 };
325
- case 'buildInfo': return { ok: 1, version: '5.2.1-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 2, 1, 0] };
325
+ case 'buildInfo': return { ok: 1, version: '5.3.0-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 3, 0, 0] };
326
326
  case 'getParameter': return { ok: 1 };
327
327
  case 'endSessions': return { ok: 1 };
328
328
  case 'listDatabases':
@@ -357,13 +357,53 @@ class MongoServer {
357
357
  }
358
358
  return { ok: 1, n: docs.length };
359
359
  }
360
+ case 'findAndModify': {
361
+ const coll = String(doc.findAndModify);
362
+ const engine = await this._ensureCollection(coll);
363
+ const q = doc.query || {};
364
+ const rows = this._matching(engine, coll, q);
365
+ const found = rows[0] || null;
366
+ const set = doc.update && doc.update.$set ? doc.update.$set : (doc.update || {});
367
+ let value = null;
368
+ if (found) {
369
+ if (doc.remove) {
370
+ this._removeRows(engine, coll, [found]);
371
+ value = found;
372
+ } else {
373
+ const patched = { ...found, ...set };
374
+ this._patchRows(engine, coll, [found], set);
375
+ value = doc.new ? patched : found;
376
+ }
377
+ } else if (doc.upsert) {
378
+ const merged = { ...q, ...set };
379
+ const id = engine.insert(coll, merged);
380
+ value = doc.new ? { ...merged, _id: id } : null;
381
+ }
382
+ return { ok: 1, value, lastErrorObject: { n: found ? 1 : (doc.upsert ? 1 : 0), updatedExisting: !!found && !doc.remove } };
383
+ }
384
+ case 'distinct': {
385
+ const coll = String(doc.distinct);
386
+ const engine = await this._ensureCollection(coll);
387
+ const key = String(doc.key || '');
388
+ const rows = this._matching(engine, coll, doc.query);
389
+ const seen = new Set();
390
+ const values = [];
391
+ for (const r of rows) {
392
+ const v = r[key];
393
+ if (!seen.has(JSON.stringify(v))) { seen.add(JSON.stringify(v)); values.push(v); }
394
+ }
395
+ return { ok: 1, values };
396
+ }
397
+ case 'dropDatabase':
398
+ return { ok: 1, dropped: String(doc.dropDatabase || this._dbOf(doc)) };
360
399
  case 'find': {
361
400
  const coll = String(doc.find);
362
401
  const engine = await this._ensureCollection(coll);
363
- const rows = engine.find(coll, doc.filter || {}, {
364
- limit: typeof doc.limit === 'number' ? doc.limit : 0,
365
- offset: typeof doc.skip === 'number' ? doc.skip : 0,
366
- });
402
+ let rows = this._matching(engine, coll, doc.filter);
403
+ const skip = typeof doc.skip === 'number' ? doc.skip : 0;
404
+ if (skip > 0) rows = rows.slice(skip);
405
+ const limit = typeof doc.limit === 'number' ? doc.limit : 0;
406
+ if (limit > 0) rows = rows.slice(0, limit);
367
407
  return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
368
408
  }
369
409
  case 'getMore':
@@ -377,7 +417,8 @@ class MongoServer {
377
417
  const q = u.q || {};
378
418
  const set = u.u && u.u.$set ? u.u.$set : (u.u || {});
379
419
  // 单文档更新
380
- let target = engine.find(coll, q, { limit: 1 })[0] || null;
420
+ let targets = this._matching(engine, coll, q);
421
+ let target = targets[0] || null;
381
422
  if (!target && u.upsert) {
382
423
  const merged = { ...q, ...set };
383
424
  engine.insert(coll, merged);
@@ -385,7 +426,7 @@ class MongoServer {
385
426
  continue;
386
427
  }
387
428
  if (target) {
388
- engine.update(coll, q, set);
429
+ this._patchRows(engine, coll, [target], set);
389
430
  n++;
390
431
  }
391
432
  }
@@ -396,15 +437,16 @@ class MongoServer {
396
437
  const engine = await this._ensureCollection(coll);
397
438
  let n = 0;
398
439
  for (const d of (doc.deletes || [])) {
399
- const res = engine.removeWhere(coll, d.q || {});
400
- n += res.count;
440
+ const rows = this._matching(engine, coll, d.q || {});
441
+ if (rows.length > 0) this._removeRows(engine, coll, rows);
442
+ n += rows.length;
401
443
  }
402
444
  return { ok: 1, n };
403
445
  }
404
446
  case 'count': {
405
447
  const coll = String(doc.count);
406
448
  const engine = await this._ensureCollection(coll);
407
- const rows = engine.find(coll, doc.query || {});
449
+ const rows = this._matching(engine, coll, doc.query);
408
450
  return { ok: 1, n: Number(doc.limit) > 0 ? Math.min(rows.length, doc.limit) : rows.length };
409
451
  }
410
452
  case 'aggregate': {
@@ -416,6 +458,46 @@ class MongoServer {
416
458
  if (stage.$match) rows = rows.filter((r) => this._match(r, stage.$match));
417
459
  else if (stage.$count) rows = [{ [stage.$count]: rows.length }];
418
460
  else if (stage.$limit) rows = rows.slice(0, stage.$limit);
461
+ else if (stage.$skip) rows = rows.slice(Number(stage.$skip) || 0);
462
+ else if (stage.$sort) {
463
+ const sortKeys = Object.entries(stage.$sort);
464
+ rows = [...rows].sort((a, b) => {
465
+ for (const [k, dir] of sortKeys) {
466
+ const av = a[k]; const bv = b[k];
467
+ if (av === bv) continue;
468
+ if (av == null) return 1;
469
+ if (bv == null) return -1;
470
+ const cmp = av < bv ? -1 : 1;
471
+ return Number(dir) < 0 ? -cmp : cmp;
472
+ }
473
+ return 0;
474
+ });
475
+ }
476
+ else if (stage.$project) {
477
+ const proj = stage.$project;
478
+ rows = rows.map((r) => {
479
+ const out = {};
480
+ for (const [k, v] of Object.entries(proj)) {
481
+ if (k === '_id' && v === 0) continue;
482
+ if (v === 0) continue;
483
+ if (typeof v === 'string' && v.startsWith('$')) out[k] = r[v.slice(1)];
484
+ else if (v === 1) out[k] = r[k];
485
+ else if (v === 0) { /* exclude */ }
486
+ else out[k] = v;
487
+ }
488
+ return out;
489
+ });
490
+ }
491
+ else if (stage.$unwind) {
492
+ const field = String(stage.$unwind).replace(/^\$/, '');
493
+ const out = [];
494
+ for (const r of rows) {
495
+ const arr = r[field];
496
+ if (!Array.isArray(arr) || arr.length === 0) { out.push({ ...r, [field]: null }); continue; }
497
+ for (const item of arr) out.push({ ...r, [field]: item });
498
+ }
499
+ rows = out;
500
+ }
419
501
  else if (stage.$group) {
420
502
  const acc = {};
421
503
  for (const [k, v] of Object.entries(stage.$group)) {
@@ -435,12 +517,45 @@ class MongoServer {
435
517
  return row._id != null ? row._id : row.id;
436
518
  }
437
519
 
520
+ _matching(engine, coll, filter) {
521
+ return engine.find(coll, {}).filter((r) => this._match(r, filter || {}));
522
+ }
523
+
524
+ _patchRows(engine, coll, rows, set) {
525
+ const table = engine._ensureTable(coll);
526
+ for (const r of rows) Object.assign(r, set);
527
+ table._rebuildPKIndex && table._rebuildPKIndex();
528
+ table._rebuildAllBTrees && table._rebuildAllBTrees();
529
+ engine._markDirty && engine._markDirty(coll);
530
+ }
531
+
532
+ _removeRows(engine, coll, rows) {
533
+ const table = engine._ensureTable(coll);
534
+ const gone = new Set(rows);
535
+ table._rows = table._rows.filter((r) => !gone.has(r));
536
+ table._rebuildPKIndex && table._rebuildPKIndex();
537
+ table._rebuildAllBTrees && table._rebuildAllBTrees();
538
+ engine._markDirty && engine._markDirty(coll);
539
+ }
540
+
438
541
  _match(row, filter) {
439
542
  for (const [k, cond] of Object.entries(filter || {})) {
440
543
  if (k === '$or') {
441
544
  if (!cond.some((f) => this._match(row, f))) return false;
442
545
  continue;
443
546
  }
547
+ if (k === '$and') {
548
+ if (!cond.every((f) => this._match(row, f))) return false;
549
+ continue;
550
+ }
551
+ if (k === '$nor') {
552
+ if (cond.some((f) => this._match(row, f))) return false;
553
+ continue;
554
+ }
555
+ if (k === '$not') {
556
+ if (this._match(row, cond)) return false;
557
+ continue;
558
+ }
444
559
  if (cond && typeof cond === 'object' && !Array.isArray(cond)) {
445
560
  for (const [op, v] of Object.entries(cond)) {
446
561
  const rv = row[k];
@@ -452,6 +567,16 @@ class MongoServer {
452
567
  if (op === '$in' && !(v.includes(rv))) return false;
453
568
  if (op === '$nin' && v.includes(rv)) return false;
454
569
  if (op === '$exists' && (v ? (rv === undefined) : (rv !== undefined))) return false;
570
+ if (op === '$regex') {
571
+ const flags = String(cond.$options || '').includes('i') ? 'i' : '';
572
+ if (typeof rv !== 'string' || !new RegExp(String(v), flags).test(rv)) return false;
573
+ }
574
+ if (op === '$type') {
575
+ const t = v === 'string' ? 'string' : v === 'int' || v === 'long' || v === 'double' || v === 'number' ? 'number' : v === 'bool' ? 'boolean' : v === 'null' ? 'null' : v === 'array' ? 'array' : typeof rv;
576
+ if (typeof rv !== t) return false;
577
+ }
578
+ if (op === '$size' && !(Array.isArray(rv) && rv.length === v)) return false;
579
+ if (op === '$elemMatch' && !(Array.isArray(rv) && rv.some((el) => this._match(el, v)))) return false;
455
580
  }
456
581
  } else if (row[k] !== cond) {
457
582
  return false;
@@ -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();