jsql-neo 5.3.0 → 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 +248 -33
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +182 -23
- package/lib/query.js +20 -2
- package/lib/sql.js +44 -6
- package/lib/sqlite_worker.js +5 -1
- package/lib/table.js +20 -2
- 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/query.js
CHANGED
|
@@ -537,7 +537,7 @@ class Query {
|
|
|
537
537
|
const key = fr[join.foreignField];
|
|
538
538
|
const matches = localRows.filter(lr => lr[join.localField] === key);
|
|
539
539
|
if (matches.length === 0) {
|
|
540
|
-
result.push(
|
|
540
|
+
result.push(this._rightNullRow(localRows, fr, join));
|
|
541
541
|
} else {
|
|
542
542
|
for (const lr of matches) {
|
|
543
543
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -582,7 +582,7 @@ class Query {
|
|
|
582
582
|
for (const fr of foreignRows) {
|
|
583
583
|
const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
|
|
584
584
|
if (matches.length === 0) {
|
|
585
|
-
result.push(
|
|
585
|
+
result.push(this._rightNullRow(localRows, fr, join));
|
|
586
586
|
} else {
|
|
587
587
|
for (const lr of matches) {
|
|
588
588
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -621,6 +621,24 @@ class Query {
|
|
|
621
621
|
return nulls;
|
|
622
622
|
}
|
|
623
623
|
|
|
624
|
+
/**
|
|
625
|
+
* RIGHT JOIN 未匹配右表行:保留右表数据,本地表字段填 null。
|
|
626
|
+
* 返回 { localNulls, merged },其中 merged = { ...nulls, ...fr }
|
|
627
|
+
*/
|
|
628
|
+
_rightNullRow(localRows, foreignRow, join) {
|
|
629
|
+
const localSchema = this._table._schema || {};
|
|
630
|
+
const localNulls = {};
|
|
631
|
+
for (const field of Object.keys(localSchema)) {
|
|
632
|
+
if (field !== '_softDelete') localNulls[field] = null;
|
|
633
|
+
}
|
|
634
|
+
const prefix = join.as ? join.as + '_' : '';
|
|
635
|
+
const foreignPrefixed = {};
|
|
636
|
+
for (const [key, value] of Object.entries(foreignRow)) {
|
|
637
|
+
foreignPrefixed[prefix + key] = value;
|
|
638
|
+
}
|
|
639
|
+
return { ...localNulls, ...foreignPrefixed };
|
|
640
|
+
}
|
|
641
|
+
|
|
624
642
|
// ============================================================
|
|
625
643
|
// 内部: 排序
|
|
626
644
|
// ============================================================
|
package/lib/sql.js
CHANGED
|
@@ -261,12 +261,18 @@ class Parser {
|
|
|
261
261
|
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
262
262
|
this.next();
|
|
263
263
|
const t2 = this.next();
|
|
264
|
-
if (t2.type !== 'ident'
|
|
264
|
+
if (t2.type !== 'ident' && !(t2.type === 'keyword' && this._isSchemaView(t2.value))) {
|
|
265
|
+
throw new Error(`Expected table name after '.', got '${t2.value}'`);
|
|
266
|
+
}
|
|
265
267
|
return t.value + '.' + t2.value;
|
|
266
268
|
}
|
|
267
269
|
return t.value;
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
_isSchemaView(v) {
|
|
273
|
+
return ['TABLES', 'COLUMNS', 'SCHEMATA', 'STATISTICS', 'KEY_COLUMN_USAGE', 'REFERENTIAL_CONSTRAINTS', 'TABLE_CONSTRAINTS', 'VIEWS'].includes(String(v).toUpperCase());
|
|
274
|
+
}
|
|
275
|
+
|
|
270
276
|
parseCreateTable() {
|
|
271
277
|
this.expectKeyword('CREATE');
|
|
272
278
|
this.expectKeyword('TABLE');
|
|
@@ -1870,11 +1876,12 @@ class SQLExecutor {
|
|
|
1870
1876
|
const schema = this.engine.getTableSchema
|
|
1871
1877
|
? await this.engine.getTableSchema(statement.table)
|
|
1872
1878
|
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
1879
|
+
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1873
1880
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1874
1881
|
let count = 0;
|
|
1875
1882
|
for (const row of all) {
|
|
1876
1883
|
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1877
|
-
const id =
|
|
1884
|
+
const id = this._rowPkId(row, pkCols);
|
|
1878
1885
|
if (id !== undefined) {
|
|
1879
1886
|
const data = {};
|
|
1880
1887
|
for (const [col, val] of statement.assignments) {
|
|
@@ -1892,11 +1899,12 @@ class SQLExecutor {
|
|
|
1892
1899
|
const schema = this.engine.getTableSchema
|
|
1893
1900
|
? await this.engine.getTableSchema(statement.table)
|
|
1894
1901
|
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
1902
|
+
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1895
1903
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1896
1904
|
const ids = [];
|
|
1897
1905
|
for (const row of all) {
|
|
1898
1906
|
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1899
|
-
const id =
|
|
1907
|
+
const id = this._rowPkId(row, pkCols);
|
|
1900
1908
|
if (id !== undefined) ids.push(id);
|
|
1901
1909
|
}
|
|
1902
1910
|
}
|
|
@@ -2166,6 +2174,19 @@ class SQLExecutor {
|
|
|
2166
2174
|
return schema;
|
|
2167
2175
|
}
|
|
2168
2176
|
|
|
2177
|
+
/**
|
|
2178
|
+
* 取行的行 ID:优先内部 _rid,否则用实际主键字段值(不再硬编码 id)。
|
|
2179
|
+
*/
|
|
2180
|
+
_rowPkId(row, pkCols) {
|
|
2181
|
+
if (row && row._rid !== undefined) return row._rid;
|
|
2182
|
+
if (pkCols.length > 0) {
|
|
2183
|
+
for (const c of pkCols) {
|
|
2184
|
+
if (row[c] !== undefined && row[c] !== null) return row[c];
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return row ? row.id : undefined;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2169
2190
|
async _readTable(table) {
|
|
2170
2191
|
const schema = await this._getSchema(table);
|
|
2171
2192
|
const rows = (await this.engine.find(table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
@@ -2366,6 +2387,19 @@ class SQLExecutor {
|
|
|
2366
2387
|
return out;
|
|
2367
2388
|
}
|
|
2368
2389
|
|
|
2390
|
+
/**
|
|
2391
|
+
* 生成对端表的前缀 null 行:仅含 `prefix.col` 键(值为 null),
|
|
2392
|
+
* 用于 JOIN 未匹配行补齐限定列,避免回退到未前缀副本拿错值。
|
|
2393
|
+
*/
|
|
2394
|
+
_nullPrefixedRow(schema, prefix) {
|
|
2395
|
+
const out = {};
|
|
2396
|
+
for (const k of Object.keys(schema)) {
|
|
2397
|
+
if (k === '_softDelete') continue;
|
|
2398
|
+
out[prefix + '.' + k] = null;
|
|
2399
|
+
}
|
|
2400
|
+
return out;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2369
2403
|
_aggValue(rows, fn, column) {
|
|
2370
2404
|
if (fn === 'COUNT') return rows.length;
|
|
2371
2405
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
@@ -2445,6 +2479,7 @@ class SQLExecutor {
|
|
|
2445
2479
|
|
|
2446
2480
|
let schema = null;
|
|
2447
2481
|
let all;
|
|
2482
|
+
let rowsAll;
|
|
2448
2483
|
if (!statement.from) {
|
|
2449
2484
|
// 无 FROM:虚拟行(SELECT 1, 'a')
|
|
2450
2485
|
all = [{ _virtual: true }];
|
|
@@ -2469,7 +2504,6 @@ class SQLExecutor {
|
|
|
2469
2504
|
const rows = filtered.map(r => outCols.map(c => (c in r ? r[c] : null)));
|
|
2470
2505
|
return { ok: true, type: 'select', table: firstItem.table, columns: outCols, rows, raw: filtered };
|
|
2471
2506
|
}
|
|
2472
|
-
let rowsAll;
|
|
2473
2507
|
if (firstItem.subquery) {
|
|
2474
2508
|
const res = await this.executeSelect(firstItem.subquery);
|
|
2475
2509
|
rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
|
|
@@ -2495,6 +2529,10 @@ class SQLExecutor {
|
|
|
2495
2529
|
}
|
|
2496
2530
|
const matched = [];
|
|
2497
2531
|
const unmatchedRight = new Set(rightRows.map((r, i) => i));
|
|
2532
|
+
// 未匹配行补对端表的前缀 null 列:限定列名(如 a.id / b.id)按前缀解析,
|
|
2533
|
+
// 避免回退到未前缀副本拿到错误值。
|
|
2534
|
+
const rightNulls = (rightRes.schema) ? this._nullPrefixedRow(rightRes.schema, rightPrefix) : null;
|
|
2535
|
+
const leftNulls = (rowsAll && rowsAll.schema) ? this._nullPrefixedRow(rowsAll.schema, firstPrefix) : null;
|
|
2498
2536
|
rows.forEach(l => {
|
|
2499
2537
|
let m = null;
|
|
2500
2538
|
for (let ri = 0; ri < rightRows.length; ri++) {
|
|
@@ -2507,11 +2545,11 @@ class SQLExecutor {
|
|
|
2507
2545
|
matched.push({ ...l, ...rightRows[m] });
|
|
2508
2546
|
unmatchedRight.delete(m);
|
|
2509
2547
|
} else if (j.type === 'left') {
|
|
2510
|
-
matched.push({ ...l });
|
|
2548
|
+
matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
|
|
2511
2549
|
}
|
|
2512
2550
|
});
|
|
2513
2551
|
if (j.type === 'right') {
|
|
2514
|
-
for (const ri of unmatchedRight) matched.push({ ...rightRows[ri] });
|
|
2552
|
+
for (const ri of unmatchedRight) matched.push(leftNulls ? { ...leftNulls, ...rightRows[ri] } : { ...rightRows[ri] });
|
|
2515
2553
|
}
|
|
2516
2554
|
rows = matched;
|
|
2517
2555
|
}
|
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/table.js
CHANGED
|
@@ -242,6 +242,23 @@ class Table {
|
|
|
242
242
|
const schema = this._schema;
|
|
243
243
|
const checkConstraints = this._checkConstraints;
|
|
244
244
|
const foreignKeys = this._foreignKeys;
|
|
245
|
+
// 批量预分配自增 ID 范围:先扫描显式提供的最大值,一次性推进计数器,
|
|
246
|
+
// 再在循环内用本地序号递增,避免批量内重复读取/写入同一计数器字段。
|
|
247
|
+
const baseAutoInc = this._autoIncrement;
|
|
248
|
+
if (autoIncField) {
|
|
249
|
+
let maxExplicit = 0;
|
|
250
|
+
for (const it of items) {
|
|
251
|
+
const v = it[autoIncField];
|
|
252
|
+
if (v !== undefined && v !== null) {
|
|
253
|
+
const n = Number(v);
|
|
254
|
+
if (!isNaN(n) && n > maxExplicit) maxExplicit = n;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (maxExplicit > baseAutoInc) {
|
|
258
|
+
this._autoIncrement = maxExplicit;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let autoIncSeq = 0;
|
|
245
262
|
for (let i = 0; i < N; i++) {
|
|
246
263
|
let data = items[i];
|
|
247
264
|
for (const [f, dv] of Object.entries(defaults)) {
|
|
@@ -279,8 +296,9 @@ class Table {
|
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
298
|
if (autoIncField && data[autoIncField] === undefined) {
|
|
282
|
-
|
|
283
|
-
data[autoIncField] =
|
|
299
|
+
autoIncSeq++;
|
|
300
|
+
data[autoIncField] = baseAutoInc + autoIncSeq;
|
|
301
|
+
this._autoIncrement = data[autoIncField];
|
|
284
302
|
} else if (autoIncField && data[autoIncField] > this._autoIncrement) {
|
|
285
303
|
this._autoIncrement = data[autoIncField];
|
|
286
304
|
}
|
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
|
},
|