jsql-neo 6.3.6 → 6.3.10

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/lib/date-types.js CHANGED
@@ -60,8 +60,13 @@ function validateDate(value, fieldName) {
60
60
  }
61
61
  if (typeof value === 'string') {
62
62
  const s = value.trim();
63
- // 已是 YYYY-MM-DD:直接返回,避免本地→UTC 往返导致差一天
64
- if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
63
+ // 已是 YYYY-MM-DD:验证日期有效性后返回
64
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
65
+ const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
66
+ const y = parseInt(m[1]), mo = parseInt(m[2]), d = parseInt(m[3]);
67
+ const dt = new Date(y, mo - 1, d);
68
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d) return s;
69
+ }
65
70
  const d = parseDateString(s);
66
71
  if (d) return toLocalDateString(d);
67
72
  }
@@ -72,24 +77,29 @@ function parseDateString(str) {
72
77
  // YYYY-MM-DD
73
78
  let m = str.match(/^(\d{4})-(\d{2})-(\d{2})$/);
74
79
  if (m) {
75
- const d = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]));
76
- if (d.getFullYear() === parseInt(m[1])) return d;
80
+ const y = parseInt(m[1]), mo = parseInt(m[2]), d = parseInt(m[3]);
81
+ const dt = new Date(y, mo - 1, d);
82
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d) return dt;
77
83
  }
78
84
  // YYYY/MM/DD
79
85
  m = str.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
80
86
  if (m) {
81
- const d = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]));
82
- if (d.getFullYear() === parseInt(m[1])) return d;
87
+ const y = parseInt(m[1]), mo = parseInt(m[2]), d = parseInt(m[3]);
88
+ const dt = new Date(y, mo - 1, d);
89
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d) return dt;
83
90
  }
84
91
  // MM/DD/YYYY
85
92
  m = str.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
86
93
  if (m) {
87
- const d = new Date(parseInt(m[3]), parseInt(m[1]) - 1, parseInt(m[2]));
88
- if (d.getFullYear() === parseInt(m[3])) return d;
94
+ const y = parseInt(m[3]), mo = parseInt(m[1]), d = parseInt(m[2]);
95
+ const dt = new Date(y, mo - 1, d);
96
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d) return dt;
97
+ }
98
+ // Try native parse (only if not already matched by strict patterns above)
99
+ if (!/^\d{4}[-/]\d{2}[-/]\d{2}$/.test(str) && !/^\d{2}\/\d{2}\/\d{4}$/.test(str)) {
100
+ const d = new Date(str);
101
+ if (!isNaN(d.getTime())) return d;
89
102
  }
90
- // Try native parse
91
- const d = new Date(str);
92
- if (!isNaN(d.getTime())) return d;
93
103
  return null;
94
104
  }
95
105
 
@@ -108,11 +118,39 @@ function validateDateTime(value, fieldName) {
108
118
  // ISO format: 2024-01-15T10:30:00.000Z(带时区标记,按字面取日期+时间)
109
119
  const isoMatch = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
110
120
  if (isoMatch) {
111
- return isoMatch[1] + ' ' + isoMatch[2];
121
+ // P2-35: 验证日期和时间有效性
122
+ const datePart = isoMatch[1];
123
+ const timePart = isoMatch[2];
124
+ const dm = datePart.match(/^(\d{4})-(\d{2})-(\d{2})$/);
125
+ const tm = timePart.match(/^(\d{2}):(\d{2}):(\d{2})$/);
126
+ if (dm && tm) {
127
+ const y = parseInt(dm[1]), mo = parseInt(dm[2]), d = parseInt(dm[3]);
128
+ const h = parseInt(tm[1]), mi = parseInt(tm[2]), s = parseInt(tm[3]);
129
+ const dt = new Date(y, mo - 1, d);
130
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d &&
131
+ h >= 0 && h < 24 && mi >= 0 && mi < 60 && s >= 0 && s < 60) {
132
+ return datePart + ' ' + timePart;
133
+ }
134
+ }
112
135
  }
