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,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DML 与表约束回归测试。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖两类曾静默出错的问题:
|
|
5
|
+
* 1. 无主键表批量删除删错行 —— 存储层把 id 当 1-based 行号,
|
|
6
|
+
* 边解析边删会让后续行号整体左移(DELETE ... WHERE id <= 2
|
|
7
|
+
* 在 5 行表上只删掉 1 行,且留下的是第 2 行)。
|
|
8
|
+
* 2. UPDATE 改主键/唯一列不校验冲突 —— 直接 Object.assign,
|
|
9
|
+
* 静默产生两行同主键(MySQL / SQLite 都会报 UNIQUE constraint failed)。
|
|
10
|
+
*
|
|
11
|
+
* 每个用例独立建库,避免状态污染。
|
|
12
|
+
*/
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const ROOT = path.join(__dirname, '..');
|
|
16
|
+
const { Database } = require(path.join(ROOT, 'index.js'));
|
|
17
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
|
|
18
|
+
|
|
19
|
+
const OPTS = { safety: false };
|
|
20
|
+
let pass = 0, fail = 0;
|
|
21
|
+
|
|
22
|
+
function ok(name, cond, extra) {
|
|
23
|
+
if (cond) { pass++; console.log('[OK] ', name); }
|
|
24
|
+
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
|
|
25
|
+
}
|
|
26
|
+
const rowsOf = r => (Array.isArray(r) ? r[0] : r).rows;
|
|
27
|
+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
28
|
+
|
|
29
|
+
/** 跑一个用例:每次给全新的 :memory: 库 */
|
|
30
|
+
async function t(name, fn) {
|
|
31
|
+
const db = new Database(':memory:');
|
|
32
|
+
try {
|
|
33
|
+
const r = await fn({
|
|
34
|
+
exec: (sql) => executeSQL(db, sql, OPTS),
|
|
35
|
+
rows: (sql) => executeSQL(db, sql, OPTS).then(rowsOf),
|
|
36
|
+
scalar: (sql) => executeSQL(db, sql, OPTS).then(r => rowsOf(r)[0][0]),
|
|
37
|
+
mustFail: async (sql) => {
|
|
38
|
+
try { await executeSQL(db, sql, OPTS); return '本该报错却成功了'; }
|
|
39
|
+
catch (e) { return true; }
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (r === true) ok(name, true);
|
|
43
|
+
else ok(name, false, r);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
ok(name, false, String(e.message || e).slice(0, 120));
|
|
46
|
+
} finally {
|
|
47
|
+
try { await db.stop(); } catch (_) { /* ignore */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
(async () => {
|
|
52
|
+
console.log('\n--- 无主键表:批量删除曾删错行 ---');
|
|
53
|
+
await t('无主键 DELETE 多个连续行', async ({ exec, rows }) => {
|
|
54
|
+
await exec(`CREATE TABLE t (id INT)`);
|
|
55
|
+
await exec(`INSERT INTO t VALUES (1),(2),(3),(4),(5)`);
|
|
56
|
+
await exec(`DELETE FROM t WHERE id <= 2`);
|
|
57
|
+
return eq(await rows(`SELECT id FROM t ORDER BY id`), [[3], [4], [5]]);
|
|
58
|
+
});
|
|
59
|
+
await t('无主键 DELETE IN 列表', async ({ exec, rows }) => {
|
|
60
|
+
await exec(`CREATE TABLE t (id INT)`);
|
|
61
|
+
await exec(`INSERT INTO t VALUES (1),(2),(3),(4),(5)`);
|
|
62
|
+
await exec(`DELETE FROM t WHERE id IN (1,4)`);
|
|
63
|
+
return eq(await rows(`SELECT id FROM t ORDER BY id`), [[2], [3], [5]]);
|
|
64
|
+
});
|
|
65
|
+
await t('无主键 DELETE 单行(回归对照)', async ({ exec, rows }) => {
|
|
66
|
+
await exec(`CREATE TABLE t (id INT)`);
|
|
67
|
+
await exec(`INSERT INTO t VALUES (1),(2),(3)`);
|
|
68
|
+
await exec(`DELETE FROM t WHERE id = 2`);
|
|
69
|
+
return eq(await rows(`SELECT id FROM t ORDER BY id`), [[1], [3]]);
|
|
70
|
+
});
|
|
71
|
+
await t('无主键 DELETE 全部', async ({ exec, rows }) => {
|
|
72
|
+
await exec(`CREATE TABLE t (id INT)`);
|
|
73
|
+
await exec(`INSERT INTO t VALUES (1),(2),(3)`);
|
|
74
|
+
await exec(`DELETE FROM t`);
|
|
75
|
+
return eq(await rows(`SELECT id FROM t`), []);
|
|
76
|
+
});
|
|
77
|
+
await t('有主键 DELETE 多个(回归对照)', async ({ exec, rows }) => {
|
|
78
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY)`);
|
|
79
|
+
await exec(`INSERT INTO t VALUES (1),(2),(3),(4),(5)`);
|
|
80
|
+
await exec(`DELETE FROM t WHERE id <= 2`);
|
|
81
|
+
return eq(await rows(`SELECT id FROM t ORDER BY id`), [[3], [4], [5]]);
|
|
82
|
+
});
|
|
83
|
+
await t('无主键 DELETE 后行数正确', async ({ exec, scalar }) => {
|
|
84
|
+
await exec(`CREATE TABLE t (id INT)`);
|
|
85
|
+
await exec(`INSERT INTO t VALUES (1),(2),(3)`);
|
|
86
|
+
const r = await exec(`DELETE FROM t WHERE id > 1`);
|
|
87
|
+
if (r.affectedRows !== 2) return { affectedRows: r.affectedRows, want: 2 };
|
|
88
|
+
return eq(await scalar(`SELECT COUNT(*) AS c FROM t`), 1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log('\n--- UPDATE 改主键 / 唯一列曾不校验冲突 ---');
|
|
92
|
+
await t('UPDATE 主键成已存在值 → 报错', async ({ exec, mustFail }) => {
|
|
93
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, v INT)`);
|
|
94
|
+
await exec(`INSERT INTO t VALUES (1,10),(2,20)`);
|
|
95
|
+
return mustFail(`UPDATE t SET id = 1 WHERE id = 2`);
|
|
96
|
+
});
|
|
97
|
+
await t('UPDATE 主键冲突后数据未损坏', async ({ exec, rows }) => {
|
|
98
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, v INT)`);
|
|
99
|
+
await exec(`INSERT INTO t VALUES (1,10),(2,20),(3,30)`);
|
|
100
|
+
try { await exec(`UPDATE t SET id = 1 WHERE id = 2`); } catch (_) { /* expected */ }
|
|
101
|
+
return eq(await rows(`SELECT id, v FROM t ORDER BY id`), [[1, 10], [2, 20], [3, 30]]);
|
|
102
|
+
});
|
|
103
|
+
await t('UPDATE 主键成不冲突值 → 成功', async ({ exec, rows }) => {
|
|
104
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, v INT)`);
|
|
105
|
+
await exec(`INSERT INTO t VALUES (1,10),(2,20),(3,30)`);
|
|
106
|
+
await exec(`UPDATE t SET id = 9 WHERE id = 2`);
|
|
107
|
+
return eq(await rows(`SELECT id, v FROM t ORDER BY id`), [[1, 10], [3, 30], [9, 20]]);
|
|
108
|
+
});
|
|
109
|
+
await t('UPDATE 主键为自身值 → 允许', async ({ exec, scalar }) => {
|
|
110
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, v INT)`);
|
|
111
|
+
await exec(`INSERT INTO t VALUES (1,10),(9,20)`);
|
|
112
|
+
await exec(`UPDATE t SET id = 9 WHERE id = 9`);
|
|
113
|
+
return eq(await scalar(`SELECT COUNT(*) AS c FROM t`), 2);
|
|
114
|
+
});
|
|
115
|
+
await t('UPDATE UNIQUE 列成已存在值 → 报错', async ({ exec, mustFail }) => {
|
|
116
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, e TEXT UNIQUE)`);
|
|
117
|
+
await exec(`INSERT INTO t VALUES (1,'a'),(2,'b')`);
|
|
118
|
+
return mustFail(`UPDATE t SET e = 'a' WHERE id = 2`);
|
|
119
|
+
});
|
|
120
|
+
await t('UPDATE UNIQUE 列成不冲突值 → 成功', async ({ exec, rows }) => {
|
|
121
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, e TEXT UNIQUE)`);
|
|
122
|
+
await exec(`INSERT INTO t VALUES (1,'a'),(2,'b')`);
|
|
123
|
+
await exec(`UPDATE t SET e = 'c' WHERE id = 2`);
|
|
124
|
+
return eq(await rows(`SELECT id, e FROM t ORDER BY id`), [[1, 'a'], [2, 'c']]);
|
|
125
|
+
});
|
|
126
|
+
await t('UPDATE 非约束列不受影响', async ({ exec, rows }) => {
|
|
127
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, v INT)`);
|
|
128
|
+
await exec(`INSERT INTO t VALUES (1,10),(2,20)`);
|
|
129
|
+
await exec(`UPDATE t SET v = 99 WHERE id = 1`);
|
|
130
|
+
return eq(await rows(`SELECT id, v FROM t ORDER BY id`), [[1, 99], [2, 20]]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log('\n--- 既有约束行为回归对照 ---');
|
|
134
|
+
await t('INSERT 主键重复 → 报错', async ({ exec, mustFail }) => {
|
|
135
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY)`);
|
|
136
|
+
await exec(`INSERT INTO t VALUES (1)`);
|
|
137
|
+
return mustFail(`INSERT INTO t VALUES (1)`);
|
|
138
|
+
});
|
|
139
|
+
await t('INSERT NOT NULL 违反 → 报错', async ({ exec, mustFail }) => {
|
|
140
|
+
await exec(`CREATE TABLE t (id INT, v TEXT NOT NULL)`);
|
|
141
|
+
return mustFail(`INSERT INTO t VALUES (1,NULL)`);
|
|
142
|
+
});
|
|
143
|
+
await t('INSERT UNIQUE 重复 → 报错', async ({ exec, mustFail }) => {
|
|
144
|
+
await exec(`CREATE TABLE t (id INT, e TEXT UNIQUE)`);
|
|
145
|
+
await exec(`INSERT INTO t VALUES (1,'a')`);
|
|
146
|
+
return mustFail(`INSERT INTO t VALUES (2,'a')`);
|
|
147
|
+
});
|
|
148
|
+
await t('UNIQUE 允许多个 NULL', async ({ exec, scalar }) => {
|
|
149
|
+
await exec(`CREATE TABLE t (id INT, e TEXT UNIQUE)`);
|
|
150
|
+
await exec(`INSERT INTO t VALUES (1,NULL),(2,NULL)`);
|
|
151
|
+
return eq(await scalar(`SELECT COUNT(*) AS c FROM t`), 2);
|
|
152
|
+
});
|
|
153
|
+
await t('删除后主键可复用', async ({ exec, scalar }) => {
|
|
154
|
+
await exec(`CREATE TABLE t (id INT PRIMARY KEY, v TEXT)`);
|
|
155
|
+
await exec(`INSERT INTO t VALUES (1,'a')`);
|
|
156
|
+
await exec(`DELETE FROM t WHERE id = 1`);
|
|
157
|
+
await exec(`INSERT INTO t VALUES (1,'c')`);
|
|
158
|
+
return eq(await scalar(`SELECT v FROM t WHERE id = 1`), 'c');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
162
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
163
|
+
})();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* require.cache 全局劫持回归测试(P0-1)
|
|
3
|
+
*
|
|
4
|
+
* 背景:旧版 enableMySQLCompat() 会遍历 node_modules,把 mysql2 的
|
|
5
|
+
* require.cache 条目替换成内存兼容层。它靠 process.cwd() 猜路径,
|
|
6
|
+
* mysql2 通常尚未加载,于是被静默替换 —— 真实 MySQL 连接悄悄改查内存库
|
|
7
|
+
* 且不报错,表现为「本地跑通、上线全错」。
|
|
8
|
+
*
|
|
9
|
+
* 本测试锁定修复后的契约:
|
|
10
|
+
* 1. 默认调用 enableMySQLCompat() 绝不改动 require.cache
|
|
11
|
+
* 2. { global: true } 缺少环境变量确认时必须抛错
|
|
12
|
+
* 3. injectMySQLCompat() 只改写传入对象的引用,不污染全局
|
|
13
|
+
* 4. 兼容层仍可显式使用
|
|
14
|
+
*/
|
|
15
|
+
const assert = require('assert');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
let pass = 0, fail = 0;
|
|
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) : '')); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 造一个隔离的假 mysql2,用于验证劫持行为
|
|
27
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-hijack-'));
|
|
28
|
+
const nm = path.join(tmp, 'node_modules', 'mysql2');
|
|
29
|
+
fs.mkdirSync(nm, { recursive: true });
|
|
30
|
+
fs.writeFileSync(path.join(nm, 'index.js'),
|
|
31
|
+
'exports.createConnection = function(){ return { __real: true }; };\n' +
|
|
32
|
+
'exports.createPool = function(){ return { __real: true }; };\n');
|
|
33
|
+
fs.writeFileSync(path.join(nm, 'package.json'),
|
|
34
|
+
JSON.stringify({ name: 'mysql2', version: '3.0.0', main: 'index.js' }));
|
|
35
|
+
|
|
36
|
+
const jsql = require('../index.js');
|
|
37
|
+
const mysql2Path = path.join(nm, 'index.js');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
console.log('--- 1. 默认调用不得劫持 require.cache ---');
|
|
41
|
+
// 直接检查 index.js 源码不含无条件注入路径
|
|
42
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
43
|
+
const fnBody = src.slice(src.indexOf('function enableMySQLCompat'));
|
|
44
|
+
ok('enableMySQLCompat 内部对 global 做了显式判断',
|
|
45
|
+
/opts\.global\s*!==\s*true/.test(fnBody));
|
|
46
|
+
ok('危险路径需环境变量确认',
|
|
47
|
+
/JSQL_NEO_ALLOW_GLOBAL_HIJACK/.test(fnBody));
|
|
48
|
+
|
|
49
|
+
console.log('--- 2. 调用后 require.cache 不含被伪造的 mysql2 ---');
|
|
50
|
+
jsql.enableMySQLCompat();
|
|
51
|
+
const cached = require.cache[mysql2Path];
|
|
52
|
+
ok('require.cache 中没有 mysql2 条目', cached === undefined,
|
|
53
|
+
cached ? '仍被注入' : undefined);
|
|
54
|
+
|
|
55
|
+
console.log('--- 3. { global: true } 无环境变量时抛错 ---');
|
|
56
|
+
let threw = null;
|
|
57
|
+
try { jsql.enableMySQLCompat({ global: true }); } catch (e) { threw = e; }
|
|
58
|
+
ok('抛错并说明原因', threw && /require\.cache/.test(threw.message), threw && threw.message);
|
|
59
|
+
ok('抛错后仍未注入', require.cache[mysql2Path] === undefined);
|
|
60
|
+
|
|
61
|
+
console.log('--- 4. 显式注入只影响目标对象 ---');
|
|
62
|
+
const target = {
|
|
63
|
+
createConnection: () => ({ __fake: true }),
|
|
64
|
+
Driver: { createPool: () => ({ __fake: true }) },
|
|
65
|
+
};
|
|
66
|
+
const before = target.createConnection;
|
|
67
|
+
const touched = jsql.injectMySQLCompat(target);
|
|
68
|
+
ok('injectMySQLCompat 返回 true', touched === true);
|
|
69
|
+
ok('顶层 createConnection 已被替换', target.createConnection !== before);
|
|
70
|
+
ok('嵌套 Driver.createPool 已被替换',
|
|
71
|
+
typeof target.Driver.createPool === 'function' &&
|
|
72
|
+
target.Driver.createPool !== before);
|
|
73
|
+
ok('未触碰 require.cache', require.cache[mysql2Path] === undefined);
|
|
74
|
+
|
|
75
|
+
console.log('--- 5. 兼容层仍可显式使用 ---');
|
|
76
|
+
ok('导出 mysql2.createConnection', typeof jsql.mysql2.createConnection === 'function');
|
|
77
|
+
ok('导出 mysql2.createPool', typeof jsql.mysql2.createPool === 'function');
|
|
78
|
+
ok('导出 createConnection 快捷方式', typeof jsql.createConnection === 'function');
|
|
79
|
+
ok('导出 injectMySQLCompat', typeof jsql.injectMySQLCompat === 'function');
|
|
80
|
+
ok('enableMySQLCompat 默认调用返回兼容层',
|
|
81
|
+
jsql.enableMySQLCompat() === jsql.mysql2);
|
|
82
|
+
} finally {
|
|
83
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
87
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* native 多实例回归测试(P0-2)
|
|
3
|
+
*
|
|
4
|
+
* 背景:6.3.3 及以前 native 层是 `thread_local! static ENGINE`,
|
|
5
|
+
* 整个进程只有一个 HybridEngine。于是:
|
|
6
|
+
* new Database(dirA) → new Database(dirB)
|
|
7
|
+
* 会把 dirA 的所有表 clear() 掉(HybridEngine::open 无条件 clear),
|
|
8
|
+
* 同进程无法持有两个独立数据库 —— 对自称「嵌入式数据库」是硬伤。
|
|
9
|
+
*
|
|
10
|
+
* 6.3.4 改为句柄注册表:jsqlInstanceNew() 分配句柄,
|
|
11
|
+
* 所有操作走 jsqlXxxH(handle, ...);旧的 jsqlXxx() 走保留的默认实例 0。
|
|
12
|
+
*
|
|
13
|
+
* 若加载的是旧版预编译二进制(无 jsqlInstanceNew),整套多实例断言跳过,
|
|
14
|
+
* 但向后兼容路径仍需验证。
|
|
15
|
+
*/
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const native = require('../native/jsql-neo-native.node');
|
|
21
|
+
const { JSQL } = require('../lib/native_client.js');
|
|
22
|
+
|
|
23
|
+
let pass = 0, fail = 0, skip = 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
|
+
function skipTest(name, why) { skip++; console.log('[SKIP] ' + name + ' — ' + why); }
|
|
29
|
+
|
|
30
|
+
const tmp = (tag) => fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-native-' + tag + '-'));
|
|
31
|
+
const SCHEMA = { id: { type: 'integer', primaryKey: true }, tag: { type: 'string' } };
|
|
32
|
+
const hasHandles = typeof native.jsqlInstanceNew === 'function';
|
|
33
|
+
|
|
34
|
+
(async () => {
|
|
35
|
+
console.log('--- 1. native 二进制能力探测 ---');
|
|
36
|
+
console.log(' 句柄版 API: ' + (hasHandles ? '可用' : '不可用(旧版二进制)'));
|
|
37
|
+
ok('二进制可加载', !!native);
|
|
38
|
+
ok('旧版无句柄 API 仍存在(向后兼容)', typeof native.jsqlOpen === 'function');
|
|
39
|
+
|
|
40
|
+
if (hasHandles) {
|
|
41
|
+
console.log('--- 2. 两个独立引擎实例互不干扰(核心回归)---');
|
|
42
|
+
const dA = tmp('A'), dB = tmp('B');
|
|
43
|
+
const SC = JSON.stringify(SCHEMA);
|
|
44
|
+
const hA = native.jsqlInstanceNew();
|
|
45
|
+
native.jsqlOpenH(hA, dA, 'hybrid');
|
|
46
|
+
native.jsqlCreateTableH(hA, 't', SC);
|
|
47
|
+
native.jsqlInsertH(hA, 't', JSON.stringify([{ tag: 'A1' }]));
|
|
48
|
+
native.jsqlInsertH(hA, 't', JSON.stringify([{ tag: 'A2' }]));
|
|
49
|
+
|
|
50
|
+
// 关键:此时再开第二个库,旧版会把 A 的表全部清空
|
|
51
|
+
const hB = native.jsqlInstanceNew();
|
|
52
|
+
native.jsqlOpenH(hB, dB, 'hybrid');
|
|
53
|
+
native.jsqlCreateTableH(hB, 't', SC);
|
|
54
|
+
native.jsqlInsertH(hB, 't', JSON.stringify([{ tag: 'B1' }]));
|
|
55
|
+
|
|
56
|
+
ok('两个句柄不同', hA !== hB, { hA, hB });
|
|
57
|
+
ok('A 的数据未被 B 的 open() 清空', parseInt(native.jsqlCountH(hA, 't'), 10) === 2,
|
|
58
|
+
native.jsqlCountH(hA, 't'));
|
|
59
|
+
ok('B 的数据独立', parseInt(native.jsqlCountH(hB, 't'), 10) === 1,
|
|
60
|
+
native.jsqlCountH(hB, 't'));
|
|
61
|
+
const aRows = JSON.parse(native.jsqlFindH(hA, 't', '', 10, 0)).map(r => r.fields.tag);
|
|
62
|
+
const bRows = JSON.parse(native.jsqlFindH(hB, 't', '', 10, 0)).map(r => r.fields.tag);
|
|
63
|
+
ok('A 数据内容正确', JSON.stringify(aRows) === '["A1","A2"]', aRows);
|
|
64
|
+
ok('B 数据内容正确', JSON.stringify(bRows) === '["B1"]', bRows);
|
|
65
|
+
|
|
66
|
+
console.log('--- 3. 实例计数与销毁 ---');
|
|
67
|
+
ok('显式实例数为 2', native.jsqlInstanceCount() === 2, native.jsqlInstanceCount());
|
|
68
|
+
native.jsqlCloseH(hA); native.jsqlCloseH(hB);
|
|
69
|
+
JSON.parse(native.jsqlInstanceFree(hA));
|
|
70
|
+
JSON.parse(native.jsqlInstanceFree(hB));
|
|
71
|
+
ok('free 后实例归零', native.jsqlInstanceCount() === 0, native.jsqlInstanceCount());
|
|
72
|
+
ok('访问已销毁实例返回错误而非崩溃',
|
|
73
|
+
/no such database instance/.test(native.jsqlCountH(hA, 't')),
|
|
74
|
+
native.jsqlCountH(hA, 't'));
|
|
75
|
+
fs.rmSync(dA, { recursive: true, force: true });
|
|
76
|
+
fs.rmSync(dB, { recursive: true, force: true });
|
|
77
|
+
} else {
|
|
78
|
+
skipTest('多实例句柄测试', '旧版预编译二进制无 jsqlInstanceNew');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('--- 4. 默认实例向后兼容(无句柄 API)---');
|
|
82
|
+
{
|
|
83
|
+
const dD = tmp('D');
|
|
84
|
+
const r = JSON.parse(native.jsqlOpen(dD, 'hybrid'));
|
|
85
|
+
ok('jsqlOpen 走默认实例可用', r.ok === true, r);
|
|
86
|
+
native.jsqlCreateTable('t', JSON.stringify(SCHEMA));
|
|
87
|
+
native.jsqlInsert('t', JSON.stringify([{ tag: 'legacy' }]));
|
|
88
|
+
ok('jsqlCount 默认实例可读', native.jsqlCount('t') === '1', native.jsqlCount('t'));
|
|
89
|
+
const rows = JSON.parse(native.jsqlFind('t', '', 10, 0)).map(r => r.fields.tag);
|
|
90
|
+
ok('默认实例数据正确', JSON.stringify(rows) === '["legacy"]', rows);
|
|
91
|
+
native.jsqlClose();
|
|
92
|
+
fs.rmSync(dD, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('--- 5. JS 层:两个 NativeJSQL 并存 ---');
|
|
96
|
+
{
|
|
97
|
+
const dA = tmp('jsA'), dB = tmp('jsB');
|
|
98
|
+
const A = new JSQL({ path: dA, mode: 'hybrid' });
|
|
99
|
+
const B = new JSQL({ path: dB, mode: 'hybrid' });
|
|
100
|
+
await A.start();
|
|
101
|
+
await B.start();
|
|
102
|
+
if (!hasHandles) {
|
|
103
|
+
// 旧版二进制没有句柄 API,两个 JS 实例会落到同一个默认实例上,
|
|
104
|
+
// 此时建同名表必然冲突 —— 只能验证「回退到单实例」这个事实本身。
|
|
105
|
+
ok('旧版二进制下回退单实例(_handle 为 null)', A._handle === null && B._handle === null);
|
|
106
|
+
skipTest('JS 层多实例并存测试', '旧版预编译二进制无 jsqlInstanceNew,两实例共享默认实例');
|
|
107
|
+
await A.stop();
|
|
108
|
+
await B.stop();
|
|
109
|
+
} else {
|
|
110
|
+
ok('JS 层已启用多实例', A._multiInstance === true);
|
|
111
|
+
ok('两实例句柄不同', A._handle !== B._handle, { a: A._handle, b: B._handle });
|
|
112
|
+
await A.createTable('t', SCHEMA);
|
|
113
|
+
await A.insert('t', { tag: 'A1' });
|
|
114
|
+
await A.insert('t', { tag: 'A2' });
|
|
115
|
+
await B.createTable('t', SCHEMA);
|
|
116
|
+
await B.insert('t', { tag: 'B1' });
|
|
117
|
+
|
|
118
|
+
const ca = await A.count('t'), cb = await B.count('t');
|
|
119
|
+
ok('A 保留自己的 2 行', ca === 2, ca);
|
|
120
|
+
ok('B 有自己的 1 行', cb === 1, cb);
|
|
121
|
+
const ra = (await A.find('t', {})).map(r => r.tag);
|
|
122
|
+
ok('A 数据内容正确', JSON.stringify(ra) === '["A1","A2"]', ra);
|
|
123
|
+
|
|
124
|
+
await B.createTable('only_b', { id: { type: 'integer', primaryKey: true } });
|
|
125
|
+
let aSeesB = true;
|
|
126
|
+
try { await A.find('only_b', {}); aSeesB = false; } catch (e) { aSeesB = true; }
|
|
127
|
+
ok('A 看不到 B 独有的表', aSeesB);
|
|
128
|
+
|
|
129
|
+
await A.stop();
|
|
130
|
+
await B.stop();
|
|
131
|
+
ok('stop 释放句柄', A._handle === null && B._handle === null);
|
|
132
|
+
} // else: hasHandles
|
|
133
|
+
fs.rmSync(dA, { recursive: true, force: true });
|
|
134
|
+
fs.rmSync(dB, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`\n${pass} passed, ${fail} failed${skip ? ', ' + skip + ' skipped' : ''}`);
|
|
138
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
139
|
+
})().catch(e => { console.error('FATAL', e); process.exit(1); });
|
|
@@ -528,6 +528,31 @@ async function main() {
|
|
|
528
528
|
ok('null bulk $-1', JSON.stringify((p('*2\r\n$3\r\nGET\r\n$-1\r\n') || {}).args) === '[null]');
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
+
console.log('--- 版本号一致性(package.json 是唯一真相)---');
|
|
532
|
+
{
|
|
533
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
534
|
+
const raw = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8');
|
|
535
|
+
const v = pkg.version;
|
|
536
|
+
ok('package.json 版本号格式合法', /^\d+\.\d+\.\d+$/.test(v), v);
|
|
537
|
+
|
|
538
|
+
// README 头部横幅必须与 package.json 一致
|
|
539
|
+
const banner = raw.match(/^> \*\*v(\d+\.\d+\.\d+)\*\*/m);
|
|
540
|
+
ok('README 顶部版本横幅与 package.json 一致', banner && banner[1] === v,
|
|
541
|
+
banner ? `README=${banner[1]} package=${v}` : '未找到顶部横幅');
|
|
542
|
+
|
|
543
|
+
// 所有 "jsql-neo vX.Y.Z" / "文档版本:vX.Y.Z" 形式的版本声明都必须等于当前版本
|
|
544
|
+
const claims = [...raw.matchAll(/(?:jsql-neo v|文档版本:v|ver=)(\d+\.\d+\.\d+)/g)]
|
|
545
|
+
.map(m => m[1]);
|
|
546
|
+
const stale = [...new Set(claims.filter(c => c !== v))];
|
|
547
|
+
ok('README 无陈旧版本号声明', stale.length === 0, stale);
|
|
548
|
+
|
|
549
|
+
// index.js 头部注释也必须同步
|
|
550
|
+
const idx = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
551
|
+
const hdr = idx.match(/JSQL-NEO v(\d+\.\d+\.\d+)/);
|
|
552
|
+
ok('index.js 头部版本注释与 package.json 一致', hdr && hdr[1] === v,
|
|
553
|
+
hdr ? `index.js=${hdr[1]} package=${v}` : '未找到版本注释');
|
|
554
|
+
}
|
|
555
|
+
|
|
531
556
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
532
557
|
process.exit(fail === 0 ? 0 : 1);
|
|
533
558
|
}
|
package/test/sql-parser.test.js
CHANGED
|
@@ -584,6 +584,195 @@ const rowsOf = (r) => (Array.isArray(r) ? r[0] : r).rows;
|
|
|
584
584
|
ok('分组 + ORDER BY + LIMIT 只返回 1 行', lim.rows.length === 1 && lim.rows[0][0] === 'ops', lim.rows);
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
+
/* ---- 括号分组后接运算符:曾报 Unexpected token '*' ---- */
|
|
588
|
+
console.log('\n--- BUG 修复: 括号分组参与后续运算 ---');
|
|
589
|
+
{
|
|
590
|
+
const run = async (sql) => executeSQL(engine, sql, OPTS);
|
|
591
|
+
// parseNot 的括号分组分支:解析到 ')' 就闭合返回,运算符留在流里
|
|
592
|
+
const cases = [
|
|
593
|
+
['SELECT (1+2)*3 AS v', 9],
|
|
594
|
+
['SELECT (3)*4 AS v', 12],
|
|
595
|
+
['SELECT (1+2)*(3+4) AS v', 21],
|
|
596
|
+
['SELECT (1+2)/2 AS v', 1.5],
|
|
597
|
+
['SELECT 10-(2+3) AS v', 5],
|
|
598
|
+
['SELECT (2+3)%2 AS v', 1],
|
|
599
|
+
['SELECT (1+2)+3 AS v', 6],
|
|
600
|
+
['SELECT ((1+2))*3 AS v', 9], // 嵌套括号
|
|
601
|
+
['SELECT (1+2) AS v', 3], // 无运算符(回归对照)
|
|
602
|
+
['SELECT 2*(3+4) AS v', 14], // ( 在右操作数位置(回归对照)
|
|
603
|
+
];
|
|
604
|
+
for (const [sql, expect] of cases) {
|
|
605
|
+
let got = null, err = null;
|
|
606
|
+
try { got = (await run(sql)).rows[0][0]; } catch (e) { err = e; }
|
|
607
|
+
ok(`${sql} = ${expect}`, err === null && got === expect,
|
|
608
|
+
err ? String(err.message || err) : got);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// WHERE 里的括号同样要能参与比较
|
|
612
|
+
const w = await run(`SELECT name FROM emp WHERE (sal + 100) > 350 ORDER BY sal DESC`);
|
|
613
|
+
ok('WHERE (sal+100) > 350 括号参与比较',
|
|
614
|
+
w.rows.length === 2 && w.rows[0][0] === 'c' && w.rows[1][0] === 'd', w.rows);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* ---- 聚合参与算术:SUM(x)/COUNT(*) 这类写法曾直接报错 ---- */
|
|
618
|
+
console.log('\n--- BUG 修复: 聚合函数参与算术运算 ---');
|
|
619
|
+
{
|
|
620
|
+
const run = async (sql) => executeSQL(engine, sql, OPTS);
|
|
621
|
+
// mock emp: eng(sal 100,200) / ops(sal 300,300)
|
|
622
|
+
// 全表 SUM=900 COUNT=4 AVG=225 MIN=100 MAX=300
|
|
623
|
+
const cases = [
|
|
624
|
+
['SELECT SUM(sal)/COUNT(*) AS avg FROM emp', 225], // 算平均值的标准写法
|
|
625
|
+
['SELECT SUM(sal)-MIN(sal) AS range FROM emp', 800], // 极差
|
|
626
|
+
['SELECT MAX(sal)*2 AS m FROM emp', 600],
|
|
627
|
+
['SELECT SUM(sal)+1 AS s FROM emp', 901],
|
|
628
|
+
['SELECT COUNT(*)*100 AS c FROM emp', 400],
|
|
629
|
+
['SELECT SUM(sal)/COUNT(*) FROM emp', 225], // 无别名
|
|
630
|
+
];
|
|
631
|
+
for (const [sql, expect] of cases) {
|
|
632
|
+
let got = null, err = null;
|
|
633
|
+
try { got = (await run(sql)).rows[0][0]; } catch (e) { err = e; }
|
|
634
|
+
ok(`${sql} = ${expect}`, err === null && got === expect,
|
|
635
|
+
err ? String(err.message || err) : got);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// 分组场景:分组路径也要能算 SUM/COUNT
|
|
639
|
+
const g = await run(`SELECT dept, SUM(sal)/COUNT(*) AS avg FROM emp GROUP BY dept ORDER BY avg DESC`);
|
|
640
|
+
ok('分组 + SUM/COUNT 每组各自的平均',
|
|
641
|
+
g.rows.length === 2 && g.rows[0][0] === 'ops' && g.rows[0][1] === 300
|
|
642
|
+
&& g.rows[1][1] === 150, g.rows);
|
|
643
|
+
|
|
644
|
+
// 与其它聚合混用不能互相破坏
|
|
645
|
+
const m = await run(`SELECT SUM(sal)/COUNT(*) AS avg, MAX(sal) AS mx, MIN(sal) AS mn FROM emp`);
|
|
646
|
+
ok('SUM/COUNT 与 MAX/MIN 并存', m.rows[0][0] === 225 && m.rows[0][1] === 300 && m.rows[0][2] === 100, m.rows);
|
|
647
|
+
|
|
648
|
+
// 原有路径回归对照(不带运算符)
|
|
649
|
+
const s = await run(`SELECT SUM(sal) AS s, COUNT(*) AS c, AVG(sal) AS a FROM emp`);
|
|
650
|
+
ok('SUM/COUNT/AVG 单独使用不受影响', s.rows[0][0] === 900 && s.rows[0][1] === 4 && s.rows[0][2] === 225, s.rows);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* ---- TIMESTAMPDIFF 整月差:曾按日历月差高估 ---- */
|
|
654
|
+
console.log('\n--- BUG 修复: TIMESTAMPDIFF 整月差语义 ---');
|
|
655
|
+
{
|
|
656
|
+
const run = async (sql) => executeSQL(engine, sql, OPTS);
|
|
657
|
+
// MySQL:先算日历月差,再按「结束日的日 < 起始日的日」退一档。
|
|
658
|
+
// 只算日历月差会高估,且 YEAR 同样受影响。
|
|
659
|
+
const cases = [
|
|
660
|
+
// [SQL 片段, 期望, 描述]
|
|
661
|
+
[`TIMESTAMPDIFF(MONTH,'2026-01-31','2026-03-01')`, 1, '月末出发不足整月'],
|
|
662
|
+
[`TIMESTAMPDIFF(MONTH,'2026-01-15','2026-03-14')`, 1, '差一月零29天不满两月'],
|
|
663
|
+
[`TIMESTAMPDIFF(MONTH,'2026-01-01','2026-03-01')`, 2, '月初对齐'],
|
|
664
|
+
[`TIMESTAMPDIFF(MONTH,'2026-01-15','2026-03-15')`, 2, '同日对齐'],
|
|
665
|
+
[`TIMESTAMPDIFF(YEAR,'2020-06-15','2026-06-14')`, 5, 'YEAR 差一天不满整年'],
|
|
666
|
+
[`TIMESTAMPDIFF(YEAR,'2020-06-15','2026-06-15')`, 6, 'YEAR 整年'],
|
|
667
|
+
[`TIMESTAMPDIFF(QUARTER,'2026-01-01','2026-07-01')`, 2, 'QUARTER 整季'],
|
|
668
|
+
[`TIMESTAMPDIFF(DAY,'2026-01-01','2026-03-01')`, 59, 'DAY 不受影响'],
|
|
669
|
+
];
|
|
670
|
+
for (const [expr, expect, desc] of cases) {
|
|
671
|
+
let got = null, err = null;
|
|
672
|
+
try { got = (await run(`SELECT ${expr} AS v`)).rows[0][0]; } catch (e) { err = e; }
|
|
673
|
+
ok(`${desc}: ${expr} = ${expect}`, err === null && got === expect,
|
|
674
|
+
err ? String(err.message || err) : got);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* ---- 隐式类型转换:+ 必须是算术而非字符串拼接 ---- */
|
|
679
|
+
console.log('\n--- BUG 修复: 算术运算中的隐式类型转换 ---');
|
|
680
|
+
{
|
|
681
|
+
const run = async (sql) => executeSQL(engine, sql, OPTS);
|
|
682
|
+
// 语义已用 Python sqlite3 实测核对:'5'+5=10、'abc'+1=1、'a'+'b'=0
|
|
683
|
+
const cases = [
|
|
684
|
+
[`SELECT '5' + 5 AS v`, 10, '数字字符串 + 数字'],
|
|
685
|
+
[`SELECT 5 + '5' AS v`, 10, '数字 + 数字字符串'],
|
|
686
|
+
[`SELECT '5' + '5' AS v`, 10, '两个数字字符串'],
|
|
687
|
+
[`SELECT '2.5' * 2 AS v`, 5, '数字字符串乘法'],
|
|
688
|
+
[`SELECT '10' - 4 AS v`, 6, '数字字符串减法'],
|
|
689
|
+
[`SELECT 'abc' + 1 AS v`, 1, '非数字按 0(MySQL/SQLite 语义)'],
|
|
690
|
+
[`SELECT 'a' + 'b' AS v`, 0, '两侧都非数字返 0'],
|
|
691
|
+
[`SELECT 1 + 2 AS v`, 3, '纯数字回归对照'],
|
|
692
|
+
];
|
|
693
|
+
for (const [sql, expect, desc] of cases) {
|
|
694
|
+
let got = null, err = null;
|
|
695
|
+
try { got = (await run(sql)).rows[0][0]; } catch (e) { err = e; }
|
|
696
|
+
ok(`${desc}: ${sql} = ${expect}`, err === null && got === expect,
|
|
697
|
+
err ? String(err.message || err) : got);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// CAST(... AS SIGNED) 是 MySQL 整型别名,此前未列入整型分支,
|
|
701
|
+
// 导致 CAST('12abc' AS SIGNED) 原样返回字符串
|
|
702
|
+
const cast = [
|
|
703
|
+
[`SELECT CAST('12abc' AS SIGNED) AS v`, 12, 'SIGNED 取数字前缀'],
|
|
704
|
+
[`SELECT CAST('abc' AS SIGNED) AS v`, 0, 'SIGNED 非数字返 0'],
|
|
705
|
+
[`SELECT CAST(12.9 AS SIGNED) AS v`, 12, 'SIGNED 截断小数'],
|
|
706
|
+
[`SELECT CAST(12.9 AS INTEGER) AS v`, 12, 'INTEGER 回归对照'],
|
|
707
|
+
];
|
|
708
|
+
for (const [sql, expect, desc] of cast) {
|
|
709
|
+
let got = null, err = null;
|
|
710
|
+
try { got = (await run(sql)).rows[0][0]; } catch (e) { err = e; }
|
|
711
|
+
ok(`${desc}: ${sql} = ${expect}`, err === null && got === expect,
|
|
712
|
+
err ? String(err.message || err) : got);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* ---- CASE WHEN 裸值条件 + 补齐缺失的 MySQL 函数 ---- */
|
|
717
|
+
console.log('\n--- BUG 修复: CASE 裸值条件 / SIGN / TRUNCATE / SUBSTRING_INDEX ---');
|
|
718
|
+
{
|
|
719
|
+
const run = async (sql) => executeSQL(engine, sql, OPTS);
|
|
720
|
+
const one = async (sql) => (await run(sql)).rows[0][0];
|
|
721
|
+
|
|
722
|
+
// 搜索形式 CASE 的条件可能是裸值/裸列,此前 parseComparison 只收比较式
|
|
723
|
+
const caseCases = [
|
|
724
|
+
[`SELECT CASE WHEN 1 THEN 'a' ELSE 'b' END AS v`, 'a', 'CASE WHEN 裸数字'],
|
|
725
|
+
[`SELECT CASE WHEN 0 THEN 'a' ELSE 'b' END AS v`, 'b', 'CASE WHEN 假'],
|
|
726
|
+
[`SELECT CASE WHEN NULL THEN 'a' ELSE 'b' END AS v`, 'b', 'CASE WHEN NULL 走 ELSE'],
|
|
727
|
+
[`SELECT CASE WHEN 1=1 THEN 'a' ELSE 'b' END AS v`, 'a', 'CASE WHEN 比较式'],
|
|
728
|
+
[`SELECT CASE WHEN 1 IN (1,2) THEN 'y' ELSE 'n' END AS v`, 'y', 'CASE WHEN IN'],
|
|
729
|
+
[`SELECT CASE 2 WHEN 2 THEN 'y' ELSE 'n' END AS v`, 'y', 'CASE 简单形式'],
|
|
730
|
+
];
|
|
731
|
+
for (const [sql, expect, desc] of caseCases) {
|
|
732
|
+
let got = null, err = null;
|
|
733
|
+
try { got = await one(sql); } catch (e) { err = e; }
|
|
734
|
+
ok(`${desc}`, err === null && got === expect, err ? String(err.message || err) : got);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 缺失的 MySQL 函数
|
|
738
|
+
const fnCases = [
|
|
739
|
+
['SIGN(-9)', -1, 'SIGN 负'], ['SIGN(9)', 1, 'SIGN 正'], ['SIGN(0)', 0, 'SIGN 零'],
|
|
740
|
+
['TRUNCATE(3.987,2)', 3.98, 'TRUNCATE 截断而非四舍五入'],
|
|
741
|
+
['TRUNCATE(-3.987,2)', -3.98, 'TRUNCATE 负数'],
|
|
742
|
+
[`SUBSTRING_INDEX('a.b.c','.',2)`, 'a.b', 'SUBSTRING_INDEX 取左'],
|
|
743
|
+
[`SUBSTRING_INDEX('a.b.c','.',-1)`, 'c', 'SUBSTRING_INDEX 取右'],
|
|
744
|
+
[`SUBSTRING_INDEX('a.b.c','.',0)`, '', 'SUBSTRING_INDEX 取 0 段'],
|
|
745
|
+
];
|
|
746
|
+
for (const [expr, expect, desc] of fnCases) {
|
|
747
|
+
let got = null, err = null;
|
|
748
|
+
try { got = await one(`SELECT ${expr} AS v`); } catch (e) { err = e; }
|
|
749
|
+
ok(`${desc}: ${expr} = ${JSON.stringify(expect)}`, err === null && got === expect,
|
|
750
|
+
err ? String(err.message || err) : got);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/* ---- 列级 CHECK 约束:此前直接报 CREATE TABLE 语法错误 ---- */
|
|
755
|
+
console.log('\n--- BUG 修复: 列级 CHECK 约束解析 ---');
|
|
756
|
+
{
|
|
757
|
+
// 表级 CHECK 早已能解析(同样只是跳过),列级却会报
|
|
758
|
+
// Expected ',' or ')' in CREATE TABLE, got 'CHECK'
|
|
759
|
+
// 因为 parseColumnDef 的 default 分支直接 return def,把 CHECK 留在流里。
|
|
760
|
+
// CHECK 刻意不在 KEYWORDS 里(避免影响同名标识符),要用 isWord 匹配。
|
|
761
|
+
const ddls = [
|
|
762
|
+
`CREATE TABLE ck1 (a INT CHECK(a > 0))`,
|
|
763
|
+
`CREATE TABLE ck2 (a INT CHECK(a > 0), b TEXT)`,
|
|
764
|
+
`CREATE TABLE ck3 (a INT NOT NULL CHECK(a > 0))`,
|
|
765
|
+
`CREATE TABLE ck4 (a TEXT, b INT CHECK(b > 0))`,
|
|
766
|
+
`CREATE TABLE ck5 (a INT, CHECK(a > 0))`, // 表级(回归对照)
|
|
767
|
+
`CREATE TABLE ck6 (a INT UNIQUE CHECK(a > 0))`,
|
|
768
|
+
];
|
|
769
|
+
for (const ddl of ddls) {
|
|
770
|
+
let err = null;
|
|
771
|
+
try { parseSQL(ddl); } catch (e) { err = e; }
|
|
772
|
+
ok(`可解析:${ddl}`, err === null, err ? String(err.message || err) : '');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
587
776
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
588
777
|
process.exit(fail === 0 ? 0 : 1);
|
|
589
778
|
})();
|