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.
- package/lib/mysql_server.js +1 -1
- package/lib/native_client.js +102 -13
- package/lib/sql.js +155 -25
- package/lib/wasm_client.js +85 -1
- package/package.json +1 -1
package/lib/mysql_server.js
CHANGED
|
@@ -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();
|
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]);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
589
|
-
|
|
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.
|
|
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 (
|
|
1170
|
+
if (t2.type === 'keyword' && t2.value === 'IS') {
|
|
1125
1171
|
this.next();
|
|
1126
|
-
const
|
|
1127
|
-
if (
|
|
1128
|
-
this.
|
|
1129
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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: '
|
|
1223
|
+
return { type: 'regexp', operand: left, pattern, not };
|
|
1168
1224
|
}
|
|
1169
1225
|
|
|
1170
|
-
if (
|
|
1226
|
+
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
|
|
1171
1227
|
this.next();
|
|
1172
1228
|
const right = this.parseOperand();
|
|
1173
|
-
return { type: 'compare', op:
|
|
1229
|
+
return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
|
|
1174
1230
|
}
|
|
1175
1231
|
|
|
1176
|
-
throw new Error(`Expected comparison operator, got '${
|
|
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':
|
|
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
|
|
1300
|
-
|
|
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
|
-
|
|
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':
|
|
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)
|
|
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
|
}
|
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
|
|