jsql-neo 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -94
- package/lib/mysql_server.js +284 -5
- package/lib/sql.js +425 -53
- package/lib/table.js +4 -2
- package/package.json +10 -1
- package/wasm/browser.mjs +404 -0
- package/wasm/browser_bg.mjs +462 -0
package/lib/sql.js
CHANGED
|
@@ -47,12 +47,13 @@ const KEYWORDS = new Set([
|
|
|
47
47
|
'KEY', 'AUTO_INCREMENT', 'AUTOINCREMENT', 'INTEGER', 'INT', 'BIGINT', 'STRING', 'TEXT',
|
|
48
48
|
'VARCHAR', 'CHAR', 'FLOAT', 'DOUBLE', 'REAL', 'NUMERIC', 'DECIMAL', 'BOOLEAN', 'BOOL', 'DATE', 'DATETIME',
|
|
49
49
|
'TIMESTAMP', 'ANY', 'OBJECT', 'ARRAY', 'JSON', 'SMALLINT', 'TINYINT', 'BEGIN', 'COMMIT', 'ROLLBACK',
|
|
50
|
+
'START',
|
|
50
51
|
'TRANSACTION', 'WORK', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'AS', 'UNIQUE',
|
|
51
52
|
'NOTNULL', 'DEFAULT', 'IF', 'EXISTS', 'DISTINCT', 'SHOW', 'USE', 'TABLES',
|
|
52
53
|
'DATABASES', 'DATABASE', 'DESCRIBE', 'DESC', 'ON', 'DUPLICATE',
|
|
53
54
|
'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'CROSS',
|
|
54
55
|
'GROUP', 'HAVING', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
|
|
55
|
-
'BETWEEN', 'USING', 'FULL'
|
|
56
|
+
'BETWEEN', 'USING', 'FULL', 'UNSIGNED', 'ZEROFILL', 'TRUNCATE'
|
|
56
57
|
]);
|
|
57
58
|
|
|
58
59
|
function tokenize(sql) {
|
|
@@ -191,6 +192,12 @@ class Parser {
|
|
|
191
192
|
return t.type === 'keyword' && t.value === kw;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
// 匹配大小写不敏感的词(无论被 tokenize 成 ident 还是 keyword)
|
|
196
|
+
isWord(kw, offset = 0) {
|
|
197
|
+
const t = this.peek(offset);
|
|
198
|
+
return t.value !== undefined && String(t.value).toUpperCase() === kw;
|
|
199
|
+
}
|
|
200
|
+
|
|
194
201
|
parseStatement() {
|
|
195
202
|
const t = this.peek();
|
|
196
203
|
if (t.type === 'eof') return null;
|
|
@@ -206,13 +213,16 @@ class Parser {
|
|
|
206
213
|
if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
|
|
207
214
|
throw new Error('Unsupported DROP statement');
|
|
208
215
|
case 'INSERT': return this.parseInsert();
|
|
216
|
+
case 'TRUNCATE': this.expectKeyword('TRUNCATE'); if (this.isKeyword('TABLE')) this.next(); return { type: 'truncate', name: this.parseTableName() };
|
|
209
217
|
case 'SELECT': return this.parseSelect();
|
|
210
218
|
case 'UPDATE': return this.parseUpdate();
|
|
211
219
|
case 'DELETE': return this.parseDelete();
|
|
212
220
|
case 'BEGIN': this.expectKeyword('BEGIN'); this.optionalTransaction(); return { type: 'begin' };
|
|
221
|
+
case 'START': this.expectKeyword('START'); if (this.isKeyword('TRANSACTION')) this.next(); return { type: 'begin' };
|
|
213
222
|
case 'COMMIT': this.expectKeyword('COMMIT'); this.optionalTransaction(); return { type: 'commit' };
|
|
214
223
|
case 'ROLLBACK': this.expectKeyword('ROLLBACK'); this.optionalTransaction(); return { type: 'rollback' };
|
|
215
224
|
case 'SHOW': return this.parseShow();
|
|
225
|
+
case 'SET': return this.parseSet();
|
|
216
226
|
case 'DESCRIBE': case 'DESC': return this.parseDescribe();
|
|
217
227
|
case 'USE': return this.parseUse();
|
|
218
228
|
default: throw new Error(`Unsupported statement: ${t.value}`);
|
|
@@ -284,6 +294,11 @@ class Parser {
|
|
|
284
294
|
throw new Error(`Expected ',' or ')' in CREATE TABLE, got '${sep.value}'`);
|
|
285
295
|
}
|
|
286
296
|
this.expect('op', ')');
|
|
297
|
+
|
|
298
|
+
// 跳过表选项: ENGINE=InnoDB, DEFAULT CHARSET=..., AUTO_INCREMENT=1, COLLATE=...(直到 ; 或语句结束)
|
|
299
|
+
while (!(this.peek().type === 'eof' || (this.peek().type === 'op' && this.peek().value === ';'))) {
|
|
300
|
+
this.next();
|
|
301
|
+
}
|
|
287
302
|
this.optionalTailSemicolon();
|
|
288
303
|
|
|
289
304
|
if (!hasPk && schema.id === undefined) {
|
|
@@ -339,6 +354,9 @@ class Parser {
|
|
|
339
354
|
case 'AUTO_INCREMENT':
|
|
340
355
|
case 'AUTOINCREMENT':
|
|
341
356
|
this.next(); def.autoIncrement = true; break;
|
|
357
|
+
case 'UNSIGNED':
|
|
358
|
+
case 'ZEROFILL':
|
|
359
|
+
this.next(); def.unsigned = true; break;
|
|
342
360
|
case 'UNIQUE':
|
|
343
361
|
this.next(); def.unique = true; break;
|
|
344
362
|
case 'NOT':
|
|
@@ -423,6 +441,7 @@ class Parser {
|
|
|
423
441
|
const t = this.next();
|
|
424
442
|
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
425
443
|
if (t.type === 'keyword' && t.value === 'NULL') return null;
|
|
444
|
+
if (t.type === 'keyword' && t.value === 'DEFAULT') return { _default: true };
|
|
426
445
|
if (t.type === 'op' && t.value === '-') {
|
|
427
446
|
const num = this.next();
|
|
428
447
|
if (num.type !== 'number') throw new Error('Expected number after -');
|
|
@@ -448,16 +467,18 @@ class Parser {
|
|
|
448
467
|
if (t.type === 'keyword' && t.value === 'COUNT') {
|
|
449
468
|
this.next();
|
|
450
469
|
this.expect('op', '(');
|
|
451
|
-
|
|
470
|
+
let col = null;
|
|
471
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
472
|
+
else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
|
|
452
473
|
this.expect('op', ')');
|
|
453
|
-
aggregate = { type: 'COUNT' };
|
|
474
|
+
aggregate = { type: 'COUNT', column: col };
|
|
454
475
|
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
455
|
-
columns.push({ expr:
|
|
476
|
+
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias });
|
|
456
477
|
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
457
478
|
this.next();
|
|
458
479
|
const fn = t.value;
|
|
459
480
|
this.expect('op', '(');
|
|
460
|
-
const col = this.
|
|
481
|
+
const col = this.parseScalar();
|
|
461
482
|
this.expect('op', ')');
|
|
462
483
|
aggregate = { type: fn, column: col };
|
|
463
484
|
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
@@ -465,21 +486,23 @@ class Parser {
|
|
|
465
486
|
} else if (t.type === 'op' && t.value === '*') {
|
|
466
487
|
this.next();
|
|
467
488
|
columns.push({ expr: '*' });
|
|
468
|
-
} else if (t.type === 'number' || t.type === 'string') {
|
|
469
|
-
this.next();
|
|
470
|
-
let alias = null;
|
|
471
|
-
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
472
|
-
columns.push({ expr: null, literal: t.value, alias });
|
|
473
489
|
} else if (t.type === 'keyword' && t.value === 'CASE') {
|
|
474
490
|
const caseExpr = this.parseOperand();
|
|
475
491
|
let alias = null;
|
|
476
492
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
477
493
|
columns.push({ expr: null, caseExpr, alias });
|
|
478
494
|
} else {
|
|
479
|
-
|
|
495
|
+
// 列 / 常量 / 函数 / 算术表达式
|
|
496
|
+
const expr = this.parseScalar();
|
|
480
497
|
let alias = null;
|
|
481
498
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
482
|
-
|
|
499
|
+
if (expr.type === 'aggregate') {
|
|
500
|
+
columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
|
|
501
|
+
} else if (expr.type === 'column') {
|
|
502
|
+
columns.push({ expr: expr.name, scalar: expr, alias });
|
|
503
|
+
} else {
|
|
504
|
+
columns.push({ expr: null, scalar: expr, alias });
|
|
505
|
+
}
|
|
483
506
|
}
|
|
484
507
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
485
508
|
break;
|
|
@@ -678,13 +701,80 @@ class Parser {
|
|
|
678
701
|
this.next();
|
|
679
702
|
let database = null;
|
|
680
703
|
if (this.isKeyword('FROM')) { this.next(); database = this.parseTableName(); }
|
|
704
|
+
let like = null;
|
|
705
|
+
if (this.isKeyword('LIKE')) { this.next(); like = this.parseValue(); }
|
|
681
706
|
this.optionalTailSemicolon();
|
|
682
|
-
return { type: 'showTables', database };
|
|
707
|
+
return { type: 'showTables', database, like };
|
|
683
708
|
}
|
|
684
709
|
if (this.isKeyword('DATABASES')) { this.next(); this.optionalTailSemicolon(); return { type: 'showDatabases' }; }
|
|
710
|
+
if (this.isWord('FULL')) this.next();
|
|
711
|
+
if (this.isWord('COLUMNS')) {
|
|
712
|
+
this.next();
|
|
713
|
+
if (!(this.isKeyword('FROM') || this.isWord('IN'))) throw new Error("Expected FROM after SHOW COLUMNS");
|
|
714
|
+
this.next();
|
|
715
|
+
let table = this.parseTableName();
|
|
716
|
+
if (table.includes('.')) table = table.slice(table.lastIndexOf('.') + 1);
|
|
717
|
+
let like = null;
|
|
718
|
+
if (this.isKeyword('LIKE')) { this.next(); like = this.parseValue(); }
|
|
719
|
+
this.optionalTailSemicolon();
|
|
720
|
+
return { type: 'showColumns', table, like };
|
|
721
|
+
}
|
|
722
|
+
if (this.isWord('INDEX') || this.isWord('INDEXES') || this.isWord('KEYS')) {
|
|
723
|
+
this.next();
|
|
724
|
+
if (!(this.isKeyword('FROM') || this.isWord('IN'))) throw new Error("Expected FROM after SHOW INDEX");
|
|
725
|
+
this.next();
|
|
726
|
+
let table = this.parseTableName();
|
|
727
|
+
if (table.includes('.')) table = table.slice(table.lastIndexOf('.') + 1);
|
|
728
|
+
this.optionalTailSemicolon();
|
|
729
|
+
return { type: 'showIndex', table };
|
|
730
|
+
}
|
|
731
|
+
if (this.isKeyword('CREATE')) {
|
|
732
|
+
this.next();
|
|
733
|
+
this.expectKeyword('TABLE');
|
|
734
|
+
let table = this.parseTableName();
|
|
735
|
+
if (table.includes('.')) table = table.slice(table.lastIndexOf('.') + 1);
|
|
736
|
+
this.optionalTailSemicolon();
|
|
737
|
+
return { type: 'showCreateTable', table };
|
|
738
|
+
}
|
|
739
|
+
if (this.isWord('SESSION') || this.isWord('GLOBAL')) {
|
|
740
|
+
this.next();
|
|
741
|
+
}
|
|
742
|
+
if (this.isWord('VARIABLES')) {
|
|
743
|
+
this.next();
|
|
744
|
+
let like = null;
|
|
745
|
+
if (this.isKeyword('LIKE')) { this.next(); like = this.parseValue(); }
|
|
746
|
+
this.optionalTailSemicolon();
|
|
747
|
+
return { type: 'showVariables', like };
|
|
748
|
+
}
|
|
749
|
+
if (this.isWord('STATUS')) {
|
|
750
|
+
this.next();
|
|
751
|
+
this.optionalTailSemicolon();
|
|
752
|
+
return { type: 'showStatus' };
|
|
753
|
+
}
|
|
754
|
+
if (this.isWord('GRANTS')) {
|
|
755
|
+
this.next();
|
|
756
|
+
if (this.isKeyword('FOR')) { this.next(); this.parseTableName(); }
|
|
757
|
+
this.optionalTailSemicolon();
|
|
758
|
+
return { type: 'showGrants' };
|
|
759
|
+
}
|
|
760
|
+
if (this.isWord('WARNINGS') || this.isWord('ERRORS')) {
|
|
761
|
+
this.next();
|
|
762
|
+
this.optionalTailSemicolon();
|
|
763
|
+
return { type: 'showWarnings' };
|
|
764
|
+
}
|
|
685
765
|
throw new Error('Unsupported SHOW statement');
|
|
686
766
|
}
|
|
687
767
|
|
|
768
|
+
parseSet() {
|
|
769
|
+
this.expectKeyword('SET');
|
|
770
|
+
const parts = [];
|
|
771
|
+
while (!(this.peek().type === 'eof' || (this.peek().type === 'op' && this.peek().value === ';'))) {
|
|
772
|
+
parts.push(this.next().value);
|
|
773
|
+
}
|
|
774
|
+
this.optionalTailSemicolon();
|
|
775
|
+
return { type: 'set', raw: parts.join(' ') };
|
|
776
|
+
}
|
|
777
|
+
|
|
688
778
|
parseDescribe() {
|
|
689
779
|
this.next();
|
|
690
780
|
const table = this.parseTableName();
|
|
@@ -751,17 +841,46 @@ class Parser {
|
|
|
751
841
|
if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
|
|
752
842
|
return { type: 'column', name: t.value + '.' + col.value };
|
|
753
843
|
}
|
|
844
|
+
// 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
|
|
845
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
846
|
+
const name = t.value;
|
|
847
|
+
this.next();
|
|
848
|
+
const args = [];
|
|
849
|
+
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
850
|
+
for (;;) {
|
|
851
|
+
args.push(this.parseOperand());
|
|
852
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
this.expect('op', ')');
|
|
857
|
+
return { type: 'func', name, args };
|
|
858
|
+
}
|
|
754
859
|
return { type: 'column', name: t.value };
|
|
755
860
|
}
|
|
756
861
|
if (t.type === 'number' || t.type === 'string') return { type: 'value', value: t.value };
|
|
757
862
|
if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
|
|
758
863
|
if (t.type === 'keyword' && t.value === 'CASE') return this.parseCase();
|
|
864
|
+
if (t.type === 'keyword' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
865
|
+
const name = t.value;
|
|
866
|
+
this.next();
|
|
867
|
+
const args = [];
|
|
868
|
+
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
869
|
+
for (;;) {
|
|
870
|
+
args.push(this.parseOperand());
|
|
871
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
this.expect('op', ')');
|
|
876
|
+
return { type: 'func', name, args };
|
|
877
|
+
}
|
|
759
878
|
if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value)) {
|
|
760
879
|
const fn = t.value;
|
|
761
880
|
this.expect('op', '(');
|
|
762
881
|
let column = null;
|
|
763
882
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
764
|
-
else column = this.
|
|
883
|
+
else if (!(this.peek().type === 'op' && this.peek().value === ')')) column = this.parseScalar();
|
|
765
884
|
this.expect('op', ')');
|
|
766
885
|
return { type: 'aggregate', fn, column };
|
|
767
886
|
}
|
|
@@ -778,6 +897,37 @@ class Parser {
|
|
|
778
897
|
throw new Error(`Expected value or column, got '${t.value}'`);
|
|
779
898
|
}
|
|
780
899
|
|
|
900
|
+
// 算术表达式: + - * / % (左结合, * / 优先)
|
|
901
|
+
parseScalar() {
|
|
902
|
+
let node = this.parseTerm();
|
|
903
|
+
for (;;) {
|
|
904
|
+
const t = this.peek();
|
|
905
|
+
if (t.type === 'op' && (t.value === '+' || t.value === '-')) {
|
|
906
|
+
this.next();
|
|
907
|
+
const right = this.parseTerm();
|
|
908
|
+
node = { type: 'arith', op: t.value, left: node, right };
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
return node;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
parseTerm() {
|
|
917
|
+
let node = this.parseOperand();
|
|
918
|
+
for (;;) {
|
|
919
|
+
const t = this.peek();
|
|
920
|
+
if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
|
|
921
|
+
this.next();
|
|
922
|
+
const right = this.parseOperand();
|
|
923
|
+
node = { type: 'arith', op: t.value, left: node, right };
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
return node;
|
|
929
|
+
}
|
|
930
|
+
|
|
781
931
|
parseCase() {
|
|
782
932
|
let base = null;
|
|
783
933
|
if (!this.isKeyword('WHEN')) {
|
|
@@ -875,17 +1025,101 @@ const OPERATORS = {
|
|
|
875
1025
|
};
|
|
876
1026
|
|
|
877
1027
|
function resolveOperand(operand, row) {
|
|
878
|
-
if (operand
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1028
|
+
if (operand === null || operand === undefined) return null;
|
|
1029
|
+
if (typeof operand === 'string') return resolveOperand({ type: 'column', name: operand }, row);
|
|
1030
|
+
switch (operand.type) {
|
|
1031
|
+
case 'value':
|
|
1032
|
+
case 'literal':
|
|
1033
|
+
return operand.value;
|
|
1034
|
+
case 'column': {
|
|
1035
|
+
const n = operand.name;
|
|
1036
|
+
if (n === undefined) return undefined;
|
|
1037
|
+
if (row[n] !== undefined) return row[n];
|
|
1038
|
+
if (n.includes('.')) {
|
|
1039
|
+
if (row[n] !== undefined) return row[n];
|
|
1040
|
+
const col = n.slice(n.lastIndexOf('.') + 1);
|
|
1041
|
+
return row[col];
|
|
1042
|
+
}
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
case 'arith': {
|
|
1046
|
+
const l = resolveOperand(operand.left, row);
|
|
1047
|
+
const r = resolveOperand(operand.right, row);
|
|
1048
|
+
if (l === null || r === null || l === undefined || r === undefined) return null;
|
|
1049
|
+
switch (operand.op) {
|
|
1050
|
+
case '+': return l + r;
|
|
1051
|
+
case '-': return l - r;
|
|
1052
|
+
case '*': return l * r;
|
|
1053
|
+
case '/': return r === 0 ? null : l / r;
|
|
1054
|
+
case '%': return r === 0 ? null : l % r;
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
case 'func':
|
|
1059
|
+
return applyScalarFunction(operand, row);
|
|
1060
|
+
case 'case':
|
|
1061
|
+
return evaluateCaseVal(operand, row);
|
|
1062
|
+
case 'aggregate':
|
|
1063
|
+
case 'subquery':
|
|
1064
|
+
return undefined;
|
|
1065
|
+
default:
|
|
1066
|
+
return undefined;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function applyScalarFunction(fnNode, row) {
|
|
1071
|
+
const name = (fnNode.name || '').toUpperCase();
|
|
1072
|
+
const args = (fnNode.args || []).map(a => resolveOperand(a, row));
|
|
1073
|
+
switch (name) {
|
|
1074
|
+
case 'VERSION': return '8.0.0-jsql-neo';
|
|
1075
|
+
case 'NOW': case 'CURRENT_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
1076
|
+
case 'CURDATE': case 'CURRENT_DATE': return new Date().toISOString().slice(0, 10);
|
|
1077
|
+
case 'CURTIME': return new Date().toISOString().slice(11, 19);
|
|
1078
|
+
case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
|
|
1079
|
+
case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
|
|
1080
|
+
case 'CONCAT_WS': return (args.slice(1).map(a => a === null || a === undefined ? '' : String(a))).join(args[0] == null ? ',' : String(args[0]));
|
|
1081
|
+
case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
|
|
1082
|
+
case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
|
|
1083
|
+
case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
|
|
1084
|
+
case 'TRIM': return args[0] == null ? null : String(args[0]).trim();
|
|
1085
|
+
case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
|
|
1086
|
+
case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
|
|
1087
|
+
case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
|
|
1088
|
+
case 'ROUND': return args[0] == null ? null : (args[1] !== undefined ? Number(args[0].toFixed(args[1])) : Math.round(args[0]));
|
|
1089
|
+
case 'FLOOR': return args[0] == null ? null : Math.floor(args[0]);
|
|
1090
|
+
case 'CEIL': case 'CEILING': return args[0] == null ? null : Math.ceil(args[0]);
|
|
1091
|
+
case 'MOD': return (args[0] == null || args[1] === 0) ? null : args[0] % args[1];
|
|
1092
|
+
case 'POWER': case 'POW': return args[0] == null ? null : Math.pow(args[0], args[1]);
|
|
1093
|
+
case 'SQRT': return args[0] == null ? null : Math.sqrt(args[0]);
|
|
1094
|
+
case 'IFNULL': case 'NVL': return args[0] != null ? args[0] : args[1];
|
|
1095
|
+
case 'COALESCE': return args.find(a => a != null);
|
|
1096
|
+
case 'NULLIF': return args[0] === args[1] ? null : args[0];
|
|
1097
|
+
case 'IF': return args[0] ? args[1] : args[2];
|
|
1098
|
+
case 'REPLACE': return args[0] == null ? null : String(args[0]).split(args[1]).join(args[2]);
|
|
1099
|
+
case 'SUBSTRING': case 'SUBSTR': {
|
|
1100
|
+
if (args[0] == null) return null;
|
|
1101
|
+
const s = String(args[0]);
|
|
1102
|
+
const start = Number(args[1]);
|
|
1103
|
+
if (args[2] !== undefined) return s.substr(start - 1, Number(args[2]));
|
|
1104
|
+
return s.substr(start - 1);
|
|
1105
|
+
}
|
|
1106
|
+
case 'LEFT': return args[0] == null ? null : String(args[0]).slice(0, Number(args[1]));
|
|
1107
|
+
case 'RIGHT': return args[0] == null ? null : String(args[0]).slice(-Number(args[1]));
|
|
1108
|
+
case 'LOCATE': case 'INSTR': {
|
|
1109
|
+
if (args[0] == null || args[1] == null) return null;
|
|
1110
|
+
const idx = String(args[1]).indexOf(String(args[0]));
|
|
1111
|
+
return idx + 1;
|
|
1112
|
+
}
|
|
1113
|
+
case 'GREATEST': return args.reduce((m, a) => a > m ? a : m, args[0]);
|
|
1114
|
+
case 'LEAST': return args.reduce((m, a) => a < m ? a : m, args[0]);
|
|
1115
|
+
case 'UUID': return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
1116
|
+
const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
1117
|
+
return v.toString(16);
|
|
1118
|
+
});
|
|
1119
|
+
case 'DATABASE': case 'SCHEMA': return row && row.__db !== undefined ? row.__db : 'default';
|
|
1120
|
+
default:
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
889
1123
|
}
|
|
890
1124
|
|
|
891
1125
|
function likeMatch(value, pattern) {
|
|
@@ -976,8 +1210,68 @@ function evaluateCaseVal(caseExpr, row) {
|
|
|
976
1210
|
return caseExpr.elseVal ? resolveOperand(caseExpr.elseVal, row) : null;
|
|
977
1211
|
}
|
|
978
1212
|
|
|
979
|
-
function
|
|
980
|
-
if (
|
|
1213
|
+
function scalarName(node) {
|
|
1214
|
+
if (!node) return 'expr';
|
|
1215
|
+
switch (node.type) {
|
|
1216
|
+
case 'column': {
|
|
1217
|
+
const dot = node.name.indexOf('.');
|
|
1218
|
+
return dot !== -1 ? node.name.slice(dot + 1) : node.name;
|
|
1219
|
+
}
|
|
1220
|
+
case 'func': return node.name;
|
|
1221
|
+
case 'value':
|
|
1222
|
+
case 'literal': return String(node.value);
|
|
1223
|
+
case 'arith': return scalarName(node.left) + ' ' + node.op + ' ' + scalarName(node.right);
|
|
1224
|
+
case 'case': return 'CASE';
|
|
1225
|
+
case 'aggregate': return node.fn + '(' + (node.column || '*') + ')';
|
|
1226
|
+
default: return 'expr';
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function scalarColumnName(c) {
|
|
1231
|
+
if (c.alias) return c.alias;
|
|
1232
|
+
if (c.scalar) return scalarName(c.scalar);
|
|
1233
|
+
if (c.aggregate) return c.alias || (c.aggregate === 'COUNT' ? 'COUNT(*)' : c.aggregate + '(' + c.column + ')');
|
|
1234
|
+
if (c.literal !== undefined) return String(c.literal);
|
|
1235
|
+
if (c.caseExpr) return 'CASE';
|
|
1236
|
+
if (c.expr === '*') return '*';
|
|
1237
|
+
const dot = (c.expr || '').indexOf('.');
|
|
1238
|
+
return dot !== -1 ? c.expr.slice(dot + 1) : c.expr;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function scalarColumnValue(c, r, ctx) {
|
|
1242
|
+
if (c.scalar) return resolveOperand(c.scalar, r);
|
|
1243
|
+
if (c.aggregate) return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column);
|
|
1244
|
+
if (c.literal !== undefined) return c.literal;
|
|
1245
|
+
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
1246
|
+
if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
|
|
1247
|
+
return resolveOperand({ type: 'column', name: c.expr }, r);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function sqlTypeName(type) {
|
|
1251
|
+
const t = String(type || 'string').toLowerCase();
|
|
1252
|
+
if (t === 'string' || t === 'text') return 'varchar(255)';
|
|
1253
|
+
if (t === 'integer') return 'int';
|
|
1254
|
+
if (t === 'float' || t === 'double') return 'float';
|
|
1255
|
+
if (t === 'boolean') return 'tinyint(1)';
|
|
1256
|
+
if (t === 'date' || t === 'datetime' || t === 'timestamp') return 'datetime';
|
|
1257
|
+
if (t === 'object' || t === 'array') return 'json';
|
|
1258
|
+
return t;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function buildCreateTableSql(name, schema) {
|
|
1262
|
+
const parts = Object.entries(schema).map(([col, def]) => {
|
|
1263
|
+
const seg = ['`' + col + '`', sqlTypeName(def.type)];
|
|
1264
|
+
if (def.autoIncrement) seg.push('AUTO_INCREMENT');
|
|
1265
|
+
if (def.nullable === false) seg.push('NOT NULL');
|
|
1266
|
+
if (def.default !== undefined) seg.push('DEFAULT ' + (typeof def.default === 'string' ? "'" + def.default + "'" : def.default));
|
|
1267
|
+
return seg.join(' ');
|
|
1268
|
+
});
|
|
1269
|
+
const pks = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
1270
|
+
if (pks.length > 0) parts.push('PRIMARY KEY (' + pks.map(k => '`' + k + '`').join(', ') + ')');
|
|
1271
|
+
return 'CREATE TABLE `' + name + '` (\n ' + parts.join(',\n ') + '\n) ENGINE=JSQL DEFAULT CHARSET=utf8mb4';
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function normalizeRow(row, schema) { if (row && typeof row === 'object' && row.fields && typeof row.fields === 'object') {
|
|
981
1275
|
const flat = { ...row.fields };
|
|
982
1276
|
if (schema) {
|
|
983
1277
|
const pkCols = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
@@ -1014,9 +1308,25 @@ class SQLExecutor {
|
|
|
1014
1308
|
await this.engine.dropTable(statement.table);
|
|
1015
1309
|
return { ok: true, type: 'dropTable', table: statement.table, affectedRows: 0 };
|
|
1016
1310
|
}
|
|
1311
|
+
case 'truncate': {
|
|
1312
|
+
if (this.engine.hasTable(statement.name)) await this.engine.truncate(statement.name);
|
|
1313
|
+
return { ok: true, type: 'truncate', table: statement.name, affectedRows: 0 };
|
|
1314
|
+
}
|
|
1017
1315
|
case 'insert': {
|
|
1018
1316
|
let dataRows = statement.dataRows;
|
|
1019
1317
|
let schema = null;
|
|
1318
|
+
const stripDefault = (row) => {
|
|
1319
|
+
const out = {};
|
|
1320
|
+
for (const [k, v] of Object.entries(row)) {
|
|
1321
|
+
if (v && typeof v === 'object' && v._default) continue;
|
|
1322
|
+
out[k] = v;
|
|
1323
|
+
}
|
|
1324
|
+
return out;
|
|
1325
|
+
};
|
|
1326
|
+
if (statement.dataRows) {
|
|
1327
|
+
statement.dataRows = statement.dataRows.map(stripDefault);
|
|
1328
|
+
dataRows = statement.dataRows;
|
|
1329
|
+
}
|
|
1020
1330
|
if (dataRows === null && statement.values) {
|
|
1021
1331
|
schema = this.engine.getTableSchema
|
|
1022
1332
|
? await this.engine.getTableSchema(statement.name)
|
|
@@ -1028,7 +1338,8 @@ class SQLExecutor {
|
|
|
1028
1338
|
const row = {};
|
|
1029
1339
|
let vi = 0;
|
|
1030
1340
|
colNames.forEach(c => {
|
|
1031
|
-
|
|
1341
|
+
const isDefault = vals[vi] && typeof vals[vi] === 'object' && vals[vi]._default;
|
|
1342
|
+
if (schema[c].autoIncrement && (skipAuto || vals[vi] === undefined || vals[vi] === null || isDefault)) {
|
|
1032
1343
|
if (!skipAuto && vi < vals.length) vi++;
|
|
1033
1344
|
return;
|
|
1034
1345
|
}
|
|
@@ -1038,6 +1349,7 @@ class SQLExecutor {
|
|
|
1038
1349
|
});
|
|
1039
1350
|
return row;
|
|
1040
1351
|
});
|
|
1352
|
+
dataRows = dataRows.map(stripDefault);
|
|
1041
1353
|
}
|
|
1042
1354
|
if (!schema) {
|
|
1043
1355
|
schema = this.engine.getTableSchema
|
|
@@ -1169,7 +1481,12 @@ class SQLExecutor {
|
|
|
1169
1481
|
return { ok: true, type: 'rollback' };
|
|
1170
1482
|
case 'showTables': {
|
|
1171
1483
|
const tables = this.engine.getTables ? this.engine.getTables() : (this.engine.tables ? this.engine.tables() : []);
|
|
1172
|
-
|
|
1484
|
+
let list = tables.map(t => [t]);
|
|
1485
|
+
if (statement.like) {
|
|
1486
|
+
const re = new RegExp('^' + statement.like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i');
|
|
1487
|
+
list = list.filter(r => re.test(r[0]));
|
|
1488
|
+
}
|
|
1489
|
+
return { ok: true, type: 'showTables', columns: ['Tables_in_' + (statement.database || 'default')], rows: list };
|
|
1173
1490
|
}
|
|
1174
1491
|
case 'showDatabases': {
|
|
1175
1492
|
const list = this.engine.listDatabases
|
|
@@ -1177,6 +1494,72 @@ class SQLExecutor {
|
|
|
1177
1494
|
: ['jsql'];
|
|
1178
1495
|
return { ok: true, type: 'showDatabases', columns: ['Database'], rows: list.map(d => [d]) };
|
|
1179
1496
|
}
|
|
1497
|
+
case 'showColumns': {
|
|
1498
|
+
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1499
|
+
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
1500
|
+
let rows = Object.entries(schema).map(([col, def]) => [
|
|
1501
|
+
col,
|
|
1502
|
+
sqlTypeName(def.type),
|
|
1503
|
+
def.nullable === false ? 'NO' : 'YES',
|
|
1504
|
+
def.primaryKey ? 'PRI' : (def.unique ? 'UNI' : ''),
|
|
1505
|
+
def.default !== undefined && def.default !== null ? String(def.default) : null,
|
|
1506
|
+
def.autoIncrement ? 'auto_increment' : '',
|
|
1507
|
+
]);
|
|
1508
|
+
if (statement.like) {
|
|
1509
|
+
const re = new RegExp('^' + statement.like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i');
|
|
1510
|
+
rows = rows.filter(r => re.test(r[0]));
|
|
1511
|
+
}
|
|
1512
|
+
return { ok: true, type: 'showColumns', table: statement.table, columns: ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra'], rows };
|
|
1513
|
+
}
|
|
1514
|
+
case 'showIndex': {
|
|
1515
|
+
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1516
|
+
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
1517
|
+
const rows = [];
|
|
1518
|
+
let seq = 0;
|
|
1519
|
+
for (const col of Object.keys(schema).filter(k => schema[k].primaryKey)) {
|
|
1520
|
+
rows.push([statement.table, 0, 'PRIMARY', ++seq, col, 'A', 0, null, null, schema[col].nullable === false ? '' : 'YES', 'BTREE', '']);
|
|
1521
|
+
}
|
|
1522
|
+
for (const col of Object.keys(schema).filter(k => schema[k].unique && !schema[k].primaryKey)) {
|
|
1523
|
+
rows.push([statement.table, 0, col, ++seq, col, 'A', 0, null, null, schema[col].nullable === false ? '' : 'YES', 'BTREE', '']);
|
|
1524
|
+
}
|
|
1525
|
+
return { ok: true, type: 'showIndex', table: statement.table, columns: ['Table', 'Non_unique', 'Key_name', 'Seq_in_index', 'Column_name', 'Collation', 'Cardinality', 'Sub_part', 'Packed', 'Null', 'Index_type', 'Comment'], rows };
|
|
1526
|
+
}
|
|
1527
|
+
case 'showCreateTable': {
|
|
1528
|
+
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1529
|
+
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
1530
|
+
const ddl = buildCreateTableSql(statement.table, schema);
|
|
1531
|
+
return { ok: true, type: 'showCreateTable', table: statement.table, columns: ['Table', 'Create Table'], rows: [[statement.table, ddl]] };
|
|
1532
|
+
}
|
|
1533
|
+
case 'showVariables': {
|
|
1534
|
+
const vars = {
|
|
1535
|
+
'version': '8.0.0-jsql-neo',
|
|
1536
|
+
'version_comment': 'JSQL-NEO',
|
|
1537
|
+
'version_compile_os': 'any',
|
|
1538
|
+
'sql_mode': '',
|
|
1539
|
+
'character_set_client': 'utf8mb4',
|
|
1540
|
+
'character_set_connection': 'utf8mb4',
|
|
1541
|
+
'character_set_server': 'utf8mb4',
|
|
1542
|
+
'collation_server': 'utf8mb4_general_ci',
|
|
1543
|
+
'lower_case_table_names': '1',
|
|
1544
|
+
'max_allowed_packet': '1048576',
|
|
1545
|
+
'autocommit': 'ON',
|
|
1546
|
+
'transaction_isolation': 'REPEATABLE-READ',
|
|
1547
|
+
};
|
|
1548
|
+
let entries = Object.entries(vars);
|
|
1549
|
+
if (statement.like) {
|
|
1550
|
+
const re = new RegExp('^' + statement.like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i');
|
|
1551
|
+
entries = entries.filter(([k]) => re.test(k));
|
|
1552
|
+
}
|
|
1553
|
+
return { ok: true, type: 'showVariables', columns: ['Variable_name', 'Value'], rows: entries };
|
|
1554
|
+
}
|
|
1555
|
+
case 'showStatus':
|
|
1556
|
+
return { ok: true, type: 'showStatus', columns: ['Variable_name', 'Value'], rows: [] };
|
|
1557
|
+
case 'showGrants':
|
|
1558
|
+
return { ok: true, type: 'showGrants', columns: ['Grants for root@localhost'], rows: ['GRANT ALL PRIVILEGES ON *.* TO `root`@`localhost`'].map(g => [g]) };
|
|
1559
|
+
case 'showWarnings':
|
|
1560
|
+
return { ok: true, type: 'showWarnings', columns: ['Level', 'Code', 'Message'], rows: [] };
|
|
1561
|
+
case 'set':
|
|
1562
|
+
return { ok: true, type: 'set', raw: statement.raw };
|
|
1180
1563
|
case 'createDatabase': {
|
|
1181
1564
|
if (!this.engine.createDatabase) throw new Error('CREATE DATABASE is not supported by this engine');
|
|
1182
1565
|
await this.engine.createDatabase(statement.database, { ifNotExists: statement.ifNotExists });
|
|
@@ -1278,7 +1661,8 @@ class SQLExecutor {
|
|
|
1278
1661
|
|
|
1279
1662
|
_aggValue(rows, fn, column) {
|
|
1280
1663
|
if (fn === 'COUNT') return rows.length;
|
|
1281
|
-
const
|
|
1664
|
+
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
1665
|
+
const values = rows.map(r => resolveOperand(op, r)).filter(v => v !== null && v !== undefined);
|
|
1282
1666
|
if (fn === 'SUM') return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
1283
1667
|
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
1284
1668
|
if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
@@ -1350,6 +1734,10 @@ class SQLExecutor {
|
|
|
1350
1734
|
|
|
1351
1735
|
// 读第一表
|
|
1352
1736
|
const firstItem = statement.from.tables[0];
|
|
1737
|
+
if (firstItem && firstItem.table && String(firstItem.table).toLowerCase().startsWith('information_schema.')) {
|
|
1738
|
+
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
1739
|
+
return { ok: true, type: 'select', table: null, columns: cols, rows: [], raw: [] };
|
|
1740
|
+
}
|
|
1353
1741
|
let rowsAll;
|
|
1354
1742
|
if (firstItem.subquery) {
|
|
1355
1743
|
const res = await this.executeSelect(firstItem.subquery);
|
|
@@ -1443,8 +1831,10 @@ class SQLExecutor {
|
|
|
1443
1831
|
const seen = new Set();
|
|
1444
1832
|
rows = rows.filter(r => {
|
|
1445
1833
|
const key = JSON.stringify(statement.columns.map(c => {
|
|
1834
|
+
if (c.scalar) return resolveOperand(c.scalar, r);
|
|
1835
|
+
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
1446
1836
|
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
|
|
1447
|
-
return resolveOperand(
|
|
1837
|
+
return resolveOperand({ type: 'column', name: c.expr }, r);
|
|
1448
1838
|
}));
|
|
1449
1839
|
if (seen.has(key)) return false;
|
|
1450
1840
|
seen.add(key);
|
|
@@ -1463,23 +1853,10 @@ class SQLExecutor {
|
|
|
1463
1853
|
|
|
1464
1854
|
// GROUP BY 输出:每组的列(含聚合列)
|
|
1465
1855
|
if (statement.groupBy) {
|
|
1466
|
-
const
|
|
1467
|
-
const cols = statement.columns.map(c => {
|
|
1468
|
-
if (c.alias) return c.alias;
|
|
1469
|
-
if (c.aggregate) return aggName(c);
|
|
1470
|
-
if (c.literal !== undefined) return String(c.literal);
|
|
1471
|
-
if (c.caseExpr) return 'CASE';
|
|
1472
|
-
const dot = c.expr.indexOf('.');
|
|
1473
|
-
return dot !== -1 ? c.expr.slice(dot + 1) : c.expr;
|
|
1474
|
-
});
|
|
1856
|
+
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
1475
1857
|
const mapped = rows.map(r => {
|
|
1476
1858
|
const group = r._group || [r];
|
|
1477
|
-
return statement.columns.map(c => {
|
|
1478
|
-
if (c.aggregate) return this._aggValue(group, c.aggregate || 'COUNT', c.column);
|
|
1479
|
-
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
1480
|
-
if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
|
|
1481
|
-
return resolveOperand({ type: 'column', name: c.expr }, r);
|
|
1482
|
-
});
|
|
1859
|
+
return statement.columns.map(c => scalarColumnValue(c, r, { _aggValue: this._aggValue.bind(this), group }));
|
|
1483
1860
|
});
|
|
1484
1861
|
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: mapped, raw: rows };
|
|
1485
1862
|
}
|
|
@@ -1515,14 +1892,9 @@ class SQLExecutor {
|
|
|
1515
1892
|
return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
|
|
1516
1893
|
}
|
|
1517
1894
|
|
|
1518
|
-
const cols = statement.columns.map(c =>
|
|
1519
|
-
if (c.alias) return c.alias;
|
|
1520
|
-
if (c.literal !== undefined) return String(c.literal);
|
|
1521
|
-
if (c.caseExpr) return 'CASE';
|
|
1522
|
-
const dot = c.expr.indexOf('.');
|
|
1523
|
-
return dot !== -1 ? c.expr.slice(dot + 1) : c.expr;
|
|
1524
|
-
});
|
|
1895
|
+
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
1525
1896
|
const mapped = rows.map(r => statement.columns.map(c => {
|
|
1897
|
+
if (c.scalar) return resolveOperand(c.scalar, r);
|
|
1526
1898
|
if (c.expr === '*') return null;
|
|
1527
1899
|
if (c.literal !== undefined) return c.literal;
|
|
1528
1900
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|