jsql-neo 5.4.2 → 5.5.0
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/LICENSE +202 -202
- package/README.md +8097 -7971
- package/bin/jsql +320 -320
- package/bin/jsql-server +90 -90
- package/bin/jsql-server-control.js +177 -177
- package/index.d.ts +385 -357
- package/index.js +124 -118
- package/lib/ast.js +214 -0
- package/lib/btree.js +413 -413
- package/lib/cache.js +328 -328
- package/lib/client.js +125 -125
- package/lib/database.js +1744 -1744
- package/lib/date-types.js +182 -182
- package/lib/errors.js +89 -89
- package/lib/jsql_format.js +497 -497
- package/lib/migrate.js +254 -254
- package/lib/mod.js +292 -292
- package/lib/mongo_server.js +607 -607
- package/lib/multiserver.js +140 -140
- package/lib/mysql_compat.js +413 -413
- package/lib/mysql_server.js +1237 -1237
- package/lib/native_client.js +732 -732
- package/lib/nedb_compat.js +506 -506
- package/lib/pg_server.js +864 -864
- package/lib/plugin.js +34 -34
- package/lib/query.js +815 -815
- package/lib/redis_server.js +571 -571
- package/lib/sql.js +3460 -2925
- package/lib/sqlite_compat.js +323 -323
- package/lib/sqlite_worker.js +264 -264
- package/lib/table.js +1304 -1304
- package/lib/tui.js +502 -502
- package/lib/wasm.js +1 -1
- package/lib/wasm_client.js +718 -718
- package/lib/web_ui.js +257 -257
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
- package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
- package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
- package/nativesrc/jsql-neo-core/src/types.rs +93 -93
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
- package/nativesrc/jsql-neo-native/build.rs +3 -3
- package/nativesrc/jsql-neo-native/package.json +7 -7
- package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
- package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
- package/package.json +87 -86
- package/postinstall.js +158 -158
- package/test/btree.test.js +128 -128
- package/test/coverage.js +187 -187
- package/test/join.test.js +110 -110
- package/test/native.test.js +158 -158
- package/test/regress-5.1.0.js +271 -271
- package/test/smoke.js +58 -58
- package/test/sql-parser.test.js +273 -0
- package/test/wasm.test.js +110 -110
- package/wasm/browser.d.ts +89 -89
- package/wasm/browser.mjs +569 -569
- package/wasm/browser_bg.mjs +597 -597
- package/wasm/jsql_neo_wasm.d.ts +38 -38
- package/wasm/jsql_neo_wasm.js +520 -520
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
- package/wasm/package.json +11 -11
package/test/smoke.js
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Zero-dependency smoke test for the SQL engine core.
|
|
3
|
-
* npm test (or: node test/smoke.js)
|
|
4
|
-
*/
|
|
5
|
-
const { executeSQL, parseSQL, applyParams } = require('../lib/sql.js');
|
|
6
|
-
const { createMysqlServer } = require('../lib/mysql_server.js');
|
|
7
|
-
|
|
8
|
-
let pass = 0, fail = 0;
|
|
9
|
-
const ok = (name, cond, extra) => {
|
|
10
|
-
if (cond) { pass++; console.log('[OK]', name); }
|
|
11
|
-
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '→ ' + JSON.stringify(extra) : ''); }
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const engine = {
|
|
15
|
-
hasTable: () => true,
|
|
16
|
-
truncate: async () => {},
|
|
17
|
-
flush: async () => {},
|
|
18
|
-
find: async () => [
|
|
19
|
-
{ id: 1, name: 'A', age: 30 },
|
|
20
|
-
{ id: 2, name: 'B', age: 40 },
|
|
21
|
-
{ id: 3, name: 'C', age: 25 },
|
|
22
|
-
],
|
|
23
|
-
getTableSchema: async () => ({
|
|
24
|
-
id: { type: 'integer', primaryKey: true },
|
|
25
|
-
name: { type: 'string' },
|
|
26
|
-
age: { type: 'integer' },
|
|
27
|
-
}),
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
(async () => {
|
|
31
|
-
const opts = { safety: false };
|
|
32
|
-
|
|
33
|
-
const agg = await executeSQL(engine, 'SELECT COUNT(*) AS n, MAX(age) AS m, SUM(age) AS s, AVG(age) AS a FROM users', opts);
|
|
34
|
-
ok('multi-aggregate SELECT returns all columns', JSON.stringify(agg.columns) === JSON.stringify(['n', 'm', 's', 'a'])
|
|
35
|
-
&& JSON.stringify(agg.rows[0]) === JSON.stringify([3, 40, 95, 31.666666666666668]), { cols: agg.columns, row: agg.rows[0] });
|
|
36
|
-
|
|
37
|
-
const w = await executeSQL(engine, 'SELECT name, age FROM users WHERE age > 29 ORDER BY age DESC', opts);
|
|
38
|
-
ok('WHERE + ORDER BY', w.rows.length === 2 && w.rows[0][1] === 40, w.rows);
|
|
39
|
-
|
|
40
|
-
const funcs = await executeSQL(engine, "SELECT CONCAT(name, '!') AS c, UPPER(name) AS u, IFNULL(age, 0) AS f FROM users LIMIT 1", opts);
|
|
41
|
-
ok('scalar functions CONCAT/UPPER/IFNULL', funcs.rows[0][0] === 'A!' && funcs.rows[0][1] === 'A', funcs.rows);
|
|
42
|
-
|
|
43
|
-
const c1 = await executeSQL(engine, 'SELECT COUNT(1) AS n FROM users', opts);
|
|
44
|
-
ok('COUNT(1) literal', c1.rows[0][0] === 3, c1.rows);
|
|
45
|
-
|
|
46
|
-
ok('parseSQL TRUNCATE', parseSQL('TRUNCATE TABLE users').type === 'truncate');
|
|
47
|
-
ok('parseSQL START TRANSACTION', parseSQL('START TRANSACTION').type === 'begin');
|
|
48
|
-
ok('parseSQL VALUES DEFAULT', parseSQL(applyParams('INSERT INTO t (id, name) VALUES (DEFAULT, ?)', ['x'])).dataRows[0].id._default === true);
|
|
49
|
-
|
|
50
|
-
const srv = createMysqlServer({ port: 0, host: '127.0.0.1', dataDir: ':memory:', noAuth: true });
|
|
51
|
-
await new Promise(r => srv.listen(r));
|
|
52
|
-
const addr = srv.address;
|
|
53
|
-
ok('createMysqlServer listens on ephemeral port', addr && addr.port > 0, addr);
|
|
54
|
-
srv.close();
|
|
55
|
-
|
|
56
|
-
console.log(fail === 0 ? '\nALL SMOKE TESTS PASSED' : `\n${fail} FAILURES`);
|
|
57
|
-
process.exit(fail === 0 ? 0 : 1);
|
|
58
|
-
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
|
|
1
|
+
/*
|
|
2
|
+
* Zero-dependency smoke test for the SQL engine core.
|
|
3
|
+
* npm test (or: node test/smoke.js)
|
|
4
|
+
*/
|
|
5
|
+
const { executeSQL, parseSQL, applyParams } = require('../lib/sql.js');
|
|
6
|
+
const { createMysqlServer } = require('../lib/mysql_server.js');
|
|
7
|
+
|
|
8
|
+
let pass = 0, fail = 0;
|
|
9
|
+
const ok = (name, cond, extra) => {
|
|
10
|
+
if (cond) { pass++; console.log('[OK]', name); }
|
|
11
|
+
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '→ ' + JSON.stringify(extra) : ''); }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const engine = {
|
|
15
|
+
hasTable: () => true,
|
|
16
|
+
truncate: async () => {},
|
|
17
|
+
flush: async () => {},
|
|
18
|
+
find: async () => [
|
|
19
|
+
{ id: 1, name: 'A', age: 30 },
|
|
20
|
+
{ id: 2, name: 'B', age: 40 },
|
|
21
|
+
{ id: 3, name: 'C', age: 25 },
|
|
22
|
+
],
|
|
23
|
+
getTableSchema: async () => ({
|
|
24
|
+
id: { type: 'integer', primaryKey: true },
|
|
25
|
+
name: { type: 'string' },
|
|
26
|
+
age: { type: 'integer' },
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
(async () => {
|
|
31
|
+
const opts = { safety: false };
|
|
32
|
+
|
|
33
|
+
const agg = await executeSQL(engine, 'SELECT COUNT(*) AS n, MAX(age) AS m, SUM(age) AS s, AVG(age) AS a FROM users', opts);
|
|
34
|
+
ok('multi-aggregate SELECT returns all columns', JSON.stringify(agg.columns) === JSON.stringify(['n', 'm', 's', 'a'])
|
|
35
|
+
&& JSON.stringify(agg.rows[0]) === JSON.stringify([3, 40, 95, 31.666666666666668]), { cols: agg.columns, row: agg.rows[0] });
|
|
36
|
+
|
|
37
|
+
const w = await executeSQL(engine, 'SELECT name, age FROM users WHERE age > 29 ORDER BY age DESC', opts);
|
|
38
|
+
ok('WHERE + ORDER BY', w.rows.length === 2 && w.rows[0][1] === 40, w.rows);
|
|
39
|
+
|
|
40
|
+
const funcs = await executeSQL(engine, "SELECT CONCAT(name, '!') AS c, UPPER(name) AS u, IFNULL(age, 0) AS f FROM users LIMIT 1", opts);
|
|
41
|
+
ok('scalar functions CONCAT/UPPER/IFNULL', funcs.rows[0][0] === 'A!' && funcs.rows[0][1] === 'A', funcs.rows);
|
|
42
|
+
|
|
43
|
+
const c1 = await executeSQL(engine, 'SELECT COUNT(1) AS n FROM users', opts);
|
|
44
|
+
ok('COUNT(1) literal', c1.rows[0][0] === 3, c1.rows);
|
|
45
|
+
|
|
46
|
+
ok('parseSQL TRUNCATE', parseSQL('TRUNCATE TABLE users').type === 'truncate');
|
|
47
|
+
ok('parseSQL START TRANSACTION', parseSQL('START TRANSACTION').type === 'begin');
|
|
48
|
+
ok('parseSQL VALUES DEFAULT', parseSQL(applyParams('INSERT INTO t (id, name) VALUES (DEFAULT, ?)', ['x'])).dataRows[0].id._default === true);
|
|
49
|
+
|
|
50
|
+
const srv = createMysqlServer({ port: 0, host: '127.0.0.1', dataDir: ':memory:', noAuth: true });
|
|
51
|
+
await new Promise(r => srv.listen(r));
|
|
52
|
+
const addr = srv.address;
|
|
53
|
+
ok('createMysqlServer listens on ephemeral port', addr && addr.port > 0, addr);
|
|
54
|
+
srv.close();
|
|
55
|
+
|
|
56
|
+
console.log(fail === 0 ? '\nALL SMOKE TESTS PASSED' : `\n${fail} FAILURES`);
|
|
57
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
58
|
+
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
|
|
@@ -0,0 +1,273 @@
|
|
|
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
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
272
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
273
|
+
})();
|
package/test/wasm.test.js
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* WASM (.wasm) regression tests
|
|
3
|
-
* node test/wasm.test.js
|
|
4
|
-
* Covers: JSQL CRUD + flat row shape, shorthand schemas, pk-aware by-id CRUD,
|
|
5
|
-
* non-auto pk insert backfill, and beginTransaction / rollback / commit.
|
|
6
|
-
*/
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const ROOT = path.join(__dirname, '..');
|
|
9
|
-
|
|
10
|
-
let passed = 0, failed = 0;
|
|
11
|
-
function ok(name, cond, extra) {
|
|
12
|
-
if (cond) { passed++; console.log('[OK]', name); }
|
|
13
|
-
else { failed++; console.log('[FAIL]', name, extra !== undefined ? JSON.stringify(extra) : ''); }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
(async () => {
|
|
17
|
-
const { JSQL } = require(path.join(ROOT, 'lib', 'wasm_client'));
|
|
18
|
-
|
|
19
|
-
/* ---------- basic CRUD + flat row shape ---------- */
|
|
20
|
-
{
|
|
21
|
-
const db = new JSQL({ modules: false });
|
|
22
|
-
await db.start();
|
|
23
|
-
await db.createTable('users', { id: 'integer primary key auto_increment', name: 'string', age: 'integer' });
|
|
24
|
-
const ids = await db.insert('users', [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]);
|
|
25
|
-
ok('wasm batch insert returns ids', Array.isArray(ids) && ids.length === 2, ids);
|
|
26
|
-
|
|
27
|
-
const row = await db.findById('users', 1);
|
|
28
|
-
ok('wasm findById flat row', row && row.id === 1 && row.name === 'Alice' && !('fields' in row), row);
|
|
29
|
-
|
|
30
|
-
const rows = await db.find('users', {});
|
|
31
|
-
ok('wasm find returns flat rows', rows.length === 2 && rows[1].name === 'Bob', rows);
|
|
32
|
-
ok('wasm filter match', (await db.find('users', { age: 25 })).length === 1);
|
|
33
|
-
ok('wasm count', (await db.count('users')) === 2);
|
|
34
|
-
|
|
35
|
-
await db.updateById('users', 1, { age: 31 });
|
|
36
|
-
ok('wasm updateById applied', (await db.findById('users', 1)).age === 31, await db.findById('users', 1));
|
|
37
|
-
|
|
38
|
-
await db.updateByIds('users', [[2, { age: 99 }]]);
|
|
39
|
-
ok('wasm updateByIds applied', (await db.findById('users', 2)).age === 99, await db.findById('users', 2));
|
|
40
|
-
|
|
41
|
-
ok('wasm findByIds', (await db.findByIds('users', [1, 2])).length === 2);
|
|
42
|
-
|
|
43
|
-
await db.removeById('users', 1);
|
|
44
|
-
ok('wasm removeById applied', (await db.count('users')) === 1, await db.count('users'));
|
|
45
|
-
|
|
46
|
-
await db.removeByIds('users', [2]);
|
|
47
|
-
ok('wasm removeByIds applied', (await db.count('users')) === 0, await db.count('users'));
|
|
48
|
-
await db.stop();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/* ---------- non-auto pk: id resolution by pk value ---------- */
|
|
52
|
-
{
|
|
53
|
-
const db = new JSQL({ modules: false });
|
|
54
|
-
await db.start();
|
|
55
|
-
await db.createTable('config', { key: { type: 'string', primaryKey: true }, value: 'string' });
|
|
56
|
-
const ids = await db.insert('config', [{ key: 'a', value: '1' }, { key: 'b', value: '2' }]);
|
|
57
|
-
ok('wasm non-auto pk insert returns pk values', JSON.stringify(ids) === JSON.stringify(['a', 'b']), ids);
|
|
58
|
-
|
|
59
|
-
const a = await db.findById('config', 'a');
|
|
60
|
-
ok('wasm findById resolves string pk', a && a.value === '1' && !('fields' in a), a);
|
|
61
|
-
ok('wasm findById pk miss returns null', (await db.findById('config', 'nope')) === null);
|
|
62
|
-
|
|
63
|
-
await db.updateById('config', 'b', { value: '22' });
|
|
64
|
-
ok('wasm updateById resolves string pk', (await db.findById('config', 'b')).value === '22', await db.findById('config', 'b'));
|
|
65
|
-
|
|
66
|
-
await db.removeById('config', 'a');
|
|
67
|
-
ok('wasm removeById resolves string pk', (await db.count('config')) === 1, await db.count('config'));
|
|
68
|
-
|
|
69
|
-
await db.removeByIds('config', ['b']);
|
|
70
|
-
ok('wasm removeByIds resolves string pk', (await db.count('config')) === 0, await db.count('config'));
|
|
71
|
-
await db.stop();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* ---------- shorthand schema ---------- */
|
|
75
|
-
{
|
|
76
|
-
const db = new JSQL({ modules: false });
|
|
77
|
-
await db.start();
|
|
78
|
-
await db.createTable('t2', { name: 'string unique', n: 'number', ok: 'boolean' });
|
|
79
|
-
await db.insert('t2', { name: 'x', n: 1.5, ok: true });
|
|
80
|
-
const r2 = (await db.find('t2', {}))[0];
|
|
81
|
-
ok('wasm shorthand schema fields', r2.name === 'x' && r2.n === 1.5 && r2.ok === true, r2);
|
|
82
|
-
|
|
83
|
-
await db.createTable('cfg', 'key string primary key, value string');
|
|
84
|
-
const cfgIds = await db.insert('cfg', { key: 'a', value: '1' });
|
|
85
|
-
ok('wasm full shorthand string schema', cfgIds[0] === 'a' && (await db.findById('cfg', 'a')).value === '1', cfgIds);
|
|
86
|
-
await db.stop();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* ---------- transactions ---------- */
|
|
90
|
-
{
|
|
91
|
-
const db = new JSQL({ modules: false });
|
|
92
|
-
await db.start();
|
|
93
|
-
await db.createTable('tx', { id: 'integer primary key auto_increment', name: 'string' });
|
|
94
|
-
await db.insert('tx', { name: 'n1' });
|
|
95
|
-
await db.beginTransaction();
|
|
96
|
-
await db.insert('tx', { name: 'n2' });
|
|
97
|
-
await db.rollback();
|
|
98
|
-
ok('wasm rollback reverts insert', (await db.count('tx')) === 1, await db.count('tx'));
|
|
99
|
-
await db.beginTransaction();
|
|
100
|
-
await db.insert('tx', { name: 'n3' });
|
|
101
|
-
await db.commit();
|
|
102
|
-
ok('wasm commit keeps insert', (await db.count('tx')) === 2, await db.count('tx'));
|
|
103
|
-
await db.stop();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
107
|
-
process.exit(failed ? 1 : 0);
|
|
108
|
-
})().catch(e => {
|
|
109
|
-
console.error('[FATAL]', e.stack.split('\n').slice(0, 3).join('\n'));
|
|
110
|
-
process.exit(1);
|
|
1
|
+
/*
|
|
2
|
+
* WASM (.wasm) regression tests
|
|
3
|
+
* node test/wasm.test.js
|
|
4
|
+
* Covers: JSQL CRUD + flat row shape, shorthand schemas, pk-aware by-id CRUD,
|
|
5
|
+
* non-auto pk insert backfill, and beginTransaction / rollback / commit.
|
|
6
|
+
*/
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const ROOT = path.join(__dirname, '..');
|
|
9
|
+
|
|
10
|
+
let passed = 0, failed = 0;
|
|
11
|
+
function ok(name, cond, extra) {
|
|
12
|
+
if (cond) { passed++; console.log('[OK]', name); }
|
|
13
|
+
else { failed++; console.log('[FAIL]', name, extra !== undefined ? JSON.stringify(extra) : ''); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
(async () => {
|
|
17
|
+
const { JSQL } = require(path.join(ROOT, 'lib', 'wasm_client'));
|
|
18
|
+
|
|
19
|
+
/* ---------- basic CRUD + flat row shape ---------- */
|
|
20
|
+
{
|
|
21
|
+
const db = new JSQL({ modules: false });
|
|
22
|
+
await db.start();
|
|
23
|
+
await db.createTable('users', { id: 'integer primary key auto_increment', name: 'string', age: 'integer' });
|
|
24
|
+
const ids = await db.insert('users', [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]);
|
|
25
|
+
ok('wasm batch insert returns ids', Array.isArray(ids) && ids.length === 2, ids);
|
|
26
|
+
|
|
27
|
+
const row = await db.findById('users', 1);
|
|
28
|
+
ok('wasm findById flat row', row && row.id === 1 && row.name === 'Alice' && !('fields' in row), row);
|
|
29
|
+
|
|
30
|
+
const rows = await db.find('users', {});
|
|
31
|
+
ok('wasm find returns flat rows', rows.length === 2 && rows[1].name === 'Bob', rows);
|
|
32
|
+
ok('wasm filter match', (await db.find('users', { age: 25 })).length === 1);
|
|
33
|
+
ok('wasm count', (await db.count('users')) === 2);
|
|
34
|
+
|
|
35
|
+
await db.updateById('users', 1, { age: 31 });
|
|
36
|
+
ok('wasm updateById applied', (await db.findById('users', 1)).age === 31, await db.findById('users', 1));
|
|
37
|
+
|
|
38
|
+
await db.updateByIds('users', [[2, { age: 99 }]]);
|
|
39
|
+
ok('wasm updateByIds applied', (await db.findById('users', 2)).age === 99, await db.findById('users', 2));
|
|
40
|
+
|
|
41
|
+
ok('wasm findByIds', (await db.findByIds('users', [1, 2])).length === 2);
|
|
42
|
+
|
|
43
|
+
await db.removeById('users', 1);
|
|
44
|
+
ok('wasm removeById applied', (await db.count('users')) === 1, await db.count('users'));
|
|
45
|
+
|
|
46
|
+
await db.removeByIds('users', [2]);
|
|
47
|
+
ok('wasm removeByIds applied', (await db.count('users')) === 0, await db.count('users'));
|
|
48
|
+
await db.stop();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ---------- non-auto pk: id resolution by pk value ---------- */
|
|
52
|
+
{
|
|
53
|
+
const db = new JSQL({ modules: false });
|
|
54
|
+
await db.start();
|
|
55
|
+
await db.createTable('config', { key: { type: 'string', primaryKey: true }, value: 'string' });
|
|
56
|
+
const ids = await db.insert('config', [{ key: 'a', value: '1' }, { key: 'b', value: '2' }]);
|
|
57
|
+
ok('wasm non-auto pk insert returns pk values', JSON.stringify(ids) === JSON.stringify(['a', 'b']), ids);
|
|
58
|
+
|
|
59
|
+
const a = await db.findById('config', 'a');
|
|
60
|
+
ok('wasm findById resolves string pk', a && a.value === '1' && !('fields' in a), a);
|
|
61
|
+
ok('wasm findById pk miss returns null', (await db.findById('config', 'nope')) === null);
|
|
62
|
+
|
|
63
|
+
await db.updateById('config', 'b', { value: '22' });
|
|
64
|
+
ok('wasm updateById resolves string pk', (await db.findById('config', 'b')).value === '22', await db.findById('config', 'b'));
|
|
65
|
+
|
|
66
|
+
await db.removeById('config', 'a');
|
|
67
|
+
ok('wasm removeById resolves string pk', (await db.count('config')) === 1, await db.count('config'));
|
|
68
|
+
|
|
69
|
+
await db.removeByIds('config', ['b']);
|
|
70
|
+
ok('wasm removeByIds resolves string pk', (await db.count('config')) === 0, await db.count('config'));
|
|
71
|
+
await db.stop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ---------- shorthand schema ---------- */
|
|
75
|
+
{
|
|
76
|
+
const db = new JSQL({ modules: false });
|
|
77
|
+
await db.start();
|
|
78
|
+
await db.createTable('t2', { name: 'string unique', n: 'number', ok: 'boolean' });
|
|
79
|
+
await db.insert('t2', { name: 'x', n: 1.5, ok: true });
|
|
80
|
+
const r2 = (await db.find('t2', {}))[0];
|
|
81
|
+
ok('wasm shorthand schema fields', r2.name === 'x' && r2.n === 1.5 && r2.ok === true, r2);
|
|
82
|
+
|
|
83
|
+
await db.createTable('cfg', 'key string primary key, value string');
|
|
84
|
+
const cfgIds = await db.insert('cfg', { key: 'a', value: '1' });
|
|
85
|
+
ok('wasm full shorthand string schema', cfgIds[0] === 'a' && (await db.findById('cfg', 'a')).value === '1', cfgIds);
|
|
86
|
+
await db.stop();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ---------- transactions ---------- */
|
|
90
|
+
{
|
|
91
|
+
const db = new JSQL({ modules: false });
|
|
92
|
+
await db.start();
|
|
93
|
+
await db.createTable('tx', { id: 'integer primary key auto_increment', name: 'string' });
|
|
94
|
+
await db.insert('tx', { name: 'n1' });
|
|
95
|
+
await db.beginTransaction();
|
|
96
|
+
await db.insert('tx', { name: 'n2' });
|
|
97
|
+
await db.rollback();
|
|
98
|
+
ok('wasm rollback reverts insert', (await db.count('tx')) === 1, await db.count('tx'));
|
|
99
|
+
await db.beginTransaction();
|
|
100
|
+
await db.insert('tx', { name: 'n3' });
|
|
101
|
+
await db.commit();
|
|
102
|
+
ok('wasm commit keeps insert', (await db.count('tx')) === 2, await db.count('tx'));
|
|
103
|
+
await db.stop();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
107
|
+
process.exit(failed ? 1 : 0);
|
|
108
|
+
})().catch(e => {
|
|
109
|
+
console.error('[FATAL]', e.stack.split('\n').slice(0, 3).join('\n'));
|
|
110
|
+
process.exit(1);
|
|
111
111
|
});
|