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/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({ ...fr, ...this._nullRow(join, true) });
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({ ...fr, ...this._nullRow(join, true) });
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') throw new Error(`Expected table name after '.', got '${t2.value}'`);
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 = row._rid !== undefined ? row._rid : row.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 = row._rid !== undefined ? row._rid : row.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
  }
@@ -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
- this._autoIncrement++;
283
- data[autoIncField] = this._autoIncrement;
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
  }
@@ -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
- mapped[field] = { type: NATIVE_TYPE_MAP[def.toLowerCase()] || def.toLowerCase() };
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
- mapped[field] = { ...def, type: NATIVE_TYPE_MAP[t] || t };
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 (!schema || typeof row !== 'object' || row === null) return row;
43
- const out = {};
44
- for (const [field, def] of Object.entries(row)) {
45
- if (field === 'fields' && typeof row[field] === 'object' && row[field] !== null) {
46
- out[field] = restoreRow(row[field], schema);
47
- continue;
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 row[field] === 'string') {
54
- try { out[field] = JSON.parse(row[field]); } catch (e) { out[field] = row[field]; }
55
- } else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof row[field] === 'string' && /^-?\d+$/.test(row[field])) {
56
- out[field] = Number(row[field]);
57
- } else if ((t === 'float' || t === 'double' || t === 'number') && typeof row[field] === 'string' && !Number.isNaN(Number(row[field]))) {
58
- out[field] = Number(row[field]);
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] = row[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 = safeJsonParse(wasmBindings.jsql_find_by_id(table, BigInt(id)));
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 = entries.map(([id, data]) => [Number(id), data]);
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 r = safeJsonParse(wasmBindings.jsql_remove_by_ids(table, JSON.stringify(ids)));
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
- const r = safeJsonParse(wasmBindings.jsql_find_by_ids(table, JSON.stringify(ids)));
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 r = safeJsonParse(wasmBindings.jsql_update_by_id(table, BigInt(id), JSON.stringify(data)));
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 r = safeJsonParse(wasmBindings.jsql_remove_by_id(table, BigInt(id)));
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 || 8080;
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';
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsql-neo-core"
3
- version = "0.1.0"
3
+ version = "5.4.0"
4
4
  edition = "2021"
5
5
  description = "Embedded database engine — core library (supports WASM)"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsql-neo-native"
3
- version = "0.1.0"
3
+ version = "5.4.0"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -171,7 +171,7 @@ dependencies = [
171
171
 
172
172
  [[package]]
173
173
  name = "jsql-neo-core"
174
- version = "0.1.0"
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 = "0.1.0"
190
+ version = "5.4.0"
191
191
  dependencies = [
192
192
  "js-sys",
193
193
  "jsql-neo-core",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsql-neo-wasm"
3
- version = "0.1.0"
3
+ version = "5.4.0"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -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.0",
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
  },