jsql-neo 6.5.14 → 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/database.js +6 -1
- package/lib/sql.js +10 -1
- 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/database.js
CHANGED
|
@@ -1938,6 +1938,9 @@ class Database {
|
|
|
1938
1938
|
const clash = table._rows.find(r => r !== row && r[field] === nv);
|
|
1939
1939
|
if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
|
|
1940
1940
|
}
|
|
1941
|
+
// 主键/必填列的 NULL 校验:updateById 此前只做类型校验与重复检查,
|
|
1942
|
+
// `UPDATE t SET id = NULL` 会被静默写入(主键行变成 NULL 主键)。
|
|
1943
|
+
table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
|
|
1941
1944
|
Object.assign(row, validated);
|
|
1942
1945
|
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
|
|
1943
1946
|
this._emit('update', { table: tableName, id, data: validated });
|
|
@@ -1956,7 +1959,9 @@ class Database {
|
|
|
1956
1959
|
for (const [id, data] of pairs) {
|
|
1957
1960
|
const row = this._resolveId(table, id);
|
|
1958
1961
|
if (row) {
|
|
1959
|
-
|
|
1962
|
+
const validated = table._validateDataTypes({ ...data }, true);
|
|
1963
|
+
table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
|
|
1964
|
+
Object.assign(row, validated);
|
|
1960
1965
|
count++;
|
|
1961
1966
|
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
|
|
1962
1967
|
}
|
package/lib/sql.js
CHANGED
|
@@ -2989,7 +2989,16 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2989
2989
|
if (!rowHasPrefixedKeys(row) && row[col] !== undefined) return row[col];
|
|
2990
2990
|
}
|
|
2991
2991
|
// 相关子查询:回退到外层行(ctx.__outer 由 _evalExists 临时注入)
|
|
2992
|
-
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
|
+
}
|
|
2993
3002
|
return undefined;
|
|
2994
3003
|
}
|
|
2995
3004
|
case 'arith': {
|
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
|
+
});
|