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.
Files changed (72) hide show
  1. package/LICENSE +202 -202
  2. package/README.md +8097 -7971
  3. package/bin/jsql +320 -320
  4. package/bin/jsql-server +90 -90
  5. package/bin/jsql-server-control.js +177 -177
  6. package/index.d.ts +385 -357
  7. package/index.js +124 -118
  8. package/lib/ast.js +214 -0
  9. package/lib/btree.js +413 -413
  10. package/lib/cache.js +328 -328
  11. package/lib/client.js +125 -125
  12. package/lib/database.js +1744 -1744
  13. package/lib/date-types.js +182 -182
  14. package/lib/errors.js +89 -89
  15. package/lib/jsql_format.js +497 -497
  16. package/lib/migrate.js +254 -254
  17. package/lib/mod.js +292 -292
  18. package/lib/mongo_server.js +607 -607
  19. package/lib/multiserver.js +140 -140
  20. package/lib/mysql_compat.js +413 -413
  21. package/lib/mysql_server.js +1237 -1237
  22. package/lib/native_client.js +732 -732
  23. package/lib/nedb_compat.js +506 -506
  24. package/lib/pg_server.js +864 -864
  25. package/lib/plugin.js +34 -34
  26. package/lib/query.js +815 -815
  27. package/lib/redis_server.js +571 -571
  28. package/lib/sql.js +3460 -2925
  29. package/lib/sqlite_compat.js +323 -323
  30. package/lib/sqlite_worker.js +264 -264
  31. package/lib/table.js +1304 -1304
  32. package/lib/tui.js +502 -502
  33. package/lib/wasm.js +1 -1
  34. package/lib/wasm_client.js +718 -718
  35. package/lib/web_ui.js +257 -257
  36. package/native/jsql-neo-native.node +0 -0
  37. package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
  38. package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
  39. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
  40. package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
  41. package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
  42. package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
  43. package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
  44. package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
  45. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
  46. package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
  47. package/nativesrc/jsql-neo-core/src/types.rs +93 -93
  48. package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
  49. package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
  50. package/nativesrc/jsql-neo-native/build.rs +3 -3
  51. package/nativesrc/jsql-neo-native/package.json +7 -7
  52. package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
  53. package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
  54. package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
  55. package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
  56. package/package.json +87 -86
  57. package/postinstall.js +158 -158
  58. package/test/btree.test.js +128 -128
  59. package/test/coverage.js +187 -187
  60. package/test/join.test.js +110 -110
  61. package/test/native.test.js +158 -158
  62. package/test/regress-5.1.0.js +271 -271
  63. package/test/smoke.js +58 -58
  64. package/test/sql-parser.test.js +273 -0
  65. package/test/wasm.test.js +110 -110
  66. package/wasm/browser.d.ts +89 -89
  67. package/wasm/browser.mjs +569 -569
  68. package/wasm/browser_bg.mjs +597 -597
  69. package/wasm/jsql_neo_wasm.d.ts +38 -38
  70. package/wasm/jsql_neo_wasm.js +520 -520
  71. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
  72. package/wasm/package.json +11 -11
@@ -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); });