jsql-neo 4.5.1 → 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.
@@ -909,7 +909,7 @@ class MysqlServer {
909
909
  this._engine = null;
910
910
  this._ownEngine = false;
911
911
  this._databases = new Map();
912
- this._dbDir = options.dataDir ? path.resolve(options.dataDir) : null;
912
+ this._dbDir = options.dataDir && options.dataDir !== ':memory:' ? path.resolve(options.dataDir) : null;
913
913
  this._defaultDbName = options.defaultDatabase || 'default';
914
914
  this._connectionCounter = 0;
915
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]);
@@ -279,14 +342,23 @@ class JSQL {
279
342
  async start() {
280
343
  if (this._mode !== 'memory') {
281
344
  if (!this._path) throw new Error('hybrid/disk mode requires a directory path');
282
- 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
+ }
283
351
  if (r && r.ok === false) throw new Error(r.error || 'open storage failed');
284
352
  if (r && Array.isArray(r.tables)) {
285
353
  this._tableNames = new Set(r.tables);
286
354
  if (r.schemas) this._schemas = r.schemas;
287
355
  }
288
356
  } else {
289
- 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
+ }
290
362
  }
291
363
  if (this._mode !== 'memory') {
292
364
  if (this._flushInterval > 0) {
@@ -319,8 +391,19 @@ class JSQL {
319
391
  }
320
392
 
321
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
+ }
322
405
  const bin = encodeBatch(rows);
323
- const r = JSON.parse(native.jsqlInsertBuf(table, bin));
406
+ const r = safeParse(native.jsqlInsertBuf(table, bin));
324
407
  if (r && r.error) throw new Error(r.error);
325
408
  return r;
326
409
  }
@@ -340,13 +423,13 @@ class JSQL {
340
423
  var ids = remove[table];
341
424
  if (ids.size === 0) continue;
342
425
  var idsArr = Array.from(ids);
343
- var r = JSON.parse(native.jsqlRemoveByIds(table, JSON.stringify(idsArr)));
426
+ var r = safeParse(native.jsqlRemoveByIds(table, JSON.stringify(idsArr)));
344
427
  this._emit('delete', { table, ids: idsArr, result: r });
345
428
  }
346
429
  for (var table in update) {
347
430
  var entries = update[table];
348
431
  if (entries.length === 0) continue;
349
- var r = JSON.parse(native.jsqlUpdateByIds(table, JSON.stringify(entries)));
432
+ var r = safeParse(native.jsqlUpdateByIds(table, JSON.stringify(entries)));
350
433
  this._emit('update', { table, entries, result: r });
351
434
  }
352
435
  this._opBuffer = { remove: {}, update: {} };
