jsql-neo 5.4.2 → 6.0.0-beta2
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 +198 -14
- package/index.d.ts +29 -0
- package/index.js +12 -2
- package/lib/ast.js +214 -0
- package/lib/database.js +26 -60
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +43 -66
- package/lib/plugin.js +227 -3
- package/lib/plugins/audit.js +37 -0
- package/lib/plugins/index.js +72 -0
- package/lib/plugins/logger.js +35 -0
- package/lib/plugins/metrics.js +38 -0
- package/lib/plugins/timestamps.js +43 -0
- package/lib/plugins/validation.js +128 -0
- package/lib/sql.js +1212 -84
- package/lib/wasm_client.js +41 -57
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
- package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
- package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
- package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
- package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
- package/package.json +4 -3
- package/test/sql-parser.test.js +450 -0
- package/wasm/browser.mjs +12 -0
- package/wasm/browser_bg.mjs +269 -248
- package/wasm/jsql_neo_wasm.d.ts +6 -0
- package/wasm/jsql_neo_wasm.js +248 -127
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
- package/wasm/package.json +1 -1
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* test/sql-parser.test.js — SQL 解析与执行回归测试
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 jsql-neo 5.5.0 的:
|
|
5
|
+
* BUG 修复 : 反引号分号误切分 / RLIKE / TRUE|FALSE 字面量 / 限定列回退
|
|
6
|
+
* 新增语法 : CTE、窗口函数、EXISTS、INTERSECT/EXCEPT、? 占位符、INSERT...SELECT
|
|
7
|
+
* 新增能力 : AST 访问与改写
|
|
8
|
+
*
|
|
9
|
+
* 运行:node test/sql-parser.test.js
|
|
10
|
+
*/
|
|
11
|
+
const { parseSQL, executeSQL, splitStatements, tokenize, AST } = require('../lib/sql.js');
|
|
12
|
+
|
|
13
|
+
let pass = 0, fail = 0;
|
|
14
|
+
const ok = (name, cond, extra) => {
|
|
15
|
+
if (cond) { pass++; console.log('[OK]', name); }
|
|
16
|
+
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '→ ' + JSON.stringify(extra) : ''); }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ROWS = [
|
|
20
|
+
{ id: 1, dept: 'eng', name: 'a', sal: 100 },
|
|
21
|
+
{ id: 2, dept: 'eng', name: 'b', sal: 200 },
|
|
22
|
+
{ id: 3, dept: 'ops', name: 'c', sal: 300 },
|
|
23
|
+
{ id: 4, dept: 'ops', name: 'd', sal: 300 },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const engine = {
|
|
27
|
+
hasTable: () => true,
|
|
28
|
+
truncate: async () => {},
|
|
29
|
+
flush: async () => {},
|
|
30
|
+
find: async () => ROWS.map(r => ({ ...r })),
|
|
31
|
+
getTableSchema: async () => ({
|
|
32
|
+
id: { type: 'integer', primaryKey: true },
|
|
33
|
+
dept: { type: 'string' },
|
|
34
|
+
name: { type: 'string' },
|
|
35
|
+
sal: { type: 'integer' },
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const OPTS = { safety: false };
|
|
40
|
+
const rowsOf = (r) => (Array.isArray(r) ? r[0] : r).rows;
|
|
41
|
+
|
|
42
|
+
(async () => {
|
|
43
|
+
|
|
44
|
+
/* ================= BUG 1: splitStatements 反引号内的分号 ================= */
|
|
45
|
+
console.log('\n--- BUG 1: 反引号标识符中的分号不再被误切分 ---');
|
|
46
|
+
{
|
|
47
|
+
ok('反引号内含分号只切出 1 条语句',
|
|
48
|
+
splitStatements('SELECT `a;b` FROM t').length === 1,
|
|
49
|
+
splitStatements('SELECT `a;b` FROM t'));
|
|
50
|
+
ok('反引号内容保持完整',
|
|
51
|
+
splitStatements('SELECT `a;b` FROM t')[0] === 'SELECT `a;b` FROM t');
|
|
52
|
+
ok('转义反引号 `` 不提前结束标识符',
|
|
53
|
+
splitStatements('SELECT `a``b;c` FROM t')[0] === 'SELECT `a``b;c` FROM t',
|
|
54
|
+
splitStatements('SELECT `a``b;c` FROM t'));
|
|
55
|
+
ok('普通字符串内的分号仍不被切分(既有行为)',
|
|
56
|
+
splitStatements("SELECT 'a;b'; SELECT 2").length === 2);
|
|
57
|
+
ok('多条语句依旧正常切分',
|
|
58
|
+
splitStatements('SELECT 1; SELECT 2').length === 2);
|
|
59
|
+
ok('反引号列名可正常解析',
|
|
60
|
+
parseSQL('SELECT `a;b` FROM t').columns.length === 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ================= BUG 2: RLIKE 方言兼容 ================= */
|
|
64
|
+
console.log('\n--- BUG 2: RLIKE 作为 REGEXP 同义词 ---');
|
|
65
|
+
{
|
|
66
|
+
const ast = parseSQL("SELECT * FROM t WHERE a RLIKE '^x$'");
|
|
67
|
+
ok('RLIKE 解析为 regexp 节点', ast.where.type === 'regexp', ast.where.type);
|
|
68
|
+
const ast2 = parseSQL("SELECT * FROM t WHERE a NOT RLIKE '^x$'");
|
|
69
|
+
ok('NOT RLIKE 带 not 标记', ast2.where.type === 'regexp' && ast2.where.not === true);
|
|
70
|
+
ok('REGEXP 行为未变', parseSQL("SELECT * FROM t WHERE a REGEXP 'x'").where.type === 'regexp');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ================= BUG 3: TRUE / FALSE 字面量 ================= */
|
|
74
|
+
console.log('\n--- BUG 3: TRUE/FALSE 可作为值字面量 ---');
|
|
75
|
+
{
|
|
76
|
+
ok('WHERE a = TRUE 可解析',
|
|
77
|
+
parseSQL('SELECT * FROM t WHERE a = TRUE').where.right.value === true);
|
|
78
|
+
ok('WHERE a = FALSE 可解析',
|
|
79
|
+
parseSQL('SELECT * FROM t WHERE a = FALSE').where.right.value === false);
|
|
80
|
+
ok('IS TRUE 行为未变', parseSQL('SELECT * FROM t WHERE a IS TRUE').where.type === 'isTruth');
|
|
81
|
+
const r = await executeSQL(engine, 'SELECT name FROM emp WHERE 1 = TRUE', OPTS);
|
|
82
|
+
ok('1 = TRUE 求值为真(MySQL 语义)', rowsOf(r).length === ROWS.length, rowsOf(r));
|
|
83
|
+
const r2 = await executeSQL(engine, 'SELECT name FROM emp WHERE 0 = FALSE', OPTS);
|
|
84
|
+
ok('0 = FALSE 求值为真', rowsOf(r2).length === ROWS.length, rowsOf(r2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ================= BUG 4: 限定列回退到裸列名 ================= */
|
|
88
|
+
console.log('\n--- BUG 4: 未知限定符不再静默回退到裸列名 ---');
|
|
89
|
+
{
|
|
90
|
+
// 相关子查询曾因 e.dept 回退到内层行的 dept 而变成自匹配,EXISTS 恒为真
|
|
91
|
+
const r = await executeSQL(engine,
|
|
92
|
+
'SELECT name FROM emp e WHERE EXISTS (SELECT 1 FROM emp x WHERE x.dept = e.dept AND x.sal > 250)', OPTS);
|
|
93
|
+
ok('相关 EXISTS 只返回匹配外层的行',
|
|
94
|
+
JSON.stringify(rowsOf(r)) === '[["c"],["d"]]', rowsOf(r));
|
|
95
|
+
const r2 = await executeSQL(engine,
|
|
96
|
+
'SELECT name FROM emp e WHERE NOT EXISTS (SELECT 1 FROM emp x WHERE x.dept = e.dept AND x.sal > 250)', OPTS);
|
|
97
|
+
ok('NOT EXISTS 返回补集', JSON.stringify(rowsOf(r2)) === '[["a"],["b"]]', rowsOf(r2));
|
|
98
|
+
// 未加前缀的行(来自子查询投影)仍允许回退,保证既有行为不破
|
|
99
|
+
const r3 = await executeSQL(engine, 'SELECT x.name FROM emp x WHERE x.sal > 250', OPTS);
|
|
100
|
+
ok('限定列正常解析不受影响', JSON.stringify(rowsOf(r3)) === '[["c"],["d"]]', rowsOf(r3));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ================= 新增:CTE ================= */
|
|
104
|
+
console.log('\n--- 新增: WITH CTE ---');
|
|
105
|
+
{
|
|
106
|
+
ok('简单 CTE 可解析', parseSQL('WITH c AS (SELECT * FROM t) SELECT * FROM c').type === 'with');
|
|
107
|
+
ok('RECURSIVE 关键字可解析',
|
|
108
|
+
parseSQL('WITH RECURSIVE c AS (SELECT 1 AS n) SELECT * FROM c').recursive === true);
|
|
109
|
+
ok('多 CTE 可解析', parseSQL('WITH a AS (SELECT * FROM t), b AS (SELECT * FROM a) SELECT * FROM b').ctes.length === 2);
|
|
110
|
+
ok('CTE 列清单可解析',
|
|
111
|
+
parseSQL('WITH c (x, y) AS (SELECT a, b FROM t) SELECT x FROM c').ctes[0].columns.length === 2);
|
|
112
|
+
const r = await executeSQL(engine, "WITH e AS (SELECT * FROM emp WHERE dept = 'ops') SELECT name FROM e", OPTS);
|
|
113
|
+
ok('CTE 执行结果正确', JSON.stringify(rowsOf(r)) === '[["c"],["d"]]', rowsOf(r));
|
|
114
|
+
const r2 = await executeSQL(engine, 'WITH a AS (SELECT * FROM emp), b AS (SELECT * FROM a WHERE sal > 150) SELECT name FROM b', OPTS);
|
|
115
|
+
ok('嵌套 CTE(引用前一个 CTE)', JSON.stringify(rowsOf(r2)) === '[["b"],["c"],["d"]]', rowsOf(r2));
|
|
116
|
+
const r3 = await executeSQL(engine, 'WITH e AS (SELECT * FROM emp) SELECT COUNT(*) AS n FROM e', OPTS);
|
|
117
|
+
ok('CTE + 聚合', JSON.stringify(rowsOf(r3)) === '[[4]]', rowsOf(r3));
|
|
118
|
+
const r4 = await executeSQL(engine, 'WITH t (who, howmuch) AS (SELECT name, sal FROM emp) SELECT who FROM t', OPTS);
|
|
119
|
+
ok('CTE 显式列名清单生效', JSON.stringify(rowsOf(r4)) === '[["a"],["b"],["c"],["d"]]', rowsOf(r4));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ================= 新增:窗口函数 ================= */
|
|
123
|
+
console.log('\n--- 新增: 窗口函数 OVER() ---');
|
|
124
|
+
{
|
|
125
|
+
ok('ROW_NUMBER() OVER 可解析',
|
|
126
|
+
parseSQL('SELECT ROW_NUMBER() OVER (PARTITION BY a ORDER BY b) AS rn FROM t').columns[0].scalar.over != null);
|
|
127
|
+
ok('SUM() OVER 可解析',
|
|
128
|
+
parseSQL('SELECT SUM(a) OVER (PARTITION BY b) AS s FROM t').columns[0].window === true);
|
|
129
|
+
ok('COUNT(*) OVER () 可解析',
|
|
130
|
+
parseSQL('SELECT COUNT(*) OVER () AS c FROM t').columns[0].over != null);
|
|
131
|
+
// AVG 是关键字,走聚合分支 → 窗口定义挂在 columns[0].over 上
|
|
132
|
+
ok('窗口帧 ROWS BETWEEN 可解析',
|
|
133
|
+
parseSQL('SELECT AVG(a) OVER (ORDER BY b ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS m FROM t')
|
|
134
|
+
.columns[0].over.frame.unit === 'ROWS');
|
|
135
|
+
|
|
136
|
+
// 排序:sal DESC → c(300) d(300) b(200) a(100)
|
|
137
|
+
const rn = await executeSQL(engine, 'SELECT name, ROW_NUMBER() OVER (ORDER BY sal DESC) AS rn FROM emp', OPTS);
|
|
138
|
+
ok('ROW_NUMBER 连续编号', JSON.stringify(rowsOf(rn)) === '[["a",4],["b",3],["c",1],["d",2]]', rowsOf(rn));
|
|
139
|
+
|
|
140
|
+
const rk = await executeSQL(engine, 'SELECT name, RANK() OVER (ORDER BY sal DESC) AS rk FROM emp', OPTS);
|
|
141
|
+
ok('RANK 并列跳号', JSON.stringify(rowsOf(rk)) === '[["a",4],["b",3],["c",1],["d",1]]', rowsOf(rk));
|
|
142
|
+
|
|
143
|
+
const dr = await executeSQL(engine, 'SELECT name, DENSE_RANK() OVER (ORDER BY sal DESC) AS dr FROM emp', OPTS);
|
|
144
|
+
ok('DENSE_RANK 并列不跳号', JSON.stringify(rowsOf(dr)) === '[["a",3],["b",2],["c",1],["d",1]]', rowsOf(dr));
|
|
145
|
+
|
|
146
|
+
const sm = await executeSQL(engine, 'SELECT dept, name, SUM(sal) OVER (PARTITION BY dept) AS tot FROM emp', OPTS);
|
|
147
|
+
ok('SUM() OVER (PARTITION BY)', JSON.stringify(rowsOf(sm)) === '[["eng","a",300],["eng","b",300],["ops","c",600],["ops","d",600]]', rowsOf(sm));
|
|
148
|
+
|
|
149
|
+
const ct = await executeSQL(engine, 'SELECT name, COUNT(*) OVER () AS total FROM emp', OPTS);
|
|
150
|
+
ok('COUNT(*) OVER ()', JSON.stringify(rowsOf(ct)) === '[["a",4],["b",4],["c",4],["d",4]]', rowsOf(ct));
|
|
151
|
+
|
|
152
|
+
const lg = await executeSQL(engine, 'SELECT name, LAG(sal, 1) OVER (ORDER BY sal) AS prev FROM emp', OPTS);
|
|
153
|
+
ok('LAG 取上一行', JSON.stringify(rowsOf(lg)) === '[["a",null],["b",100],["c",200],["d",300]]', rowsOf(lg));
|
|
154
|
+
|
|
155
|
+
const nt = await executeSQL(engine, 'SELECT name, NTILE(2) OVER (ORDER BY sal) AS nt FROM emp', OPTS);
|
|
156
|
+
ok('NTILE 分桶', JSON.stringify(rowsOf(nt)) === '[["a",1],["b",1],["c",2],["d",2]]', rowsOf(nt));
|
|
157
|
+
|
|
158
|
+
const fv = await executeSQL(engine, 'SELECT name, FIRST_VALUE(sal) OVER (PARTITION BY dept ORDER BY sal) AS f FROM emp', OPTS);
|
|
159
|
+
ok('FIRST_VALUE 取分区首行', JSON.stringify(rowsOf(fv)) === '[["a",100],["b",100],["c",300],["d",300]]', rowsOf(fv));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ================= 新增:集合运算 ================= */
|
|
163
|
+
console.log('\n--- 新增: INTERSECT / EXCEPT ---');
|
|
164
|
+
{
|
|
165
|
+
ok('INTERSECT 可解析', parseSQL('SELECT * FROM t1 INTERSECT SELECT * FROM t2').intersect != null);
|
|
166
|
+
ok('EXCEPT 可解析', parseSQL('SELECT * FROM t1 EXCEPT SELECT * FROM t2').except != null);
|
|
167
|
+
ok('UNION DISTINCT 可解析', parseSQL('SELECT * FROM t1 UNION DISTINCT SELECT * FROM t2').union != null);
|
|
168
|
+
|
|
169
|
+
const is = await executeSQL(engine, 'SELECT dept FROM emp WHERE sal > 100 INTERSECT SELECT dept FROM emp WHERE sal < 300', OPTS);
|
|
170
|
+
ok('INTERSECT 默认去重', JSON.stringify(rowsOf(is)) === '[["eng"]]', rowsOf(is));
|
|
171
|
+
|
|
172
|
+
const ex = await executeSQL(engine, 'SELECT dept FROM emp EXCEPT SELECT dept FROM emp WHERE sal < 200', OPTS);
|
|
173
|
+
ok('EXCEPT 默认去重', JSON.stringify(rowsOf(ex)) === '[["ops"]]', rowsOf(ex));
|
|
174
|
+
|
|
175
|
+
const un = await executeSQL(engine, "SELECT name FROM emp WHERE dept = 'eng' UNION SELECT name FROM emp WHERE dept = 'ops'", OPTS);
|
|
176
|
+
ok('UNION 去重(既有行为)', rowsOf(un).length === 4, rowsOf(un));
|
|
177
|
+
const ua = await executeSQL(engine, "SELECT name FROM emp WHERE dept = 'eng' UNION ALL SELECT name FROM emp WHERE name = 'a'", OPTS);
|
|
178
|
+
ok('UNION ALL 保留重复', rowsOf(ua).length === 3, rowsOf(ua));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ================= 新增:? 占位符 / INSERT...SELECT ================= */
|
|
182
|
+
console.log('\n--- 新增: ? 占位符 / INSERT ... SELECT ---');
|
|
183
|
+
{
|
|
184
|
+
const paramTok = tokenize('SELECT * FROM t WHERE a = ?').find(t => t.value === null && t.type === 'param');
|
|
185
|
+
ok('? 被词法层识别为 param', !!paramTok, tokenize('SELECT * FROM t WHERE a = ?').map(t => t.type));
|
|
186
|
+
ok('?1 编号占位符', tokenize('SELECT * FROM t WHERE a = ?1').some(t => t.type === 'param' && t.value === 1));
|
|
187
|
+
ok('?? 标识符占位符', tokenize('SELECT * FROM t WHERE a = ??').some(t => t.type === 'param' && t.value === '??'));
|
|
188
|
+
const ast = parseSQL('SELECT * FROM t WHERE a = ?');
|
|
189
|
+
ok('? 生成 param 节点', ast.where.right.type === 'param');
|
|
190
|
+
const r = await executeSQL(engine, 'SELECT name FROM emp WHERE sal > ?', { ...OPTS, params: [150] });
|
|
191
|
+
ok('原生 ? 参数绑定生效', JSON.stringify(rowsOf(r)) === '[["b"],["c"],["d"]]', rowsOf(r));
|
|
192
|
+
const r2 = await executeSQL(engine, 'SELECT name FROM emp WHERE dept = ? OR sal > ?', { ...OPTS, params: ['ops', 250] });
|
|
193
|
+
ok('多个 ? 按序绑定', JSON.stringify(rowsOf(r2)) === '[["c"],["d"]]', rowsOf(r2));
|
|
194
|
+
// applyParams 路径(数组入参)依然有效
|
|
195
|
+
const r3 = await executeSQL(engine, 'SELECT name FROM emp WHERE sal > ?', [150]);
|
|
196
|
+
ok('数组入参走 applyParams 仍有效', JSON.stringify(rowsOf(r3)) === '[["b"],["c"],["d"]]', rowsOf(r3));
|
|
197
|
+
|
|
198
|
+
ok('INSERT ... SELECT 可解析', parseSQL('INSERT INTO u SELECT * FROM t').select != null);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ================= 新增:AST 访问与改写 ================= */
|
|
202
|
+
console.log('\n--- 新增: AST 访问与改写 ---');
|
|
203
|
+
{
|
|
204
|
+
const ast = parseSQL('SELECT name, sal FROM emp WHERE sal > 100 ORDER BY sal DESC');
|
|
205
|
+
ok('tables() 列出引用表', JSON.stringify(AST.tables(ast)) === '["emp"]', AST.tables(ast));
|
|
206
|
+
ok('columns() 列出引用列',
|
|
207
|
+
AST.columns(ast).includes('sal') && AST.columns(ast).includes('name'), AST.columns(ast));
|
|
208
|
+
|
|
209
|
+
let count = 0;
|
|
210
|
+
AST.walk(ast, () => { count++; });
|
|
211
|
+
ok('walk 遍历到多个节点', count > 3, count);
|
|
212
|
+
|
|
213
|
+
const compares = AST.collect(ast, n => n.type === 'compare');
|
|
214
|
+
ok('collect 找出 compare 节点', compares.length === 1, compares.length);
|
|
215
|
+
|
|
216
|
+
const found = AST.find(ast, n => n.type === 'column' && n.name === 'sal');
|
|
217
|
+
ok('find 定位指定列', found && found.name === 'sal');
|
|
218
|
+
|
|
219
|
+
// 改写:把列名统一改成小写(此处已是小写,改为验证 transform 生效与不可变性)
|
|
220
|
+
const renamed = AST.transform(ast, (n) => {
|
|
221
|
+
if (n && n.type === 'column' && n.name === 'name') return { ...n, name: 'NAME' };
|
|
222
|
+
});
|
|
223
|
+
ok('transform 改写生效', AST.columns(renamed).includes('NAME'), AST.columns(renamed));
|
|
224
|
+
ok('transform 不修改原 AST', AST.columns(ast).includes('name') && !AST.columns(ast).includes('NAME'));
|
|
225
|
+
|
|
226
|
+
const ast2 = parseSQL('SELECT name FROM emp WHERE sal > 100');
|
|
227
|
+
AST.rewrite(ast2, (n) => {
|
|
228
|
+
if (n && n.type === 'value' && n.value === 100) return { ...n, value: 250 };
|
|
229
|
+
});
|
|
230
|
+
ok('rewrite 原地改写生效',
|
|
231
|
+
AST.find(ast2, n => n.type === 'value').value === 250);
|
|
232
|
+
|
|
233
|
+
class Upper extends AST.StatementVisitor {
|
|
234
|
+
visitColumn(node) { node.name = String(node.name).toUpperCase(); }
|
|
235
|
+
}
|
|
236
|
+
const ast3 = parseSQL('SELECT name FROM emp');
|
|
237
|
+
new Upper().run(ast3);
|
|
238
|
+
ok('StatementVisitor 按类型分派', AST.columns(ast3).includes('NAME'), AST.columns(ast3));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ================= 既有行为回归:新增关键字不得误伤标识符 ================= */
|
|
242
|
+
console.log('\n--- 回归: 新增关键字不得误伤普通标识符 ---');
|
|
243
|
+
{
|
|
244
|
+
for (const col of ['rank', 'row', 'current', 'over', 'window', 'partition', 'unbounded', 'preceding', 'following', 'dense_rank']) {
|
|
245
|
+
let bad = null;
|
|
246
|
+
try { parseSQL(`SELECT ${col} FROM t`); } catch (e) { bad = e.message; }
|
|
247
|
+
ok(`列名为 ${col} 仍可解析`, bad === null, bad);
|
|
248
|
+
}
|
|
249
|
+
ok('表名 with 之外的既有语句不受影响', parseSQL('SELECT * FROM t WHERE a = 1').where.type === 'compare');
|
|
250
|
+
ok('CREATE TABLE 不受影响', parseSQL('CREATE TABLE t (id INTEGER PRIMARY KEY)').type === 'createTable');
|
|
251
|
+
ok('INSERT VALUES 不受影响', parseSQL("INSERT INTO t (a) VALUES (1)").type === 'insert');
|
|
252
|
+
ok('UPDATE 不受影响', parseSQL('UPDATE t SET a = 1').type === 'update');
|
|
253
|
+
ok('DELETE 不受影响', parseSQL('DELETE FROM t WHERE a = 1').type === 'delete');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* ================= 边界与错误输入 ================= */
|
|
257
|
+
console.log('\n--- 边界与错误输入应抛出明确错误而非崩溃 ---');
|
|
258
|
+
{
|
|
259
|
+
const bad = ['SELECT', 'SELECT * FROM', 'SELECT * FROM t WHERE', 'FOO BAR', 'SELECT a FROM t WHERE a'];
|
|
260
|
+
for (const sql of bad) {
|
|
261
|
+
let threw = null;
|
|
262
|
+
try { parseSQL(sql); } catch (e) { threw = e; }
|
|
263
|
+
ok(`畸形输入抛出 Error: ${sql}`, threw instanceof Error);
|
|
264
|
+
}
|
|
265
|
+
ok('空语句 parseSQL 返回 null', parseSQL('') === null);
|
|
266
|
+
let threw = null;
|
|
267
|
+
try { parseSQL('SELECT * FROM t WHERE a = ?'); } catch (e) { threw = e; }
|
|
268
|
+
ok('未绑定参数不应在解析期报错', threw === null);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ================= 5.6.0: 相关子查询(曾静默返回 null) ================= */
|
|
272
|
+
console.log('\n--- 5.6.0: 相关子查询 ---');
|
|
273
|
+
{
|
|
274
|
+
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Cara' }];
|
|
275
|
+
const orders = [{ id: 1, uid: 1 }, { id: 2, uid: 1 }, { id: 3, uid: 1 }, { id: 4, uid: 3 }];
|
|
276
|
+
const rel = {
|
|
277
|
+
hasTable: () => true, truncate: async () => {}, flush: async () => {},
|
|
278
|
+
find: async (t) => (t === 'orders' ? orders : users).map(r => ({ ...r })),
|
|
279
|
+
getTableSchema: async (t) => (t === 'orders'
|
|
280
|
+
? { id: { type: 'integer', primaryKey: true }, uid: { type: 'integer' } }
|
|
281
|
+
: { id: { type: 'integer', primaryKey: true }, name: { type: 'string' } }),
|
|
282
|
+
};
|
|
283
|
+
// README 首页示例:修复前每行都是 null
|
|
284
|
+
const r = await executeSQL(rel, 'SELECT name, (SELECT COUNT(*) FROM orders o WHERE o.uid = u.id) AS c FROM users u', OPTS);
|
|
285
|
+
ok('SELECT 列表相关标量子查询返回正确计数',
|
|
286
|
+
JSON.stringify(rowsOf(r)) === '[["Alice",3],["Bob",0],["Cara",1]]', rowsOf(r));
|
|
287
|
+
const r2 = await executeSQL(rel, 'SELECT name FROM users u WHERE (SELECT COUNT(*) FROM orders o WHERE o.uid = u.id) > 2', OPTS);
|
|
288
|
+
ok('WHERE 中相关标量子查询', JSON.stringify(rowsOf(r2)) === '[["Alice"]]', rowsOf(r2));
|
|
289
|
+
const r3 = await executeSQL(rel, 'SELECT name FROM users u WHERE u.id IN (SELECT uid FROM orders o WHERE o.uid = u.id)', OPTS);
|
|
290
|
+
ok('相关 IN 子查询', JSON.stringify(rowsOf(r3)) === '[["Alice"],["Cara"]]', rowsOf(r3));
|
|
291
|
+
const r4 = await executeSQL(rel, 'SELECT name FROM users WHERE id IN (SELECT uid FROM orders)', OPTS);
|
|
292
|
+
ok('非相关 IN 仍正确', JSON.stringify(rowsOf(r4)) === '[["Alice"],["Cara"]]', rowsOf(r4));
|
|
293
|
+
const r5 = await executeSQL(rel, 'SELECT (SELECT COUNT(*) FROM orders) AS n FROM users LIMIT 1', OPTS);
|
|
294
|
+
ok('非相关标量子查询仍正确', JSON.stringify(rowsOf(r5)) === '[[4]]', rowsOf(r5));
|
|
295
|
+
const r6 = await executeSQL(rel, 'SELECT name FROM users WHERE (id = 1 OR id = 3)', OPTS);
|
|
296
|
+
ok('括号分组未被误判为子查询', JSON.stringify(rowsOf(r6)) === '[["Alice"],["Cara"]]', rowsOf(r6));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ================= 5.6.0: 表达式与算子 ================= */
|
|
300
|
+
console.log('\n--- 5.6.0: CAST / ILIKE / ANY / ORDER BY 表达式 / FETCH FIRST ---');
|
|
301
|
+
{
|
|
302
|
+
ok('CAST 可解析', parseSQL('SELECT CAST(a AS INTEGER) FROM t').columns[0].scalar.type === 'cast');
|
|
303
|
+
const c = await executeSQL(engine, 'SELECT CAST(sal AS TEXT) AS s FROM emp LIMIT 1', OPTS);
|
|
304
|
+
ok('CAST 执行(转字符串)', JSON.stringify(rowsOf(c)) === '[["100"]]', rowsOf(c));
|
|
305
|
+
const c2 = await executeSQL(engine, 'SELECT CAST(sal AS FLOAT) AS f FROM emp LIMIT 1', OPTS);
|
|
306
|
+
ok('CAST 执行(转数值)', JSON.stringify(rowsOf(c2)) === '[[100]]', rowsOf(c2));
|
|
307
|
+
|
|
308
|
+
ok('ILIKE 解析为大小写不敏感 like',
|
|
309
|
+
parseSQL("SELECT * FROM t WHERE a ILIKE 'x'").where.ci === true);
|
|
310
|
+
const i1 = await executeSQL(engine, "SELECT name FROM emp WHERE name ILIKE 'A'", OPTS);
|
|
311
|
+
ok('ILIKE 大小写不敏感', JSON.stringify(rowsOf(i1)) === '[["a"]]', rowsOf(i1));
|
|
312
|
+
|
|
313
|
+
const a1 = await executeSQL(engine, "SELECT name FROM emp WHERE sal = ANY (SELECT sal FROM emp WHERE dept = 'ops')", OPTS);
|
|
314
|
+
ok('= ANY (SELECT ...)', rowsOf(a1).length === 2, rowsOf(a1));
|
|
315
|
+
|
|
316
|
+
ok('ORDER BY 表达式可解析', typeof parseSQL('SELECT * FROM t ORDER BY a + b').orderBy[0].column === 'object');
|
|
317
|
+
const o1 = await executeSQL(engine, 'SELECT sal FROM emp ORDER BY sal + 0 DESC', OPTS);
|
|
318
|
+
ok('ORDER BY 表达式排序', JSON.stringify(rowsOf(o1)) === '[[300],[300],[200],[100]]', rowsOf(o1));
|
|
319
|
+
const o2 = await executeSQL(engine, 'SELECT name FROM emp ORDER BY UPPER(name) DESC', OPTS);
|
|
320
|
+
ok('ORDER BY 函数排序', JSON.stringify(rowsOf(o2)) === '[["d"],["c"],["b"],["a"]]', rowsOf(o2));
|
|
321
|
+
const o3 = await executeSQL(engine, 'SELECT name FROM emp ORDER BY sal LIMIT 1', OPTS);
|
|
322
|
+
ok('ORDER BY 列名 + LIMIT 未被破坏', JSON.stringify(rowsOf(o3)) === '[["a"]]', rowsOf(o3));
|
|
323
|
+
|
|
324
|
+
const f1 = await executeSQL(engine, 'SELECT name FROM emp FETCH FIRST 2 ROWS ONLY', OPTS);
|
|
325
|
+
ok('FETCH FIRST n ROWS ONLY', rowsOf(f1).length === 2, rowsOf(f1));
|
|
326
|
+
ok('FETCH FIRST 写入 limit', parseSQL('SELECT * FROM t FETCH FIRST 7 ROWS ONLY').limit === 7);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* ================= 5.6.0: FULL OUTER JOIN / WITH ROLLUP / EXPLAIN ================= */
|
|
330
|
+
console.log('\n--- 5.6.0: FULL OUTER JOIN / WITH ROLLUP / EXPLAIN ---');
|
|
331
|
+
{
|
|
332
|
+
ok('FULL OUTER JOIN 可解析',
|
|
333
|
+
parseSQL('SELECT * FROM a FULL OUTER JOIN b ON a.id = b.id').from.joins[0].type === 'full');
|
|
334
|
+
|
|
335
|
+
const roll = await executeSQL(engine, 'SELECT dept, SUM(sal) AS total FROM emp GROUP BY dept WITH ROLLUP', OPTS);
|
|
336
|
+
ok('WITH ROLLUP 追加汇总行', rowsOf(roll).length === 3 && rowsOf(roll)[2][0] === null, rowsOf(roll));
|
|
337
|
+
ok('WITH ROLLUP 汇总值正确',
|
|
338
|
+
rowsOf(roll)[2][1] === (100 + 200 + 300 + 300), rowsOf(roll)[2]);
|
|
339
|
+
ok('WITH ROLLUP 可解析', parseSQL('SELECT a, SUM(b) FROM t GROUP BY a WITH ROLLUP').rollup === true);
|
|
340
|
+
|
|
341
|
+
const ex = await executeSQL(engine, 'EXPLAIN SELECT name FROM emp WHERE sal > 50', OPTS);
|
|
342
|
+
ok('EXPLAIN 返回计划行', Array.isArray(rowsOf(ex)) && rowsOf(ex).length > 0, rowsOf(ex));
|
|
343
|
+
ok('EXPLAIN 计划含 SCAN', rowsOf(ex).some(r => String(r[0]).includes('SCAN')), rowsOf(ex));
|
|
344
|
+
ok('EXPLAIN 展示被扫描的表', rowsOf(ex).some(r => r[1] === 'emp'), rowsOf(ex));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ================= 5.6.0: 可写引擎上的 DML 能力 ================= */
|
|
348
|
+
console.log('\n--- 5.6.0: RETURNING / INSERT IGNORE / REPLACE / ON CONFLICT / 视图 ---');
|
|
349
|
+
{
|
|
350
|
+
const SCHEMAS = {
|
|
351
|
+
emp: { id: { type: 'integer', primaryKey: true }, dept: { type: 'string' }, name: { type: 'string' }, sal: { type: 'integer' } },
|
|
352
|
+
};
|
|
353
|
+
const mkEngine = () => {
|
|
354
|
+
const data = { emp: [{ id: 1, dept: 'eng', name: 'a', sal: 100 }] };
|
|
355
|
+
return {
|
|
356
|
+
hasTable: (t) => t in data,
|
|
357
|
+
truncate: async () => {}, flush: async () => {},
|
|
358
|
+
getTableSchema: async (t) => SCHEMAS[t],
|
|
359
|
+
find: async (t) => JSON.parse(JSON.stringify(data[t] || [])),
|
|
360
|
+
insert: async (t, rows) => { data[t] = (data[t] || []).concat(rows); return rows.map(r => r.id); },
|
|
361
|
+
updateById: async (t, id, patch) => { const r = (data[t] || []).find(x => x.id === id); if (r) Object.assign(r, patch); return 1; },
|
|
362
|
+
update: async (t, f, patch) => { let n = 0; for (const r of (data[t] || [])) if (Object.entries(f).every(([k, v]) => String(r[k]) === String(v))) { Object.assign(r, patch); n++; } return n; },
|
|
363
|
+
removeByIds: async (t, ids) => { data[t] = (data[t] || []).filter(r => !ids.includes(r.id)); return ids.length; },
|
|
364
|
+
removeById: async (t, id) => { data[t] = (data[t] || []).filter(r => r.id !== id); return 1; },
|
|
365
|
+
dump: () => JSON.parse(JSON.stringify(data.emp)),
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
ok('RETURNING 可解析', Array.isArray(parseSQL('INSERT INTO t (a) VALUES (1) RETURNING a').returning));
|
|
370
|
+
ok('REPLACE 可解析', parseSQL('REPLACE INTO t (a) VALUES (1)').replace === true);
|
|
371
|
+
ok('INSERT IGNORE 可解析', parseSQL('INSERT IGNORE INTO t (a) VALUES (1)').ignore === true);
|
|
372
|
+
ok('ON CONFLICT DO NOTHING 可解析',
|
|
373
|
+
parseSQL('INSERT INTO t (a) VALUES (1) ON CONFLICT (a) DO NOTHING').onConflict.action === 'nothing');
|
|
374
|
+
ok('ON CONFLICT DO UPDATE 可解析',
|
|
375
|
+
parseSQL('INSERT INTO t (a) VALUES (1) ON CONFLICT (a) DO UPDATE SET a = 2').onConflict.action === 'update');
|
|
376
|
+
ok('SAVEPOINT 可解析', parseSQL('SAVEPOINT sp1').type === 'savepoint');
|
|
377
|
+
ok('ROLLBACK TO 可解析', parseSQL('ROLLBACK TO SAVEPOINT sp1').type === 'rollbackTo');
|
|
378
|
+
ok('RELEASE SAVEPOINT 可解析', parseSQL('RELEASE SAVEPOINT sp1').type === 'releaseSavepoint');
|
|
379
|
+
ok('CREATE VIEW 可解析', parseSQL('CREATE VIEW v AS SELECT * FROM t').type === 'createView');
|
|
380
|
+
ok('DROP VIEW IF EXISTS 可解析', parseSQL('DROP VIEW IF EXISTS v').ifExists === true);
|
|
381
|
+
|
|
382
|
+
const e1 = mkEngine();
|
|
383
|
+
const ins = await executeSQL(e1, "INSERT INTO emp (id, dept, name, sal) VALUES (9, 'x', 'z', 1) RETURNING id, name", OPTS);
|
|
384
|
+
ok('INSERT ... RETURNING 返回指定列',
|
|
385
|
+
JSON.stringify(ins.returning.rows) === '[[9,"z"]]', ins.returning);
|
|
386
|
+
|
|
387
|
+
const e2 = mkEngine();
|
|
388
|
+
const del = await executeSQL(e2, 'DELETE FROM emp WHERE id = 1 RETURNING *', OPTS);
|
|
389
|
+
ok('DELETE ... RETURNING 返回被删行',
|
|
390
|
+
del.returning.rows.length === 1 && del.returning.rows[0][0] === 1, del.returning);
|
|
391
|
+
|
|
392
|
+
const e3 = mkEngine();
|
|
393
|
+
const up = await executeSQL(e3, 'UPDATE emp SET sal = 555 WHERE id = 1 RETURNING id, sal', OPTS);
|
|
394
|
+
ok('UPDATE ... RETURNING 返回更新后行',
|
|
395
|
+
JSON.stringify(up.returning.rows) === '[[1,555]]', up.returning);
|
|
396
|
+
|
|
397
|
+
const e4 = mkEngine();
|
|
398
|
+
let dupErr = null;
|
|
399
|
+
try { await executeSQL(e4, "INSERT INTO emp (id, dept) VALUES (1, 'dup')", OPTS); } catch (err) { dupErr = err; }
|
|
400
|
+
ok('默认主键冲突仍报 ER_DUP_ENTRY', dupErr && /ER_DUP_ENTRY/.test(dupErr.message));
|
|
401
|
+
|
|
402
|
+
const e5 = mkEngine();
|
|
403
|
+
const ig = await executeSQL(e5, "INSERT IGNORE INTO emp (id, dept) VALUES (1, 'ignored')", OPTS);
|
|
404
|
+
ok('INSERT IGNORE 跳过冲突且不改数据',
|
|
405
|
+
ig.affectedRows === 0 && ig.duplicateSkipped === 1 && e5.dump().length === 1, { ig, rows: e5.dump() });
|
|
406
|
+
|
|
407
|
+
const e6 = mkEngine();
|
|
408
|
+
const oc = await executeSQL(e6, "INSERT INTO emp (id, dept) VALUES (1, 'x') ON CONFLICT (id) DO NOTHING", OPTS);
|
|
409
|
+
ok('ON CONFLICT DO NOTHING 跳过', oc.affectedRows === 0 && e6.dump()[0].dept === 'eng', e6.dump());
|
|
410
|
+
|
|
411
|
+
const e7 = mkEngine();
|
|
412
|
+
await executeSQL(e7, "INSERT INTO emp (id, dept) VALUES (1, 'x') ON CONFLICT (id) DO UPDATE SET dept = 'upd2'", OPTS);
|
|
413
|
+
ok('ON CONFLICT DO UPDATE 生效', e7.dump()[0].dept === 'upd2', e7.dump());
|
|
414
|
+
|
|
415
|
+
const e8 = mkEngine();
|
|
416
|
+
await executeSQL(e8, "REPLACE INTO emp (id, dept, name, sal) VALUES (1, 'replaced', 'r', 7)", OPTS);
|
|
417
|
+
ok('REPLACE INTO 替换整行', e8.dump()[0].name === 'r' && e8.dump()[0].sal === 7, e8.dump());
|
|
418
|
+
|
|
419
|
+
// 视图
|
|
420
|
+
const e9 = mkEngine();
|
|
421
|
+
await executeSQL(e9, "INSERT INTO emp (id, dept, name, sal) VALUES (2, 'ops', 'b', 200)", OPTS);
|
|
422
|
+
await executeSQL(e9, "CREATE VIEW v_eng AS SELECT * FROM emp WHERE dept = 'eng'", OPTS);
|
|
423
|
+
const vq = await executeSQL(e9, 'SELECT name FROM v_eng', OPTS);
|
|
424
|
+
ok('视图可查询', JSON.stringify(rowsOf(vq)) === '[["a"]]', rowsOf(vq));
|
|
425
|
+
let viewErr = null;
|
|
426
|
+
try { await executeSQL(e9, 'CREATE VIEW v_eng AS SELECT * FROM emp', OPTS); } catch (err) { viewErr = err; }
|
|
427
|
+
ok('重复创建视图报错', viewErr && /already exists/.test(viewErr.message));
|
|
428
|
+
await executeSQL(e9, 'CREATE OR REPLACE VIEW v_eng AS SELECT * FROM emp', OPTS);
|
|
429
|
+
const vq2 = await executeSQL(e9, 'SELECT COUNT(*) AS n FROM v_eng', OPTS);
|
|
430
|
+
ok('CREATE OR REPLACE VIEW 覆盖生效', JSON.stringify(rowsOf(vq2)) === '[[2]]', rowsOf(vq2));
|
|
431
|
+
await executeSQL(e9, 'DROP VIEW v_eng', OPTS);
|
|
432
|
+
let goneErr = null;
|
|
433
|
+
try { await executeSQL(e9, 'SELECT name FROM v_eng', OPTS); } catch (err) { goneErr = err; }
|
|
434
|
+
ok('DROP VIEW 后视图不可查', goneErr !== null);
|
|
435
|
+
const skippedDrop = await executeSQL(e9, 'DROP VIEW IF EXISTS nope', OPTS);
|
|
436
|
+
ok('DROP VIEW IF EXISTS 不报错', skippedDrop && skippedDrop.skipped === true);
|
|
437
|
+
|
|
438
|
+
// 保存点:明确不支持,绝不静默假装成功
|
|
439
|
+
const e10 = mkEngine();
|
|
440
|
+
const sp = await executeSQL(e10, 'SAVEPOINT sp1', OPTS);
|
|
441
|
+
ok('SAVEPOINT 返回登记结果', sp && sp.type === 'savepoint');
|
|
442
|
+
let rbErr = null;
|
|
443
|
+
try { await executeSQL(e10, 'ROLLBACK TO SAVEPOINT sp1', OPTS); } catch (err) { rbErr = err; }
|
|
444
|
+
ok('ROLLBACK TO 明确报不支持(不静默假成功)',
|
|
445
|
+
rbErr instanceof Error && /not supported/.test(rbErr.message));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
449
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
450
|
+
})();
|
package/wasm/browser.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
jsql_reset,
|
|
3
3
|
jsql_create_table,
|
|
4
|
+
jsql_add_column,
|
|
4
5
|
jsql_drop_table,
|
|
5
6
|
jsql_insert_json,
|
|
6
7
|
jsql_find,
|
|
@@ -395,6 +396,17 @@ export class JSQL {
|
|
|
395
396
|
return r;
|
|
396
397
|
}
|
|
397
398
|
|
|
399
|
+
async addColumn(table, name, def) {
|
|
400
|
+
await this._flush();
|
|
401
|
+
const mapped = mapNativeSchema({ [name]: def })[name];
|
|
402
|
+
const r = safeJsonParse(jsql_add_column(table, name, JSON.stringify(mapped)));
|
|
403
|
+
if (r && r.ok === false) throw new Error(r.error || 'add column failed');
|
|
404
|
+
if (!this._schemas[table]) this._schemas[table] = {};
|
|
405
|
+
this._schemas[table][name] = mapped;
|
|
406
|
+
this._emit('addColumn', { table, name, def: mapped });
|
|
407
|
+
return r;
|
|
408
|
+
}
|
|
409
|
+
|
|
398
410
|
async dropTable(name) {
|
|
399
411
|
await this._flush();
|
|
400
412
|
if (!this._runHooks('beforeDropTable', [name])) return null;
|