jsql-neo 6.3.3 → 6.3.5
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 +60 -20
- package/index.js +73 -6
- package/lib/btree.js +5 -41
- package/lib/database.js +276 -35
- package/lib/native_client.js +57 -21
- package/lib/sql.js +304 -25
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +8 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +274 -178
- package/package.json +6 -5
- package/test/dml-constraints.test.js +163 -0
- package/test/mysql-compat-hijack.test.js +87 -0
- package/test/native-multi-instance.test.js +139 -0
- package/test/readme-audit.test.js +25 -0
- package/test/sql-parser.test.js +189 -0
- package/test/wal-recovery.test.js +161 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WAL 崩溃恢复回归测试(P1-1)
|
|
3
|
+
*
|
|
4
|
+
* 背景:6.3.3 及以前的 _recoverFromWAL() 只做 fs.unlinkSync 删除 WAL 文件,
|
|
5
|
+
* 从不回放任何操作;且构造函数用 fs.existsSync(dataFile) 短路,
|
|
6
|
+
* 新建库崩溃后连 WAL 都不会被读取。合起来的效果是:
|
|
7
|
+
* 「WAL / 崩溃恢复」是虚假宣传 —— 实测开启 wal:true 后 kill -9 必然丢数据。
|
|
8
|
+
*
|
|
9
|
+
* 6.3.4 修复:
|
|
10
|
+
* 1. WAL 改为 append-only JSONL + fsync
|
|
11
|
+
* 2. insert/update/removeById 现在都写 WAL
|
|
12
|
+
* 3. _recoverFromWAL 真正按 sequence 顺序回放
|
|
13
|
+
* 4. 构造函数在「数据文件不存在但 WAL 存在」时也走恢复流程
|
|
14
|
+
* 5. .jsql 二进制格式分支不再提前 return 跳过恢复
|
|
15
|
+
*
|
|
16
|
+
* 本测试用「不调用 save() 直接丢弃实例」模拟 kill -9。
|
|
17
|
+
*/
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const Database = require('../lib/database.js');
|
|
22
|
+
|
|
23
|
+
let pass = 0, fail = 0;
|
|
24
|
+
function ok(name, cond, extra) {
|
|
25
|
+
if (cond) { pass++; console.log('[OK] ' + name); }
|
|
26
|
+
else { fail++; console.log('[FAIL] ' + name + (extra !== undefined ? ' → ' + JSON.stringify(extra) : '')); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tmpFile(tag) {
|
|
30
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-wal-' + tag + '-'));
|
|
31
|
+
return { dir, file: path.join(dir, 'db.jsql') };
|
|
32
|
+
}
|
|
33
|
+
const SCHEMA = { id: { type: 'integer', primaryKey: true }, v: { type: 'string' }, n: { type: 'integer' } };
|
|
34
|
+
|
|
35
|
+
(async () => {
|
|
36
|
+
console.log('--- 1. 全新库崩溃:WAL 必须恢复出全部数据 ---');
|
|
37
|
+
{
|
|
38
|
+
const { dir, file } = tmpFile('new');
|
|
39
|
+
const db = new Database(file, { wal: true, autoSave: false });
|
|
40
|
+
await db.createTable('t', SCHEMA);
|
|
41
|
+
await db.insert('t', { id: 1, v: 'important' });
|
|
42
|
+
await db.insert('t', { id: 2, v: 'second' });
|
|
43
|
+
await db.updateById('t', 1, { v: 'updated' });
|
|
44
|
+
await db.insert('t', { id: 3, v: 'third' });
|
|
45
|
+
await db.removeById('t', 3);
|
|
46
|
+
// 模拟 kill -9:直接丢弃,不 save 不 stop
|
|
47
|
+
const db2 = new Database(file, { wal: true });
|
|
48
|
+
const rows = await db2.find('t', {});
|
|
49
|
+
ok('崩溃后表仍存在', Object.keys(db2._tables).includes('t'));
|
|
50
|
+
ok('崩溃后行数正确 (2)', rows.length === 2, rows.length);
|
|
51
|
+
ok('崩溃后 update 已回放', rows.find(r => r.id === 1)?.v === 'updated', rows);
|
|
52
|
+
ok('崩溃后第二行存在', rows.find(r => r.id === 2)?.v === 'second');
|
|
53
|
+
ok('崩溃后 delete 已回放', !rows.find(r => r.id === 3));
|
|
54
|
+
ok('回放后 WAL 已检查点', !fs.existsSync(file + '.wal'));
|
|
55
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('--- 2. 快照 + WAL 叠加:save 后的新写入也要恢复 ---');
|
|
59
|
+
{
|
|
60
|
+
const { dir, file } = tmpFile('mix');
|
|
61
|
+
const db = new Database(file, { wal: true, autoSave: false });
|
|
62
|
+
await db.createTable('t', SCHEMA);
|
|
63
|
+
await db.insert('t', { id: 1, v: 'a' });
|
|
64
|
+
await db.insert('t', { id: 2, v: 'b' });
|
|
65
|
+
db.save(); // 快照:2 行,WAL 应清空
|
|
66
|
+
ok('save 后 WAL 被清空', !fs.existsSync(file + '.wal'));
|
|
67
|
+
await db.insert('t', { id: 3, v: 'c-after-save' }); // 只进 WAL
|
|
68
|
+
const db2 = new Database(file, { wal: true });
|
|
69
|
+
const rows = await db2.find('t', {});
|
|
70
|
+
ok('快照 2 行 + WAL 1 行 = 3 行', rows.length === 3, rows.length);
|
|
71
|
+
ok('WAL 中的新行已恢复', rows.find(r => r.id === 3)?.v === 'c-after-save');
|
|
72
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('--- 3. 幂等:二次重开不得重复回放 ---');
|
|
76
|
+
{
|
|
77
|
+
const { dir, file } = tmpFile('idem');
|
|
78
|
+
const db = new Database(file, { wal: true, autoSave: false });
|
|
79
|
+
await db.createTable('t', SCHEMA);
|
|
80
|
+
await db.insert('t', { id: 1, v: 'x' });
|
|
81
|
+
await db.insert('t', { id: 2, v: 'y' });
|
|
82
|
+
const db2 = new Database(file, { wal: true });
|
|
83
|
+
const r2 = await db2.find('t', {});
|
|
84
|
+
const db3 = new Database(file, { wal: true });
|
|
85
|
+
const r3 = await db3.find('t', {});
|
|
86
|
+
ok('首次恢复 2 行', r2.length === 2, r2.length);
|
|
87
|
+
ok('二次重开仍 2 行(不重复回放)', r3.length === 2, r3.length);
|
|
88
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('--- 4. WAL 为 append-only JSONL,崩溃写半的尾行可容错 ---');
|
|
92
|
+
{
|
|
93
|
+
const { dir, file } = tmpFile('torn');
|
|
94
|
+
const db = new Database(file, { wal: true, autoSave: false });
|
|
95
|
+
await db.createTable('t', SCHEMA);
|
|
96
|
+
await db.insert('t', { id: 1, v: 'ok' });
|
|
97
|
+
// 模拟崩溃时最后一行只写了一半
|
|
98
|
+
fs.appendFileSync(file + '.wal', '{"op":"insert","table":"t","ro');
|
|
99
|
+
const db2 = new Database(file, { wal: true });
|
|
100
|
+
const rows = await db2.find('t', {});
|
|
101
|
+
ok('半截尾行被丢弃,完整记录仍恢复', rows.length === 1, rows.length);
|
|
102
|
+
ok('数据内容正确', rows[0] && rows[0].v === 'ok', rows);
|
|
103
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('--- 5. deleteById / dropTable 也进 WAL ---');
|
|
107
|
+
{
|
|
108
|
+
const { dir, file } = tmpFile('del');
|
|
109
|
+
const db = new Database(file, { wal: true, autoSave: false });
|
|
110
|
+
await db.createTable('t', SCHEMA);
|
|
111
|
+
await db.createTable('u', SCHEMA);
|
|
112
|
+
await db.insert('t', { id: 1, v: 'a' });
|
|
113
|
+
await db.insert('u', { id: 1, v: 'b' });
|
|
114
|
+
await db.dropTable('u');
|
|
115
|
+
const db2 = new Database(file, { wal: true });
|
|
116
|
+
ok('被 drop 的表未复活', !db2._tables.u, Object.keys(db2._tables));
|
|
117
|
+
ok('保留的表数据完整', (await db2.find('t', {})).length === 1);
|
|
118
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('--- 6. 未开启 wal 时不应产生 .wal 文件 ---');
|
|
122
|
+
{
|
|
123
|
+
const { dir, file } = tmpFile('off');
|
|
124
|
+
const db = new Database(file, { wal: false, autoSave: false });
|
|
125
|
+
await db.createTable('t', SCHEMA);
|
|
126
|
+
await db.insert('t', { id: 1, v: 'a' });
|
|
127
|
+
ok('未开启 wal 时无 .wal 文件', !fs.existsSync(file + '.wal'));
|
|
128
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('--- 6b. 兼容 6.3.3 的旧格式 WAL(单行 JSON 数组)---');
|
|
132
|
+
{
|
|
133
|
+
// 升级场景:用户本地残留旧版写出的数组格式 WAL,不得被静默丢弃
|
|
134
|
+
const { dir, file } = tmpFile('oldfmt');
|
|
135
|
+
fs.writeFileSync(file + '.wal', JSON.stringify([
|
|
136
|
+
{ op: 'createTable', table: 'legacy', schema: SCHEMA },
|
|
137
|
+
]));
|
|
138
|
+
const db = new Database(file, { wal: true });
|
|
139
|
+
ok('旧格式数组 WAL 能被回放', !!db._tables.legacy, Object.keys(db._tables));
|
|
140
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('--- 7. 回放后索引仍可用(B-Tree 重建)---');
|
|
144
|
+
{
|
|
145
|
+
const { dir, file } = tmpFile('idx');
|
|
146
|
+
const db = new Database(file, { wal: true, autoSave: false });
|
|
147
|
+
await db.createTable('t', SCHEMA);
|
|
148
|
+
db.createTable && 0;
|
|
149
|
+
for (let i = 1; i <= 30; i++) await db.insert('t', { id: i, v: 'v' + i, n: i });
|
|
150
|
+
db._tables.t.createIndex('n');
|
|
151
|
+
const db2 = new Database(file, { wal: true });
|
|
152
|
+
const found = await db2.find('t', { n: 17 });
|
|
153
|
+
ok('回放后按索引字段查询正确', found.length === 1 && found[0].id === 17, found);
|
|
154
|
+
const cnt = await db2.find('t', { n: { $gte: 25 } });
|
|
155
|
+
ok('回放后范围查询正确', cnt.length === 6, cnt.length);
|
|
156
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
160
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
161
|
+
})().catch(e => { console.error('FATAL', e); process.exit(1); });
|