@@ -410,7 +493,7 @@ class JSQL {
410
493
  async createTable(name, schema) {
411
494
  await this._flush();
412
495
  if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
413
- const r = JSON.parse(native.jsqlCreateTable(name, JSON.stringify(schema)));
496
+ const r = safeParse(native.jsqlCreateTable(name, JSON.stringify(mapNativeSchema(schema))));
414
497
  if (r && r.ok === false) throw new Error(r.error || 'create table failed');
415
498
  this._tableNames.add(name);
416
499
  this._schemas[name] = schema;
@@ -422,7 +505,7 @@ class JSQL {
422
505
  async dropTable(name) {
423
506
  await this._flush();
424
507
  if (!this._runHooks('beforeDropTable', [name])) return null;
425
- const r = JSON.parse(native.jsqlDropTable(name));
508
+ const r = safeParse(native.jsqlDropTable(name));
426
509
  if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
427
510
  this._tableNames.delete(name);
428
511
  delete this._schemas[name];
@@ -434,9 +517,11 @@ class JSQL {
434
517
  findById(table, id) {
435
518
  this._flushOpsNow();
436
519
  if (!this._runHooks('beforeFind', [table, { id }])) return null;
437
- const raw = JSON.parse(native.jsqlFindById(table, Number(id)));
520
+ const raw = safeParse(native.jsqlFindById(table, Number(id)));
438
521
  if (raw && raw.error) throw new Error(raw.error);
439
522
  this._runHooks('afterFind', [table, { id }, raw]);
523
+ const schema = this._schemas[table];
524
+ if (schema && raw && typeof raw === 'object') return restoreRow(raw, schema);
440
525
  return raw;
441
526
  }
442
527
 
@@ -444,9 +529,11 @@ class JSQL {
444
529
  this._flushOpsNow();
445
530
  if (!this._runHooks('beforeFind', [table, { ids }])) return null;
446
531
  const resultStr = native.jsqlFindByIds(table, JSON.stringify(ids));
447
- const r = JSON.parse(resultStr);
532
+ const r = safeParse(resultStr);
448
533
  if (r && r.error) throw new Error(r.error);
449
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));
450
537
  return r;
451
538
  }
452
539
 
@@ -459,9 +546,11 @@ class JSQL {
459
546
  if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
460
547
  const filterStr = filter ? JSON.stringify(filter) : '';
461
548
  const { limit = 100, offset = 0 } = opts;
462
- const r = JSON.parse(native.jsqlFind(table, filterStr, limit, offset));
549
+ const r = safeParse(native.jsqlFind(table, filterStr, limit, offset));
463
550
  if (r && r.error) throw new Error(r.error);
464
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));
465
554
  return r;
466
555
  }
467
556
 
@@ -529,19 +618,19 @@ class JSQL {
529
618
 
530
619
  async beginTx() {
531
620
  this._flushOpsNow();
532
- const r = JSON.parse(native.jsqlBeginTx());
621
+ const r = safeParse(native.jsqlBeginTx());
533
622
  if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
534
623
  return r.txId;
535
624
  }
536
625
 
537
626
  async commitTx(txId) {
538
- const r = JSON.parse(native.jsqlCommitTx(String(txId)));
627
+ const r = safeParse(native.jsqlCommitTx(String(txId)));
539
628
  if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
540
629
  return true;
541
630
  }
542
631
 
543
632
  async rollbackTx(txId) {
544
- const r = JSON.parse(native.jsqlRollbackTx(String(txId)));
633
+ const r = safeParse(native.jsqlRollbackTx(String(txId)));
545
634
  if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
546
635
  return true;
547
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) {
@@ -585,8 +586,9 @@ class Parser {
585
586
  if (this.isKeyword('LIMIT')) {
586
587
  this.next();
587
588
  limit = this.parseValue();
588
- if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); }
589
- 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}`); }
590
592
  }
591
593
  let union = null;
592
594
  if (this.isKeyword('UNION')) {
@@ -684,7 +686,7 @@ class Parser {
684
686
  while (true) {
685
687
  const col = this.parseColumnRef();
686
688
  this.expect('op', '=');
687
- assignments.push([col, this.parseValue()]);
689
+ assignments.push([col, this.parseScalar()]);
688
690
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
689
691
  break;
690
692
  }
@@ -1073,6 +1075,39 @@ class Parser {
1073
1075
  }
1074
1076
  break;
1075
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
+ }
1076
1111
  return node;
1077
1112
  }
1078
1113
 
@@ -1120,23 +1155,39 @@ class Parser {
1120
1155
  parseComparison() {
1121
1156
  const left = this.parseOperand();
1122
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();
1123
1169
 
1124
- if (t.type === 'keyword' && t.value === 'IS') {
1170
+ if (t2.type === 'keyword' && t2.value === 'IS') {
1125
1171
  this.next();
1126
- const not = this.isKeyword('NOT');
1127
- if (not) this.next();
1128
- this.expectKeyword('NULL');
1129
- 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}'`);
1130
1181
  }
1131
1182
 
