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/wasm/browser.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- init,
3
2
  jsql_reset,
4
3
  jsql_create_table,
5
4
  jsql_drop_table,
@@ -12,30 +11,151 @@ import {
12
11
  jsql_remove_by_id,
13
12
  jsql_remove_by_ids,
14
13
  jsql_count,
14
+ jsql_begin_tx,
15
+ jsql_commit_tx,
16
+ jsql_rollback_tx,
15
17
  } from './browser_bg.mjs';
18
+ import init from './browser_bg.mjs';
16
19
 
17
20
  function safeJsonParse(str) {
18
21
  try { return JSON.parse(str); } catch { return str; }
19
22
  }
20
23
 
21
- const PAGE = 500;
24
+ const NATIVE_TYPE_MAP = {
25
+ text: 'string',
26
+ varchar: 'string',
27
+ double: 'float',
28
+ number: 'float',
29
+ numeric: 'float',
30
+ decimal: 'float',
31
+ date: 'string',
32
+ timestamp: 'string',
33
+ datetime: 'string',
34
+ json: 'string',
35
+ array: 'string',
36
+ object: 'string',
37
+ bool: 'boolean',
38
+ bigint: 'string',
39
+ int: 'integer',
40
+ };
41
+
42
+ function parseShorthandSchema(str) {
43
+ const def = { type: str };
44
+ const strip = (re) => {
45
+ const next = def.type.replace(re, '').trim();
46
+ def.type = next || def.type;
47
+ };
48
+ if (/\bprimary\s+key\b/i.test(def.type)) { def.primaryKey = true; strip(/\bprimary\s+key\b/gi); }
49
+ if (/\bauto_?increment\b/i.test(def.type)) { def.autoIncrement = true; strip(/\bauto_?increment\b/gi); }
50
+ if (/\bnot\s+null\b/i.test(def.type)) { def.required = true; strip(/\bnot\s+null\b/gi); }
51
+ if (/\bunique\b/i.test(def.type)) { def.unique = true; strip(/\bunique\b/gi); }
52
+ if (/\bdefault\s+(\S+)/i.test(def.type)) {
53
+ const m = def.type.match(/\bdefault\s+(\S+)/i);
54
+ def.default = m[1].replace(/^['"]|['"]$/g, '');
55
+ strip(/\bdefault\s+\S+/gi);
56
+ }
57
+ return def;
58
+ }
59
+
60
+ function parseSchemaString(str) {
61
+ const out = {};
62
+ for (const seg of str.split(',')) {
63
+ const trimmed = seg.trim();
64
+ if (!trimmed) continue;
65
+ const parts = trimmed.split(/\s+/);
66
+ const name = parts.shift();
67
+ const def = parseShorthandSchema(parts.join(' ').toLowerCase().trim());
68
+ if (def.type.length === 0) def.type = 'string';
69
+ out[name] = def;
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function mapNativeSchema(schema) {
75
+ if (typeof schema === 'string') schema = parseSchemaString(schema);
76
+ const mapped = {};
77
+ for (const [field, def] of Object.entries(schema || {})) {
78
+ if (typeof def === 'string') {
79
+ const parsed = parseShorthandSchema(def.toLowerCase().trim());
80
+ if (parsed.type.length === 0) parsed.type = 'string';
81
+ mapped[field] = { ...parsed, type: NATIVE_TYPE_MAP[parsed.type] || parsed.type };
82
+ } else if (def && typeof def === 'object') {
83
+ const t = (def.type || 'string').toLowerCase();
84
+ const out = { ...def, type: NATIVE_TYPE_MAP[t] || t };
85
+ if (out.primary_key !== undefined && out.primaryKey === undefined) { out.primaryKey = out.primary_key; delete out.primary_key; }
86
+ if (out.auto_increment !== undefined && out.autoIncrement === undefined) { out.autoIncrement = out.auto_increment; delete out.auto_increment; }
87
+ mapped[field] = out;
88
+ } else {
89
+ mapped[field] = { type: 'string' };
90
+ }
91
+ }
92
+ return mapped;
93
+ }
94
+
95
+ function restoreRow(row, schema) {
96
+ if (typeof row !== 'object' || row === null) return row;
97
+ let src = row;
98
+ if (row.fields && typeof row.fields === 'object' && row.fields !== null) {
99
+ src = { ...row.fields };
100
+ if (row.id !== undefined) {
101
+ const pkCols = [];
102
+ if (schema) {
103
+ for (const [k, def] of Object.entries(schema)) {
104
+ if (def && typeof def === 'object' && def.autoIncrement) continue;
105
+ const isPk = typeof def === 'string'
106
+ ? /\bprimary\s+key\b/i.test(def)
107
+ : !!(def && (def.primaryKey || def.primary_key));
108
+ if (isPk) pkCols.push(k);
109
+ }
110
+ } else if (src.id === undefined) {
111
+ pkCols.push('id');
112
+ }
113
+ for (const c of pkCols) {
114
+ const v = src[c];
115
+ if (v === undefined || v === null || v === 0 || v === '' || v === false) src[c] = row.id;
116
+ }
117
+ }
118
+ if (row.created_at !== undefined) src.created_at = row.created_at;
119
+ if (row.updated_at !== undefined) src.updated_at = row.updated_at;
120
+ }
121
+ if (!schema) return src;
122
+ const out = {};
123
+ for (const [field, value] of Object.entries(src)) {
124
+ const colDef = schema[field];
125
+ let t = null;
126
+ if (typeof colDef === 'string') t = colDef.toLowerCase();
127
+ else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
128
+ if (['json', 'array', 'object'].includes(t) && typeof value === 'string') {
129
+ try { out[field] = JSON.parse(value); } catch (e) { out[field] = value; }
130
+ } else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof value === 'string' && /^-?\d+$/.test(value)) {
131
+ out[field] = Number(value);
132
+ } else if ((t === 'float' || t === 'double' || t === 'number') && typeof value === 'string' && !Number.isNaN(Number(value))) {
133
+ out[field] = Number(value);
134
+ } else {
135
+ out[field] = value;
136
+ }
137
+ }
138
+ return out;
139
+ }
140
+
141
+ const INT64_TAG = 1;
142
+ const FLOAT_TAG = 2;
143
+ const STR_TAG = 3;
144
+ const BOOL_TAG = 4;
145
+ const INT32_TAG = 5;
146
+
22
147
 
23
- /**
24
- * JSQL-NEO browser engine — full SQL database running in the browser.
25
- *
26
- * WASM (Rust) for data, IndexedDB for persistence, the shared SQL engine for
27
- * queries. Node.js and browsers share the same JSQL API.
28
- */
29
148
  export class JSQL {
30
149
  constructor(opts = {}) {
31
- this._dbName = opts.dbName || 'jsql-neo';
32
- this._persist = opts.persistence !== false;
33
- this._wasmBytes = opts.wasmBytes || null;
150
+ this._flushThreshold = opts.flushThreshold || 5000;
151
+ this._buffer = {};
152
+ this._bufferSize = 0;
34
153
  this._tableNames = new Set();
35
154
  this._schemas = {};
36
- this._dbList = new Set(['default']);
37
155
  this._txId = undefined;
38
- this._txSnapshot = null;
156
+ this._autoPlugins = opts.modules !== false;
157
+
158
+ this._plugins = [];
39
159
  this._hooks = {
40
160
  beforeInsert: [], afterInsert: [],
41
161
  beforeUpdate: [], afterUpdate: [],
@@ -44,19 +164,55 @@ export class JSQL {
44
164
  beforeCreateTable: [], afterCreateTable: [],
45
165
  beforeDropTable: [], afterDropTable: [],
46
166
  beforeFlush: [], afterFlush: [],
47
- onStart: [], onStop: [],
167
+ beforeCount: [], afterCount: [],
168
+ onStart: [], onStop: []
48
169
  };
49
170
  this._eventListeners = [];
50
- this._idb = null;
51
- this._idbPromise = null;
52
- this._persistTimer = null;
53
- this._snapPromise = null;
171
+ if (this._autoPlugins && typeof globalThis.__jsqlLoadModules === 'function') {
172
+ globalThis.__jsqlLoadModules(this);
173
+ }
54
174
  }
55
175
 
56
- /* ---------- hooks / events (same API as Node engines) ---------- */
176
+ use(plugin) {
177
+ if (typeof plugin === 'function') {
178
+ plugin = { install: plugin };
179
+ }
180
+ if (plugin.hooks) {
181
+ for (const [event, fn] of Object.entries(plugin.hooks)) {
182
+ if (this._hooks[event]) {
183
+ this._hooks[event].push(fn);
184
+ }
185
+ }
186
+ }
187
+ if (typeof plugin.install === 'function') {
188
+ plugin.install(this, this._buildCtx(plugin));
189
+ }
190
+ if (typeof plugin.onEvent === 'function') {
191
+ this._eventListeners.push(plugin.onEvent);
192
+ }
193
+ this._plugins.push(plugin);
194
+ return this;
195
+ }
196
+
197
+ _buildCtx(plugin) {
198
+ const self = this;
199
+ return {
200
+ name: plugin.name || 'anonymous',
201
+ engine: this,
202
+ plugin,
203
+ on(hook, fn) { return self.on(hook, fn); },
204
+ onEvent(fn) { return self.onEvent(fn); },
205
+ emit(eventName, data) { self._emit(eventName, data); },
206
+ tables() { return Array.from(self._tableNames); },
207
+ hasTable(name) { return self._tableNames.has(name); },
208
+ getTableSchema(name) { return self._schemas[name] || null; }
209
+ };
210
+ }
57
211
 
58
212
  on(event, fn) {
59
- if (this._hooks[event]) this._hooks[event].push(fn);
213
+ if (this._hooks[event]) {
214
+ this._hooks[event].push(fn);
215
+ }
60
216
  return this;
61
217
  }
62
218
 
@@ -67,7 +223,7 @@ export class JSQL {
67
223
 
68
224
  _emit(eventName, data) {
69
225
  for (const fn of this._eventListeners) {
70
- try { fn(eventName, data); } catch { /* ignore */ }
226
+ try { fn(eventName, data); } catch (e) { /* ignore */ }
71
227
  }
72
228
  }
73
229
 
@@ -77,130 +233,170 @@ export class JSQL {
77
233
  for (const fn of hooks) {
78
234
  const r = fn(...args);
79
235
  if (r === false) return false;
80
- if (r !== undefined && args.length > 0) args[0] = r;
236
+ if (r !== undefined && args.length > 0) {
237
+ args[0] = r;
238
+ }
81
239
  }
82
240
  return true;
83
241
  }
84
242
 
85
- /* ---------- IndexedDB persistence ---------- */
86
-
87
- _openIDB() {
88
- if (this._idbPromise) return this._idbPromise;
89
- this._idbPromise = new Promise((resolve, reject) => {
90
- const req = indexedDB.open(this._dbName, 1);
91
- req.onupgradeneeded = () => {
92
- const db = req.result;
93
- if (!db.objectStoreNames.contains('store')) db.createObjectStore('store', { keyPath: 'k' });
94
- };
95
- req.onsuccess = () => { this._idb = req.result; resolve(this._idb); };
96
- req.onerror = () => reject(req.error);
97
- });
98
- return this._idbPromise;
99
- }
100
-
101
- async _readSnapshot() {
102
- const db = await this._openIDB();
103
- return new Promise((resolve, reject) => {
104
- const tx = db.transaction('store', 'readonly');
105
- const get = tx.objectStore('store').get('snap');
106
- get.onsuccess = () => resolve(get.result ? get.result.v : null);
107
- get.onerror = () => reject(get.error);
108
- });
109
- }
110
-
111
- async _writeSnapshot() {
112
- if (!this._persist) return;
113
- if (this._snapPromise) return this._snapPromise;
114
- this._snapPromise = (async () => {
115
- const tables = {};
116
- for (const name of this._tableNames) {
117
- const rows = [];
118
- let offset = 0;
119
- for (;;) {
120
- const page = safeJsonParse(jsql_find(name, '', PAGE, offset)) || [];
121
- rows.push(...page);
122
- if (page.length < PAGE) break;
123
- offset += page.length;
124
- }
125
- tables[name] = { schema: this._schemas[name] || {}, rows: rows.map(r => r.fields) };
126
- }
127
- const db = await this._openIDB();
128
- await new Promise((resolve, reject) => {
129
- const tx = db.transaction('store', 'readwrite');
130
- tx.objectStore('store').put({ k: 'snap', v: { tables } });
131
- tx.oncomplete = () => resolve();
132
- tx.onerror = () => reject(tx.error);
133
- });
134
- })().finally(() => { this._snapPromise = null; });
135
- return this._snapPromise;
243
+ async start() {
244
+ await init();
245
+ jsql_reset();
246
+ this._runHooks('onStart', []);
247
+ this._emit('start', {});
136
248
  }
137
249
 
138
- _schedulePersist() {
139
- if (!this._persist) return;
140
- clearTimeout(this._persistTimer);
141
- this._persistTimer = setTimeout(() => { this._writeSnapshot(); }, 300);
250
+ _pkOf(table) {
251
+ const schema = this._schemas[table];
252
+ if (!schema) return null;
253
+ for (const [k, def] of Object.entries(schema)) {
254
+ const isPk = (typeof def === 'string' && /\bprimary\s+key\b/i.test(def))
255
+ || !!(def && typeof def === 'object' && (def.primaryKey || def.primary_key));
256
+ if (isPk) return k;
257
+ }
258
+ return null;
259
+ }
260
+
261
+ _pkAutoIncrement(table) {
262
+ const schema = this._schemas[table];
263
+ const pk = this._pkOf(table);
264
+ if (!pk || !schema) return false;
265
+ const def = schema[pk];
266
+ if (typeof def === 'string') return /\bauto_?increment\b/i.test(def);
267
+ return !!(def && def.autoIncrement);
268
+ }
269
+
270
+ _coercePkId(table, id) {
271
+ const schema = this._schemas[table];
272
+ const pk = this._pkOf(table);
273
+ if (!schema || !pk) return id;
274
+ const def = schema[pk];
275
+ let t = null;
276
+ if (typeof def === 'string') t = def.toLowerCase();
277
+ else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
278
+ if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
279
+ return id;
280
+ }
281
+
282
+ _findRawById(table, id) {
283
+ const pk = this._pkOf(table);
284
+ if (pk) {
285
+ const filter = {};
286
+ filter[pk] = this._coercePkId(table, id);
287
+ const r = safeJsonParse(jsql_find(table, JSON.stringify(filter), 1, 0));
288
+ if (r && r.error) return r;
289
+ return Array.isArray(r) && r.length > 0 ? r[0] : null;
290
+ }
291
+ return safeJsonParse(jsql_find_by_id(table, BigInt(id)));
142
292
  }
143
293
 
144
- /* ---------- lifecycle ---------- */
145
-
146
- async start() {
147
- const bytes = this._wasmBytes ||
148
- new Uint8Array(await (await fetch(new URL('./jsql_neo_wasm_bg.wasm', import.meta.url))).arrayBuffer());
149
- await init(bytes);
150
- jsql_reset();
151
- if (this._persist) {
152
- const snap = await this._readSnapshot();
153
- if (snap && snap.tables) {
154
- for (const [name, t] of Object.entries(snap.tables)) {
155
- if (!t.schema) continue;
156
- jsql_create_table(name, JSON.stringify(t.schema));
157
- this._tableNames.add(name);
158
- this._schemas[name] = t.schema;
159
- if (t.rows && t.rows.length) {
160
- for (let i = 0; i < t.rows.length; i += PAGE) {
161
- jsql_insert_json(name, JSON.stringify(t.rows.slice(i, i + PAGE)));
162
- }
294
+ _seqIdByPk(table, id) {
295
+ const pk = this._pkOf(table);
296
+ if (pk) {
297
+ const filter = {};
298
+ filter[pk] = this._coercePkId(table, id);
299
+ const r = safeJsonParse(jsql_find(table, JSON.stringify(filter), 1, 0));
300
+ if (Array.isArray(r) && r.length > 0) return r[0].id;
301
+ return null;
302
+ }
303
+ return Number(id);
304
+ }
305
+
306
+ async _insertBatch(table, rows) {
307
+ const schema = this._schemas[table];
308
+ if (schema) {
309
+ rows = rows.map(row => {
310
+ const out = {};
311
+ for (const [k, v] of Object.entries(row)) {
312
+ const def = schema[k];
313
+ let t = null;
314
+ if (typeof def === 'string') t = def.toLowerCase();
315
+ else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
316
+ if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
317
+ out[k] = JSON.stringify(v);
318
+ } else if (v instanceof Date) {
319
+ out[k] = v.toISOString();
320
+ } else {
321
+ out[k] = v;
163
322
  }
164
323
  }
324
+ return out;
325
+ });
326
+ }
327
+ const r = safeJsonParse(jsql_insert_json(table, JSON.stringify(rows)));
328
+ if (r && r.error) throw new Error(r.error);
329
+ if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
330
+ const pk = this._pkOf(table);
331
+ if (pk) {
332
+ return r.map((seq, i) => {
333
+ const row = rows[i];
334
+ return (row && row[pk] !== undefined && row[pk] !== null) ? row[pk] : seq;
335
+ });
165
336
  }
166
337
  }
167
- this._runHooks('onStart', []);
168
- this._emit('start', {});
338
+ return r;
169
339
  }
170
340
 
171
- async flush() {
172
- if (!this._runHooks('beforeFlush', [])) return;
173
- await this._writeSnapshot();
341
+ async _flush() {
342
+ if (!this._runHooks('beforeFlush', [])) return null;
343
+ const flushed = {};
344
+ for (const [table, rows] of Object.entries(this._buffer)) {
345
+ if (rows.length === 0) continue;
346
+ const r = await this._insertBatch(table, rows);
347
+ flushed[table] = r;
348
+ }
349
+ this._buffer = {};
350
+ this._bufferSize = 0;
174
351
  this._runHooks('afterFlush', []);
352
+ return flushed;
175
353
  }
176
354
 
177
- async stop() {
178
- clearTimeout(this._persistTimer);
179
- await this._writeSnapshot();
180
- if (this._idb) {
181
- this._idb.close();
182
- this._idb = null;
355
+ async flush() {
356
+ await this._flush();
357
+ }
358
+
359
+ async insert(table, data) {
360
+ const arr = Array.isArray(data) ? data : [data];
361
+ if (!this._runHooks('beforeInsert', [table, arr])) return [];
362
+ if (arr.length > 1) {
363
+ await this._flush();
364
+ let result;
365
+ for (let i = 0; i < arr.length; i += this._flushThreshold) {
366
+ const chunk = arr.slice(i, i + this._flushThreshold);
367
+ const r = await this._insertBatch(table, chunk);
368
+ if (r && r.error) throw new Error(r.error);
369
+ if (!result) result = r;
370
+ }
371
+ this._emit('insert', { table, count: arr.length, ids: result });
372
+ this._runHooks('afterInsert', [table, arr, result]);
373
+ return result;
183
374
  }
184
- this._idbPromise = null;
185
- this._runHooks('onStop', []);
186
- this._emit('stop', {});
375
+ if (!this._buffer[table]) this._buffer[table] = [];
376
+ this._buffer[table].push(arr[0]);
377
+ this._bufferSize++;
378
+ const flushed = await this._flush();
379
+ return flushed && flushed[table] ? flushed[table] : null;
187
380
  }
188
381
 
189
- /* ---------- CRUD ---------- */
382
+ async insertMany(table, data) {
383
+ return this.insert(table, data);
384
+ }
190
385
 
191
386
  async createTable(name, schema) {
387
+ await this._flush();
192
388
  if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
193
- const r = safeJsonParse(jsql_create_table(name, JSON.stringify(schema)));
389
+ const r = safeJsonParse(jsql_create_table(name, JSON.stringify(mapNativeSchema(schema))));
194
390
  if (r && r.ok === false) throw new Error(r.error || 'create table failed');
195
391
  this._tableNames.add(name);
196
- this._schemas[name] = schema;
392
+ this._schemas[name] = typeof schema === 'string' ? mapNativeSchema(schema) : schema;
197
393
  this._emit('createTable', { name, schema });
198
394
  this._runHooks('afterCreateTable', [name, schema]);
199
- this._schedulePersist();
200
395
  return r;
201
396
  }
202
397
 
203
398
  async dropTable(name) {
399
+ await this._flush();
204
400
  if (!this._runHooks('beforeDropTable', [name])) return null;
205
401
  const r = safeJsonParse(jsql_drop_table(name));
206
402
  if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
@@ -208,197 +404,166 @@ export class JSQL {
208
404
  delete this._schemas[name];
209
405
  this._emit('dropTable', { name });
210
406
  this._runHooks('afterDropTable', [name]);
211
- this._schedulePersist();
212
407
  return r;
213
408
  }
214
409
 
215
- async insert(table, data) {
216
- const arr = Array.isArray(data) ? data : [data];
217
- if (!this._runHooks('beforeInsert', [table, arr])) return [];
218
- let result = [];
219
- for (let i = 0; i < arr.length; i += PAGE) {
220
- const r = safeJsonParse(jsql_insert_json(table, JSON.stringify(arr.slice(i, i + PAGE))));
221
- if (r && r.error) throw new Error(r.error);
222
- result = result.concat(r || []);
223
- }
224
- this._emit('insert', { table, count: arr.length, ids: result });
225
- this._runHooks('afterInsert', [table, arr, result]);
226
- this._schedulePersist();
227
- return result;
228
- }
229
-
230
- async insertMany(table, data) {
231
- return this.insert(table, data);
232
- }
233
-
234
410
  async findById(table, id) {
235
- const r = safeJsonParse(jsql_find_by_id(table, BigInt(id)));
236
- if (r && r.error) throw new Error(r.error);
237
- return r;
238
- }
239
-
240
- async findByIds(table, ids) {
241
- const r = safeJsonParse(jsql_find_by_ids(table, JSON.stringify(ids)));
411
+ await this._flush();
412
+ if (!this._runHooks('beforeFind', [table, { id }])) return null;
413
+ const r = this._findRawById(table, id);
242
414
  if (r && r.error) throw new Error(r.error);
415
+ this._runHooks('afterFind', [table, { id }, r]);
416
+ const schema = this._schemas[table];
417
+ if (schema && r && typeof r === 'object') return restoreRow(r, schema);
243
418
  return r;
244
419
  }
245
420
 
246
421
  async find(table, filter, opts = {}) {
422
+ await this._flush();
247
423
  if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
248
- const { limit = 100, offset = 0 } = opts;
249
424
  const filterStr = filter ? JSON.stringify(filter) : '';
425
+ const { limit = 100, offset = 0 } = opts;
250
426
  const r = safeJsonParse(jsql_find(table, filterStr, limit, offset));
251
427
  if (r && r.error) throw new Error(r.error);
252
428
  this._runHooks('afterFind', [table, { filter, opts }, r]);
429
+ const schema = this._schemas[table];
430
+ if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
253
431
  return r;
254
432
  }
255
433
 
256
434
  async count(table) {
435
+ await this._flush();
436
+ this._runHooks('beforeCount', [table]);
257
437
  const r = parseInt(jsql_count(table), 10);
258
- return isNaN(r) ? 0 : r;
259
- }
260
-
261
- async updateById(table, id, data) {
262
- if (!this._runHooks('beforeUpdate', [table, id, data])) return;
263
- const r = safeJsonParse(jsql_update_by_id(table, BigInt(id), JSON.stringify(data)));
264
- if (r && r.ok === false) throw new Error(r.error || 'update failed');
265
- this._emit('update', { table, id, data });
266
- this._runHooks('afterUpdate', [table, id, data, r]);
267
- this._schedulePersist();
268
- return r;
438
+ const n = isNaN(r) ? 0 : r;
439
+ this._runHooks('afterCount', [table, n]);
440
+ return n;
269
441
  }
270
442
 
271
443
  async updateByIds(table, entries) {
272
- const pairs = entries.map(([id, data]) => [Number(id), data]);
444
+ const pairs = [];
445
+ for (const [id, data] of entries) {
446
+ const seq = this._seqIdByPk(table, id);
447
+ if (seq !== null) pairs.push([seq, data]);
448
+ }
273
449
  if (!this._runHooks('beforeUpdate', [table, pairs])) return;
450
+ await this._flush();
274
451
  const r = safeJsonParse(jsql_update_by_ids(table, JSON.stringify(pairs)));
275
452
  if (r && r.error) throw new Error(r.error);
276
453
  this._emit('update', { table, entries: pairs, result: r });
277
454
  this._runHooks('afterUpdate', [table, pairs, r]);
278
- this._schedulePersist();
279
- return r;
280
- }
281
-
282
- async removeById(table, id) {
283
- if (!this._runHooks('beforeDelete', [table, id])) return;
284
- const r = safeJsonParse(jsql_remove_by_id(table, BigInt(id)));
285
- if (r && r.ok === false) throw new Error(r.error || 'remove failed');
286
- this._emit('delete', { table, id });
287
- this._runHooks('afterDelete', [table, id, r]);
288
- this._schedulePersist();
289
455
  return r;
290
456
  }
291
457
 
292
458
  async removeByIds(table, ids) {
293
459
  if (!this._runHooks('beforeDelete', [table, ids])) return;
294
- const r = safeJsonParse(jsql_remove_by_ids(table, JSON.stringify(ids)));
460
+ await this._flush();
461
+ const seqs = ids.map(id => this._seqIdByPk(table, id)).filter(s => s !== null);
462
+ const r = safeJsonParse(jsql_remove_by_ids(table, JSON.stringify(seqs)));
295
463
  if (r && r.error) throw new Error(r.error);
296
- this._emit('delete', { table, ids, result: r });
464
+ this._emit('delete', { table, ids: seqs, result: r });
297
465
  this._runHooks('afterDelete', [table, ids, r]);
298
- this._schedulePersist();
299
466
  return r;
300
467
  }
301
468
 
302
- async hasTable(name) {
303
- return this._tableNames.has(name);
469
+ async findByIds(table, ids) {
470
+ await this._flush();
471
+ if (!this._runHooks('beforeFind', [table, { ids }])) return null;
472
+ let r;
473
+ if (this._pkOf(table)) {
474
+ r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
475
+ } else {
476
+ r = safeJsonParse(jsql_find_by_ids(table, JSON.stringify(ids)));
477
+ }
478
+ if (r && r.error) throw new Error(r.error);
479
+ this._runHooks('afterFind', [table, { ids }, r]);
480
+ const schema = this._schemas[table];
481
+ if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
482
+ return r;
304
483
  }
305
484
 
306
- async getTables() {
307
- return Array.from(this._tableNames);
485
+ findByIdsRaw(table, ids) {
486
+ return this.findByIds(table, ids);
308
487
  }
309
488
 
310
- tables() {
311
- return Array.from(this._tableNames);
489
+ async updateById(table, id, data) {
490
+ if (!this._runHooks('beforeUpdate', [table, id, data])) return;
491
+ await this._flush();
492
+ const seq = this._seqIdByPk(table, id);
493
+ if (seq === null) return null;
494
+ const r = safeJsonParse(jsql_update_by_id(table, BigInt(seq), JSON.stringify(data)));
495
+ if (r && r.ok === false) throw new Error(r.error || 'update failed');
496
+ this._emit('update', { table, id, data });
497
+ this._runHooks('afterUpdate', [table, id, data, r]);
498
+ return r;
312
499
  }
313
500
 
314
- async getTableSchema(name) {
315
- return this._schemas[name] || null;
501
+ async removeById(table, id) {
502
+ if (!this._runHooks('beforeDelete', [table, id])) return;
503
+ await this._flush();
504
+ const seq = this._seqIdByPk(table, id);
505
+ if (seq === null) return null;
506
+ const r = safeJsonParse(jsql_remove_by_id(table, BigInt(seq)));
507
+ if (r && r.ok === false) throw new Error(r.error || 'remove failed');
508
+ this._emit('delete', { table, id });
509
+ this._runHooks('afterDelete', [table, id, r]);
510
+ return r;
316
511
  }
317
512
 
318
- /* ---------- transactions (snapshot rollback) ---------- */
319
-
320
513
  async beginTx() {
321
- this._txSnapshot = await this._snapshotAll();
322
- this._txId = (this._txId || 0) + 1;
323
- return this._txId;
514
+ await this._flush();
515
+ const r = safeJsonParse(jsql_begin_tx());
516
+ if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
517
+ this._txId = r.txId;
518
+ return r.txId;
324
519
  }
325
520
 
326
- async commitTx() {
327
- this._txId = undefined;
328
- this._txSnapshot = null;
521
+ async commitTx(txId) {
522
+ const r = safeJsonParse(jsql_commit_tx(String(txId)));
523
+ if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
524
+ return true;
329
525
  }
330
526
 
331
- async rollbackTx() {
332
- const snap = this._txSnapshot;
333
- this._txId = undefined;
334
- this._txSnapshot = null;
335
- if (snap) await this._restoreSnapshot(snap);
336
- this._schedulePersist();
337
- }
338
-
339
- /* same-name aliases used by some SQL paths */
340
- async begin() { return this.beginTx(); }
341
- async commit() { return this.commitTx(); }
342
- async rollback() { return this.rollbackTx(); }
343
-
344
- async _snapshotAll() {
345
- const tables = {};
346
- for (const name of this._tableNames) {
347
- const rows = [];
348
- let offset = 0;
349
- for (;;) {
350
- const page = safeJsonParse(jsql_find(name, '', PAGE, offset)) || [];
351
- rows.push(...page);
352
- if (page.length < PAGE) break;
353
- offset += page.length;
354
- }
355
- tables[name] = { schema: this._schemas[name], rows: rows.map(r => r.fields) };
356
- }
357
- return tables;
527
+ async rollbackTx(txId) {
528
+ const r = safeJsonParse(jsql_rollback_tx(String(txId)));
529
+ if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
530
+ return true;
358
531
  }
359
532
 
360
- async _restoreSnapshot(tables) {
361
- jsql_reset();
362
- this._tableNames.clear();
363
- for (const [name, t] of Object.entries(tables)) {
364
- jsql_create_table(name, JSON.stringify(t.schema));
365
- this._tableNames.add(name);
366
- this._schemas[name] = t.schema;
367
- if (t.rows && t.rows.length) {
368
- for (let i = 0; i < t.rows.length; i += PAGE) {
369
- jsql_insert_json(name, JSON.stringify(t.rows.slice(i, i + PAGE)));
370
- }
371
- }
372
- }
533
+ async beginTransaction() {
534
+ return this.beginTx();
373
535
  }
374
536
 
375
- /* ---------- multi-database (single-DB simulation) ---------- */
376
-
377
- async listDatabases() {
378
- return Array.from(this._dbList);
537
+ async commit() {
538
+ if (this._txId === undefined) return true;
539
+ const r = await this.commitTx(this._txId);
540
+ this._txId = undefined;
541
+ return r;
379
542
  }
380
543
 
381
- async createDatabase(name) {
382
- this._dbList.add(name);
544
+ async rollback() {
545
+ if (this._txId === undefined) return true;
546
+ const r = await this.rollbackTx(this._txId);
547
+ this._txId = undefined;
548
+ return r;
383
549
  }
384
550
 
385
- async dropDatabase(name) {
386
- this._dbList.delete(name);
551
+ async hasTable(name) {
552
+ return this._tableNames.has(name);
387
553
  }
388
554
 
389
- async useDatabase(name) {
390
- if (!this._dbList.has(name)) {
391
- throw new Error(`Unknown database '${name}'`);
392
- }
555
+ async getTables() {
556
+ return Array.from(this._tableNames);
393
557
  }
394
558
 
395
- /* ---------- SQL ---------- */
559
+ async getTableSchema(name) {
560
+ return this._schemas[name] || null;
561
+ }
396
562
 
397
- async executeSQL(sqlText, params) {
398
- const mod = await import('../lib/sql.js');
399
- const sql = mod.default && mod.default.executeSQL ? mod.default : mod;
400
- return sql.executeSQL(this, sqlText, params);
563
+ async stop() {
564
+ await this._flush();
565
+ this._runHooks('onStop', []);
566
+ this._emit('stop', {});
401
567
  }
402
568
  }
403
569
 
404
- export default { JSQL };