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 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
 
@@ -104,8 +104,19 @@ function handshakePacket(connectionId, seed) {
104
104
  return b.build();
105
105
  }
106
106
 
107
- function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) {
108
- const b = new PacketBuilder();
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 = crypto.randomBytes(20);
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();
@@ -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
- for (const fn of hooks) {
261
- const r = fn(...args);
262
- if (r === false) return false;
263
- if (r !== undefined && args.length > 0) {
264
- args[0] = r;
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
- const r = JSON.parse(native.jsqlOpen(this._path, this._mode));
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
- JSON.parse(native.jsqlOpen('', 'memory'));
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 = JSON.parse(native.jsqlInsertBuf(table, bin));
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 = JSON.parse(native.jsqlRemoveByIds(table, JSON.stringify(idsArr)));
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 = JSON.parse(native.jsqlUpdateByIds(table, JSON.stringify(entries)));
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 = JSON.parse(native.jsqlCreateTable(name, JSON.stringify(schema)));
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 = JSON.parse(native.jsqlDropTable(name));
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 = JSON.parse(native.jsqlFindById(table, Number(id)));
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 = JSON.parse(resultStr);
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 = JSON.parse(native.jsqlFind(table, filterStr, limit, offset));
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 = JSON.parse(native.jsqlBeginTx());
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 = JSON.parse(native.jsqlCommitTx(String(txId)));
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 = JSON.parse(native.jsqlRollbackTx(String(txId)));
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 pkCol = this.parseTableName();
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
- if (schema[pkCol]) schema[pkCol].primaryKey = true;
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 uCol = this.parseTableName();
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
- if (schema[uCol]) schema[uCol].unique = true;
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 (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); }
577
- else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); }
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.parseValue()]);
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 (t.type === 'keyword' && t.value === 'IS') {
1170
+ if (t2.type === 'keyword' && t2.value === 'IS') {
1113
1171
  this.next();
1114
- const not = this.isKeyword('NOT');
1115
- if (not) this.next();
1116
- this.expectKeyword('NULL');
1117
- return { type: 'isNull', operand: left, not: !!not };
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 (t.type === 'keyword' && t.value === 'IN') {
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 (t.type === 'keyword' && t.value === 'BETWEEN') {
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 (t.type === 'keyword' && t.value === 'LIKE') {
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: 'like', operand: left, pattern };
1223
+ return { type: 'regexp', operand: left, pattern, not };
1156
1224
  }
1157
1225
 
1158
- if (t.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t.value)) {
1226
+ if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
1159
1227
  this.next();
1160
1228
  const right = this.parseOperand();
1161
- return { type: 'compare', op: t.value === '<>' ? '!=' : t.value, left, right };
1229
+ return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
1162
1230
  }
1163
1231
 
1164
- throw new Error(`Expected comparison operator, got '${t.value}'`);
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': return (args.slice(1).map(a => a === null || a === undefined ? '' : String(a))).join(args[0] == null ? ',' : String(args[0]));
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 start = Number(args[1]);
1288
- if (args[2] !== undefined) return s.substr(start - 1, Number(args[2]));
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
- if (expr.op === '=') return l === r || (l === null && r === null) || (l !== null && r !== null && String(l) === String(r));
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': return likeMatch(resolveOperand(expr.operand, row, ctx), expr.pattern);
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) pkMap.set(keyOf(row), row.id);
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(statement.name, existingId, data);
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) data[col] = val;
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
  }
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
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",