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/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.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
- 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;
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({ ...fr, ...this._nullRow(join, true) });
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({ ...fr, ...this._nullRow(join, true) });
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
  // ============================================================
@@ -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') throw new Error(`Expected table name after '.', got '${t2.value}'`);
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 = row._rid !== undefined ? row._rid : row.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 = row._rid !== undefined ? row._rid : row.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
- this._autoIncrement++;
283
- data[autoIncField] = this._autoIncrement;
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
  }