jsql-neo 5.2.1 → 5.3.1
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 +7622 -209
- package/bin/jsql +13 -0
- package/index.js +16 -0
- package/lib/mongo_server.js +135 -10
- package/lib/query.js +20 -2
- package/lib/redis_server.js +105 -0
- package/lib/sql.js +44 -6
- package/lib/table.js +20 -2
- package/lib/tui.js +503 -0
- package/package.json +1 -1
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
|
};
|
package/lib/mongo_server.js
CHANGED
|
@@ -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.
|
|
325
|
+
case 'buildInfo': return { ok: 1, version: '5.3.1-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 3, 1, 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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
400
|
-
|
|
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 =
|
|
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;
|
package/lib/query.js
CHANGED
|
@@ -537,7 +537,7 @@ class Query {
|
|
|
537
537
|
const key = fr[join.foreignField];
|
|
538
538
|
const matches = localRows.filter(lr => lr[join.localField] === key);
|
|
539
539
|
if (matches.length === 0) {
|
|
540
|
-
result.push(
|
|
540
|
+
result.push(this._rightNullRow(localRows, fr, join));
|
|
541
541
|
} else {
|
|
542
542
|
for (const lr of matches) {
|
|
543
543
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -582,7 +582,7 @@ class Query {
|
|
|
582
582
|
for (const fr of foreignRows) {
|
|
583
583
|
const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
|
|
584
584
|
if (matches.length === 0) {
|
|
585
|
-
result.push(
|
|
585
|
+
result.push(this._rightNullRow(localRows, fr, join));
|
|
586
586
|
} else {
|
|
587
587
|
for (const lr of matches) {
|
|
588
588
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -621,6 +621,24 @@ class Query {
|
|
|
621
621
|
return nulls;
|
|
622
622
|
}
|
|
623
623
|
|
|
624
|
+
/**
|
|
625
|
+
* RIGHT JOIN 未匹配右表行:保留右表数据,本地表字段填 null。
|
|
626
|
+
* 返回 { localNulls, merged },其中 merged = { ...nulls, ...fr }
|
|
627
|
+
*/
|
|
628
|
+
_rightNullRow(localRows, foreignRow, join) {
|
|
629
|
+
const localSchema = this._table._schema || {};
|
|
630
|
+
const localNulls = {};
|
|
631
|
+
for (const field of Object.keys(localSchema)) {
|
|
632
|
+
if (field !== '_softDelete') localNulls[field] = null;
|
|
633
|
+
}
|
|
634
|
+
const prefix = join.as ? join.as + '_' : '';
|
|
635
|
+
const foreignPrefixed = {};
|
|
636
|
+
for (const [key, value] of Object.entries(foreignRow)) {
|
|
637
|
+
foreignPrefixed[prefix + key] = value;
|
|
638
|
+
}
|
|
639
|
+
return { ...localNulls, ...foreignPrefixed };
|
|
640
|
+
}
|
|
641
|
+
|
|
624
642
|
// ============================================================
|
|
625
643
|
// 内部: 排序
|
|
626
644
|
// ============================================================
|
package/lib/redis_server.js
CHANGED
|
@@ -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();
|
package/lib/sql.js
CHANGED
|
@@ -261,12 +261,18 @@ class Parser {
|
|
|
261
261
|
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
262
262
|
this.next();
|
|
263
263
|
const t2 = this.next();
|
|
264
|
-
if (t2.type !== 'ident'
|
|
264
|
+
if (t2.type !== 'ident' && !(t2.type === 'keyword' && this._isSchemaView(t2.value))) {
|
|
265
|
+
throw new Error(`Expected table name after '.', got '${t2.value}'`);
|
|
266
|
+
}
|
|
265
267
|
return t.value + '.' + t2.value;
|
|
266
268
|
}
|
|
267
269
|
return t.value;
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
_isSchemaView(v) {
|
|
273
|
+
return ['TABLES', 'COLUMNS', 'SCHEMATA', 'STATISTICS', 'KEY_COLUMN_USAGE', 'REFERENTIAL_CONSTRAINTS', 'TABLE_CONSTRAINTS', 'VIEWS'].includes(String(v).toUpperCase());
|
|
274
|
+
}
|
|
275
|
+
|
|
270
276
|
parseCreateTable() {
|
|
271
277
|
this.expectKeyword('CREATE');
|
|
272
278
|
this.expectKeyword('TABLE');
|
|
@@ -1870,11 +1876,12 @@ class SQLExecutor {
|
|
|
1870
1876
|
const schema = this.engine.getTableSchema
|
|
1871
1877
|
? await this.engine.getTableSchema(statement.table)
|
|
1872
1878
|
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
1879
|
+
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1873
1880
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1874
1881
|
let count = 0;
|
|
1875
1882
|
for (const row of all) {
|
|
1876
1883
|
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1877
|
-
const id =
|
|
1884
|
+
const id = this._rowPkId(row, pkCols);
|
|
1878
1885
|
if (id !== undefined) {
|
|
1879
1886
|
const data = {};
|
|
1880
1887
|
for (const [col, val] of statement.assignments) {
|
|
@@ -1892,11 +1899,12 @@ class SQLExecutor {
|
|
|
1892
1899
|
const schema = this.engine.getTableSchema
|
|
1893
1900
|
? await this.engine.getTableSchema(statement.table)
|
|
1894
1901
|
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
1902
|
+
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1895
1903
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1896
1904
|
const ids = [];
|
|
1897
1905
|
for (const row of all) {
|
|
1898
1906
|
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1899
|
-
const id =
|
|
1907
|
+
const id = this._rowPkId(row, pkCols);
|
|
1900
1908
|
if (id !== undefined) ids.push(id);
|
|
1901
1909
|
}
|
|
1902
1910
|
}
|
|
@@ -2166,6 +2174,19 @@ class SQLExecutor {
|
|
|
2166
2174
|
return schema;
|
|
2167
2175
|
}
|
|
2168
2176
|
|
|
2177
|
+
/**
|
|
2178
|
+
* 取行的行 ID:优先内部 _rid,否则用实际主键字段值(不再硬编码 id)。
|
|
2179
|
+
*/
|
|
2180
|
+
_rowPkId(row, pkCols) {
|
|
2181
|
+
if (row && row._rid !== undefined) return row._rid;
|
|
2182
|
+
if (pkCols.length > 0) {
|
|
2183
|
+
for (const c of pkCols) {
|
|
2184
|
+
if (row[c] !== undefined && row[c] !== null) return row[c];
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return row ? row.id : undefined;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2169
2190
|
async _readTable(table) {
|
|
2170
2191
|
const schema = await this._getSchema(table);
|
|
2171
2192
|
const rows = (await this.engine.find(table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
@@ -2366,6 +2387,19 @@ class SQLExecutor {
|
|
|
2366
2387
|
return out;
|
|
2367
2388
|
}
|
|
2368
2389
|
|
|
2390
|
+
/**
|
|
2391
|
+
* 生成对端表的前缀 null 行:仅含 `prefix.col` 键(值为 null),
|
|
2392
|
+
* 用于 JOIN 未匹配行补齐限定列,避免回退到未前缀副本拿错值。
|
|
2393
|
+
*/
|
|
2394
|
+
_nullPrefixedRow(schema, prefix) {
|
|
2395
|
+
const out = {};
|
|
2396
|
+
for (const k of Object.keys(schema)) {
|
|
2397
|
+
if (k === '_softDelete') continue;
|
|
2398
|
+
out[prefix + '.' + k] = null;
|
|
2399
|
+
}
|
|
2400
|
+
return out;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2369
2403
|
_aggValue(rows, fn, column) {
|
|
2370
2404
|
if (fn === 'COUNT') return rows.length;
|
|
2371
2405
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
@@ -2445,6 +2479,7 @@ class SQLExecutor {
|
|
|
2445
2479
|
|
|
2446
2480
|
let schema = null;
|
|
2447
2481
|
let all;
|
|
2482
|
+
let rowsAll;
|
|
2448
2483
|
if (!statement.from) {
|
|
2449
2484
|
// 无 FROM:虚拟行(SELECT 1, 'a')
|
|
2450
2485
|
all = [{ _virtual: true }];
|
|
@@ -2469,7 +2504,6 @@ class SQLExecutor {
|
|
|
2469
2504
|
const rows = filtered.map(r => outCols.map(c => (c in r ? r[c] : null)));
|
|
2470
2505
|
return { ok: true, type: 'select', table: firstItem.table, columns: outCols, rows, raw: filtered };
|
|
2471
2506
|
}
|
|
2472
|
-
let rowsAll;
|
|
2473
2507
|
if (firstItem.subquery) {
|
|
2474
2508
|
const res = await this.executeSelect(firstItem.subquery);
|
|
2475
2509
|
rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
|
|
@@ -2495,6 +2529,10 @@ class SQLExecutor {
|
|
|
2495
2529
|
}
|
|
2496
2530
|
const matched = [];
|
|
2497
2531
|
const unmatchedRight = new Set(rightRows.map((r, i) => i));
|
|
2532
|
+
// 未匹配行补对端表的前缀 null 列:限定列名(如 a.id / b.id)按前缀解析,
|
|
2533
|
+
// 避免回退到未前缀副本拿到错误值。
|
|
2534
|
+
const rightNulls = (rightRes.schema) ? this._nullPrefixedRow(rightRes.schema, rightPrefix) : null;
|
|
2535
|
+
const leftNulls = (rowsAll && rowsAll.schema) ? this._nullPrefixedRow(rowsAll.schema, firstPrefix) : null;
|
|
2498
2536
|
rows.forEach(l => {
|
|
2499
2537
|
let m = null;
|
|
2500
2538
|
for (let ri = 0; ri < rightRows.length; ri++) {
|
|
@@ -2507,11 +2545,11 @@ class SQLExecutor {
|
|
|
2507
2545
|
matched.push({ ...l, ...rightRows[m] });
|
|
2508
2546
|
unmatchedRight.delete(m);
|
|
2509
2547
|
} else if (j.type === 'left') {
|
|
2510
|
-
matched.push({ ...l });
|
|
2548
|
+
matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
|
|
2511
2549
|
}
|
|
2512
2550
|
});
|
|
2513
2551
|
if (j.type === 'right') {
|
|
2514
|
-
for (const ri of unmatchedRight) matched.push({ ...rightRows[ri] });
|
|
2552
|
+
for (const ri of unmatchedRight) matched.push(leftNulls ? { ...leftNulls, ...rightRows[ri] } : { ...rightRows[ri] });
|
|
2515
2553
|
}
|
|
2516
2554
|
rows = matched;
|
|
2517
2555
|
}
|
package/lib/table.js
CHANGED
|
@@ -242,6 +242,23 @@ class Table {
|
|
|
242
242
|
const schema = this._schema;
|
|
243
243
|
const checkConstraints = this._checkConstraints;
|
|
244
244
|
const foreignKeys = this._foreignKeys;
|
|
245
|
+
// 批量预分配自增 ID 范围:先扫描显式提供的最大值,一次性推进计数器,
|
|
246
|
+
// 再在循环内用本地序号递增,避免批量内重复读取/写入同一计数器字段。
|
|
247
|
+
const baseAutoInc = this._autoIncrement;
|
|
248
|
+
if (autoIncField) {
|
|
249
|
+
let maxExplicit = 0;
|
|
250
|
+
for (const it of items) {
|
|
251
|
+
const v = it[autoIncField];
|
|
252
|
+
if (v !== undefined && v !== null) {
|
|
253
|
+
const n = Number(v);
|
|
254
|
+
if (!isNaN(n) && n > maxExplicit) maxExplicit = n;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (maxExplicit > baseAutoInc) {
|
|
258
|
+
this._autoIncrement = maxExplicit;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let autoIncSeq = 0;
|
|
245
262
|
for (let i = 0; i < N; i++) {
|
|
246
263
|
let data = items[i];
|
|
247
264
|
for (const [f, dv] of Object.entries(defaults)) {
|
|
@@ -279,8 +296,9 @@ class Table {
|
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
298
|
if (autoIncField && data[autoIncField] === undefined) {
|
|
282
|
-
|
|
283
|
-
data[autoIncField] =
|
|
299
|
+
autoIncSeq++;
|
|
300
|
+
data[autoIncField] = baseAutoInc + autoIncSeq;
|
|
301
|
+
this._autoIncrement = data[autoIncField];
|
|
284
302
|
} else if (autoIncField && data[autoIncField] > this._autoIncrement) {
|
|
285
303
|
this._autoIncrement = data[autoIncField];
|
|
286
304
|
}
|