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,271 +1,271 @@
1
- /*
2
- * Regression tests locking in the bug fixes shipped in 5.1.0 / 5.1.1.
3
- * Zero runtime dependencies (uses Node builtins + in-repo libs only).
4
- *
5
- * node test/regress-5.1.0.js
6
- *
7
- * Covers:
8
- * M1 $like regex-escape M2 removeById(s) index consistency
9
- * M3 B-Tree open intervals M4 transaction deep-copy snapshot
10
- * M5 swap-pop hash sync M6 importFromJSON shapes/overwrite
11
- * H1/H3 parseFieldShorthand + ER_DUP_ENTRY
12
- * S1 Redis per-socket AUTH S2/S4 WebUI CORS & authToken
13
- * S3/S4 MySQL ACL (1044) S6 mysql_compat pool filename
14
- * N1 CLI --version sync
15
- */
16
- const path = require('path');
17
- const fs = require('fs');
18
- const os = require('os');
19
- const net = require('net');
20
- const { execFileSync } = require('child_process');
21
-
22
- const ROOT = path.join(__dirname, '..');
23
- const Database = require(path.join(ROOT, 'lib/database'));
24
- const BTree = require(path.join(ROOT, 'lib/btree'));
25
- const migrate = require(path.join(ROOT, 'lib/migrate'));
26
- const { RedisServer } = require(path.join(ROOT, 'lib/redis_server'));
27
- const { WebUI } = require(path.join(ROOT, 'lib/web_ui'));
28
- const { MysqlServer } = require(path.join(ROOT, 'lib/mysql_server'));
29
- const compat = require(path.join(ROOT, 'lib/mysql_compat'));
30
-
31
- let passed = 0, failed = 0;
32
- function ok(name, cond, extra) {
33
- if (cond) { passed++; console.log('[OK]', name); }
34
- else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
35
- }
36
-
37
- function tmp(name) {
38
- const p = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-r-')), name);
39
- fs.mkdirSync(path.dirname(p), { recursive: true });
40
- return p;
41
- }
42
-
43
- function freePort() {
44
- return new Promise((res) => {
45
- const s = net.createServer();
46
- s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); });
47
- });
48
- }
49
-
50
- /* Minimal RESP client for integration tests. */
51
- function respClient(port) {
52
- return new Promise((resolve, reject) => {
53
- const socket = net.connect(port, '127.0.0.1');
54
- let buf = '';
55
- const queue = [];
56
- socket.on('connect', () => resolve({
57
- cmd(line) {
58
- return new Promise((r) => {
59
- queue.push(r);
60
- socket.write(line);
61
- });
62
- },
63
- end() { socket.end(); },
64
- }));
65
- socket.on('data', (chunk) => {
66
- buf += chunk.toString('utf8');
67
- while (queue.length) {
68
- const idx = buf.indexOf('\r\n');
69
- if (idx < 0) break;
70
- const line = buf.slice(0, idx);
71
- buf = buf.slice(idx + 2);
72
- queue.shift()(line);
73
- }
74
- });
75
- socket.on('error', reject);
76
- });
77
- }
78
-
79
- (async () => {
80
- /* ============ M3: B-Tree strict open intervals ============ */
81
- {
82
- const t = new BTree();
83
- for (const k of [1, 3, 5, 7, 9]) t.insert(k, k);
84
- ok('btree.greaterThan excludes min', JSON.stringify(t.greaterThan(5)) === '[7,9]', t.greaterThan(5));
85
- ok('btree.greaterThan(9) empty', JSON.stringify(t.greaterThan(9)) === '[]');
86
- ok('btree.lessThan excludes max', JSON.stringify(t.lessThan(5)) === '[1,3]', t.lessThan(5));
87
- ok('btree.lessThan(1) empty', JSON.stringify(t.lessThan(1)) === '[]');
88
- }
89
-
90
- /* ============ Core engine ============ */
91
- const db = new Database(':memory:', { autoSave: false });
92
- await db.start();
93
- db.createTable('users', { id: 'integer primary key auto_increment', name: 'string', email: 'string unique', age: 'integer' });
94
- for (const [n, a] of [['A', 10], ['B', 20], ['C', 30]]) db.insert('users', { name: n, email: n.toLowerCase() + '@x', age: a });
95
-
96
- {
97
- const gt = db.find('users', { age: { $gt: 20 } }).map(r => r.name);
98
- ok('find $gt excludes boundary', JSON.stringify(gt) === '["C"]', gt);
99
- const lt = db.find('users', { age: { $lt: 20 } }).map(r => r.name);
100
- ok('find $lt excludes boundary', JSON.stringify(lt) === '["A"]', lt);
101
- }
102
-
103
- {
104
- db.removeById('users', 2); // B (id=2) removed, C still id=3
105
- ok('removeById with PK count', db.count('users') === 2, db.count('users'));
106
- ok('removeById PK lookup null', db.findById('users', 2) === null);
107
- ok('removeById B-Tree still consistent', db.find('users', { age: { $gt: 15 } }).length === 1);
108
- }
109
-
110
- {
111
- db.createTable('np', { v: 'string', n: 'integer' });
112
- for (let i = 0; i < 10; i++) db.insert('np', { v: 'v' + i, n: i });
113
- db.createTable('h', { id: 'integer primary key auto_increment', grp: 'string', val: 'integer' });
114
- for (let i = 0; i < 12; i++) db.insert('h', { grp: 'g' + (i % 4), val: i });
115
- db._tables.h.createIndex('grp');
116
-
117
- db.removeByIds('np', [1, 2]);
118
- ok('removeByIds no-PK rebuilds indexes', db.find('np').length === 8, db.find('np').length);
119
-
120
- const pre = db.find('h', { grp: 'g1' }).map(r => r.id).sort((a, b) => a - b);
121
- db.removeById('h', 1); db.removeById('h', 5); db.removeById('h', 9); // the g0 rows
122
- const post = db.find('h', { grp: 'g1' }).map(r => r.id).sort((a, b) => a - b);
123
- ok('hash index survives swap-pop (g1 intact)', JSON.stringify(post) === JSON.stringify(pre), { pre, post });
124
- ok('hash index has no stale g0 rows', db.find('h', { grp: 'g0' }).length === 0);
125
- let stale = 0;
126
- for (const list of db._tables.h._indexes.grp.values()) for (const i of list) if (!db._tables.h._rows[i]) stale++;
127
- ok('hash index zero stale entries', stale === 0, stale);
128
- }
129
-
130
- {
131
- db.createTable('docs', { id: 'integer primary key', data: 'string' });
132
- db.insert('docs', { id: 1, data: JSON.stringify({ a: { b: 1 }, list: [1, 2, 3] }) });
133
- db.begin();
134
- db.updateById('docs', 1, { data: JSON.stringify({ a: { b: 2 }, list: [9] }) });
135
- db.rollback();
136
- const d = JSON.parse(db.findById('docs', 1).data);
137
- ok('transaction snapshot deep-copies nested object', d.a.b === 1, d);
138
- ok('transaction snapshot deep-copies arrays', JSON.stringify(d.list) === '[1,2,3]', d.list);
139
- }
140
-
141
- {
142
- const pfs = Database.parseFieldShorthand;
143
- const s1 = pfs('integer primary key auto_increment');
144
- ok('parseFieldShorthand pk+ai', s1.primaryKey === true && s1.autoIncrement === true && s1.type === 'integer', s1);
145
- const s2 = pfs('string unique not null');
146
- ok('parseFieldShorthand unique+notnull', s2.unique === true && s2.required === true && s2.type === 'string', s2);
147
- const s3 = pfs('string default x');
148
- ok('parseFieldShorthand default', s3.default === 'x' && s3.type === 'string', s3);
149
-
150
- let e = null;
151
- try { db.insert('users', { name: 'X', email: 'a@x', age: 1 }); } catch (err) { e = err; }
152
- ok('duplicate unique throws ER_DUP_ENTRY', e && (e.code === 1062 || String(e).includes('1062')), e && (e.code || e.message));
153
- db.createTable('pk2', { a: 'integer primary key', b: 'string' });
154
- db.insert('pk2', { a: 1, b: 'x' });
155
- e = null;
156
- try { db.insert('pk2', { a: 1, b: 'y' }); } catch (err) { e = err; }
157
- ok('duplicate primary key throws ER_DUP_ENTRY', e && (e.code === 1062 || String(e).includes('1062')), e && (e.code || e.message));
158
- }
159
-
160
- {
161
- const r = await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 1 }, { id: 2 }] });
162
- ok('importFromJSON single-table shape', r && db.count('t1') === 2);
163
- let threw = null;
164
- try { await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 3 }] }); } catch (err) { threw = err; }
165
- ok('importFromJSON refuses existing table without overwrite', !!threw, threw && (threw.message || String(threw)));
166
- await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 3 }] }, { overwrite: true });
167
- ok('importFromJSON overwrite replaces table', db.count('t1') === 1, db.count('t1'));
168
- }
169
-
170
- {
171
- db.createTable('lk', { s: 'string' });
172
- for (const s of ['a.b', 'axb', 'aXb', 'abc']) db.insert('lk', { s });
173
- const m = db.find('lk', { s: { $like: 'a.b' } }).map(r => r.s);
174
- ok('$like escapes regex metacharacters', JSON.stringify(m) === '["a.b"]', m);
175
- const m2 = db.find('lk', { s: { $like: 'a%b' } }).map(r => r.s);
176
- ok('$like % wildcard still works', JSON.stringify(m2) === '["a.b","axb","aXb"]', m2);
177
- }
178
- await db.stop();
179
-
180
- /* ============ CLI (N1) ============ */
181
- {
182
- const pkg = require(path.join(ROOT, 'package.json')).version;
183
- const v = execFileSync(process.execPath, [path.join(ROOT, 'bin/jsql'), '--version']).toString().trim();
184
- ok('cli --version reports package version', v === pkg, v);
185
- const v2 = execFileSync(process.execPath, [path.join(ROOT, 'bin/jsql'), 'version']).toString().trim();
186
- ok('cli version command matches', v2 === pkg, v2);
187
- const help = execFileSync(process.execPath, [path.join(ROOT, 'bin/jsql'), 'ui', '--help']).toString();
188
- ok('cli subcommand --help does not crash', help.includes('127.0.0.1') && help.includes('auth-token'));
189
- }
190
-
191
- /* ============ WebUI CORS + auth (N2 / S2) ============ */
192
- {
193
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-wui-'));
194
- fs.writeFileSync(path.join(dir, 'db.json'), JSON.stringify({ __schema__: { u: { id: { type: 'integer', primaryKey: true } } }, u: [{ id: 1 }] }));
195
-
196
- const p1 = await freePort();
197
- const w1 = new WebUI({ port: p1, host: undefined, dataDir: dir });
198
- await w1.start();
199
- ok('webui default host is 127.0.0.1', w1.host === '127.0.0.1', w1.host);
200
- let r = await fetch('http://127.0.0.1:' + p1 + '/api/databases', { headers: { Origin: 'http://evil.example' } });
201
- ok('webui no-token emits no ACAO', r.headers.get('access-control-allow-origin') === null, r.headers.get('access-control-allow-origin'));
202
- r = await fetch('http://127.0.0.1:' + p1 + '/api/query', { method: 'OPTIONS', headers: { Origin: 'http://evil.example', 'Access-Control-Request-Method': 'POST' } });
203
- ok('webui no-token preflight emits no ACAO', r.headers.get('access-control-allow-origin') === null);
204
- await w1.stop();
205
-
206
- const p2 = await freePort();
207
- const w2 = new WebUI({ port: p2, host: '127.0.0.1', dataDir: dir, authToken: 'tok' });
208
- await w2.start();
209
- r = await fetch('http://127.0.0.1:' + p2 + '/api/databases', { headers: { Origin: 'http://evil.example' } });
210
- ok('webui missing bearer returns 401', r.status === 401);
211
- r = await fetch('http://127.0.0.1:' + p2 + '/api/databases', { headers: { Origin: 'http://good.example', Authorization: 'Bearer tok' } });
212
- ok('webui auth echoes origin', r.status === 200 && r.headers.get('access-control-allow-origin') === 'http://good.example', { status: r.status, acao: r.headers.get('access-control-allow-origin') });
213
- await w2.stop();
214
-
215
- const p3 = await freePort();
216
- const w3 = new WebUI({ port: p3, host: '127.0.0.1', dataDir: dir, allowOrigin: '*' });
217
- await w3.start();
218
- r = await fetch('http://127.0.0.1:' + p3 + '/api/databases', { headers: { Origin: 'http://any.example' } });
219
- ok('webui explicit allowOrigin honored', r.headers.get('access-control-allow-origin') === '*');
220
- await w3.stop();
221
- fs.rmSync(dir, { recursive: true, force: true });
222
- }
223
-
224
- /* ============ Redis per-socket AUTH (S1) ============ */
225
- {
226
- const p = await freePort();
227
- const rs = new RedisServer({ port: p, host: '127.0.0.1', password: 'pw' });
228
- rs.listen();
229
- await new Promise((res) => rs.server.once('listening', res));
230
-
231
- const a = await respClient(p);
232
- const b = await respClient(p);
233
- const noauthA = await a.cmd('*1\r\n$4\r\nPING\r\n');
234
- ok('redis unauth socket gets NOAUTH', noauthA.startsWith('-NOAUTH'), noauthA);
235
- const authB = await b.cmd('*2\r\n$4\r\nAUTH\r\n$2\r\npw\r\n');
236
- ok('redis AUTH pw succeeds', authB === '+OK', authB);
237
- const pingB = await b.cmd('*1\r\n$4\r\nPING\r\n');
238
- ok('redis authed socket works', pingB === '+PONG', pingB);
239
- const pingA = await a.cmd('*1\r\n$4\r\nPING\r\n');
240
- ok('redis auth is per-socket (other socket still NOAUTH)', pingA.startsWith('-NOAUTH'), pingA);
241
- a.end(); b.end();
242
- rs.server.close();
243
- }
244
-
245
- /* ============ MySQL ACL (S3 / S4) ============ */
246
- {
247
- const ms = new MysqlServer({ dataDir: ':memory:', auth: { alice: { password: 'pw', databases: ['app'] } } });
248
- ok('mysql acl allows member db', ms._canAccessDb('alice', 'app') === true);
249
- ok('mysql acl denies foreign db', ms._canAccessDb('alice', 'other') === false);
250
- let code = null;
251
- try { await ms._switchDb({ user: 'alice' }, 'other'); } catch (e) { code = e.code; }
252
- ok('mysql cross-db switch denied (1044)', code === 1044, code);
253
- }
254
-
255
- /* ============ mysql_compat pool filename (S6) ============ */
256
- {
257
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-cp-'));
258
- const file = path.join(dir, 'pool.json');
259
- const pool = compat.createPool({ filename: file });
260
- await pool.query('CREATE TABLE t (id INT PRIMARY KEY, v VARCHAR(20))');
261
- await pool.query("INSERT INTO t VALUES (1,'x')");
262
- const [rows] = await pool.query('SELECT * FROM t');
263
- ok('mysql_compat pool query roundtrip', rows.length === 1 && rows[0].v === 'x', rows);
264
- ok('mysql_compat pool persists to filename', fs.existsSync(file) && fs.statSync(file).size > 0);
265
- if (typeof pool.end === 'function') await pool.end();
266
- fs.rmSync(dir, { recursive: true, force: true });
267
- }
268
-
269
- console.log(failed === 0 ? `\nALL ${passed} REGRESSION TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
270
- process.exit(failed === 0 ? 0 : 1);
271
- })().catch((e) => { console.error('FATAL', e); process.exit(1); });
1
+ /*
2
+ * Regression tests locking in the bug fixes shipped in 5.1.0 / 5.1.1.
3
+ * Zero runtime dependencies (uses Node builtins + in-repo libs only).
4
+ *
5
+ * node test/regress-5.1.0.js
6
+ *
7
+ * Covers:
8
+ * M1 $like regex-escape M2 removeById(s) index consistency
9
+ * M3 B-Tree open intervals M4 transaction deep-copy snapshot
10
+ * M5 swap-pop hash sync M6 importFromJSON shapes/overwrite
11
+ * H1/H3 parseFieldShorthand + ER_DUP_ENTRY
12
+ * S1 Redis per-socket AUTH S2/S4 WebUI CORS & authToken
13
+ * S3/S4 MySQL ACL (1044) S6 mysql_compat pool filename
14
+ * N1 CLI --version sync
15
+ */
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const net = require('net');
20
+ const { execFileSync } = require('child_process');
21
+
22
+ const ROOT = path.join(__dirname, '..');
23
+ const Database = require(path.join(ROOT, 'lib/database'));
24
+ const BTree = require(path.join(ROOT, 'lib/btree'));
25
+ const migrate = require(path.join(ROOT, 'lib/migrate'));
26
+ const { RedisServer } = require(path.join(ROOT, 'lib/redis_server'));
27
+ const { WebUI } = require(path.join(ROOT, 'lib/web_ui'));
28
+ const { MysqlServer } = require(path.join(ROOT, 'lib/mysql_server'));
29
+ const compat = require(path.join(ROOT, 'lib/mysql_compat'));
30
+
31
+ let passed = 0, failed = 0;
32
+ function ok(name, cond, extra) {
33
+ if (cond) { passed++; console.log('[OK]', name); }
34
+ else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
35
+ }
36
+
37
+ function tmp(name) {
38
+ const p = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-r-')), name);
39
+ fs.mkdirSync(path.dirname(p), { recursive: true });
40
+ return p;
41
+ }
42
+
43
+ function freePort() {
44
+ return new Promise((res) => {
45
+ const s = net.createServer();
46
+ s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); });
47
+ });
48
+ }
49
+
50
+ /* Minimal RESP client for integration tests. */
51
+ function respClient(port) {
52
+ return new Promise((resolve, reject) => {
53
+ const socket = net.connect(port, '127.0.0.1');
54
+ let buf = '';
55
+ const queue = [];
56
+ socket.on('connect', () => resolve({
57
+ cmd(line) {
58
+ return new Promise((r) => {
59
+ queue.push(r);
60
+ socket.write(line);
61
+ });
62
+ },
63
+ end() { socket.end(); },
64
+ }));
65
+ socket.on('data', (chunk) => {
66
+ buf += chunk.toString('utf8');
67
+ while (queue.length) {
68
+ const idx = buf.indexOf('\r\n');
69
+ if (idx < 0) break;
70
+ const line = buf.slice(0, idx);
71
+ buf = buf.slice(idx + 2);
72
+ queue.shift()(line);
73
+ }
74
+ });
75
+ socket.on('error', reject);
76
+ });
77
+ }
78
+
79
+ (async () => {
80
+ /* ============ M3: B-Tree strict open intervals ============ */
81
+ {
82
+ const t = new BTree();
83
+ for (const k of [1, 3, 5, 7, 9]) t.insert(k, k);
84
+ ok('btree.greaterThan excludes min', JSON.stringify(t.greaterThan(5)) === '[7,9]', t.greaterThan(5));
85
+ ok('btree.greaterThan(9) empty', JSON.stringify(t.greaterThan(9)) === '[]');
86
+ ok('btree.lessThan excludes max', JSON.stringify(t.lessThan(5)) === '[1,3]', t.lessThan(5));
87
+ ok('btree.lessThan(1) empty', JSON.stringify(t.lessThan(1)) === '[]');
88
+ }
89
+
90
+ /* ============ Core engine ============ */
91
+ const db = new Database(':memory:', { autoSave: false });
92
+ await db.start();
93
+ db.createTable('users', { id: 'integer primary key auto_increment', name: 'string', email: 'string unique', age: 'integer' });
94
+ for (const [n, a] of [['A', 10], ['B', 20], ['C', 30]]) db.insert('users', { name: n, email: n.toLowerCase() + '@x', age: a });
95
+
96
+ {
97
+ const gt = db.find('users', { age: { $gt: 20 } }).map(r => r.name);
98
+ ok('find $gt excludes boundary', JSON.stringify(gt) === '["C"]', gt);
99
+ const lt = db.find('users', { age: { $lt: 20 } }).map(r => r.name);
100
+ ok('find $lt excludes boundary', JSON.stringify(lt) === '["A"]', lt);
101
+ }
102
+
103
+ {
104
+ db.removeById('users', 2); // B (id=2) removed, C still id=3
105
+ ok('removeById with PK count', db.count('users') === 2, db.count('users'));
106
+ ok('removeById PK lookup null', db.findById('users', 2) === null);
107
+ ok('removeById B-Tree still consistent', db.find('users', { age: { $gt: 15 } }).length === 1);
108
+ }
109
+
110
+ {
111
+ db.createTable('np', { v: 'string', n: 'integer' });
112
+ for (let i = 0; i < 10; i++) db.insert('np', { v: 'v' + i, n: i });
113
+ db.createTable('h', { id: 'integer primary key auto_increment', grp: 'string', val: 'integer' });
114
+ for (let i = 0; i < 12; i++) db.insert('h', { grp: 'g' + (i % 4), val: i });
115
+ db._tables.h.createIndex('grp');
116
+
117
+ db.removeByIds('np', [1, 2]);
118
+ ok('removeByIds no-PK rebuilds indexes', db.find('np').length === 8, db.find('np').length);
119
+
120
+ const pre = db.find('h', { grp: 'g1' }).map(r => r.id).sort((a, b) => a - b);
121
+ db.removeById('h', 1); db.removeById('h', 5); db.removeById('h', 9); // the g0 rows
122
+ const post = db.find('h', { grp: 'g1' }).map(r => r.id).sort((a, b) => a - b);
123
+ ok('hash index survives swap-pop (g1 intact)', JSON.stringify(post) === JSON.stringify(pre), { pre, post });
124
+ ok('hash index has no stale g0 rows', db.find('h', { grp: 'g0' }).length === 0);
125
+ let stale = 0;
126
+ for (const list of db._tables.h._indexes.grp.values()) for (const i of list) if (!db._tables.h._rows[i]) stale++;
127
+ ok('hash index zero stale entries', stale === 0, stale);
128
+ }
129
+
130
+ {
131
+ db.createTable('docs', { id: 'integer primary key', data: 'string' });
132
+ db.insert('docs', { id: 1, data: JSON.stringify({ a: { b: 1 }, list: [1, 2, 3] }) });
133
+ db.begin();
134
+ db.updateById('docs', 1, { data: JSON.stringify({ a: { b: 2 }, list: [9] }) });
135
+ db.rollback();
136
+ const d = JSON.parse(db.findById('docs', 1).data);
137
+ ok('transaction snapshot deep-copies nested object', d.a.b === 1, d);
138
+ ok('transaction snapshot deep-copies arrays', JSON.stringify(d.list) === '[1,2,3]', d.list);
139
+ }
140
+
141
+ {
142
+ const pfs = Database.parseFieldShorthand;
143
+ const s1 = pfs('integer primary key auto_increment');
144
+ ok('parseFieldShorthand pk+ai', s1.primaryKey === true && s1.autoIncrement === true && s1.type === 'integer', s1);
145
+ const s2 = pfs('string unique not null');
146
+ ok('parseFieldShorthand unique+notnull', s2.unique === true && s2.required === true && s2.type === 'string', s2);
147
+ const s3 = pfs('string default x');
148
+ ok('parseFieldShorthand default', s3.default === 'x' && s3.type === 'string', s3);
149
+
150
+ let e = null;
151
+ try { db.insert('users', { name: 'X', email: 'a@x', age: 1 }); } catch (err) { e = err; }
152
+ ok('duplicate unique throws ER_DUP_ENTRY', e && (e.code === 1062 || String(e).includes('1062')), e && (e.code || e.message));
153
+ db.createTable('pk2', { a: 'integer primary key', b: 'string' });
154
+ db.insert('pk2', { a: 1, b: 'x' });
155
+ e = null;
156
+ try { db.insert('pk2', { a: 1, b: 'y' }); } catch (err) { e = err; }
157
+ ok('duplicate primary key throws ER_DUP_ENTRY', e && (e.code === 1062 || String(e).includes('1062')), e && (e.code || e.message));
158
+ }
159
+
160
+ {
161
+ const r = await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 1 }, { id: 2 }] });
162
+ ok('importFromJSON single-table shape', r && db.count('t1') === 2);
163
+ let threw = null;
164
+ try { await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 3 }] }); } catch (err) { threw = err; }
165
+ ok('importFromJSON refuses existing table without overwrite', !!threw, threw && (threw.message || String(threw)));
166
+ await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 3 }] }, { overwrite: true });
167
+ ok('importFromJSON overwrite replaces table', db.count('t1') === 1, db.count('t1'));
168
+ }
169
+
170
+ {
171
+ db.createTable('lk', { s: 'string' });
172
+ for (const s of ['a.b', 'axb', 'aXb', 'abc']) db.insert('lk', { s });
173
+ const m = db.find('lk', { s: { $like: 'a.b' } }).map(r => r.s);
174
+ ok('$like escapes regex metacharacters', JSON.stringify(m) === '["a.b"]', m);
175
+ const m2 = db.find('lk', { s: { $like: 'a%b' } }).map(r => r.s);
176
+ ok('$like % wildcard still works', JSON.stringify(m2) === '["a.b","axb","aXb"]', m2);
177
+ }
178
+ await db.stop();
179
+
180
+ /* ============ CLI (N1) ============ */
181
+ {
182
+ const pkg = require(path.join(ROOT, 'package.json')).version;
183
+ const v = execFileSync(process.execPath, [path.join(ROOT, 'bin/jsql'), '--version']).toString().trim();
184
+ ok('cli --version reports package version', v === pkg, v);
185
+ const v2 = execFileSync(process.execPath, [path.join(ROOT, 'bin/jsql'), 'version']).toString().trim();
186
+ ok('cli version command matches', v2 === pkg, v2);
187
+ const help = execFileSync(process.execPath, [path.join(ROOT, 'bin/jsql'), 'ui', '--help']).toString();
188
+ ok('cli subcommand --help does not crash', help.includes('127.0.0.1') && help.includes('auth-token'));
189
+ }
190
+
191
+ /* ============ WebUI CORS + auth (N2 / S2) ============ */
192
+ {
193
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-wui-'));
194
+ fs.writeFileSync(path.join(dir, 'db.json'), JSON.stringify({ __schema__: { u: { id: { type: 'integer', primaryKey: true } } }, u: [{ id: 1 }] }));
195
+
196
+ const p1 = await freePort();
197
+ const w1 = new WebUI({ port: p1, host: undefined, dataDir: dir });
198
+ await w1.start();
199
+ ok('webui default host is 127.0.0.1', w1.host === '127.0.0.1', w1.host);
200
+ let r = await fetch('http://127.0.0.1:' + p1 + '/api/databases', { headers: { Origin: 'http://evil.example' } });
201
+ ok('webui no-token emits no ACAO', r.headers.get('access-control-allow-origin') === null, r.headers.get('access-control-allow-origin'));
202
+ r = await fetch('http://127.0.0.1:' + p1 + '/api/query', { method: 'OPTIONS', headers: { Origin: 'http://evil.example', 'Access-Control-Request-Method': 'POST' } });
203
+ ok('webui no-token preflight emits no ACAO', r.headers.get('access-control-allow-origin') === null);
204
+ await w1.stop();
205
+
206
+ const p2 = await freePort();
207
+ const w2 = new WebUI({ port: p2, host: '127.0.0.1', dataDir: dir, authToken: 'tok' });
208
+ await w2.start();
209
+ r = await fetch('http://127.0.0.1:' + p2 + '/api/databases', { headers: { Origin: 'http://evil.example' } });
210
+ ok('webui missing bearer returns 401', r.status === 401);
211
+ r = await fetch('http://127.0.0.1:' + p2 + '/api/databases', { headers: { Origin: 'http://good.example', Authorization: 'Bearer tok' } });
212
+ ok('webui auth echoes origin', r.status === 200 && r.headers.get('access-control-allow-origin') === 'http://good.example', { status: r.status, acao: r.headers.get('access-control-allow-origin') });
213
+ await w2.stop();
214
+
215
+ const p3 = await freePort();
216
+ const w3 = new WebUI({ port: p3, host: '127.0.0.1', dataDir: dir, allowOrigin: '*' });
217
+ await w3.start();
218
+ r = await fetch('http://127.0.0.1:' + p3 + '/api/databases', { headers: { Origin: 'http://any.example' } });
219
+ ok('webui explicit allowOrigin honored', r.headers.get('access-control-allow-origin') === '*');
220
+ await w3.stop();
221
+ fs.rmSync(dir, { recursive: true, force: true });
222
+ }
223
+
224
+ /* ============ Redis per-socket AUTH (S1) ============ */
225
+ {
226
+ const p = await freePort();
227
+ const rs = new RedisServer({ port: p, host: '127.0.0.1', password: 'pw' });
228
+ rs.listen();
229
+ await new Promise((res) => rs.server.once('listening', res));
230
+
231
+ const a = await respClient(p);
232
+ const b = await respClient(p);
233
+ const noauthA = await a.cmd('*1\r\n$4\r\nPING\r\n');
234
+ ok('redis unauth socket gets NOAUTH', noauthA.startsWith('-NOAUTH'), noauthA);
235
+ const authB = await b.cmd('*2\r\n$4\r\nAUTH\r\n$2\r\npw\r\n');
236
+ ok('redis AUTH pw succeeds', authB === '+OK', authB);
237
+ const pingB = await b.cmd('*1\r\n$4\r\nPING\r\n');
238
+ ok('redis authed socket works', pingB === '+PONG', pingB);
239
+ const pingA = await a.cmd('*1\r\n$4\r\nPING\r\n');
240
+ ok('redis auth is per-socket (other socket still NOAUTH)', pingA.startsWith('-NOAUTH'), pingA);
241
+ a.end(); b.end();
242
+ rs.server.close();
243
+ }
244
+
245
+ /* ============ MySQL ACL (S3 / S4) ============ */
246
+ {
247
+ const ms = new MysqlServer({ dataDir: ':memory:', auth: { alice: { password: 'pw', databases: ['app'] } } });
248
+ ok('mysql acl allows member db', ms._canAccessDb('alice', 'app') === true);
249
+ ok('mysql acl denies foreign db', ms._canAccessDb('alice', 'other') === false);
250
+ let code = null;
251
+ try { await ms._switchDb({ user: 'alice' }, 'other'); } catch (e) { code = e.code; }
252
+ ok('mysql cross-db switch denied (1044)', code === 1044, code);
253
+ }
254
+
255
+ /* ============ mysql_compat pool filename (S6) ============ */
256
+ {
257
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-cp-'));
258
+ const file = path.join(dir, 'pool.json');
259
+ const pool = compat.createPool({ filename: file });
260
+ await pool.query('CREATE TABLE t (id INT PRIMARY KEY, v VARCHAR(20))');
261
+ await pool.query("INSERT INTO t VALUES (1,'x')");
262
+ const [rows] = await pool.query('SELECT * FROM t');
263
+ ok('mysql_compat pool query roundtrip', rows.length === 1 && rows[0].v === 'x', rows);
264
+ ok('mysql_compat pool persists to filename', fs.existsSync(file) && fs.statSync(file).size > 0);
265
+ if (typeof pool.end === 'function') await pool.end();
266
+ fs.rmSync(dir, { recursive: true, force: true });
267
+ }
268
+
269
+ console.log(failed === 0 ? `\nALL ${passed} REGRESSION TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
270
+ process.exit(failed === 0 ? 0 : 1);
271
+ })().catch((e) => { console.error('FATAL', e); process.exit(1); });