jsql-neo 5.3.1 → 5.4.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/README.md +240 -33
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +182 -23
- package/lib/sqlite_worker.js +5 -1
- package/lib/wasm_client.js +200 -23
- package/lib/web_ui.js +1 -1
- package/nativesrc/jsql-neo-core/Cargo.toml +1 -1
- package/nativesrc/jsql-neo-native/Cargo.toml +1 -1
- package/nativesrc/jsql-neo-wasm/Cargo.lock +2 -2
- package/nativesrc/jsql-neo-wasm/Cargo.toml +1 -1
- package/nativesrc/jsql-neo-wasm/src/lib.rs +24 -0
- package/package.json +3 -3
- package/test/join.test.js +110 -0
- package/test/native.test.js +159 -0
- package/test/wasm.test.js +111 -0
- package/wasm/browser.d.ts +81 -80
- package/wasm/browser.mjs +407 -242
- package/wasm/browser_bg.mjs +166 -31
- package/wasm/jsql_neo_wasm.d.ts +6 -0
- package/wasm/jsql_neo_wasm.js +57 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +3 -0
- package/wasm/package.json +1 -1
- package/lib/compress_pool.js +0 -122
- package/lib/compress_worker.js +0 -32
- package/wasm/jsql_neo_wasm_bg.js +0 -373
package/lib/sqlite_worker.js
CHANGED
|
@@ -20,7 +20,7 @@ function startEngine(filename, options) {
|
|
|
20
20
|
const opts = options || {};
|
|
21
21
|
const isMemory = filename === ':memory:' || !filename;
|
|
22
22
|
if (opts.engine === 'native') {
|
|
23
|
-
const { NativeJSQL } = require('./native_client');
|
|
23
|
+
const { JSQL: NativeJSQL } = require('./native_client');
|
|
24
24
|
const engine = new NativeJSQL({ mode: 'memory' });
|
|
25
25
|
db = {
|
|
26
26
|
start: async () => { await engine.start(); },
|
|
@@ -30,10 +30,14 @@ function startEngine(filename, options) {
|
|
|
30
30
|
dropTable: n => engine.dropTable(n),
|
|
31
31
|
insert: (n, rows) => engine.insert(n, rows),
|
|
32
32
|
find: (n, f, o) => engine.find(n, f, o),
|
|
33
|
+
updateById: (n, id, data) => engine.updateById(n, id, data),
|
|
33
34
|
updateByIds: (n, e) => engine.updateByIds(n, e),
|
|
34
35
|
removeByIds: (n, ids) => engine.removeByIds(n, ids),
|
|
35
36
|
count: (n, f) => engine.count ? engine.count(n, f) : engine.find(n, f || {}, {}).then(rs => rs.length),
|
|
36
37
|
getTableSchema: n => engine._schemas[n] || null,
|
|
38
|
+
beginTx: () => engine.beginTx(),
|
|
39
|
+
commitTx: id => engine.commitTx(id),
|
|
40
|
+
rollbackTx: id => engine.rollbackTx(id),
|
|
37
41
|
truncate: async () => { throw new Error('truncate not supported on native engine'); },
|
|
38
42
|
flush: async () => { if (engine.flush) await engine.flush(); },
|
|
39
43
|
listTables: () => Array.from(engine._tableNames || []),
|
package/lib/wasm_client.js
CHANGED
|
@@ -23,14 +23,52 @@ const NATIVE_TYPE_MAP = {
|
|
|
23
23
|
int: 'integer',
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
function parseShorthandSchema(str) {
|
|
27
|
+
const def = { type: str };
|
|
28
|
+
const strip = (re) => {
|
|
29
|
+
const next = def.type.replace(re, '').trim();
|
|
30
|
+
def.type = next || def.type;
|
|
31
|
+
};
|
|
32
|
+
if (/\bprimary\s+key\b/i.test(def.type)) { def.primaryKey = true; strip(/\bprimary\s+key\b/gi); }
|
|
33
|
+
if (/\bauto_?increment\b/i.test(def.type)) { def.autoIncrement = true; strip(/\bauto_?increment\b/gi); }
|
|
34
|
+
if (/\bnot\s+null\b/i.test(def.type)) { def.required = true; strip(/\bnot\s+null\b/gi); }
|
|
35
|
+
if (/\bunique\b/i.test(def.type)) { def.unique = true; strip(/\bunique\b/gi); }
|
|
36
|
+
if (/\bdefault\s+(\S+)/i.test(def.type)) {
|
|
37
|
+
const m = def.type.match(/\bdefault\s+(\S+)/i);
|
|
38
|
+
def.default = m[1].replace(/^['"]|['"]$/g, '');
|
|
39
|
+
strip(/\bdefault\s+\S+/gi);
|
|
40
|
+
}
|
|
41
|
+
return def;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseSchemaString(str) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const seg of str.split(',')) {
|
|
47
|
+
const trimmed = seg.trim();
|
|
48
|
+
if (!trimmed) continue;
|
|
49
|
+
const parts = trimmed.split(/\s+/);
|
|
50
|
+
const name = parts.shift();
|
|
51
|
+
const def = parseShorthandSchema(parts.join(' ').toLowerCase().trim());
|
|
52
|
+
if (def.type.length === 0) def.type = 'string';
|
|
53
|
+
out[name] = def;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
26
58
|
function mapNativeSchema(schema) {
|
|
59
|
+
if (typeof schema === 'string') schema = parseSchemaString(schema);
|
|
27
60
|
const mapped = {};
|
|
28
61
|
for (const [field, def] of Object.entries(schema || {})) {
|
|
29
62
|
if (typeof def === 'string') {
|
|
30
|
-
|
|
63
|
+
const parsed = parseShorthandSchema(def.toLowerCase().trim());
|
|
64
|
+
if (parsed.type.length === 0) parsed.type = 'string';
|
|
65
|
+
mapped[field] = { ...parsed, type: NATIVE_TYPE_MAP[parsed.type] || parsed.type };
|
|
31
66
|
} else if (def && typeof def === 'object') {
|
|
32
67
|
const t = (def.type || 'string').toLowerCase();
|
|
33
|
-
|
|
68
|
+
const out = { ...def, type: NATIVE_TYPE_MAP[t] || t };
|
|
69
|
+
if (out.primary_key !== undefined && out.primaryKey === undefined) { out.primaryKey = out.primary_key; delete out.primary_key; }
|
|
70
|
+
if (out.auto_increment !== undefined && out.autoIncrement === undefined) { out.autoIncrement = out.auto_increment; delete out.auto_increment; }
|
|
71
|
+
mapped[field] = out;
|
|
34
72
|
} else {
|
|
35
73
|
mapped[field] = { type: 'string' };
|
|
36
74
|
}
|
|
@@ -39,25 +77,46 @@ function mapNativeSchema(schema) {
|
|
|
39
77
|
}
|
|
40
78
|
|
|
41
79
|
function restoreRow(row, schema) {
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
80
|
+
if (typeof row !== 'object' || row === null) return row;
|
|
81
|
+
let src = row;
|
|
82
|
+
if (row.fields && typeof row.fields === 'object' && row.fields !== null) {
|
|
83
|
+
src = { ...row.fields };
|
|
84
|
+
if (row.id !== undefined) {
|
|
85
|
+
const pkCols = [];
|
|
86
|
+
if (schema) {
|
|
87
|
+
for (const [k, def] of Object.entries(schema)) {
|
|
88
|
+
if (def && typeof def === 'object' && def.autoIncrement) continue;
|
|
89
|
+
const isPk = typeof def === 'string'
|
|
90
|
+
? /\bprimary\s+key\b/i.test(def)
|
|
91
|
+
: !!(def && (def.primaryKey || def.primary_key));
|
|
92
|
+
if (isPk) pkCols.push(k);
|
|
93
|
+
}
|
|
94
|
+
} else if (src.id === undefined) {
|
|
95
|
+
pkCols.push('id');
|
|
96
|
+
}
|
|
97
|
+
for (const c of pkCols) {
|
|
98
|
+
const v = src[c];
|
|
99
|
+
if (v === undefined || v === null || v === 0 || v === '' || v === false) src[c] = row.id;
|
|
100
|
+
}
|
|
48
101
|
}
|
|
102
|
+
if (row.created_at !== undefined) src.created_at = row.created_at;
|
|
103
|
+
if (row.updated_at !== undefined) src.updated_at = row.updated_at;
|
|
104
|
+
}
|
|
105
|
+
if (!schema) return src;
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [field, value] of Object.entries(src)) {
|
|
49
108
|
const colDef = schema[field];
|
|
50
109
|
let t = null;
|
|
51
110
|
if (typeof colDef === 'string') t = colDef.toLowerCase();
|
|
52
111
|
else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
|
|
53
|
-
if (['json', 'array', 'object'].includes(t) && typeof
|
|
54
|
-
try { out[field] = JSON.parse(
|
|
55
|
-
} else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof
|
|
56
|
-
out[field] = Number(
|
|
57
|
-
} else if ((t === 'float' || t === 'double' || t === 'number') && typeof
|
|
58
|
-
out[field] = Number(
|
|
112
|
+
if (['json', 'array', 'object'].includes(t) && typeof value === 'string') {
|
|
113
|
+
try { out[field] = JSON.parse(value); } catch (e) { out[field] = value; }
|
|
114
|
+
} else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof value === 'string' && /^-?\d+$/.test(value)) {
|
|
115
|
+
out[field] = Number(value);
|
|
116
|
+
} else if ((t === 'float' || t === 'double' || t === 'number') && typeof value === 'string' && !Number.isNaN(Number(value))) {
|
|
117
|
+
out[field] = Number(value);
|
|
59
118
|
} else {
|
|
60
|
-
out[field] =
|
|
119
|
+
out[field] = value;
|
|
61
120
|
}
|
|
62
121
|
}
|
|
63
122
|
return out;
|
|
@@ -230,6 +289,7 @@ class JSQL {
|
|
|
230
289
|
this._bufferSize = 0;
|
|
231
290
|
this._tableNames = new Set();
|
|
232
291
|
this._schemas = {};
|
|
292
|
+
this._txId = undefined;
|
|
233
293
|
this._autoPlugins = opts.modules !== false;
|
|
234
294
|
|
|
235
295
|
this._plugins = [];
|
|
@@ -324,6 +384,62 @@ class JSQL {
|
|
|
324
384
|
this._emit('start', {});
|
|
325
385
|
}
|
|
326
386
|
|
|
387
|
+
_pkOf(table) {
|
|
388
|
+
const schema = this._schemas[table];
|
|
389
|
+
if (!schema) return null;
|
|
390
|
+
for (const [k, def] of Object.entries(schema)) {
|
|
391
|
+
const isPk = (typeof def === 'string' && /\bprimary\s+key\b/i.test(def))
|
|
392
|
+
|| !!(def && typeof def === 'object' && (def.primaryKey || def.primary_key));
|
|
393
|
+
if (isPk) return k;
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
_pkAutoIncrement(table) {
|
|
399
|
+
const schema = this._schemas[table];
|
|
400
|
+
const pk = this._pkOf(table);
|
|
401
|
+
if (!pk || !schema) return false;
|
|
402
|
+
const def = schema[pk];
|
|
403
|
+
if (typeof def === 'string') return /\bauto_?increment\b/i.test(def);
|
|
404
|
+
return !!(def && def.autoIncrement);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_coercePkId(table, id) {
|
|
408
|
+
const schema = this._schemas[table];
|
|
409
|
+
const pk = this._pkOf(table);
|
|
410
|
+
if (!schema || !pk) return id;
|
|
411
|
+
const def = schema[pk];
|
|
412
|
+
let t = null;
|
|
413
|
+
if (typeof def === 'string') t = def.toLowerCase();
|
|
414
|
+
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
415
|
+
if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
|
|
416
|
+
return id;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_findRawById(table, id) {
|
|
420
|
+
const pk = this._pkOf(table);
|
|
421
|
+
if (pk) {
|
|
422
|
+
const filter = {};
|
|
423
|
+
filter[pk] = this._coercePkId(table, id);
|
|
424
|
+
const r = safeJsonParse(wasmBindings.jsql_find(table, JSON.stringify(filter), 1, 0));
|
|
425
|
+
if (r && r.error) return r;
|
|
426
|
+
return Array.isArray(r) && r.length > 0 ? r[0] : null;
|
|
427
|
+
}
|
|
428
|
+
return safeJsonParse(wasmBindings.jsql_find_by_id(table, BigInt(id)));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_seqIdByPk(table, id) {
|
|
432
|
+
const pk = this._pkOf(table);
|
|
433
|
+
if (pk) {
|
|
434
|
+
const filter = {};
|
|
435
|
+
filter[pk] = this._coercePkId(table, id);
|
|
436
|
+
const r = safeJsonParse(wasmBindings.jsql_find(table, JSON.stringify(filter), 1, 0));
|
|
437
|
+
if (Array.isArray(r) && r.length > 0) return r[0].id;
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
return Number(id);
|
|
441
|
+
}
|
|
442
|
+
|
|
327
443
|
async _insertBatch(table, rows) {
|
|
328
444
|
const schema = this._schemas[table];
|
|
329
445
|
if (schema) {
|
|
@@ -347,6 +463,15 @@ class JSQL {
|
|
|
347
463
|
}
|
|
348
464
|
const r = safeJsonParse(wasmBindings.jsql_insert_json(table, JSON.stringify(rows)));
|
|
349
465
|
if (r && r.error) throw new Error(r.error);
|
|
466
|
+
if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
|
|
467
|
+
const pk = this._pkOf(table);
|
|
468
|
+
if (pk) {
|
|
469
|
+
return r.map((seq, i) => {
|
|
470
|
+
const row = rows[i];
|
|
471
|
+
return (row && row[pk] !== undefined && row[pk] !== null) ? row[pk] : seq;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
350
475
|
return r;
|
|
351
476
|
}
|
|
352
477
|
|
|
@@ -401,7 +526,7 @@ class JSQL {
|
|
|
401
526
|
const r = safeJsonParse(wasmBindings.jsql_create_table(name, JSON.stringify(mapNativeSchema(schema))));
|
|
402
527
|
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|
|
403
528
|
this._tableNames.add(name);
|
|
404
|
-
this._schemas[name] = schema;
|
|
529
|
+
this._schemas[name] = typeof schema === 'string' ? mapNativeSchema(schema) : schema;
|
|
405
530
|
this._emit('createTable', { name, schema });
|
|
406
531
|
this._runHooks('afterCreateTable', [name, schema]);
|
|
407
532
|
return r;
|
|
@@ -422,7 +547,7 @@ class JSQL {
|
|
|
422
547
|
async findById(table, id) {
|
|
423
548
|
await this._flush();
|
|
424
549
|
if (!this._runHooks('beforeFind', [table, { id }])) return null;
|
|
425
|
-
const r =
|
|
550
|
+
const r = this._findRawById(table, id);
|
|
426
551
|
if (r && r.error) throw new Error(r.error);
|
|
427
552
|
this._runHooks('afterFind', [table, { id }, r]);
|
|
428
553
|
const schema = this._schemas[table];
|
|
@@ -453,7 +578,11 @@ class JSQL {
|
|
|
453
578
|
}
|
|
454
579
|
|
|
455
580
|
async updateByIds(table, entries) {
|
|
456
|
-
const pairs =
|
|
581
|
+
const pairs = [];
|
|
582
|
+
for (const [id, data] of entries) {
|
|
583
|
+
const seq = this._seqIdByPk(table, id);
|
|
584
|
+
if (seq !== null) pairs.push([seq, data]);
|
|
585
|
+
}
|
|
457
586
|
if (!this._runHooks('beforeUpdate', [table, pairs])) return;
|
|
458
587
|
await this._flush();
|
|
459
588
|
const r = safeJsonParse(wasmBindings.jsql_update_by_ids(table, JSON.stringify(pairs)));
|
|
@@ -466,9 +595,10 @@ class JSQL {
|
|
|
466
595
|
async removeByIds(table, ids) {
|
|
467
596
|
if (!this._runHooks('beforeDelete', [table, ids])) return;
|
|
468
597
|
await this._flush();
|
|
469
|
-
const
|
|
598
|
+
const seqs = ids.map(id => this._seqIdByPk(table, id)).filter(s => s !== null);
|
|
599
|
+
const r = safeJsonParse(wasmBindings.jsql_remove_by_ids(table, JSON.stringify(seqs)));
|
|
470
600
|
if (r && r.error) throw new Error(r.error);
|
|
471
|
-
this._emit('delete', { table, ids, result: r });
|
|
601
|
+
this._emit('delete', { table, ids: seqs, result: r });
|
|
472
602
|
this._runHooks('afterDelete', [table, ids, r]);
|
|
473
603
|
return r;
|
|
474
604
|
}
|
|
@@ -476,7 +606,12 @@ class JSQL {
|
|
|
476
606
|
async findByIds(table, ids) {
|
|
477
607
|
await this._flush();
|
|
478
608
|
if (!this._runHooks('beforeFind', [table, { ids }])) return null;
|
|
479
|
-
|
|
609
|
+
let r;
|
|
610
|
+
if (this._pkOf(table)) {
|
|
611
|
+
r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
|
|
612
|
+
} else {
|
|
613
|
+
r = safeJsonParse(wasmBindings.jsql_find_by_ids(table, JSON.stringify(ids)));
|
|
614
|
+
}
|
|
480
615
|
if (r && r.error) throw new Error(r.error);
|
|
481
616
|
this._runHooks('afterFind', [table, { ids }, r]);
|
|
482
617
|
const schema = this._schemas[table];
|
|
@@ -491,7 +626,9 @@ class JSQL {
|
|
|
491
626
|
async updateById(table, id, data) {
|
|
492
627
|
if (!this._runHooks('beforeUpdate', [table, id, data])) return;
|
|
493
628
|
await this._flush();
|
|
494
|
-
const
|
|
629
|
+
const seq = this._seqIdByPk(table, id);
|
|
630
|
+
if (seq === null) return null;
|
|
631
|
+
const r = safeJsonParse(wasmBindings.jsql_update_by_id(table, BigInt(seq), JSON.stringify(data)));
|
|
495
632
|
if (r && r.ok === false) throw new Error(r.error || 'update failed');
|
|
496
633
|
this._emit('update', { table, id, data });
|
|
497
634
|
this._runHooks('afterUpdate', [table, id, data, r]);
|
|
@@ -501,13 +638,53 @@ class JSQL {
|
|
|
501
638
|
async removeById(table, id) {
|
|
502
639
|
if (!this._runHooks('beforeDelete', [table, id])) return;
|
|
503
640
|
await this._flush();
|
|
504
|
-
const
|
|
641
|
+
const seq = this._seqIdByPk(table, id);
|
|
642
|
+
if (seq === null) return null;
|
|
643
|
+
const r = safeJsonParse(wasmBindings.jsql_remove_by_id(table, BigInt(seq)));
|
|
505
644
|
if (r && r.ok === false) throw new Error(r.error || 'remove failed');
|
|
506
645
|
this._emit('delete', { table, id });
|
|
507
646
|
this._runHooks('afterDelete', [table, id, r]);
|
|
508
647
|
return r;
|
|
509
648
|
}
|
|
510
649
|
|
|
650
|
+
async beginTx() {
|
|
651
|
+
await this._flush();
|
|
652
|
+
const r = safeJsonParse(wasmBindings.jsql_begin_tx());
|
|
653
|
+
if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
|
|
654
|
+
this._txId = r.txId;
|
|
655
|
+
return r.txId;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async commitTx(txId) {
|
|
659
|
+
const r = safeJsonParse(wasmBindings.jsql_commit_tx(String(txId)));
|
|
660
|
+
if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async rollbackTx(txId) {
|
|
665
|
+
const r = safeJsonParse(wasmBindings.jsql_rollback_tx(String(txId)));
|
|
666
|
+
if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async beginTransaction() {
|
|
671
|
+
return this.beginTx();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async commit() {
|
|
675
|
+
if (this._txId === undefined) return true;
|
|
676
|
+
const r = await this.commitTx(this._txId);
|
|
677
|
+
this._txId = undefined;
|
|
678
|
+
return r;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async rollback() {
|
|
682
|
+
if (this._txId === undefined) return true;
|
|
683
|
+
const r = await this.rollbackTx(this._txId);
|
|
684
|
+
this._txId = undefined;
|
|
685
|
+
return r;
|
|
686
|
+
}
|
|
687
|
+
|
|
511
688
|
async hasTable(name) {
|
|
512
689
|
return this._tableNames.has(name);
|
|
513
690
|
}
|
package/lib/web_ui.js
CHANGED
|
@@ -111,7 +111,7 @@ load();
|
|
|
111
111
|
|
|
112
112
|
class WebUI {
|
|
113
113
|
constructor(opts = {}) {
|
|
114
|
-
this.port = opts.port
|
|
114
|
+
this.port = opts.port !== undefined ? opts.port : 8080;
|
|
115
115
|
this.dataDir = opts.dataDir || '.';
|
|
116
116
|
this.readonly = !!opts.readonly;
|
|
117
117
|
this.host = opts.host || '127.0.0.1';
|
|
@@ -171,7 +171,7 @@ dependencies = [
|
|
|
171
171
|
|
|
172
172
|
[[package]]
|
|
173
173
|
name = "jsql-neo-core"
|
|
174
|
-
version = "
|
|
174
|
+
version = "5.4.0"
|
|
175
175
|
dependencies = [
|
|
176
176
|
"bincode",
|
|
177
177
|
"chrono",
|
|
@@ -187,7 +187,7 @@ dependencies = [
|
|
|
187
187
|
|
|
188
188
|
[[package]]
|
|
189
189
|
name = "jsql-neo-wasm"
|
|
190
|
-
version = "
|
|
190
|
+
version = "5.4.0"
|
|
191
191
|
dependencies = [
|
|
192
192
|
"js-sys",
|
|
193
193
|
"jsql-neo-core",
|
|
@@ -421,3 +421,27 @@ pub fn jsql_find_by_ids(table: &str, ids_json: &str) -> String {
|
|
|
421
421
|
}
|
|
422
422
|
})
|
|
423
423
|
}
|
|
424
|
+
|
|
425
|
+
#[wasm_bindgen]
|
|
426
|
+
pub fn jsql_begin_tx() -> String {
|
|
427
|
+
ENGINE.with(|eng| match eng.borrow_mut().begin_tx() {
|
|
428
|
+
Ok(tx_id) => serde_json::json!({ "ok": true, "txId": tx_id }).to_string(),
|
|
429
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#[wasm_bindgen]
|
|
434
|
+
pub fn jsql_commit_tx(tx_id: &str) -> String {
|
|
435
|
+
ENGINE.with(|eng| match eng.borrow_mut().commit_tx(tx_id) {
|
|
436
|
+
Ok(()) => r#"{"ok":true}"#.to_string(),
|
|
437
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#[wasm_bindgen]
|
|
442
|
+
pub fn jsql_rollback_tx(tx_id: &str) -> String {
|
|
443
|
+
ENGINE.with(|eng| match eng.borrow_mut().rollback_tx(tx_id) {
|
|
444
|
+
Ok(()) => r#"{"ok":true}"#.to_string(),
|
|
445
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
446
|
+
})
|
|
447
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"types": "./wasm/browser.d.ts",
|
|
20
20
|
"default": "./wasm/browser.mjs"
|
|
21
21
|
},
|
|
22
|
-
"./wasm/browser_bg.mjs": "./wasm/browser_bg.mjs",
|
|
23
22
|
"./wasm/browser.d.ts": "./wasm/browser.d.ts",
|
|
24
23
|
"./wasm/*": "./wasm/*",
|
|
25
24
|
"./lib/*.js": "./lib/*.js",
|
|
@@ -48,8 +47,9 @@
|
|
|
48
47
|
"postinstall": "node postinstall.js",
|
|
49
48
|
"test": "node test/smoke.js",
|
|
50
49
|
"test:btree": "node test/btree.test.js",
|
|
50
|
+
"test:native": "node test/native.test.js",
|
|
51
51
|
"test:regress": "node test/regress-5.1.0.js",
|
|
52
|
-
"test:all": "node test/smoke.js && node test/coverage.js && node test/regress-5.1.0.js && node test/btree.test.js",
|
|
52
|
+
"test:all": "node test/smoke.js && node test/native.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js",
|
|
53
53
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/mysql_server.js' --exclude='lib/native_client.js' --exclude='lib/wasm_client.js' --exclude='lib/mysql_compat.js' --exclude='lib/nedb_compat.js' --exclude='lib/plugin.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
54
54
|
"test:orms": "node examples/orms/run-all.js"
|
|
55
55
|
},
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Regression tests for SQL JOIN null-fill correctness.
|
|
3
|
+
* Zero runtime dependencies (uses Node builtins + in-repo libs only).
|
|
4
|
+
*
|
|
5
|
+
* node test/join.test.js
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* J1 LEFT JOIN unmatched rows null-fill right-table qualified columns
|
|
9
|
+
* J2 RIGHT JOIN unmatched rows null-fill left-table qualified columns
|
|
10
|
+
* J3 INNER JOIN unaffected
|
|
11
|
+
* J4 unqualified column reference in JOIN
|
|
12
|
+
* J5 WHERE filtering on the null-filled side
|
|
13
|
+
* J6 self-join with table alias
|
|
14
|
+
* J7 chained joins
|
|
15
|
+
*/
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const ROOT = path.join(__dirname, '..');
|
|
19
|
+
const Database = require(path.join(ROOT, 'lib/database'));
|
|
20
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql'));
|
|
21
|
+
|
|
22
|
+
let passed = 0, failed = 0;
|
|
23
|
+
function ok(name, cond, extra) {
|
|
24
|
+
if (cond) { passed++; console.log('[OK]', name); }
|
|
25
|
+
else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function setup(db) {
|
|
29
|
+
await executeSQL(db, 'CREATE TABLE a (id INT PRIMARY KEY, a_name STRING)');
|
|
30
|
+
await executeSQL(db, 'CREATE TABLE b (id INT PRIMARY KEY, a_id INT, b_name STRING)');
|
|
31
|
+
await executeSQL(db, "INSERT INTO a VALUES (1,'A1'),(2,'A2'),(3,'A3')");
|
|
32
|
+
await executeSQL(db, "INSERT INTO b VALUES (10,2,'B2'),(20,99,'B99')");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
(async () => {
|
|
36
|
+
/* ============ J1 LEFT JOIN null-fill ============ */
|
|
37
|
+
{
|
|
38
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
39
|
+
await setup(db);
|
|
40
|
+
const r = await executeSQL(db, 'SELECT a.id AS aid, a.a_name, b.id AS bid, b.b_name FROM a LEFT JOIN b ON a.id = b.a_id');
|
|
41
|
+
const row1 = r.rows[0]; // a=1 unmatched
|
|
42
|
+
ok('J1 left unmatched row', row1 && row1[0] === 1 && row1[1] === 'A1' && row1[2] === null && row1[3] === null, row1);
|
|
43
|
+
const row3 = r.rows[2]; // a=3 unmatched
|
|
44
|
+
ok('J1 second unmatched row', row3 && row3[0] === 3 && row3[2] === null, row3);
|
|
45
|
+
const row2 = r.rows[1]; // matched
|
|
46
|
+
ok('J1 matched row intact', row2 && row2[0] === 2 && row2[1] === 'A2' && row2[2] === 10 && row2[3] === 'B2', row2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ============ J2 RIGHT JOIN null-fill ============ */
|
|
50
|
+
{
|
|
51
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
52
|
+
await setup(db);
|
|
53
|
+
const r = await executeSQL(db, 'SELECT a.id AS aid, a.a_name, b.id AS bid, b.b_name FROM a RIGHT JOIN b ON a.id = b.a_id');
|
|
54
|
+
const b99 = r.rows[1]; // b=20 unmatched (a_id=99)
|
|
55
|
+
ok('J2 right unmatched row', b99 && b99[0] === null && b99[1] === null && b99[2] === 20 && b99[3] === 'B99', b99);
|
|
56
|
+
const b2 = r.rows[0]; // matched
|
|
57
|
+
ok('J2 matched row intact', b2 && b2[0] === 2 && b2[1] === 'A2' && b2[2] === 10 && b2[3] === 'B2', b2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ============ J3 INNER JOIN ============ */
|
|
61
|
+
{
|
|
62
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
63
|
+
await setup(db);
|
|
64
|
+
const r = await executeSQL(db, 'SELECT a.id AS aid, b.id AS bid FROM a INNER JOIN b ON a.id = b.a_id');
|
|
65
|
+
ok('J3 inner join', r.rows.length === 1 && r.rows[0][0] === 2 && r.rows[0][1] === 10, r.rows);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ============ J4 unqualified reference ============ */
|
|
69
|
+
{
|
|
70
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
71
|
+
await setup(db);
|
|
72
|
+
const r = await executeSQL(db, 'SELECT a.id AS aid, b.b_name FROM a LEFT JOIN b ON a.id = b.a_id');
|
|
73
|
+
ok('J4 unprefixed id resolves to left table', r.rows[0][0] === 1 && r.rows[0][1] === null, r.rows[0]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ============ J5 WHERE on null-filled side ============ */
|
|
77
|
+
{
|
|
78
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
79
|
+
await setup(db);
|
|
80
|
+
const r = await executeSQL(db, "SELECT a.id AS aid, b.b_name FROM a LEFT JOIN b ON a.id = b.a_id WHERE b.id IS NULL");
|
|
81
|
+
ok('J5 left where b.id IS NULL', r.rows.length === 2 && r.rows.every(x => x[1] === null), r.rows);
|
|
82
|
+
const r2 = await executeSQL(db, "SELECT a.a_name, b.id AS bid FROM a RIGHT JOIN b ON a.id = b.a_id WHERE a.id IS NULL");
|
|
83
|
+
ok('J5 right where a.id IS NULL', r2.rows.length === 1 && r2.rows[0][0] === null && r2.rows[0][1] === 20, r2.rows);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ============ J6 self-join with alias ============ */
|
|
87
|
+
{
|
|
88
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
89
|
+
await executeSQL(db, 'CREATE TABLE emp (id INT PRIMARY KEY, mgr_id INT, name STRING)');
|
|
90
|
+
await executeSQL(db, "INSERT INTO emp VALUES (1,NULL,'boss'),(2,1,'alice'),(3,999,'orphan')");
|
|
91
|
+
const r = await executeSQL(db, 'SELECT e.name AS ename, m.name AS mname FROM emp e LEFT JOIN emp m ON e.mgr_id = m.id');
|
|
92
|
+
ok('J6 self left join', r.rows.length === 3 && r.rows[0][0] === 'boss' && r.rows[0][1] === null && r.rows[1][1] === 'boss' && r.rows[2][1] === null, r.rows);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ============ J7 chained joins ============ */
|
|
96
|
+
{
|
|
97
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
98
|
+
await executeSQL(db, 'CREATE TABLE a (id INT PRIMARY KEY, a_name STRING)');
|
|
99
|
+
await executeSQL(db, 'CREATE TABLE b (id INT PRIMARY KEY, a_id INT, b_name STRING)');
|
|
100
|
+
await executeSQL(db, 'CREATE TABLE c (id INT PRIMARY KEY, b_id INT, c_name STRING)');
|
|
101
|
+
await executeSQL(db, "INSERT INTO a VALUES (1,'A1'),(2,'A2'),(3,'A3')");
|
|
102
|
+
await executeSQL(db, "INSERT INTO b VALUES (10,2,'B2'),(20,99,'B99')");
|
|
103
|
+
await executeSQL(db, "INSERT INTO c VALUES (100,10,'C10'),(200,77,'C77')");
|
|
104
|
+
const r = await executeSQL(db, 'SELECT a.id AS aid, b.id AS bid, c.id AS cid, c.c_name FROM a LEFT JOIN b ON a.id = b.a_id LEFT JOIN c ON b.id = c.b_id');
|
|
105
|
+
ok('J7 chained left joins', r.rows.length === 3 && r.rows[1][1] === 10 && r.rows[1][2] === 100 && r.rows[1][3] === 'C10' && r.rows[0][1] === null && r.rows[0][2] === null, r.rows);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(failed === 0 ? `\nALL ${passed} JOIN TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
|
|
109
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
110
|
+
})().catch((e) => { console.error('FATAL', e); process.exit(1); });
|