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/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
- this.expect('op', '*');
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: null, aggregate: 'COUNT' });
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.parseColumnRef();
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
- const col = this.parseColumnRef();
495
+ // / 常量 / 函数 / 算术表达式
496
+ const expr = this.parseScalar();
480
497
  let alias = null;
481
498
  if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
482
- columns.push({ expr: col, alias });
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.parseColumnRef();
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.type === 'value') return operand.value;
879
- const n = operand.name;
880
- if (n === undefined) return undefined;
881
- if (row[n] !== undefined) return row[n];
882
- if (n.includes('.')) {
883
- const k = n;
884
- if (row[k] !== undefined) return row[k];
885
- const col = n.slice(n.lastIndexOf('.') + 1);
886
- return row[col];
887
- }
888
- return undefined;
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 normalizeRow(row, schema) {
980
- if (row && typeof row === 'object' && row.fields && typeof row.fields === 'object') {
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
- if (schema[c].autoIncrement && (skipAuto || vals[vi] === undefined || vals[vi] === null)) {
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
- return { ok: true, type: 'showTables', columns: ['Tables'], rows: tables.map(t => [t]) };
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 values = rows.map(r => resolveOperand({ type: 'column', name: column }, r)).filter(v => v !== null && v !== undefined);
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(c.caseExpr || { type: 'column', name: c.expr }, r);
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 aggName = (c) => c.alias || (c.aggregate === 'COUNT' ? 'COUNT(*)' : c.aggregate + '(' + c.column + ')');
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);