jsql-neo 4.2.0 → 4.3.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 +48 -94
- package/lib/mysql_server.js +284 -5
- package/lib/sql.js +425 -53
- package/lib/table.js +4 -2
- package/package.json +10 -1
- package/wasm/browser.mjs +404 -0
- package/wasm/browser_bg.mjs +462 -0
package/lib/table.js
CHANGED
|
@@ -255,7 +255,8 @@ class Table {
|
|
|
255
255
|
}
|
|
256
256
|
for (const field of this._cachedSchemaFields) {
|
|
257
257
|
const def = schema[field];
|
|
258
|
-
if (def.required && (data[field] === undefined || data[field] === null)
|
|
258
|
+
if (def.required && (data[field] === undefined || data[field] === null)
|
|
259
|
+
&& !(def.autoIncrement && data[field] === undefined)) throw createError('ER_BAD_NULL_ERROR', field);
|
|
259
260
|
if (def.unique && data[field] !== undefined) {
|
|
260
261
|
if (uniqueTracker[field].has(data[field])) throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
261
262
|
uniqueTracker[field].add(data[field]);
|
|
@@ -986,7 +987,8 @@ class Table {
|
|
|
986
987
|
if (field === '_softDelete') continue;
|
|
987
988
|
|
|
988
989
|
// NOT NULL
|
|
989
|
-
if (def.required && (data[field] === undefined || data[field] === null)
|
|
990
|
+
if (def.required && (data[field] === undefined || data[field] === null)
|
|
991
|
+
&& !(def.autoIncrement && data[field] === undefined)) {
|
|
990
992
|
throw createError('ER_BAD_NULL_ERROR', field);
|
|
991
993
|
}
|
|
992
994
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.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
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"browser": "./wasm/browser.mjs",
|
|
9
|
+
"default": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"./wasm/*": "./wasm/*",
|
|
12
|
+
"./lib/*": "./lib/*",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
6
15
|
"bin": {
|
|
7
16
|
"jsql": "./bin/jsql"
|
|
8
17
|
},
|
package/wasm/browser.mjs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import {
|
|
2
|
+
init,
|
|
3
|
+
jsql_reset,
|
|
4
|
+
jsql_create_table,
|
|
5
|
+
jsql_drop_table,
|
|
6
|
+
jsql_insert_json,
|
|
7
|
+
jsql_find,
|
|
8
|
+
jsql_find_by_id,
|
|
9
|
+
jsql_find_by_ids,
|
|
10
|
+
jsql_update_by_id,
|
|
11
|
+
jsql_update_by_ids,
|
|
12
|
+
jsql_remove_by_id,
|
|
13
|
+
jsql_remove_by_ids,
|
|
14
|
+
jsql_count,
|
|
15
|
+
} from './browser_bg.mjs';
|
|
16
|
+
|
|
17
|
+
function safeJsonParse(str) {
|
|
18
|
+
try { return JSON.parse(str); } catch { return str; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PAGE = 500;
|
|
22
|
+
|
|
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
|
+
export class JSQL {
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
this._dbName = opts.dbName || 'jsql-neo';
|
|
32
|
+
this._persist = opts.persistence !== false;
|
|
33
|
+
this._wasmBytes = opts.wasmBytes || null;
|
|
34
|
+
this._tableNames = new Set();
|
|
35
|
+
this._schemas = {};
|
|
36
|
+
this._dbList = new Set(['default']);
|
|
37
|
+
this._txId = undefined;
|
|
38
|
+
this._txSnapshot = null;
|
|
39
|
+
this._hooks = {
|
|
40
|
+
beforeInsert: [], afterInsert: [],
|
|
41
|
+
beforeUpdate: [], afterUpdate: [],
|
|
42
|
+
beforeDelete: [], afterDelete: [],
|
|
43
|
+
beforeFind: [], afterFind: [],
|
|
44
|
+
beforeCreateTable: [], afterCreateTable: [],
|
|
45
|
+
beforeDropTable: [], afterDropTable: [],
|
|
46
|
+
beforeFlush: [], afterFlush: [],
|
|
47
|
+
onStart: [], onStop: [],
|
|
48
|
+
};
|
|
49
|
+
this._eventListeners = [];
|
|
50
|
+
this._idb = null;
|
|
51
|
+
this._idbPromise = null;
|
|
52
|
+
this._persistTimer = null;
|
|
53
|
+
this._snapPromise = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ---------- hooks / events (same API as Node engines) ---------- */
|
|
57
|
+
|
|
58
|
+
on(event, fn) {
|
|
59
|
+
if (this._hooks[event]) this._hooks[event].push(fn);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onEvent(fn) {
|
|
64
|
+
this._eventListeners.push(fn);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_emit(eventName, data) {
|
|
69
|
+
for (const fn of this._eventListeners) {
|
|
70
|
+
try { fn(eventName, data); } catch { /* ignore */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_runHooks(hookName, args) {
|
|
75
|
+
const hooks = this._hooks[hookName];
|
|
76
|
+
if (!hooks || hooks.length === 0) return true;
|
|
77
|
+
for (const fn of hooks) {
|
|
78
|
+
const r = fn(...args);
|
|
79
|
+
if (r === false) return false;
|
|
80
|
+
if (r !== undefined && args.length > 0) args[0] = r;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
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;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_schedulePersist() {
|
|
139
|
+
if (!this._persist) return;
|
|
140
|
+
clearTimeout(this._persistTimer);
|
|
141
|
+
this._persistTimer = setTimeout(() => { this._writeSnapshot(); }, 300);
|
|
142
|
+
}
|
|
143
|
+
|
|
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
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this._runHooks('onStart', []);
|
|
168
|
+
this._emit('start', {});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async flush() {
|
|
172
|
+
if (!this._runHooks('beforeFlush', [])) return;
|
|
173
|
+
await this._writeSnapshot();
|
|
174
|
+
this._runHooks('afterFlush', []);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async stop() {
|
|
178
|
+
clearTimeout(this._persistTimer);
|
|
179
|
+
await this._writeSnapshot();
|
|
180
|
+
if (this._idb) {
|
|
181
|
+
this._idb.close();
|
|
182
|
+
this._idb = null;
|
|
183
|
+
}
|
|
184
|
+
this._idbPromise = null;
|
|
185
|
+
this._runHooks('onStop', []);
|
|
186
|
+
this._emit('stop', {});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ---------- CRUD ---------- */
|
|
190
|
+
|
|
191
|
+
async createTable(name, schema) {
|
|
192
|
+
if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
|
|
193
|
+
const r = safeJsonParse(jsql_create_table(name, JSON.stringify(schema)));
|
|
194
|
+
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|
|
195
|
+
this._tableNames.add(name);
|
|
196
|
+
this._schemas[name] = schema;
|
|
197
|
+
this._emit('createTable', { name, schema });
|
|
198
|
+
this._runHooks('afterCreateTable', [name, schema]);
|
|
199
|
+
this._schedulePersist();
|
|
200
|
+
return r;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async dropTable(name) {
|
|
204
|
+
if (!this._runHooks('beforeDropTable', [name])) return null;
|
|
205
|
+
const r = safeJsonParse(jsql_drop_table(name));
|
|
206
|
+
if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
|
|
207
|
+
this._tableNames.delete(name);
|
|
208
|
+
delete this._schemas[name];
|
|
209
|
+
this._emit('dropTable', { name });
|
|
210
|
+
this._runHooks('afterDropTable', [name]);
|
|
211
|
+
this._schedulePersist();
|
|
212
|
+
return r;
|
|
213
|
+
}
|
|
214
|
+
|
|
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
|
+
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)));
|
|
242
|
+
if (r && r.error) throw new Error(r.error);
|
|
243
|
+
return r;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async find(table, filter, opts = {}) {
|
|
247
|
+
if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
|
|
248
|
+
const { limit = 100, offset = 0 } = opts;
|
|
249
|
+
const filterStr = filter ? JSON.stringify(filter) : '';
|
|
250
|
+
const r = safeJsonParse(jsql_find(table, filterStr, limit, offset));
|
|
251
|
+
if (r && r.error) throw new Error(r.error);
|
|
252
|
+
this._runHooks('afterFind', [table, { filter, opts }, r]);
|
|
253
|
+
return r;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async count(table) {
|
|
257
|
+
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;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async updateByIds(table, entries) {
|
|
272
|
+
const pairs = entries.map(([id, data]) => [Number(id), data]);
|
|
273
|
+
if (!this._runHooks('beforeUpdate', [table, pairs])) return;
|
|
274
|
+
const r = safeJsonParse(jsql_update_by_ids(table, JSON.stringify(pairs)));
|
|
275
|
+
if (r && r.error) throw new Error(r.error);
|
|
276
|
+
this._emit('update', { table, entries: pairs, result: r });
|
|
277
|
+
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
|
+
return r;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async removeByIds(table, ids) {
|
|
293
|
+
if (!this._runHooks('beforeDelete', [table, ids])) return;
|
|
294
|
+
const r = safeJsonParse(jsql_remove_by_ids(table, JSON.stringify(ids)));
|
|
295
|
+
if (r && r.error) throw new Error(r.error);
|
|
296
|
+
this._emit('delete', { table, ids, result: r });
|
|
297
|
+
this._runHooks('afterDelete', [table, ids, r]);
|
|
298
|
+
this._schedulePersist();
|
|
299
|
+
return r;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async hasTable(name) {
|
|
303
|
+
return this._tableNames.has(name);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async getTables() {
|
|
307
|
+
return Array.from(this._tableNames);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
tables() {
|
|
311
|
+
return Array.from(this._tableNames);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async getTableSchema(name) {
|
|
315
|
+
return this._schemas[name] || null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ---------- transactions (snapshot rollback) ---------- */
|
|
319
|
+
|
|
320
|
+
async beginTx() {
|
|
321
|
+
this._txSnapshot = await this._snapshotAll();
|
|
322
|
+
this._txId = (this._txId || 0) + 1;
|
|
323
|
+
return this._txId;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async commitTx() {
|
|
327
|
+
this._txId = undefined;
|
|
328
|
+
this._txSnapshot = null;
|
|
329
|
+
}
|
|
330
|
+
|
|
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;
|
|
358
|
+
}
|
|
359
|
+
|
|
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
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* ---------- multi-database (single-DB simulation) ---------- */
|
|
376
|
+
|
|
377
|
+
async listDatabases() {
|
|
378
|
+
return Array.from(this._dbList);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async createDatabase(name) {
|
|
382
|
+
this._dbList.add(name);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async dropDatabase(name) {
|
|
386
|
+
this._dbList.delete(name);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async useDatabase(name) {
|
|
390
|
+
if (!this._dbList.has(name)) {
|
|
391
|
+
throw new Error(`Unknown database '${name}'`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ---------- SQL ---------- */
|
|
396
|
+
|
|
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);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default { JSQL };
|