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/btree.test.js
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* B-Tree 专项回归测试 — 覆盖 GitHub Issue #3 的三个 bug 及随机混合操作不变量。
|
|
3
|
-
* node test/btree.test.js
|
|
4
|
-
*/
|
|
5
|
-
const BTree = require('../lib/btree');
|
|
6
|
-
|
|
7
|
-
let passed = 0, failed = 0;
|
|
8
|
-
function ok(name, cond, extra) {
|
|
9
|
-
if (cond) { passed++; }
|
|
10
|
-
else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function validate(bt) {
|
|
14
|
-
const errs = [];
|
|
15
|
-
(function walk(node) {
|
|
16
|
-
if (node.leaf) {
|
|
17
|
-
if (node.children.length !== 0) errs.push('leaf has children');
|
|
18
|
-
for (let i = 1; i < node.keys.length; i++) if (node.keys[i] <= node.keys[i - 1]) errs.push('leaf unsorted');
|
|
19
|
-
} else {
|
|
20
|
-
if (node.children.length !== node.keys.length + 1) errs.push('internal children !== keys+1');
|
|
21
|
-
for (let i = 1; i < node.keys.length; i++) if (node.keys[i] <= node.keys[i - 1]) errs.push('internal unsorted');
|
|
22
|
-
node.children.forEach(walk);
|
|
23
|
-
}
|
|
24
|
-
})(bt._root);
|
|
25
|
-
return errs;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/* ---- Issue #3 Bug #1: 删除崩溃 ---- */
|
|
29
|
-
{
|
|
30
|
-
const bt = new BTree(4);
|
|
31
|
-
for (let i = 0; i < 20; i++) bt.insert(i, i);
|
|
32
|
-
const removed = bt.remove(0, 0);
|
|
33
|
-
ok('issue#3 Bug#1 remove(0,0) no crash', removed === true && bt.search(0).length === 0 && bt.size === 19);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/* ---- Issue #3 Bug #2: entries() 乱序重复 ---- */
|
|
37
|
-
{
|
|
38
|
-
const bt = new BTree(4);
|
|
39
|
-
for (let i = 0; i < 20; i++) bt.insert(i, i);
|
|
40
|
-
const e = bt.entries();
|
|
41
|
-
let dup = 0, disorder = 0;
|
|
42
|
-
for (let i = 0; i < e.length; i++) {
|
|
43
|
-
if (i > 0 && e[i].key <= e[i - 1].key) disorder++;
|
|
44
|
-
for (let j = i + 1; j < e.length; j++) if (e[i].key === e[j].key) dup++;
|
|
45
|
-
}
|
|
46
|
-
ok('issue#3 Bug#2 entries sorted & dedup', e.length === 20 && dup === 0 && disorder === 0, { len: e.length, dup, disorder });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* ---- Issue #3 Bug #3: 唯一索引返回所有 values ---- */
|
|
50
|
-
{
|
|
51
|
-
const bt = new BTree(4, true);
|
|
52
|
-
bt.insert('a', 1); bt.insert('a', 2); bt.insert('a', 3);
|
|
53
|
-
ok('issue#3 Bug#3 unique search single value', JSON.stringify(bt.search('a')) === '[1]' && bt.size === 1);
|
|
54
|
-
const n = new BTree(4, false);
|
|
55
|
-
n.insert('a', 1); n.insert('a', 2); n.insert('b', 5);
|
|
56
|
-
ok('non-unique accumulates values', JSON.stringify(n.search('a')) === '[1,2]');
|
|
57
|
-
n.remove('a', 1); n.remove('a', 2); n.remove('a', 3);
|
|
58
|
-
ok('non-unique remove value keeps key until last', n.search('a').length === 0 && n.size === 1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/* ---- 多值键删除只影响目标 rowIndex(size 对照 distinct keys) ---- */
|
|
62
|
-
{
|
|
63
|
-
const bt = new BTree(8);
|
|
64
|
-
bt.insert('x', 1); bt.insert('x', 2); bt.insert('x', 3);
|
|
65
|
-
bt.remove('x', 2);
|
|
66
|
-
ok('multi-value remove one keeps others', JSON.stringify(bt.search('x').sort()) === '[1,3]' && bt.size === 1);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/* ---- 随机顺序插入/删除压力 ---- */
|
|
70
|
-
for (const order of [4, 8, 16, 64]) {
|
|
71
|
-
for (let trial = 0; trial < 15; trial++) {
|
|
72
|
-
const bt = new BTree(order);
|
|
73
|
-
const n = 200;
|
|
74
|
-
const seq = [...Array(n).keys()];
|
|
75
|
-
for (let i = n - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [seq[i], seq[j]] = [seq[j], seq[i]]; }
|
|
76
|
-
for (const k of seq) bt.insert(k, k);
|
|
77
|
-
const e = validate(bt);
|
|
78
|
-
ok('o' + order + ' insert invariant t' + trial, e.length === 0, e.slice(0, 2));
|
|
79
|
-
for (const k of seq) bt.remove(k, k);
|
|
80
|
-
ok('o' + order + ' full removal t' + trial, bt.size === 0 && bt.entries().length === 0 && validate(bt).length === 0);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* ---- 随机混合操作 vs 参考模型 ---- */
|
|
85
|
-
for (let t = 0; t < 25; t++) {
|
|
86
|
-
const order = [4, 8, 64][t % 3];
|
|
87
|
-
const bt = new BTree(order);
|
|
88
|
-
const ref = new Map(); // key -> Set(rowIndex)
|
|
89
|
-
let okAll = true;
|
|
90
|
-
for (let op = 0; op < 300; op++) {
|
|
91
|
-
const k = Math.floor(Math.random() * 40);
|
|
92
|
-
if (Math.random() < 0.55) {
|
|
93
|
-
const r = Math.floor(Math.random() * 5);
|
|
94
|
-
bt.insert(k, r);
|
|
95
|
-
if (!ref.has(k)) ref.set(k, new Set());
|
|
96
|
-
ref.get(k).add(r);
|
|
97
|
-
} else {
|
|
98
|
-
const r = Math.floor(Math.random() * 5);
|
|
99
|
-
bt.remove(k, r);
|
|
100
|
-
if (ref.has(k)) { ref.get(k).delete(r); if (ref.get(k).size === 0) ref.delete(k); }
|
|
101
|
-
}
|
|
102
|
-
if (op % 60 === 0) {
|
|
103
|
-
for (let x = 0; x < 40 && okAll; x++) {
|
|
104
|
-
const got = bt.search(x);
|
|
105
|
-
const exp = ref.has(x) ? [...ref.get(x)].sort((a, b) => a - b) : [];
|
|
106
|
-
const g = got ? [...got].sort((a, b) => a - b) : [];
|
|
107
|
-
if (JSON.stringify(g) !== JSON.stringify(exp)) okAll = false;
|
|
108
|
-
}
|
|
109
|
-
const es = bt.entries();
|
|
110
|
-
const emap = new Map(es.map(e => [e.key, [...e.value].sort((a, b) => a - b)]));
|
|
111
|
-
for (const [key, vals] of ref) {
|
|
112
|
-
const g = emap.get(key);
|
|
113
|
-
if (!g || JSON.stringify([...vals].sort()) !== JSON.stringify(g)) okAll = false;
|
|
114
|
-
}
|
|
115
|
-
const gtExp = [...ref.keys()].filter(k => k > 20).flatMap(k => [...ref.get(k)]).sort((a, b) => a - b);
|
|
116
|
-
const gt = bt.greaterThan(20).sort((a, b) => a - b);
|
|
117
|
-
if (JSON.stringify(gt) !== JSON.stringify(gtExp)) okAll = false;
|
|
118
|
-
const ltExp = [...ref.keys()].filter(k => k < 20).flatMap(k => [...ref.get(k)]).sort((a, b) => a - b);
|
|
119
|
-
const lt = bt.lessThan(20).sort((a, b) => a - b);
|
|
120
|
-
if (JSON.stringify(lt) !== JSON.stringify(ltExp)) okAll = false;
|
|
121
|
-
if (validate(bt).length) okAll = false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
ok('random mixed t' + t + ' vs reference', okAll && bt.size === ref.size, { size: bt.size, ref: ref.size });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
console.log(failed === 0 ? `\nALL ${passed} BTREE TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
|
|
128
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
1
|
+
/*
|
|
2
|
+
* B-Tree 专项回归测试 — 覆盖 GitHub Issue #3 的三个 bug 及随机混合操作不变量。
|
|
3
|
+
* node test/btree.test.js
|
|
4
|
+
*/
|
|
5
|
+
const BTree = require('../lib/btree');
|
|
6
|
+
|
|
7
|
+
let passed = 0, failed = 0;
|
|
8
|
+
function ok(name, cond, extra) {
|
|
9
|
+
if (cond) { passed++; }
|
|
10
|
+
else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validate(bt) {
|
|
14
|
+
const errs = [];
|
|
15
|
+
(function walk(node) {
|
|
16
|
+
if (node.leaf) {
|
|
17
|
+
if (node.children.length !== 0) errs.push('leaf has children');
|
|
18
|
+
for (let i = 1; i < node.keys.length; i++) if (node.keys[i] <= node.keys[i - 1]) errs.push('leaf unsorted');
|
|
19
|
+
} else {
|
|
20
|
+
if (node.children.length !== node.keys.length + 1) errs.push('internal children !== keys+1');
|
|
21
|
+
for (let i = 1; i < node.keys.length; i++) if (node.keys[i] <= node.keys[i - 1]) errs.push('internal unsorted');
|
|
22
|
+
node.children.forEach(walk);
|
|
23
|
+
}
|
|
24
|
+
})(bt._root);
|
|
25
|
+
return errs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ---- Issue #3 Bug #1: 删除崩溃 ---- */
|
|
29
|
+
{
|
|
30
|
+
const bt = new BTree(4);
|
|
31
|
+
for (let i = 0; i < 20; i++) bt.insert(i, i);
|
|
32
|
+
const removed = bt.remove(0, 0);
|
|
33
|
+
ok('issue#3 Bug#1 remove(0,0) no crash', removed === true && bt.search(0).length === 0 && bt.size === 19);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ---- Issue #3 Bug #2: entries() 乱序重复 ---- */
|
|
37
|
+
{
|
|
38
|
+
const bt = new BTree(4);
|
|
39
|
+
for (let i = 0; i < 20; i++) bt.insert(i, i);
|
|
40
|
+
const e = bt.entries();
|
|
41
|
+
let dup = 0, disorder = 0;
|
|
42
|
+
for (let i = 0; i < e.length; i++) {
|
|
43
|
+
if (i > 0 && e[i].key <= e[i - 1].key) disorder++;
|
|
44
|
+
for (let j = i + 1; j < e.length; j++) if (e[i].key === e[j].key) dup++;
|
|
45
|
+
}
|
|
46
|
+
ok('issue#3 Bug#2 entries sorted & dedup', e.length === 20 && dup === 0 && disorder === 0, { len: e.length, dup, disorder });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ---- Issue #3 Bug #3: 唯一索引返回所有 values ---- */
|
|
50
|
+
{
|
|
51
|
+
const bt = new BTree(4, true);
|
|
52
|
+
bt.insert('a', 1); bt.insert('a', 2); bt.insert('a', 3);
|
|
53
|
+
ok('issue#3 Bug#3 unique search single value', JSON.stringify(bt.search('a')) === '[1]' && bt.size === 1);
|
|
54
|
+
const n = new BTree(4, false);
|
|
55
|
+
n.insert('a', 1); n.insert('a', 2); n.insert('b', 5);
|
|
56
|
+
ok('non-unique accumulates values', JSON.stringify(n.search('a')) === '[1,2]');
|
|
57
|
+
n.remove('a', 1); n.remove('a', 2); n.remove('a', 3);
|
|
58
|
+
ok('non-unique remove value keeps key until last', n.search('a').length === 0 && n.size === 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---- 多值键删除只影响目标 rowIndex(size 对照 distinct keys) ---- */
|
|
62
|
+
{
|
|
63
|
+
const bt = new BTree(8);
|
|
64
|
+
bt.insert('x', 1); bt.insert('x', 2); bt.insert('x', 3);
|
|
65
|
+
bt.remove('x', 2);
|
|
66
|
+
ok('multi-value remove one keeps others', JSON.stringify(bt.search('x').sort()) === '[1,3]' && bt.size === 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---- 随机顺序插入/删除压力 ---- */
|
|
70
|
+
for (const order of [4, 8, 16, 64]) {
|
|
71
|
+
for (let trial = 0; trial < 15; trial++) {
|
|
72
|
+
const bt = new BTree(order);
|
|
73
|
+
const n = 200;
|
|
74
|
+
const seq = [...Array(n).keys()];
|
|
75
|
+
for (let i = n - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [seq[i], seq[j]] = [seq[j], seq[i]]; }
|
|
76
|
+
for (const k of seq) bt.insert(k, k);
|
|
77
|
+
const e = validate(bt);
|
|
78
|
+
ok('o' + order + ' insert invariant t' + trial, e.length === 0, e.slice(0, 2));
|
|
79
|
+
for (const k of seq) bt.remove(k, k);
|
|
80
|
+
ok('o' + order + ' full removal t' + trial, bt.size === 0 && bt.entries().length === 0 && validate(bt).length === 0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ---- 随机混合操作 vs 参考模型 ---- */
|
|
85
|
+
for (let t = 0; t < 25; t++) {
|
|
86
|
+
const order = [4, 8, 64][t % 3];
|
|
87
|
+
const bt = new BTree(order);
|
|
88
|
+
const ref = new Map(); // key -> Set(rowIndex)
|
|
89
|
+
let okAll = true;
|
|
90
|
+
for (let op = 0; op < 300; op++) {
|
|
91
|
+
const k = Math.floor(Math.random() * 40);
|
|
92
|
+
if (Math.random() < 0.55) {
|
|
93
|
+
const r = Math.floor(Math.random() * 5);
|
|
94
|
+
bt.insert(k, r);
|
|
95
|
+
if (!ref.has(k)) ref.set(k, new Set());
|
|
96
|
+
ref.get(k).add(r);
|
|
97
|
+
} else {
|
|
98
|
+
const r = Math.floor(Math.random() * 5);
|
|
99
|
+
bt.remove(k, r);
|
|
100
|
+
if (ref.has(k)) { ref.get(k).delete(r); if (ref.get(k).size === 0) ref.delete(k); }
|
|
101
|
+
}
|
|
102
|
+
if (op % 60 === 0) {
|
|
103
|
+
for (let x = 0; x < 40 && okAll; x++) {
|
|
104
|
+
const got = bt.search(x);
|
|
105
|
+
const exp = ref.has(x) ? [...ref.get(x)].sort((a, b) => a - b) : [];
|
|
106
|
+
const g = got ? [...got].sort((a, b) => a - b) : [];
|
|
107
|
+
if (JSON.stringify(g) !== JSON.stringify(exp)) okAll = false;
|
|
108
|
+
}
|
|
109
|
+
const es = bt.entries();
|
|
110
|
+
const emap = new Map(es.map(e => [e.key, [...e.value].sort((a, b) => a - b)]));
|
|
111
|
+
for (const [key, vals] of ref) {
|
|
112
|
+
const g = emap.get(key);
|
|
113
|
+
if (!g || JSON.stringify([...vals].sort()) !== JSON.stringify(g)) okAll = false;
|
|
114
|
+
}
|
|
115
|
+
const gtExp = [...ref.keys()].filter(k => k > 20).flatMap(k => [...ref.get(k)]).sort((a, b) => a - b);
|
|
116
|
+
const gt = bt.greaterThan(20).sort((a, b) => a - b);
|
|
117
|
+
if (JSON.stringify(gt) !== JSON.stringify(gtExp)) okAll = false;
|
|
118
|
+
const ltExp = [...ref.keys()].filter(k => k < 20).flatMap(k => [...ref.get(k)]).sort((a, b) => a - b);
|
|
119
|
+
const lt = bt.lessThan(20).sort((a, b) => a - b);
|
|
120
|
+
if (JSON.stringify(lt) !== JSON.stringify(ltExp)) okAll = false;
|
|
121
|
+
if (validate(bt).length) okAll = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
ok('random mixed t' + t + ' vs reference', okAll && bt.size === ref.size, { size: bt.size, ref: ref.size });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(failed === 0 ? `\nALL ${passed} BTREE TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
|
|
128
|
+
process.exit(failed === 0 ? 0 : 1);
|
package/test/coverage.js
CHANGED
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Integration coverage test — exercises Redis server, Web UI, migration tools
|
|
3
|
-
* and CLI logic in-process (zero dependencies) so c8 can measure real usage.
|
|
4
|
-
*
|
|
5
|
-
* npm run coverage (c8 node test/coverage.js)
|
|
6
|
-
*/
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
|
|
11
|
-
const ROOT = path.join(__dirname, '..');
|
|
12
|
-
let passed = 0, failed = 0;
|
|
13
|
-
|
|
14
|
-
function ok(name, cond) {
|
|
15
|
-
if (cond) { passed++; console.log('[OK]', name); }
|
|
16
|
-
else { failed++; console.log('[FAIL]', name); }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function tmp(name) {
|
|
20
|
-
const p = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-cov-')), name);
|
|
21
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
22
|
-
return p;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
(async () => {
|
|
26
|
-
/* ---------- Redis server ---------- */
|
|
27
|
-
const { RedisServer } = require(path.join(ROOT, 'lib/redis_server'));
|
|
28
|
-
const Database = require(path.join(ROOT, 'lib/database'));
|
|
29
|
-
const { executeSQL } = require(path.join(ROOT, 'lib/sql'));
|
|
30
|
-
const { WebUI } = require(path.join(ROOT, 'lib/web_ui'));
|
|
31
|
-
const migrate = require(path.join(ROOT, 'lib/migrate'));
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
const rs = new RedisServer({ dataDir: tmp('redis') });
|
|
35
|
-
ok('redis PING', rs.execute('PING', []) === 'PONG');
|
|
36
|
-
ok('redis ECHO', rs.execute('ECHO', ['hi']) === 'hi');
|
|
37
|
-
rs.execute('SET', ['k', 'v']);
|
|
38
|
-
ok('redis GET', rs.execute('GET', ['k']) === 'v');
|
|
39
|
-
ok('redis SETNX existing', rs.execute('SETNX', ['k', 'x']) === 0);
|
|
40
|
-
ok('redis SETNX new', rs.execute('SETNX', ['n', 'x']) === 1);
|
|
41
|
-
rs.execute('INCR', ['cnt']);
|
|
42
|
-
rs.execute('INCR', ['cnt']);
|
|
43
|
-
ok('redis INCR', rs.execute('GET', ['cnt']) === '2');
|
|
44
|
-
ok('redis DECR', rs.execute('DECR', ['cnt']) === 1);
|
|
45
|
-
ok('redis INCRBY', rs.execute('INCRBY', ['cnt', '5']) === 6);
|
|
46
|
-
rs.execute('APPEND', ['a', 'hello']);
|
|
47
|
-
ok('redis APPEND/STRLEN', rs.execute('STRLEN', ['a']) === 5);
|
|
48
|
-
rs.execute('HSET', ['h', 'f1', 'v1', 'f2', 'v2']);
|
|
49
|
-
ok('redis HGET', rs.execute('HGET', ['h', 'f1']) === 'v1');
|
|
50
|
-
const hg = rs.execute('HGETALL', ['h']);
|
|
51
|
-
ok('redis HGETALL', Array.isArray(hg) && hg.length === 4);
|
|
52
|
-
ok('redis HDEL', rs.execute('HDEL', ['h', 'f1']) === 1);
|
|
53
|
-
ok('redis HLEN', rs.execute('HLEN', ['h']) === 1);
|
|
54
|
-
ok('redis HKEYS', JSON.stringify(rs.execute('HKEYS', ['h'])) === '["f2"]');
|
|
55
|
-
rs.execute('LPUSH', ['l', 'a', 'b']);
|
|
56
|
-
ok('redis LLEN', rs.execute('LLEN', ['l']) === 2);
|
|
57
|
-
const lr = rs.execute('LRANGE', ['l', '0', '-1']);
|
|
58
|
-
ok('redis LRANGE', JSON.stringify(lr) === '["b","a"]');
|
|
59
|
-
ok('redis LINDEX', rs.execute('LINDEX', ['l', '0']) === 'b');
|
|
60
|
-
rs.execute('RPUSH', ['l', 'c']);
|
|
61
|
-
ok('redis LPOP', rs.execute('LPOP', ['l']) === 'b');
|
|
62
|
-
ok('redis LREM', rs.execute('LREM', ['l', '0', 'a']) === 1);
|
|
63
|
-
rs.execute('SADD', ['s', 'm1', 'm2', 'm3']);
|
|
64
|
-
ok('redis SCARD', rs.execute('SCARD', ['s']) === 3);
|
|
65
|
-
ok('redis SISMEMBER', rs.execute('SISMEMBER', ['s', 'm1']) === 1);
|
|
66
|
-
ok('redis SMEMBERS', rs.execute('SMEMBERS', ['s']).length === 3);
|
|
67
|
-
ok('redis SREM', rs.execute('SREM', ['s', 'm1']) === 1);
|
|
68
|
-
ok('redis KEYS', rs.execute('KEYS', ['*']).length >= 5);
|
|
69
|
-
ok('redis EXISTS', rs.execute('EXISTS', ['k', 'zzz']) === 1);
|
|
70
|
-
ok('redis TYPE', rs.execute('TYPE', ['h']) === 'hash');
|
|
71
|
-
ok('redis TTL missing', rs.execute('TTL', ['zzz']) === -2);
|
|
72
|
-
rs.execute('EXPIRE', ['k', '100']);
|
|
73
|
-
ok('redis TTL set', rs.execute('TTL', ['k']) > 0);
|
|
74
|
-
ok('redis PERSIST', rs.execute('PERSIST', ['k']) === 1);
|
|
75
|
-
ok('redis DBSIZE', rs.execute('DBSIZE', []) >= 5);
|
|
76
|
-
ok('redis FLUSHALL', rs.execute('FLUSHALL', []) === 'OK');
|
|
77
|
-
ok('redis DBSIZE after flush', rs.execute('DBSIZE', []) === 0);
|
|
78
|
-
ok('redis INFO', rs.execute('INFO', []).includes('redis_version'));
|
|
79
|
-
ok('redis SELECT', rs.execute('SELECT', ['3']) === 'OK');
|
|
80
|
-
rs.stop();
|
|
81
|
-
const persisted = JSON.parse(fs.readFileSync(rs.snapshotPath(), 'utf8'));
|
|
82
|
-
ok('redis snapshot file written', Object.keys(persisted).length >= 0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/* ---------- Web UI ---------- */
|
|
86
|
-
{
|
|
87
|
-
const dbFile = tmp('webui/db.json');
|
|
88
|
-
fs.mkdirSync(path.dirname(dbFile), { recursive: true });
|
|
89
|
-
fs.writeFileSync(dbFile, JSON.stringify({
|
|
90
|
-
__schema__: { users: { id: { type: 'integer', primaryKey: true, autoIncrement: true }, name: { type: 'string' } } },
|
|
91
|
-
__meta__: { version: 1 },
|
|
92
|
-
users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
|
|
93
|
-
}));
|
|
94
|
-
const ui = new WebUI({ port: 0, dataDir: path.dirname(dbFile) });
|
|
95
|
-
const port = await ui.start();
|
|
96
|
-
const dbs = await (await fetch(`http://127.0.0.1:${port}/api/databases`)).json();
|
|
97
|
-
ok('webui list databases', dbs.length === 1 && dbs[0].name === 'db');
|
|
98
|
-
const tbls = await (await fetch(`http://127.0.0.1:${port}/api/tables?db=db`)).json();
|
|
99
|
-
ok('webui list tables', tbls.tables.length === 1 && tbls.tables[0].name === 'users' && tbls.tables[0].count === 2);
|
|
100
|
-
const q = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
101
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
102
|
-
body: JSON.stringify({ db: 'db', sql: 'SELECT name FROM users WHERE id = 1' }),
|
|
103
|
-
})).json();
|
|
104
|
-
ok('webui query', q.ok && JSON.stringify(q.rows) === '[[\"Alice\"]]');
|
|
105
|
-
const ins = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
106
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
107
|
-
body: JSON.stringify({ db: 'db', sql: "INSERT INTO users (name) VALUES ('Carol')" }),
|
|
108
|
-
})).json();
|
|
109
|
-
ok('webui insert', ins.ok && ins.affected === 1);
|
|
110
|
-
const bad = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
111
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
112
|
-
body: JSON.stringify({ db: 'db', sql: 'SELECT * FROM nope' }),
|
|
113
|
-
})).json();
|
|
114
|
-
ok('webui error path', !bad.ok && bad.error.includes('nope'));
|
|
115
|
-
const html = await (await fetch(`http://127.0.0.1:${port}/`)).text();
|
|
116
|
-
ok('webui page', html.includes('JSQL-NEO'));
|
|
117
|
-
ok('webui missing db', (await (await fetch(`http://127.0.0.1:${port}/api/tables?db=x`)).json()).error !== undefined);
|
|
118
|
-
await ui.stop();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/* ---------- Migration tools + SQL engine ---------- */
|
|
122
|
-
{
|
|
123
|
-
const db = new Database(':memory:', { autoSave: false });
|
|
124
|
-
const dump = `CREATE TABLE products (
|
|
125
|
-
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
126
|
-
name VARCHAR(100) COLLATE utf8mb4_bin DEFAULT NULL,
|
|
127
|
-
price DECIMAL(10,2) DEFAULT '0.00',
|
|
128
|
-
note TEXT
|
|
129
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
130
|
-
-- comment line
|
|
131
|
-
INSERT INTO products (name, price, note) VALUES
|
|
132
|
-
('Laptop', 999.99, 'It\\'s a \\"pro\\" model\\nwith newline'),
|
|
133
|
-
('Mouse', 19.50, NULL),
|
|
134
|
-
('Cable', 9.90, 'USB\\tC');
|
|
135
|
-
`;
|
|
136
|
-
const r = await migrate.importDump(db, dump);
|
|
137
|
-
ok('migrate import dump', r.created.includes('products') && r.inserted === 3);
|
|
138
|
-
const csv = await migrate.exportTableToCSV(db, 'products');
|
|
139
|
-
ok('migrate export csv', csv.split('\n').length >= 4 && csv.includes('Laptop'));
|
|
140
|
-
const rows = await db.find('products', {}, { limit: 10 });
|
|
141
|
-
ok('migrate escaped round-trip', rows[0].name === 'Laptop' && rows[0].note.includes('with newline'));
|
|
142
|
-
const json = await migrate.exportAllToJSON(db);
|
|
143
|
-
ok('migrate export json', json.products && json.products.schema && json.products.rows.length === 3);
|
|
144
|
-
const db2 = new Database(':memory:', { autoSave: false });
|
|
145
|
-
await migrate.importFromJSON(db2, json);
|
|
146
|
-
ok('migrate import json', (await db2.count('products')) === 3);
|
|
147
|
-
const r2 = await migrate.importFromCSV(db2, 'products2', 'a,b\n1,hello\n2,"quo,ted"\n', { schema: { a: { type: 'string' }, b: { type: 'string' } } });
|
|
148
|
-
ok('migrate csv import', r2.inserted === 2);
|
|
149
|
-
|
|
150
|
-
await executeSQL(db, 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name STRING, age INTEGER)');
|
|
151
|
-
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25)");
|
|
152
|
-
const sel = await executeSQL(db, 'SELECT name, age FROM users WHERE age > 26 ORDER BY age DESC');
|
|
153
|
-
ok('sql select where order', JSON.stringify(sel.rows) === '[["Alice",30]]');
|
|
154
|
-
const agg = await executeSQL(db, 'SELECT COUNT(*) AS n, AVG(age) AS a FROM users');
|
|
155
|
-
ok('sql aggregate', agg.rows[0][0] === 2);
|
|
156
|
-
const fn = await executeSQL(db, 'SELECT VERSION(), CONCAT("x","y")');
|
|
157
|
-
ok('sql func column names', fn.columns[0] === 'VERSION()');
|
|
158
|
-
await executeSQL(db, 'UPDATE users SET age = 31 WHERE name = \'Bob\'');
|
|
159
|
-
await executeSQL(db, 'DELETE FROM users WHERE id = 1');
|
|
160
|
-
ok('sql count after delete', (await db.count('users')) === 1);
|
|
161
|
-
await executeSQL(db, 'BEGIN');
|
|
162
|
-
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('Temp', 99)");
|
|
163
|
-
await executeSQL(db, 'ROLLBACK');
|
|
164
|
-
ok('sql rollback', (await db.count('users')) === 1);
|
|
165
|
-
await executeSQL(db, 'START TRANSACTION');
|
|
166
|
-
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('X', 1)");
|
|
167
|
-
await executeSQL(db, 'TRUNCATE TABLE users');
|
|
168
|
-
ok('sql truncate', (await db.count('users')) === 0);
|
|
169
|
-
const sqlApi = require(path.join(ROOT, 'lib/sql'));
|
|
170
|
-
const parsed = sqlApi.parseSQL('SELECT * FROM t WHERE id = 5');
|
|
171
|
-
ok('parseSQL', parsed.type === 'select');
|
|
172
|
-
ok('applyParams', sqlApi.applyParams('SELECT * FROM t WHERE id = ?', [7]) === 'SELECT * FROM t WHERE id = 7');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/* ---------- CLI module load ---------- */
|
|
176
|
-
{
|
|
177
|
-
try {
|
|
178
|
-
require(path.join(ROOT, 'bin/jsql'));
|
|
179
|
-
ok('cli loads', true);
|
|
180
|
-
} catch (e) {
|
|
181
|
-
ok('cli loads', !/Cannot find module/.test(e.message));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
186
|
-
process.exit(failed ? 1 : 0);
|
|
187
|
-
})().catch(e => { console.error('[FATAL]', e.stack.split('\n').slice(0, 3).join('\n')); process.exit(1); });
|
|
1
|
+
/*
|
|
2
|
+
* Integration coverage test — exercises Redis server, Web UI, migration tools
|
|
3
|
+
* and CLI logic in-process (zero dependencies) so c8 can measure real usage.
|
|
4
|
+
*
|
|
5
|
+
* npm run coverage (c8 node test/coverage.js)
|
|
6
|
+
*/
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const ROOT = path.join(__dirname, '..');
|
|
12
|
+
let passed = 0, failed = 0;
|
|
13
|
+
|
|
14
|
+
function ok(name, cond) {
|
|
15
|
+
if (cond) { passed++; console.log('[OK]', name); }
|
|
16
|
+
else { failed++; console.log('[FAIL]', name); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function tmp(name) {
|
|
20
|
+
const p = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-cov-')), name);
|
|
21
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
(async () => {
|
|
26
|
+
/* ---------- Redis server ---------- */
|
|
27
|
+
const { RedisServer } = require(path.join(ROOT, 'lib/redis_server'));
|
|
28
|
+
const Database = require(path.join(ROOT, 'lib/database'));
|
|
29
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql'));
|
|
30
|
+
const { WebUI } = require(path.join(ROOT, 'lib/web_ui'));
|
|
31
|
+
const migrate = require(path.join(ROOT, 'lib/migrate'));
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
const rs = new RedisServer({ dataDir: tmp('redis') });
|
|
35
|
+
ok('redis PING', rs.execute('PING', []) === 'PONG');
|
|
36
|
+
ok('redis ECHO', rs.execute('ECHO', ['hi']) === 'hi');
|
|
37
|
+
rs.execute('SET', ['k', 'v']);
|
|
38
|
+
ok('redis GET', rs.execute('GET', ['k']) === 'v');
|
|
39
|
+
ok('redis SETNX existing', rs.execute('SETNX', ['k', 'x']) === 0);
|
|
40
|
+
ok('redis SETNX new', rs.execute('SETNX', ['n', 'x']) === 1);
|
|
41
|
+
rs.execute('INCR', ['cnt']);
|
|
42
|
+
rs.execute('INCR', ['cnt']);
|
|
43
|
+
ok('redis INCR', rs.execute('GET', ['cnt']) === '2');
|
|
44
|
+
ok('redis DECR', rs.execute('DECR', ['cnt']) === 1);
|
|
45
|
+
ok('redis INCRBY', rs.execute('INCRBY', ['cnt', '5']) === 6);
|
|
46
|
+
rs.execute('APPEND', ['a', 'hello']);
|
|
47
|
+
ok('redis APPEND/STRLEN', rs.execute('STRLEN', ['a']) === 5);
|
|
48
|
+
rs.execute('HSET', ['h', 'f1', 'v1', 'f2', 'v2']);
|
|
49
|
+
ok('redis HGET', rs.execute('HGET', ['h', 'f1']) === 'v1');
|
|
50
|
+
const hg = rs.execute('HGETALL', ['h']);
|
|
51
|
+
ok('redis HGETALL', Array.isArray(hg) && hg.length === 4);
|
|
52
|
+
ok('redis HDEL', rs.execute('HDEL', ['h', 'f1']) === 1);
|
|
53
|
+
ok('redis HLEN', rs.execute('HLEN', ['h']) === 1);
|
|
54
|
+
ok('redis HKEYS', JSON.stringify(rs.execute('HKEYS', ['h'])) === '["f2"]');
|
|
55
|
+
rs.execute('LPUSH', ['l', 'a', 'b']);
|
|
56
|
+
ok('redis LLEN', rs.execute('LLEN', ['l']) === 2);
|
|
57
|
+
const lr = rs.execute('LRANGE', ['l', '0', '-1']);
|
|
58
|
+
ok('redis LRANGE', JSON.stringify(lr) === '["b","a"]');
|
|
59
|
+
ok('redis LINDEX', rs.execute('LINDEX', ['l', '0']) === 'b');
|
|
60
|
+
rs.execute('RPUSH', ['l', 'c']);
|
|
61
|
+
ok('redis LPOP', rs.execute('LPOP', ['l']) === 'b');
|
|
62
|
+
ok('redis LREM', rs.execute('LREM', ['l', '0', 'a']) === 1);
|
|
63
|
+
rs.execute('SADD', ['s', 'm1', 'm2', 'm3']);
|
|
64
|
+
ok('redis SCARD', rs.execute('SCARD', ['s']) === 3);
|
|
65
|
+
ok('redis SISMEMBER', rs.execute('SISMEMBER', ['s', 'm1']) === 1);
|
|
66
|
+
ok('redis SMEMBERS', rs.execute('SMEMBERS', ['s']).length === 3);
|
|
67
|
+
ok('redis SREM', rs.execute('SREM', ['s', 'm1']) === 1);
|
|
68
|
+
ok('redis KEYS', rs.execute('KEYS', ['*']).length >= 5);
|
|
69
|
+
ok('redis EXISTS', rs.execute('EXISTS', ['k', 'zzz']) === 1);
|
|
70
|
+
ok('redis TYPE', rs.execute('TYPE', ['h']) === 'hash');
|
|
71
|
+
ok('redis TTL missing', rs.execute('TTL', ['zzz']) === -2);
|
|
72
|
+
rs.execute('EXPIRE', ['k', '100']);
|
|
73
|
+
ok('redis TTL set', rs.execute('TTL', ['k']) > 0);
|
|
74
|
+
ok('redis PERSIST', rs.execute('PERSIST', ['k']) === 1);
|
|
75
|
+
ok('redis DBSIZE', rs.execute('DBSIZE', []) >= 5);
|
|
76
|
+
ok('redis FLUSHALL', rs.execute('FLUSHALL', []) === 'OK');
|
|
77
|
+
ok('redis DBSIZE after flush', rs.execute('DBSIZE', []) === 0);
|
|
78
|
+
ok('redis INFO', rs.execute('INFO', []).includes('redis_version'));
|
|
79
|
+
ok('redis SELECT', rs.execute('SELECT', ['3']) === 'OK');
|
|
80
|
+
rs.stop();
|
|
81
|
+
const persisted = JSON.parse(fs.readFileSync(rs.snapshotPath(), 'utf8'));
|
|
82
|
+
ok('redis snapshot file written', Object.keys(persisted).length >= 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ---------- Web UI ---------- */
|
|
86
|
+
{
|
|
87
|
+
const dbFile = tmp('webui/db.json');
|
|
88
|
+
fs.mkdirSync(path.dirname(dbFile), { recursive: true });
|
|
89
|
+
fs.writeFileSync(dbFile, JSON.stringify({
|
|
90
|
+
__schema__: { users: { id: { type: 'integer', primaryKey: true, autoIncrement: true }, name: { type: 'string' } } },
|
|
91
|
+
__meta__: { version: 1 },
|
|
92
|
+
users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
|
|
93
|
+
}));
|
|
94
|
+
const ui = new WebUI({ port: 0, dataDir: path.dirname(dbFile) });
|
|
95
|
+
const port = await ui.start();
|
|
96
|
+
const dbs = await (await fetch(`http://127.0.0.1:${port}/api/databases`)).json();
|
|
97
|
+
ok('webui list databases', dbs.length === 1 && dbs[0].name === 'db');
|
|
98
|
+
const tbls = await (await fetch(`http://127.0.0.1:${port}/api/tables?db=db`)).json();
|
|
99
|
+
ok('webui list tables', tbls.tables.length === 1 && tbls.tables[0].name === 'users' && tbls.tables[0].count === 2);
|
|
100
|
+
const q = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
101
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({ db: 'db', sql: 'SELECT name FROM users WHERE id = 1' }),
|
|
103
|
+
})).json();
|
|
104
|
+
ok('webui query', q.ok && JSON.stringify(q.rows) === '[[\"Alice\"]]');
|
|
105
|
+
const ins = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
106
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({ db: 'db', sql: "INSERT INTO users (name) VALUES ('Carol')" }),
|
|
108
|
+
})).json();
|
|
109
|
+
ok('webui insert', ins.ok && ins.affected === 1);
|
|
110
|
+
const bad = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
111
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ db: 'db', sql: 'SELECT * FROM nope' }),
|
|
113
|
+
})).json();
|
|
114
|
+
ok('webui error path', !bad.ok && bad.error.includes('nope'));
|
|
115
|
+
const html = await (await fetch(`http://127.0.0.1:${port}/`)).text();
|
|
116
|
+
ok('webui page', html.includes('JSQL-NEO'));
|
|
117
|
+
ok('webui missing db', (await (await fetch(`http://127.0.0.1:${port}/api/tables?db=x`)).json()).error !== undefined);
|
|
118
|
+
await ui.stop();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ---------- Migration tools + SQL engine ---------- */
|
|
122
|
+
{
|
|
123
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
124
|
+
const dump = `CREATE TABLE products (
|
|
125
|
+
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
126
|
+
name VARCHAR(100) COLLATE utf8mb4_bin DEFAULT NULL,
|
|
127
|
+
price DECIMAL(10,2) DEFAULT '0.00',
|
|
128
|
+
note TEXT
|
|
129
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
130
|
+
-- comment line
|
|
131
|
+
INSERT INTO products (name, price, note) VALUES
|
|
132
|
+
('Laptop', 999.99, 'It\\'s a \\"pro\\" model\\nwith newline'),
|
|
133
|
+
('Mouse', 19.50, NULL),
|
|
134
|
+
('Cable', 9.90, 'USB\\tC');
|
|
135
|
+
`;
|
|
136
|
+
const r = await migrate.importDump(db, dump);
|
|
137
|
+
ok('migrate import dump', r.created.includes('products') && r.inserted === 3);
|
|
138
|
+
const csv = await migrate.exportTableToCSV(db, 'products');
|
|
139
|
+
ok('migrate export csv', csv.split('\n').length >= 4 && csv.includes('Laptop'));
|
|
140
|
+
const rows = await db.find('products', {}, { limit: 10 });
|
|
141
|
+
ok('migrate escaped round-trip', rows[0].name === 'Laptop' && rows[0].note.includes('with newline'));
|
|
142
|
+
const json = await migrate.exportAllToJSON(db);
|
|
143
|
+
ok('migrate export json', json.products && json.products.schema && json.products.rows.length === 3);
|
|
144
|
+
const db2 = new Database(':memory:', { autoSave: false });
|
|
145
|
+
await migrate.importFromJSON(db2, json);
|
|
146
|
+
ok('migrate import json', (await db2.count('products')) === 3);
|
|
147
|
+
const r2 = await migrate.importFromCSV(db2, 'products2', 'a,b\n1,hello\n2,"quo,ted"\n', { schema: { a: { type: 'string' }, b: { type: 'string' } } });
|
|
148
|
+
ok('migrate csv import', r2.inserted === 2);
|
|
149
|
+
|
|
150
|
+
await executeSQL(db, 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name STRING, age INTEGER)');
|
|
151
|
+
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25)");
|
|
152
|
+
const sel = await executeSQL(db, 'SELECT name, age FROM users WHERE age > 26 ORDER BY age DESC');
|
|
153
|
+
ok('sql select where order', JSON.stringify(sel.rows) === '[["Alice",30]]');
|
|
154
|
+
const agg = await executeSQL(db, 'SELECT COUNT(*) AS n, AVG(age) AS a FROM users');
|
|
155
|
+
ok('sql aggregate', agg.rows[0][0] === 2);
|
|
156
|
+
const fn = await executeSQL(db, 'SELECT VERSION(), CONCAT("x","y")');
|
|
157
|
+
ok('sql func column names', fn.columns[0] === 'VERSION()');
|
|
158
|
+
await executeSQL(db, 'UPDATE users SET age = 31 WHERE name = \'Bob\'');
|
|
159
|
+
await executeSQL(db, 'DELETE FROM users WHERE id = 1');
|
|
160
|
+
ok('sql count after delete', (await db.count('users')) === 1);
|
|
161
|
+
await executeSQL(db, 'BEGIN');
|
|
162
|
+
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('Temp', 99)");
|
|
163
|
+
await executeSQL(db, 'ROLLBACK');
|
|
164
|
+
ok('sql rollback', (await db.count('users')) === 1);
|
|
165
|
+
await executeSQL(db, 'START TRANSACTION');
|
|
166
|
+
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('X', 1)");
|
|
167
|
+
await executeSQL(db, 'TRUNCATE TABLE users');
|
|
168
|
+
ok('sql truncate', (await db.count('users')) === 0);
|
|
169
|
+
const sqlApi = require(path.join(ROOT, 'lib/sql'));
|
|
170
|
+
const parsed = sqlApi.parseSQL('SELECT * FROM t WHERE id = 5');
|
|
171
|
+
ok('parseSQL', parsed.type === 'select');
|
|
172
|
+
ok('applyParams', sqlApi.applyParams('SELECT * FROM t WHERE id = ?', [7]) === 'SELECT * FROM t WHERE id = 7');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ---------- CLI module load ---------- */
|
|
176
|
+
{
|
|
177
|
+
try {
|
|
178
|
+
require(path.join(ROOT, 'bin/jsql'));
|
|
179
|
+
ok('cli loads', true);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
ok('cli loads', !/Cannot find module/.test(e.message));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
186
|
+
process.exit(failed ? 1 : 0);
|
|
187
|
+
})().catch(e => { console.error('[FATAL]', e.stack.split('\n').slice(0, 3).join('\n')); process.exit(1); });
|