diff --git a/.gitignore b/.gitignore index c1119a3..1a8c027 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ package-lock.json *.log *.swp dist/parser/sqlParser.js +.vscode/ diff --git a/.travis.yml b/.travis.yml index 4ba6f27..5d6b173 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,3 @@ node_js: - "v6.9.1" - "v8.11.3" - "v10.7.0" -scripts: { - "test": "npm run test:build" - } diff --git a/CHANGELOG b/CHANGELOG index 73b22bb..dc5dd0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,2 +1,13 @@ 1.0.6 add MIT license. 1.0.7 fix alias for identifier #16 , pr: #17 +1.0.8 support union grammar follow: https://dev.mysql.com/doc/refman/8.0/en/union.html +1.0.9 fix stringify having #29 , pr: #28 +1.1.0 hotfix: Remove unused import +1.1.1 fix stringify having #29 , pr: #28 +1.2.0 fix typo "refrence" to "reference" #24 +1.2.1 fix stringify having #29 , pr: #28 +1.2.2 feat: add support for "`" quoted alias, pr: #33 +1.3.0 fix tableFactor alias bug. AST changed in tableFactor. #34 +1.4.0 fix bug `using ' & " for column alias?` #40 #44 +1.4.1 hogfix "support quoted alias: multiple alias and orderby support" +1.5.0 support feature placeholder. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ca658ba --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +publish: test + @npm publish + +test: + @npm test + +test-with-log: + @DEBUG=js-sql-parser npm test + +.PHONY: publish test diff --git a/README.md b/README.md index 642fa81..e113d48 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,18 @@ sql grammar follows https://dev.mysql.com/doc/refman/5.7/en/select.html +## news + +- Unicode extended char support for column name or alias & Function call in `table_factor` since v1.6.0 [#58](https://github.com/JavaScriptor/js-sql-parser/pull/58), [#60](https://github.com/JavaScriptor/js-sql-parser/issues/60) +- Support feature `PlaceHolder like ${param}` since v1.5.0 [#43](https://github.com/JavaScriptor/js-sql-parser/pull/43) +- Fix bug `using ' & " for column alias?` since v1.4.1 [#40](https://github.com/JavaScriptor/js-sql-parser/issues/40), [#44](https://github.com/JavaScriptor/js-sql-parser/issues/44) +- Fix bug tableFactor alias since v1.3.0 [#34](https://github.com/JavaScriptor/js-sql-parser/issues/34) +- Add support for "`" quoted alias since v1.2.2. [#33](https://github.com/JavaScriptor/js-sql-parser/issues/33) +- Fix bug stringify keyword `having` since v1.2.1. [#29](https://github.com/JavaScriptor/js-sql-parser/issues/29) +- Typo 'refrence' has been fixed to 'reference' since v1.2.0. + +for more changes see [CHANGELOG](./CHANGELOG) + ## commonjs usage `npm install --save js-sql-parser` @@ -25,6 +37,18 @@ console.log(parser.stringify(ast)); // SELECT foo FROM bar ``` +```js +// placeholder test +const parser = require('js-sql-parser'); +const ast = parser.parse('select ${a} as a'); + +ast['value']['selectItems']['value'][0]['value'] = "'value'"; +console.log(parser.stringify(ast)); +// SELECT 'value' AS a +``` + +Note: PlaceHolder is an `literal` value but not an `identifier`. Table_name / column_name / function_name are `identifier` thus should NOT be placed with placeholder. + ## script tag ```js @@ -50,10 +74,6 @@ var sql = sqlParser.stringify(ast); - intervalexpr: Date INTERVAL keyword. // to support - into outfile: INTO OUTFILE keyword. // to support -## TODO - -- ${value} like value place holder support. - ## Build - Run `npm run build` to build the distributable. diff --git a/package.json b/package.json index 07942c0..bd3083b 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,14 @@ { "name": "js-sql-parser", - "version": "1.0.7", + "version": "1.6.0", "description": "", "main": "./dist/parser/sqlParser.js", "scripts": { "build": "jison -m js ./src/sqlParser.jison -o ./dist/parser/sqlParser.js && npm run build-concat", - "build-concat": "cat src/stringify.js src/suffix.js >> ./dist/parser/sqlParser.js", - "postbuild": "npm run test:build", - "test": "npm run build", + "build-concat": "minicat src/stringify.js src/suffix.js >> dist/parser/sqlParser.js", + "test": "npm run build && npm run test:build", "test:all": "mocha --require babel-register", - "test:build": "mocha --require babel-register 'test/*.test.js'", + "test:build": "mocha --require babel-register test/*.test.js", "test:benchmark": "mocha --require babel-register test/benchmark.js" }, "repository": { @@ -33,6 +32,7 @@ "concat": "^1.0.3", "debug": "^3.1.0", "jison": "^0.4.17", + "minicat": "^1.0.0", "mocha": "^3.2.0" } } diff --git a/src/sqlParser.jison b/src/sqlParser.jison index 40b0631..271c94a 100644 --- a/src/sqlParser.jison +++ b/src/sqlParser.jison @@ -10,10 +10,11 @@ [-][-]\s.*\n /* skip sql comments */ [#]\s.*\n /* skip sql comments */ \s+ /* skip whitespace */ - -[`][a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*[`] return 'IDENTIFIER' -[\w]+[\u4e00-\u9fa5]+[0-9a-zA-Z_\u4e00-\u9fa5]* return 'IDENTIFIER' -[\u4e00-\u9fa5][0-9a-zA-Z_\u4e00-\u9fa5]* return 'IDENTIFIER' + +[$][{](.+?)[}] return 'PLACE_HOLDER' +[`][a-zA-Z0-9_\u0080-\uFFFF]*[`] return 'IDENTIFIER' +[\w]+[\u0080-\uFFFF]+[0-9a-zA-Z_\u0080-\uFFFF]* return 'IDENTIFIER' +[\u0080-\uFFFF][0-9a-zA-Z_\u0080-\uFFFF]* return 'IDENTIFIER' SELECT return 'SELECT' ALL return 'ALL' ANY return 'ANY' @@ -89,6 +90,7 @@ SHARE return 'SHARE' MODE return 'MODE' OJ return 'OJ' LIMIT return 'LIMIT' +UNION return 'UNION' "," return ',' "=" return '=' @@ -116,17 +118,19 @@ LIMIT return 'LIMIT' "{" return '{' "}" return '}' ";" return ';' - + ['](\\.|[^'])*['] return 'STRING' ["](\\.|[^"])*["] return 'STRING' [0][x][0-9a-fA-F]+ return 'HEX_NUMERIC' [-]?[0-9]+(\.[0-9]+)? return 'NUMERIC' [-]?[0-9]+(\.[0-9]+)?[eE][-][0-9]+(\.[0-9]+)? return 'EXPONENT_NUMERIC' -[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]* return 'IDENTIFIER' +[a-zA-Z_\u0080-\uFFFF][a-zA-Z0-9_\u0080-\uFFFF]* return 'IDENTIFIER' \. return 'DOT' -['"][a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*["'] return 'QUOTED_IDENTIFIER' - +["][a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*["] return 'STRING' +['][a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*['] return 'STRING' +([`])(?:(?=(\\?))\2.)*?\1 return 'IDENTIFIER' + <> return 'EOF' . return 'INVALID' @@ -159,12 +163,36 @@ LIMIT return 'LIMIT' %% /* language grammar */ main - : selectClause EOF { return {nodeType: 'Main', value: $1}; } - | selectClause ';' EOF { return {nodeType: 'Main', value: $1, hasSemicolon: true}; } + : selectClause semicolonOpt EOF { return {nodeType: 'Main', value: $1, hasSemicolon: $2}; } + | unionClause semicolonOpt EOF { return {nodeType: 'Main', value: $1, hasSemicolon: $2}; } + ; + +semicolonOpt + : ';' { $$ = true } + | { $$ = false } + ; + +unionClause + : unionClauseNotParenthesized { $$ = $1 } + | unionClauseParenthesized order_by_opt limit_opt { $$ = $1, $$.orderBy = $2, $$.limit = $3; } + ; + +unionClauseParenthesized + : selectClauseParenthesized UNION distinctOpt selectClauseParenthesized { $$ = { type: 'Union', left: $1, distinctOpt: $3, right: $4 }; } + | selectClauseParenthesized UNION distinctOpt unionClauseParenthesized { $$ = { type: 'Union', left: $1, distinctOpt: $3, right: $4 }; } + ; + +selectClauseParenthesized + : '(' selectClause ')' { $$ = { type: 'SelectParenthesized', value: $2 }; } + ; + +unionClauseNotParenthesized + : selectClause UNION distinctOpt selectClause { $$ = { type: 'Union', left: $1, distinctOpt: $3, right: $4 } } + | selectClause UNION distinctOpt unionClauseNotParenthesized { $$ = { type: 'Union', left: $1, distinctOpt: $3, right: $4 } } ; selectClause - : SELECT + : SELECT distinctOpt highPriorityOpt maxStateMentTimeOpt @@ -203,7 +231,7 @@ selectClause ; distinctOpt - : ALL { $$ = $1 } + : ALL { $$ = $1 } | DISTINCT { $$ = $1 } | DISTINCTROW { $$ = $1 } | { $$ = null } @@ -254,11 +282,11 @@ selectExprAliasOpt : { $$ = {alias: null, hasAs: null} } | AS IDENTIFIER { $$ = {alias: $2, hasAs: true} } | IDENTIFIER { $$ = {alias: $1, hasAs: false} } + | AS STRING { $$ = {alias: $2, hasAs: true} } + | STRING { $$ = {alias: $2, hasAs: false} } ; - string - : QUOTED_IDENTIFIER { $$ = { type: 'String', value: $1 } } - | STRING { $$ = { type: 'String', value: $1 } } + : STRING { $$ = { type: 'String', value: $1 } } ; number : NUMERIC { $$ = { type: 'Number', value: $1 } } @@ -277,6 +305,7 @@ literal | number { $$ = $1 } | boolean { $$ = $1 } | null { $$ = $1 } + | place_holder { $$ = $1 } ; function_call : IDENTIFIER '(' function_call_param_list ')' { $$ = {type: 'FunctionCall', name: $1, params: $3} } @@ -336,7 +365,7 @@ simple_expr ; bit_expr : simple_expr { $$ = $1 } - | bit_expr '|' bit_expr { $$ = { type: 'BitExpression', operator: '|', left: $1, right: $3 } } + | bit_expr '|' bit_expr { $$ = { type: 'BitExpression', operator: '|', left: $1, right: $3 } } | bit_expr '&' bit_expr { $$ = { type: 'BitExpression', operator: '&', left: $1, right: $3 } } | bit_expr '<<' bit_expr { $$ = { type: 'BitExpression', operator: '<<', left: $1, right: $3 } } | bit_expr '>>' bit_expr { $$ = { type: 'BitExpression', operator: '>>', left: $1, right: $3 } } @@ -465,16 +494,16 @@ for_update_lock_in_share_mode_opt ; selectDataSetOpt : { $$ = {} } - | FROM table_refrences partitionOpt where_opt group_by_opt having_opt order_by_opt limit_opt procedure_opt for_update_lock_in_share_mode_opt + | FROM table_references partitionOpt where_opt group_by_opt having_opt order_by_opt limit_opt procedure_opt for_update_lock_in_share_mode_opt { $$ = { from: $2, partition: $3, where: $4, groupBy: $5, having: $6, orderBy: $7, limit: $8, procedure: $9, updateLockMode: $10 } } ; -table_refrences - : escaped_table_reference { $$ = { type: 'TableRefrences', value: [ $1 ] } } - | table_refrences ',' escaped_table_reference %prec TABLE_REF_COMMA { $$ = $1; $1.value.push($3); } +table_references + : escaped_table_reference { $$ = { type: 'TableReferences', value: [ $1 ] } } + | table_references ',' escaped_table_reference %prec TABLE_REF_COMMA { $$ = $1; $1.value.push($3); } ; escaped_table_reference - : table_reference { $$ = { type: 'TableRefrence', value: $1 } } - | '{' OJ table_reference '}' { $$ = { type: 'TableRefrence', hasOj: true, value: $3 } } + : table_reference { $$ = { type: 'TableReference', value: $1 } } + | '{' OJ table_reference '}' { $$ = { type: 'TableReference', hasOj: true, value: $3 } } ; join_inner_cross : { $$ = null } @@ -557,6 +586,10 @@ index_hint ; table_factor : identifier partitionOpt aliasOpt index_hint_list_opt { $$ = { type: 'TableFactor', value: $1, partition: $2, alias: $3.alias, hasAs: $3.hasAs, indexHintOpt: $4 } } - | '(' selectClause ')' aliasOpt { $$ = { type: 'SubQuery', value: $2, alias: $4.alias, hasAs: $4.hasAs } } - | '(' table_refrences ')' { $$ = $2; $$.hasParentheses = true } + | '(' selectClause ')' aliasOpt { $$ = { type: 'TableFactor', value: { type: 'SubQuery', value: $2 }, alias: $4.alias, hasAs: $4.hasAs} } + | '(' table_references ')' { $$ = $2; $$.hasParentheses = true } + | function_call aliasOpt index_hint_list_opt { $$ = { type: 'TableFactor', value: $1, alias: $2.alias, hasAs: $2.hasAs, indexHintOpt: $3 } } ; +place_holder + : PLACE_HOLDER { $$ = { type: 'PlaceHolder', value: $1, param: $1.slice(2, -1)} } + ; \ No newline at end of file diff --git a/src/stringify.js b/src/stringify.js index fd1d289..4679850 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -6,13 +6,13 @@ function Sql() { this.buffer = ''; } -sqlParser.stringify = function (ast) { +sqlParser.stringify = function(ast) { var sql = new Sql(); sql.travelMain(ast); return sql.buffer; -} +}; -Sql.prototype.travel = function (ast) { +Sql.prototype.travel = function(ast) { if (!ast) return; if (typeof ast === 'string') { @@ -21,7 +21,7 @@ Sql.prototype.travel = function (ast) { var processor = this['travel' + ast.type]; processor.call(this, ast); -} +}; var noSuffixFlag = false; Sql.prototype.appendKeyword = function(keyword, noPrefix, noSuffix) { @@ -38,7 +38,7 @@ Sql.prototype.appendKeyword = function(keyword, noPrefix, noSuffix) { if (noSuffix) { noSuffixFlag = true; } -} +}; Sql.prototype.append = function(word, noPrefix, noSuffix) { if (noSuffixFlag) { noPrefix = true; @@ -53,15 +53,15 @@ Sql.prototype.append = function(word, noPrefix, noSuffix) { if (noSuffix) { noSuffixFlag = true; } -} +}; Sql.prototype.travelMain = function(ast) { - this.travelSelect(ast.value); + this.travel(ast.value); if (ast.hasSemicolon) { this.append(';', true); } -} +}; Sql.prototype.travelSelect = function(ast) { - this.appendKeyword('select', true); + this.appendKeyword('select'); if (ast.distinctOpt) { this.appendKeyword(ast.distinctOpt); } @@ -107,6 +107,7 @@ Sql.prototype.travelSelect = function(ast) { this.travel(ast.groupBy); } if (ast.having) { + this.appendKeyword('having'); this.travel(ast.having); } if (ast.orderBy) { @@ -122,8 +123,8 @@ Sql.prototype.travelSelect = function(ast) { if (ast.updateLockMode) { this.appendKeyword(ast.updateLockMode); } -} -Sql.prototype.travelSelectExpr = function (ast) { +}; +Sql.prototype.travelSelectExpr = function(ast) { var exprList = ast.value; for (var i = 0; i < exprList.length; i++) { if (typeof ast === 'string') { @@ -141,69 +142,69 @@ Sql.prototype.travelSelectExpr = function (ast) { this.append(',', true); } } -} -Sql.prototype.travelIsExpression = function (ast) { +}; +Sql.prototype.travelIsExpression = function(ast) { this.travel(ast.left); this.appendKeyword('in'); if (ast.hasNot) { this.appendKeyword('not'); } this.append(ast.right); -} -Sql.prototype.travelNotExpression = function (ast) { +}; +Sql.prototype.travelNotExpression = function(ast) { this.appendKeyword('not'); this.travel(ast.value); -} -Sql.prototype.travelOrExpression = -Sql.prototype.travelAndExpression = -Sql.prototype.travelXORExpression = function (ast) { +}; +Sql.prototype.travelOrExpression = Sql.prototype.travelAndExpression = Sql.prototype.travelXORExpression = function( + ast +) { this.travel(ast.left); this.appendKeyword(ast.operator); this.travel(ast.right); -} -Sql.prototype.travelNull = -Sql.prototype.travelBoolean = -Sql.prototype.travelBooleanExtra = function (ast) { +}; +Sql.prototype.travelNull = Sql.prototype.travelBoolean = Sql.prototype.travelBooleanExtra = function( + ast +) { this.appendKeyword(ast.value); -} -Sql.prototype.travelNumber = function (ast) { +}; +Sql.prototype.travelNumber = function(ast) { this.append(ast.value); -} -Sql.prototype.travelString = function (ast) { +}; +Sql.prototype.travelString = function(ast) { this.append(ast.value); -} -Sql.prototype.travelFunctionCall = function (ast) { +}; +Sql.prototype.travelFunctionCall = function(ast) { this.append(ast.name); this.append('(', true, true); var params = ast.params; for (var i = 0; i < params.length; i++) { var param = params[i]; this.travel(param); - if (i !== params.length -1) { + if (i !== params.length - 1) { this.append(',', true); } } this.append(')', true); -} -Sql.prototype.travelFunctionCallParam = function (ast) { +}; +Sql.prototype.travelFunctionCallParam = function(ast) { if (ast.distinctOpt) { this.appendKeyword(ast.distinctOpt); } this.travel(ast.value); -} -Sql.prototype.travelIdentifier = function (ast) { +}; +Sql.prototype.travelIdentifier = function(ast) { this.append(ast.value); -} -Sql.prototype.travelIdentifierList = function (ast) { +}; +Sql.prototype.travelIdentifierList = function(ast) { var list = ast.value; for (var i = 0; i < list.length; i++) { this.travel(list[i]); - if (i !== list.length -1) { + if (i !== list.length - 1) { this.append(',', true); } } -} -Sql.prototype.travelWhenThenList = function (ast) { +}; +Sql.prototype.travelWhenThenList = function(ast) { var list = ast.value; for (var i = 0; i < list.length; i++) { this.appendKeyword('when'); @@ -211,8 +212,8 @@ Sql.prototype.travelWhenThenList = function (ast) { this.appendKeyword('then'); this.travel(list[i].then); } -} -Sql.prototype.travelCaseWhen = function (ast) { +}; +Sql.prototype.travelCaseWhen = function(ast) { this.appendKeyword('case'); if (ast.caseExprOpt) { this.travel(ast.caseExprOpt); @@ -223,39 +224,39 @@ Sql.prototype.travelCaseWhen = function (ast) { this.travel(ast.else); } this.appendKeyword('end'); -} -Sql.prototype.travelPrefix = function (ast) { +}; +Sql.prototype.travelPrefix = function(ast) { this.appendKeyword(ast.prefix); this.travel(ast.value); -} -Sql.prototype.travelSimpleExprParentheses = function (ast) { +}; +Sql.prototype.travelSimpleExprParentheses = function(ast) { if (ast.hasRow) { this.appendKeyword('row'); } this.append('(', false, true); this.travel(ast.value); this.append(')', true); -} -Sql.prototype.travelSubQuery = function (ast) { +}; +Sql.prototype.travelSubQuery = function(ast) { if (ast.hasExists) { this.appendKeyword('exists'); } this.append('(', false, true); this.travel(ast.value); this.append(')', true); -} -Sql.prototype.travelIdentifierExpr = function (ast) { +}; +Sql.prototype.travelIdentifierExpr = function(ast) { this.append('{'); this.travel(ast.identifier); this.travel(ast.value); this.append('}'); -} -Sql.prototype.travelBitExpression = function (ast) { +}; +Sql.prototype.travelBitExpression = function(ast) { this.travel(ast.left); this.appendKeyword(ast.operator); this.travel(ast.right); -} -Sql.prototype.travelInSubQueryPredicate = function (ast) { +}; +Sql.prototype.travelInSubQueryPredicate = function(ast) { this.travel(ast.left); if (ast.hasNot) { this.appendKeyword('not'); @@ -264,8 +265,8 @@ Sql.prototype.travelInSubQueryPredicate = function (ast) { this.append('(', false, true); this.travel(ast.right); this.append(')'); -} -Sql.prototype.travelInExpressionListPredicate = function (ast) { +}; +Sql.prototype.travelInExpressionListPredicate = function(ast) { this.travel(ast.left); if (ast.hasNot) { this.appendKeyword('not'); @@ -274,8 +275,8 @@ Sql.prototype.travelInExpressionListPredicate = function (ast) { this.append('(', false, true); this.travel(ast.right); this.append(')'); -} -Sql.prototype.travelBetweenPredicate = function (ast) { +}; +Sql.prototype.travelBetweenPredicate = function(ast) { this.travel(ast.left); if (ast.hasNot) { this.appendKeyword('not'); @@ -284,14 +285,14 @@ Sql.prototype.travelBetweenPredicate = function (ast) { this.travel(ast.right.left); this.appendKeyword('and'); this.travel(ast.right.right); -} -Sql.prototype.travelSoundsLikePredicate = function (ast) { +}; +Sql.prototype.travelSoundsLikePredicate = function(ast) { this.travel(ast.left); this.appendKeyword('sounds'); this.appendKeyword('like'); this.travel(ast.right); -} -Sql.prototype.travelLikePredicate = function (ast) { +}; +Sql.prototype.travelLikePredicate = function(ast) { this.travel(ast.left); if (ast.hasNot) { this.appendKeyword('not'); @@ -299,40 +300,40 @@ Sql.prototype.travelLikePredicate = function (ast) { this.appendKeyword('like'); this.travel(ast.right); if (ast.escape) { - this.appendKeyword('escape') + this.appendKeyword('escape'); this.travel(ast.escape); } -} -Sql.prototype.travelRegexpPredicate = function (ast) { +}; +Sql.prototype.travelRegexpPredicate = function(ast) { this.travel(ast.left); if (ast.hasNot) { this.appendKeyword('not'); } this.appendKeyword('regexp'); this.travel(ast.right); -} -Sql.prototype.travelIsNullBooleanPrimary = function (ast) { +}; +Sql.prototype.travelIsNullBooleanPrimary = function(ast) { this.travel(ast.value); this.appendKeyword('is'); if (ast.hasNot) { this.appendKeyword('not'); } this.appendKeyword('null'); -} -Sql.prototype.travelComparisonBooleanPrimary = function (ast) { +}; +Sql.prototype.travelComparisonBooleanPrimary = function(ast) { this.travel(ast.left); this.append(ast.operator); this.travel(ast.right); -} -Sql.prototype.travelComparisonSubQueryBooleanPrimary = function (ast) { +}; +Sql.prototype.travelComparisonSubQueryBooleanPrimary = function(ast) { this.travel(ast.left); this.append(ast.operator); this.appendKeyword(ast.subQueryOpt); this.append('(', false, true); this.travel(ast.right); this.append(')'); -} -Sql.prototype.travelExpressionList = function (ast) { +}; +Sql.prototype.travelExpressionList = function(ast) { var list = ast.value; for (var i = 0; i < list.length; i++) { this.travel(list[i]); @@ -340,8 +341,8 @@ Sql.prototype.travelExpressionList = function (ast) { this.append(',', true); } } -} -Sql.prototype.travelGroupBy = function (ast) { +}; +Sql.prototype.travelGroupBy = function(ast) { this.appendKeyword('group by'); var list = ast.value; for (var i = 0; i < list.length; i++) { @@ -350,8 +351,8 @@ Sql.prototype.travelGroupBy = function (ast) { this.append(',', true); } } -} -Sql.prototype.travelOrderBy = function (ast) { +}; +Sql.prototype.travelOrderBy = function(ast) { this.appendKeyword('order by'); var list = ast.value; for (var i = 0; i < list.length; i++) { @@ -363,14 +364,14 @@ Sql.prototype.travelOrderBy = function (ast) { if (ast.rollUp) { this.appendKeyword('with rollup'); } -} -Sql.prototype.travelGroupByOrderByItem = function (ast) { +}; +Sql.prototype.travelGroupByOrderByItem = function(ast) { this.travel(ast.value); if (ast.sortOpt) { this.appendKeyword(ast.sortOpt); } -} -Sql.prototype.travelLimit = function (ast) { +}; +Sql.prototype.travelLimit = function(ast) { this.appendKeyword('limit'); var list = ast.value; if (list.length === 1) { @@ -386,10 +387,10 @@ Sql.prototype.travelLimit = function (ast) { this.append(list[1]); } } -} -Sql.prototype.travelTableRefrences = function (ast) { +}; +Sql.prototype.travelTableReferences = function(ast) { var list = ast.value; - if (ast.TableRefrences) { + if (ast.TableReferences) { this.append('(', false, true); } for (var i = 0; i < list.length; i++) { @@ -398,11 +399,11 @@ Sql.prototype.travelTableRefrences = function (ast) { this.append(',', true); } } - if (ast.TableRefrences) { + if (ast.TableReferences) { this.append(')'); } -} -Sql.prototype.travelTableRefrence = function (ast) { +}; +Sql.prototype.travelTableReference = function(ast) { if (ast.hasOj) { this.append('{'); this.appendKeyword('oj'); @@ -411,8 +412,8 @@ Sql.prototype.travelTableRefrence = function (ast) { } else { this.travel(ast.value); } -} -Sql.prototype.travelInnerCrossJoinTable = function (ast) { +}; +Sql.prototype.travelInnerCrossJoinTable = function(ast) { this.travel(ast.left); if (ast.innerCrossOpt) { this.appendKeyword(ast.innerCrossOpt); @@ -422,14 +423,14 @@ Sql.prototype.travelInnerCrossJoinTable = function (ast) { if (ast.condition) { this.travel(ast.condition); } -} -Sql.prototype.travelStraightJoinTable = function (ast) { +}; +Sql.prototype.travelStraightJoinTable = function(ast) { this.travel(ast.left); this.appendKeyword('straight_join'); this.travel(ast.right); this.travel(ast.condition); -} -Sql.prototype.travelLeftRightJoinTable = function (ast) { +}; +Sql.prototype.travelLeftRightJoinTable = function(ast) { this.travel(ast.left); this.appendKeyword(ast.leftRight); if (ast.outOpt) { @@ -438,8 +439,8 @@ Sql.prototype.travelLeftRightJoinTable = function (ast) { this.appendKeyword('join'); this.travel(ast.right); this.travel(ast.condition); -} -Sql.prototype.travelNaturalJoinTable = function (ast) { +}; +Sql.prototype.travelNaturalJoinTable = function(ast) { this.travel(ast.left); this.appendKeyword('natural'); if (ast.leftRight) { @@ -450,18 +451,18 @@ Sql.prototype.travelNaturalJoinTable = function (ast) { } this.appendKeyword('join'); this.travel(ast.right); -} -Sql.prototype.travelOnJoinCondition = function (ast) { +}; +Sql.prototype.travelOnJoinCondition = function(ast) { this.appendKeyword('on'); this.travel(ast.value); -} -Sql.prototype.travelUsingJoinCondition = function (ast) { +}; +Sql.prototype.travelUsingJoinCondition = function(ast) { this.appendKeyword('using'); this.appendKeyword('(', false, true); this.travel(ast.value); this.appendKeyword(')'); -} -Sql.prototype.travelPartitions = function (ast) { +}; +Sql.prototype.travelPartitions = function(ast) { this.appendKeyword('partition'); this.appendKeyword('(', false, true); var list = ast.value; @@ -472,12 +473,12 @@ Sql.prototype.travelPartitions = function (ast) { } } this.appendKeyword(')'); -} -Sql.prototype.travelForOptIndexHint = function (ast) { +}; +Sql.prototype.travelForOptIndexHint = function(ast) { this.appendKeyword('for'); this.appendKeyword(ast.value); -} -Sql.prototype.travelIndexList = function (ast) { +}; +Sql.prototype.travelIndexList = function(ast) { var list = ast.value; for (var i = 0; i < list.length; i++) { this.travel(list[i]); @@ -485,8 +486,8 @@ Sql.prototype.travelIndexList = function (ast) { this.append(',', true); } } -} -Sql.prototype.travelUseIndexHint = function (ast) { +}; +Sql.prototype.travelUseIndexHint = function(ast) { this.appendKeyword('use'); this.appendKeyword(ast.indexOrKey); if (ast.forOpt) { @@ -497,8 +498,8 @@ Sql.prototype.travelUseIndexHint = function (ast) { this.travel(ast.value); } this.appendKeyword(')'); -} -Sql.prototype.travelIgnoreIndexHint = function (ast) { +}; +Sql.prototype.travelIgnoreIndexHint = function(ast) { this.appendKeyword('ignore'); this.appendKeyword(ast.indexOrKey); if (ast.forOpt) { @@ -509,8 +510,8 @@ Sql.prototype.travelIgnoreIndexHint = function (ast) { this.travel(ast.value); } this.appendKeyword(')'); -} -Sql.prototype.travelForceIndexHint = function (ast) { +}; +Sql.prototype.travelForceIndexHint = function(ast) { this.appendKeyword('force'); this.appendKeyword(ast.indexOrKey); if (ast.forOpt) { @@ -521,8 +522,8 @@ Sql.prototype.travelForceIndexHint = function (ast) { this.travel(ast.value); } this.appendKeyword(')'); -} -Sql.prototype.travelTableFactor = function (ast) { +}; +Sql.prototype.travelTableFactor = function(ast) { this.travel(ast.value); if (ast.partition) { this.travel(ast.partition); @@ -536,4 +537,22 @@ Sql.prototype.travelTableFactor = function (ast) { if (ast.indexHintOpt) { this.travel(ast.indexHintOpt); } -} +}; +Sql.prototype.travelUnion = function(ast) { + this.travel(ast.left); + this.appendKeyword('UNION'); + if (ast.distinctOpt) { + this.appendKeyword(ast.distinctOpt); + } + this.travel(ast.right); +}; +Sql.prototype.travelSelectParenthesized = function(ast) { + this.appendKeyword('('); + this.travel(ast.value); + this.appendKeyword(')'); +}; +Sql.prototype.travelPlaceHolder = function (ast) { + if (ast.value) { + this.travel(ast.value); + } +}; \ No newline at end of file diff --git a/test/main.test.js b/test/main.test.js index b4114ba..4bf50b6 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -2,24 +2,26 @@ const debug = require('debug')('js-sql-parser'); const parser = require('../'); +const assert = require('assert'); const testParser = function (sql) { let firstAst = parser.parse(sql); + debug(JSON.stringify(firstAst, null, 2)); let firstSql = parser.stringify(firstAst); + debug(firstSql); let secondAst = parser.parse(firstSql); + debug(parser.stringify(secondAst)); let secondSql = parser.stringify(secondAst); + debug(JSON.stringify(secondAst, null, 2)); if (firstSql !== secondSql) { console.log('firstSql', firstSql); console.log('secondSql', secondSql); - throw 'err firstSql don\'t equals secondSql. '; + throw "err firstSql don't equals secondSql. "; } - debug(JSON.stringify(secondAst, null, 2)); - debug(parser.stringify(secondAst)); - return secondAst; -} +}; describe('select grammar support', function () { it('test0', function () { @@ -57,17 +59,17 @@ describe('select grammar support', function () { `); }); - it ('test5', function () { + it('test5', function () { testParser('select function(), function(1, "sd", 0x1F)'); }); - it ('test6 unicode', function () { + it('test6 unicode', function () { testParser(` select in中文 from tags `); }); - it ('test7', function () { + it('test7', function () { testParser(` SELECT DISTINCT high_priority MAX_STATEMENT_TIME=1 STRAIGHT_JOIN SQL_SMALL_RESULT SQL_BIG_RESULT SQL_BUFFER_RESULT SQL_CACHE SQL_CALC_FOUND_ROWS fm_customer.lname AS name1, @@ -88,7 +90,7 @@ describe('select grammar support', function () { `); }); - it ('test8', function () { + it('test8', function () { testParser(` SELECT P1.PAYMENTNO, P1.AMOUNT, (P1.AMOUNT * 100) / SUM(P2.AMOUNT) @@ -98,7 +100,7 @@ describe('select grammar support', function () { `); }); - it ('test9', function () { + it('test9', function () { testParser(` SELECT PLAYERS.PLAYERNO, NAME, (SELECT COUNT(*) @@ -113,7 +115,7 @@ describe('select grammar support', function () { `); }); - it ('test10', function () { + it('test10', function () { testParser(` SELECT rd.*, rd.rd_numberofrooms - ( SELECT SUM(rn.reservation_numberofrooms) AS count_reserve_room @@ -147,11 +149,11 @@ describe('select grammar support', function () { `); }); - it ('test11 SELECT `LEFT`(a, 3) FROM b support.', function () { + it('test11 SELECT `LEFT`(a, 3) FROM b support.', function () { testParser('SELECT `LEFT`(a, 3) FROM b'); }); - it ('test12', function () { + it('test12', function () { testParser(` select a.product_id, @@ -201,7 +203,7 @@ describe('select grammar support', function () { `); }); - it ('test13', function () { + it('test13', function () { testParser(` SELECT a.*, f.ORG_NAME DEPT_NAME, @@ -285,7 +287,7 @@ describe('select grammar support', function () { `); }); - it ('test14', function () { + it('test14', function () { testParser(` SELECT k.*, @@ -344,21 +346,126 @@ describe('select grammar support', function () { `); }); - it ('limit support.', function () { + it('test15', function () { + testParser(` + SELECT P1.PAYMENTNO, P1.AMOUNT, (P1.AMOUNT * 100) / SUM(P2.AMOUNT) + FROM PENALTIES AS P1, PENALTIES AS P2 + GROUP BY P1.PAYMENTNO, P1.AMOUNT + HAVING MAX(P2.AMOUNT) > 0 + `); + }); + + it('limit support.', function () { testParser('select a from b limit 2, 3'); }); - it ('fix not equal.', function () { + it('fix not equal.', function () { testParser('select a from b where a <> 1 limit 2, 3'); }); - it ('restore semicolon.', function () { + it('restore semicolon.', function () { testParser('select a from b limit 2;'); }); - it ('recognoce alias for sql-function calls in stringify function.', function () { + it('recognoce alias for sql-function calls in stringify function.', function () { testParser('SELECT COUNT(*) AS total, a b, b as c, c/2 d, d & e an FROM b'); }); -}); + it('union support, https://dev.mysql.com/doc/refman/8.0/en/union.html', function () { + testParser('select a from dual union select a from foo;'); + }); + + it('union Parenthesized support, https://dev.mysql.com/doc/refman/8.0/en/union.html', function () { + testParser('(select a from dual) union (select a from foo) order by a desc limit 100, 100;'); + }); + + it('union all support, https://dev.mysql.com/doc/refman/8.0/en/union.html', function () { + testParser('(select a from dual) union all (select a from foo) order by a limit 100'); + }); + + it('union distinct support, https://dev.mysql.com/doc/refman/8.0/en/union.html', function () { + testParser( + 'select a from dual order by a desc limit 1, 1 union distinct select a from foo order by a limit 1' + ); + }); + + it('support quoted alias', function () { + testParser('select a as `A-A` from b limit 2;'); + testParser('select a as `A#A` from b limit 2;'); + testParser('select a as `A?A` from b limit 2;'); + testParser('select a as `A/B` from b limit 2;'); + testParser('select a as `A.A` from b limit 2;'); + testParser('select a as `A|A` from b limit 2;'); + testParser('select a as `A A` from b limit 2;'); + }); + it('bugfix table alias', function () { + testParser(` + SELECT stime, A.names, B.names FROM ( + SELECT stime, names FROM iaas_data.iaas_d3c0d0681cc1900 + ) AS A LEFT JOIN ( + SELECT stime, names FROM iaas_data.iaas_1071f89feaa0e100 + ) AS B ON A.stime = B.stime + `); + }); + + it('bugfix table alias2', function () { + testParser('select a.* from a t1 join b t2 on t1.a = t2.a') + }); + it('place holder support', function() { + testParser( + "select sum(quota_value) value, busi_col2 as sh, ${a} as a, YEAR(now()) from der_quota_summary where table_ename = 'gshmyyszje_derivedidx' and cd = (select id from t1 where a = ${t1})" + ) + }); + it('place holder support2', function() { + testParser( + "select sum(quota_value) b, busi_col2 as sh, '${a}' as a, YEAR(now()) from der_quota_summary where table_ename = 'gshmyyszje_derivedidx' and cd = (select id from t1 where a = '${t1}')" + ) + }); + it('place holder support3', function() { + let firstAst = parser.parse('select ${a} as a'); + firstAst['value']['selectItems']['value'][0]['value'] = "'value'"; + let firstSql = parser.stringify(firstAst); + debug(JSON.stringify(firstAst, null, 2)); + assert.equal(firstSql.trim().toUpperCase(), "select 'value' as a".toUpperCase()); + testParser(firstSql); + }); + + it('support quoted alias: multiple alias and orderby support', function () { + testParser('select a as `A A`, b as `B B` from z'); + testParser('select a as `A A` from z order by `A A` desc'); + testParser('select a as `A A`, b as `B B` from z group by `A A`, `B B` order by `A A` desc'); + }); + + it('support double quoted alias', function () { + testParser('select a as "A A" from z'); + testParser('select a as "A A" from z order by "A A" desc'); + }); + + it('support quoted alias', function () { + testParser('select a as \'A A\' from z'); + testParser('select a as \'"A#A\' from z order by \'"A#A\' desc'); + }); + + it('test IDENTIFIER', function () { + testParser('select `aa#sfs`(a) as \'A A\' from z'); + }); + + it('Support unicode extended char (U+0080..U+FFFF) as column name or alias', function() { + testParser(`select + país, + MAX(produção) as maior_produção, + Ĉapelo, + Δάσος, + Молоко, + سلام, + かわいい + from table`) + }) + + it('#60 Call function in FROM clause', function() { + testParser(` + SELECT one.name, group_concat(j.value, ', ') FROM one, json_each(one.stringArray) AS j GROUP BY one.id + `) + }) +});