jsql-neo 4.2.0 → 4.4.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 +112 -94
- package/bin/jsql +138 -0
- package/index.d.ts +351 -0
- package/index.js +18 -0
- package/lib/migrate.js +242 -0
- package/lib/mysql_server.js +335 -6
- package/lib/redis_server.js +448 -0
- package/lib/sql.js +533 -87
- package/lib/table.js +4 -2
- package/lib/web_ui.js +226 -0
- package/package.json +66 -47
- package/test/smoke.js +58 -0
- package/wasm/browser.d.ts +88 -0
- 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', 'COLLATE', 'CHARACTER'
|
|
56
57
|
]);
|
|
57
58
|
|
|
58
59
|
function tokenize(sql) {
|
|
@@ -84,7 +85,7 @@ function tokenize(sql) {
|
|
|
84
85
|
while (j < n) {
|
|
85
86
|
if (sql[j] === '\\' && j + 1 < n) {
|
|
86
87
|
const esc = sql[j + 1];
|
|
87
|
-
const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\' };
|
|
88
|
+
const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\', b: '\b', Z: '\x1a', a: '\a' };
|
|
88
89
|
str += map[esc] !== undefined ? map[esc] : esc;
|
|
89
90
|
j += 2;
|
|
90
91
|
} else if (sql[j] === quote) {
|
|
@@ -140,6 +141,22 @@ function tokenize(sql) {
|
|
|
140
141
|
continue;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
if (c === '@') {
|
|
145
|
+
let j = i;
|
|
146
|
+
while (j < n && sql[j] === '@') j++;
|
|
147
|
+
const ats = j - i;
|
|
148
|
+
let k = j;
|
|
149
|
+
while (k < n && /[A-Za-z0-9_$]/.test(sql[k])) k++;
|
|
150
|
+
if (k > j) {
|
|
151
|
+
tokens.push(new SQLToken('sysvar', sql.slice(j, k), i));
|
|
152
|
+
if (ats > 1) i = k;
|
|
153
|
+
else i = k;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
i = j;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
const two = sql.slice(i, i + 2);
|
|
144
161
|
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==') {
|
|
145
162
|
tokens.push(new SQLToken('op', two, i));
|
|
@@ -191,6 +208,12 @@ class Parser {
|
|
|
191
208
|
return t.type === 'keyword' && t.value === kw;
|
|
192
209
|
}
|
|
193
210
|
|
|
211
|
+
// 匹配大小写不敏感的词(无论被 tokenize 成 ident 还是 keyword)
|
|
212
|
+
isWord(kw, offset = 0) {
|
|
213
|
+
const t = this.peek(offset);
|
|
214
|
+
return t.value !== undefined && String(t.value).toUpperCase() === kw;
|
|
215
|
+
}
|
|
216
|
+
|
|
194
217
|
parseStatement() {
|
|
195
218
|
const t = this.peek();
|
|
196
219
|
if (t.type === 'eof') return null;
|
|
@@ -206,13 +229,16 @@ class Parser {
|
|
|
206
229
|
if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
|
|
207
230
|
throw new Error('Unsupported DROP statement');
|
|
208
231
|
case 'INSERT': return this.parseInsert();
|
|
232
|
+
case 'TRUNCATE': this.expectKeyword('TRUNCATE'); if (this.isKeyword('TABLE')) this.next(); return { type: 'truncate', name: this.parseTableName() };
|
|
209
233
|
case 'SELECT': return this.parseSelect();
|
|
210
234
|
case 'UPDATE': return this.parseUpdate();
|
|
211
235
|
case 'DELETE': return this.parseDelete();
|
|
212
236
|
case 'BEGIN': this.expectKeyword('BEGIN'); this.optionalTransaction(); return { type: 'begin' };
|
|
237
|
+
case 'START': this.expectKeyword('START'); if (this.isKeyword('TRANSACTION')) this.next(); return { type: 'begin' };
|
|
213
238
|
case 'COMMIT': this.expectKeyword('COMMIT'); this.optionalTransaction(); return { type: 'commit' };
|
|
214
239
|
case 'ROLLBACK': this.expectKeyword('ROLLBACK'); this.optionalTransaction(); return { type: 'rollback' };
|
|
215
240
|
case 'SHOW': return this.parseShow();
|
|
241
|
+
case 'SET': return this.parseSet();
|
|
216
242
|
case 'DESCRIBE': case 'DESC': return this.parseDescribe();
|
|
217
243
|
case 'USE': return this.parseUse();
|
|
218
244
|
default: throw new Error(`Unsupported statement: ${t.value}`);
|
|
@@ -284,6 +310,11 @@ class Parser {
|
|
|
284
310
|
throw new Error(`Expected ',' or ')' in CREATE TABLE, got '${sep.value}'`);
|
|
285
311
|
}
|
|
286
312
|
this.expect('op', ')');
|
|
313
|
+
|
|
314
|
+
// 跳过表选项: ENGINE=InnoDB, DEFAULT CHARSET=..., AUTO_INCREMENT=1, COLLATE=...(直到 ; 或语句结束)
|
|
315
|
+
while (!(this.peek().type === 'eof' || (this.peek().type === 'op' && this.peek().value === ';'))) {
|
|
316
|
+
this.next();
|
|
317
|
+
}
|
|
287
318
|
this.optionalTailSemicolon();
|
|
288
319
|
|
|
289
320
|
if (!hasPk && schema.id === undefined) {
|
|
@@ -339,6 +370,9 @@ class Parser {
|
|
|
339
370
|
case 'AUTO_INCREMENT':
|
|
340
371
|
case 'AUTOINCREMENT':
|
|
341
372
|
this.next(); def.autoIncrement = true; break;
|
|
373
|
+
case 'UNSIGNED':
|
|
374
|
+
case 'ZEROFILL':
|
|
375
|
+
this.next(); def.unsigned = true; break;
|
|
342
376
|
case 'UNIQUE':
|
|
343
377
|
this.next(); def.unique = true; break;
|
|
344
378
|
case 'NOT':
|
|
@@ -347,6 +381,13 @@ class Parser {
|
|
|
347
381
|
this.next(); break;
|
|
348
382
|
case 'DEFAULT':
|
|
349
383
|
this.next(); def.default = this.parseValue(); break;
|
|
384
|
+
case 'COLLATE':
|
|
385
|
+
this.next(); if (this.peek().type !== 'op' && this.peek().type !== 'eof') this.next(); break;
|
|
386
|
+
case 'CHARACTER':
|
|
387
|
+
this.next();
|
|
388
|
+
if (this.isKeyword('SET')) this.next();
|
|
389
|
+
if (this.peek().type !== 'op' && this.peek().type !== 'eof') this.next();
|
|
390
|
+
break;
|
|
350
391
|
default:
|
|
351
392
|
return def;
|
|
352
393
|
}
|
|
@@ -423,6 +464,7 @@ class Parser {
|
|
|
423
464
|
const t = this.next();
|
|
424
465
|
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
425
466
|
if (t.type === 'keyword' && t.value === 'NULL') return null;
|
|
467
|
+
if (t.type === 'keyword' && t.value === 'DEFAULT') return { _default: true };
|
|
426
468
|
if (t.type === 'op' && t.value === '-') {
|
|
427
469
|
const num = this.next();
|
|
428
470
|
if (num.type !== 'number') throw new Error('Expected number after -');
|
|
@@ -448,16 +490,18 @@ class Parser {
|
|
|
448
490
|
if (t.type === 'keyword' && t.value === 'COUNT') {
|
|
449
491
|
this.next();
|
|
450
492
|
this.expect('op', '(');
|
|
451
|
-
|
|
493
|
+
let col = null;
|
|
494
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
495
|
+
else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
|
|
452
496
|
this.expect('op', ')');
|
|
453
|
-
aggregate = { type: 'COUNT' };
|
|
497
|
+
aggregate = { type: 'COUNT', column: col };
|
|
454
498
|
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
455
|
-
columns.push({ expr:
|
|
499
|
+
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias });
|
|
456
500
|
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
457
501
|
this.next();
|
|
458
502
|
const fn = t.value;
|
|
459
503
|
this.expect('op', '(');
|
|
460
|
-
const col = this.
|
|
504
|
+
const col = this.parseScalar();
|
|
461
505
|
this.expect('op', ')');
|
|
462
506
|
aggregate = { type: fn, column: col };
|
|
463
507
|
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
@@ -465,21 +509,23 @@ class Parser {
|
|
|
465
509
|
} else if (t.type === 'op' && t.value === '*') {
|
|
466
510
|
this.next();
|
|
467
511
|
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
512
|
} else if (t.type === 'keyword' && t.value === 'CASE') {
|
|
474
513
|
const caseExpr = this.parseOperand();
|
|
475
514
|
let alias = null;
|
|
476
515
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
477
516
|
columns.push({ expr: null, caseExpr, alias });
|
|
478
517
|
} else {
|
|
479
|
-
|
|
518
|
+
// 列 / 常量 / 函数 / 算术表达式
|
|
519
|
+
const expr = this.parseScalar();
|
|
480
520
|
let alias = null;
|
|
481
521
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
482
|
-
|
|
522
|
+
if (expr.type === 'aggregate') {
|
|
523
|
+
columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
|
|
524
|
+
} else if (expr.type === 'column') {
|
|
525
|
+
columns.push({ expr: expr.name, scalar: expr, alias });
|
|
526
|
+
} else {
|
|
527
|
+
columns.push({ expr: null, scalar: expr, alias });
|
|
528
|
+
}
|
|
483
529
|
}
|
|
484
530
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
485
531
|
break;
|
|
@@ -678,13 +724,80 @@ class Parser {
|
|
|
678
724
|
this.next();
|
|
679
725
|
let database = null;
|
|
680
726
|
if (this.isKeyword('FROM')) { this.next(); database = this.parseTableName(); }
|
|
727
|
+
let like = null;
|
|
728
|
+
if (this.isKeyword('LIKE')) { this.next(); like = this.parseValue(); }
|
|
681
729
|
this.optionalTailSemicolon();
|
|
682
|
-
return { type: 'showTables', database };
|
|
730
|
+
return { type: 'showTables', database, like };
|
|
683
731
|
}
|
|
684
732
|
if (this.isKeyword('DATABASES')) { this.next(); this.optionalTailSemicolon(); return { type: 'showDatabases' }; }
|
|
733
|
+
if (this.isWord('FULL')) this.next();
|
|
734
|
+
if (this.isWord('COLUMNS')) {
|
|
735
|
+
this.next();
|
|
736
|
+
if (!(this.isKeyword('FROM') || this.isWord('IN'))) throw new Error("Expected FROM after SHOW COLUMNS");
|
|
737
|
+
this.next();
|
|
738
|
+
let table = this.parseTableName();
|
|
739
|
+
if (table.includes('.')) table = table.slice(table.lastIndexOf('.') + 1);
|
|
740
|
+
let like = null;
|
|
741
|
+
if (this.isKeyword('LIKE')) { this.next(); like = this.parseValue(); }
|
|
742
|
+
this.optionalTailSemicolon();
|
|
743
|
+
return { type: 'showColumns', table, like };
|
|
744
|
+
}
|
|
745
|
+
if (this.isWord('INDEX') || this.isWord('INDEXES') || this.isWord('KEYS')) {
|
|
746
|
+
this.next();
|
|
747
|
+
if (!(this.isKeyword('FROM') || this.isWord('IN'))) throw new Error("Expected FROM after SHOW INDEX");
|
|
748
|
+
this.next();
|
|
749
|
+
let table = this.parseTableName();
|
|
750
|
+
if (table.includes('.')) table = table.slice(table.lastIndexOf('.') + 1);
|
|
751
|
+
this.optionalTailSemicolon();
|
|
752
|
+
return { type: 'showIndex', table };
|
|
753
|
+
}
|
|
754
|
+
if (this.isKeyword('CREATE')) {
|
|
755
|
+
this.next();
|
|
756
|
+
this.expectKeyword('TABLE');
|
|
757
|
+
let table = this.parseTableName();
|
|
758
|
+
if (table.includes('.')) table = table.slice(table.lastIndexOf('.') + 1);
|
|
759
|
+
this.optionalTailSemicolon();
|
|
760
|
+
return { type: 'showCreateTable', table };
|
|
761
|
+
}
|
|
762
|
+
if (this.isWord('SESSION') || this.isWord('GLOBAL')) {
|
|
763
|
+
this.next();
|
|
764
|
+
}
|
|
765
|
+
if (this.isWord('VARIABLES')) {
|
|
766
|
+
this.next();
|
|
767
|
+
let like = null;
|
|
768
|
+
if (this.isKeyword('LIKE')) { this.next(); like = this.parseValue(); }
|
|
769
|
+
this.optionalTailSemicolon();
|
|
770
|
+
return { type: 'showVariables', like };
|
|
771
|
+
}
|
|
772
|
+
if (this.isWord('STATUS')) {
|
|
773
|
+
this.next();
|
|
774
|
+
this.optionalTailSemicolon();
|
|
775
|
+
return { type: 'showStatus' };
|
|
776
|
+
}
|
|
777
|
+
if (this.isWord('GRANTS')) {
|
|
778
|
+
this.next();
|
|
779
|
+
if (this.isKeyword('FOR')) { this.next(); this.parseTableName(); }
|
|
780
|
+
this.optionalTailSemicolon();
|
|
781
|
+
return { type: 'showGrants' };
|
|
782
|
+
}
|
|
783
|
+
if (this.isWord('WARNINGS') || this.isWord('ERRORS')) {
|
|
784
|
+
this.next();
|
|
785
|
+
this.optionalTailSemicolon();
|
|
786
|
+
return { type: 'showWarnings' };
|
|
787
|
+
}
|
|
685
788
|
throw new Error('Unsupported SHOW statement');
|
|
686
789
|
}
|
|
687
790
|
|
|
791
|
+
parseSet() {
|
|
792
|
+
this.expectKeyword('SET');
|
|
793
|
+
const parts = [];
|
|
794
|
+
while (!(this.peek().type === 'eof' || (this.peek().type === 'op' && this.peek().value === ';'))) {
|
|
795
|
+
parts.push(this.next().value);
|
|
796
|
+
}
|
|
797
|
+
this.optionalTailSemicolon();
|
|
798
|
+
return { type: 'set', raw: parts.join(' ') };
|
|
799
|
+
}
|
|
800
|
+
|
|
688
801
|
parseDescribe() {
|
|
689
802
|
this.next();
|
|
690
803
|
const table = this.parseTableName();
|
|
@@ -751,17 +864,47 @@ class Parser {
|
|
|
751
864
|
if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
|
|
752
865
|
return { type: 'column', name: t.value + '.' + col.value };
|
|
753
866
|
}
|
|
867
|
+
// 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
|
|
868
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
869
|
+
const name = t.value;
|
|
870
|
+
this.next();
|
|
871
|
+
const args = [];
|
|
872
|
+
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
873
|
+
for (;;) {
|
|
874
|
+
args.push(this.parseOperand());
|
|
875
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
this.expect('op', ')');
|
|
880
|
+
return { type: 'func', name, args };
|
|
881
|
+
}
|
|
754
882
|
return { type: 'column', name: t.value };
|
|
755
883
|
}
|
|
756
884
|
if (t.type === 'number' || t.type === 'string') return { type: 'value', value: t.value };
|
|
885
|
+
if (t.type === 'sysvar') return { type: 'sysvar', name: t.value };
|
|
757
886
|
if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
|
|
758
887
|
if (t.type === 'keyword' && t.value === 'CASE') return this.parseCase();
|
|
888
|
+
if (t.type === 'keyword' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
889
|
+
const name = t.value;
|
|
890
|
+
this.next();
|
|
891
|
+
const args = [];
|
|
892
|
+
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
893
|
+
for (;;) {
|
|
894
|
+
args.push(this.parseOperand());
|
|
895
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
this.expect('op', ')');
|
|
900
|
+
return { type: 'func', name, args };
|
|
901
|
+
}
|
|
759
902
|
if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value)) {
|
|
760
903
|
const fn = t.value;
|
|
761
904
|
this.expect('op', '(');
|
|
762
905
|
let column = null;
|
|
763
906
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
764
|
-
else column = this.
|
|
907
|
+
else if (!(this.peek().type === 'op' && this.peek().value === ')')) column = this.parseScalar();
|
|
765
908
|
this.expect('op', ')');
|
|
766
909
|
return { type: 'aggregate', fn, column };
|
|
767
910
|
}
|
|
@@ -778,6 +921,37 @@ class Parser {
|
|
|
778
921
|
throw new Error(`Expected value or column, got '${t.value}'`);
|
|
779
922
|
}
|
|
780
923
|
|
|
924
|
+
// 算术表达式: + - * / % (左结合, * / 优先)
|
|
925
|
+
parseScalar() {
|
|
926
|
+
let node = this.parseTerm();
|
|
927
|
+
for (;;) {
|
|
928
|
+
const t = this.peek();
|
|
929
|
+
if (t.type === 'op' && (t.value === '+' || t.value === '-')) {
|
|
930
|
+
this.next();
|
|
931
|
+
const right = this.parseTerm();
|
|
932
|
+
node = { type: 'arith', op: t.value, left: node, right };
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
return node;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
parseTerm() {
|
|
941
|
+
let node = this.parseOperand();
|
|
942
|
+
for (;;) {
|
|
943
|
+
const t = this.peek();
|
|
944
|
+
if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
|
|
945
|
+
this.next();
|
|
946
|
+
const right = this.parseOperand();
|
|
947
|
+
node = { type: 'arith', op: t.value, left: node, right };
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
return node;
|
|
953
|
+
}
|
|
954
|
+
|
|
781
955
|
parseCase() {
|
|
782
956
|
let base = null;
|
|
783
957
|
if (!this.isKeyword('WHEN')) {
|
|
@@ -874,18 +1048,136 @@ const OPERATORS = {
|
|
|
874
1048
|
'>=': (a, b) => a >= b
|
|
875
1049
|
};
|
|
876
1050
|
|
|
877
|
-
function resolveOperand(operand, row) {
|
|
878
|
-
if (operand
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1051
|
+
function resolveOperand(operand, row, ctx) {
|
|
1052
|
+
if (operand === null || operand === undefined) return null;
|
|
1053
|
+
if (typeof operand === 'string') return resolveOperand({ type: 'column', name: operand }, row, ctx);
|
|
1054
|
+
switch (operand.type) {
|
|
1055
|
+
case 'value':
|
|
1056
|
+
case 'literal':
|
|
1057
|
+
return operand.value;
|
|
1058
|
+
case 'sysvar': {
|
|
1059
|
+
const s = (ctx && ctx.session && ctx.session.sysvars) || {};
|
|
1060
|
+
const name = String(operand.name).toLowerCase();
|
|
1061
|
+
if (s[name] !== undefined) return s[name];
|
|
1062
|
+
const DEFAULTS = {
|
|
1063
|
+
'version': '8.0.0-jsql-neo',
|
|
1064
|
+
'version_comment': 'jsql-neo',
|
|
1065
|
+
'version_compile_os': 'linux',
|
|
1066
|
+
'sql_mode': '',
|
|
1067
|
+
'autocommit': 1,
|
|
1068
|
+
'character_set_client': 'utf8mb4',
|
|
1069
|
+
'character_set_connection': 'utf8mb4',
|
|
1070
|
+
'character_set_results': 'utf8mb4',
|
|
1071
|
+
'collation_connection': 'utf8mb4_general_ci',
|
|
1072
|
+
'transaction_isolation': 'REPEATABLE-READ',
|
|
1073
|
+
'max_allowed_packet': 67108864,
|
|
1074
|
+
'wait_timeout': 28800,
|
|
1075
|
+
'lower_case_table_names': 0,
|
|
1076
|
+
'sql_auto_is_null': 0,
|
|
1077
|
+
};
|
|
1078
|
+
return DEFAULTS[name] !== undefined ? DEFAULTS[name] : '';
|
|
1079
|
+
}
|
|
1080
|
+
case 'column': {
|
|
1081
|
+
const n = operand.name;
|
|
1082
|
+
if (n === undefined) return undefined;
|
|
1083
|
+
if (row[n] !== undefined) return row[n];
|
|
1084
|
+
if (n.includes('.')) {
|
|
1085
|
+
if (row[n] !== undefined) return row[n];
|
|
1086
|
+
const col = n.slice(n.lastIndexOf('.') + 1);
|
|
1087
|
+
return row[col];
|
|
1088
|
+
}
|
|
1089
|
+
return undefined;
|
|
1090
|
+
}
|
|
1091
|
+
case 'arith': {
|
|
1092
|
+
const l = resolveOperand(operand.left, row, ctx);
|
|
1093
|
+
const r = resolveOperand(operand.right, row, ctx);
|
|
1094
|
+
if (l === null || r === null || l === undefined || r === undefined) return null;
|
|
1095
|
+
switch (operand.op) {
|
|
1096
|
+
case '+': return l + r;
|
|
1097
|
+
case '-': return l - r;
|
|
1098
|
+
case '*': return l * r;
|
|
1099
|
+
case '/': return r === 0 ? null : l / r;
|
|
1100
|
+
case '%': return r === 0 ? null : l % r;
|
|
1101
|
+
}
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
case 'func':
|
|
1105
|
+
return applyScalarFunction(operand, row, ctx);
|
|
1106
|
+
case 'case':
|
|
1107
|
+
return evaluateCaseVal(operand, row, ctx);
|
|
1108
|
+
case 'aggregate':
|
|
1109
|
+
case 'subquery':
|
|
1110
|
+
return undefined;
|
|
1111
|
+
default:
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function applyScalarFunction(fnNode, row, ctx) {
|
|
1117
|
+
const name = (fnNode.name || '').toUpperCase();
|
|
1118
|
+
const args = (fnNode.args || []).map(a => resolveOperand(a, row, ctx));
|
|
1119
|
+
const session = ctx && ctx.session;
|
|
1120
|
+
switch (name) {
|
|
1121
|
+
case 'VERSION': return '8.0.0-jsql-neo';
|
|
1122
|
+
case 'LAST_INSERT_ID': {
|
|
1123
|
+
if (args.length > 0) {
|
|
1124
|
+
if (session) session.lastInsertId = args[0];
|
|
1125
|
+
return args[0];
|
|
1126
|
+
}
|
|
1127
|
+
return session && session.lastInsertId !== undefined ? session.lastInsertId : 0;
|
|
1128
|
+
}
|
|
1129
|
+
case 'ROW_COUNT': return session && session.rowCount !== undefined ? session.rowCount : 0;
|
|
1130
|
+
case 'FOUND_ROWS': return session && session.foundRows !== undefined ? session.foundRows : 0;
|
|
1131
|
+
case 'CONNECTION_ID': return session && session.connectionId !== undefined ? session.connectionId : 0;
|
|
1132
|
+
case 'DATABASE': case 'SCHEMA': return session && session.currentDb ? session.currentDb : 'default';
|
|
1133
|
+
case 'NOW': case 'CURRENT_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
1134
|
+
case 'CURDATE': case 'CURRENT_DATE': return new Date().toISOString().slice(0, 10);
|
|
1135
|
+
case 'CURTIME': return new Date().toISOString().slice(11, 19);
|
|
1136
|
+
case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
|
|
1137
|
+
case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
|
|
1138
|
+
case 'CONCAT_WS': return (args.slice(1).map(a => a === null || a === undefined ? '' : String(a))).join(args[0] == null ? ',' : String(args[0]));
|
|
1139
|
+
case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
|
|
1140
|
+
case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
|
|
1141
|
+
case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
|
|
1142
|
+
case 'TRIM': return args[0] == null ? null : String(args[0]).trim();
|
|
1143
|
+
case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
|
|
1144
|
+
case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
|
|
1145
|
+
case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
|
|
1146
|
+
case 'ROUND': return args[0] == null ? null : (args[1] !== undefined ? Number(args[0].toFixed(args[1])) : Math.round(args[0]));
|
|
1147
|
+
case 'FLOOR': return args[0] == null ? null : Math.floor(args[0]);
|
|
1148
|
+
case 'CEIL': case 'CEILING': return args[0] == null ? null : Math.ceil(args[0]);
|
|
1149
|
+
case 'MOD': return (args[0] == null || args[1] === 0) ? null : args[0] % args[1];
|
|
1150
|
+
case 'POWER': case 'POW': return args[0] == null ? null : Math.pow(args[0], args[1]);
|
|
1151
|
+
case 'SQRT': return args[0] == null ? null : Math.sqrt(args[0]);
|
|
1152
|
+
case 'IFNULL': case 'NVL': return args[0] != null ? args[0] : args[1];
|
|
1153
|
+
case 'COALESCE': return args.find(a => a != null);
|
|
1154
|
+
case 'NULLIF': return args[0] === args[1] ? null : args[0];
|
|
1155
|
+
case 'IF': return args[0] ? args[1] : args[2];
|
|
1156
|
+
case 'REPLACE': return args[0] == null ? null : String(args[0]).split(args[1]).join(args[2]);
|
|
1157
|
+
case 'SUBSTRING': case 'SUBSTR': {
|
|
1158
|
+
if (args[0] == null) return null;
|
|
1159
|
+
const s = String(args[0]);
|
|
1160
|
+
const start = Number(args[1]);
|
|
1161
|
+
if (args[2] !== undefined) return s.substr(start - 1, Number(args[2]));
|
|
1162
|
+
return s.substr(start - 1);
|
|
1163
|
+
}
|
|
1164
|
+
case 'LEFT': return args[0] == null ? null : String(args[0]).slice(0, Number(args[1]));
|
|
1165
|
+
case 'RIGHT': return args[0] == null ? null : String(args[0]).slice(-Number(args[1]));
|
|
1166
|
+
case 'LOCATE': case 'INSTR': {
|
|
1167
|
+
if (args[0] == null || args[1] == null) return null;
|
|
1168
|
+
const idx = String(args[1]).indexOf(String(args[0]));
|
|
1169
|
+
return idx + 1;
|
|
1170
|
+
}
|
|
1171
|
+
case 'GREATEST': return args.reduce((m, a) => a > m ? a : m, args[0]);
|
|
1172
|
+
case 'LEAST': return args.reduce((m, a) => a < m ? a : m, args[0]);
|
|
1173
|
+
case 'UUID': return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
1174
|
+
const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
1175
|
+
return v.toString(16);
|
|
1176
|
+
});
|
|
1177
|
+
case 'DATABASE': case 'SCHEMA': return row && row.__db !== undefined ? row.__db : 'default';
|
|
1178
|
+
default:
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
889
1181
|
}
|
|
890
1182
|
|
|
891
1183
|
function likeMatch(value, pattern) {
|
|
@@ -922,36 +1214,36 @@ function extractEqualPushdown(expr, schema) {
|
|
|
922
1214
|
return { filter, rest };
|
|
923
1215
|
}
|
|
924
1216
|
|
|
925
|
-
function evaluateExpr(expr, row) {
|
|
1217
|
+
function evaluateExpr(expr, row, ctx) {
|
|
926
1218
|
if (expr === null || expr === undefined) return false;
|
|
927
1219
|
switch (expr.type) {
|
|
928
|
-
case 'and': return evaluateExpr(expr.left, row) && evaluateExpr(expr.right, row);
|
|
929
|
-
case 'or': return evaluateExpr(expr.left, row) || evaluateExpr(expr.right, row);
|
|
930
|
-
case 'not': return !evaluateExpr(expr.expr, row);
|
|
1220
|
+
case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
|
|
1221
|
+
case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
|
|
1222
|
+
case 'not': return !evaluateExpr(expr.expr, row, ctx);
|
|
931
1223
|
case 'compare': {
|
|
932
|
-
const l = resolveOperand(expr.left, row);
|
|
933
|
-
const r = resolveOperand(expr.right, row);
|
|
1224
|
+
const l = resolveOperand(expr.left, row, ctx);
|
|
1225
|
+
const r = resolveOperand(expr.right, row, ctx);
|
|
934
1226
|
if (expr.op === '=') return l === r || (l === null && r === null) || (l !== null && r !== null && String(l) === String(r));
|
|
935
1227
|
if (l === null || r === null) return false;
|
|
936
1228
|
const fn = OPERATORS[expr.op];
|
|
937
1229
|
return typeof l === 'number' && typeof r === 'number' ? fn(l, r) : fn(String(l), String(r));
|
|
938
1230
|
}
|
|
939
1231
|
case 'isNull': {
|
|
940
|
-
const v = resolveOperand(expr.operand, row);
|
|
1232
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
941
1233
|
const isNull = v === null || v === undefined;
|
|
942
1234
|
return expr.not ? !isNull : isNull;
|
|
943
1235
|
}
|
|
944
1236
|
case 'in': {
|
|
945
|
-
const v = resolveOperand(expr.operand, row);
|
|
1237
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
946
1238
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
947
1239
|
if (!expr.list) return false;
|
|
948
1240
|
return expr.list.some(x => x === v || String(x) === String(v));
|
|
949
1241
|
}
|
|
950
|
-
case 'like': return likeMatch(resolveOperand(expr.operand, row), expr.pattern);
|
|
1242
|
+
case 'like': return likeMatch(resolveOperand(expr.operand, row, ctx), expr.pattern);
|
|
951
1243
|
case 'between': {
|
|
952
|
-
const v = resolveOperand(expr.operand, row);
|
|
953
|
-
const lo = resolveOperand(expr.low, row);
|
|
954
|
-
const hi = resolveOperand(expr.high, row);
|
|
1244
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
1245
|
+
const lo = resolveOperand(expr.low, row, ctx);
|
|
1246
|
+
const hi = resolveOperand(expr.high, row, ctx);
|
|
955
1247
|
if (v === null || lo === null || hi === null) return false;
|
|
956
1248
|
const inRange = typeof v === 'number' && typeof lo === 'number' && typeof hi === 'number'
|
|
957
1249
|
? v >= lo && v <= hi
|
|
@@ -960,24 +1252,84 @@ function evaluateExpr(expr, row) {
|
|
|
960
1252
|
}
|
|
961
1253
|
case 'case': {
|
|
962
1254
|
for (const b of expr.branches) {
|
|
963
|
-
if (evaluateExpr(b.cond, row)) return resolveOperand(b.val, row);
|
|
1255
|
+
if (evaluateExpr(b.cond, row, ctx)) return resolveOperand(b.val, row, ctx);
|
|
964
1256
|
}
|
|
965
|
-
return expr.elseVal ? resolveOperand(expr.elseVal, row) : null;
|
|
1257
|
+
return expr.elseVal ? resolveOperand(expr.elseVal, row, ctx) : null;
|
|
966
1258
|
}
|
|
967
1259
|
default: return false;
|
|
968
1260
|
}
|
|
969
1261
|
}
|
|
970
1262
|
|
|
971
|
-
function evaluateCaseVal(caseExpr, row) {
|
|
1263
|
+
function evaluateCaseVal(caseExpr, row, ctx) {
|
|
972
1264
|
if (!caseExpr || caseExpr.type !== 'case') return undefined;
|
|
973
1265
|
for (const b of caseExpr.branches) {
|
|
974
|
-
if (evaluateExpr(b.cond, row)) return resolveOperand(b.val, row);
|
|
1266
|
+
if (evaluateExpr(b.cond, row, ctx)) return resolveOperand(b.val, row, ctx);
|
|
1267
|
+
}
|
|
1268
|
+
return caseExpr.elseVal ? resolveOperand(caseExpr.elseVal, row, ctx) : null;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function scalarName(node) {
|
|
1272
|
+
if (!node) return 'expr';
|
|
1273
|
+
switch (node.type) {
|
|
1274
|
+
case 'column': {
|
|
1275
|
+
const dot = node.name.indexOf('.');
|
|
1276
|
+
return dot !== -1 ? node.name.slice(dot + 1) : node.name;
|
|
1277
|
+
}
|
|
1278
|
+
case 'func': return node.name + '()';
|
|
1279
|
+
case 'value':
|
|
1280
|
+
case 'literal': return String(node.value);
|
|
1281
|
+
case 'arith': return scalarName(node.left) + ' ' + node.op + ' ' + scalarName(node.right);
|
|
1282
|
+
case 'case': return 'CASE';
|
|
1283
|
+
case 'aggregate': return node.fn + '(' + (node.column || '*') + ')';
|
|
1284
|
+
default: return 'expr';
|
|
975
1285
|
}
|
|
976
|
-
return caseExpr.elseVal ? resolveOperand(caseExpr.elseVal, row) : null;
|
|
977
1286
|
}
|
|
978
1287
|
|
|
979
|
-
function
|
|
980
|
-
if (
|
|
1288
|
+
function scalarColumnName(c) {
|
|
1289
|
+
if (c.alias) return c.alias;
|
|
1290
|
+
if (c.scalar) return scalarName(c.scalar);
|
|
1291
|
+
if (c.aggregate) return c.alias || (c.aggregate === 'COUNT' ? 'COUNT(*)' : c.aggregate + '(' + c.column + ')');
|
|
1292
|
+
if (c.literal !== undefined) return String(c.literal);
|
|
1293
|
+
if (c.caseExpr) return 'CASE';
|
|
1294
|
+
if (c.expr === '*') return '*';
|
|
1295
|
+
const dot = (c.expr || '').indexOf('.');
|
|
1296
|
+
return dot !== -1 ? c.expr.slice(dot + 1) : c.expr;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function scalarColumnValue(c, r, ctx) {
|
|
1300
|
+
if (c.scalar) return resolveOperand(c.scalar, r, ctx);
|
|
1301
|
+
if (c.aggregate) return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column);
|
|
1302
|
+
if (c.literal !== undefined) return c.literal;
|
|
1303
|
+
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
|
|
1304
|
+
if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
|
|
1305
|
+
return resolveOperand({ type: 'column', name: c.expr }, r, ctx);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function sqlTypeName(type) {
|
|
1309
|
+
const t = String(type || 'string').toLowerCase();
|
|
1310
|
+
if (t === 'string' || t === 'text') return 'varchar(255)';
|
|
1311
|
+
if (t === 'integer') return 'int';
|
|
1312
|
+
if (t === 'float' || t === 'double') return 'float';
|
|
1313
|
+
if (t === 'boolean') return 'tinyint(1)';
|
|
1314
|
+
if (t === 'date' || t === 'datetime' || t === 'timestamp') return 'datetime';
|
|
1315
|
+
if (t === 'object' || t === 'array') return 'json';
|
|
1316
|
+
return t;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function buildCreateTableSql(name, schema) {
|
|
1320
|
+
const parts = Object.entries(schema).map(([col, def]) => {
|
|
1321
|
+
const seg = ['`' + col + '`', sqlTypeName(def.type)];
|
|
1322
|
+
if (def.autoIncrement) seg.push('AUTO_INCREMENT');
|
|
1323
|
+
if (def.nullable === false) seg.push('NOT NULL');
|
|
1324
|
+
if (def.default !== undefined) seg.push('DEFAULT ' + (typeof def.default === 'string' ? "'" + def.default + "'" : def.default));
|
|
1325
|
+
return seg.join(' ');
|
|
1326
|
+
});
|
|
1327
|
+
const pks = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
1328
|
+
if (pks.length > 0) parts.push('PRIMARY KEY (' + pks.map(k => '`' + k + '`').join(', ') + ')');
|
|
1329
|
+
return 'CREATE TABLE `' + name + '` (\n ' + parts.join(',\n ') + '\n) ENGINE=JSQL DEFAULT CHARSET=utf8mb4';
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function normalizeRow(row, schema) { if (row && typeof row === 'object' && row.fields && typeof row.fields === 'object') {
|
|
981
1333
|
const flat = { ...row.fields };
|
|
982
1334
|
if (schema) {
|
|
983
1335
|
const pkCols = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
@@ -994,8 +1346,9 @@ function normalizeRow(row, schema) {
|
|
|
994
1346
|
}
|
|
995
1347
|
|
|
996
1348
|
class SQLExecutor {
|
|
997
|
-
constructor(engine) {
|
|
1349
|
+
constructor(engine, ctx) {
|
|
998
1350
|
this.engine = engine;
|
|
1351
|
+
this.ctx = ctx || null;
|
|
999
1352
|
}
|
|
1000
1353
|
|
|
1001
1354
|
async execute(statement) {
|
|
@@ -1014,9 +1367,25 @@ class SQLExecutor {
|
|
|
1014
1367
|
await this.engine.dropTable(statement.table);
|
|
1015
1368
|
return { ok: true, type: 'dropTable', table: statement.table, affectedRows: 0 };
|
|
1016
1369
|
}
|
|
1370
|
+
case 'truncate': {
|
|
1371
|
+
if (this.engine.hasTable(statement.name)) await this.engine.truncate(statement.name);
|
|
1372
|
+
return { ok: true, type: 'truncate', table: statement.name, affectedRows: 0 };
|
|
1373
|
+
}
|
|
1017
1374
|
case 'insert': {
|
|
1018
1375
|
let dataRows = statement.dataRows;
|
|
1019
1376
|
let schema = null;
|
|
1377
|
+
const stripDefault = (row) => {
|
|
1378
|
+
const out = {};
|
|
1379
|
+
for (const [k, v] of Object.entries(row)) {
|
|
1380
|
+
if (v && typeof v === 'object' && v._default) continue;
|
|
1381
|
+
out[k] = v;
|
|
1382
|
+
}
|
|
1383
|
+
return out;
|
|
1384
|
+
};
|
|
1385
|
+
if (statement.dataRows) {
|
|
1386
|
+
statement.dataRows = statement.dataRows.map(stripDefault);
|
|
1387
|
+
dataRows = statement.dataRows;
|
|
1388
|
+
}
|
|
1020
1389
|
if (dataRows === null && statement.values) {
|
|
1021
1390
|
schema = this.engine.getTableSchema
|
|
1022
1391
|
? await this.engine.getTableSchema(statement.name)
|
|
@@ -1028,7 +1397,8 @@ class SQLExecutor {
|
|
|
1028
1397
|
const row = {};
|
|
1029
1398
|
let vi = 0;
|
|
1030
1399
|
colNames.forEach(c => {
|
|
1031
|
-
|
|
1400
|
+
const isDefault = vals[vi] && typeof vals[vi] === 'object' && vals[vi]._default;
|
|
1401
|
+
if (schema[c].autoIncrement && (skipAuto || vals[vi] === undefined || vals[vi] === null || isDefault)) {
|
|
1032
1402
|
if (!skipAuto && vi < vals.length) vi++;
|
|
1033
1403
|
return;
|
|
1034
1404
|
}
|
|
@@ -1038,6 +1408,7 @@ class SQLExecutor {
|
|
|
1038
1408
|
});
|
|
1039
1409
|
return row;
|
|
1040
1410
|
});
|
|
1411
|
+
dataRows = dataRows.map(stripDefault);
|
|
1041
1412
|
}
|
|
1042
1413
|
if (!schema) {
|
|
1043
1414
|
schema = this.engine.getTableSchema
|
|
@@ -1119,7 +1490,7 @@ class SQLExecutor {
|
|
|
1119
1490
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1120
1491
|
let count = 0;
|
|
1121
1492
|
for (const row of all) {
|
|
1122
|
-
if (!statement.where || evaluateExpr(statement.where, row)) {
|
|
1493
|
+
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1123
1494
|
const id = row._rid !== undefined ? row._rid : row.id;
|
|
1124
1495
|
if (id !== undefined) {
|
|
1125
1496
|
const data = {};
|
|
@@ -1139,7 +1510,7 @@ class SQLExecutor {
|
|
|
1139
1510
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1140
1511
|
const ids = [];
|
|
1141
1512
|
for (const row of all) {
|
|
1142
|
-
if (!statement.where || evaluateExpr(statement.where, row)) {
|
|
1513
|
+
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1143
1514
|
const id = row._rid !== undefined ? row._rid : row.id;
|
|
1144
1515
|
if (id !== undefined) ids.push(id);
|
|
1145
1516
|
}
|
|
@@ -1169,7 +1540,12 @@ class SQLExecutor {
|
|
|
1169
1540
|
return { ok: true, type: 'rollback' };
|
|
1170
1541
|
case 'showTables': {
|
|
1171
1542
|
const tables = this.engine.getTables ? this.engine.getTables() : (this.engine.tables ? this.engine.tables() : []);
|
|
1172
|
-
|
|
1543
|
+
let list = tables.map(t => [t]);
|
|
1544
|
+
if (statement.like) {
|
|
1545
|
+
const re = new RegExp('^' + statement.like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i');
|
|
1546
|
+
list = list.filter(r => re.test(r[0]));
|
|
1547
|
+
}
|
|
1548
|
+
return { ok: true, type: 'showTables', columns: ['Tables_in_' + (statement.database || 'default')], rows: list };
|
|
1173
1549
|
}
|
|
1174
1550
|
case 'showDatabases': {
|
|
1175
1551
|
const list = this.engine.listDatabases
|
|
@@ -1177,6 +1553,83 @@ class SQLExecutor {
|
|
|
1177
1553
|
: ['jsql'];
|
|
1178
1554
|
return { ok: true, type: 'showDatabases', columns: ['Database'], rows: list.map(d => [d]) };
|
|
1179
1555
|
}
|
|
1556
|
+
case 'showColumns': {
|
|
1557
|
+
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1558
|
+
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
1559
|
+
let rows = Object.entries(schema).map(([col, def]) => [
|
|
1560
|
+
col,
|
|
1561
|
+
sqlTypeName(def.type),
|
|
1562
|
+
def.nullable === false ? 'NO' : 'YES',
|
|
1563
|
+
def.primaryKey ? 'PRI' : (def.unique ? 'UNI' : ''),
|
|
1564
|
+
def.default !== undefined && def.default !== null ? String(def.default) : null,
|
|
1565
|
+
def.autoIncrement ? 'auto_increment' : '',
|
|
1566
|
+
]);
|
|
1567
|
+
if (statement.like) {
|
|
1568
|
+
const re = new RegExp('^' + statement.like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i');
|
|
1569
|
+
rows = rows.filter(r => re.test(r[0]));
|
|
1570
|
+
}
|
|
1571
|
+
return { ok: true, type: 'showColumns', table: statement.table, columns: ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra'], rows };
|
|
1572
|
+
}
|
|
1573
|
+
case 'showIndex': {
|
|
1574
|
+
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1575
|
+
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
1576
|
+
const rows = [];
|
|
1577
|
+
let seq = 0;
|
|
1578
|
+
for (const col of Object.keys(schema).filter(k => schema[k].primaryKey)) {
|
|
1579
|
+
rows.push([statement.table, 0, 'PRIMARY', ++seq, col, 'A', 0, null, null, schema[col].nullable === false ? '' : 'YES', 'BTREE', '']);
|
|
1580
|
+
}
|
|
1581
|
+
for (const col of Object.keys(schema).filter(k => schema[k].unique && !schema[k].primaryKey)) {
|
|
1582
|
+
rows.push([statement.table, 0, col, ++seq, col, 'A', 0, null, null, schema[col].nullable === false ? '' : 'YES', 'BTREE', '']);
|
|
1583
|
+
}
|
|
1584
|
+
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 };
|
|
1585
|
+
}
|
|
1586
|
+
case 'showCreateTable': {
|
|
1587
|
+
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1588
|
+
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
1589
|
+
const ddl = buildCreateTableSql(statement.table, schema);
|
|
1590
|
+
return { ok: true, type: 'showCreateTable', table: statement.table, columns: ['Table', 'Create Table'], rows: [[statement.table, ddl]] };
|
|
1591
|
+
}
|
|
1592
|
+
case 'showVariables': {
|
|
1593
|
+
const vars = {
|
|
1594
|
+
'version': '8.0.0-jsql-neo',
|
|
1595
|
+
'version_comment': 'JSQL-NEO',
|
|
1596
|
+
'version_compile_os': 'any',
|
|
1597
|
+
'sql_mode': '',
|
|
1598
|
+
'character_set_client': 'utf8mb4',
|
|
1599
|
+
'character_set_connection': 'utf8mb4',
|
|
1600
|
+
'character_set_server': 'utf8mb4',
|
|
1601
|
+
'collation_server': 'utf8mb4_general_ci',
|
|
1602
|
+
'lower_case_table_names': '1',
|
|
1603
|
+
'max_allowed_packet': '1048576',
|
|
1604
|
+
'autocommit': 'ON',
|
|
1605
|
+
'transaction_isolation': 'REPEATABLE-READ',
|
|
1606
|
+
};
|
|
1607
|
+
let entries = Object.entries(vars);
|
|
1608
|
+
if (statement.like) {
|
|
1609
|
+
const re = new RegExp('^' + statement.like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i');
|
|
1610
|
+
entries = entries.filter(([k]) => re.test(k));
|
|
1611
|
+
}
|
|
1612
|
+
return { ok: true, type: 'showVariables', columns: ['Variable_name', 'Value'], rows: entries };
|
|
1613
|
+
}
|
|
1614
|
+
case 'showStatus':
|
|
1615
|
+
return { ok: true, type: 'showStatus', columns: ['Variable_name', 'Value'], rows: [] };
|
|
1616
|
+
case 'showGrants':
|
|
1617
|
+
return { ok: true, type: 'showGrants', columns: ['Grants for root@localhost'], rows: ['GRANT ALL PRIVILEGES ON *.* TO `root`@`localhost`'].map(g => [g]) };
|
|
1618
|
+
case 'showWarnings':
|
|
1619
|
+
return { ok: true, type: 'showWarnings', columns: ['Level', 'Code', 'Message'], rows: [] };
|
|
1620
|
+
case 'set': {
|
|
1621
|
+
const session = this.ctx && this.ctx.session;
|
|
1622
|
+
if (session) {
|
|
1623
|
+
const raw = String(statement.raw || '');
|
|
1624
|
+
const m = raw.match(/^\s*([A-Za-z0-9_]+)(?:\.[A-Za-z0-9_]+)?\s*=\s*(.+)$/);
|
|
1625
|
+
if (m) {
|
|
1626
|
+
let val = m[2].trim().replace(/^'|'$/g, '').replace(/^"|"$/g, '');
|
|
1627
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) val = Number(val);
|
|
1628
|
+
session.sysvars[m[1].toLowerCase()] = val;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return { ok: true, type: 'set', raw: statement.raw };
|
|
1632
|
+
}
|
|
1180
1633
|
case 'createDatabase': {
|
|
1181
1634
|
if (!this.engine.createDatabase) throw new Error('CREATE DATABASE is not supported by this engine');
|
|
1182
1635
|
await this.engine.createDatabase(statement.database, { ifNotExists: statement.ifNotExists });
|
|
@@ -1278,7 +1731,8 @@ class SQLExecutor {
|
|
|
1278
1731
|
|
|
1279
1732
|
_aggValue(rows, fn, column) {
|
|
1280
1733
|
if (fn === 'COUNT') return rows.length;
|
|
1281
|
-
const
|
|
1734
|
+
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
1735
|
+
const values = rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
|
|
1282
1736
|
if (fn === 'SUM') return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
1283
1737
|
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
1284
1738
|
if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
@@ -1350,6 +1804,10 @@ class SQLExecutor {
|
|
|
1350
1804
|
|
|
1351
1805
|
// 读第一表
|
|
1352
1806
|
const firstItem = statement.from.tables[0];
|
|
1807
|
+
if (firstItem && firstItem.table && String(firstItem.table).toLowerCase().startsWith('information_schema.')) {
|
|
1808
|
+
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
1809
|
+
return { ok: true, type: 'select', table: null, columns: cols, rows: [], raw: [] };
|
|
1810
|
+
}
|
|
1353
1811
|
let rowsAll;
|
|
1354
1812
|
if (firstItem.subquery) {
|
|
1355
1813
|
const res = await this.executeSelect(firstItem.subquery);
|
|
@@ -1401,14 +1859,14 @@ class SQLExecutor {
|
|
|
1401
1859
|
|
|
1402
1860
|
let rows = all;
|
|
1403
1861
|
if (statement.where) {
|
|
1404
|
-
rows = rows.filter(r => evaluateExpr(statement.where, r));
|
|
1862
|
+
rows = rows.filter(r => evaluateExpr(statement.where, r, this.ctx));
|
|
1405
1863
|
}
|
|
1406
1864
|
|
|
1407
1865
|
// 分组聚合
|
|
1408
1866
|
if (statement.groupBy) {
|
|
1409
1867
|
const groups = new Map();
|
|
1410
1868
|
for (const row of rows) {
|
|
1411
|
-
const key = JSON.stringify(statement.groupBy.map(g => resolveOperand({ type: 'column', name: g }, row)));
|
|
1869
|
+
const key = JSON.stringify(statement.groupBy.map(g => resolveOperand({ type: 'column', name: g }, row, this.ctx)));
|
|
1412
1870
|
if (!groups.has(key)) groups.set(key, []);
|
|
1413
1871
|
groups.get(key).push(row);
|
|
1414
1872
|
}
|
|
@@ -1443,8 +1901,10 @@ class SQLExecutor {
|
|
|
1443
1901
|
const seen = new Set();
|
|
1444
1902
|
rows = rows.filter(r => {
|
|
1445
1903
|
const key = JSON.stringify(statement.columns.map(c => {
|
|
1904
|
+
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
1905
|
+
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
1446
1906
|
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
|
|
1447
|
-
return resolveOperand(
|
|
1907
|
+
return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
|
|
1448
1908
|
}));
|
|
1449
1909
|
if (seen.has(key)) return false;
|
|
1450
1910
|
seen.add(key);
|
|
@@ -1453,33 +1913,24 @@ class SQLExecutor {
|
|
|
1453
1913
|
}
|
|
1454
1914
|
|
|
1455
1915
|
// 聚合输出(无 GROUP BY 时)
|
|
1456
|
-
if (statement.
|
|
1457
|
-
const
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1916
|
+
if (!statement.groupBy && statement.columns.some(c => c.aggregate)) {
|
|
1917
|
+
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
1918
|
+
const valueOf = (c) => {
|
|
1919
|
+
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
|
|
1920
|
+
if (c.scalar) return resolveOperand(c.scalar, rows[0] || {}, this.ctx);
|
|
1921
|
+
if (c.caseExpr) return resolveOperand(c.caseExpr, rows[0] || {}, this.ctx);
|
|
1922
|
+
if (c.expr !== null && c.expr !== '*') return rows[0] ? resolveOperand({ type: 'column', name: c.expr }, rows[0]) : null;
|
|
1923
|
+
return null;
|
|
1924
|
+
};
|
|
1925
|
+
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: [[...statement.columns.map(valueOf)]], aggregate: statement.aggregate };
|
|
1462
1926
|
}
|
|
1463
1927
|
|
|
1464
1928
|
// GROUP BY 输出:每组的列(含聚合列)
|
|
1465
1929
|
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
|
-
});
|
|
1930
|
+
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
1475
1931
|
const mapped = rows.map(r => {
|
|
1476
1932
|
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
|
-
});
|
|
1933
|
+
return statement.columns.map(c => scalarColumnValue(c, r, { _aggValue: this._aggValue.bind(this), group }));
|
|
1483
1934
|
});
|
|
1484
1935
|
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: mapped, raw: rows };
|
|
1485
1936
|
}
|
|
@@ -1487,8 +1938,8 @@ class SQLExecutor {
|
|
|
1487
1938
|
if (statement.orderBy) {
|
|
1488
1939
|
const cmp = (a, b) => {
|
|
1489
1940
|
for (const o of statement.orderBy) {
|
|
1490
|
-
const av = resolveOperand({ type: 'column', name: o.column }, a);
|
|
1491
|
-
const bv = resolveOperand({ type: 'column', name: o.column }, b);
|
|
1941
|
+
const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
|
|
1942
|
+
const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
|
|
1492
1943
|
if (av === bv || (av === undefined && bv === undefined)) continue;
|
|
1493
1944
|
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
1494
1945
|
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
@@ -1515,18 +1966,13 @@ class SQLExecutor {
|
|
|
1515
1966
|
return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
|
|
1516
1967
|
}
|
|
1517
1968
|
|
|
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
|
-
});
|
|
1969
|
+
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
1525
1970
|
const mapped = rows.map(r => statement.columns.map(c => {
|
|
1971
|
+
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
1526
1972
|
if (c.expr === '*') return null;
|
|
1527
1973
|
if (c.literal !== undefined) return c.literal;
|
|
1528
1974
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
1529
|
-
return resolveOperand({ type: 'column', name: c.expr }, r);
|
|
1975
|
+
return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
|
|
1530
1976
|
}));
|
|
1531
1977
|
return { ok: true, type: 'select', table: tableName, columns: cols, rows: mapped, raw: rows };
|
|
1532
1978
|
}
|
|
@@ -1696,7 +2142,7 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
1696
2142
|
if (dangerous) throw new Error(`SQL statement blocked by security policy: ${dangerous}`);
|
|
1697
2143
|
}
|
|
1698
2144
|
}
|
|
1699
|
-
const executor = new SQLExecutor(engine);
|
|
2145
|
+
const executor = new SQLExecutor(engine, opts.session ? { session: opts.session } : null);
|
|
1700
2146
|
const results = [];
|
|
1701
2147
|
for (const stmtSql of statements) {
|
|
1702
2148
|
const stmt = parseSQL(stmtSql);
|