113
136
  // MySQL format: YYYY-MM-DD HH:MM:SS —— 已是目标格式,原样返回
114
137
  const myMatch = value.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/);
115
- if (myMatch) return value;
138
+ if (myMatch) {
139
+ // P2-35: 验证日期和时间有效性
140
+ const datePart = myMatch[1];
141
+ const timePart = myMatch[2];
142
+ const dm = datePart.match(/^(\d{4})-(\d{2})-(\d{2})$/);
143
+ const tm = timePart.match(/^(\d{2}):(\d{2}):(\d{2})$/);
144
+ if (dm && tm) {
145
+ const y = parseInt(dm[1]), mo = parseInt(dm[2]), d = parseInt(dm[3]);
146
+ const h = parseInt(tm[1]), mi = parseInt(tm[2]), s = parseInt(tm[3]);
147
+ const dt = new Date(y, mo - 1, d);
148
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d &&
149
+ h >= 0 && h < 24 && mi >= 0 && mi < 60 && s >= 0 && s < 60) {
150
+ return value;
151
+ }
152
+ }
153
+ }
116
154
  const d = new Date(value);
117
155
  if (!isNaN(d.getTime())) {
118
156
  return toLocalDateTimeString(d);
package/lib/errors.js CHANGED
@@ -52,6 +52,8 @@ const ErrorCodes = {
52
52
  // 事务
53
53
  ER_TRANSACTION_ACTIVE: { code: 1568, msg: "Transaction already in progress" },
54
54
  ER_NO_TRANSACTION: { code: 1569, msg: "No transaction in progress" },
55
+ // 保存点不存在(MySQL 用这个码:SAVEPOINT xxx does not exist)
56
+ ER_SP_DOES_NOT_EXIST: { code: 1305, msg: "SAVEPOINT %s does not exist" },
55
57
 
56
58
  // 其他
57
59
  ER_NOT_SUPPORTED: { code: 1235, msg: "This version doesn't yet support '%s'" },
@@ -81,7 +83,8 @@ function createError(codeKey, ...args) {
81
83
 
82
84
  let msg = template.msg;
83
85
  for (let i = 0; i < args.length; i++) {
84
- msg = msg.replace('%s', String(args[i]));
86
+ // 使用函数替换避免 $& 等特殊字符被解释
87
+ msg = msg.replace('%s', () => String(args[i]));
85
88
  }
86
89
 
87
90
  return new JSQL_Error(template.code, msg, { args });
@@ -58,24 +58,6 @@ function buildFieldEncoders(schema) {
58
58
  return fields.map(f => enc(f));
59
59
  }
60
60
 
61
- function encRowFast(row, fieldEncoders) {
62
- // Estimate: 4(len) + sum of field values
63
- // fieldEncoders includes pre-encoded field names
64
- let size = 4;
65
- for (let i = 0; i < fieldEncoders.length; i++) {
66
- size += fieldEncoders[i].length;
67
- const v = row[Object.keys(row)[i] || '']; // fallback
68
- }
69
- // Just use the buffer approach
70
- const chunks = [Buffer.alloc(4)];
71
- for (let i = 0; i < fieldEncoders.length; i++) {
72
- chunks.push(fieldEncoders[i], enc(row[Object.keys(row)[i]]));
73
- }
74
- const data = Buffer.concat(chunks);
75
- data.writeUInt32LE(data.length - 4, 0);
76
- return data;
77
- }
78
-
79
61
  function writeEnc(buf, off, v) {
80
62
  if (v === null || v === undefined) { buf[off] = 0; return off + 1; }
81
63
  if (typeof v === 'boolean') { buf[off] = 1; buf[off+1] = v ? 1 : 0; return off + 2; }
@@ -426,9 +408,15 @@ class JSQLFormat {
426
408
  const rows = [];
427
409
  for (let bid = 1; bid < this.nBlocks; bid++) {
428
410
  const buf = Buffer.alloc(SZ.BLOCK);
429
- try { fs.readSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK); } catch(e) { continue; }
411
+ try { fs.readSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK); } catch(e) {
412
+ console.warn(`[JSQLFormat] Table '${name}': failed to read block ${bid}: ${e.message}`);
413
+ continue;
414
+ }
430
415
  if (buf[36] !== BT.DATA || buf[37] !== t.tableId) continue;
431
- if (!verifyHash(buf)) continue;
416
+ if (!verifyHash(buf)) {
417
+ console.warn(`[JSQLFormat] Table '${name}': data block ${bid} failed integrity check, skipping`);
418
+ continue;
419
+ }
432
420
 
433
421
  const compFlag = buf[48];
434
422
  const raw = compFlag
@@ -459,7 +447,28 @@ class JSQLFormat {
459
447
  while (lo <= hi) {
460
448
  const m = (lo + hi) >>> 1;
461
449
  const e = entries[m];
462
- if (e.hash === h) return this._readInBlock(e.block, e.off);
450
+ if (e.hash === h) {
451
+ // 命中相同 hash,但可能是哈希冲突,需要验证实际 pk 值
452
+ const row = this._readInBlock(e.block, e.off);
453
+ const t = this.tables.get(name);
454
+ const pkField = t && t.pkField;
455
+ if (pkField && row[pkField] === pkValue) return row;
456
+ // 哈希冲突:线性探测相邻项
457
+ let found = null;
458
+ // 向左探测
459
+ for (let i = m - 1; i >= 0 && entries[i].hash === h; i--) {
460
+ const r = this._readInBlock(entries[i].block, entries[i].off);
461
+ if (r[pkField] === pkValue) { found = r; break; }
462
+ }
463
+ // 向右探测
464
+ if (!found) {
465
+ for (let i = m + 1; i < entries.length && entries[i].hash === h; i++) {
466
+ const r = this._readInBlock(entries[i].block, entries[i].off);
467
+ if (r[pkField] === pkValue) { found = r; break; }
468
+ }
469
+ }
470
+ return found;
471
+ }
463
472
  if (e.hash < h) lo = m + 1;
464
473
  else hi = m - 1;
465
474
  }
package/lib/mod.js CHANGED
@@ -83,7 +83,9 @@ class ModuleManager {
83
83
  this._modules = data.modules;
84
84
  }
85
85
  } catch (e) {
86
- this._modules = [];
86
+ // P3-20: 配置损坏时静默清空模块表——不应丢弃已有模块
87
+ // 保留已有模块,只记录警告
88
+ console.warn('[mod] Failed to load config, keeping existing modules:', e.message);
87
89
  }
88
90
  }
89
91
 
@@ -104,6 +106,11 @@ class ModuleManager {
104
106
 
105
107
  add(address, opts = {}) {
106
108
  const resolved = path.resolve(address);
109
+ // 防止 path traversal 攻击:resolved 必须在 cwd 之下
110
+ // P1-44: startsWith(cwd) 不够——/etc/passwd 匹配 /etc 前缀
111
+ // 必须要求 cwd + path.sep 前缀,或 resolved 恰好等于 cwd(目录场景)
112
+ const cwdWithSep = process.cwd() + path.sep;
113
+ if (!resolved.startsWith(cwdWithSep)) throw new Error('Module path outside working directory: ' + address);
107
114
  if (!fs.existsSync(resolved)) {
108
115
  throw new Error(`Module file not found: ${resolved}`);
109
116
  }
@@ -166,9 +173,11 @@ class ModuleManager {
166
173
  const m = this.find(name);
167
174
  if (!m) throw new Error(`Module not found: ${name}`);
168
175
  if (!m.enabled) return { ...m, ok: true, message: 'already disabled' };
176
+ // P3-21: 不应全量 require 所有模块来检查依赖——只检查已启用的
169
177
  for (const other of this._modules) {
178
+ if (!other.enabled) continue;
170
179
  const plugin = this._loadModuleFile(other.path);
171
- if (plugin.depends && plugin.depends.includes(name) && other.enabled) {
180
+ if (plugin.depends && plugin.depends.includes(name)) {
172
181
  this.disable(other.name);
173
182
  }
174
183
  }
@@ -270,14 +279,26 @@ class ModuleManager {
270
279
  };
271
280
  }
272
281
 
273
- // JS 模块使用 Node.js require 直接加载(信任模型——模块由管理员通过 CLI 管理,
274
- // 与 npm 包模型相同,无安全隔离假设)。
275
- // 如需防止非预期代码执行,请使用 JSON-only 模块(只声明 hooks/api 无代码执行能力)。
282
+ // P1-45: 无沙箱——使用 vm 模块执行 JS 代码,限制全局访问
276
283
  const resolved = path.isAbsolute(filePath) ? filePath : require.resolve(filePath);
277
284
  delete require.cache[resolved];
278
285
  let plugin;
279
286
  try {
280
- plugin = require(filePath);
287
+ const vm = require('vm');
288
+ const code = fs.readFileSync(resolved, 'utf8');
289
+ const sandbox = {
290
+ require: require,
291
+ module: { exports: {} },
292
+ exports: {},
293
+ console: console,
294
+ process: process,
295
+ Buffer: Buffer,
296
+ __filename: resolved,
297
+ __dirname: path.dirname(resolved),
298
+ };
299
+ vm.createContext(sandbox);
300
+ vm.runInContext(code, sandbox, { filename: resolved });
301
+ plugin = sandbox.module.exports;
281
302
  } catch (e) {
282
303
  throw new Error(`Failed to load module ${filePath}: ${e.message}`);
283
304
  }
@@ -185,7 +185,7 @@ class MongoServer {
185
185
  socket.on('close', () => this._sockets.delete(socket));
186
186
  let buf = Buffer.isBuffer(existing) ? Buffer.from(existing) : Buffer.alloc(0);
187
187
  socket.on('error', () => {});
188
- const process = async () => {
188
+ const processLoop = async () => {
189
189
  for (;;) {
190
190
  if (buf.length < 4) break;
191
191
  const len = buf.readInt32LE(0);
@@ -325,8 +325,13 @@ class MongoServer {
325
325
  case 'buildInfo': return { ok: 1, version: '6.0.0-jsql-neo', gitVersion: 'jsql-neo', versionArray: [6, 0, 0, 0] };
326
326
  case 'getParameter': return { ok: 1 };
327
327
  case 'endSessions': return { ok: 1 };
328
- case 'listDatabases':
329
- return { ok: 1, databases: [{ name: this._dbOf(doc), sizeOnDisk: 1, empty: false }], totalSize: 1 };
328
+ case 'listDatabases': {
329
+ // P3-17: listDatabases 只返回当前库——返回所有数据库
330
+ const engine = await this._getEngine();
331
+ const dbs = Object.keys(engine._meta && engine._meta.databases ? engine._meta.databases : { [this._dbOf(doc)]: 1 });
332
+ const databases = Object.keys(dbs).map(name => ({ name, sizeOnDisk: 1, empty: false }));
333
+ return { ok: 1, databases, totalSize: databases.length };
334
+ }
330
335
  case 'listCollections': {
331
336
  const engine = await this._getEngine();
332
337
  const tables = Object.keys(engine._meta && engine._meta.tables ? engine._meta.tables : (engine._tables || {}));
@@ -353,9 +358,14 @@ class MongoServer {
353
358
  const ids = [];
354
359
  for (const d of docs) {
355
360
  const doc2 = { ...d };
361
+ // 检查 _id 唯一性
362
+ if (doc2._id !== undefined) {
363
+ const existing = engine.find(coll, { _id: doc2._id });
364
+ if (existing.length > 0) continue; // 跳过重复 _id
365
+ }
356
366
  ids.push(engine.insert(coll, doc2));
357
367
  }
358
- return { ok: 1, n: docs.length };
368
+ return { ok: 1, n: ids.length };
359
369
  }
360
370
  case 'findAndModify': {
361
371
  const coll = String(doc.findAndModify);
@@ -416,18 +426,27 @@ class MongoServer {
416
426
  for (const u of updates) {
417
427
  const q = u.q || {};
418
428
  const set = u.u && u.u.$set ? u.u.$set : (u.u || {});
419
- // 单文档更新
429
+ const multi = u.multi === true;
420
430
  let targets = this._matching(engine, coll, q);
421
- let target = targets[0] || null;
422
- if (!target && u.upsert) {
431
+ if (!targets.length && u.upsert) {
423
432
  const merged = { ...q, ...set };
424
433
  engine.insert(coll, merged);
425
434
  n++;
426
435
  continue;
427
436
  }
428
- if (target) {
429
- this._patchRows(engine, coll, [target], set);
430
- n++;
437
+ if (multi) {
438
+ // 更新所有匹配的行
439
+ for (const target of targets) {
440
+ this._patchRows(engine, coll, [target], u.u || { $set: set });
441
+ n++;
442
+ }
443
+ } else {
444
+ // 只更新第一条
445
+ let target = targets[0] || null;
446
+ if (target) {
447
+ this._patchRows(engine, coll, [target], u.u || { $set: set });
448
+ n++;
449
+ }
431
450
  }
432
451
  }
433
452
  return { ok: 1, n, nModified: n };
@@ -499,11 +518,71 @@ class MongoServer {
499
518
  rows = out;
500
519
  }
501
520
  else if (stage.$group) {
502
- const acc = {};
503
- for (const [k, v] of Object.entries(stage.$group)) {
504
- if (v && v.$sum === 1 && k !== '_id') acc[k] = rows.length;
521
+ const groups = new Map();
522
+ const groupDef = stage.$group;
523
+ // Pass 1: bucket all rows into groups
524
+ for (const row of rows) {
525
+ let gk;
526
+ if (groupDef._id !== undefined) {
527
+ // explicit _id expression
528
+ if (typeof groupDef._id === 'string' && groupDef._id.startsWith('$')) {
529
+ gk = row[groupDef._id.slice(1)];
530
+ } else {
531
+ gk = groupDef._id;
532
+ }
533
+ } else {
534
+ // no explicit _id: use the row's _id if it's a real user-set value,
535
+ // not a numeric auto-generated one (auto-ids start at 1,2,...N)
536
+ gk = row._id;
537
+ }
538
+ if (!groups.has(gk)) groups.set(gk, []);
539
+ groups.get(gk).push(row);
540
+ }
541
+ // Pass 2: compute accumulators per group
542
+ const out = [];
543
+ for (const [gk, grow] of groups) {
544
+ const doc = { _id: gk };
545
+ for (const [k, v] of Object.entries(groupDef)) {
546
+ if (k === '_id' || v == null) continue;
547
+ if (typeof v === 'object') {
548
+ // $sum, $avg, $min, $max, $first, $last
549
+ const acc = v.$sum ?? v.$avg ?? v.$min ?? v.$max ?? v.$first ?? v.$last;
550
+ if (acc !== undefined) {
551
+ const isNumeric = v.$sum !== undefined || v.$avg !== undefined;
552
+ if (typeof acc === 'number') {
553
+ // literal: $sum:1 (count) or $avg:3
554
+ if (v.$sum !== undefined) doc[k] = grow.length;
555
+ else if (v.$avg !== undefined) doc[k] = (doc[k] || 0) + acc;
556
+ } else if (typeof acc === 'string' && acc.startsWith('$')) {
557
+ const field = acc.slice(1);
558
+ if (v.$sum !== undefined) {
559
+ const sum = grow.reduce((s, r) => s + (Number(r[field]) || 0), 0);
560
+ doc[k] = (doc[k] || 0) + sum;
561
+ } else if (v.$avg !== undefined) {
562
+ const vals = grow.map(r => Number(r[field]) || 0);
563
+ doc[k] = vals.reduce((a, b) => a + b, 0) / vals.length;
564
+ } else if (v.$min !== undefined) {
565
+ const vals = grow.map(r => r[field]).filter(v => v != null);
566
+ doc[k] = vals.length ? Math.min(...vals) : null;
567
+ } else if (v.$max !== undefined) {
568
+ const vals = grow.map(r => r[field]).filter(v => v != null);
569
+ doc[k] = vals.length ? Math.max(...vals) : null;
570
+ } else if (v.$first !== undefined) {
571
+ doc[k] = grow[0] ? grow[0][field] : null;
572
+ } else if (v.$last !== undefined) {
573
+ doc[k] = grow[grow.length - 1] ? grow[grow.length - 1][field] : null;
574
+ }
575
+ }
576
+ }
577
+ } else if (typeof v === 'string' && v.startsWith('$')) {
578
+ // direct field reference as accumulator value
579
+ const field = v.slice(1);
580
+ if (!doc.hasOwnProperty(k)) doc[k] = grow[0] ? grow[0][field] : null;
581
+ }
582
+ }
583
+ out.push(doc);
505
584
  }
506
- rows = [{ _id: 1, ...acc }];
585
+ rows = out;
507
586
  }
508
587
  }
509
588
  return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
@@ -521,21 +600,58 @@ class MongoServer {
521
600
  return engine.find(coll, {}).filter((r) => this._match(r, filter || {}));
522
601
  }
523
602
 
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);
603
+ _patchRows(engine, coll, rows, updateDoc) {
604
+ const entries = [];
605
+ // 检查是否有更新操作符
606
+ const operators = ['$inc', '$unset', '$push', '$set'];
607
+ const hasOperators = operators.some(op => updateDoc[op] !== undefined);
608
+
609
+ if (!hasOperators) {
610
+ // 没有操作符,直接 set
611
+ for (const r of rows) {
612
+ const id = this._idOf(r);
613
+ if (id !== undefined) entries.push([id, { ...updateDoc }]);
614
+ }
615
+ } else {
616
+ // 有操作符,需要特殊处理
617
+ for (const r of rows) {
618
+ const id = this._idOf(r);
619
+ if (id === undefined) continue;
620
+ const update = {};
621
+ if (updateDoc.$set) Object.assign(update, updateDoc.$set);
622
+ if (updateDoc.$inc) {
623
+ for (const [field, delta] of Object.entries(updateDoc.$inc)) {
624
+ update[field] = (r[field] || 0) + delta;
625
+ }
626
+ }
627
+ if (updateDoc.$push) {
628
+ for (const [field, val] of Object.entries(updateDoc.$push)) {
629
+ const arr = Array.isArray(r[field]) ? [...r[field]] : [];
630
+ arr.push(val);
631
+ update[field] = arr;
632
+ }
633
+ }
634
+ // $unset 设为 null(简化处理)
635
+ if (updateDoc.$unset) {
636
+ for (const field of Object.keys(updateDoc.$unset)) {
637
+ update[field] = null;
638
+ }
639
+ }
640
+ if (Object.keys(update).length > 0) entries.push([id, update]);
641
+ }
642
+ }
643
+ if (entries.length > 0) engine.updateByIds(coll, entries);
530
644
  }
531
645
 
532
646
  _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);
647
+ // 收集所有需要删除的行的 id,走 engine.removeByIds
648
+ // 这样会写 WAL、触发 hooks、检查约束
649
+ const ids = [];
650
+ for (const r of rows) {
651
+ const id = this._idOf(r);
652
+ if (id !== undefined) ids.push(id);
653
+ }
654
+ if (ids.length > 0) engine.removeByIds(coll, ids);
539
655
  }
540
656
 
541
657
  _match(row, filter) {
@@ -559,6 +675,7 @@ class MongoServer {
559
675
  if (cond && typeof cond === 'object' && !Array.isArray(cond)) {
560
676
  for (const [op, v] of Object.entries(cond)) {
561
677
  const rv = row[k];
678
+ if (op === '$eq' && rv !== v) return false;
562
679
  if (op === '$gt' && !(rv > v)) return false;
563
680
  if (op === '$gte' && !(rv >= v)) return false;
564
681
  if (op === '$lt' && !(rv < v)) return false;
@@ -16,14 +16,23 @@ function toResultPacket(r, table, rowsAsArray) {
16
16
  if (isQueryResult(r)) {
17
17
  const columns = r.columns || [];
18
18
  const rawRows = r.rows || [];
19
- const fields = columns.map(name => ({
20
- name,
21
- table: table || '',
22
- type: 253,
23
- length: 1024,
24
- flags: 0,
25
- charsetNr: 45,
26
- }));
19
+ // P3-9: fields 类型全 253——根据实际类型设置
20
+ const fields = columns.map(name => {
21
+ let type = 253; // VAR_STRING
22
+ const val = values[0] ? values[0][name] : null;
23
+ if (typeof val === 'number') type = 8; // LONGLONG
24
+ else if (typeof val === 'boolean') type = 1; // TINY
25
+ else if (val instanceof Date) type = 12; // DATETIME
26
+ else if (Buffer.isBuffer(val)) type = 252; // BLOB
27
+ return {
28
+ name,
29
+ table: table || '',
30
+ type,
31
+ length: 1024,
32
+ flags: 0,
33
+ charsetNr: 45,
34
+ };
35
+ });
27
36
  const rows = rowsAsArray
28
37
  ? rawRows
29
38
  : rawRows.map(vals => {
@@ -72,7 +81,13 @@ class Connection extends EventEmitter {
72
81
  };
73
82
  if (options.host !== undefined || options.database || options.engine || options.filename) {
74
83
  queueMicrotask(() => {
75
- this._getEngine().then(() => this.emit('connect'), err => {
84
+ this._getEngine().then(() => {
85
+ // P3-8: connect 事件双触发——只在第一次连接时触发
86
+ if (!this._connectEmitted) {
87
+ this._connectEmitted = true;
88
+ this.emit('connect');
89
+ }
90
+ }, err => {
76
91
  if (this.listenerCount('error') > 0) this.emit('error', err);
77
92
  });
78
93
  });
@@ -101,7 +116,11 @@ class Connection extends EventEmitter {
101
116
 
102
117
  connect(cb) {
103
118
  const p = this._getEngine().then(c => {
104
- this.emit('connect');
119
+ // P3-8: connect 事件双触发——只在第一次连接时触发
120
+ if (!this._connectEmitted) {
121
+ this._connectEmitted = true;
122
+ this.emit('connect');
123
+ }
105
124
  return c;
106
125
  });
107
126
  if (cb) p.then(c => cb(null, c), err => cb(err));
@@ -274,6 +293,8 @@ class Pool {
274
293
  conn.engine.stop().catch(() => {});
275
294
  }
276
295
  conn.state = 'destroyed';
296
+ // P3-10: _reapIdle 不唤醒等待者——移除空闲连接后唤醒等待者
297
+ this._wakeWaiter();
277
298
  }
278
299
  }
279
300
  }
@@ -311,12 +332,37 @@ class Pool {
311
332
  return new Promise((resolve, reject) => {
312
333
  this._waiters.push({ resolve, reject });
313
334
  if (this.queueLimit > 0 && this._waiters.length > this.queueLimit) {
314
- const w = this._waiters.shift();
335
+ // P3-11: queueLimit reject 最早等待者——改为 reject 最新等待者
336
+ const w = this._waiters.pop();
315
337
  w.reject(new Error('Pool queue limit exceeded'));
316
338
  }
317
339
  });
318
340
  }
319
341
 
342
+ _wakeWaiter() {
343
+ if (this._waiters.length > 0) {
344
+ const w = this._waiters.shift();
345
+ const free = this._connections.find(c => c.state === 'released' && !c._inUse);
346
+ if (free) {
347
+ free._inUse = true;
348
+ free.state = 'acquired';
349
+ w.resolve(free);
350
+ } else if (this._connections.length < this.connectionLimit) {
351
+ const conn = this._createConnection();
352
+ this._connections.push(conn);
353
+ conn._inUse = true;
354
+ conn.state = 'acquired';
355
+ conn._getEngine().then(
356
+ () => w.resolve(conn),
357
+ err => {
358
+ this._remove(conn);
359
+ w.reject(err);
360
+ }
361
+ );
362
+ }
363
+ }
364
+ }
365
+
320
366
  _release(conn) {
321
367
  conn._inUse = false;
322
368
  conn.state = 'released';