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'
+
 <<EOF>>                                                           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
+    `)
+  })
+});