1132
- if (t.type === 'keyword' && t.value === 'IN') {
1183
+ if (t2.type === 'keyword' && t2.value === 'IN') {
1133
1184
  this.next();
1134
1185
  this.expect('op', '(');
1135
1186
  // IN (SELECT ...) 子查询
1136
1187
  if (this.isKeyword('SELECT')) {
1137
1188
  const sub = this.parseSelect();
1138
1189
  this.expect('op', ')');
1139
- return { type: 'in', operand: left, subquery: sub };
1190
+ return { type: 'in', operand: left, subquery: sub, not };
1140
1191
  }
1141
1192
  const list = [];
1142
1193
  while (true) {
@@ -1145,13 +1196,12 @@ class Parser {
1145
1196
  break;
1146
1197
  }
1147
1198
  this.expect('op', ')');
1148
- return { type: 'in', operand: left, list };
1199
+ return { type: 'in', operand: left, list, not };
1149
1200
  }
1150
1201
 
1151
- if (t.type === 'keyword' && t.value === 'BETWEEN') {
1202
+ if (t2.type === 'keyword' && t2.value === 'BETWEEN') {
1152
1203
  this.next();
1153
1204
  const low = this.parseOperand();
1154
- const not = false;
1155
1205
  let andTok = this.peek();
1156
1206
  if (andTok.type === 'keyword' && andTok.value === 'AND') {
1157
1207
  this.next();
@@ -1161,19 +1211,25 @@ class Parser {
1161
1211
  throw new Error(`Expected AND in BETWEEN, got '${andTok.value}'`);
1162
1212
  }
1163
1213
 
1164
- 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') {
1165
1221
  this.next();
1166
1222
  const pattern = this.parseValue();
1167
- return { type: 'like', operand: left, pattern };
1223
+ return { type: 'regexp', operand: left, pattern, not };
1168
1224
  }
1169
1225
 
1170
- if (t.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t.value)) {
1226
+ if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
1171
1227
  this.next();
1172
1228
  const right = this.parseOperand();
1173
- return { type: 'compare', op: t.value === '<>' ? '!=' : t.value, left, right };
1229
+ return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
1174
1230
  }
1175
1231
 
1176
- throw new Error(`Expected comparison operator, got '${t.value}'`);
1232
+ throw new Error(`Expected comparison operator, got '${t2.value}'`);
1177
1233
  }
1178
1234
  }
1179
1235
 
@@ -1244,6 +1300,13 @@ function resolveOperand(operand, row, ctx) {
1244
1300
  return applyScalarFunction(operand, row, ctx);
1245
1301
  case 'case':
1246
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);
1247
1310
  case 'aggregate':
1248
1311
  case 'subquery':
1249
1312
  return undefined;
@@ -1274,7 +1337,10 @@ function applyScalarFunction(fnNode, row, ctx) {
1274
1337
  case 'CURTIME': return new Date().toISOString().slice(11, 19);
1275
1338
  case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
1276
1339
  case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
1277
- 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
+ }
1278
1344
  case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
1279
1345
  case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
1280
1346
  case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
@@ -1296,8 +1362,16 @@ function applyScalarFunction(fnNode, row, ctx) {
1296
1362
  case 'SUBSTRING': case 'SUBSTR': {
1297
1363
  if (args[0] == null) return null;
1298
1364
  const s = String(args[0]);
1299
- const start = Number(args[1]);
1300
- 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
+ }
1301
1375
  return s.substr(start - 1);
1302
1376
  }
1303
1377
  case 'LEFT': return args[0] == null ? null : String(args[0]).slice(0, Number(args[1]));
@@ -1307,6 +1381,33 @@ function applyScalarFunction(fnNode, row, ctx) {
1307
1381
  const idx = String(args[1]).indexOf(String(args[0]));
1308
1382
  return idx + 1;
1309
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
+ }
1310
1411
  case 'GREATEST': return args.reduce((m, a) => a > m ? a : m, args[0]);
1311
1412
  case 'LEAST': return args.reduce((m, a) => a < m ? a : m, args[0]);
1312
1413
  case 'UUID': return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
@@ -1319,6 +1420,15 @@ function applyScalarFunction(fnNode, row, ctx) {
1319
1420
  }
1320
1421
  }
1321
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
+
1322
1432
  function likeMatch(value, pattern) {
1323
1433
  if (typeof value !== 'string') return false;
1324
1434
  const regex = pattern
@@ -1362,8 +1472,9 @@ function evaluateExpr(expr, row, ctx) {
1362
1472
  case 'compare': {
1363
1473
  const l = resolveOperand(expr.left, row, ctx);
1364
1474
  const r = resolveOperand(expr.right, row, ctx);
1365
- 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)
1366
1476
  if (l === null || r === null) return false;
1477
+ if (expr.op === '=') return l === r || (l !== null && r !== null && String(l) === String(r));
1367
1478
  const fn = OPERATORS[expr.op];
1368
1479
  return typeof l === 'number' && typeof r === 'number' ? fn(l, r) : fn(String(l), String(r));
1369
1480
  }
@@ -1378,7 +1489,24 @@ function evaluateExpr(expr, row, ctx) {
1378
1489
  if (!expr.list) return false;
1379
1490
  return expr.list.some(x => x === v || String(x) === String(v));
1380
1491
  }
1381
- 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
+ }
1382
1510
  case 'between': {
1383
1511
  const v = resolveOperand(expr.operand, row, ctx);
1384
1512
  const lo = resolveOperand(expr.low, row, ctx);
@@ -1698,7 +1826,9 @@ class SQLExecutor {
1698
1826
  const id = row._rid !== undefined ? row._rid : row.id;
1699
1827
  if (id !== undefined) {
1700
1828
  const data = {};
1701
- 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
+ }
1702
1832
  this.engine.updateById(statement.table, id, data);
1703
1833
  count++;
1704
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.1",
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",