jsql-neo 4.5.0 → 4.5.2
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/database.js +42 -0
- package/lib/mysql_server.js +15 -4
- package/lib/native_client.js +117 -19
- package/lib/sql.js +183 -32
- package/lib/wasm_client.js +85 -1
- package/package.json +1 -1
package/lib/database.js
CHANGED
|
@@ -1142,9 +1142,46 @@ class Database {
|
|
|
1142
1142
|
async start() {
|
|
1143
1143
|
this._runHooks('onStart', []);
|
|
1144
1144
|
this._emit('start', {});
|
|
1145
|
+
if (this._dirMode && !this._sigInstalled) {
|
|
1146
|
+
this._sigInstalled = true;
|
|
1147
|
+
Database._installSignalHandler(this);
|
|
1148
|
+
}
|
|
1145
1149
|
return this;
|
|
1146
1150
|
}
|
|
1147
1151
|
|
|
1152
|
+
/**
|
|
1153
|
+
* 进程退出时兜底刷盘:拦截 SIGINT/SIGTERM,同步落盘脏表后再退出。
|
|
1154
|
+
* 使用静态共享 handler,避免多个数据库实例重复注册/互相覆盖。
|
|
1155
|
+
* @private
|
|
1156
|
+
*/
|
|
1157
|
+
static _installSignalHandler(db) {
|
|
1158
|
+
if (!Database._signalHandlerInstalled) {
|
|
1159
|
+
Database._signalHandlerInstalled = true;
|
|
1160
|
+
Database._signalDBs = new Set();
|
|
1161
|
+
const handler = (sig) => {
|
|
1162
|
+
for (const d of Database._signalDBs) {
|
|
1163
|
+
try {
|
|
1164
|
+
if (d._dirMode) {
|
|
1165
|
+
if (d._flushTimer) { clearTimeout(d._flushTimer); d._flushTimer = null; }
|
|
1166
|
+
d._flushDirty();
|
|
1167
|
+
d._saveMeta();
|
|
1168
|
+
}
|
|
1169
|
+
} catch (e) { /* 兜底失败不阻塞退出 */ }
|
|
1170
|
+
}
|
|
1171
|
+
process.exit(128 + (sig === 'SIGINT' ? 2 : 15));
|
|
1172
|
+
};
|
|
1173
|
+
process.on('SIGINT', handler);
|
|
1174
|
+
process.on('SIGTERM', handler);
|
|
1175
|
+
Database._signalHandler = handler;
|
|
1176
|
+
}
|
|
1177
|
+
Database._signalDBs.add(db);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/** 从进程退出兜底集合移除(stop 时调用) */
|
|
1181
|
+
static _uninstallSignalHandler(db) {
|
|
1182
|
+
if (Database._signalDBs) Database._signalDBs.delete(db);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1148
1185
|
insert(tableName, data) {
|
|
1149
1186
|
const table = this._ensureTable(tableName);
|
|
1150
1187
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
@@ -1333,11 +1370,16 @@ class Database {
|
|
|
1333
1370
|
this._runHooks('onStop', []);
|
|
1334
1371
|
this._emit('stop', {});
|
|
1335
1372
|
if (this._dirMode) {
|
|
1373
|
+
if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
|
|
1336
1374
|
try {
|
|
1337
1375
|
this._flushDirty();
|
|
1338
1376
|
this._saveMeta();
|
|
1339
1377
|
} catch (e) {}
|
|
1340
1378
|
}
|
|
1379
|
+
if (this._sigInstalled) {
|
|
1380
|
+
this._sigInstalled = false;
|
|
1381
|
+
Database._uninstallSignalHandler(this);
|
|
1382
|
+
}
|
|
1341
1383
|
return this;
|
|
1342
1384
|
}
|
|
1343
1385
|
|
package/lib/mysql_server.js
CHANGED
|
@@ -104,8 +104,19 @@ function handshakePacket(connectionId, seed) {
|
|
|
104
104
|
return b.build();
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
/**
|
|
108
|
+
* 生成握手 seed:使用可打印 ASCII(33-126),避开 0x00。
|
|
109
|
+
* 老客户端按 C 字符串读取 seed,若含 \0 会提前截断导致握手失败/断连。
|
|
110
|
+
*/
|
|
111
|
+
function genSeed(len = 20) {
|
|
112
|
+
const out = Buffer.alloc(len);
|
|
113
|
+
for (let i = 0; i < len; i++) {
|
|
114
|
+
out[i] = 33 + Math.floor(Math.random() * 94);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) { const b = new PacketBuilder();
|
|
109
120
|
b.byte(0x00);
|
|
110
121
|
b.raw(encodeLenenc(affectedRows));
|
|
111
122
|
b.raw(encodeLenenc(insertId));
|
|
@@ -364,7 +375,7 @@ class MysqlConnection {
|
|
|
364
375
|
this.socket = socket;
|
|
365
376
|
this.server = server;
|
|
366
377
|
this.connectionId = ++server._connectionCounter;
|
|
367
|
-
this.seed =
|
|
378
|
+
this.seed = genSeed(20);
|
|
368
379
|
this.buffer = Buffer.alloc(0);
|
|
369
380
|
this.sequence = 0;
|
|
370
381
|
this.authenticated = false;
|
|
@@ -898,7 +909,7 @@ class MysqlServer {
|
|
|
898
909
|
this._engine = null;
|
|
899
910
|
this._ownEngine = false;
|
|
900
911
|
this._databases = new Map();
|
|
901
|
-
this._dbDir = options.dataDir ? path.resolve(options.dataDir) : null;
|
|
912
|
+
this._dbDir = options.dataDir && options.dataDir !== ':memory:' ? path.resolve(options.dataDir) : null;
|
|
902
913
|
this._defaultDbName = options.defaultDatabase || 'default';
|
|
903
914
|
this._connectionCounter = 0;
|
|
904
915
|
this._sockets = new Set();
|
package/lib/native_client.js
CHANGED
|
@@ -7,6 +7,69 @@ const STR_TAG = 3;
|
|
|
7
7
|
const BOOL_TAG = 4;
|
|
8
8
|
const INT32_TAG = 5;
|
|
9
9
|
|
|
10
|
+
function safeParse(str, fallback) {
|
|
11
|
+
if (typeof str !== 'string') return fallback;
|
|
12
|
+
try { return JSON.parse(str); } catch (e) { return fallback; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const NATIVE_TYPE_MAP = {
|
|
16
|
+
text: 'string',
|
|
17
|
+
varchar: 'string',
|
|
18
|
+
double: 'float',
|
|
19
|
+
number: 'float',
|
|
20
|
+
numeric: 'float',
|
|
21
|
+
decimal: 'float',
|
|
22
|
+
date: 'string',
|
|
23
|
+
timestamp: 'string',
|
|
24
|
+
datetime: 'string',
|
|
25
|
+
json: 'string',
|
|
26
|
+
array: 'string',
|
|
27
|
+
object: 'string',
|
|
28
|
+
bool: 'boolean',
|
|
29
|
+
bigint: 'string',
|
|
30
|
+
int: 'integer',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function mapNativeSchema(schema) {
|
|
34
|
+
const mapped = {};
|
|
35
|
+
for (const [field, def] of Object.entries(schema || {})) {
|
|
36
|
+
if (typeof def === 'string') {
|
|
37
|
+
mapped[field] = { type: NATIVE_TYPE_MAP[def.toLowerCase()] || def.toLowerCase() };
|
|
38
|
+
} else if (def && typeof def === 'object') {
|
|
39
|
+
const t = (def.type || 'string').toLowerCase();
|
|
40
|
+
mapped[field] = { ...def, type: NATIVE_TYPE_MAP[t] || t };
|
|
41
|
+
} else {
|
|
42
|
+
mapped[field] = { type: 'string' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return mapped;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function restoreRow(row, schema) {
|
|
49
|
+
if (!schema || typeof row !== 'object' || row === null) return row;
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [field, def] of Object.entries(row)) {
|
|
52
|
+
if (field === 'fields' && typeof row[field] === 'object' && row[field] !== null) {
|
|
53
|
+
out[field] = restoreRow(row[field], schema);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const colDef = schema[field];
|
|
57
|
+
let t = null;
|
|
58
|
+
if (typeof colDef === 'string') t = colDef.toLowerCase();
|
|
59
|
+
else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
|
|
60
|
+
if (['json', 'array', 'object'].includes(t) && typeof row[field] === 'string') {
|
|
61
|
+
try { out[field] = JSON.parse(row[field]); } catch (e) { out[field] = row[field]; }
|
|
62
|
+
} else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof row[field] === 'string' && /^-?\d+$/.test(row[field])) {
|
|
63
|
+
out[field] = Number(row[field]);
|
|
64
|
+
} else if ((t === 'float' || t === 'double' || t === 'number') && typeof row[field] === 'string' && !Number.isNaN(Number(row[field]))) {
|
|
65
|
+
out[field] = Number(row[field]);
|
|
66
|
+
} else {
|
|
67
|
+
out[field] = row[field];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
10
73
|
function encodeBatch(rows) {
|
|
11
74
|
if (rows.length === 0) return new Uint8Array(0);
|
|
12
75
|
const fieldNames = Object.keys(rows[0]);
|
|
@@ -257,27 +320,45 @@ class JSQL {
|
|
|
257
320
|
_runHooks(hookName, args) {
|
|
258
321
|
const hooks = this._hooks[hookName];
|
|
259
322
|
if (!hooks || hooks.length === 0) return true;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
323
|
+
if (this._inHook) {
|
|
324
|
+
// 防止插件 hook 内重入 native 调用导致 N-API 崩溃(段错误)
|
|
325
|
+
throw new Error('ER_PLUGIN_REENTRY: plugin hook "' + hookName + '" re-entered native call; plugin must not call engine methods inside its own hook');
|
|
326
|
+
}
|
|
327
|
+
this._inHook = true;
|
|
328
|
+
try {
|
|
329
|
+
for (const fn of hooks) {
|
|
330
|
+
const r = fn(...args);
|
|
331
|
+
if (r === false) return false;
|
|
332
|
+
if (r !== undefined && args.length > 0) {
|
|
333
|
+
args[0] = r;
|
|
334
|
+
}
|
|
265
335
|
}
|
|
336
|
+
return true;
|
|
337
|
+
} finally {
|
|
338
|
+
this._inHook = false;
|
|
266
339
|
}
|
|
267
|
-
return true;
|
|
268
340
|
}
|
|
269
341
|
|
|
270
342
|
async start() {
|
|
271
343
|
if (this._mode !== 'memory') {
|
|
272
344
|
if (!this._path) throw new Error('hybrid/disk mode requires a directory path');
|
|
273
|
-
|
|
345
|
+
let r;
|
|
346
|
+
try {
|
|
347
|
+
r = safeParse(native.jsqlOpen(this._path, this._mode));
|
|
348
|
+
} catch (e) {
|
|
349
|
+
throw new Error('failed to open native storage: ' + e.message);
|
|
350
|
+
}
|
|
274
351
|
if (r && r.ok === false) throw new Error(r.error || 'open storage failed');
|
|
275
352
|
if (r && Array.isArray(r.tables)) {
|
|
276
353
|
this._tableNames = new Set(r.tables);
|
|
277
354
|
if (r.schemas) this._schemas = r.schemas;
|
|
278
355
|
}
|
|
279
356
|
} else {
|
|
280
|
-
|
|
357
|
+
try {
|
|
358
|
+
JSON.parse(native.jsqlOpen('', 'memory'));
|
|
359
|
+
} catch (e) {
|
|
360
|
+
throw new Error('failed to open native memory storage: ' + e.message);
|
|
361
|
+
}
|
|
281
362
|
}
|
|
282
363
|
if (this._mode !== 'memory') {
|
|
283
364
|
if (this._flushInterval > 0) {
|
|
@@ -310,8 +391,19 @@ class JSQL {
|
|
|
310
391
|
}
|
|
311
392
|
|
|
312
393
|
async _insertBatch(table, rows) {
|
|
394
|
+
const schema = this._schemas[table];
|
|
395
|
+
if (schema) {
|
|
396
|
+
rows = rows.map(row => {
|
|
397
|
+
const out = {};
|
|
398
|
+
for (const [k, v] of Object.entries(row)) {
|
|
399
|
+
if (v instanceof Date) out[k] = v.toISOString();
|
|
400
|
+
else out[k] = v;
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
313
405
|
const bin = encodeBatch(rows);
|
|
314
|
-
const r =
|
|
406
|
+
const r = safeParse(native.jsqlInsertBuf(table, bin));
|
|
315
407
|
if (r && r.error) throw new Error(r.error);
|
|
316
408
|
return r;
|
|
317
409
|
}
|
|
@@ -331,13 +423,13 @@ class JSQL {
|
|
|
331
423
|
var ids = remove[table];
|
|
332
424
|
if (ids.size === 0) continue;
|
|
333
425
|
var idsArr = Array.from(ids);
|
|
334
|
-
var r =
|
|
426
|
+
var r = safeParse(native.jsqlRemoveByIds(table, JSON.stringify(idsArr)));
|
|
335
427
|
this._emit('delete', { table, ids: idsArr, result: r });
|
|
336
428
|
}
|
|
337
429
|
for (var table in update) {
|
|
338
430
|
var entries = update[table];
|
|
339
431
|
if (entries.length === 0) continue;
|
|
340
|
-
var r =
|
|
432
|
+
var r = safeParse(native.jsqlUpdateByIds(table, JSON.stringify(entries)));
|
|
341
433
|
this._emit('update', { table, entries, result: r });
|
|
342
434
|
}
|
|
343
435
|
this._opBuffer = { remove: {}, update: {} };
|
|
@@ -401,7 +493,7 @@ class JSQL {
|
|
|
401
493
|
async createTable(name, schema) {
|
|
402
494
|
await this._flush();
|
|
403
495
|
if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
|
|
404
|
-
const r =
|
|
496
|
+
const r = safeParse(native.jsqlCreateTable(name, JSON.stringify(mapNativeSchema(schema))));
|
|
405
497
|
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|
|
406
498
|
this._tableNames.add(name);
|
|
407
499
|
this._schemas[name] = schema;
|
|
@@ -413,7 +505,7 @@ class JSQL {
|
|
|
413
505
|
async dropTable(name) {
|
|
414
506
|
await this._flush();
|
|
415
507
|
if (!this._runHooks('beforeDropTable', [name])) return null;
|
|
416
|
-
const r =
|
|
508
|
+
const r = safeParse(native.jsqlDropTable(name));
|
|
417
509
|
if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
|
|
418
510
|
this._tableNames.delete(name);
|
|
419
511
|
delete this._schemas[name];
|
|
@@ -425,9 +517,11 @@ class JSQL {
|
|
|
425
517
|
findById(table, id) {
|
|
426
518
|
this._flushOpsNow();
|
|
427
519
|
if (!this._runHooks('beforeFind', [table, { id }])) return null;
|
|
428
|
-
const raw =
|
|
520
|
+
const raw = safeParse(native.jsqlFindById(table, Number(id)));
|
|
429
521
|
if (raw && raw.error) throw new Error(raw.error);
|
|
430
522
|
this._runHooks('afterFind', [table, { id }, raw]);
|
|
523
|
+
const schema = this._schemas[table];
|
|
524
|
+
if (schema && raw && typeof raw === 'object') return restoreRow(raw, schema);
|
|
431
525
|
return raw;
|
|
432
526
|
}
|
|
433
527
|
|
|
@@ -435,9 +529,11 @@ class JSQL {
|
|
|
435
529
|
this._flushOpsNow();
|
|
436
530
|
if (!this._runHooks('beforeFind', [table, { ids }])) return null;
|
|
437
531
|
const resultStr = native.jsqlFindByIds(table, JSON.stringify(ids));
|
|
438
|
-
const r =
|
|
532
|
+
const r = safeParse(resultStr);
|
|
439
533
|
if (r && r.error) throw new Error(r.error);
|
|
440
534
|
this._runHooks('afterFind', [table, { ids }, r]);
|
|
535
|
+
const schema = this._schemas[table];
|
|
536
|
+
if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
|
|
441
537
|
return r;
|
|
442
538
|
}
|
|
443
539
|
|
|
@@ -450,9 +546,11 @@ class JSQL {
|
|
|
450
546
|
if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
|
|
451
547
|
const filterStr = filter ? JSON.stringify(filter) : '';
|
|
452
548
|
const { limit = 100, offset = 0 } = opts;
|
|
453
|
-
const r =
|
|
549
|
+
const r = safeParse(native.jsqlFind(table, filterStr, limit, offset));
|
|
454
550
|
if (r && r.error) throw new Error(r.error);
|
|
455
551
|
this._runHooks('afterFind', [table, { filter, opts }, r]);
|
|
552
|
+
const schema = this._schemas[table];
|
|
553
|
+
if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
|
|
456
554
|
return r;
|
|
457
555
|
}
|
|
458
556
|
|
|
@@ -520,19 +618,19 @@ class JSQL {
|
|
|
520
618
|
|
|
521
619
|
async beginTx() {
|
|
522
620
|
this._flushOpsNow();
|
|
523
|
-
const r =
|
|
621
|
+
const r = safeParse(native.jsqlBeginTx());
|
|
524
622
|
if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
|
|
525
623
|
return r.txId;
|
|
526
624
|
}
|
|
527
625
|
|
|
528
626
|
async commitTx(txId) {
|
|
529
|
-
const r =
|
|
627
|
+
const r = safeParse(native.jsqlCommitTx(String(txId)));
|
|
530
628
|
if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
|
|
531
629
|
return true;
|
|
532
630
|
}
|
|
533
631
|
|
|
534
632
|
async rollbackTx(txId) {
|
|
535
|
-
const r =
|
|
633
|
+
const r = safeParse(native.jsqlRollbackTx(String(txId)));
|
|
536
634
|
if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
|
|
537
635
|
return true;
|
|
538
636
|
}
|
package/lib/sql.js
CHANGED
|
@@ -55,7 +55,8 @@ const KEYWORDS = new Set([
|
|
|
55
55
|
'GROUP', 'HAVING', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
|
|
56
56
|
'BETWEEN', 'USING', 'FULL', 'UNSIGNED', 'ZEROFILL', 'TRUNCATE', 'COLLATE', 'CHARACTER',
|
|
57
57
|
'ALTER', 'ADD', 'COLUMN', 'MODIFY', 'CHANGE', 'INDEX', 'FOREIGN', 'REFERENCES',
|
|
58
|
-
'CONSTRAINT', 'RENAME', 'TO', 'AFTER', 'FIRST', 'ENGINE', 'AUTO_INCREMENT', 'SPATIAL'
|
|
58
|
+
'CONSTRAINT', 'RENAME', 'TO', 'AFTER', 'FIRST', 'ENGINE', 'AUTO_INCREMENT', 'SPATIAL',
|
|
59
|
+
'REGEXP', 'TRUE', 'FALSE', 'RLIKE'
|
|
59
60
|
]);
|
|
60
61
|
|
|
61
62
|
function tokenize(sql) {
|
|
@@ -284,16 +285,28 @@ class Parser {
|
|
|
284
285
|
if (t.value === 'PRIMARY') {
|
|
285
286
|
this.expectKeyword('PRIMARY'); this.expectKeyword('KEY');
|
|
286
287
|
this.expect('op', '(');
|
|
287
|
-
const
|
|
288
|
+
const pkCols = [this.parseTableName()];
|
|
289
|
+
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
290
|
+
this.next();
|
|
291
|
+
pkCols.push(this.parseTableName());
|
|
292
|
+
}
|
|
288
293
|
this.expect('op', ')');
|
|
289
|
-
|
|
294
|
+
for (const pkCol of pkCols) {
|
|
295
|
+
if (schema[pkCol]) schema[pkCol].primaryKey = true;
|
|
296
|
+
}
|
|
290
297
|
hasPk = true;
|
|
291
298
|
} else {
|
|
292
299
|
this.expectKeyword('UNIQUE');
|
|
293
300
|
this.expect('op', '(');
|
|
294
|
-
const
|
|
301
|
+
const uCols = [this.parseTableName()];
|
|
302
|
+
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
303
|
+
this.next();
|
|
304
|
+
uCols.push(this.parseTableName());
|
|
305
|
+
}
|
|
295
306
|
this.expect('op', ')');
|
|
296
|
-
|
|
307
|
+
for (const uCol of uCols) {
|
|
308
|
+
if (schema[uCol]) schema[uCol].unique = true;
|
|
309
|
+
}
|
|
297
310
|
}
|
|
298
311
|
} else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
|
|
299
312
|
this.expectKeyword('CONSTRAINT');
|
|
@@ -573,8 +586,9 @@ class Parser {
|
|
|
573
586
|
if (this.isKeyword('LIMIT')) {
|
|
574
587
|
this.next();
|
|
575
588
|
limit = this.parseValue();
|
|
576
|
-
if (
|
|
577
|
-
|
|
589
|
+
if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
|
|
590
|
+
if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); if (typeof offset === 'number' && offset < 0) throw new Error(`OFFSET must be a non-negative integer, got ${offset}`); }
|
|
591
|
+
else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`); }
|
|
578
592
|
}
|
|
579
593
|
let union = null;
|
|
580
594
|
if (this.isKeyword('UNION')) {
|
|
@@ -672,7 +686,7 @@ class Parser {
|
|
|
672
686
|
while (true) {
|
|
673
687
|
const col = this.parseColumnRef();
|
|
674
688
|
this.expect('op', '=');
|
|
675
|
-
assignments.push([col, this.
|
|
689
|
+
assignments.push([col, this.parseScalar()]);
|
|
676
690
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
677
691
|
break;
|
|
678
692
|
}
|
|
@@ -1012,7 +1026,7 @@ class Parser {
|
|
|
1012
1026
|
if (t.type === 'sysvar') return { type: 'sysvar', name: t.value };
|
|
1013
1027
|
if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
|
|
1014
1028
|
if (t.type === 'keyword' && t.value === 'CASE') return this.parseCase();
|
|
1015
|
-
if (t.type === 'keyword' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1029
|
+
if (t.type === 'keyword' && !['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value) && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1016
1030
|
const name = t.value;
|
|
1017
1031
|
this.next();
|
|
1018
1032
|
const args = [];
|
|
@@ -1061,6 +1075,39 @@ class Parser {
|
|
|
1061
1075
|
}
|
|
1062
1076
|
break;
|
|
1063
1077
|
}
|
|
1078
|
+
// 后缀:expr IN (...)、expr IS [NOT] TRUE/FALSE/NULL(标量上下文,如 SELECT 1 IN (...))
|
|
1079
|
+
for (;;) {
|
|
1080
|
+
const t = this.peek();
|
|
1081
|
+
if (t.type === 'keyword' && t.value === 'IN') {
|
|
1082
|
+
this.next();
|
|
1083
|
+
this.expect('op', '(');
|
|
1084
|
+
if (this.isKeyword('SELECT')) {
|
|
1085
|
+
const sub = this.parseSelect();
|
|
1086
|
+
this.expect('op', ')');
|
|
1087
|
+
node = { type: 'in', operand: node, subquery: sub };
|
|
1088
|
+
} else {
|
|
1089
|
+
const list = [];
|
|
1090
|
+
while (true) {
|
|
1091
|
+
list.push(this.parseValue());
|
|
1092
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
this.expect('op', ')');
|
|
1096
|
+
node = { type: 'in', operand: node, list };
|
|
1097
|
+
}
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (t.type === 'keyword' && t.value === 'IS') {
|
|
1101
|
+
this.next();
|
|
1102
|
+
const not = this.isKeyword('NOT');
|
|
1103
|
+
if (not) this.next();
|
|
1104
|
+
if (this.isKeyword('NULL')) { this.next(); node = { type: 'isNull', operand: node, not: !!not }; continue; }
|
|
1105
|
+
if (this.isKeyword('TRUE')) { this.next(); node = { type: 'isTruth', operand: node, not: !!not, truth: true }; continue; }
|
|
1106
|
+
if (this.isKeyword('FALSE')) { this.next(); node = { type: 'isTruth', operand: node, not: !!not, truth: false }; continue; }
|
|
1107
|
+
throw new Error(`Expected NULL, TRUE or FALSE after IS, got '${this.peek().value}'`);
|
|
1108
|
+
}
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1064
1111
|
return node;
|
|
1065
1112
|
}
|
|
1066
1113
|
|
|
@@ -1108,23 +1155,39 @@ class Parser {
|
|
|
1108
1155
|
parseComparison() {
|
|
1109
1156
|
const left = this.parseOperand();
|
|
1110
1157
|
const t = this.peek();
|
|
1158
|
+
let not = false;
|
|
1159
|
+
if (t.type === 'keyword' && t.value === 'NOT') {
|
|
1160
|
+
this.next();
|
|
1161
|
+
not = true;
|
|
1162
|
+
// NOT 后必须紧跟比较关键字
|
|
1163
|
+
const n = this.peek();
|
|
1164
|
+
if (!['IN', 'BETWEEN', 'LIKE', 'REGEXP'].includes(n.value)) {
|
|
1165
|
+
throw new Error(`Expected IN, BETWEEN, LIKE or REGEXP after NOT, got '${n.value}'`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const t2 = this.peek();
|
|
1111
1169
|
|
|
1112
|
-
if (
|
|
1170
|
+
if (t2.type === 'keyword' && t2.value === 'IS') {
|
|
1113
1171
|
this.next();
|
|
1114
|
-
const
|
|
1115
|
-
if (
|
|
1116
|
-
this.
|
|
1117
|
-
|
|
1172
|
+
const n = this.isKeyword('NOT');
|
|
1173
|
+
if (n) this.next();
|
|
1174
|
+
if (this.isKeyword('NULL')) {
|
|
1175
|
+
this.next();
|
|
1176
|
+
return { type: 'isNull', operand: left, not: not || !!n };
|
|
1177
|
+
}
|
|
1178
|
+
if (this.isKeyword('TRUE')) { this.next(); return { type: 'isTruth', operand: left, not: not || !!n, truth: true }; }
|
|
1179
|
+
if (this.isKeyword('FALSE')) { this.next(); return { type: 'isTruth', operand: left, not: not || !!n, truth: false }; }
|
|
1180
|
+
throw new Error(`Expected NULL, TRUE or FALSE after IS, got '${this.peek().value}'`);
|
|
1118
1181
|
}
|
|
1119
1182
|
|
|
1120
|
-
if (
|
|
1183
|
+
if (t2.type === 'keyword' && t2.value === 'IN') {
|
|
1121
1184
|
this.next();
|
|
1122
1185
|
this.expect('op', '(');
|
|
1123
1186
|
// IN (SELECT ...) 子查询
|
|
1124
1187
|
if (this.isKeyword('SELECT')) {
|
|
1125
1188
|
const sub = this.parseSelect();
|
|
1126
1189
|
this.expect('op', ')');
|
|
1127
|
-
return { type: 'in', operand: left, subquery: sub };
|
|
1190
|
+
return { type: 'in', operand: left, subquery: sub, not };
|
|
1128
1191
|
}
|
|
1129
1192
|
const list = [];
|
|
1130
1193
|
while (true) {
|
|
@@ -1133,13 +1196,12 @@ class Parser {
|
|
|
1133
1196
|
break;
|
|
1134
1197
|
}
|
|
1135
1198
|
this.expect('op', ')');
|
|
1136
|
-
return { type: 'in', operand: left, list };
|
|
1199
|
+
return { type: 'in', operand: left, list, not };
|
|
1137
1200
|
}
|
|
1138
1201
|
|
|
1139
|
-
if (
|
|
1202
|
+
if (t2.type === 'keyword' && t2.value === 'BETWEEN') {
|
|
1140
1203
|
this.next();
|
|
1141
1204
|
const low = this.parseOperand();
|
|
1142
|
-
const not = false;
|
|
1143
1205
|
let andTok = this.peek();
|
|
1144
1206
|
if (andTok.type === 'keyword' && andTok.value === 'AND') {
|
|
1145
1207
|
this.next();
|
|
@@ -1149,19 +1211,25 @@ class Parser {
|
|
|
1149
1211
|
throw new Error(`Expected AND in BETWEEN, got '${andTok.value}'`);
|
|
1150
1212
|
}
|
|
1151
1213
|
|
|
1152
|
-
if (
|
|
1214
|
+
if (t2.type === 'keyword' && t2.value === 'LIKE') {
|
|
1215
|
+
this.next();
|
|
1216
|
+
const pattern = this.parseValue();
|
|
1217
|
+
return { type: 'like', operand: left, pattern, not };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (t2.type === 'keyword' && t2.value === 'REGEXP') {
|
|
1153
1221
|
this.next();
|
|
1154
1222
|
const pattern = this.parseValue();
|
|
1155
|
-
return { type: '
|
|
1223
|
+
return { type: 'regexp', operand: left, pattern, not };
|
|
1156
1224
|
}
|
|
1157
1225
|
|
|
1158
|
-
if (
|
|
1226
|
+
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
|
|
1159
1227
|
this.next();
|
|
1160
1228
|
const right = this.parseOperand();
|
|
1161
|
-
return { type: 'compare', op:
|
|
1229
|
+
return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
|
|
1162
1230
|
}
|
|
1163
1231
|
|
|
1164
|
-
throw new Error(`Expected comparison operator, got '${
|
|
1232
|
+
throw new Error(`Expected comparison operator, got '${t2.value}'`);
|
|
1165
1233
|
}
|
|
1166
1234
|
}
|
|
1167
1235
|
|
|
@@ -1232,6 +1300,13 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1232
1300
|
return applyScalarFunction(operand, row, ctx);
|
|
1233
1301
|
case 'case':
|
|
1234
1302
|
return evaluateCaseVal(operand, row, ctx);
|
|
1303
|
+
case 'in':
|
|
1304
|
+
case 'isNull':
|
|
1305
|
+
case 'isTruth':
|
|
1306
|
+
case 'regexp':
|
|
1307
|
+
case 'like':
|
|
1308
|
+
case 'between':
|
|
1309
|
+
return evaluateExpr(operand, row, ctx);
|
|
1235
1310
|
case 'aggregate':
|
|
1236
1311
|
case 'subquery':
|
|
1237
1312
|
return undefined;
|
|
@@ -1262,7 +1337,10 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1262
1337
|
case 'CURTIME': return new Date().toISOString().slice(11, 19);
|
|
1263
1338
|
case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
|
|
1264
1339
|
case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
|
|
1265
|
-
case 'CONCAT_WS':
|
|
1340
|
+
case 'CONCAT_WS': {
|
|
1341
|
+
const sep = args[0] == null ? ',' : String(args[0]);
|
|
1342
|
+
return args.slice(1).filter(a => a !== null && a !== undefined).map(a => String(a)).join(sep);
|
|
1343
|
+
}
|
|
1266
1344
|
case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
|
|
1267
1345
|
case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
|
|
1268
1346
|
case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
|
|
@@ -1284,8 +1362,16 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1284
1362
|
case 'SUBSTRING': case 'SUBSTR': {
|
|
1285
1363
|
if (args[0] == null) return null;
|
|
1286
1364
|
const s = String(args[0]);
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1365
|
+
const len = s.length;
|
|
1366
|
+
let start = Number(args[1]);
|
|
1367
|
+
// MySQL 语义:1-based;负数从末尾倒数;0 视为 1(MySQL 返回空串)
|
|
1368
|
+
if (start === 0) return '';
|
|
1369
|
+
if (start < 0) start = len + start + 1;
|
|
1370
|
+
if (args[2] !== undefined) {
|
|
1371
|
+
let n = Number(args[2]);
|
|
1372
|
+
if (n < 0) return '';
|
|
1373
|
+
return s.substr(start - 1, n);
|
|
1374
|
+
}
|
|
1289
1375
|
return s.substr(start - 1);
|
|
1290
1376
|
}
|
|
1291
1377
|
case 'LEFT': return args[0] == null ? null : String(args[0]).slice(0, Number(args[1]));
|
|
@@ -1295,6 +1381,33 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1295
1381
|
const idx = String(args[1]).indexOf(String(args[0]));
|
|
1296
1382
|
return idx + 1;
|
|
1297
1383
|
}
|
|
1384
|
+
case 'REVERSE': return args[0] == null ? null : String(args[0]).split('').reverse().join('');
|
|
1385
|
+
case 'LPAD': {
|
|
1386
|
+
if (args[0] == null) return null;
|
|
1387
|
+
let s = String(args[0]);
|
|
1388
|
+
const n = Number(args[1]);
|
|
1389
|
+
const pad = args[2] == null ? ' ' : String(args[2]);
|
|
1390
|
+
if (n <= s.length) return s.slice(0, n);
|
|
1391
|
+
while (s.length < n) s = pad + s;
|
|
1392
|
+
return s;
|
|
1393
|
+
}
|
|
1394
|
+
case 'RPAD': {
|
|
1395
|
+
if (args[0] == null) return null;
|
|
1396
|
+
let s = String(args[0]);
|
|
1397
|
+
const n = Number(args[1]);
|
|
1398
|
+
const pad = args[2] == null ? ' ' : String(args[2]);
|
|
1399
|
+
if (n <= s.length) return s.slice(0, n);
|
|
1400
|
+
while (s.length < n) s = s + pad;
|
|
1401
|
+
return s;
|
|
1402
|
+
}
|
|
1403
|
+
case 'RAND': return args.length > 0 && args[0] != null ? seedRand(Number(args[0]))() : Math.random();
|
|
1404
|
+
case 'UNIX_TIMESTAMP': {
|
|
1405
|
+
if (args.length > 0 && args[0] != null) {
|
|
1406
|
+
const d = new Date(String(args[0]).replace(' ', 'T'));
|
|
1407
|
+
return isNaN(d.getTime()) ? 0 : Math.floor(d.getTime() / 1000);
|
|
1408
|
+
}
|
|
1409
|
+
return Math.floor(Date.now() / 1000);
|
|
1410
|
+
}
|
|
1298
1411
|
case 'GREATEST': return args.reduce((m, a) => a > m ? a : m, args[0]);
|
|
1299
1412
|
case 'LEAST': return args.reduce((m, a) => a < m ? a : m, args[0]);
|
|
1300
1413
|
case 'UUID': return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
@@ -1307,6 +1420,15 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1307
1420
|
}
|
|
1308
1421
|
}
|
|
1309
1422
|
|
|
1423
|
+
function seedRand(seed) {
|
|
1424
|
+
let s = Math.abs(seed) % 2147483647;
|
|
1425
|
+
if (s <= 0) s = 1;
|
|
1426
|
+
return () => {
|
|
1427
|
+
s = (s * 16807) % 2147483647;
|
|
1428
|
+
return (s - 1) / 2147483646;
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1310
1432
|
function likeMatch(value, pattern) {
|
|
1311
1433
|
if (typeof value !== 'string') return false;
|
|
1312
1434
|
const regex = pattern
|
|
@@ -1350,8 +1472,9 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
1350
1472
|
case 'compare': {
|
|
1351
1473
|
const l = resolveOperand(expr.left, row, ctx);
|
|
1352
1474
|
const r = resolveOperand(expr.right, row, ctx);
|
|
1353
|
-
|
|
1475
|
+
// SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
|
|
1354
1476
|
if (l === null || r === null) return false;
|
|
1477
|
+
if (expr.op === '=') return l === r || (l !== null && r !== null && String(l) === String(r));
|
|
1355
1478
|
const fn = OPERATORS[expr.op];
|
|
1356
1479
|
return typeof l === 'number' && typeof r === 'number' ? fn(l, r) : fn(String(l), String(r));
|
|
1357
1480
|
}
|
|
@@ -1366,7 +1489,24 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
1366
1489
|
if (!expr.list) return false;
|
|
1367
1490
|
return expr.list.some(x => x === v || String(x) === String(v));
|
|
1368
1491
|
}
|
|
1369
|
-
case 'like':
|
|
1492
|
+
case 'like': {
|
|
1493
|
+
const r = likeMatch(resolveOperand(expr.operand, row, ctx), expr.pattern);
|
|
1494
|
+
return expr.not ? !r : r;
|
|
1495
|
+
}
|
|
1496
|
+
case 'regexp': {
|
|
1497
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
1498
|
+
if (v === null || v === undefined) return false;
|
|
1499
|
+
const re = new RegExp(String(expr.pattern), 'i');
|
|
1500
|
+
const r = re.test(String(v));
|
|
1501
|
+
return expr.not ? !r : r;
|
|
1502
|
+
}
|
|
1503
|
+
case 'isTruth': {
|
|
1504
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
1505
|
+
const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
|
|
1506
|
+
const isFalse = !isTrue && v !== null && v !== undefined;
|
|
1507
|
+
const result = expr.truth ? isTrue : isFalse;
|
|
1508
|
+
return expr.not ? !result : result;
|
|
1509
|
+
}
|
|
1370
1510
|
case 'between': {
|
|
1371
1511
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
1372
1512
|
const lo = resolveOperand(expr.low, row, ctx);
|
|
@@ -1608,7 +1748,10 @@ class SQLExecutor {
|
|
|
1608
1748
|
if (explicit.length > 0) {
|
|
1609
1749
|
const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1610
1750
|
const pkMap = new Map();
|
|
1611
|
-
for (const row of all)
|
|
1751
|
+
for (const row of all) {
|
|
1752
|
+
const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
1753
|
+
if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
|
|
1754
|
+
}
|
|
1612
1755
|
const conflicts = [];
|
|
1613
1756
|
const fresh = [];
|
|
1614
1757
|
for (const d of dataRows) {
|
|
@@ -1626,7 +1769,13 @@ class SQLExecutor {
|
|
|
1626
1769
|
for (const { d, existingId } of conflicts) {
|
|
1627
1770
|
const data = {};
|
|
1628
1771
|
for (const [col, val] of statement.onDuplicate) data[col] = val;
|
|
1629
|
-
this.engine.updateById
|
|
1772
|
+
if (this.engine.updateById && existingId.length === 1) {
|
|
1773
|
+
this.engine.updateById(statement.name, existingId[0], data);
|
|
1774
|
+
} else if (this.engine.update) {
|
|
1775
|
+
const filter = {};
|
|
1776
|
+
pkCols.forEach((c, i) => { filter[c] = existingId[i]; });
|
|
1777
|
+
this.engine.update(statement.name, filter, data);
|
|
1778
|
+
}
|
|
1630
1779
|
updated++;
|
|
1631
1780
|
}
|
|
1632
1781
|
await this.engine.flush();
|
|
@@ -1677,7 +1826,9 @@ class SQLExecutor {
|
|
|
1677
1826
|
const id = row._rid !== undefined ? row._rid : row.id;
|
|
1678
1827
|
if (id !== undefined) {
|
|
1679
1828
|
const data = {};
|
|
1680
|
-
for (const [col, val] of statement.assignments)
|
|
1829
|
+
for (const [col, val] of statement.assignments) {
|
|
1830
|
+
data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
|
|
1831
|
+
}
|
|
1681
1832
|
this.engine.updateById(statement.table, id, data);
|
|
1682
1833
|
count++;
|
|
1683
1834
|
}
|
package/lib/wasm_client.js
CHANGED
|
@@ -5,6 +5,64 @@ function safeJsonParse(str) {
|
|
|
5
5
|
try { return JSON.parse(str); } catch { return str; }
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
const NATIVE_TYPE_MAP = {
|
|
9
|
+
text: 'string',
|
|
10
|
+
varchar: 'string',
|
|
11
|
+
double: 'float',
|
|
12
|
+
number: 'float',
|
|
13
|
+
numeric: 'float',
|
|
14
|
+
decimal: 'float',
|
|
15
|
+
date: 'string',
|
|
16
|
+
timestamp: 'string',
|
|
17
|
+
datetime: 'string',
|
|
18
|
+
json: 'string',
|
|
19
|
+
array: 'string',
|
|
20
|
+
object: 'string',
|
|
21
|
+
bool: 'boolean',
|
|
22
|
+
bigint: 'string',
|
|
23
|
+
int: 'integer',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function mapNativeSchema(schema) {
|
|
27
|
+
const mapped = {};
|
|
28
|
+
for (const [field, def] of Object.entries(schema || {})) {
|
|
29
|
+
if (typeof def === 'string') {
|
|
30
|
+
mapped[field] = { type: NATIVE_TYPE_MAP[def.toLowerCase()] || def.toLowerCase() };
|
|
31
|
+
} else if (def && typeof def === 'object') {
|
|
32
|
+
const t = (def.type || 'string').toLowerCase();
|
|
33
|
+
mapped[field] = { ...def, type: NATIVE_TYPE_MAP[t] || t };
|
|
34
|
+
} else {
|
|
35
|
+
mapped[field] = { type: 'string' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return mapped;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
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;
|
|
48
|
+
}
|
|
49
|
+
const colDef = schema[field];
|
|
50
|
+
let t = null;
|
|
51
|
+
if (typeof colDef === 'string') t = colDef.toLowerCase();
|
|
52
|
+
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]);
|
|
59
|
+
} else {
|
|
60
|
+
out[field] = row[field];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
8
66
|
const INT64_TAG = 1;
|
|
9
67
|
const FLOAT_TAG = 2;
|
|
10
68
|
const STR_TAG = 3;
|
|
@@ -267,6 +325,26 @@ class JSQL {
|
|
|
267
325
|
}
|
|
268
326
|
|
|
269
327
|
async _insertBatch(table, rows) {
|
|
328
|
+
const schema = this._schemas[table];
|
|
329
|
+
if (schema) {
|
|
330
|
+
rows = rows.map(row => {
|
|
331
|
+
const out = {};
|
|
332
|
+
for (const [k, v] of Object.entries(row)) {
|
|
333
|
+
const def = schema[k];
|
|
334
|
+
let t = null;
|
|
335
|
+
if (typeof def === 'string') t = def.toLowerCase();
|
|
336
|
+
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
337
|
+
if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
|
|
338
|
+
out[k] = JSON.stringify(v);
|
|
339
|
+
} else if (v instanceof Date) {
|
|
340
|
+
out[k] = v.toISOString();
|
|
341
|
+
} else {
|
|
342
|
+
out[k] = v;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
270
348
|
const r = safeJsonParse(wasmBindings.jsql_insert_json(table, JSON.stringify(rows)));
|
|
271
349
|
if (r && r.error) throw new Error(r.error);
|
|
272
350
|
return r;
|
|
@@ -320,7 +398,7 @@ class JSQL {
|
|
|
320
398
|
async createTable(name, schema) {
|
|
321
399
|
await this._flush();
|
|
322
400
|
if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
|
|
323
|
-
const r = safeJsonParse(wasmBindings.jsql_create_table(name, JSON.stringify(schema)));
|
|
401
|
+
const r = safeJsonParse(wasmBindings.jsql_create_table(name, JSON.stringify(mapNativeSchema(schema))));
|
|
324
402
|
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|
|
325
403
|
this._tableNames.add(name);
|
|
326
404
|
this._schemas[name] = schema;
|
|
@@ -347,6 +425,8 @@ class JSQL {
|
|
|
347
425
|
const r = safeJsonParse(wasmBindings.jsql_find_by_id(table, BigInt(id)));
|
|
348
426
|
if (r && r.error) throw new Error(r.error);
|
|
349
427
|
this._runHooks('afterFind', [table, { id }, r]);
|
|
428
|
+
const schema = this._schemas[table];
|
|
429
|
+
if (schema && r && typeof r === 'object') return restoreRow(r, schema);
|
|
350
430
|
return r;
|
|
351
431
|
}
|
|
352
432
|
|
|
@@ -358,6 +438,8 @@ class JSQL {
|
|
|
358
438
|
const r = safeJsonParse(wasmBindings.jsql_find(table, filterStr, limit, offset));
|
|
359
439
|
if (r && r.error) throw new Error(r.error);
|
|
360
440
|
this._runHooks('afterFind', [table, { filter, opts }, r]);
|
|
441
|
+
const schema = this._schemas[table];
|
|
442
|
+
if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
|
|
361
443
|
return r;
|
|
362
444
|
}
|
|
363
445
|
|
|
@@ -397,6 +479,8 @@ class JSQL {
|
|
|
397
479
|
const r = safeJsonParse(wasmBindings.jsql_find_by_ids(table, JSON.stringify(ids)));
|
|
398
480
|
if (r && r.error) throw new Error(r.error);
|
|
399
481
|
this._runHooks('afterFind', [table, { ids }, r]);
|
|
482
|
+
const schema = this._schemas[table];
|
|
483
|
+
if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
|
|
400
484
|
return r;
|
|
401
485
|
}
|
|
402
486
|
|