content: /*
   * Copyright 1999-2012 Alibaba Group.
   *  
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *  
   *      http://www.apache.org/licenses/LICENSE-2.0
   *  
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  /**
   * (created at 2011-6-1)
   */
  package com.alibaba.cobar.parser.visitor;
  
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_FALSE;
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_NOT_FALSE;
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_NOT_NULL;
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_NOT_TRUE;
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_NOT_UNKNOWN;
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_NULL;
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_TRUE;
  import static com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression.IS_UNKNOWN;
  
  import java.util.List;
  import java.util.Map;
  
  import com.alibaba.cobar.parser.ast.ASTNode;
  import com.alibaba.cobar.parser.ast.expression.BinaryOperatorExpression;
  import com.alibaba.cobar.parser.ast.expression.Expression;
  import com.alibaba.cobar.parser.ast.expression.PolyadicOperatorExpression;
  import com.alibaba.cobar.parser.ast.expression.TernaryOperatorExpression;
  import com.alibaba.cobar.parser.ast.expression.UnaryOperatorExpression;
  import com.alibaba.cobar.parser.ast.expression.comparison.BetweenAndExpression;
  import com.alibaba.cobar.parser.ast.expression.comparison.ComparisionEqualsExpression;
  import com.alibaba.cobar.parser.ast.expression.comparison.ComparisionIsExpression;
  import com.alibaba.cobar.parser.ast.expression.comparison.ComparisionNullSafeEqualsExpression;
  import com.alibaba.cobar.parser.ast.expression.comparison.InExpression;
  import com.alibaba.cobar.parser.ast.expression.logical.LogicalAndExpression;
  import com.alibaba.cobar.parser.ast.expression.logical.LogicalOrExpression;
  import com.alibaba.cobar.parser.ast.expression.misc.InExpressionList;
  import com.alibaba.cobar.parser.ast.expression.misc.QueryExpression;
  import com.alibaba.cobar.parser.ast.expression.misc.UserExpression;
  import com.alibaba.cobar.parser.ast.expression.primary.CaseWhenOperatorExpression;
  import com.alibaba.cobar.parser.ast.expression.primary.DefaultValue;
  import com.alibaba.cobar.parser.ast.expression.primary.ExistsPrimary;
  import com.alibaba.cobar.parser.ast.expression.primary.Identifier;
  import com.alibaba.cobar.parser.ast.expression.primary.MatchExpression;
  import com.alibaba.cobar.parser.ast.expression.primary.ParamMarker;
  import com.alibaba.cobar.parser.ast.expression.primary.PlaceHolder;
  import com.alibaba.cobar.parser.ast.expression.primary.RowExpression;
  import com.alibaba.cobar.parser.ast.expression.primary.SysVarPrimary;
  import com.alibaba.cobar.parser.ast.expression.primary.UsrDefVarPrimary;
  import com.alibaba.cobar.parser.ast.expression.primary.VariableExpression;
  import com.alibaba.cobar.parser.ast.expression.primary.function.FunctionExpression;
  import com.alibaba.cobar.parser.ast.expression.primary.function.cast.Cast;
  import com.alibaba.cobar.parser.ast.expression.primary.function.cast.Convert;
  import com.alibaba.cobar.parser.ast.expression.primary.function.datetime.Extract;
  import com.alibaba.cobar.parser.ast.expression.primary.function.datetime.GetFormat;
  import com.alibaba.cobar.parser.ast.expression.primary.function.datetime.Timestampadd;
  import com.alibaba.cobar.parser.ast.expression.primary.function.datetime.Timestampdiff;
  import com.alibaba.cobar.parser.ast.expression.primary.function.groupby.Avg;
  import com.alibaba.cobar.parser.ast.expression.primary.function.groupby.Count;
  import com.alibaba.cobar.parser.ast.expression.primary.function.groupby.GroupConcat;
  import com.alibaba.cobar.parser.ast.expression.primary.function.groupby.Max;
  import com.alibaba.cobar.parser.ast.expression.primary.function.groupby.Min;
  import com.alibaba.cobar.parser.ast.expression.primary.function.groupby.Sum;
  import com.alibaba.cobar.parser.ast.expression.primary.function.string.Char;
  import com.alibaba.cobar.parser.ast.expression.primary.function.string.Trim;
  import com.alibaba.cobar.parser.ast.expression.primary.literal.IntervalPrimary;
  import com.alibaba.cobar.parser.ast.expression.primary.literal.LiteralBitField;
  import com.alibaba.cobar.parser.ast.expression.primary.literal.LiteralBoolean;
  import com.alibaba.cobar.parser.ast.expression.primary.literal.LiteralHexadecimal;
  import com.alibaba.cobar.parser.ast.expression.primary.literal.LiteralNull;
  import com.alibaba.cobar.parser.ast.expression.primary.literal.LiteralNumber;
  import com.alibaba.cobar.parser.ast.expression.primary.literal.LiteralString;
  import com.alibaba.cobar.parser.ast.expression.string.LikeExpression;
  import com.alibaba.cobar.parser.ast.expression.type.CollateExpression;
  import com.alibaba.cobar.parser.ast.fragment.GroupBy;
  import com.alibaba.cobar.parser.ast.fragment.Limit;
  import com.alibaba.cobar.parser.ast.fragment.OrderBy;
  import com.alibaba.cobar.parser.ast.fragment.SortOrder;
  import com.alibaba.cobar.parser.ast.fragment.VariableScope;
  import com.alibaba.cobar.parser.ast.fragment.ddl.ColumnDefinition;
  import com.alibaba.cobar.parser.ast.fragment.ddl.TableOptions;
  import com.alibaba.cobar.parser.ast.fragment.ddl.datatype.DataType;
  import com.alibaba.cobar.parser.ast.fragment.ddl.index.IndexColumnName;
  import com.alibaba.cobar.parser.ast.fragment.ddl.index.IndexOption;
  import com.alibaba.cobar.parser.ast.fragment.tableref.Dual;
  import com.alibaba.cobar.parser.ast.fragment.tableref.IndexHint;
  import com.alibaba.cobar.parser.ast.fragment.tableref.InnerJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.NaturalJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.OuterJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.StraightJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.SubqueryFactor;
  import com.alibaba.cobar.parser.ast.fragment.tableref.TableRefFactor;
  import com.alibaba.cobar.parser.ast.fragment.tableref.TableReference;
  import com.alibaba.cobar.parser.ast.fragment.tableref.TableReferences;
  import com.alibaba.cobar.parser.ast.stmt.dal.DALSetCharacterSetStatement;
  import com.alibaba.cobar.parser.ast.stmt.dal.DALSetNamesStatement;
  import com.alibaba.cobar.parser.ast.stmt.dal.DALSetStatement;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowAuthors;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowBinLogEvent;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowBinaryLog;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowCharaterSet;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowCollation;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowColumns;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowContributors;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowCreate;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowDatabases;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowEngine;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowEngines;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowErrors;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowEvents;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowFunctionCode;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowFunctionStatus;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowGrants;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowIndex;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowMasterStatus;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowOpenTables;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowPlugins;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowPrivileges;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowProcedureCode;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowProcedureStatus;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowProcesslist;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowProfile;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowProfiles;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowSlaveHosts;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowSlaveStatus;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowStatus;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowTableStatus;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowTables;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowTriggers;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowVariables;
  import com.alibaba.cobar.parser.ast.stmt.dal.ShowWarnings;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLAlterTableStatement;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLAlterTableStatement.AlterSpecification;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLCreateIndexStatement;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLCreateTableStatement;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLDropIndexStatement;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLDropTableStatement;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLRenameTableStatement;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DDLTruncateStatement;
  import com.alibaba.cobar.parser.ast.stmt.ddl.DescTableStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLCallStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLDeleteStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLInsertStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLReplaceStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLSelectStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLSelectUnionStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLUpdateStatement;
  import com.alibaba.cobar.parser.ast.stmt.extension.ExtDDLCreatePolicy;
  import com.alibaba.cobar.parser.ast.stmt.extension.ExtDDLDropPolicy;
  import com.alibaba.cobar.parser.ast.stmt.mts.MTSReleaseStatement;
  import com.alibaba.cobar.parser.ast.stmt.mts.MTSRollbackStatement;
  import com.alibaba.cobar.parser.ast.stmt.mts.MTSSavepointStatement;
  import com.alibaba.cobar.parser.ast.stmt.mts.MTSSetTransactionStatement;
  import com.alibaba.cobar.parser.util.Pair;
  
  /**
   * @author <a href="mailto:shuo.qius@alibaba-inc.com">QIU Shuo</a>
   */
  public final class MySQLOutputASTVisitor implements SQLASTVisitor {
      private static final Object[] EMPTY_OBJ_ARRAY = new Object[0];
      private static final int[] EMPTY_INT_ARRAY = new int[0];
      private final StringBuilder appendable;
      private final Object[] args;
      private int[] argsIndex;
      private Map<PlaceHolder, Object> placeHolderToString;
  
      public MySQLOutputASTVisitor(StringBuilder appendable) {
          this(appendable, null);
      }
  
      /**
       * @param args parameters for {@link java.sql.PreparedStatement
       *            preparedStmt}
       */
      public MySQLOutputASTVisitor(StringBuilder appendable, Object[] args) {
          this.appendable = appendable;
          this.args = args == null ? EMPTY_OBJ_ARRAY : args;
          this.argsIndex = args == null ? EMPTY_INT_ARRAY : new int[args.length];
      }
  
      public void setPlaceHolderToString(Map<PlaceHolder, Object> map) {
          this.placeHolderToString = map;
      }
  
      public String getSql() {
          return appendable.toString();
      }
  
      /**
       * @return never null. rst[i] ≡ {@link #args}[{@link #argsIndex}[i]]
       */
      public Object[] getArguments() {
          final int argsIndexSize = argsIndex.length;
          if (argsIndexSize <= 0)
              return EMPTY_OBJ_ARRAY;
  
          boolean noChange = true;
          for (int i = 0; i < argsIndexSize; ++i) {
              if (i != argsIndex[i]) {
                  noChange = false;
                  break;
              }
          }
          if (noChange)
              return args;
  
          Object[] rst = new Object[argsIndexSize];
          for (int i = 0; i < argsIndexSize; ++i) {
              rst[i] = args[argsIndex[i]];
          }
          return rst;
      }
  
      /**
       * @param list never null
       */
      private void printList(List<? extends ASTNode> list) {
          printList(list, ", ");
      }
  
      /**
       * @param list never null
       */
      private void printList(List<? extends ASTNode> list, String sep) {
          boolean isFst = true;
          for (ASTNode arg : list) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(sep);
              arg.accept(this);
          }
      }
  
      @Override
      public void visit(BetweenAndExpression node) {
          Expression comparee = node.getFirst();
          boolean paren = comparee.getPrecedence() <= node.getPrecedence();
          if (paren)
              appendable.append('(');
          comparee.accept(this);
          if (paren)
              appendable.append(')');
  
          if (node.isNot())
              appendable.append(" NOT BETWEEN ");
          else
              appendable.append(" BETWEEN ");
  
          Expression start = node.getSecond();
          paren = start.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          start.accept(this);
          if (paren)
              appendable.append(')');
  
          appendable.append(" AND ");
  
          Expression end = node.getThird();
          paren = end.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          end.accept(this);
          if (paren)
              appendable.append(')');
      }
  
      @Override
      public void visit(ComparisionIsExpression node) {
          Expression comparee = node.getOperand();
          boolean paren = comparee.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          comparee.accept(this);
          if (paren)
              appendable.append(')');
          switch (node.getMode()) {
          case IS_NULL:
              appendable.append(" IS NULL");
              break;
          case IS_TRUE:
              appendable.append(" IS TRUE");
              break;
          case IS_FALSE:
              appendable.append(" IS FALSE");
              break;
          case IS_UNKNOWN:
              appendable.append(" IS UNKNOWN");
              break;
          case IS_NOT_NULL:
              appendable.append(" IS NOT NULL");
              break;
          case IS_NOT_TRUE:
              appendable.append(" IS NOT TRUE");
              break;
          case IS_NOT_FALSE:
              appendable.append(" IS NOT FALSE");
              break;
          case IS_NOT_UNKNOWN:
              appendable.append(" IS NOT UNKNOWN");
              break;
          default:
              throw new IllegalArgumentException("unknown mode for IS expression: " + node.getMode());
          }
      }
  
      @Override
      public void visit(InExpressionList node) {
          appendable.append('(');
          printList(node.getList());
          appendable.append(')');
      }
  
      @Override
      public void visit(LikeExpression node) {
          Expression comparee = node.getFirst();
          boolean paren = comparee.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          comparee.accept(this);
          if (paren)
              appendable.append(')');
  
          if (node.isNot())
              appendable.append(" NOT LIKE ");
          else
              appendable.append(" LIKE ");
  
          Expression pattern = node.getSecond();
          paren = pattern.getPrecedence() <= node.getPrecedence();
          if (paren)
              appendable.append('(');
          pattern.accept(this);
          if (paren)
              appendable.append(')');
  
          Expression escape = node.getThird();
          if (escape != null) {
              appendable.append(" ESCAPE ");
              paren = escape.getPrecedence() <= node.getPrecedence();
              if (paren)
                  appendable.append('(');
              escape.accept(this);
              if (paren)
                  appendable.append(')');
          }
      }
  
      @Override
      public void visit(CollateExpression node) {
          Expression string = node.getString();
          boolean paren = string.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          string.accept(this);
          if (paren)
              appendable.append(')');
  
          appendable.append(" COLLATE ").append(node.getCollateName());
      }
  
      @Override
      public void visit(UserExpression node) {
          appendable.append(node.getUserAtHost());
      }
  
      @Override
      public void visit(UnaryOperatorExpression node) {
          appendable.append(node.getOperator()).append(' ');
          boolean paren = node.getOperand().getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          node.getOperand().accept(this);
          if (paren)
              appendable.append(')');
      }
  
      @Override
      public void visit(BinaryOperatorExpression node) {
          Expression left = node.getLeftOprand();
          boolean paren = node.isLeftCombine()
                  ? left.getPrecedence() < node.getPrecedence() : left.getPrecedence() <= node.getPrecedence();
          if (paren)
              appendable.append('(');
          left.accept(this);
          if (paren)
              appendable.append(')');
  
          appendable.append(' ').append(node.getOperator()).append(' ');
  
          Expression right = node.getRightOprand();
          paren = node.isLeftCombine()
                  ? right.getPrecedence() <= node.getPrecedence() : right.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          right.accept(this);
          if (paren)
              appendable.append(')');
      }
  
      @Override
      public void visit(PolyadicOperatorExpression node) {
          for (int i = 0, len = node.getArity(); i < len; ++i) {
              if (i > 0)
                  appendable.append(' ').append(node.getOperator()).append(' ');
              Expression operand = node.getOperand(i);
              boolean paren = operand.getPrecedence() < node.getPrecedence();
              if (paren)
                  appendable.append('(');
              operand.accept(this);
              if (paren)
                  appendable.append(')');
          }
      }
  
      @Override
      public void visit(LogicalAndExpression node) {
          visit((PolyadicOperatorExpression) node);
      }
  
      @Override
      public void visit(LogicalOrExpression node) {
          visit((PolyadicOperatorExpression) node);
      }
  
      @Override
      public void visit(ComparisionEqualsExpression node) {
          visit((BinaryOperatorExpression) node);
      }
  
      @Override
      public void visit(ComparisionNullSafeEqualsExpression node) {
          visit((BinaryOperatorExpression) node);
      }
  
      @Override
      public void visit(InExpression node) {
          visit((BinaryOperatorExpression) node);
      }
  
      @Override
      public void visit(FunctionExpression node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(Char node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          printList(node.getArguments());
          String charset = node.getCharset();
          if (charset != null) {
              appendable.append(" USING ").append(charset);
          }
          appendable.append(')');
      }
  
      @Override
      public void visit(Convert node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          printList(node.getArguments());
          String transcodeName = node.getTranscodeName();
          appendable.append(" USING ").append(transcodeName);
          appendable.append(')');
      }
  
      @Override
      public void visit(Trim node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          Expression remStr = node.getRemainString();
          switch (node.getDirection()) {
          case DEFAULT:
              if (remStr != null) {
                  remStr.accept(this);
                  appendable.append(" FROM ");
              }
              break;
          case BOTH:
              appendable.append("BOTH ");
              if (remStr != null)
                  remStr.accept(this);
              appendable.append(" FROM ");
              break;
          case LEADING:
              appendable.append("LEADING ");
              if (remStr != null)
                  remStr.accept(this);
              appendable.append(" FROM ");
              break;
          case TRAILING:
              appendable.append("TRAILING ");
              if (remStr != null)
                  remStr.accept(this);
              appendable.append(" FROM ");
              break;
          default:
              throw new IllegalArgumentException("unknown trim direction: " + node.getDirection());
          }
          Expression str = node.getString();
          str.accept(this);
          appendable.append(')');
      }
  
      @Override
      public void visit(Cast node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          node.getExpr().accept(this);
          appendable.append(" AS ");
          String typeName = node.getTypeName();
          appendable.append(typeName);
          Expression info1 = node.getTypeInfo1();
          if (info1 != null) {
              appendable.append('(');
              info1.accept(this);
              Expression info2 = node.getTypeInfo2();
              if (info2 != null) {
                  appendable.append(", ");
                  info2.accept(this);
              }
              appendable.append(')');
          }
          appendable.append(')');
      }
  
      @Override
      public void visit(Avg node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          if (node.isDistinct()) {
              appendable.append("DISTINCT ");
          }
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(Max node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          if (node.isDistinct()) {
              appendable.append("DISTINCT ");
          }
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(Min node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          if (node.isDistinct()) {
              appendable.append("DISTINCT ");
          }
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(Sum node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          if (node.isDistinct()) {
              appendable.append("DISTINCT ");
          }
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(Count node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          if (node.isDistinct()) {
              appendable.append("DISTINCT ");
          }
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(GroupConcat node) {
          String functionName = node.getFunctionName();
          appendable.append(functionName).append('(');
          if (node.isDistinct()) {
              appendable.append("DISTINCT ");
          }
          printList(node.getArguments());
          Expression orderBy = node.getOrderBy();
          if (orderBy != null) {
              appendable.append(" ORDER BY ");
              orderBy.accept(this);
              if (node.isDesc())
                  appendable.append(" DESC");
              else
                  appendable.append(" ASC");
              List<Expression> list = node.getAppendedColumnNames();
              if (list != null && !list.isEmpty()) {
                  appendable.append(", ");
                  printList(list);
              }
          }
          String sep = node.getSeparator();
          if (sep != null) {
              appendable.append(" SEPARATOR ").append(sep);
          }
          appendable.append(')');
      }
  
      @Override
      public void visit(Extract node) {
          appendable.append("EXTRACT(").append(node.getUnit().name()).append(" FROM ");
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(Timestampdiff node) {
          appendable.append("TIMESTAMPDIFF(").append(node.getUnit().name()).append(", ");
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(Timestampadd node) {
          appendable.append("TIMESTAMPADD(").append(node.getUnit().name()).append(", ");
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(GetFormat node) {
          appendable.append("GET_FORMAT(");
          GetFormat.FormatType type = node.getFormatType();
          appendable.append(type.name()).append(", ");
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(PlaceHolder node) {
          if (placeHolderToString == null) {
              appendable.append("${").append(node.getName()).append('}');
              return;
          }
          Object toStringer = placeHolderToString.get(node);
          if (toStringer == null) {
              appendable.append("${").append(node.getName()).append('}');
          } else {
              appendable.append(toStringer.toString());
          }
      }
  
      @Override
      public void visit(IntervalPrimary node) {
          appendable.append("INTERVAL ");
          Expression quantity = node.getQuantity();
          boolean paren = quantity.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          quantity.accept(this);
          if (paren)
              appendable.append(')');
          IntervalPrimary.Unit unit = node.getUnit();
          appendable.append(' ').append(unit.name());
      }
  
      @Override
      public void visit(LiteralBitField node) {
          String introducer = node.getIntroducer();
          if (introducer != null)
              appendable.append(introducer).append(' ');
          appendable.append("b'").append(node.getText()).append('\'');
      }
  
      @Override
      public void visit(LiteralBoolean node) {
          if (node.isTrue()) {
              appendable.append("TRUE");
          } else {
              appendable.append("FALSE");
          }
      }
  
      @Override
      public void visit(LiteralHexadecimal node) {
          String introducer = node.getIntroducer();
          if (introducer != null)
              appendable.append(introducer).append(' ');
          appendable.append("x'");
          node.appendTo(appendable);
          appendable.append('\'');
      }
  
      @Override
      public void visit(LiteralNull node) {
          appendable.append("NULL");
      }
  
      @Override
      public void visit(LiteralNumber node) {
          appendable.append(String.valueOf(node.getNumber()));
      }
  
      @Override
      public void visit(LiteralString node) {
          String introducer = node.getIntroducer();
          if (introducer != null) {
              appendable.append(introducer);
          } else if (node.isNchars()) {
              appendable.append('N');
          }
          appendable.append('\'').append(node.getString()).append('\'');
      }
  
      @Override
      public void visit(CaseWhenOperatorExpression node) {
          appendable.append("CASE");
          Expression comparee = node.getComparee();
          if (comparee != null) {
              appendable.append(' ');
              comparee.accept(this);
          }
          List<Pair<Expression, Expression>> whenList = node.getWhenList();
          for (Pair<Expression, Expression> whenthen : whenList) {
              appendable.append(" WHEN ");
              Expression when = whenthen.getKey();
              when.accept(this);
              appendable.append(" THEN ");
              Expression then = whenthen.getValue();
              then.accept(this);
          }
          Expression elseRst = node.getElseResult();
          if (elseRst != null) {
              appendable.append(" ELSE ");
              elseRst.accept(this);
          }
          appendable.append(" END");
      }
  
      @Override
      public void visit(DefaultValue node) {
          appendable.append("DEFAULT");
      }
  
      @Override
      public void visit(ExistsPrimary node) {
          appendable.append("EXISTS (");
          node.getSubquery().accept(this);
          appendable.append(')');
      }
  
      @Override
      public void visit(Identifier node) {
          Expression parent = node.getParent();
          if (parent != null) {
              parent.accept(this);
              appendable.append('.');
          }
          appendable.append(node.getIdText());
      }
  
      private static boolean containsCompIn(Expression pat) {
          if (pat.getPrecedence() > Expression.PRECEDENCE_COMPARISION)
              return false;
          if (pat instanceof BinaryOperatorExpression) {
              if (pat instanceof InExpression) {
                  return true;
              }
              BinaryOperatorExpression bp = (BinaryOperatorExpression) pat;
              if (bp.isLeftCombine()) {
                  return containsCompIn(bp.getLeftOprand());
              } else {
                  return containsCompIn(bp.getLeftOprand());
              }
          } else if (pat instanceof ComparisionIsExpression) {
              ComparisionIsExpression is = (ComparisionIsExpression) pat;
              return containsCompIn(is.getOperand());
          } else if (pat instanceof TernaryOperatorExpression) {
              TernaryOperatorExpression tp = (TernaryOperatorExpression) pat;
              return containsCompIn(tp.getFirst()) || containsCompIn(tp.getSecond()) || containsCompIn(tp.getThird());
          } else if (pat instanceof UnaryOperatorExpression) {
              UnaryOperatorExpression up = (UnaryOperatorExpression) pat;
              return containsCompIn(up.getOperand());
          } else {
              return false;
          }
      }
  
      @Override
      public void visit(MatchExpression node) {
          appendable.append("MATCH (");
          printList(node.getColumns());
          appendable.append(") AGAINST (");
          Expression pattern = node.getPattern();
          boolean inparen = containsCompIn(pattern);
          if (inparen)
              appendable.append('(');
          pattern.accept(this);
          if (inparen)
              appendable.append(')');
          switch (node.getModifier()) {
          case IN_BOOLEAN_MODE:
              appendable.append(" IN BOOLEAN MODE");
              break;
          case IN_NATURAL_LANGUAGE_MODE:
              appendable.append(" IN NATURAL LANGUAGE MODE");
              break;
          case IN_NATURAL_LANGUAGE_MODE_WITH_QUERY_EXPANSION:
              appendable.append(" IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION");
              break;
          case WITH_QUERY_EXPANSION:
              appendable.append(" WITH QUERY EXPANSION");
              break;
          case _DEFAULT:
              break;
          default:
              throw new IllegalArgumentException("unkown modifier for match expression: " + node.getModifier());
          }
          appendable.append(')');
      }
  
      private int index = -1;
  
      private void appendArgsIndex(int value) {
          int i = ++index;
          if (argsIndex.length <= i) {
              int[] a = new int[i + 1];
              if (i > 0)
                  System.arraycopy(argsIndex, 0, a, 0, i);
              argsIndex = a;
          }
          argsIndex[i] = value;
      }
  
      @Override
      public void visit(ParamMarker node) {
          appendable.append('?');
          appendArgsIndex(node.getParamIndex() - 1);
      }
  
      @Override
      public void visit(RowExpression node) {
          appendable.append("ROW(");
          printList(node.getRowExprList());
          appendable.append(')');
      }
  
      @Override
      public void visit(SysVarPrimary node) {
          VariableScope scope = node.getScope();
          switch (scope) {
          case GLOBAL:
              appendable.append("@@global.");
              break;
          case SESSION:
              appendable.append("@@");
              break;
          default:
              throw new IllegalArgumentException("unkown scope for sysVar primary: " + scope);
          }
          appendable.append(node.getVarText());
      }
  
      @Override
      public void visit(UsrDefVarPrimary node) {
          appendable.append(node.getVarText());
      }
  
      @Override
      public void visit(IndexHint node) {
          IndexHint.IndexAction action = node.getAction();
          switch (action) {
          case FORCE:
              appendable.append("FORCE ");
              break;
          case IGNORE:
              appendable.append("IGNORE ");
              break;
          case USE:
              appendable.append("USE ");
              break;
          default:
              throw new IllegalArgumentException("unkown index action for index hint: " + action);
          }
          IndexHint.IndexType type = node.getType();
          switch (type) {
          case INDEX:
              appendable.append("INDEX ");
              break;
          case KEY:
              appendable.append("KEY ");
              break;
          default:
              throw new IllegalArgumentException("unkown index type for index hint: " + type);
          }
          IndexHint.IndexScope scope = node.getScope();
          switch (scope) {
          case GROUP_BY:
              appendable.append("FOR GROUP BY ");
              break;
          case ORDER_BY:
              appendable.append("FOR ORDER BY ");
              break;
          case JOIN:
              appendable.append("FOR JOIN ");
              break;
          case ALL:
              break;
          default:
              throw new IllegalArgumentException("unkown index scope for index hint: " + scope);
          }
          appendable.append('(');
          List<String> indexList = node.getIndexList();
          boolean isFst = true;
          for (String indexName : indexList) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(", ");
              appendable.append(indexName);
          }
          appendable.append(')');
      }
  
      @Override
      public void visit(TableReferences node) {
          printList(node.getTableReferenceList());
      }
  
      @Override
      public void visit(InnerJoin node) {
          TableReference left = node.getLeftTableRef();
          boolean paren = left.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          left.accept(this);
          if (paren)
              appendable.append(')');
  
          appendable.append(" INNER JOIN ");
          TableReference right = node.getRightTableRef();
          paren = right.getPrecedence() <= node.getPrecedence();
          if (paren)
              appendable.append('(');
          right.accept(this);
          if (paren)
              appendable.append(')');
  
          Expression on = node.getOnCond();
          List<String> using = node.getUsing();
          if (on != null) {
              appendable.append(" ON ");
              on.accept(this);
          } else if (using != null) {
              appendable.append(" USING (");
              boolean isFst = true;
              for (String col : using) {
                  if (isFst)
                      isFst = false;
                  else
                      appendable.append(", ");
                  appendable.append(col);
              }
              appendable.append(")");
          }
      }
  
      @Override
      public void visit(NaturalJoin node) {
          TableReference left = node.getLeftTableRef();
          boolean paren = left.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          left.accept(this);
          if (paren)
              appendable.append(')');
  
          appendable.append(" NATURAL ");
          if (node.isOuter()) {
              if (node.isLeft())
                  appendable.append("LEFT ");
              else
                  appendable.append("RIGHT ");
          }
          appendable.append("JOIN ");
  
          TableReference right = node.getRightTableRef();
          paren = right.getPrecedence() <= node.getPrecedence();
          if (paren)
              appendable.append('(');
          right.accept(this);
          if (paren)
              appendable.append(')');
      }
  
      @Override
      public void visit(StraightJoin node) {
          TableReference left = node.getLeftTableRef();
          boolean paren = left.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          left.accept(this);
          if (paren)
              appendable.append(')');
  
          appendable.append(" STRAIGHT_JOIN ");
  
          TableReference right = node.getRightTableRef();
          paren = right.getPrecedence() <= node.getPrecedence();
          if (paren)
              appendable.append('(');
          right.accept(this);
          if (paren)
              appendable.append(')');
  
          Expression on = node.getOnCond();
          if (on != null) {
              appendable.append(" ON ");
              on.accept(this);
          }
      }
  
      @Override
      public void visit(OuterJoin node) {
          TableReference left = node.getLeftTableRef();
          boolean paren = left.getPrecedence() < node.getPrecedence();
          if (paren)
              appendable.append('(');
          left.accept(this);
          if (paren)
              appendable.append(')');
  
          if (node.isLeftJoin())
              appendable.append(" LEFT JOIN ");
          else
              appendable.append(" RIGHT JOIN ");
  
          TableReference right = node.getRightTableRef();
          paren = right.getPrecedence() <= node.getPrecedence();
          if (paren)
              appendable.append('(');
          right.accept(this);
          if (paren)
              appendable.append(')');
  
          Expression on = node.getOnCond();
          List<String> using = node.getUsing();
          if (on != null) {
              appendable.append(" ON ");
              on.accept(this);
          } else if (using != null) {
              appendable.append(" USING (");
              boolean isFst = true;
              for (String col : using) {
                  if (isFst)
                      isFst = false;
                  else
                      appendable.append(", ");
                  appendable.append(col);
              }
              appendable.append(")");
          } else {
              throw new IllegalArgumentException("either ON or USING must be included for OUTER JOIN");
          }
      }
  
      @Override
      public void visit(SubqueryFactor node) {
          appendable.append('(');
          QueryExpression query = node.getSubquery();
          query.accept(this);
          appendable.append(") AS ").append(node.getAlias());
      }
  
      @Override
      public void visit(TableRefFactor node) {
          Identifier table = node.getTable();
          table.accept(this);
          String alias = node.getAlias();
          if (alias != null) {
              appendable.append(" AS ").append(alias);
          }
          List<IndexHint> list = node.getHintList();
          if (list != null && !list.isEmpty()) {
              appendable.append(' ');
              printList(list, " ");
          }
      }
  
      @Override
      public void visit(Dual dual) {
          appendable.append("DUAL");
      }
  
      @Override
      public void visit(GroupBy node) {
          appendable.append("GROUP BY ");
          boolean isFst = true;
          for (Pair<Expression, SortOrder> p : node.getOrderByList()) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(", ");
              Expression col = p.getKey();
              col.accept(this);
              switch (p.getValue()) {
              case DESC:
                  appendable.append(" DESC");
                  break;
              }
          }
          if (node.isWithRollup()) {
              appendable.append(" WITH ROLLUP");
          }
      }
  
      @Override
      public void visit(OrderBy node) {
          appendable.append("ORDER BY ");
          boolean isFst = true;
          for (Pair<Expression, SortOrder> p : node.getOrderByList()) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(", ");
              Expression col = p.getKey();
              col.accept(this);
              switch (p.getValue()) {
              case DESC:
                  appendable.append(" DESC");
                  break;
              }
          }
      }
  
      @Override
      public void visit(Limit node) {
          appendable.append("LIMIT ");
          Object offset = node.getOffset();
          if (offset instanceof ParamMarker) {
              ((ParamMarker) offset).accept(this);
          } else {
              appendable.append(String.valueOf(offset));
          }
          appendable.append(", ");
          Object size = node.getSize();
          if (size instanceof ParamMarker) {
              ((ParamMarker) size).accept(this);
          } else {
              appendable.append(String.valueOf(size));
          }
      }
  
      @Override
      public void visit(ColumnDefinition node) {
          throw new UnsupportedOperationException("col_def in CREATE TABLE is partially parsed");
      }
  
      @Override
      public void visit(IndexOption node) {
          if (node.getKeyBlockSize() != null) {
              appendable.append("KEY_BLOCK_SIZE = ");
              node.getKeyBlockSize().accept(this);
          } else if (node.getIndexType() != null) {
              appendable.append("USING ");
              switch (node.getIndexType()) {// USING {BTREE | HASH}
              case BTREE:
                  appendable.append("BTREE");
                  break;
              case HASH:
                  appendable.append("HASH");
                  break;
              }
          } else if (node.getParserName() != null) {
              appendable.append("WITH PARSER ");
              node.getParserName().accept(this);
          } else if (node.getComment() != null) {
              appendable.append("COMMENT ");
              node.getComment().accept(this);
          }
      }
  
      @Override
      public void visit(IndexColumnName node) {
          // QS_TODO
      }
  
      @Override
      public void visit(TableOptions node) {
          // QS_TODO
  
      }
  
      @Override
      public void visit(AlterSpecification node) {
          throw new UnsupportedOperationException("subclass have not implement visit");
      }
  
      @Override
      public void visit(DataType node) {
          throw new UnsupportedOperationException("subclass have not implement visit");
      }
  
      private void printSimpleShowStmt(String attName) {
          appendable.append("SHOW ").append(attName);
      }
  
      @Override
      public void visit(ShowAuthors node) {
          printSimpleShowStmt("AUTHORS");
      }
  
      @Override
      public void visit(ShowBinaryLog node) {
          printSimpleShowStmt("BINARY LOGS");
      }
  
      @Override
      public void visit(ShowBinLogEvent node) {
          appendable.append("SHOW BINLOG EVENTS");
          String logName = node.getLogName();
          if (logName != null)
              appendable.append(" IN ").append(logName);
          Expression pos = node.getPos();
          if (pos != null) {
              appendable.append(" FROM ");
              pos.accept(this);
          }
          Limit limit = node.getLimit();
          if (limit != null) {
              appendable.append(' ');
              limit.accept(this);
          }
      }
  
      /**
       * ' ' will be prepended
       */
      private void printLikeOrWhere(String like, Expression where) {
          if (like != null) {
              appendable.append(" LIKE ").append(like);
          } else if (where != null) {
              appendable.append(" WHERE ");
              where.accept(this);
          }
      }
  
      @Override
      public void visit(ShowCharaterSet node) {
          appendable.append("SHOW CHARACTER SET");
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowCollation node) {
          appendable.append("SHOW COLLATION");
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowColumns node) {
          appendable.append("SHOW ");
          if (node.isFull())
              appendable.append("FULL ");
          appendable.append("COLUMNS FROM ");
          node.getTable().accept(this);
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowContributors node) {
          printSimpleShowStmt("CONTRIBUTORS");
      }
  
      @Override
      public void visit(ShowCreate node) {
          appendable.append("SHOW CREATE ").append(node.getType().name()).append(' ');
          node.getId().accept(this);
      }
  
      @Override
      public void visit(ShowDatabases node) {
          appendable.append("SHOW DATABASES");
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowEngine node) {
          appendable.append("SHOW ENGINE ");
          switch (node.getType()) {
          case INNODB_MUTEX:
              appendable.append("INNODB MUTEX");
              break;
          case INNODB_STATUS:
              appendable.append("INNODB STATUS");
              break;
          case PERFORMANCE_SCHEMA_STATUS:
              appendable.append("PERFORMANCE SCHEMA STATUS");
              break;
          default:
              throw new IllegalArgumentException("unrecognized type for SHOW ENGINE: " + node.getType());
          }
      }
  
      @Override
      public void visit(ShowEngines node) {
          printSimpleShowStmt("ENGINES");
      }
  
      @Override
      public void visit(ShowErrors node) {
          appendable.append("SHOW ");
          if (node.isCount()) {
              appendable.append("COUNT(*) ERRORS");
          } else {
              appendable.append("ERRORS");
              Limit limit = node.getLimit();
              if (node.getLimit() != null) {
                  appendable.append(' ');
                  limit.accept(this);
              }
          }
      }
  
      @Override
      public void visit(ShowEvents node) {
          appendable.append("SHOW EVENTS");
          Identifier schema = node.getSchema();
          if (schema != null) {
              appendable.append(" FROM ");
              schema.accept(this);
          }
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowFunctionCode node) {
          appendable.append("SHOW FUNCTION CODE ");
          node.getFunctionName().accept(this);
      }
  
      @Override
      public void visit(ShowFunctionStatus node) {
          appendable.append("SHOW FUNCTION STATUS");
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowGrants node) {
          appendable.append("SHOW GRANTS");
          Expression user = node.getUser();
          if (user != null) {
              appendable.append(" FOR ");
              user.accept(this);
          }
      }
  
      @Override
      public void visit(ShowIndex node) {
          appendable.append("SHOW ");
          switch (node.getType()) {
          case INDEX:
              appendable.append("INDEX ");
              break;
          case INDEXES:
              appendable.append("INDEXES ");
              break;
          case KEYS:
              appendable.append("KEYS ");
              break;
          default:
              throw new IllegalArgumentException("unrecognized type for SHOW INDEX: " + node.getType());
          }
          appendable.append("IN ");
          node.getTable().accept(this);
      }
  
      @Override
      public void visit(ShowMasterStatus node) {
          printSimpleShowStmt("MASTER STATUS");
      }
  
      @Override
      public void visit(ShowOpenTables node) {
          appendable.append("SHOW OPEN TABLES");
          Identifier db = node.getSchema();
          if (db != null) {
              appendable.append(" FROM ");
              db.accept(this);
          }
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowPlugins node) {
          printSimpleShowStmt("PLUGINS");
      }
  
      @Override
      public void visit(ShowPrivileges node) {
          printSimpleShowStmt("PRIVILEGES");
      }
  
      @Override
      public void visit(ShowProcedureCode node) {
          appendable.append("SHOW PROCEDURE CODE ");
          node.getProcedureName().accept(this);
      }
  
      @Override
      public void visit(ShowProcedureStatus node) {
          appendable.append("SHOW PROCEDURE STATUS");
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowProcesslist node) {
          appendable.append("SHOW ");
          if (node.isFull())
              appendable.append("FULL ");
          appendable.append("PROCESSLIST");
      }
  
      @Override
      public void visit(ShowProfile node) {
          appendable.append("SHOW PROFILE");
          List<ShowProfile.Type> types = node.getTypes();
          boolean isFst = true;
          for (ShowProfile.Type type : types) {
              if (isFst) {
                  isFst = false;
                  appendable.append(' ');
              } else {
                  appendable.append(", ");
              }
              appendable.append(type.name().replace('_', ' '));
          }
          Expression query = node.getForQuery();
          if (query != null) {
              appendable.append(" FOR QUERY ");
              query.accept(this);
          }
          Limit limit = node.getLimit();
          if (limit != null) {
              appendable.append(' ');
              limit.accept(this);
          }
      }
  
      @Override
      public void visit(ShowProfiles node) {
          printSimpleShowStmt("PROFILES");
      }
  
      @Override
      public void visit(ShowSlaveHosts node) {
          printSimpleShowStmt("SLAVE HOSTS");
      }
  
      @Override
      public void visit(ShowSlaveStatus node) {
          printSimpleShowStmt("SLAVE STATUS");
      }
  
      @Override
      public void visit(ShowStatus node) {
          appendable.append("SHOW ").append(node.getScope().name().replace('_', ' ')).append(" STATUS");
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowTables node) {
          appendable.append("SHOW");
          if (node.isFull())
              appendable.append(" FULL");
          appendable.append(" TABLES");
          Identifier schema = node.getSchema();
          if (schema != null) {
              appendable.append(" FROM ");
              schema.accept(this);
          }
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowTableStatus node) {
          appendable.append("SHOW TABLE STATUS");
          Identifier schema = node.getDatabase();
          if (schema != null) {
              appendable.append(" FROM ");
              schema.accept(this);
          }
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowTriggers node) {
          appendable.append("SHOW TRIGGERS");
          Identifier schema = node.getSchema();
          if (schema != null) {
              appendable.append(" FROM ");
              schema.accept(this);
          }
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowVariables node) {
          appendable.append("SHOW ").append(node.getScope().name().replace('_', ' ')).append(" VARIABLES");
          printLikeOrWhere(node.getPattern(), node.getWhere());
      }
  
      @Override
      public void visit(ShowWarnings node) {
          appendable.append("SHOW ");
          if (node.isCount()) {
              appendable.append("COUNT(*) WARNINGS");
          } else {
              appendable.append("WARNINGS");
              Limit limit = node.getLimit();
              if (limit != null) {
                  appendable.append(' ');
                  limit.accept(this);
              }
          }
      }
  
      @Override
      public void visit(DescTableStatement node) {
          appendable.append("DESC ");
          node.getTable().accept(this);
      }
  
      @Override
      public void visit(DALSetStatement node) {
          appendable.append("SET ");
          boolean isFst = true;
          for (Pair<VariableExpression, Expression> p : node.getAssignmentList()) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(", ");
              p.getKey().accept(this);
              appendable.append(" = ");
              p.getValue().accept(this);
          }
      }
  
      @Override
      public void visit(DALSetNamesStatement node) {
          appendable.append("SET NAMES ");
          if (node.isDefault()) {
              appendable.append("DEFAULT");
          } else {
              appendable.append(node.getCharsetName());
              String collate = node.getCollationName();
              if (collate != null) {
                  appendable.append(" COLLATE ");
                  appendable.append(collate);
              }
          }
      }
  
      @Override
      public void visit(DALSetCharacterSetStatement node) {
          appendable.append("SET CHARACTER SET ");
          if (node.isDefault()) {
              appendable.append("DEFAULT");
          } else {
              appendable.append(node.getCharset());
          }
      }
  
      @Override
      public void visit(MTSSetTransactionStatement node) {
          appendable.append("SET ");
          VariableScope scope = node.getScope();
          if (scope != null) {
              switch (scope) {
              case SESSION:
                  appendable.append("SESSION ");
                  break;
              case GLOBAL:
                  appendable.append("GLOBAL ");
                  break;
              default:
                  throw new IllegalArgumentException("unknown scope for SET TRANSACTION ISOLATION LEVEL: " + scope);
              }
          }
          appendable.append("TRANSACTION ISOLATION LEVEL ");
          switch (node.getLevel()) {
          case READ_COMMITTED:
              appendable.append("READ COMMITTED");
              break;
          case READ_UNCOMMITTED:
              appendable.append("READ UNCOMMITTED");
              break;
          case REPEATABLE_READ:
              appendable.append("REPEATABLE READ");
              break;
          case SERIALIZABLE:
              appendable.append("SERIALIZABLE");
              break;
          default:
              throw new IllegalArgumentException("unknown level for SET TRANSACTION ISOLATION LEVEL: " + node.getLevel());
          }
      }
  
      @Override
      public void visit(MTSSavepointStatement node) {
          appendable.append("SAVEPOINT ");
          node.getSavepoint().accept(this);
      }
  
      @Override
      public void visit(MTSReleaseStatement node) {
          appendable.append("RELEASE SAVEPOINT ");
          node.getSavepoint().accept(this);
      }
  
      @Override
      public void visit(MTSRollbackStatement node) {
          appendable.append("ROLLBACK");
          Identifier savepoint = node.getSavepoint();
          if (savepoint == null) {
              MTSRollbackStatement.CompleteType type = node.getCompleteType();
              switch (type) {
              case CHAIN:
                  appendable.append(" AND CHAIN");
                  break;
              case NO_CHAIN:
                  appendable.append(" AND NO CHAIN");
                  break;
              case NO_RELEASE:
                  appendable.append(" NO RELEASE");
                  break;
              case RELEASE:
                  appendable.append(" RELEASE");
                  break;
              case UN_DEF:
                  break;
              default:
                  throw new IllegalArgumentException("unrecgnized complete type: " + type);
              }
          } else {
              appendable.append(" TO SAVEPOINT ");
              savepoint.accept(this);
          }
      }
  
      @Override
      public void visit(DMLCallStatement node) {
          appendable.append("CALL ");
          node.getProcedure().accept(this);
          appendable.append('(');
          printList(node.getArguments());
          appendable.append(')');
      }
  
      @Override
      public void visit(DMLDeleteStatement node) {
          appendable.append("DELETE ");
          if (node.isLowPriority())
              appendable.append("LOW_PRIORITY ");
          if (node.isQuick())
              appendable.append("QUICK ");
          if (node.isIgnore())
              appendable.append("IGNORE ");
          TableReferences tableRefs = node.getTableRefs();
          if (tableRefs == null) {
              appendable.append("FROM ");
              node.getTableNames().get(0).accept(this);
          } else {
              printList(node.getTableNames());
              appendable.append(" FROM ");
              node.getTableRefs().accept(this);
          }
          Expression where = node.getWhereCondition();
          if (where != null) {
              appendable.append(" WHERE ");
              where.accept(this);
          }
          OrderBy orderBy = node.getOrderBy();
          if (orderBy != null) {
              appendable.append(' ');
              orderBy.accept(this);
          }
          Limit limit = node.getLimit();
          if (limit != null) {
              appendable.append(' ');
              limit.accept(this);
          }
      }
  
      @Override
      public void visit(DMLInsertStatement node) {
          appendable.append("INSERT ");
          switch (node.getMode()) {
          case DELAY:
              appendable.append("DELAYED ");
              break;
          case HIGH:
              appendable.append("HIGH_PRIORITY ");
              break;
          case LOW:
              appendable.append("LOW_PRIORITY ");
              break;
          case UNDEF:
              break;
          default:
              throw new IllegalArgumentException("unknown mode for INSERT: " + node.getMode());
          }
          if (node.isIgnore())
              appendable.append("IGNORE ");
          appendable.append("INTO ");
          node.getTable().accept(this);
          appendable.append(' ');
  
          List<Identifier> cols = node.getColumnNameList();
          if (cols != null && !cols.isEmpty()) {
              appendable.append('(');
              printList(cols);
              appendable.append(") ");
          }
  
          QueryExpression select = node.getSelect();
          if (select == null) {
              appendable.append("VALUES ");
              List<RowExpression> rows = node.getRowList();
              if (rows != null && !rows.isEmpty()) {
                  boolean isFst = true;
                  for (RowExpression row : rows) {
                      if (row == null || row.getRowExprList().isEmpty())
                          continue;
                      if (isFst)
                          isFst = false;
                      else
                          appendable.append(", ");
                      appendable.append('(');
                      printList(row.getRowExprList());
                      appendable.append(')');
                  }
              } else {
                  throw new IllegalArgumentException("at least one row for INSERT");
              }
          } else {
              select.accept(this);
          }
  
          List<Pair<Identifier, Expression>> dup = node.getDuplicateUpdate();
          if (dup != null && !dup.isEmpty()) {
              appendable.append(" ON DUPLICATE KEY UPDATE ");
              boolean isFst = true;
              for (Pair<Identifier, Expression> p : dup) {
                  if (isFst)
                      isFst = false;
                  else
                      appendable.append(", ");
                  p.getKey().accept(this);
                  appendable.append(" = ");
                  p.getValue().accept(this);
              }
          }
      }
  
      @Override
      public void visit(DMLReplaceStatement node) {
          appendable.append("REPLACE ");
          switch (node.getMode()) {
          case DELAY:
              appendable.append("DELAYED ");
              break;
          case LOW:
              appendable.append("LOW_PRIORITY ");
              break;
          case UNDEF:
              break;
          default:
              throw new IllegalArgumentException("unknown mode for INSERT: " + node.getMode());
          }
          appendable.append("INTO ");
          node.getTable().accept(this);
          appendable.append(' ');
  
          List<Identifier> cols = node.getColumnNameList();
          if (cols != null && !cols.isEmpty()) {
              appendable.append('(');
              printList(cols);
              appendable.append(") ");
          }
  
          QueryExpression select = node.getSelect();
          if (select == null) {
              appendable.append("VALUES ");
              List<RowExpression> rows = node.getRowList();
              if (rows != null && !rows.isEmpty()) {
                  boolean isFst = true;
                  for (RowExpression row : rows) {
                      if (row == null || row.getRowExprList().isEmpty())
                          continue;
                      if (isFst)
                          isFst = false;
                      else
                          appendable.append(", ");
                      appendable.append('(');
                      printList(row.getRowExprList());
                      appendable.append(')');
                  }
              } else {
                  throw new IllegalArgumentException("at least one row for REPLACE");
              }
          } else {
              select.accept(this);
          }
      }
  
      @Override
      public void visit(DMLSelectStatement node) {
          appendable.append("SELECT ");
          final DMLSelectStatement.SelectOption option = node.getOption();
          switch (option.resultDup) {
          case ALL:
              break;
          case DISTINCT:
              appendable.append("DISTINCT ");
              break;
          case DISTINCTROW:
              appendable.append("DISTINCTROW ");
              break;
          default:
              throw new IllegalArgumentException("unknown option for SELECT: " + option);
          }
          if (option.highPriority) {
              appendable.append("HIGH_PRIORITY ");
          }
          if (option.straightJoin) {
              appendable.append("STRAIGHT_JOIN ");
          }
          switch (option.resultSize) {
          case SQL_BIG_RESULT:
              appendable.append("SQL_BIG_RESULT ");
              break;
          case SQL_SMALL_RESULT:
              appendable.append("SQL_SMALL_RESULT ");
              break;
          case UNDEF:
              break;
          default:
              throw new IllegalArgumentException("unknown option for SELECT: " + option);
          }
          if (option.sqlBufferResult) {
              appendable.append("SQL_BUFFER_RESULT ");
          }
          switch (option.queryCache) {
          case SQL_CACHE:
              appendable.append("SQL_CACHE ");
              break;
          case SQL_NO_CACHE:
              appendable.append("SQL_NO_CACHE ");
              break;
          case UNDEF:
              break;
          default:
              throw new IllegalArgumentException("unknown option for SELECT: " + option);
          }
          if (option.sqlCalcFoundRows) {
              appendable.append("SQL_CALC_FOUND_ROWS ");
          }
  
          boolean isFst = true;
          List<Pair<Expression, String>> exprList = node.getSelectExprList();
          for (Pair<Expression, String> p : exprList) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(", ");
              p.getKey().accept(this);
              String alias = p.getValue();
              if (alias != null) {
                  appendable.append(" AS ").append(alias);
              }
          }
  
          TableReferences from = node.getTables();
          if (from != null) {
              appendable.append(" FROM ");
              from.accept(this);
          }
  
          Expression where = node.getWhere();
          if (where != null) {
              appendable.append(" WHERE ");
              where.accept(this);
          }
  
          GroupBy group = node.getGroup();
          if (group != null) {
              appendable.append(' ');
              group.accept(this);
          }
  
          Expression having = node.getHaving();
          if (having != null) {
              appendable.append(" HAVING ");
              having.accept(this);
          }
  
          OrderBy order = node.getOrder();
          if (order != null) {
              appendable.append(' ');
              order.accept(this);
          }
  
          Limit limit = node.getLimit();
          if (limit != null) {
              appendable.append(' ');
              limit.accept(this);
          }
  
          switch (option.lockMode) {
          case FOR_UPDATE:
              appendable.append(" FOR UPDATE");
              break;
          case LOCK_IN_SHARE_MODE:
              appendable.append(" LOCK IN SHARE MODE");
              break;
          case UNDEF:
              break;
          default:
              throw new IllegalArgumentException("unknown option for SELECT: " + option);
          }
      }
  
      @Override
      public void visit(DMLSelectUnionStatement node) {
          List<DMLSelectStatement> list = node.getSelectStmtList();
          if (list == null || list.isEmpty()) {
              throw new IllegalArgumentException("SELECT UNION must have at least one SELECT");
          }
          final int fstDist = node.getFirstDistinctIndex();
          int i = 0;
          for (DMLSelectStatement select : list) {
              if (i > 0) {
                  appendable.append(" UNION ");
                  if (i > fstDist) {
                      appendable.append("ALL ");
                  }
              }
              appendable.append('(');
              select.accept(this);
              appendable.append(')');
              ++i;
          }
          OrderBy order = node.getOrderBy();
          if (order != null) {
              appendable.append(' ');
              order.accept(this);
          }
          Limit limit = node.getLimit();
          if (limit != null) {
              appendable.append(' ');
              limit.accept(this);
          }
      }
  
      @Override
      public void visit(DMLUpdateStatement node) {
          appendable.append("UPDATE ");
          if (node.isLowPriority()) {
              appendable.append("LOW_PRIORITY ");
          }
          if (node.isIgnore()) {
              appendable.append("IGNORE ");
          }
          node.getTableRefs().accept(this);
          appendable.append(" SET ");
          boolean isFst = true;
          for (Pair<Identifier, Expression> p : node.getValues()) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(", ");
              p.getKey().accept(this);
              appendable.append(" = ");
              p.getValue().accept(this);
          }
          Expression where = node.getWhere();
          if (where != null) {
              appendable.append(" WHERE ");
              where.accept(this);
          }
          OrderBy order = node.getOrderBy();
          if (order != null) {
              appendable.append(' ');
              order.accept(this);
          }
          Limit limit = node.getLimit();
          if (limit != null) {
              appendable.append(' ');
              limit.accept(this);
          }
      }
  
      @Override
      public void visit(DDLTruncateStatement node) {
          appendable.append("TRUNCATE TABLE ");
          node.getTable().accept(this);
      }
  
      @Override
      public void visit(DDLAlterTableStatement node) {
          throw new UnsupportedOperationException("ALTER TABLE is partially parsed");
      }
  
      @Override
      public void visit(DDLCreateIndexStatement node) {
          throw new UnsupportedOperationException("CREATE INDEX is partially parsed");
      }
  
      @Override
      public void visit(DDLCreateTableStatement node) {
          throw new UnsupportedOperationException("CREATE TABLE is partially parsed");
      }
  
      @Override
      public void visit(DDLRenameTableStatement node) {
          appendable.append("RENAME TABLE ");
          boolean isFst = true;
          for (Pair<Identifier, Identifier> p : node.getList()) {
              if (isFst)
                  isFst = false;
              else
                  appendable.append(", ");
              p.getKey().accept(this);
              appendable.append(" TO ");
              p.getValue().accept(this);
          }
      }
  
      @Override
      public void visit(DDLDropIndexStatement node) {
          appendable.append("DROP INDEX ");
          node.getIndexName().accept(this);
          appendable.append(" ON ");
          node.getTable().accept(this);
      }
  
      @Override
      public void visit(DDLDropTableStatement node) {
          appendable.append("DROP ");
          if (node.isTemp()) {
              appendable.append("TEMPORARY ");
          }
          appendable.append("TABLE ");
          if (node.isIfExists()) {
              appendable.append("IF EXISTS ");
          }
          printList(node.getTableNames());
          switch (node.getMode()) {
          case CASCADE:
              appendable.append(" CASCADE");
              break;
          case RESTRICT:
              appendable.append(" RESTRICT");
              break;
          case UNDEF:
              break;
          default:
              throw new IllegalArgumentException("unsupported mode for DROP TABLE: " + node.getMode());
          }
      }
  
      @Override
      public void visit(ExtDDLCreatePolicy node) {
          appendable.append("CREATE POLICY ");
          node.getName().accept(this);
          appendable.append(" (");
          boolean first = true;
          for (Pair<Integer, Expression> p : node.getProportion()) {
              if (first)
                  first = false;
              else
                  appendable.append(", ");
              appendable.append(p.getKey()).append(' ');
              p.getValue().accept(this);
          }
          appendable.append(')');
      }
  
      @Override
      public void visit(ExtDDLDropPolicy node) {
          appendable.append("DROP POLICY ");
          node.getPolicyName().accept(this);
      }
  
  }
  