jsql-neo 6.5.13 → 6.5.15
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 +1 -1
- package/index.js +1 -1
- package/lib/btree.js +52 -116
- package/lib/database.js +16 -21
- package/lib/errors.js +4 -5
- package/lib/migrate.js +6 -2
- package/lib/mongo_server.js +20 -3
- package/lib/mysql_server.js +3 -2
- package/lib/nedb_compat.js +15 -2
- package/lib/pg_server.js +4 -1
- package/lib/plugin.js +14 -3
- package/lib/sql.js +54 -8
- package/lib/star-nag.js +20 -9
- package/lib/table.js +41 -16
- package/lib/web_ui.js +8 -5
- package/lib/xql/src/compiler.js +18 -8
- package/package.json +2 -2
- package/test/v6.5.14-regression.test.js +295 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.5.
|
|
6
|
+
> **v6.5.15** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
package/index.js
CHANGED
package/lib/btree.js
CHANGED
|
@@ -24,8 +24,6 @@ class BTree {
|
|
|
24
24
|
this._unique = unique;
|
|
25
25
|
this._root = new BTreeNode();
|
|
26
26
|
this._size = 0;
|
|
27
|
-
this._minKeys = Math.floor(order / 2) - 1;
|
|
28
|
-
this._minDegree = Math.ceil(order / 2);
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
get size() { return this._size; }
|
|
@@ -273,122 +271,81 @@ class BTree {
|
|
|
273
271
|
|
|
274
272
|
remove(key, rowIndex) {
|
|
275
273
|
if (!this._root) return false;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
return removed;
|
|
274
|
+
const res = this._removeFromNode(this._root, key, rowIndex);
|
|
275
|
+
if (!res.found) return false;
|
|
276
|
+
if (!res.remaining) this._size--;
|
|
277
|
+
// 只做惰性删除 + 分隔键归一:不再在下降途中借用/合并。
|
|
278
|
+
// 旧实现先对路由到的子节点做 borrow/merge 再检查键是否存在,
|
|
279
|
+
// 删除一个不存在的键(或 rowIndex 不匹配)也会重构整棵树,
|
|
280
|
+
// 合并时丢掉的分隔键副本可能正好是仍然存活的数据键 —— 条目静默丢失。
|
|
281
|
+
this._normalize();
|
|
282
|
+
return !res.remaining;
|
|
287
283
|
}
|
|
288
284
|
|
|
289
285
|
_removeFromNode(node, key, rowIndex) {
|
|
290
286
|
if (node.leaf) {
|
|
291
287
|
const i = this._lowerBound(node.keys, key);
|
|
292
|
-
if (i >= node.keys.length || node.keys[i] !== key) return false;
|
|
293
|
-
|
|
288
|
+
if (i >= node.keys.length || node.keys[i] !== key) return { found: false };
|
|
289
|
+
const pos = node.values[i].indexOf(rowIndex);
|
|
290
|
+
if (pos === -1) return { found: false };
|
|
294
291
|
const vals = node.values[i].filter(v => v !== rowIndex);
|
|
295
292
|
if (vals.length === 0) {
|
|
296
293
|
node.keys.splice(i, 1);
|
|
297
294
|
node.values.splice(i, 1);
|
|
298
|
-
return true;
|
|
295
|
+
return { found: true, remaining: false };
|
|
299
296
|
}
|
|
300
297
|
node.values[i] = vals;
|
|
301
|
-
return
|
|
298
|
+
return { found: true, remaining: true };
|
|
302
299
|
}
|
|
303
300
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// 下降前确保子节点有足够键,避免欠满子节点
|
|
301
|
+
const i = this._route(node, key);
|
|
307
302
|
const child = node.children[i];
|
|
308
|
-
if (child
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
return this._removeFromNode(node.children[i], key, rowIndex);
|
|
303
|
+
if (!child) return { found: false };
|
|
304
|
+
return this._removeFromNode(child, key, rowIndex);
|
|
312
305
|
}
|
|
313
306
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (i > 0) {
|
|
324
|
-
this._mergeChildren(parent, i - 1);
|
|
325
|
-
return i - 1;
|
|
326
|
-
}
|
|
327
|
-
this._mergeChildren(parent, i);
|
|
328
|
-
return i;
|
|
307
|
+
/**
|
|
308
|
+
* 惰性回收:删掉节点里空的叶子/空子树,折叠只剩一个孩子的内部节点,
|
|
309
|
+
* 并按「分隔键 = 左子树最大键」的约定重算所有内部键,最后重建叶子链表。
|
|
310
|
+
* 节点欠满不影响正确性(插入时仍然按满则分裂),因此无需借用/合并。
|
|
311
|
+
*/
|
|
312
|
+
_normalize() {
|
|
313
|
+
const root = this._cleanNode(this._root);
|
|
314
|
+
this._root = root || new BTreeNode();
|
|
315
|
+
this._relinkLeaves();
|
|
329
316
|
}
|
|
330
317
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const v = left.values.pop();
|
|
338
|
-
right.keys.unshift(k);
|
|
339
|
-
right.values.unshift(v);
|
|
340
|
-
// 父节点分隔键应为左叶新的最大值(pop 后的最后一个键)
|
|
341
|
-
parent.keys[i - 1] = left.keys[left.keys.length - 1];
|
|
342
|
-
parent.values[i - 1] = left.values[left.values.length - 1];
|
|
343
|
-
} else {
|
|
344
|
-
right.keys.unshift(parent.keys[i - 1]);
|
|
345
|
-
right.values.unshift(parent.values[i - 1]);
|
|
346
|
-
parent.keys[i - 1] = left.keys.pop();
|
|
347
|
-
parent.values[i - 1] = left.values.pop();
|
|
348
|
-
right.children.unshift(left.children.pop());
|
|
318
|
+
_cleanNode(node) {
|
|
319
|
+
if (node.leaf) return node.keys.length === 0 ? null : node;
|
|
320
|
+
const kids = [];
|
|
321
|
+
for (const c of node.children) {
|
|
322
|
+
const kept = this._cleanNode(c);
|
|
323
|
+
if (kept) kids.push(kept);
|
|
349
324
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
left.keys.push(k);
|
|
360
|
-
left.values.push(v);
|
|
361
|
-
parent.keys[i] = k;
|
|
362
|
-
parent.values[i] = v;
|
|
363
|
-
} else {
|
|
364
|
-
left.keys.push(parent.keys[i]);
|
|
365
|
-
left.values.push(parent.values[i]);
|
|
366
|
-
parent.keys[i] = right.keys.shift();
|
|
367
|
-
parent.values[i] = right.values.shift();
|
|
368
|
-
left.children.push(right.children.shift());
|
|
325
|
+
node.children = kids;
|
|
326
|
+
if (kids.length === 0) return null;
|
|
327
|
+
if (kids.length === 1) return kids[0];
|
|
328
|
+
node.keys = [];
|
|
329
|
+
node.values = [];
|
|
330
|
+
for (let i = 0; i < kids.length - 1; i++) {
|
|
331
|
+
const m = this._getMax(kids[i]);
|
|
332
|
+
node.keys.push(m.key);
|
|
333
|
+
node.values.push(m.value);
|
|
369
334
|
}
|
|
335
|
+
return node;
|
|
370
336
|
}
|
|
371
337
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
* +28:1 +24:4 +0:0 +14:2 -24:4 +25:4 +24:2 +28:1 -26:2
|
|
382
|
-
* 前 8 步都正常,第 9 步把 keys=[24,25] 变成 [24,24]。
|
|
383
|
-
*/
|
|
384
|
-
_fixSeparators(node) {
|
|
385
|
-
if (node.leaf) return;
|
|
386
|
-
for (let i = 0; i < node.keys.length; i++) {
|
|
387
|
-
const m = this._getMax(node.children[i]);
|
|
388
|
-
node.keys[i] = m.key;
|
|
389
|
-
node.values[i] = m.value;
|
|
338
|
+
_relinkLeaves() {
|
|
339
|
+
let head = this._root;
|
|
340
|
+
while (head && !head.leaf) head = head.children[0];
|
|
341
|
+
let prev = null;
|
|
342
|
+
let node = head;
|
|
343
|
+
while (node) {
|
|
344
|
+
if (prev) prev.next = node;
|
|
345
|
+
prev = node;
|
|
346
|
+
node = node.next;
|
|
390
347
|
}
|
|
391
|
-
|
|
348
|
+
if (prev) prev.next = null;
|
|
392
349
|
}
|
|
393
350
|
|
|
394
351
|
_getMax(node) {
|
|
@@ -397,27 +354,6 @@ class BTree {
|
|
|
397
354
|
return { key: node.keys[i], value: node.values[i] };
|
|
398
355
|
}
|
|
399
356
|
|
|
400
|
-
_mergeChildren(parent, index) {
|
|
401
|
-
const left = parent.children[index];
|
|
402
|
-
const right = parent.children[index + 1];
|
|
403
|
-
const midKey = parent.keys[index];
|
|
404
|
-
const midVal = parent.values[index];
|
|
405
|
-
|
|
406
|
-
// 叶子节点:分隔键是叶子的副本(已在 left 或 right 中),不能重复压入
|
|
407
|
-
if (!left.leaf) {
|
|
408
|
-
left.keys.push(midKey);
|
|
409
|
-
left.values.push(midVal);
|
|
410
|
-
}
|
|
411
|
-
for (const __e of right.keys) left.keys.push(__e);
|
|
412
|
-
for (const __e of right.values) left.values.push(__e);
|
|
413
|
-
if (!left.leaf) for (const __e of right.children) left.children.push(__e);
|
|
414
|
-
if (left.leaf) left.next = right.next;
|
|
415
|
-
|
|
416
|
-
parent.keys.splice(index, 1);
|
|
417
|
-
parent.values.splice(index, 1);
|
|
418
|
-
parent.children.splice(index + 1, 1);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
357
|
// ============================================================
|
|
422
358
|
// 遍历
|
|
423
359
|
// ============================================================
|
package/lib/database.js
CHANGED
|
@@ -1344,15 +1344,11 @@ class Database {
|
|
|
1344
1344
|
startedAt: Date.now()
|
|
1345
1345
|
};
|
|
1346
1346
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
for (const [name, table] of Object.entries(this._tables)) {
|
|
1353
|
-
this._transaction.autoIncrements[name] = table._autoIncrement;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1347
|
+
// 无论隔离级别都要保存完整快照:ROLLBACK 必须撤销本事务写入的行。
|
|
1348
|
+
// 此前 READ_COMMITTED 只记 autoIncrement,回滚后行原样残留(静默不撤销)。
|
|
1349
|
+
// 隔离级别只影响「读取时能否看到其他事务的已提交数据」(这里是单连接内嵌库,
|
|
1350
|
+
// 不涉及并发可见性),不应影响回滚能力。
|
|
1351
|
+
this._transaction.snapshot = this._captureTxSnapshot();
|
|
1356
1352
|
|
|
1357
1353
|
this._savepointStack = [];
|
|
1358
1354
|
this._walWrite({ op: 'begin', level });
|
|
@@ -1374,12 +1370,6 @@ class Database {
|
|
|
1374
1370
|
|
|
1375
1371
|
if (this._transaction.snapshot) {
|
|
1376
1372
|
this._restoreTxSnapshot(this._transaction.snapshot);
|
|
1377
|
-
} else {
|
|
1378
|
-
// READ_COMMITTED:没有行快照,只能退回自增值
|
|
1379
|
-
for (const [name, table] of Object.entries(this._tables)) {
|
|
1380
|
-
const v = this._transaction.autoIncrements[name];
|
|
1381
|
-
if (v !== undefined) table._autoIncrement = v;
|
|
1382
|
-
}
|
|
1383
1373
|
}
|
|
1384
1374
|
|
|
1385
1375
|
this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
|
|
@@ -1857,8 +1847,8 @@ class Database {
|
|
|
1857
1847
|
let rows = table._applyFilterOptimized(table._getVisibleRows(), filter || {});
|
|
1858
1848
|
if (limit > 0) rows = rows.slice(offset, offset + limit);
|
|
1859
1849
|
else if (offset > 0) rows = rows.slice(offset);
|
|
1860
|
-
// P1-11:
|
|
1861
|
-
rows = rows.map(r => (
|
|
1850
|
+
// P1-11: 返回深拷贝,防止调用方修改(含嵌套对象/数组)影响内部数据
|
|
1851
|
+
rows = rows.map(r => Table.cloneRow(r));
|
|
1862
1852
|
this._touchTable(tableName);
|
|
1863
1853
|
await this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
|
|
1864
1854
|
return rows;
|
|
@@ -1890,8 +1880,8 @@ class Database {
|
|
|
1890
1880
|
const row = this._resolveId(table, id);
|
|
1891
1881
|
// P1-4: 软删除行对普通读取不可见
|
|
1892
1882
|
if (row && row._deleted) return null;
|
|
1893
|
-
// P1-11:
|
|
1894
|
-
const result = row ?
|
|
1883
|
+
// P1-11: 返回深拷贝,防止调用方修改(含嵌套对象/数组)影响内部数据
|
|
1884
|
+
const result = row ? Table.cloneRow(row) : null;
|
|
1895
1885
|
this._touchTable(tableName);
|
|
1896
1886
|
await this._runHooks('afterFind', [tableName, { id }, result]);
|
|
1897
1887
|
return result;
|
|
@@ -1902,7 +1892,7 @@ class Database {
|
|
|
1902
1892
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1903
1893
|
if (!(await this._runHooks('beforeFind', [tableName, { ids }]))) return null;
|
|
1904
1894
|
// P1-4: 过滤掉软删除行;P1-11: 返回浅拷贝
|
|
1905
|
-
const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => (
|
|
1895
|
+
const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => Table.cloneRow(r));
|
|
1906
1896
|
this._touchTable(tableName);
|
|
1907
1897
|
await this._runHooks('afterFind', [tableName, { ids }, rows]);
|
|
1908
1898
|
return rows;
|
|
@@ -1948,6 +1938,9 @@ class Database {
|
|
|
1948
1938
|
const clash = table._rows.find(r => r !== row && r[field] === nv);
|
|
1949
1939
|
if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
|
|
1950
1940
|
}
|
|
1941
|
+
// 主键/必填列的 NULL 校验:updateById 此前只做类型校验与重复检查,
|
|
1942
|
+
// `UPDATE t SET id = NULL` 会被静默写入(主键行变成 NULL 主键)。
|
|
1943
|
+
table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
|
|
1951
1944
|
Object.assign(row, validated);
|
|
1952
1945
|
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
|
|
1953
1946
|
this._emit('update', { table: tableName, id, data: validated });
|
|
@@ -1966,7 +1959,9 @@ class Database {
|
|
|
1966
1959
|
for (const [id, data] of pairs) {
|
|
1967
1960
|
const row = this._resolveId(table, id);
|
|
1968
1961
|
if (row) {
|
|
1969
|
-
|
|
1962
|
+
const validated = table._validateDataTypes({ ...data }, true);
|
|
1963
|
+
table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
|
|
1964
|
+
Object.assign(row, validated);
|
|
1970
1965
|
count++;
|
|
1971
1966
|
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
|
|
1972
1967
|
}
|
package/lib/errors.js
CHANGED
|
@@ -81,11 +81,10 @@ function createError(codeKey, ...args) {
|
|
|
81
81
|
return new JSQL_Error('UNKNOWN', `Unknown error: ${codeKey}`, { codeKey, args });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
84
|
+
// 单趟替换所有 %s:既避免参数值里本身含有 %s 时被后续替换再次消费造成串位,
|
|
85
|
+
// 也避免实参不足时消息里残留裸 %s。
|
|
86
|
+
let idx = 0;
|
|
87
|
+
const msg = template.msg.replace(/%s/g, () => (idx < args.length ? String(args[idx++]) : ''));
|
|
89
88
|
|
|
90
89
|
return new JSQL_Error(template.code, msg, { args });
|
|
91
90
|
}
|
package/lib/migrate.js
CHANGED
|
@@ -30,8 +30,12 @@ function normalizeSchema(schema) {
|
|
|
30
30
|
if (d.type === 'varchar' || d.type === 'text' || d.type === 'char') d.type = 'string';
|
|
31
31
|
if (d.type === 'double' || d.type === 'real' || d.type === 'decimal' || d.type === 'numeric') d.type = 'float';
|
|
32
32
|
if (d.type === 'bool') d.type = 'boolean';
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// 保留用户显式配置的 length;maxLength 只是别名,仅在 length 未给出时回填。
|
|
34
|
+
// 此前无条件 delete d.length 再仅从 maxLength 恢复,导致显式 length 丢失。
|
|
35
|
+
if (d.maxLength !== undefined) {
|
|
36
|
+
if (d.length === undefined) d.length = d.maxLength;
|
|
37
|
+
delete d.maxLength;
|
|
38
|
+
}
|
|
35
39
|
out[name] = d;
|
|
36
40
|
}
|
|
37
41
|
return out;
|
package/lib/mongo_server.js
CHANGED
|
@@ -461,7 +461,7 @@ class MongoServer {
|
|
|
461
461
|
await engine.insert(coll, merged);
|
|
462
462
|
value = doc.new ? { ...merged } : null;
|
|
463
463
|
}
|
|
464
|
-
return { ok: 1, value, lastErrorObject: { n: found ? 1 : (doc.upsert ? 1 : 0), updatedExisting: !!found && !doc.remove } };
|
|
464
|
+
return { ok: 1, value: value ? this._publicDoc(value) : value, lastErrorObject: { n: found ? 1 : (doc.upsert ? 1 : 0), updatedExisting: !!found && !doc.remove } };
|
|
465
465
|
}
|
|
466
466
|
case 'distinct': {
|
|
467
467
|
const coll = String(doc.distinct);
|
|
@@ -496,7 +496,7 @@ class MongoServer {
|
|
|
496
496
|
if (skip > 0) rows = rows.slice(skip);
|
|
497
497
|
const limit = typeof doc.limit === 'number' ? doc.limit : 0;
|
|
498
498
|
if (limit > 0) rows = rows.slice(0, limit);
|
|
499
|
-
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
|
|
499
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows.map(r => this._publicDoc(r)) } };
|
|
500
500
|
}
|
|
501
501
|
case 'getMore':
|
|
502
502
|
return { ok: 1, cursor: { id: { $long: 0 }, ns: String(doc.collection || ''), nextBatch: [] } };
|
|
@@ -673,7 +673,7 @@ class MongoServer {
|
|
|
673
673
|
rows = out;
|
|
674
674
|
}
|
|
675
675
|
}
|
|
676
|
-
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
|
|
676
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows.map(r => this._publicDoc(r)) } };
|
|
677
677
|
}
|
|
678
678
|
default:
|
|
679
679
|
return { ok: 1, n: 0 };
|
|
@@ -684,6 +684,23 @@ class MongoServer {
|
|
|
684
684
|
return row._id != null ? row._id : row.id;
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
/**
|
|
688
|
+
* 剥离 JSQL 内部字段后再返回给 Mongo 客户端。
|
|
689
|
+
*
|
|
690
|
+
* _deleted(软删除标记)、_rid(内部行号)、_score(全文检索得分)
|
|
691
|
+
* 都只属于引擎内部实现,泄漏到文档里会污染用户数据、影响后续 $set/$unset
|
|
692
|
+
* 以及客户端 schema 推断。
|
|
693
|
+
*/
|
|
694
|
+
_publicDoc(row) {
|
|
695
|
+
if (!row || typeof row !== 'object') return row;
|
|
696
|
+
if (row._deleted === undefined && row._rid === undefined && row._score === undefined) return row;
|
|
697
|
+
const out = { ...row };
|
|
698
|
+
delete out._deleted;
|
|
699
|
+
delete out._rid;
|
|
700
|
+
delete out._score;
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
|
|
687
704
|
async _matching(engine, coll, filter) {
|
|
688
705
|
const rows = await engine.find(coll, {});
|
|
689
706
|
return rows.filter((r) => this._match(r, filter || {}));
|
package/lib/mysql_server.js
CHANGED
|
@@ -578,8 +578,9 @@ class MysqlConnection {
|
|
|
578
578
|
case 0x19: this._handleStmtClose(body); break; // COM_STMT_CLOSE
|
|
579
579
|
case 0x1a: this._send(okPacket()); break; // COM_STMT_RESET
|
|
580
580
|
case 0x0a: { // COM_PROCESS_INFO
|
|
581
|
-
|
|
582
|
-
|
|
581
|
+
// _handlePacket 已把 this.sequence 设为 cmdSeq+1,_send 会在写入后自增。
|
|
582
|
+
// 此前这里额外自增一次,导致首个响应包序列号比请求大 2(跳号),
|
|
583
|
+
// 严格校验序列号的客户端会判定协议错误并断开。
|
|
583
584
|
this._send(encodeLenenc(1));
|
|
584
585
|
this._send(columnDefinition({ name: 'Id', type: MYSQL_TYPE_LONG }));
|
|
585
586
|
this._send(eofPacket());
|
package/lib/nedb_compat.js
CHANGED
|
@@ -77,8 +77,21 @@ function matchOperator(value, op, expected) {
|
|
|
77
77
|
case '$in': return Array.isArray(expected) && expected.some(e => looseEqual(value, e));
|
|
78
78
|
case '$nin': return Array.isArray(expected) && !expected.some(e => looseEqual(value, e));
|
|
79
79
|
case '$exists': return expected ? value !== undefined : value === undefined;
|
|
80
|
-
case '$regex':
|
|
81
|
-
|
|
80
|
+
case '$regex': {
|
|
81
|
+
if (typeof value !== 'string') return false;
|
|
82
|
+
// expected 允许两种写法:/re/flags(RegExp),或 MySQL 风格的字符串模式。
|
|
83
|
+
// 此前直接读 expected.source:字符串没有 source → new RegExp(undefined)
|
|
84
|
+
// 等价于 /(?:)/,静默匹配所有文档。
|
|
85
|
+
let re;
|
|
86
|
+
if (expected instanceof RegExp) {
|
|
87
|
+
re = expected;
|
|
88
|
+
} else if (typeof expected === 'string') {
|
|
89
|
+
try { re = new RegExp(expected); } catch { return false; }
|
|
90
|
+
} else {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return re.test(value);
|
|
94
|
+
}
|
|
82
95
|
case '$size': return Array.isArray(value) && value.length === expected;
|
|
83
96
|
default: return false;
|
|
84
97
|
}
|
package/lib/pg_server.js
CHANGED
|
@@ -792,7 +792,10 @@ class PgServer {
|
|
|
792
792
|
if (user !== this.options.user) return undefined;
|
|
793
793
|
return this.options.password || '';
|
|
794
794
|
}
|
|
795
|
-
|
|
795
|
+
// 未配置任何认证信息:默认拒绝连接(返回 undefined → 认证失败)。
|
|
796
|
+
// 此前返回 '' 会被 _startAuth 当成 trust,导致任意用户名 + 空密码即可连入,
|
|
797
|
+
// 与 MysqlServer 的默认拒绝策略保持一致。仅显式 noAuth: true 才放行。
|
|
798
|
+
return undefined;
|
|
796
799
|
}
|
|
797
800
|
|
|
798
801
|
_canAccessDb(user, dbName) {
|
package/lib/plugin.js
CHANGED
|
@@ -104,10 +104,16 @@ function definePlugin(name, def = {}) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** 登记单个钩子;未知钩子名会自动创建,便于插件自定义扩展点 */
|
|
107
|
-
function registerHook(engine, hook, fn) {
|
|
107
|
+
function registerHook(engine, hook, fn, priority = 0) {
|
|
108
108
|
if (typeof fn !== 'function') return engine;
|
|
109
109
|
if (!engine._hooks) engine._hooks = createHookRegistry();
|
|
110
110
|
if (!engine._hooks[hook]) engine._hooks[hook] = [];
|
|
111
|
+
// 记录优先级:即使插件是后加载的,也应按 priority 决定执行顺序,
|
|
112
|
+
// 而不是简单追加在已有钩子之后(此前 sort(_plugins) 只排插件列表,
|
|
113
|
+
// 钩子链仍按注册顺序执行,导致 priority 实际无效)。
|
|
114
|
+
try {
|
|
115
|
+
Object.defineProperty(fn, '__jsqlHookPriority', { value: priority, configurable: true, writable: true });
|
|
116
|
+
} catch (e) { /* 冻结函数等无法打标,按 0 处理 */ }
|
|
111
117
|
engine._hooks[hook].push(fn);
|
|
112
118
|
return engine;
|
|
113
119
|
}
|
|
@@ -115,9 +121,10 @@ function registerHook(engine, hook, fn) {
|
|
|
115
121
|
/** 把一个插件声明的所有钩子登记到引擎上 */
|
|
116
122
|
function registerHooks(engine, plugin) {
|
|
117
123
|
if (!plugin || !plugin.hooks) return engine;
|
|
124
|
+
const priority = plugin.priority || 0;
|
|
118
125
|
for (const [hook, val] of Object.entries(plugin.hooks)) {
|
|
119
126
|
const fns = Array.isArray(val) ? val : [val];
|
|
120
|
-
for (const fn of fns) registerHook(engine, hook, fn);
|
|
127
|
+
for (const fn of fns) registerHook(engine, hook, fn, priority);
|
|
121
128
|
}
|
|
122
129
|
return engine;
|
|
123
130
|
}
|
|
@@ -253,7 +260,11 @@ function runHooks(engine, hookName, args) {
|
|
|
253
260
|
|
|
254
261
|
return (async () => {
|
|
255
262
|
try {
|
|
256
|
-
|
|
263
|
+
// priority 大的先执行;同 priority 保持注册顺序(sort 在现代引擎是稳定的)
|
|
264
|
+
const ordered = hooks.length > 1
|
|
265
|
+
? hooks.slice().sort((a, b) => (b.__jsqlHookPriority || 0) - (a.__jsqlHookPriority || 0))
|
|
266
|
+
: hooks;
|
|
267
|
+
for (const fn of ordered) {
|
|
257
268
|
const result = await fn(...args);
|
|
258
269
|
if (result === false) return false;
|
|
259
270
|
}
|
package/lib/sql.js
CHANGED
|
@@ -2811,8 +2811,15 @@ class Parser {
|
|
|
2811
2811
|
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
2812
2812
|
if (t2.type === 'keyword' && (t2.value === 'REGEXP' || t2.value === 'RLIKE')) {
|
|
2813
2813
|
this.next();
|
|
2814
|
-
|
|
2815
|
-
|
|
2814
|
+
let pattern = this.parseValue();
|
|
2815
|
+
// `expr REGEXP BINARY 'pat'`:BINARY 前缀表示大小写敏感匹配。
|
|
2816
|
+
// 解析出的 pattern 会包一层 { type:'binary', operand },这里解包并标记 ci=false。
|
|
2817
|
+
let binary = false;
|
|
2818
|
+
if (pattern && typeof pattern === 'object' && pattern.type === 'binary') {
|
|
2819
|
+
binary = true;
|
|
2820
|
+
pattern = pattern.operand;
|
|
2821
|
+
}
|
|
2822
|
+
return { type: 'regexp', operand: left, pattern, not, ci: !binary };
|
|
2816
2823
|
}
|
|
2817
2824
|
|
|
2818
2825
|
// PG JSON 包含操作符
|
|
@@ -2982,7 +2989,16 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2982
2989
|
if (!rowHasPrefixedKeys(row) && row[col] !== undefined) return row[col];
|
|
2983
2990
|
}
|
|
2984
2991
|
// 相关子查询:回退到外层行(ctx.__outer 由 _evalExists 临时注入)
|
|
2985
|
-
if (ctx && ctx.__outer
|
|
2992
|
+
if (ctx && ctx.__outer) {
|
|
2993
|
+
if (ctx.__outer[n] !== undefined) return ctx.__outer[n];
|
|
2994
|
+
// 外层行通常是未加前缀的裸列({id:1}),而引用可能带表层限定符(a.id)。
|
|
2995
|
+
// 内层行已套前缀时,row[col] 的回退已被禁止,这里必须再按裸列名查一次,
|
|
2996
|
+
// 否则 `WHERE b.id = a.id` 在 UPDATE/SELECT 的相关子查询里恒为 NULL。
|
|
2997
|
+
if (n.indexOf('.') !== -1) {
|
|
2998
|
+
const col = n.slice(n.lastIndexOf('.') + 1);
|
|
2999
|
+
if (ctx.__outer[col] !== undefined) return ctx.__outer[col];
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
2986
3002
|
return undefined;
|
|
2987
3003
|
}
|
|
2988
3004
|
case 'arith': {
|
|
@@ -4476,7 +4492,14 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4476
4492
|
switch (expr.type) {
|
|
4477
4493
|
case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
|
|
4478
4494
|
case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
|
|
4479
|
-
case 'xor':
|
|
4495
|
+
case 'xor': {
|
|
4496
|
+
// 三值逻辑:任一操作数为 UNKNOWN 时结果也是 UNKNOWN。
|
|
4497
|
+
// 旧实现用 Boolean() 归一,Boolean(null)=false 会把 NULL XOR TRUE 误判为 TRUE。
|
|
4498
|
+
const l = evaluateExpr(expr.left, row, ctx);
|
|
4499
|
+
const r = evaluateExpr(expr.right, row, ctx);
|
|
4500
|
+
return sqlXor(l === null || l === undefined ? null : Boolean(l),
|
|
4501
|
+
r === null || r === undefined ? null : Boolean(r));
|
|
4502
|
+
}
|
|
4480
4503
|
case 'not': {
|
|
4481
4504
|
// 三值逻辑:NOT UNKNOWN = UNKNOWN(返回 null,在 WHERE/ON/HAVING 中视为不选中)
|
|
4482
4505
|
// 必须用 resolveOperand 取值:evaluateExpr 只认布尔类节点,
|
|
@@ -4569,8 +4592,18 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4569
4592
|
case 'regexp': {
|
|
4570
4593
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
4571
4594
|
if (v === null || v === undefined) return false;
|
|
4572
|
-
//
|
|
4573
|
-
|
|
4595
|
+
// pattern 可能是字面量、列引用或表达式:统一用 resolveOperand 取值。
|
|
4596
|
+
// 过去 String(expr.pattern) 对 { type:'binary'|'column' } 对象只会得到
|
|
4597
|
+
// "[object Object]",BINARY 前缀的大小写敏感语义也因此完全失效。
|
|
4598
|
+
// pattern 可能是字面量、列引用或表达式:字面量(primitive)直接使用,
|
|
4599
|
+
// 只有 AST 节点才走 resolveOperand —— 否则字符串会被当成列名。
|
|
4600
|
+
const pat = (expr.pattern && typeof expr.pattern === 'object')
|
|
4601
|
+
? resolveOperand(expr.pattern, row, ctx)
|
|
4602
|
+
: expr.pattern;
|
|
4603
|
+
if (pat === null || pat === undefined) return false;
|
|
4604
|
+
let re;
|
|
4605
|
+
try { re = new RegExp(String(pat), expr.ci === false ? '' : 'i'); } catch { return false; }
|
|
4606
|
+
// REGEXP / RLIKE 默认大小写不敏感;`~` 敏感、`~*` 不敏感;REGEXP BINARY 敏感
|
|
4574
4607
|
const r = re.test(String(v));
|
|
4575
4608
|
return expr.not ? !r : r;
|
|
4576
4609
|
}
|
|
@@ -4590,8 +4623,11 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4590
4623
|
// 三值逻辑:任一操作数为 NULL 时结果是 UNKNOWN(用 null 表示),
|
|
4591
4624
|
// 与普通比较运算符一致;不能返回 false,否则 NOT BETWEEN 会被误判为真。
|
|
4592
4625
|
if (v === null || v === undefined || lo === null || lo === undefined || hi === null || hi === undefined) return null;
|
|
4593
|
-
|
|
4594
|
-
|
|
4626
|
+
// 数值(含数字字符串)必须按数值比较,否则 '10' BETWEEN 2 AND 9 会退化成
|
|
4627
|
+
// 字典序 ('10' >= '2' 为 false),与 MySQL 不一致。
|
|
4628
|
+
const nv = numericIfNumeric(v), nlo = numericIfNumeric(lo), nhi = numericIfNumeric(hi);
|
|
4629
|
+
const inRange = nv !== null && nlo !== null && nhi !== null
|
|
4630
|
+
? nv >= nlo && nv <= nhi
|
|
4595
4631
|
: String(v) >= String(lo) && String(v) <= String(hi);
|
|
4596
4632
|
return expr.not ? !inRange : inRange;
|
|
4597
4633
|
}
|
|
@@ -5447,11 +5483,21 @@ class SQLExecutor {
|
|
|
5447
5483
|
}
|
|
5448
5484
|
if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
|
|
5449
5485
|
let count = 0;
|
|
5486
|
+
// UPDATE 的赋值表达式同样可能含标量子查询,SELECT 路径在 _materialize
|
|
5487
|
+
// 里统一物化,但 UPDATE 从未调用它 —— 子查询节点 _value 一直是 undefined,
|
|
5488
|
+
// resolveOperand 直接返回 NULL,`SET x = (SELECT ...)` 静默写成 NULL。
|
|
5489
|
+
for (const [, val] of statement.assignments) {
|
|
5490
|
+
if (val && typeof val === 'object') await this._materialize(val);
|
|
5491
|
+
}
|
|
5450
5492
|
for (const row of candidates) {
|
|
5451
5493
|
const id = this._rowPkId(row, pkCols);
|
|
5452
5494
|
if (id !== undefined) {
|
|
5453
5495
|
const data = {};
|
|
5454
5496
|
for (const [col, val] of statement.assignments) {
|
|
5497
|
+
if (val && typeof val === 'object' && val.type) {
|
|
5498
|
+
// 相关子查询:按当前行重算(非相关时重算是幂等的,结果一致)
|
|
5499
|
+
for (const sub of this._collectSubqueries(val)) await this._evalCorrelated(sub, row);
|
|
5500
|
+
}
|
|
5455
5501
|
data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
|
|
5456
5502
|
}
|
|
5457
5503
|
// 必须 await:wasm / native client 的 updateById 是异步的,
|
package/lib/star-nag.js
CHANGED
|
@@ -156,22 +156,33 @@ function checkStarred() {
|
|
|
156
156
|
// 用同步 API:CLI 启动阶段还没有 event loop 可用
|
|
157
157
|
try {
|
|
158
158
|
const { execFileSync } = require('child_process');
|
|
159
|
-
|
|
159
|
+
// 用 -w 追加 HTTP 状态码,按状态码判定,而不是靠 body 长度猜。
|
|
160
|
+
// 该端点语义:已 star → 204(空 body);未 star → 404。
|
|
161
|
+
// 此前把「空 body」一律当已 star:遇到 302/500 等空 body 响应或代理异常时
|
|
162
|
+
// 会误判为 star 过,从此永久闭嘴。
|
|
163
|
+
const raw = execFileSync('curl', [
|
|
160
164
|
'-sS', '-m', '4',
|
|
165
|
+
'-w', '\n%{http_code}',
|
|
161
166
|
'-H', `Authorization: Bearer ${token}`,
|
|
162
167
|
'-H', 'Accept: application/vnd.github+json',
|
|
163
168
|
'-H', 'X-GitHub-Api-Version: 2022-11-28',
|
|
164
169
|
`https://api.github.com/user/starred/${REPO}`,
|
|
165
170
|
], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString();
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
171
|
+
const nl = raw.lastIndexOf('\n');
|
|
172
|
+
const status = Number(raw.slice(nl + 1).trim());
|
|
173
|
+
const body = raw.slice(0, nl).trim();
|
|
174
|
+
if (status === 204) return true;
|
|
175
|
+
if (status === 404) return false;
|
|
176
|
+
if (status === 200 && body) {
|
|
177
|
+
// 兼容返回数组的旧行为:数组里有该仓库才算 star
|
|
178
|
+
try {
|
|
179
|
+
const j = JSON.parse(body);
|
|
180
|
+
if (Array.isArray(j)) return j.length > 0;
|
|
181
|
+
} catch (e) { /* 非 JSON,无法确认 */ }
|
|
182
|
+
}
|
|
183
|
+
return null; // 401/403/3xx/5xx → 无法确认
|
|
173
184
|
} catch (e) {
|
|
174
|
-
return null; // 网络不通 /
|
|
185
|
+
return null; // 网络不通 / 没装 curl → 不确定
|
|
175
186
|
}
|
|
176
187
|
}
|
|
177
188
|
|
package/lib/table.js
CHANGED
|
@@ -118,6 +118,24 @@ function isSafeRegex(pattern) {
|
|
|
118
118
|
return groups.length === 0;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 深拷贝一行数据(普通对象/数组/Date),用于隔离内部存储与调用方。
|
|
123
|
+
*
|
|
124
|
+
* 过去 insert/find 只做 `{ ...row }` 浅拷贝:行里的嵌套对象/数组与内部存储
|
|
125
|
+
* 共享引用,调用方修改返回值(或插入后继续改传入对象)会静默污染数据库内容。
|
|
126
|
+
* 非普通对象(Map/Set/类实例等)保持原引用,避免破坏无法安全克隆的类型。
|
|
127
|
+
*/
|
|
128
|
+
function cloneRow(value) {
|
|
129
|
+
if (value === null || typeof value !== 'object') return value;
|
|
130
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
131
|
+
if (Array.isArray(value)) return value.map(cloneRow);
|
|
132
|
+
const proto = Object.getPrototypeOf(value);
|
|
133
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const k of Object.keys(value)) out[k] = cloneRow(value[k]);
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
class Table {
|
|
122
140
|
constructor(name, schema, db) {
|
|
123
141
|
this._name = name;
|
|
@@ -245,7 +263,7 @@ class Table {
|
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
const rowIndex = this._rows.length;
|
|
248
|
-
this._rows.push(
|
|
266
|
+
this._rows.push(cloneRow(data));
|
|
249
267
|
this._addToIndexes(rowIndex, data);
|
|
250
268
|
this._dirty = true;
|
|
251
269
|
this._db._markDirty(this._name);
|
|
@@ -299,7 +317,7 @@ class Table {
|
|
|
299
317
|
const result = new Array(N);
|
|
300
318
|
for (let i = 0; i < N; i++) {
|
|
301
319
|
// 在副本上做默认值/计算列/类型归一,避免污染调用方传入的原始对象
|
|
302
|
-
let data =
|
|
320
|
+
let data = cloneRow(items[i]);
|
|
303
321
|
for (const [f, dv] of Object.entries(defaults)) {
|
|
304
322
|
if (data[f] === undefined) data[f] = typeof dv === 'function' ? dv() : dv === 'CURRENT_TIMESTAMP' ? now() : dv;
|
|
305
323
|
}
|
|
@@ -308,7 +326,8 @@ class Table {
|
|
|
308
326
|
data = this._validateDataTypes(data);
|
|
309
327
|
for (const field of this._cachedSchemaFields) {
|
|
310
328
|
const def = schema[field];
|
|
311
|
-
|
|
329
|
+
const isPk = def.primaryKey || def.primary === true;
|
|
330
|
+
if ((def.required || isPk) && (data[field] === undefined || data[field] === null)
|
|
312
331
|
&& !(def.autoIncrement && data[field] === undefined)) throw createError('ER_BAD_NULL_ERROR', field);
|
|
313
332
|
if (def.unique && data[field] !== undefined) {
|
|
314
333
|
if (uniqueTracker[field].has(data[field])) throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
@@ -346,7 +365,7 @@ class Table {
|
|
|
346
365
|
this._autoIncrement = data[autoIncField];
|
|
347
366
|
}
|
|
348
367
|
const rowIdx = this._rows.length;
|
|
349
|
-
this._rows.push(
|
|
368
|
+
this._rows.push(cloneRow(data));
|
|
350
369
|
if (this._pkIndex && this._primaryKey) {
|
|
351
370
|
const pkVal = data[this._primaryKey];
|
|
352
371
|
if (pkVal !== undefined && pkVal !== null) {
|
|
@@ -391,7 +410,7 @@ class Table {
|
|
|
391
410
|
if (Object.keys(query).length > 0) {
|
|
392
411
|
rows = this._applyFilterOptimized(rows, query);
|
|
393
412
|
}
|
|
394
|
-
return rows.map(r => (
|
|
413
|
+
return rows.map(r => cloneRow(r));
|
|
395
414
|
}
|
|
396
415
|
|
|
397
416
|
findOne(query = {}) {
|
|
@@ -399,7 +418,7 @@ class Table {
|
|
|
399
418
|
if (Object.keys(query).length > 0) {
|
|
400
419
|
rows = this._applyFilterOptimized(rows, query);
|
|
401
420
|
}
|
|
402
|
-
return rows.length > 0 ?
|
|
421
|
+
return rows.length > 0 ? cloneRow(rows[0]) : null;
|
|
403
422
|
}
|
|
404
423
|
|
|
405
424
|
exists(query = {}) {
|
|
@@ -415,7 +434,7 @@ class Table {
|
|
|
415
434
|
const rowIndex = this._pkIndex.get(id);
|
|
416
435
|
if (rowIndex !== undefined) {
|
|
417
436
|
const row = this._rows[rowIndex];
|
|
418
|
-
if (row && !row._deleted) return
|
|
437
|
+
if (row && !row._deleted) return cloneRow(row);
|
|
419
438
|
}
|
|
420
439
|
return null;
|
|
421
440
|
}
|
|
@@ -423,7 +442,7 @@ class Table {
|
|
|
423
442
|
const indices = this._btrees[this._primaryKey].search(id);
|
|
424
443
|
if (indices.length > 0) {
|
|
425
444
|
const row = this._rows[indices[0]];
|
|
426
|
-
if (row && !row._deleted) return
|
|
445
|
+
if (row && !row._deleted) return cloneRow(row);
|
|
427
446
|
}
|
|
428
447
|
return null;
|
|
429
448
|
}
|
|
@@ -431,7 +450,7 @@ class Table {
|
|
|
431
450
|
}
|
|
432
451
|
|
|
433
452
|
findAll() {
|
|
434
|
-
return this._getVisibleRows().map(r => (
|
|
453
|
+
return this._getVisibleRows().map(r => cloneRow(r));
|
|
435
454
|
}
|
|
436
455
|
|
|
437
456
|
count(query = {}) {
|
|
@@ -995,18 +1014,22 @@ class Table {
|
|
|
995
1014
|
for (const [field, condition] of Object.entries(query)) {
|
|
996
1015
|
if (field === '$or' || field === '$and' || field === '$not') continue;
|
|
997
1016
|
if (typeof condition !== 'object' || condition === null || Array.isArray(condition) || condition instanceof RegExp) {
|
|
998
|
-
|
|
1017
|
+
// 索引只收非 NULL 值:condition 为 null/undefined 时必须全表扫描,
|
|
1018
|
+
// 否则 search(null) 命中空数组直接返回 0 行(索引列 IS NULL 漏行)。
|
|
1019
|
+
const nullish = condition === null || condition === undefined;
|
|
1020
|
+
if (!nullish && this._btrees[field]) {
|
|
999
1021
|
const indices = this._btrees[field].search(condition);
|
|
1000
1022
|
if (indices.length === 0) return [];
|
|
1001
1023
|
return this._applyFilter(this._rowsFromIndices(indices), query);
|
|
1002
1024
|
}
|
|
1003
|
-
if (this._indexes[field] && this._indexes[field].has(condition)) {
|
|
1025
|
+
if (!nullish && this._indexes[field] && this._indexes[field].has(condition)) {
|
|
1004
1026
|
return this._applyFilter(this._rowsFromIndices(this._indexes[field].get(condition)), query);
|
|
1005
1027
|
}
|
|
1006
|
-
if (this._indexes[field]) return [];
|
|
1028
|
+
if (!nullish && this._indexes[field]) return [];
|
|
1007
1029
|
}
|
|
1008
1030
|
if (condition && typeof condition === 'object') {
|
|
1009
|
-
if (this._btrees[field] && Array.isArray(condition.$in) && condition.$in.length > 0
|
|
1031
|
+
if (this._btrees[field] && Array.isArray(condition.$in) && condition.$in.length > 0
|
|
1032
|
+
&& !condition.$in.some(v => v === null || v === undefined)) {
|
|
1010
1033
|
const indices = this._btrees[field].searchMany(condition.$in);
|
|
1011
1034
|
if (indices.length === 0) return [];
|
|
1012
1035
|
return this._applyFilter(this._rowsFromIndices(indices), query);
|
|
@@ -1355,8 +1378,9 @@ class Table {
|
|
|
1355
1378
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
1356
1379
|
if (field === '_softDelete') continue;
|
|
1357
1380
|
|
|
1358
|
-
// NOT NULL
|
|
1359
|
-
|
|
1381
|
+
// NOT NULL(主键隐式 NOT NULL,否则唯一约束检查会因 NULL 短路而被绕过)
|
|
1382
|
+
const isPk = def.primaryKey || def.primary === true;
|
|
1383
|
+
if ((def.required || isPk) && (data[field] === undefined || data[field] === null)
|
|
1360
1384
|
&& !(def.autoIncrement && data[field] === undefined)) {
|
|
1361
1385
|
throw createError('ER_BAD_NULL_ERROR', field);
|
|
1362
1386
|
}
|
|
@@ -1664,4 +1688,5 @@ class Table {
|
|
|
1664
1688
|
}
|
|
1665
1689
|
}
|
|
1666
1690
|
|
|
1667
|
-
module.exports = Table;
|
|
1691
|
+
module.exports = Table;
|
|
1692
|
+
Table.cloneRow = cloneRow;
|
package/lib/web_ui.js
CHANGED
|
@@ -69,19 +69,22 @@ tr:nth-child(even) td { background: #0d1117; }
|
|
|
69
69
|
<script>
|
|
70
70
|
let dbs = [], cur = null;
|
|
71
71
|
const q = async (u, o) => { const r = await fetch(u, o); return r.json(); };
|
|
72
|
-
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
72
|
+
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
|
|
73
73
|
async function load() {
|
|
74
74
|
dbs = await q('/api/databases');
|
|
75
75
|
const el = document.getElementById('dbs');
|
|
76
|
-
el.innerHTML = dbs.map(d => '<div class="db"
|
|
76
|
+
el.innerHTML = dbs.map(d => '<div class="db" data-name="' + esc(d.name) + '">' + esc(d.name) + ' <span class="rows">(' + d.tables + ' tbl)</span></div>').join('');
|
|
77
|
+
el.querySelectorAll('.db').forEach(e => e.addEventListener('click', () => openDb(e.dataset.name)));
|
|
77
78
|
if (!cur && dbs.length) openDb(dbs[0].name);
|
|
78
79
|
}
|
|
79
80
|
async function openDb(n) {
|
|
80
81
|
cur = n;
|
|
81
|
-
document.querySelectorAll('.db').forEach(e => e.classList.toggle('active', e.
|
|
82
|
+
document.querySelectorAll('.db').forEach(e => e.classList.toggle('active', e.dataset.name === n));
|
|
82
83
|
const r = await q('/api/tables?db=' + encodeURIComponent(n));
|
|
83
|
-
document.getElementById('tbls')
|
|
84
|
-
|
|
84
|
+
const tl = document.getElementById('tbls');
|
|
85
|
+
tl.innerHTML = r.tables.map(t =>
|
|
86
|
+
'<div class="tbl" data-name="' + esc(t.name) + '">' + esc(t.name) + ' (' + t.count + ')</div>').join('');
|
|
87
|
+
tl.querySelectorAll('.tbl').forEach(e => e.addEventListener('click', () => sel(e.dataset.name)));
|
|
85
88
|
}
|
|
86
89
|
function sel(n) { document.getElementById('sql').value = 'SELECT * FROM ' + n + ' LIMIT 100'; run(); }
|
|
87
90
|
async function run() {
|
package/lib/xql/src/compiler.js
CHANGED
|
@@ -136,7 +136,7 @@ class Emitter {
|
|
|
136
136
|
const lo = this.emit(node.lo);
|
|
137
137
|
const hi = this.emit(node.hi);
|
|
138
138
|
const t = this.tmp();
|
|
139
|
-
this.push(`let ${t} = (${v} == null ? null : (${v} >= ${lo} && ${v} <= ${hi}));`);
|
|
139
|
+
this.push(`let ${t} = (${v} == null || ${lo} == null || ${hi} == null ? null : (${v} >= ${lo} && ${v} <= ${hi}));`);
|
|
140
140
|
return t;
|
|
141
141
|
}
|
|
142
142
|
case 'like': {
|
|
@@ -183,8 +183,8 @@ class Emitter {
|
|
|
183
183
|
case '*': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} * ${r});`); break;
|
|
184
184
|
case '/': this.push(`let ${t} = (${l} == null || ${r} == null || Number(${r}) === 0 ? null : ${l} / ${r});`); break;
|
|
185
185
|
case '%': this.push(`let ${t} = (${l} == null || ${r} == null || Number(${r}) === 0 ? null : ${l} % ${r});`); break;
|
|
186
|
-
case '=': this.push(`let ${t} = (${l} == null ? null : ${l} === ${r});`); break;
|
|
187
|
-
case '!=': this.push(`let ${t} = (${l} == null ? null : ${l} !== ${r});`); break;
|
|
186
|
+
case '=': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} === ${r});`); break;
|
|
187
|
+
case '!=': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} !== ${r});`); break;
|
|
188
188
|
case '<': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} < ${r});`); break;
|
|
189
189
|
case '>': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} > ${r});`); break;
|
|
190
190
|
case '<=': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} <= ${r});`); break;
|
|
@@ -197,15 +197,25 @@ class Emitter {
|
|
|
197
197
|
emitIn(node) {
|
|
198
198
|
const v = this.emit(node.e);
|
|
199
199
|
const t = this.tmp();
|
|
200
|
-
const not = node.neg ? '!' : '';
|
|
201
|
-
const join = node.neg ? ' && ' : ' || ';
|
|
202
200
|
if (node.list.every(n => n.k === 'lit')) {
|
|
203
201
|
const name = '_set' + this.sets.length;
|
|
204
202
|
this.sets.push({ name, set: new Set(node.list.map(n => n.v)) });
|
|
205
|
-
|
|
203
|
+
// 完整三值逻辑:被查找值为 NULL → UNKNOWN;列表含 NULL 且未匹配 → UNKNOWN。
|
|
204
|
+
const hasNull = node.list.some(n => n.v === null);
|
|
205
|
+
if (node.neg) {
|
|
206
|
+
this.push(`let ${t} = (${v} == null ? null : (${name}.has(${v}) ? false : (${hasNull} ? null : true)));`);
|
|
207
|
+
} else {
|
|
208
|
+
this.push(`let ${t} = (${v} == null ? null : (${name}.has(${v}) ? true : (${hasNull} ? null : false)));`);
|
|
209
|
+
}
|
|
206
210
|
} else {
|
|
207
|
-
const
|
|
208
|
-
|
|
211
|
+
const elems = node.list.map(n => this.emit(n));
|
|
212
|
+
const anyMatch = elems.map(e => `(${v} === ${e})`).join(' || ');
|
|
213
|
+
const anyNull = elems.map(e => `${e} == null`).join(' || ');
|
|
214
|
+
if (node.neg) {
|
|
215
|
+
this.push(`let ${t} = (${v} == null ? null : (${anyMatch} ? false : (${anyNull} ? null : true)));`);
|
|
216
|
+
} else {
|
|
217
|
+
this.push(`let ${t} = (${v} == null ? null : (${anyMatch} ? true : (${anyNull} ? null : false)));`);
|
|
218
|
+
}
|
|
209
219
|
}
|
|
210
220
|
return t;
|
|
211
221
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.5.
|
|
3
|
+
"version": "6.5.15",
|
|
4
4
|
"description": "JSQL-NEO \u2014 Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"test:btree": "node test/btree.test.js",
|
|
53
53
|
"test:native": "node test/native.test.js",
|
|
54
54
|
"test:regress": "node test/regress-5.1.0.js",
|
|
55
|
-
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js",
|
|
55
|
+
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/v6.5.14-regression.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js",
|
|
56
56
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
57
57
|
"test:orms": "node examples/orms/run-all.js",
|
|
58
58
|
"test:dml": "node test/dml-constraints.test.js",
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* v6.5.14 回归测试:把本版修掉的高危 / 中危缺陷逐条固化。
|
|
3
|
+
*
|
|
4
|
+
* C1 B-Tree 混合插入/删除丢失索引条目
|
|
5
|
+
* H1 PostgreSQL 未配置认证时的 trust 绕过
|
|
6
|
+
* H2 NeDB $regex 字符串模式匹配一切
|
|
7
|
+
* H3 UPDATE 标量子查询写成 NULL(含限定符相关子查询)
|
|
8
|
+
* H4 BETWEEN 按字典序而非数值比较
|
|
9
|
+
* H5 主键允许 NULL
|
|
10
|
+
* H6 索引列 = null 漏行
|
|
11
|
+
* M1 错误消息 %s 串位
|
|
12
|
+
* M2 返回行浅拷贝导致内部数据被调用方污染
|
|
13
|
+
* M3 migrate.normalizeSchema 丢失显式 length
|
|
14
|
+
* M4 READ_COMMITTED 下 ROLLBACK 不撤销行
|
|
15
|
+
* M5 XOR 破坏三值逻辑
|
|
16
|
+
* M6 REGEXP BINARY 失效
|
|
17
|
+
* M9 插件钩子未按 priority 排序
|
|
18
|
+
* M10 XQL 编译器 NULL 传播
|
|
19
|
+
* M11 Mongo 结果泄漏内部字段
|
|
20
|
+
*
|
|
21
|
+
* node test/v6.5.14-regression.test.js
|
|
22
|
+
*/
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const ROOT = path.join(__dirname, '..');
|
|
26
|
+
const { Database } = require(path.join(ROOT, 'index.js'));
|
|
27
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
|
|
28
|
+
const BTree = require(path.join(ROOT, 'lib/btree'));
|
|
29
|
+
const { PgServer } = require(path.join(ROOT, 'lib/pg_server'));
|
|
30
|
+
const { Datastore } = require(path.join(ROOT, 'lib/nedb_compat'));
|
|
31
|
+
const { MongoServer } = require(path.join(ROOT, 'lib/mongo_server'));
|
|
32
|
+
const { createError } = require(path.join(ROOT, 'lib/errors'));
|
|
33
|
+
const { normalizeSchema } = require(path.join(ROOT, 'lib/migrate'));
|
|
34
|
+
const { registerHook, runHooks, createHookRegistry } = require(path.join(ROOT, 'lib/plugin'));
|
|
35
|
+
const { parse: xqlParse } = require(path.join(ROOT, 'lib/xql/src/parser'));
|
|
36
|
+
const { compileQuery } = require(path.join(ROOT, 'lib/xql/src/compiler'));
|
|
37
|
+
|
|
38
|
+
const OPTS = { safety: false };
|
|
39
|
+
let pass = 0, fail = 0;
|
|
40
|
+
|
|
41
|
+
function ok(name, cond, extra) {
|
|
42
|
+
if (cond) { pass++; console.log('[OK] ', name); }
|
|
43
|
+
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 160) : ''); }
|
|
44
|
+
}
|
|
45
|
+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
46
|
+
|
|
47
|
+
function makeDb() { return new Database(':memory:'); }
|
|
48
|
+
|
|
49
|
+
(async () => {
|
|
50
|
+
// ── C1. B-Tree 混合插入/删除 ────────────────────────────────
|
|
51
|
+
console.log('\n--- C1. B-Tree 混合插入/删除 ---');
|
|
52
|
+
{
|
|
53
|
+
const t = new BTree(4, false);
|
|
54
|
+
for (let i = 1; i <= 64; i++) t.insert(i, i);
|
|
55
|
+
ok('插入 64 条后 size=64', t.size === 64, t.size);
|
|
56
|
+
for (let i = 2; i <= 64; i += 2) t.remove(i, i);
|
|
57
|
+
ok('删除 32 条后 size=32', t.size === 32, t.size);
|
|
58
|
+
const missing = [];
|
|
59
|
+
for (let i = 1; i <= 64; i += 2) if (!t.search(i).includes(i)) missing.push(i);
|
|
60
|
+
ok('剩余奇数键全部可搜到', missing.length === 0, missing);
|
|
61
|
+
for (let i = 2; i <= 64; i += 2) t.insert(i, i);
|
|
62
|
+
ok('重新插入后 size=64', t.size === 64, t.size);
|
|
63
|
+
const missing2 = [];
|
|
64
|
+
for (let i = 1; i <= 64; i++) if (!t.search(i).includes(i)) missing2.push(i);
|
|
65
|
+
ok('全部键可搜到(无条目丢失)', missing2.length === 0, missing2);
|
|
66
|
+
// 重复键 + 多行号:删其中一行不应误删整键
|
|
67
|
+
const m = new BTree(4, false);
|
|
68
|
+
m.insert(7, 1); m.insert(7, 2); m.insert(7, 3);
|
|
69
|
+
m.remove(7, 2);
|
|
70
|
+
ok('同键多行只删指定行', eq(m.search(7).slice().sort(), [1, 3]), m.search(7));
|
|
71
|
+
ok('同键多行 size 仍为 1(size 记不同键数)', m.size === 1, m.size);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const db = makeDb();
|
|
75
|
+
await db.start();
|
|
76
|
+
const q = (sql) => executeSQL(db, sql, OPTS);
|
|
77
|
+
const one = async (sql) => (await q(sql)).rows[0][0];
|
|
78
|
+
const fails = async (sql) => { try { await q(sql); return null; } catch (e) { return e.message; } };
|
|
79
|
+
|
|
80
|
+
// ── H1. PG 认证默认拒绝 ─────────────────────────────────────
|
|
81
|
+
console.log('\n--- H1. PostgreSQL 认证 ---');
|
|
82
|
+
{
|
|
83
|
+
const srvDefault = new PgServer({});
|
|
84
|
+
ok('未配置认证时返回 undefined(拒绝连接)', srvDefault._userPassword('anyone') === undefined);
|
|
85
|
+
const srvNoAuth = new PgServer({ noAuth: true });
|
|
86
|
+
ok('显式 noAuth 才返回空密码(trust)', srvNoAuth._userPassword('anyone') === '');
|
|
87
|
+
const srvAuth = new PgServer({ auth: { alice: { password: 'pw' } } });
|
|
88
|
+
ok('配置的用户返回真实密码', srvAuth._userPassword('alice') === 'pw');
|
|
89
|
+
ok('未配置的用户返回 undefined', srvAuth._userPassword('bob') === undefined);
|
|
90
|
+
const srvOpt = new PgServer({ user: 'root', password: 'secret' });
|
|
91
|
+
ok('options.user 匹配返回密码', srvOpt._userPassword('root') === 'secret');
|
|
92
|
+
ok('options.user 不匹配拒绝', srvOpt._userPassword('other') === undefined);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── H2. NeDB $regex ─────────────────────────────────────────
|
|
96
|
+
console.log('\n--- H2. NeDB $regex ---');
|
|
97
|
+
{
|
|
98
|
+
const ndb = new Database(':memory:');
|
|
99
|
+
await ndb.start();
|
|
100
|
+
const ds = new Datastore({ database: ndb });
|
|
101
|
+
await ds.loadDatabase();
|
|
102
|
+
await ds.insert([{ n: 'abc' }, { n: 'abcd' }, { n: 'zzz' }, { n: 42 }]);
|
|
103
|
+
const hit = await ds.find({ n: { $regex: 'abc' } }).exec();
|
|
104
|
+
ok('字符串模式只匹配命中文档', hit.length === 2 && hit.every(d => /abc/.test(String(d.n))), hit);
|
|
105
|
+
const exact = await ds.find({ n: { $regex: '^abc$' } }).exec();
|
|
106
|
+
ok('锚定模式精确匹配', exact.length === 1 && exact[0].n === 'abc', exact);
|
|
107
|
+
const invalid = await ds.find({ n: { $regex: '(' } }).exec();
|
|
108
|
+
ok('非法模式返回空而不是全匹配', invalid.length === 0, invalid);
|
|
109
|
+
const re = await ds.find({ n: { $regex: /^abc/ } }).exec();
|
|
110
|
+
ok('RegExp 对象仍可用', re.length === 2, re.map(d => d.n));
|
|
111
|
+
await ndb.stop();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── H3. UPDATE 标量子查询 ───────────────────────────────────
|
|
115
|
+
console.log('\n--- H3. UPDATE 标量子查询 ---');
|
|
116
|
+
await q('CREATE TABLE a3(id INT PRIMARY KEY, v INT)');
|
|
117
|
+
await q('CREATE TABLE b3(id INT PRIMARY KEY, w INT)');
|
|
118
|
+
await q('INSERT INTO a3 VALUES (1,0),(2,0),(3,0)');
|
|
119
|
+
await q('INSERT INTO b3 VALUES (1,10),(2,20)');
|
|
120
|
+
await q('UPDATE a3 SET v = (SELECT w FROM b3 WHERE b3.id = a3.id)');
|
|
121
|
+
ok('相关标量子查询按行写入(限定符)',
|
|
122
|
+
eq((await q('SELECT id, v FROM a3')).rows, [[1, 10], [2, 20], [3, null]]),
|
|
123
|
+
(await q('SELECT id, v FROM a3')).rows);
|
|
124
|
+
ok('非相关标量子查询可用',
|
|
125
|
+
(await q('UPDATE a3 SET v = (SELECT MAX(w) FROM b3)'), (await q('SELECT id, v FROM a3')).rows.every(r => r[1] === 20)));
|
|
126
|
+
ok('SELECT 投影里的相关子查询一致',
|
|
127
|
+
eq((await q('SELECT a3.id, (SELECT w FROM b3 WHERE b3.id = a3.id) AS w FROM a3')).rows,
|
|
128
|
+
[[1, 10], [2, 20], [3, null]]));
|
|
129
|
+
ok('EXISTS 相关子查询仍正确',
|
|
130
|
+
eq((await q('SELECT a3.id FROM a3 WHERE EXISTS (SELECT 1 FROM b3 WHERE b3.id = a3.id)')).rows, [[1], [2]]));
|
|
131
|
+
|
|
132
|
+
// ── H4. BETWEEN 数值比较 ────────────────────────────────────
|
|
133
|
+
console.log('\n--- H4. BETWEEN ---');
|
|
134
|
+
ok('10 BETWEEN 2 AND 20 = 1', (await one('SELECT 10 BETWEEN 2 AND 20')) === 1);
|
|
135
|
+
ok('5 BETWEEN 10 AND 20 = 0', (await one('SELECT 5 BETWEEN 10 AND 20')) === 0);
|
|
136
|
+
ok('"10" BETWEEN 2 AND 20 仍按数值', (await one("SELECT '10' BETWEEN 2 AND 20")) === 1);
|
|
137
|
+
ok('字符串 BETWEEN 保持字典序', (await one("SELECT 'b' BETWEEN 'a' AND 'c'")) === 1);
|
|
138
|
+
|
|
139
|
+
// ── H5. 主键 NOT NULL ───────────────────────────────────────
|
|
140
|
+
console.log('\n--- H5. 主键 NULL ---');
|
|
141
|
+
await db.createTable('pk5', {
|
|
142
|
+
k: { type: 'integer', primaryKey: true },
|
|
143
|
+
v: { type: 'integer' },
|
|
144
|
+
});
|
|
145
|
+
{
|
|
146
|
+
const msg = await (async () => { try { await db.insert('pk5', { k: null, v: 1 }); return null; } catch (e) { return e.message; } })();
|
|
147
|
+
ok('非自增主键插入 NULL 报错', /cannot be null/i.test(msg || ''), msg);
|
|
148
|
+
await db.insert('pk5', { k: 1, v: 1 });
|
|
149
|
+
const upd = await (async () => { try { await db.updateById('pk5', 1, { k: null }); return null; } catch (e) { return e.message; } })();
|
|
150
|
+
ok('更新主键为 NULL 报错', /cannot be null/i.test(upd || ''), upd);
|
|
151
|
+
}
|
|
152
|
+
{
|
|
153
|
+
let accepted = false;
|
|
154
|
+
try { await q('CREATE TABLE pk5b(k INT PRIMARY KEY, v INT)'); await q('INSERT INTO pk5b VALUES (NULL, 1)'); accepted = true; } catch (_) { /* 预期抛错 */ }
|
|
155
|
+
ok('INT PRIMARY KEY 自增语义仍允许 NULL(自动取号)', accepted);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── H6. 索引列 = null ──────────────────────────────────────
|
|
159
|
+
console.log('\n--- H6. 索引列 null ---');
|
|
160
|
+
await q('CREATE TABLE n6(id INT PRIMARY KEY, c INT)');
|
|
161
|
+
await q('INSERT INTO n6 VALUES (1,NULL),(2,5),(3,NULL)');
|
|
162
|
+
await q('CREATE INDEX ixc ON n6(c)');
|
|
163
|
+
ok('索引列 find({c:null}) 不漏行', eq((await db.find('n6', { c: null })).map(r => r.id), [1, 3]),
|
|
164
|
+
await db.find('n6', { c: null }));
|
|
165
|
+
ok('索引列 find({c:5}) 正常', eq((await db.find('n6', { c: 5 })).map(r => r.id), [2]));
|
|
166
|
+
ok('$in 含 null 时回退全表', eq((await db.find('n6', { c: { $in: [5, null] } })).map(r => r.id), [1, 2, 3]),
|
|
167
|
+
await db.find('n6', { c: { $in: [5, null] } }));
|
|
168
|
+
|
|
169
|
+
// ── M1. 错误消息 %s ────────────────────────────────────────
|
|
170
|
+
console.log('\n--- M1. 错误消息 %s ---');
|
|
171
|
+
{
|
|
172
|
+
const e1 = createError('ER_DUP_ENTRY', 'x%s', 'k');
|
|
173
|
+
ok('参数里的 %s 不被二次替换', e1.message === "Duplicate entry 'x%s' for key 'k'", e1.message);
|
|
174
|
+
const e2 = createError('ER_DUP_ENTRY', 'only-one');
|
|
175
|
+
ok('参数不足时裸 %s 被清空', e2.message === "Duplicate entry 'only-one' for key ''", e2.message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── M2. 深拷贝 ────────────────────────────────────────────
|
|
179
|
+
console.log('\n--- M2. 行深拷贝 ---');
|
|
180
|
+
await db.createTable('m2', {
|
|
181
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
182
|
+
meta: { type: 'json' },
|
|
183
|
+
});
|
|
184
|
+
await db.insert('m2', { meta: { a: [1, 2] } });
|
|
185
|
+
{
|
|
186
|
+
const r1 = await db.findById('m2', 1);
|
|
187
|
+
r1.meta.a.push(99);
|
|
188
|
+
const r2 = await db.findById('m2', 1);
|
|
189
|
+
ok('修改返回值不污染内部行', eq(r2.meta, { a: [1, 2] }), r2.meta);
|
|
190
|
+
const list = await db.find('m2', {});
|
|
191
|
+
list[0].meta.a.push(100);
|
|
192
|
+
const again = await db.find('m2', {});
|
|
193
|
+
ok('find() 同样返回深拷贝', eq(again[0].meta, { a: [1, 2] }), again[0].meta);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── M3. migrate 保留 length ────────────────────────────────
|
|
197
|
+
console.log('\n--- M3. migrate length ---');
|
|
198
|
+
{
|
|
199
|
+
const s1 = normalizeSchema({ name: { type: 'string', length: 32 } });
|
|
200
|
+
ok('显式 length 保留', s1.name.length === 32, s1.name);
|
|
201
|
+
const s2 = normalizeSchema({ name: { type: 'string', maxLength: 64 } });
|
|
202
|
+
ok('maxLength 回填为 length', s2.name.length === 64 && s2.name.maxLength === undefined, s2.name);
|
|
203
|
+
const s3 = normalizeSchema({ name: { type: 'string', length: 16, maxLength: 99 } });
|
|
204
|
+
ok('显式 length 优先于 maxLength', s3.name.length === 16, s3.name);
|
|
205
|
+
const s4 = normalizeSchema({ n: 'int' });
|
|
206
|
+
ok('字符串简写仍归一化', s4.n.type === 'integer', s4.n);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── M4. READ_COMMITTED 回滚 ─────────────────────────────────
|
|
210
|
+
console.log('\n--- M4. READ_COMMITTED 回滚 ---');
|
|
211
|
+
{
|
|
212
|
+
const tx = makeDb();
|
|
213
|
+
await tx.start();
|
|
214
|
+
await tx.createTable('m4', {
|
|
215
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
216
|
+
v: { type: 'integer' },
|
|
217
|
+
});
|
|
218
|
+
await tx.insert('m4', { v: 1 });
|
|
219
|
+
tx.begin('READ_COMMITTED');
|
|
220
|
+
await tx.insert('m4', { v: 2 });
|
|
221
|
+
tx.rollback();
|
|
222
|
+
ok('ROLLBACK 撤销已插入行', eq((await tx.find('m4', {})).map(r => r.v), [1]), await tx.find('m4', {}));
|
|
223
|
+
tx.begin('READ_COMMITTED');
|
|
224
|
+
await tx.insert('m4', { v: 3 });
|
|
225
|
+
tx.commit();
|
|
226
|
+
ok('COMMIT 保留行', eq((await tx.find('m4', {})).map(r => r.v).sort(), [1, 3]));
|
|
227
|
+
await tx.stop();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── M5. XOR 三值逻辑 ───────────────────────────────────────
|
|
231
|
+
console.log('\n--- M5. XOR ---');
|
|
232
|
+
ok('TRUE XOR FALSE = 1', (await one('SELECT TRUE XOR FALSE')) === 1);
|
|
233
|
+
ok('TRUE XOR TRUE = 0', (await one('SELECT TRUE XOR TRUE')) === 0);
|
|
234
|
+
ok('NULL XOR TRUE = NULL', (await one('SELECT NULL XOR TRUE')) === null);
|
|
235
|
+
ok('NULL XOR FALSE = NULL', (await one('SELECT NULL XOR FALSE')) === null);
|
|
236
|
+
ok('NULL XOR NULL = NULL', (await one('SELECT NULL XOR NULL')) === null);
|
|
237
|
+
|
|
238
|
+
// ── M6. REGEXP BINARY ──────────────────────────────────────
|
|
239
|
+
console.log('\n--- M6. REGEXP BINARY ---');
|
|
240
|
+
await q('CREATE TABLE r6(id INT PRIMARY KEY, s TEXT)');
|
|
241
|
+
await q("INSERT INTO r6 VALUES (1,'abc'),(2,'ABC')");
|
|
242
|
+
ok('默认 REGEXP 大小写不敏感', eq((await q("SELECT id FROM r6 WHERE s REGEXP 'abc'")).rows.map(r => r[0]).sort(), [1, 2]));
|
|
243
|
+
ok('REGEXP BINARY 大小写敏感', eq((await q("SELECT id FROM r6 WHERE s REGEXP BINARY 'abc'")).rows, [[1]]));
|
|
244
|
+
ok('RLIKE BINARY 同样生效', eq((await q("SELECT id FROM r6 WHERE s RLIKE BINARY 'abc'")).rows, [[1]]));
|
|
245
|
+
ok('非法正则安全返回不匹配', eq((await q("SELECT id FROM r6 WHERE s REGEXP BINARY '('")).rows, []));
|
|
246
|
+
|
|
247
|
+
// ── M9. 插件钩子优先级 ─────────────────────────────────────
|
|
248
|
+
console.log('\n--- M9. 钩子优先级 ---');
|
|
249
|
+
{
|
|
250
|
+
const engine = { _hooks: createHookRegistry() };
|
|
251
|
+
const order = [];
|
|
252
|
+
registerHook(engine, 'beforeQuery', () => { order.push('low'); return true; }, 0);
|
|
253
|
+
registerHook(engine, 'beforeQuery', () => { order.push('high'); return true; }, 10);
|
|
254
|
+
registerHook(engine, 'beforeQuery', () => { order.push('mid'); return true; }, 5);
|
|
255
|
+
await runHooks(engine, 'beforeQuery', []);
|
|
256
|
+
ok('按 priority 降序执行', eq(order, ['high', 'mid', 'low']), order);
|
|
257
|
+
|
|
258
|
+
const order2 = [];
|
|
259
|
+
const e2 = { _hooks: createHookRegistry() };
|
|
260
|
+
registerHook(e2, 'beforeQuery', () => { order2.push('a'); return true; }, 3);
|
|
261
|
+
registerHook(e2, 'beforeQuery', () => { order2.push('b'); return true; }, 3);
|
|
262
|
+
await runHooks(e2, 'beforeQuery', []);
|
|
263
|
+
ok('同 priority 保持注册顺序', eq(order2, ['a', 'b']), order2);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── M10. XQL NULL 传播 ────────────────────────────────────
|
|
267
|
+
console.log('\n--- M10. XQL NULL ---');
|
|
268
|
+
{
|
|
269
|
+
const rows = [{ id: 1, a: null }, { id: 2, a: 5 }, { id: 3, a: 3 }];
|
|
270
|
+
const run = (sql) => compileQuery(xqlParse(sql), {})(rows);
|
|
271
|
+
ok('a = 5 命中', eq(run('SELECT id FROM t WHERE a = 5'), [{ id: 2 }]));
|
|
272
|
+
ok('a = NULL 不命中任何行', eq(run('SELECT id FROM t WHERE a = NULL'), []));
|
|
273
|
+
ok('a != NULL 不命中任何行', eq(run('SELECT id FROM t WHERE a != NULL'), []));
|
|
274
|
+
ok('BETWEEN 正常', eq(run('SELECT id FROM t WHERE a BETWEEN 2 AND 4'), [{ id: 3 }]));
|
|
275
|
+
ok('IN (5, NULL) 只命中 5', eq(run('SELECT id FROM t WHERE a IN (5, NULL)'), [{ id: 2 }]));
|
|
276
|
+
ok('NOT IN 含 NULL 不命中(三值逻辑)', eq(run('SELECT id FROM t WHERE a NOT IN (5, NULL)'), []));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── M11. Mongo 内部字段 ───────────────────────────────────
|
|
280
|
+
console.log('\n--- M11. Mongo _publicDoc ---');
|
|
281
|
+
{
|
|
282
|
+
const ms = new MongoServer({});
|
|
283
|
+
ok('剥离 _deleted/_rid/_score', eq(ms._publicDoc({ _id: 1, x: 2, _rid: 9, _deleted: false, _score: 3 }), { _id: 1, x: 2 }),
|
|
284
|
+
ms._publicDoc({ _id: 1, x: 2, _rid: 9, _deleted: false, _score: 3 }));
|
|
285
|
+
ok('干净文档原样返回', eq(ms._publicDoc({ _id: 2, y: 3 }), { _id: 2, y: 3 }));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await db.stop();
|
|
289
|
+
|
|
290
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
291
|
+
if (fail > 0) process.exitCode = 1;
|
|
292
|
+
})().catch((e) => {
|
|
293
|
+
console.error('FATAL', e);
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
});
|