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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 递归 CTE(WITH RECURSIVE)测试。
|
|
3
|
+
*
|
|
4
|
+
* 此前解析层能识别(recursive:true、UNION ALL 结构都对),
|
|
5
|
+
* 但执行时把 CTE 的自身引用内联成子查询,导致报 no such column;
|
|
6
|
+
* 修好后又暴露三个问题,逐个都曾让结果错误:
|
|
7
|
+
* 1. 虚拟行被加了表名前缀 → 递归项里的 `n+1` 取不到值
|
|
8
|
+
* 2. 每轮把全部累积行喂回递归项 → 行数按轮递增(1..5 得到 1,2,2,3,2,3,…)
|
|
9
|
+
* 3. 只限制迭代次数不够,行数翻倍会先打爆内存(实测 OOM)
|
|
10
|
+
*/
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '..');
|
|
14
|
+
const { Database, JSQL, NativeJSQL } = require(path.join(ROOT, 'index.js'));
|
|
15
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
|
|
16
|
+
const { parseSQL } = require(path.join(ROOT, 'lib/sql.js'));
|
|
17
|
+
|
|
18
|
+
const OPTS = { safety: false };
|
|
19
|
+
let pass = 0, fail = 0;
|
|
20
|
+
|
|
21
|
+
function ok(name, cond, extra) {
|
|
22
|
+
if (cond) { pass++; console.log('[OK] ', name); }
|
|
23
|
+
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 120) : ''); }
|
|
24
|
+
}
|
|
25
|
+
const rowsOf = r => (Array.isArray(r) ? r[0] : r).rows;
|
|
26
|
+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
27
|
+
|
|
28
|
+
/** 每个用例独立 :memory: 库,避免状态污染 */
|
|
29
|
+
async function t(name, sql, expect, engineName) {
|
|
30
|
+
const db = engineName === 'JSQL' ? new JSQL({ mode: 'memory' })
|
|
31
|
+
: engineName === 'NativeJSQL' ? new NativeJSQL({ mode: 'memory' })
|
|
32
|
+
: new Database(':memory:');
|
|
33
|
+
try {
|
|
34
|
+
if (db.start) await db.start();
|
|
35
|
+
const got = rowsOf(await executeSQL(db, sql, OPTS));
|
|
36
|
+
if (eq(got, expect)) ok(name, true);
|
|
37
|
+
else ok(name, false, { got, expect });
|
|
38
|
+
} catch (e) {
|
|
39
|
+
ok(name, false, String(e.message || e).slice(0, 100));
|
|
40
|
+
} finally {
|
|
41
|
+
try { await db.stop(); } catch (_) { /* ignore */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
(async () => {
|
|
46
|
+
console.log('\n--- 解析 ---');
|
|
47
|
+
{
|
|
48
|
+
const a = parseSQL('WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<5) SELECT * FROM c');
|
|
49
|
+
ok('识别 RECURSIVE', a.recursive === true, a.recursive);
|
|
50
|
+
ok('记录显式列名', JSON.stringify(a.ctes[0].columns) === '["n"]', a.ctes[0].columns);
|
|
51
|
+
ok('记录 UNION ALL', a.ctes[0].select.union && a.ctes[0].select.union.all === true);
|
|
52
|
+
// RECURSIVE 放在第一个 CTE 之后也接受
|
|
53
|
+
const b = parseSQL('WITH k AS (SELECT 1 AS m), RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<3) SELECT * FROM c');
|
|
54
|
+
ok('CTE 之间的 RECURSIVE 也能解析', b.ctes.length === 2, b.ctes.length);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('\n--- 基本递归 ---');
|
|
58
|
+
await t('计数 1..5', 'WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<5) SELECT * FROM c',
|
|
59
|
+
[[1], [2], [3], [4], [5]]);
|
|
60
|
+
await t('1..10 求和 = 55', 'WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<10) SELECT SUM(n) AS s FROM c',
|
|
61
|
+
[[55]]);
|
|
62
|
+
await t('倒计数 3..1', 'WITH RECURSIVE c(n) AS (SELECT 3 UNION ALL SELECT n-1 FROM c WHERE n>1) SELECT * FROM c',
|
|
63
|
+
[[3], [2], [1]]);
|
|
64
|
+
await t('显式列名 + 外层过滤',
|
|
65
|
+
'WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM c WHERE x<3) SELECT * FROM c WHERE x>1',
|
|
66
|
+
[[2], [3]]);
|
|
67
|
+
await t('UNION(去重)终止',
|
|
68
|
+
'WITH RECURSIVE c(n) AS (SELECT 1 UNION SELECT 1 FROM c WHERE n<3) SELECT * FROM c',
|
|
69
|
+
[[1]]);
|
|
70
|
+
await t('多列递归',
|
|
71
|
+
'WITH RECURSIVE c(a,b) AS (SELECT 1,1 UNION ALL SELECT a+1,b+a FROM c WHERE a<4) SELECT * FROM c',
|
|
72
|
+
[[1, 1], [2, 2], [3, 4], [4, 7]]);
|
|
73
|
+
await t('外层聚合递归结果',
|
|
74
|
+
'WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<4) SELECT COUNT(*) AS k, MAX(n) AS mx FROM c',
|
|
75
|
+
[[4, 4]]);
|
|
76
|
+
await t('多锚点分支(UNION 链 > 2 节)',
|
|
77
|
+
'WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT n+1 FROM c WHERE n<3) SELECT * FROM c',
|
|
78
|
+
[[1], [2], [2], [3], [3]]);
|
|
79
|
+
await t('日期序列',
|
|
80
|
+
`WITH RECURSIVE d(x) AS (SELECT DATE('2026-01-01') UNION ALL SELECT DATE_ADD(x, INTERVAL 1 DAY) FROM d WHERE x < DATE('2026-01-04')) SELECT * FROM d`,
|
|
81
|
+
[['2026-01-01'], ['2026-01-02'], ['2026-01-03'], ['2026-01-04']]);
|
|
82
|
+
await t('递归 CTE 与普通 JOIN',
|
|
83
|
+
'WITH c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<3) SELECT c1.n, c2.n FROM c c1, c c2 WHERE c2.n = c1.n + 1 ORDER BY c1.n',
|
|
84
|
+
[[1, 2], [2, 3]]);
|
|
85
|
+
await t('递归 CTE 可被外层引用多次',
|
|
86
|
+
'WITH c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<3) SELECT (SELECT COUNT(*) FROM c) AS a, (SELECT MAX(n) FROM c) AS b',
|
|
87
|
+
[[3, 3]]);
|
|
88
|
+
|
|
89
|
+
console.log('\n--- 安全性:无限递归必须有兜底 ---');
|
|
90
|
+
{
|
|
91
|
+
const db = new Database(':memory:');
|
|
92
|
+
const t0 = Date.now();
|
|
93
|
+
let got = null, err = null;
|
|
94
|
+
try {
|
|
95
|
+
// WHERE 恒真 → 无限增长
|
|
96
|
+
got = rowsOf(await executeSQL(db, 'WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c) SELECT COUNT(*) AS k FROM c', OPTS));
|
|
97
|
+
} catch (e) { err = e; }
|
|
98
|
+
ok('无限递归不 OOM、不挂死(有限时间内返回)', err !== null || (got && got[0][0] > 0), err ? String(err.message).slice(0, 60) : got);
|
|
99
|
+
ok('兜底在 30s 内返回', Date.now() - t0 < 30000, ((Date.now() - t0) / 1000).toFixed(1) + 's');
|
|
100
|
+
await db.stop();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('\n--- 非递归 CTE 回归对照 ---');
|
|
104
|
+
await t('普通 CTE 不受影响', 'WITH x AS (SELECT 1 AS n) SELECT * FROM x', [[1]]);
|
|
105
|
+
await t('普通 CTE 引用前一个',
|
|
106
|
+
'WITH a AS (SELECT 1 AS m), b AS (SELECT m+1 AS n FROM a) SELECT * FROM b', [[2]]);
|
|
107
|
+
await t('普通 CTE 显式列名',
|
|
108
|
+
'WITH x(q) AS (SELECT 5) SELECT q FROM x', [[5]]);
|
|
109
|
+
await t('非递归 UNION ALL 正常',
|
|
110
|
+
'SELECT 1 AS n UNION ALL SELECT 2', [[1], [2]]);
|
|
111
|
+
await t('UNION 去重正常', 'SELECT 1 AS n UNION SELECT 1', [[1]]);
|
|
112
|
+
await t('INTERSECT / EXCEPT 正常',
|
|
113
|
+
'SELECT 1 AS n UNION SELECT 2 INTERSECT SELECT 1', [[1]]);
|
|
114
|
+
|
|
115
|
+
console.log('\n--- 三个 engine 一致性 ---');
|
|
116
|
+
for (const eng of ['JSQL', 'NativeJSQL', 'Database']) {
|
|
117
|
+
await t(`${eng} 递归 CTE`, 'WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c WHERE n<4) SELECT * FROM c',
|
|
118
|
+
[[1], [2], [3], [4]], eng);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
122
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
123
|
+
})();
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 保存点测试(SAVEPOINT / ROLLBACK TO SAVEPOINT / RELEASE SAVEPOINT)。
|
|
3
|
+
*
|
|
4
|
+
* 此前这三个语句的状态:
|
|
5
|
+
* - SAVEPOINT 只在引擎上登记一个名字(engine._savepoints 是个 Set)
|
|
6
|
+
* - RELEASE 把名字删掉
|
|
7
|
+
* - ROLLBACK TO 一律抛「engine has no savepoint/snapshot support」
|
|
8
|
+
* 也就是「登记了但完全不能回滚」—— 比报错更糟的是它看起来成功了。
|
|
9
|
+
*
|
|
10
|
+
* 现在在 Database 引擎里实现真正的行级快照。几个容易踩的点:
|
|
11
|
+
* 1. 同一个保存点可以被 ROLLBACK TO 多次,所以还原时必须再深拷贝一次,
|
|
12
|
+
* 否则第二次回滚拿到的是被后续写入改坏的数据
|
|
13
|
+
* 2. 保存点之后新建的表,回滚时要删掉,不然会残留一张当时不存在的表
|
|
14
|
+
* 3. 引擎里有两条事务路径(begin/commit/rollback 与 beginTx/commitTx/rollbackTx),
|
|
15
|
+
* SQL 层走后者 —— 保存点必须对两条都生效
|
|
16
|
+
* 4. wasm / native 引擎的事务在 Rust 侧,还没有保存点:
|
|
17
|
+
* 必须明确报错,不能静默假装成功
|
|
18
|
+
*/
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const ROOT = path.join(__dirname, '..');
|
|
22
|
+
const { Database, JSQL, NativeJSQL } = require(path.join(ROOT, 'index.js'));
|
|
23
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
|
|
24
|
+
|
|
25
|
+
const OPTS = { safety: false };
|
|
26
|
+
let pass = 0, fail = 0;
|
|
27
|
+
|
|
28
|
+
function ok(name, cond, extra) {
|
|
29
|
+
if (cond) { pass++; console.log('[OK] ', name); }
|
|
30
|
+
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 140) : ''); }
|
|
31
|
+
}
|
|
32
|
+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
33
|
+
|
|
34
|
+
async function t(name, fn) {
|
|
35
|
+
const db = new Database(':memory:');
|
|
36
|
+
const exec = (sql) => executeSQL(db, sql, OPTS);
|
|
37
|
+
const rows = async (sql) => (await exec(sql)).rows;
|
|
38
|
+
const mustFail = async (sql) => {
|
|
39
|
+
try { await exec(sql); return false; } catch (_) { return true; }
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
const r = await fn({ db, exec, rows, mustFail });
|
|
43
|
+
ok(name, r === undefined ? true : !!r);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
ok(name, false, e.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
(async () => {
|
|
50
|
+
console.log('\n--- 基本回滚 ---');
|
|
51
|
+
await t('ROLLBACK TO 还原保存点之后的数据', async ({ exec, rows }) => {
|
|
52
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, name TEXT)');
|
|
53
|
+
await exec("INSERT INTO t VALUES (1,'a')");
|
|
54
|
+
await exec('BEGIN');
|
|
55
|
+
await exec("INSERT INTO t VALUES (2,'b')");
|
|
56
|
+
await exec('SAVEPOINT sp1');
|
|
57
|
+
await exec("INSERT INTO t VALUES (3,'c')");
|
|
58
|
+
await exec("UPDATE t SET name = 'B2' WHERE id = 2");
|
|
59
|
+
const before = await rows('SELECT id, name FROM t ORDER BY id');
|
|
60
|
+
await exec('ROLLBACK TO SAVEPOINT sp1');
|
|
61
|
+
const after = await rows('SELECT id, name FROM t ORDER BY id');
|
|
62
|
+
await exec('COMMIT');
|
|
63
|
+
return eq(before, [[1, 'a'], [2, 'B2'], [3, 'c']]) && eq(after, [[1, 'a'], [2, 'b']]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await t('保存点之前的改动会保留', async ({ exec, rows, db }) => {
|
|
67
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
68
|
+
await exec("INSERT INTO t VALUES (1,'base')");
|
|
69
|
+
await exec('BEGIN');
|
|
70
|
+
await exec("UPDATE t SET v = 'before-sp' WHERE id = 1");
|
|
71
|
+
await exec('SAVEPOINT s1');
|
|
72
|
+
await exec("UPDATE t SET v = 'after-sp' WHERE id = 1");
|
|
73
|
+
await exec('ROLLBACK TO s1');
|
|
74
|
+
const r = await rows('SELECT v FROM t WHERE id = 1');
|
|
75
|
+
return db.hasActiveTx() && eq(r, [['before-sp']]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await t('回滚后事务仍然有效,可以继续写并提交', async ({ exec, rows }) => {
|
|
79
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
80
|
+
await exec('BEGIN');
|
|
81
|
+
await exec('SAVEPOINT s1');
|
|
82
|
+
await exec('INSERT INTO t VALUES (1)');
|
|
83
|
+
await exec('ROLLBACK TO s1');
|
|
84
|
+
await exec('INSERT INTO t VALUES (2)');
|
|
85
|
+
await exec('COMMIT');
|
|
86
|
+
return eq(await rows('SELECT id FROM t ORDER BY id'), [[2]]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await t('回滚掉 INSERT 之后自增值也退回', async ({ exec, rows }) => {
|
|
90
|
+
await exec('CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT)');
|
|
91
|
+
await exec('BEGIN');
|
|
92
|
+
await exec('SAVEPOINT s1');
|
|
93
|
+
await exec("INSERT INTO t (v) VALUES ('x')");
|
|
94
|
+
await exec('ROLLBACK TO s1');
|
|
95
|
+
await exec("INSERT INTO t (v) VALUES ('y')");
|
|
96
|
+
await exec('COMMIT');
|
|
97
|
+
return eq(await rows('SELECT id, v FROM t'), [[1, 'y']]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
console.log('\n--- 同一个保存点反复回滚 ---');
|
|
101
|
+
await t('同一保存点可回滚多次且每次都还原到最初状态', async ({ exec, rows }) => {
|
|
102
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
103
|
+
await exec("INSERT INTO t VALUES (1,'orig')");
|
|
104
|
+
await exec('BEGIN');
|
|
105
|
+
await exec('SAVEPOINT s1');
|
|
106
|
+
await exec("UPDATE t SET v = 'first' WHERE id = 1");
|
|
107
|
+
await exec('ROLLBACK TO s1');
|
|
108
|
+
const a = await rows('SELECT v FROM t');
|
|
109
|
+
await exec("UPDATE t SET v = 'second' WHERE id = 1");
|
|
110
|
+
await exec('ROLLBACK TO s1');
|
|
111
|
+
const b = await rows('SELECT v FROM t');
|
|
112
|
+
await exec('COMMIT');
|
|
113
|
+
// 第二次回滚若把快照本身改坏了,这里就会拿到 first/second 而不是 orig
|
|
114
|
+
return eq(a, [['orig']]) && eq(b, [['orig']]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await t('回滚后嵌套对象不受外层引用影响', async ({ exec, rows }) => {
|
|
118
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, j TEXT)');
|
|
119
|
+
await exec('BEGIN');
|
|
120
|
+
await exec('SAVEPOINT s1');
|
|
121
|
+
await exec('COMMIT');
|
|
122
|
+
return eq(await rows('SELECT id FROM t'), []);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
console.log('\n--- 多层保存点 ---');
|
|
126
|
+
await t('回滚到内层保存点不影响外层', async ({ exec, rows }) => {
|
|
127
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
128
|
+
await exec('BEGIN');
|
|
129
|
+
await exec('INSERT INTO t VALUES (1)');
|
|
130
|
+
await exec('SAVEPOINT a');
|
|
131
|
+
await exec('INSERT INTO t VALUES (2)');
|
|
132
|
+
await exec('SAVEPOINT b');
|
|
133
|
+
await exec('INSERT INTO t VALUES (3)');
|
|
134
|
+
await exec('ROLLBACK TO b');
|
|
135
|
+
const n1 = await rows('SELECT id FROM t ORDER BY id');
|
|
136
|
+
await exec('COMMIT');
|
|
137
|
+
return eq(n1, [[1], [2]]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await t('回滚到外层会丢弃内层保存点', async ({ exec, rows, db }) => {
|
|
141
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
142
|
+
await exec('BEGIN');
|
|
143
|
+
await exec('INSERT INTO t VALUES (1)');
|
|
144
|
+
await exec('SAVEPOINT a');
|
|
145
|
+
await exec('INSERT INTO t VALUES (2)');
|
|
146
|
+
await exec('SAVEPOINT b');
|
|
147
|
+
await exec('INSERT INTO t VALUES (3)');
|
|
148
|
+
const stacked = db.listSavepoints();
|
|
149
|
+
await exec('ROLLBACK TO a');
|
|
150
|
+
const left = db.listSavepoints();
|
|
151
|
+
const n = await rows('SELECT id FROM t ORDER BY id');
|
|
152
|
+
await exec('COMMIT');
|
|
153
|
+
return eq(stacked, ['a', 'b']) && eq(left, ['a']) && eq(n, [[1]]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await t('同名保存点覆盖旧的(并作废其后的)', async ({ exec, db, rows }) => {
|
|
157
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
158
|
+
await exec('BEGIN');
|
|
159
|
+
await exec('SAVEPOINT s');
|
|
160
|
+
await exec('INSERT INTO t VALUES (1)');
|
|
161
|
+
await exec('SAVEPOINT s'); // 同名覆盖,起点变成「已有 1」
|
|
162
|
+
await exec('INSERT INTO t VALUES (2)');
|
|
163
|
+
const names = db.listSavepoints();
|
|
164
|
+
await exec('ROLLBACK TO s');
|
|
165
|
+
const n = await rows('SELECT id FROM t ORDER BY id');
|
|
166
|
+
await exec('COMMIT');
|
|
167
|
+
return eq(names, ['s']) && eq(n, [[1]]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
console.log('\n--- RELEASE ---');
|
|
171
|
+
await t('RELEASE 不还原数据,只作废保存点', async ({ exec, rows, db, mustFail }) => {
|
|
172
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
173
|
+
await exec("INSERT INTO t VALUES (1,'x')");
|
|
174
|
+
await exec('BEGIN');
|
|
175
|
+
await exec('SAVEPOINT s1');
|
|
176
|
+
await exec("UPDATE t SET v = 'y' WHERE id = 1");
|
|
177
|
+
await exec('RELEASE SAVEPOINT s1');
|
|
178
|
+
const kept = await rows('SELECT v FROM t');
|
|
179
|
+
const names = db.listSavepoints();
|
|
180
|
+
const rolledBack = await mustFail('ROLLBACK TO SAVEPOINT s1');
|
|
181
|
+
await exec('COMMIT');
|
|
182
|
+
return eq(kept, [['y']]) && eq(names, []) && rolledBack;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await t('RELEASE 外层会一并作废内层', async ({ exec, db }) => {
|
|
186
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
187
|
+
await exec('BEGIN');
|
|
188
|
+
await exec('SAVEPOINT a');
|
|
189
|
+
await exec('SAVEPOINT b');
|
|
190
|
+
await exec('RELEASE SAVEPOINT a');
|
|
191
|
+
const names = db.listSavepoints();
|
|
192
|
+
await exec('COMMIT');
|
|
193
|
+
return eq(names, []);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
console.log('\n--- 生命周期 ---');
|
|
197
|
+
await t('COMMIT 释放全部保存点', async ({ exec, db }) => {
|
|
198
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
199
|
+
await exec('BEGIN');
|
|
200
|
+
await exec('SAVEPOINT a');
|
|
201
|
+
await exec('SAVEPOINT b');
|
|
202
|
+
await exec('COMMIT');
|
|
203
|
+
return eq(db.listSavepoints(), []);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await t('ROLLBACK 释放全部保存点', async ({ exec, db }) => {
|
|
207
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
208
|
+
await exec('BEGIN');
|
|
209
|
+
await exec('SAVEPOINT a');
|
|
210
|
+
await exec('ROLLBACK');
|
|
211
|
+
return eq(db.listSavepoints(), []);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await t('保存点不跨事务存活', async ({ exec, db, mustFail }) => {
|
|
215
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
216
|
+
await exec('BEGIN');
|
|
217
|
+
await exec('SAVEPOINT a');
|
|
218
|
+
await exec('COMMIT');
|
|
219
|
+
await exec('BEGIN');
|
|
220
|
+
const stillThere = await mustFail('ROLLBACK TO SAVEPOINT a');
|
|
221
|
+
const names = db.listSavepoints();
|
|
222
|
+
await exec('ROLLBACK');
|
|
223
|
+
return stillThere && eq(names, []);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
console.log('\n--- 保存点之后新建的表 ---');
|
|
227
|
+
await t('回滚会删掉保存点之后建的表', async ({ exec, db }) => {
|
|
228
|
+
await exec('CREATE TABLE exist (id INT PRIMARY KEY)');
|
|
229
|
+
await exec('BEGIN');
|
|
230
|
+
await exec('SAVEPOINT s1');
|
|
231
|
+
await exec('CREATE TABLE created_after (id INT PRIMARY KEY)');
|
|
232
|
+
const during = (await db.getTables()).slice().sort();
|
|
233
|
+
await exec('ROLLBACK TO s1');
|
|
234
|
+
const after = (await db.getTables()).slice().sort();
|
|
235
|
+
await exec('COMMIT');
|
|
236
|
+
return eq(during, ['created_after', 'exist']) && eq(after, ['exist']);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await t('回滚不会动保存点之前就存在的表', async ({ exec, rows }) => {
|
|
240
|
+
await exec('CREATE TABLE keep (id INT PRIMARY KEY, v TEXT)');
|
|
241
|
+
await exec("INSERT INTO keep VALUES (1,'k')");
|
|
242
|
+
await exec('BEGIN');
|
|
243
|
+
await exec('SAVEPOINT s1');
|
|
244
|
+
await exec('CREATE TABLE tmp (id INT PRIMARY KEY)');
|
|
245
|
+
await exec('ROLLBACK TO s1');
|
|
246
|
+
const r = await rows('SELECT id, v FROM keep');
|
|
247
|
+
await exec('COMMIT');
|
|
248
|
+
return eq(r, [[1, 'k']]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
console.log('\n--- 错误处理 ---');
|
|
252
|
+
await t('无事务时 SAVEPOINT 报错且说明原因', async ({ exec }) => {
|
|
253
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
254
|
+
try { await exec('SAVEPOINT s1'); return false; }
|
|
255
|
+
catch (e) { return /active transaction|BEGIN/i.test(e.message); }
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await t('无事务时 ROLLBACK TO 报错', async ({ exec }) => {
|
|
259
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
260
|
+
try { await exec('ROLLBACK TO SAVEPOINT s1'); return false; }
|
|
261
|
+
catch (e) { return /active transaction/i.test(e.message); }
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await t('无事务时 RELEASE 报错', async ({ exec }) => {
|
|
265
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
266
|
+
try { await exec('RELEASE SAVEPOINT s1'); return false; }
|
|
267
|
+
catch (e) { return /active transaction/i.test(e.message); }
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await t('回滚到不存在的保存点会报错,并列出已有的', async ({ exec }) => {
|
|
271
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY)');
|
|
272
|
+
await exec('BEGIN');
|
|
273
|
+
await exec('SAVEPOINT alpha');
|
|
274
|
+
let msg = '';
|
|
275
|
+
try { await exec('ROLLBACK TO SAVEPOINT nope'); } catch (e) { msg = e.message; }
|
|
276
|
+
await exec('ROLLBACK');
|
|
277
|
+
return /nope/.test(msg) && /alpha/.test(msg);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await t('回滚到不存在的保存点不会改坏数据', async ({ exec, rows }) => {
|
|
281
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
282
|
+
await exec("INSERT INTO t VALUES (1,'keep')");
|
|
283
|
+
await exec('BEGIN');
|
|
284
|
+
await exec('SAVEPOINT s1');
|
|
285
|
+
await exec("UPDATE t SET v = 'changed' WHERE id = 1");
|
|
286
|
+
try { await exec('ROLLBACK TO SAVEPOINT bogus'); } catch (_) { /* 预期 */ }
|
|
287
|
+
const r = await rows('SELECT v FROM t');
|
|
288
|
+
await exec('ROLLBACK');
|
|
289
|
+
return eq(r, [['changed']]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await t('不区分大小写', async ({ exec, rows }) => {
|
|
293
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
294
|
+
await exec("INSERT INTO t VALUES (1,'a')");
|
|
295
|
+
await exec('BEGIN');
|
|
296
|
+
await exec('savepoint SpOne');
|
|
297
|
+
await exec("UPDATE t SET v = 'b' WHERE id = 1");
|
|
298
|
+
await exec('rollback to savepoint SPONE');
|
|
299
|
+
const r = await rows('SELECT v FROM t');
|
|
300
|
+
await exec('COMMIT');
|
|
301
|
+
return eq(r, [['a']]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await t('ROLLBACK TO 可以省略 SAVEPOINT 关键字', async ({ exec, rows }) => {
|
|
305
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
306
|
+
await exec("INSERT INTO t VALUES (1,'a')");
|
|
307
|
+
await exec('BEGIN');
|
|
308
|
+
await exec('SAVEPOINT s1');
|
|
309
|
+
await exec("UPDATE t SET v = 'b' WHERE id = 1");
|
|
310
|
+
await exec('ROLLBACK TO s1');
|
|
311
|
+
const r = await rows('SELECT v FROM t');
|
|
312
|
+
await exec('COMMIT');
|
|
313
|
+
return eq(r, [['a']]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
console.log('\n--- 直连 API(begin/commit/rollback 路径)---');
|
|
317
|
+
await t('db.begin() 路径下保存点同样有效', async ({ db }) => {
|
|
318
|
+
const exec = (sql) => executeSQL(db, sql, OPTS);
|
|
319
|
+
const rows = async (sql) => (await exec(sql)).rows;
|
|
320
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
321
|
+
await exec("INSERT INTO t VALUES (1,'a')");
|
|
322
|
+
db.begin('REPEATABLE_READ');
|
|
323
|
+
await exec("UPDATE t SET v = 'b' WHERE id = 1");
|
|
324
|
+
db.savepoint('sp');
|
|
325
|
+
await exec("UPDATE t SET v = 'c' WHERE id = 1");
|
|
326
|
+
db.rollbackTo('sp');
|
|
327
|
+
const during = await rows('SELECT v FROM t');
|
|
328
|
+
db.commit();
|
|
329
|
+
const after = await rows('SELECT v FROM t');
|
|
330
|
+
return eq(during, [['b']]) && eq(after, [['b']]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await t('db.begin() 下 ROLLBACK 仍能整体回滚', async ({ db }) => {
|
|
334
|
+
const exec = (sql) => executeSQL(db, sql, OPTS);
|
|
335
|
+
const rows = async (sql) => (await exec(sql)).rows;
|
|
336
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
337
|
+
await exec("INSERT INTO t VALUES (1,'orig')");
|
|
338
|
+
db.begin('REPEATABLE_READ');
|
|
339
|
+
await exec('SAVEPOINT s1');
|
|
340
|
+
await exec("UPDATE t SET v = 'x' WHERE id = 1");
|
|
341
|
+
db.rollbackTo('s1');
|
|
342
|
+
await exec("UPDATE t SET v = 'y' WHERE id = 1");
|
|
343
|
+
db.rollback();
|
|
344
|
+
return eq(await rows('SELECT v FROM t'), [['orig']]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await t('READ_COMMITTED 下保存点也能精确还原', async ({ db }) => {
|
|
348
|
+
const exec = (sql) => executeSQL(db, sql, OPTS);
|
|
349
|
+
const rows = async (sql) => (await exec(sql)).rows;
|
|
350
|
+
await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
|
|
351
|
+
await exec("INSERT INTO t VALUES (1,'a')");
|
|
352
|
+
db.begin('READ_COMMITTED');
|
|
353
|
+
await exec('SAVEPOINT s1');
|
|
354
|
+
await exec("UPDATE t SET v = 'b' WHERE id = 1");
|
|
355
|
+
db.rollbackTo('s1');
|
|
356
|
+
const r = await rows('SELECT v FROM t');
|
|
357
|
+
db.commit();
|
|
358
|
+
return eq(r, [['a']]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
console.log('\n--- 其他引擎:明确报错而不是假装成功 ---');
|
|
362
|
+
const others = [
|
|
363
|
+
['JSQL(wasm)', () => new JSQL({ mode: 'memory' })],
|
|
364
|
+
['NativeJSQL', () => new NativeJSQL({ mode: 'memory' })],
|
|
365
|
+
];
|
|
366
|
+
for (const [name, mk] of others) {
|
|
367
|
+
let db = null;
|
|
368
|
+
try {
|
|
369
|
+
db = await mk();
|
|
370
|
+
if (db.start) await db.start();
|
|
371
|
+
await executeSQL(db, 'CREATE TABLE t (id INT PRIMARY KEY)', OPTS);
|
|
372
|
+
await executeSQL(db, 'BEGIN', OPTS);
|
|
373
|
+
let threw = false;
|
|
374
|
+
let msg = '';
|
|
375
|
+
try { await executeSQL(db, 'SAVEPOINT s1', OPTS); }
|
|
376
|
+
catch (e) { threw = true; msg = e.message; }
|
|
377
|
+
ok(`${name} SAVEPOINT 明确报错而不是静默成功`, threw && /savepoint/i.test(msg), msg.slice(0, 90));
|
|
378
|
+
} catch (e) {
|
|
379
|
+
// 引擎不可用(缺原生二进制)时跳过,不算失败
|
|
380
|
+
ok(`${name} 不可用,跳过(${String(e.message).slice(0, 40)})`, true);
|
|
381
|
+
} finally {
|
|
382
|
+
try { if (db && db.stop) await db.stop(); } catch (_) { /* ignore */ }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
387
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
388
|
+
})().catch(e => { console.error('💥', e); process.exit(1); });
|
package/test/sql-parser.test.js
CHANGED
|
@@ -460,14 +460,39 @@ const rowsOf = (r) => (Array.isArray(r) ? r[0] : r).rows;
|
|
|
460
460
|
const skippedDrop = await executeSQL(e9, 'DROP VIEW IF EXISTS nope', OPTS);
|
|
461
461
|
ok('DROP VIEW IF EXISTS 不报错', skippedDrop && skippedDrop.skipped === true);
|
|
462
462
|
|
|
463
|
-
//
|
|
463
|
+
// 保存点:引擎不支持时必须明确报错,绝不静默假装成功。
|
|
464
|
+
//
|
|
465
|
+
// 此前 SAVEPOINT 只是往 engine._savepoints 里登记个名字就返回成功,
|
|
466
|
+
// 调用方以为保存点建好了,真要回滚时才发现根本不能用 —— 比报错更糟。
|
|
467
|
+
// 现在 SAVEPOINT / ROLLBACK TO / RELEASE 都会先确认引擎有实现。
|
|
464
468
|
const e10 = mkEngine();
|
|
465
|
-
|
|
466
|
-
|
|
469
|
+
let spErr = null;
|
|
470
|
+
try { await executeSQL(e10, 'SAVEPOINT sp1', OPTS); } catch (err) { spErr = err; }
|
|
471
|
+
ok('不支持的引擎上 SAVEPOINT 明确报错(不静默假成功)',
|
|
472
|
+
spErr instanceof Error && /savepoint/i.test(spErr.message));
|
|
473
|
+
|
|
467
474
|
let rbErr = null;
|
|
468
475
|
try { await executeSQL(e10, 'ROLLBACK TO SAVEPOINT sp1', OPTS); } catch (err) { rbErr = err; }
|
|
469
|
-
ok('ROLLBACK TO
|
|
470
|
-
rbErr instanceof Error && /
|
|
476
|
+
ok('不支持的引擎上 ROLLBACK TO 明确报错',
|
|
477
|
+
rbErr instanceof Error && /savepoint/i.test(rbErr.message));
|
|
478
|
+
|
|
479
|
+
let relErr = null;
|
|
480
|
+
try { await executeSQL(e10, 'RELEASE SAVEPOINT sp1', OPTS); } catch (err) { relErr = err; }
|
|
481
|
+
ok('不支持的引擎上 RELEASE 明确报错',
|
|
482
|
+
relErr instanceof Error && /savepoint/i.test(relErr.message));
|
|
483
|
+
|
|
484
|
+
// 而内置的 Database 引擎是真的能回滚
|
|
485
|
+
const { Database } = require('../index.js');
|
|
486
|
+
const dbe = new Database(':memory:');
|
|
487
|
+
await executeSQL(dbe, 'CREATE TABLE sp_t (id INT PRIMARY KEY, v TEXT)', OPTS);
|
|
488
|
+
await executeSQL(dbe, "INSERT INTO sp_t VALUES (1,'a')", OPTS);
|
|
489
|
+
await executeSQL(dbe, 'BEGIN', OPTS);
|
|
490
|
+
await executeSQL(dbe, 'SAVEPOINT s1', OPTS);
|
|
491
|
+
await executeSQL(dbe, "UPDATE sp_t SET v = 'b' WHERE id = 1", OPTS);
|
|
492
|
+
await executeSQL(dbe, 'ROLLBACK TO SAVEPOINT s1', OPTS);
|
|
493
|
+
const spRows = rowsOf(await executeSQL(dbe, 'SELECT v FROM sp_t', OPTS));
|
|
494
|
+
await executeSQL(dbe, 'COMMIT', OPTS);
|
|
495
|
+
ok('Database 引擎的保存点真的能回滚', JSON.stringify(spRows) === '[["a"]]', spRows);
|
|
471
496
|
}
|
|
472
497
|
|
|
473
498
|
/* ================= BUG 修复: 静默算错的标量函数(对齐 MySQL 8.0) ================= */
|