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/README.md +78 -8239
- package/index.js +1 -1
- package/lib/cache.js +33 -27
- package/lib/client.js +11 -2
- package/lib/database.js +370 -56
- package/lib/date-types.js +51 -13
- package/lib/errors.js +4 -1
- package/lib/jsql_format.js +30 -21
- package/lib/mod.js +27 -6
- package/lib/mongo_server.js +143 -26
- package/lib/mysql_compat.js +57 -11
- package/lib/mysql_server.js +104 -4
- package/lib/nedb_compat.js +9 -6
- package/lib/pg_server.js +4 -11
- package/lib/plugin.js +20 -2
- package/lib/prepared.js +10 -0
- package/lib/query.js +9 -2
- package/lib/redis_server.js +88 -16
- package/lib/sql.js +856 -81
- package/lib/sqlite_compat.js +2 -1
- package/lib/sqlite_worker.js +8 -2
- package/lib/table.js +68 -26
- package/lib/tui.js +1 -1
- package/lib/web_ui.js +24 -8
- package/native/jsql-neo-native.node +0 -0
- package/package.json +10 -3
- package/test/alter-table.test.js +289 -0
- package/test/identifier-keywords.test.js +140 -0
- package/test/index-ddl.test.js +203 -0
- package/test/mongo-group.test.js +105 -0
- package/test/null-semantics.test.js +126 -0
- package/test/recursive-cte.test.js +123 -0
- package/test/savepoint.test.js +388 -0
- package/test/sql-parser.test.js +30 -5
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
|
|
64
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(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
|
|
76
|
-
|
|
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
|
|
82
|
-
|
|
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
|
|
88
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
86
|
+
// 使用函数替换避免 $& 等特殊字符被解释
|
|
87
|
+
msg = msg.replace('%s', () => String(args[i]));
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
return new JSQL_Error(template.code, msg, { args });
|
package/lib/jsql_format.js
CHANGED
|
@@ -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) {
|
|
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))
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
}
|
package/lib/mongo_server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
503
|
-
|
|
504
|
-
|
|
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 =
|
|
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,
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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;
|
package/lib/mysql_compat.js
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
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';
|