/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.apache.phoenix.compile;

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.apache.hadoop.hbase.CompareOperator;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.expression.AndExpression;
import org.apache.phoenix.expression.ArrayConstructorExpression;
import org.apache.phoenix.expression.ByteBasedLikeExpression;
import org.apache.phoenix.expression.CaseExpression;
import org.apache.phoenix.expression.CoerceExpression;
import org.apache.phoenix.expression.ComparisonExpression;
import org.apache.phoenix.expression.DateAddExpression;
import org.apache.phoenix.expression.DateSubtractExpression;
import org.apache.phoenix.expression.DecimalAddExpression;
import org.apache.phoenix.expression.DecimalDivideExpression;
import org.apache.phoenix.expression.DecimalMultiplyExpression;
import org.apache.phoenix.expression.DecimalSubtractExpression;
import org.apache.phoenix.expression.Determinism;
import org.apache.phoenix.expression.DoubleAddExpression;
import org.apache.phoenix.expression.DoubleDivideExpression;
import org.apache.phoenix.expression.DoubleMultiplyExpression;
import org.apache.phoenix.expression.DoubleSubtractExpression;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.InListExpression;
import org.apache.phoenix.expression.IsNullExpression;
import org.apache.phoenix.expression.LikeExpression;
import org.apache.phoenix.expression.LiteralExpression;
import org.apache.phoenix.expression.LongAddExpression;
import org.apache.phoenix.expression.LongDivideExpression;
import org.apache.phoenix.expression.LongMultiplyExpression;
import org.apache.phoenix.expression.LongSubtractExpression;
import org.apache.phoenix.expression.ModulusExpression;
import org.apache.phoenix.expression.NotExpression;
import org.apache.phoenix.expression.OrExpression;
import org.apache.phoenix.expression.RowValueConstructorExpression;
import org.apache.phoenix.expression.StringBasedLikeExpression;
import org.apache.phoenix.expression.StringConcatExpression;
import org.apache.phoenix.expression.TimestampAddExpression;
import org.apache.phoenix.expression.TimestampSubtractExpression;
import org.apache.phoenix.expression.function.ArrayAllComparisonExpression;
import org.apache.phoenix.expression.function.ArrayAnyComparisonExpression;
import org.apache.phoenix.expression.function.ArrayElemRefExpression;
import org.apache.phoenix.expression.function.RoundDecimalExpression;
import org.apache.phoenix.expression.function.RoundTimestampExpression;
import org.apache.phoenix.parse.AddParseNode;
import org.apache.phoenix.parse.AndParseNode;
import org.apache.phoenix.parse.ArithmeticParseNode;
import org.apache.phoenix.parse.ArrayAllComparisonNode;
import org.apache.phoenix.parse.ArrayAnyComparisonNode;
import org.apache.phoenix.parse.ArrayConstructorNode;
import org.apache.phoenix.parse.ArrayElemRefNode;
import org.apache.phoenix.parse.BindParseNode;
import org.apache.phoenix.parse.CaseParseNode;
import org.apache.phoenix.parse.CastParseNode;
import org.apache.phoenix.parse.ColumnParseNode;
import org.apache.phoenix.parse.ComparisonParseNode;
import org.apache.phoenix.parse.DivideParseNode;
import org.apache.phoenix.parse.ExistsParseNode;
import org.apache.phoenix.parse.FunctionParseNode;
import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunctionInfo;
import org.apache.phoenix.parse.InListParseNode;
import org.apache.phoenix.parse.IsNullParseNode;
import org.apache.phoenix.parse.JsonModifyParseNode;
import org.apache.phoenix.parse.JsonQueryParseNode;
import org.apache.phoenix.parse.LikeParseNode;
import org.apache.phoenix.parse.LikeParseNode.LikeType;
import org.apache.phoenix.parse.LiteralParseNode;
import org.apache.phoenix.parse.ModulusParseNode;
import org.apache.phoenix.parse.MultiplyParseNode;
import org.apache.phoenix.parse.NotParseNode;
import org.apache.phoenix.parse.OrParseNode;
import org.apache.phoenix.parse.PFunction;
import org.apache.phoenix.parse.ParseNode;
import org.apache.phoenix.parse.RowValueConstructorParseNode;
import org.apache.phoenix.parse.SequenceValueParseNode;
import org.apache.phoenix.parse.StringConcatParseNode;
import org.apache.phoenix.parse.SubqueryParseNode;
import org.apache.phoenix.parse.SubtractParseNode;
import org.apache.phoenix.parse.UDFParseNode;
import org.apache.phoenix.parse.UnsupportedAllParseNodeVisitor;
import org.apache.phoenix.query.KeyRange;
import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.query.QueryServicesOptions;
import org.apache.phoenix.schema.ColumnFamilyNotFoundException;
import org.apache.phoenix.schema.ColumnNotFoundException;
import org.apache.phoenix.schema.ColumnRef;
import org.apache.phoenix.schema.DelegateDatum;
import org.apache.phoenix.schema.IndexUncoveredDataColumnRef;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PDatum;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.TypeMismatchException;
import org.apache.phoenix.schema.types.PArrayDataType;
import org.apache.phoenix.schema.types.PBoolean;
import org.apache.phoenix.schema.types.PChar;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.schema.types.PDate;
import org.apache.phoenix.schema.types.PDecimal;
import org.apache.phoenix.schema.types.PDouble;
import org.apache.phoenix.schema.types.PLong;
import org.apache.phoenix.schema.types.PTimestamp;
import org.apache.phoenix.schema.types.PUnsignedTimestamp;
import org.apache.phoenix.schema.types.PVarbinary;
import org.apache.phoenix.schema.types.PVarchar;
import org.apache.phoenix.schema.types.PhoenixArray;
import org.apache.phoenix.util.EncodedColumnsUtil;
import org.apache.phoenix.util.ExpressionUtil;
import org.apache.phoenix.util.IndexUtil;
import org.apache.phoenix.util.SchemaUtil;
import org.apache.phoenix.util.StringUtil;

public class ExpressionCompiler extends UnsupportedAllParseNodeVisitor<Expression> {
  private boolean isAggregate;
  private boolean isJsonFragment;
  protected ParseNode aggregateFunction;
  protected final StatementContext context;
  protected final GroupBy groupBy;
  private int nodeCount;
  private int totalNodeCount;
  private final boolean resolveViewConstants;
  private static final Expression NOT_NULL_STRING = LiteralExpression
    .newConstant(PVarchar.INSTANCE.toObject(KeyRange.IS_NOT_NULL_RANGE.getLowerRange()));

  public ExpressionCompiler(StatementContext context) {
    this(context, GroupBy.EMPTY_GROUP_BY, false);
  }

  ExpressionCompiler(StatementContext context, boolean resolveViewConstants) {
    this(context, GroupBy.EMPTY_GROUP_BY, resolveViewConstants);
  }

  ExpressionCompiler(StatementContext context, GroupBy groupBy) {
    this(context, groupBy, false);
  }

  ExpressionCompiler(StatementContext context, GroupBy groupBy, boolean resolveViewConstants) {
    this.context = context;
    this.groupBy = groupBy;
    this.resolveViewConstants = resolveViewConstants;
  }

  public boolean isAggregate() {
    return isAggregate;
  }

  public boolean isJsonFragment() {
    return isJsonFragment;
  }

  public boolean isTopLevel() {
    return nodeCount == 0;
  }

  public void reset() {
    this.isAggregate = false;
    this.nodeCount = 0;
    this.totalNodeCount = 0;
    this.isJsonFragment = false;
  }

  @Override
  public boolean visitEnter(ComparisonParseNode node) {
    return true;
  }

  private void addBindParamMetaData(ParseNode lhsNode, ParseNode rhsNode, Expression lhsExpr,
    Expression rhsExpr) throws SQLException {
    if (lhsNode instanceof BindParseNode) {
      context.getBindManager().addParamMetaData((BindParseNode) lhsNode, rhsExpr);
    }
    if (rhsNode instanceof BindParseNode) {
      context.getBindManager().addParamMetaData((BindParseNode) rhsNode, lhsExpr);
    }
  }

  @Override
  public Expression visitLeave(ComparisonParseNode node, List<Expression> children)
    throws SQLException {
    ParseNode lhsNode = node.getChildren().get(0);
    ParseNode rhsNode = node.getChildren().get(1);
    Expression lhsExpr = children.get(0);
    Expression rhsExpr = children.get(1);

    PDataType dataTypeOfLHSExpr = lhsExpr.getDataType();
    if (dataTypeOfLHSExpr != null && !dataTypeOfLHSExpr.isComparisonSupported()) {
      throw new SQLExceptionInfo.Builder(SQLExceptionCode.COMPARISON_UNSUPPORTED)
        .setMessage(" for type " + dataTypeOfLHSExpr).build().buildException();
    }
    PDataType dataTypeOfRHSExpr = rhsExpr.getDataType();
    if (dataTypeOfRHSExpr != null && !dataTypeOfRHSExpr.isComparisonSupported()) {
      throw new SQLExceptionInfo.Builder(SQLExceptionCode.COMPARISON_UNSUPPORTED)
        .setMessage(" for type " + dataTypeOfRHSExpr).build().buildException();
    }

    CompareOperator op = node.getFilterOp();

    if (
      lhsNode instanceof RowValueConstructorParseNode
        && rhsNode instanceof RowValueConstructorParseNode
    ) {
      int i = 0;
      for (; i < Math.min(lhsExpr.getChildren().size(), rhsExpr.getChildren().size()); i++) {
        addBindParamMetaData(lhsNode.getChildren().get(i), rhsNode.getChildren().get(i),
          lhsExpr.getChildren().get(i), rhsExpr.getChildren().get(i));
      }
      for (; i < lhsExpr.getChildren().size(); i++) {
        addBindParamMetaData(lhsNode.getChildren().get(i), null, lhsExpr.getChildren().get(i),
          null);
      }
      for (; i < rhsExpr.getChildren().size(); i++) {
        addBindParamMetaData(null, rhsNode.getChildren().get(i), null,
          rhsExpr.getChildren().get(i));
      }
    } else if (lhsExpr instanceof RowValueConstructorExpression) {
      addBindParamMetaData(lhsNode.getChildren().get(0), rhsNode, lhsExpr.getChildren().get(0),
        rhsExpr);
      for (int i = 1; i < lhsExpr.getChildren().size(); i++) {
        addBindParamMetaData(lhsNode.getChildren().get(i), null, lhsExpr.getChildren().get(i),
          null);
      }
    } else if (rhsExpr instanceof RowValueConstructorExpression) {
      addBindParamMetaData(lhsNode, rhsNode.getChildren().get(0), lhsExpr,
        rhsExpr.getChildren().get(0));
      for (int i = 1; i < rhsExpr.getChildren().size(); i++) {
        addBindParamMetaData(null, rhsNode.getChildren().get(i), null,
          rhsExpr.getChildren().get(i));
      }
    } else {
      addBindParamMetaData(lhsNode, rhsNode, lhsExpr, rhsExpr);
    }
    return wrapGroupByExpression(ComparisonExpression.create(op, children, context.getTempPtr(),
      context.getCurrentTable().getTable().rowKeyOrderOptimizable()));
  }

  @Override
  public boolean visitEnter(AndParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(AndParseNode node, List<Expression> children) throws SQLException {
    return wrapGroupByExpression(AndExpression.create(children));
  }

  @Override
  public boolean visitEnter(OrParseNode node) throws SQLException {
    return true;
  }

  private Expression orExpression(List<Expression> children) throws SQLException {
    Iterator<Expression> iterator = children.iterator();
    Determinism determinism = Determinism.ALWAYS;
    while (iterator.hasNext()) {
      Expression child = iterator.next();
      if (child.getDataType() != PBoolean.INSTANCE) {
        throw TypeMismatchException.newException(PBoolean.INSTANCE, child.getDataType(),
          child.toString());
      }
      if (LiteralExpression.isFalse(child)) {
        iterator.remove();
      }
      if (LiteralExpression.isTrue(child)) {
        return child;
      }
      determinism = determinism.combine(child.getDeterminism());
    }
    if (children.size() == 0) {
      return LiteralExpression.newConstant(false, determinism);
    }
    if (children.size() == 1) {
      return children.get(0);
    }
    return new OrExpression(children);
  }

  @Override
  public Expression visitLeave(OrParseNode node, List<Expression> children) throws SQLException {
    return wrapGroupByExpression(orExpression(children));
  }

  @Override
  public boolean visitEnter(FunctionParseNode node) throws SQLException {
    if (node instanceof JsonQueryParseNode || node instanceof JsonModifyParseNode) {
      this.isJsonFragment = true;
    }
    // TODO: Oracle supports nested aggregate function while other DBs don't. Should we?
    if (node.isAggregate()) {
      if (aggregateFunction != null) {
        throw new SQLFeatureNotSupportedException("Nested aggregate functions are not supported");
      }
      this.aggregateFunction = node;
      this.isAggregate = true;

    }
    return true;
  }

  private Expression wrapGroupByExpression(Expression expression) {
    // If we're in an aggregate function, don't wrap a group by expression,
    // since in that case we're aggregating over the regular/ungrouped
    // column.
    if (aggregateFunction == null) {
      int index = groupBy.getExpressions().indexOf(expression);
      if (index >= 0) {
        isAggregate = true;
        expression = ExpressionUtil.convertGroupByExpressionToRowKeyColumnExpression(groupBy,
          expression, index);
      }
    }
    return expression;
  }

  /**
   * Add expression to the expression manager, returning the same one if already used.
   */
  protected Expression addExpression(Expression expression) {
    return context.getExpressionManager().addIfAbsent(expression);
  }

  @Override
  /**
   * @param node     a function expression node
   * @param children the child expression arguments to the function expression node.
   */
  public Expression visitLeave(FunctionParseNode node, List<Expression> children)
    throws SQLException {
    PFunction function = null;
    if (node instanceof UDFParseNode) {
      function = context.getResolver().resolveFunction(node.getName());
      BuiltInFunctionInfo info = new BuiltInFunctionInfo(function);
      node = new UDFParseNode(node.getName(), node.getChildren(), info);
    }
    children = node.validate(children, context);
    Expression expression = null;
    if (function == null) {
      expression = node.create(children, context);
    } else {
      expression = node.create(children, function, context);
    }
    ImmutableBytesWritable ptr = context.getTempPtr();
    BuiltInFunctionInfo info = node.getInfo();
    for (int i = 0; i < info.getRequiredArgCount(); i++) {
      // Optimization to catch cases where a required argument is null resulting in the function
      // returning null. We have to wait until after we create the function expression so that
      // we can get the proper type to use.
      if (node.evalToNullIfParamIsNull(context, i)) {
        Expression child = children.get(i);
        if (ExpressionUtil.isNull(child, ptr)) {
          return ExpressionUtil.getNullExpression(expression);
        }
      }
    }
    if (ExpressionUtil.isConstant(expression)) {
      return ExpressionUtil.getConstantExpression(expression, ptr);
    }
    expression = addExpression(expression);
    expression = wrapGroupByExpression(expression);
    if (aggregateFunction == node) {
      aggregateFunction = null; // Turn back off on the way out
    }
    return expression;
  }

  /**
   * Called by visitor to resolve a column expression node into a column reference. Derived classes
   * may use this as a hook to trap all column resolves.
   * @param node a column expression node
   * @return a resolved ColumnRef
   * @throws SQLException if the column expression node does not refer to a known/unambiguous column
   */
  protected ColumnRef resolveColumn(ColumnParseNode node) throws SQLException {
    ColumnRef ref = null;
    try {
      ref = context.getResolver().resolveColumn(node.getSchemaName(), node.getTableName(),
        node.getName());
    } catch (ColumnNotFoundException e) {
      // Rather than not use a local index when a column not contained by it is referenced, we
      // join back to the data table in our coprocessor since this is a relatively cheap
      // operation given that we know the join is local.
      if (IndexUtil.shouldIndexBeUsedForUncoveredQuery(context.getCurrentTable())) {
        try {
          context.setUncoveredIndex(true);
          return new IndexUncoveredDataColumnRef(context, context.getCurrentTable(),
            node.getName());
        } catch (ColumnFamilyNotFoundException c) {
          throw e;
        }
      } else {
        throw e;
      }
    }
    PTable table = ref.getTable();
    int pkPosition = ref.getPKSlotPosition();
    // Disallow explicit reference to salting column, tenant ID column, and index ID column
    if (pkPosition >= 0) {
      boolean isSalted = table.getBucketNum() != null;
      boolean isMultiTenant =
        context.getConnection().getTenantId() != null && table.isMultiTenant();
      boolean isSharedViewIndex = table.getViewIndexId() != null;
      int minPosition = (isSalted ? 1 : 0) + (isMultiTenant ? 1 : 0) + (isSharedViewIndex ? 1 : 0);
      if (pkPosition < minPosition) {
        throw new ColumnNotFoundException(table.getSchemaName().getString(),
          table.getTableName().getString(), null, ref.getColumn().getName().getString());
      }
    }
    return ref;
  }

  protected void addColumn(PColumn column) {
    EncodedColumnsUtil.setColumns(column, context.getCurrentTable().getTable(), context.getScan());
  }

  @Override
  public Expression visit(ColumnParseNode node) throws SQLException {
    ColumnRef ref = resolveColumn(node);
    TableRef tableRef = ref.getTableRef();
    ImmutableBytesWritable ptr = context.getTempPtr();
    PColumn column = ref.getColumn();
    // If we have an UPDATABLE view, then we compile those view constants (i.e. columns in equality
    // constraints
    // in the view) to constants. This allows the optimize to optimize out reference to them in
    // various scenarios.
    // If the column is matched in a WHERE clause against a constant not equal to it's constant,
    // then the entire
    // query would become degenerate.
    if (!resolveViewConstants && IndexUtil.getViewConstantValue(column, ptr)) {
      return LiteralExpression.newConstant(
        column.getDataType().toObject(ptr, column.getSortOrder()), column.getDataType(),
        column.getSortOrder());
    }
    if (tableRef.equals(context.getCurrentTable()) && !SchemaUtil.isPKColumn(column)) { // project
                                                                                        // only kv
                                                                                        // columns
      addColumn(column);
    }
    Expression expression =
      ref.newColumnExpression(node.isTableNameCaseSensitive(), node.isCaseSensitive());
    Expression wrappedExpression = wrapGroupByExpression(expression);
    // If we're in an aggregate expression
    // and we're not in the context of an aggregate function
    // and we didn't just wrap our column reference
    // then we're mixing aggregate and non aggregate expressions in the same expression.
    // This catches cases like this: SELECT sum(a_integer) + a_integer FROM atable GROUP BY a_string
    if (isAggregate && aggregateFunction == null && wrappedExpression == expression) {
      throwNonAggExpressionInAggException(expression.toString());
    }
    return wrappedExpression;
  }

  @Override
  public Expression visit(BindParseNode node) throws SQLException {
    Object value = context.getBindManager().getBindValue(node);
    return LiteralExpression.newConstant(value, Determinism.ALWAYS);
  }

  @Override
  public Expression visit(LiteralParseNode node) throws SQLException {
    return LiteralExpression.newConstant(node.getValue(), node.getType(), Determinism.ALWAYS);
  }

  @Override
  public List<Expression> newElementList(int size) {
    nodeCount += size;
    return new ArrayList<Expression>(size);
  }

  @Override
  public void addElement(List<Expression> l, Expression element) {
    nodeCount--;
    totalNodeCount++;
    l.add(element);
  }

  @Override
  public boolean visitEnter(CaseParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(CaseParseNode node, List<Expression> l) throws SQLException {
    final Expression caseExpression = CaseExpression.create(l);
    for (int i = 0; i < node.getChildren().size(); i += 2) {
      ParseNode childNode = node.getChildren().get(i);
      if (childNode instanceof BindParseNode) {
        context.getBindManager().addParamMetaData((BindParseNode) childNode,
          new DelegateDatum(caseExpression));
      }
    }
    return wrapGroupByExpression(caseExpression);
  }

  @Override
  public boolean visitEnter(LikeParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(LikeParseNode node, List<Expression> children) throws SQLException {
    ParseNode lhsNode = node.getChildren().get(0);
    ParseNode rhsNode = node.getChildren().get(1);
    Expression lhs = children.get(0);
    Expression rhs = children.get(1);
    if (lhs.getDataType() != null && !lhs.getDataType().isComparisonSupported()) {
      throw new SQLExceptionInfo.Builder(SQLExceptionCode.COMPARISON_UNSUPPORTED)
        .setMessage(" for type " + lhs.getDataType()).build().buildException();
    }
    if (rhs.getDataType() != null && !rhs.getDataType().isComparisonSupported()) {
      throw new SQLExceptionInfo.Builder(SQLExceptionCode.COMPARISON_UNSUPPORTED)
        .setMessage(" for type " + rhs.getDataType()).build().buildException();
    }
    if (
      rhs.getDataType() != null && lhs.getDataType() != null
        && !lhs.getDataType().isCoercibleTo(rhs.getDataType())
        && !rhs.getDataType().isCoercibleTo(lhs.getDataType())
    ) {
      throw TypeMismatchException.newException(lhs.getDataType(), rhs.getDataType(),
        node.toString());
    }
    if (lhsNode instanceof BindParseNode) {
      context.getBindManager().addParamMetaData((BindParseNode) lhsNode, rhs);
    }
    if (rhsNode instanceof BindParseNode) {
      context.getBindManager().addParamMetaData((BindParseNode) rhsNode, lhs);
    }
    if (rhs instanceof LiteralExpression) {
      String pattern = (String) ((LiteralExpression) rhs).getValue();
      if (pattern == null || pattern.length() == 0) {
        return LiteralExpression.newConstant(null, PBoolean.INSTANCE, rhs.getDeterminism());
      }
      // TODO: for pattern of '%' optimize to strlength(lhs) > 0
      // We can't use lhs IS NOT NULL b/c if lhs is NULL we need
      // to return NULL.
      int index = LikeExpression.indexOfWildcard(pattern);
      // Can't possibly be as long as the constant, then FALSE
      Integer lhsMaxLength = lhs.getMaxLength();
      if (lhsMaxLength != null && lhsMaxLength < index) {
        return LiteralExpression.newConstant(false, rhs.getDeterminism());
      }
      if (index == -1) {
        String rhsLiteral = LikeExpression.unescapeLike(pattern);
        if (node.getLikeType() == LikeType.CASE_SENSITIVE) {
          CompareOperator op = node.isNegate() ? CompareOperator.NOT_EQUAL : CompareOperator.EQUAL;
          if (pattern.equals(rhsLiteral)) {
            return new ComparisonExpression(children, op);
          } else {
            rhs = LiteralExpression.newConstant(rhsLiteral, PChar.INSTANCE, rhs.getDeterminism());
            return new ComparisonExpression(Arrays.asList(lhs, rhs), op);
          }
        }
      } else {
        byte[] wildcardString = new byte[pattern.length()];
        byte[] wildcard = { StringUtil.MULTI_CHAR_LIKE };
        StringUtil.fill(wildcardString, 0, pattern.length(), wildcard, 0, 1, false);
        if (pattern.equals(new String(wildcardString, StandardCharsets.UTF_8))) {
          List<Expression> compareChildren = Arrays.asList(lhs, NOT_NULL_STRING);
          return new ComparisonExpression(compareChildren,
            node.isNegate() ? CompareOperator.LESS : CompareOperator.GREATER_OR_EQUAL);
        }
      }
    }
    QueryServices services = context.getConnection().getQueryServices();
    boolean useByteBasedRegex = services.getProps().getBoolean(
      QueryServices.USE_BYTE_BASED_REGEX_ATTRIB, QueryServicesOptions.DEFAULT_USE_BYTE_BASED_REGEX);
    Expression expression;
    if (useByteBasedRegex) {
      expression = ByteBasedLikeExpression.create(children, node.getLikeType());
    } else {
      expression = StringBasedLikeExpression.create(children, node.getLikeType());
    }
    if (ExpressionUtil.isConstant(expression)) {
      ImmutableBytesWritable ptr = context.getTempPtr();
      if (!expression.evaluate(null, ptr)) {
        return LiteralExpression.newConstant(null, expression.getDeterminism());
      } else {
        return LiteralExpression.newConstant(
          Boolean.TRUE.equals(PBoolean.INSTANCE.toObject(ptr)) ^ node.isNegate(),
          expression.getDeterminism());
      }
    }
    if (node.isNegate()) {
      expression = new NotExpression(expression);
    }
    return wrapGroupByExpression(expression);
  }

  @Override
  public boolean visitEnter(NotParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(NotParseNode node, List<Expression> children) throws SQLException {
    ParseNode childNode = node.getChildren().get(0);
    Expression child = children.get(0);
    if (!PBoolean.INSTANCE.isCoercibleTo(child.getDataType())) {
      throw TypeMismatchException.newException(PBoolean.INSTANCE, child.getDataType(),
        node.toString());
    }
    if (childNode instanceof BindParseNode) { // TODO: valid/possibe?
      context.getBindManager().addParamMetaData((BindParseNode) childNode, child);
    }
    return wrapGroupByExpression(NotExpression.create(child, context.getTempPtr()));
  }

  @Override
  public boolean visitEnter(CastParseNode node) throws SQLException {
    return true;
  }

  // TODO: don't repeat this ugly cast logic (maybe use isCastable in the last else block.
  private static Expression convertToRoundExpressionIfNeeded(PDataType fromDataType,
    PDataType targetDataType, List<Expression> expressions) throws SQLException {
    Expression firstChildExpr = expressions.get(0);
    if (fromDataType == targetDataType) {
      return firstChildExpr;
    } else if (
      (fromDataType == PDecimal.INSTANCE || fromDataType == PTimestamp.INSTANCE
        || fromDataType == PUnsignedTimestamp.INSTANCE)
        && targetDataType.isCoercibleTo(PLong.INSTANCE)
    ) {
      return RoundDecimalExpression.create(expressions);
    } else if (
      expressions.size() == 1 && fromDataType == PTimestamp.INSTANCE
        && targetDataType.isCoercibleTo(PDate.INSTANCE)
    ) {
      return firstChildExpr;
    } else if (
      (fromDataType == PDecimal.INSTANCE || fromDataType == PTimestamp.INSTANCE
        || fromDataType == PUnsignedTimestamp.INSTANCE)
        && targetDataType.isCoercibleTo(PDate.INSTANCE)
    ) {
      return RoundTimestampExpression.create(expressions);
    } else if (fromDataType.isCastableTo(targetDataType)) {
      return firstChildExpr;
    } else {
      throw TypeMismatchException.newException(fromDataType, targetDataType,
        firstChildExpr.toString());
    }
  }

  @Override
  public Expression visitLeave(CastParseNode node, List<Expression> children) throws SQLException {
    ParseNode childNode = node.getChildren().get(0);
    PDataType targetDataType = node.getDataType();
    Expression childExpr = children.get(0);
    PDataType fromDataType = childExpr.getDataType();

    if (childNode instanceof BindParseNode) {
      context.getBindManager().addParamMetaData((BindParseNode) childNode, childExpr);
    }

    Expression expr = childExpr;
    if (fromDataType != null) {
      /*
       * IndexStatementRewriter creates a CAST parse node when rewriting the query to use indexed
       * columns. Without this check present we wrongly and unnecessarily end up creating a
       * RoundExpression.
       */
      if (context.getCurrentTable().getTable().getType() != PTableType.INDEX) {
        expr = convertToRoundExpressionIfNeeded(fromDataType, targetDataType, children);
      }
    }
    boolean rowKeyOrderOptimizable = context.getCurrentTable().getTable().rowKeyOrderOptimizable();
    return wrapGroupByExpression(CoerceExpression.create(expr, targetDataType,
      SortOrder.getDefault(), expr.getMaxLength(), rowKeyOrderOptimizable));
  }

  @Override
  public boolean visitEnter(InListParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(InListParseNode node, List<Expression> l) throws SQLException {
    List<Expression> inChildren = l;
    Expression firstChild = inChildren.get(0);
    ImmutableBytesWritable ptr = context.getTempPtr();
    PDataType firstChildType = firstChild.getDataType();
    ParseNode firstChildNode = node.getChildren().get(0);

    if (firstChildType != null && !firstChildType.isComparisonSupported()) {
      throw new SQLExceptionInfo.Builder(SQLExceptionCode.COMPARISON_UNSUPPORTED)
        .setMessage(" for type " + firstChildType).build().buildException();
    }

    if (firstChildNode instanceof BindParseNode) {
      PDatum datum = firstChild;
      if (firstChildType == null) {
        datum = inferBindDatum(inChildren);
      }
      context.getBindManager().addParamMetaData((BindParseNode) firstChildNode, datum);
    }
    for (int i = 1; i < l.size(); i++) {
      ParseNode childNode = node.getChildren().get(i);
      if (childNode instanceof BindParseNode) {
        context.getBindManager().addParamMetaData((BindParseNode) childNode, firstChild);
      }
    }
    return wrapGroupByExpression(InListExpression.create(inChildren, node.isNegate(), ptr,
      context.getCurrentTable().getTable().rowKeyOrderOptimizable()));
  }

  private static final PDatum DECIMAL_DATUM = new PDatum() {
    @Override
    public boolean isNullable() {
      return true;
    }

    @Override
    public PDataType getDataType() {
      return PDecimal.INSTANCE;
    }

    @Override
    public Integer getMaxLength() {
      return null;
    }

    @Override
    public Integer getScale() {
      return null;
    }

    @Override
    public SortOrder getSortOrder() {
      return SortOrder.getDefault();
    }
  };

  private static PDatum inferBindDatum(List<Expression> children) {
    boolean isChildTypeUnknown = false;
    PDatum datum = children.get(1);
    for (int i = 2; i < children.size(); i++) {
      Expression child = children.get(i);
      PDataType childType = child.getDataType();
      if (childType == null) {
        isChildTypeUnknown = true;
      } else if (datum.getDataType() == null) {
        datum = child;
        isChildTypeUnknown = true;
      } else if (datum.getDataType() == childType || childType.isCoercibleTo(datum.getDataType())) {
        continue;
      } else if (datum.getDataType().isCoercibleTo(childType)) {
        datum = child;
      }
    }
    // If we found an "unknown" child type and the return type is a number
    // make the return type be the most general number type of DECIMAL.
    // TODO: same for TIMESTAMP for DATE/TIME?
    if (
      isChildTypeUnknown && datum.getDataType() != null
        && datum.getDataType().isCoercibleTo(PDecimal.INSTANCE)
    ) {
      return DECIMAL_DATUM;
    }
    return datum;
  }

  @Override
  public boolean visitEnter(IsNullParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(IsNullParseNode node, List<Expression> children)
    throws SQLException {
    ParseNode childNode = node.getChildren().get(0);
    Expression child = children.get(0);
    if (childNode instanceof BindParseNode) { // TODO: valid/possibe?
      context.getBindManager().addParamMetaData((BindParseNode) childNode, child);
    }
    return wrapGroupByExpression(
      IsNullExpression.create(child, node.isNegate(), context.getTempPtr()));
  }

  private static interface ArithmeticExpressionFactory {
    Expression create(ArithmeticParseNode node, List<Expression> children) throws SQLException;
  }

  private static interface ArithmeticExpressionBinder {
    PDatum getBindMetaData(int i, List<Expression> children, Expression expression);
  }

  private Expression visitLeave(ArithmeticParseNode node, List<Expression> children,
    ArithmeticExpressionBinder binder, ArithmeticExpressionFactory factory) throws SQLException {

    boolean isNull = false;
    for (Expression child : children) {
      boolean isChildLiteral = (child instanceof LiteralExpression);
      isNull |= isChildLiteral && ((LiteralExpression) child).getValue() == null;
    }

    Expression expression = factory.create(node, children);

    for (int i = 0; i < node.getChildren().size(); i++) {
      ParseNode childNode = node.getChildren().get(i);
      if (childNode instanceof BindParseNode) {
        context.getBindManager().addParamMetaData((BindParseNode) childNode,
          binder == null ? expression : binder.getBindMetaData(i, children, expression));
      }
    }

    ImmutableBytesWritable ptr = context.getTempPtr();

    // If all children are literals, just evaluate now
    if (ExpressionUtil.isConstant(expression)) {
      return ExpressionUtil.getConstantExpression(expression, ptr);
    } else if (isNull) {
      return LiteralExpression.newConstant(null, expression.getDataType(),
        expression.getDeterminism());
    }
    // Otherwise create and return the expression
    return wrapGroupByExpression(expression);
  }

  @Override
  public boolean visitEnter(AddParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(AddParseNode node, List<Expression> children) throws SQLException {
    return visitLeave(node, children, new ArithmeticExpressionBinder() {
      @Override
      public PDatum getBindMetaData(int i, List<Expression> children, final Expression expression) {
        PDataType type = expression.getDataType();
        if (type != null && type.isCoercibleTo(PDate.INSTANCE)) {
          return getPDatumByExpression(expression, PDecimal.INSTANCE);
        }
        return expression;
      }
    }, new ArithmeticExpressionFactory() {
      @Override
      public Expression create(ArithmeticParseNode node, List<Expression> children)
        throws SQLException {
        boolean foundDate = false;
        Determinism determinism = Determinism.ALWAYS;
        PDataType theType = null;
        for (int i = 0; i < children.size(); i++) {
          Expression e = children.get(i);
          determinism = determinism.combine(e.getDeterminism());
          PDataType type = e.getDataType();
          if (type == null) {
            continue;
          } else if (type.isCoercibleTo(PTimestamp.INSTANCE)) {
            if (foundDate) {
              throw TypeMismatchException.newException(type, node.toString());
            }
            if (
              theType == null
                || (theType != PTimestamp.INSTANCE && theType != PUnsignedTimestamp.INSTANCE)
            ) {
              theType = type;
            }
            foundDate = true;
          } else if (type == PDecimal.INSTANCE) {
            if (theType == null || !theType.isCoercibleTo(PTimestamp.INSTANCE)) {
              theType = PDecimal.INSTANCE;
            }
          } else if (type.isCoercibleTo(PLong.INSTANCE)) {
            if (theType == null) {
              theType = PLong.INSTANCE;
            }
          } else if (type.isCoercibleTo(PDouble.INSTANCE)) {
            if (theType == null) {
              theType = PDouble.INSTANCE;
            }
          } else {
            throw TypeMismatchException.newException(type, node.toString());
          }
        }
        if (theType == PDecimal.INSTANCE) {
          return new DecimalAddExpression(children);
        } else if (theType == PLong.INSTANCE) {
          return new LongAddExpression(children);
        } else if (theType == PDouble.INSTANCE) {
          return new DoubleAddExpression(children);
        } else if (theType == null) {
          return LiteralExpression.newConstant(null, theType, determinism);
        } else if (theType == PTimestamp.INSTANCE || theType == PUnsignedTimestamp.INSTANCE) {
          return new TimestampAddExpression(children);
        } else if (theType.isCoercibleTo(PDate.INSTANCE)) {
          return new DateAddExpression(children);
        } else {
          throw TypeMismatchException.newException(theType, node.toString());
        }
      }
    });
  }

  @Override
  public boolean visitEnter(SubtractParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(SubtractParseNode node, List<Expression> children)
    throws SQLException {
    return visitLeave(node, children, new ArithmeticExpressionBinder() {
      @Override
      public PDatum getBindMetaData(int i, List<Expression> children, final Expression expression) {
        final PDataType type;
        // If we're binding the first parameter and the second parameter
        // is a date
        // we know that the first parameter must be a date type too.
        if (
          i == 0 && (type = children.get(1).getDataType()) != null
            && type.isCoercibleTo(PDate.INSTANCE)
        ) {
          return getPDatumByExpression(expression, type);
        } else if (
          expression.getDataType() != null && expression.getDataType().isCoercibleTo(PDate.INSTANCE)
        ) {
          return getPDatumByExpression(expression, PDecimal.INSTANCE);
        }
        // Otherwise just go with what was calculated for the expression
        return expression;
      }
    }, new ArithmeticExpressionFactory() {
      @Override
      public Expression create(ArithmeticParseNode node, List<Expression> children)
        throws SQLException {
        int i = 0;
        PDataType theType = null;
        Expression e1 = children.get(0);
        Expression e2 = children.get(1);
        Determinism determinism = e1.getDeterminism().combine(e2.getDeterminism());
        PDataType type1 = e1.getDataType();
        PDataType type2 = e2.getDataType();
        // TODO: simplify this special case for DATE conversion
        /**
         * For date1-date2, we want to coerce to a LONG because this cannot be compared against
         * another date. It has essentially become a number. For date1-5, we want to preserve the
         * DATE type because this can still be compared against another date and cannot be
         * multiplied or divided. Any other time occurs is an error. For example, 5-date1 is an
         * error. The nulls occur if we have bind variables.
         */
        boolean isType1Date = type1 != null && type1 != PTimestamp.INSTANCE
          && type1 != PUnsignedTimestamp.INSTANCE && type1.isCoercibleTo(PDate.INSTANCE);
        boolean isType2Date = type2 != null && type2 != PTimestamp.INSTANCE
          && type2 != PUnsignedTimestamp.INSTANCE && type2.isCoercibleTo(PDate.INSTANCE);
        if (isType1Date || isType2Date) {
          if (isType1Date && isType2Date) {
            i = 2;
            theType = PDecimal.INSTANCE;
          } else if (isType1Date && type2 != null && type2.isCoercibleTo(PDecimal.INSTANCE)) {
            i = 2;
            theType = PDate.INSTANCE;
          } else if (type1 == null || type2 == null) {
            /*
             * FIXME: Could be either a Date or BigDecimal, but we don't know if we're comparing to
             * a date or a number which would be disambiguate it.
             */
            i = 2;
            theType = null;
          }
        } else if (type1 == PTimestamp.INSTANCE || type2 == PTimestamp.INSTANCE) {
          i = 2;
          theType = PTimestamp.INSTANCE;
        } else if (type1 == PUnsignedTimestamp.INSTANCE || type2 == PUnsignedTimestamp.INSTANCE) {
          i = 2;
          theType = PUnsignedTimestamp.INSTANCE;
        }

        for (; i < children.size(); i++) {
          // This logic finds the common type to which all child types are coercible
          // without losing precision.
          Expression e = children.get(i);
          determinism = determinism.combine(e.getDeterminism());
          PDataType type = e.getDataType();
          if (type == null) {
            continue;
          } else if (type.isCoercibleTo(PLong.INSTANCE)) {
            if (theType == null) {
              theType = PLong.INSTANCE;
            }
          } else if (type == PDecimal.INSTANCE) {
            // Coerce return type to DECIMAL from LONG or DOUBLE if DECIMAL child found,
            // unless we're doing date arithmetic.
            if (theType == null || !theType.isCoercibleTo(PDate.INSTANCE)) {
              theType = PDecimal.INSTANCE;
            }
          } else if (type.isCoercibleTo(PDouble.INSTANCE)) {
            // Coerce return type to DOUBLE from LONG if DOUBLE child found,
            // unless we're doing date arithmetic or we've found another child of type DECIMAL
            if (
              theType == null
                || (theType != PDecimal.INSTANCE && !theType.isCoercibleTo(PDate.INSTANCE))
            ) {
              theType = PDouble.INSTANCE;
            }
          } else {
            throw TypeMismatchException.newException(type, node.toString());
          }
        }
        if (theType == PDecimal.INSTANCE) {
          return new DecimalSubtractExpression(children);
        } else if (theType == PLong.INSTANCE) {
          return new LongSubtractExpression(children);
        } else if (theType == PDouble.INSTANCE) {
          return new DoubleSubtractExpression(children);
        } else if (theType == null) {
          return LiteralExpression.newConstant(null, theType, determinism);
        } else if (theType == PTimestamp.INSTANCE || theType == PUnsignedTimestamp.INSTANCE) {
          return new TimestampSubtractExpression(children);
        } else if (theType.isCoercibleTo(PDate.INSTANCE)) {
          return new DateSubtractExpression(children);
        } else {
          throw TypeMismatchException.newException(theType, node.toString());
        }
      }
    });
  }

  @Override
  public boolean visitEnter(MultiplyParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(MultiplyParseNode node, List<Expression> children)
    throws SQLException {
    return visitLeave(node, children, null, new ArithmeticExpressionFactory() {
      @Override
      public Expression create(ArithmeticParseNode node, List<Expression> children)
        throws SQLException {
        PDataType theType = null;
        Determinism determinism = Determinism.ALWAYS;
        ExpressionDeterminism expressionDeterminism =
          new ExpressionDeterminism(node, children, theType, determinism).invoke();
        theType = expressionDeterminism.getDataType();
        determinism = expressionDeterminism.getDeterminism();
        if (theType == PDecimal.INSTANCE) {
          return new DecimalMultiplyExpression(children);
        } else if (theType == PLong.INSTANCE) {
          return new LongMultiplyExpression(children);
        } else if (theType == PDouble.INSTANCE) {
          return new DoubleMultiplyExpression(children);
        } else {
          return LiteralExpression.newConstant(null, theType, determinism);
        }
      }
    });
  }

  @Override
  public boolean visitEnter(DivideParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(DivideParseNode node, List<Expression> children)
    throws SQLException {
    for (int i = 1; i < children.size(); i++) { // Compile time check for divide by zero and null
      Expression child = children.get(i);
      if (child.getDataType() != null && child instanceof LiteralExpression) {
        LiteralExpression literal = (LiteralExpression) child;
        if (literal.getDataType() == PDecimal.INSTANCE) {
          if (PDecimal.INSTANCE.compareTo(literal.getValue(), BigDecimal.ZERO) == 0) {
            throw new SQLExceptionInfo.Builder(SQLExceptionCode.DIVIDE_BY_ZERO).build()
              .buildException();
          }
        } else {
          if (literal.getDataType().compareTo(literal.getValue(), 0L, PLong.INSTANCE) == 0) {
            throw new SQLExceptionInfo.Builder(SQLExceptionCode.DIVIDE_BY_ZERO).build()
              .buildException();
          }
        }
      }
    }
    return visitLeave(node, children, null, new ArithmeticExpressionFactory() {
      @Override
      public Expression create(ArithmeticParseNode node, List<Expression> children)
        throws SQLException {
        PDataType theType = null;
        Determinism determinism = Determinism.ALWAYS;
        ExpressionDeterminism expressionDeterminism =
          new ExpressionDeterminism(node, children, theType, determinism).invoke();
        theType = expressionDeterminism.getDataType();
        determinism = expressionDeterminism.getDeterminism();
        if (theType == PDecimal.INSTANCE) {
          return new DecimalDivideExpression(children);
        } else if (theType == PLong.INSTANCE) {
          return new LongDivideExpression(children);
        } else if (theType == PDouble.INSTANCE) {
          return new DoubleDivideExpression(children);
        } else {
          return LiteralExpression.newConstant(null, theType, determinism);
        }
      }
    });
  }

  @Override
  public boolean visitEnter(ModulusParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(ModulusParseNode node, List<Expression> children)
    throws SQLException {
    return visitLeave(node, children, null, new ArithmeticExpressionFactory() {
      @Override
      public Expression create(ArithmeticParseNode node, List<Expression> children)
        throws SQLException {
        // ensure integer types
        for (Expression child : children) {
          PDataType type = child.getDataType();
          if (type != null && !type.isCoercibleTo(PLong.INSTANCE)) {
            throw TypeMismatchException.newException(type, node.toString());
          }
        }

        return new ModulusExpression(children);
      }
    });
  }

  @Override
  public boolean visitEnter(ArrayAnyComparisonNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(ArrayAnyComparisonNode node, List<Expression> children)
    throws SQLException {
    return new ArrayAnyComparisonExpression(children);
  }

  @Override
  public boolean visitEnter(ArrayAllComparisonNode node) throws SQLException {
    return true;
  }

  @Override
  public boolean visitEnter(ArrayElemRefNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(ArrayElemRefNode node, List<Expression> l) throws SQLException {
    return new ArrayElemRefExpression(l);
  }

  @Override
  public Expression visitLeave(ArrayAllComparisonNode node, List<Expression> children)
    throws SQLException {
    return new ArrayAllComparisonExpression(children);
  }

  public static void throwNonAggExpressionInAggException(String nonAggregateExpression)
    throws SQLException {
    throw new SQLExceptionInfo.Builder(SQLExceptionCode.AGGREGATE_WITH_NOT_GROUP_BY_COLUMN)
      .setMessage(nonAggregateExpression).build().buildException();
  }

  @Override
  public Expression visitLeave(StringConcatParseNode node, List<Expression> children)
    throws SQLException {
    final StringConcatExpression expression = new StringConcatExpression(children);
    for (int i = 0; i < children.size(); i++) {
      ParseNode childNode = node.getChildren().get(i);
      if (childNode instanceof BindParseNode) {
        context.getBindManager().addParamMetaData((BindParseNode) childNode, expression);
      }
      PDataType type = children.get(i).getDataType();
      if (type == PVarbinary.INSTANCE) {
        throw new SQLExceptionInfo.Builder(SQLExceptionCode.TYPE_NOT_SUPPORTED_FOR_OPERATOR)
          .setMessage("Concatenation does not support " + type + " in expression" + node).build()
          .buildException();
      }
    }
    ImmutableBytesWritable ptr = context.getTempPtr();
    if (ExpressionUtil.isConstant(expression)) {
      return ExpressionUtil.getConstantExpression(expression, ptr);
    }
    return wrapGroupByExpression(expression);
  }

  @Override
  public boolean visitEnter(StringConcatParseNode node) throws SQLException {
    return true;
  }

  @Override
  public boolean visitEnter(RowValueConstructorParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(RowValueConstructorParseNode node, List<Expression> l)
    throws SQLException {
    // Don't trim trailing nulls here, as we'd potentially be dropping bind
    // variables that aren't bound yet.
    return wrapGroupByExpression(new RowValueConstructorExpression(l, node.isStateless()));
  }

  @Override
  public Expression visit(SequenceValueParseNode node) throws SQLException {
    // NEXT VALUE FOR is only supported in SELECT expressions and UPSERT VALUES
    throw new SQLExceptionInfo.Builder(SQLExceptionCode.INVALID_USE_OF_NEXT_VALUE_FOR)
      .setSchemaName(node.getTableName().getSchemaName())
      .setTableName(node.getTableName().getTableName()).build().buildException();
  }

  @Override
  public Expression visitLeave(ArrayConstructorNode node, List<Expression> children)
    throws SQLException {
    boolean isChildTypeUnknown = false;
    Expression arrayElemChild = null;
    PDataType arrayElemDataType = children.get(0).getDataType();
    for (int i = 0; i < children.size(); i++) {
      Expression child = children.get(i);
      PDataType childType = child.getDataType();
      if (childType == null) {
        isChildTypeUnknown = true;
      } else if (arrayElemDataType == null) {
        arrayElemDataType = childType;
        isChildTypeUnknown = true;
        arrayElemChild = child;
      } else if (arrayElemDataType == childType || childType.isCoercibleTo(arrayElemDataType)) {
        continue;
      } else if (arrayElemDataType.isCoercibleTo(childType)) {
        arrayElemChild = child;
        arrayElemDataType = childType;
      } else {
        throw new SQLExceptionInfo.Builder(SQLExceptionCode.TYPE_MISMATCH)
          .setMessage("Case expressions must have common type: " + arrayElemDataType
            + " cannot be coerced to " + childType)
          .build().buildException();
      }
    }
    // If we found an "unknown" child type and the return type is a number
    // make the return type be the most general number type of DECIMAL.
    if (
      isChildTypeUnknown && arrayElemDataType != null
        && arrayElemDataType.isCoercibleTo(PDecimal.INSTANCE)
    ) {
      arrayElemDataType = PDecimal.INSTANCE;
    }
    final PDataType theArrayElemDataType = arrayElemDataType;
    for (int i = 0; i < node.getChildren().size(); i++) {
      ParseNode childNode = node.getChildren().get(i);
      if (childNode instanceof BindParseNode) {
        context.getBindManager().addParamMetaData((BindParseNode) childNode,
          arrayElemDataType == arrayElemChild.getDataType()
            ? arrayElemChild
            : new DelegateDatum(arrayElemChild) {
              @Override
              public PDataType getDataType() {
                return theArrayElemDataType;
              }
            });
      }
    }
    ImmutableBytesWritable ptr = context.getTempPtr();
    // the value object array type should match the java known type
    Object[] elements = (Object[]) java.lang.reflect.Array
      .newInstance(theArrayElemDataType.getJavaClass(), children.size());

    boolean rowKeyOrderOptimizable = context.getCurrentTable().getTable().rowKeyOrderOptimizable();
    ArrayConstructorExpression arrayExpression =
      new ArrayConstructorExpression(children, arrayElemDataType, rowKeyOrderOptimizable);
    if (ExpressionUtil.isConstant(arrayExpression)) {
      for (int i = 0; i < children.size(); i++) {
        Expression child = children.get(i);
        child.evaluate(null, ptr);
        Object value = null;
        if (child.getDataType() == null) {
          value = arrayElemDataType.toObject(ptr, theArrayElemDataType, child.getSortOrder());
        } else {
          value = arrayElemDataType.toObject(ptr, child.getDataType(), child.getSortOrder());
        }
        elements[i] = LiteralExpression
          .newConstant(value, theArrayElemDataType, child.getDeterminism()).getValue();
      }
      Object value = PArrayDataType.instantiatePhoenixArray(arrayElemDataType, elements);
      return LiteralExpression.newConstant(value,
        PDataType.fromTypeId(arrayElemDataType.getSqlType() + PDataType.ARRAY_TYPE_BASE), null,
        null, arrayExpression.getSortOrder(), Determinism.ALWAYS, rowKeyOrderOptimizable);
    }

    return wrapGroupByExpression(arrayExpression);
  }

  @Override
  public boolean visitEnter(ArrayConstructorNode node) throws SQLException {
    return true;
  }

  @Override
  public boolean visitEnter(ExistsParseNode node) throws SQLException {
    return true;
  }

  @Override
  public Expression visitLeave(ExistsParseNode node, List<Expression> l) throws SQLException {
    LiteralExpression child = (LiteralExpression) l.get(0);
    boolean elementExists = child != null && child.getValue() != null
      && ((PhoenixArray) child.getValue()).getDimensions() > 0;
    return LiteralExpression.newConstant(elementExists ^ node.isNegate(), PBoolean.INSTANCE);
  }

  @Override
  public Expression visit(SubqueryParseNode node) throws SQLException {
    Object result = context.getSubqueryResult(node.getSelectNode());
    return LiteralExpression.newConstant(result);
  }

  public int getTotalNodeCount() {
    return totalNodeCount;
  }

  private PDatum getPDatumByExpression(Expression expression, PDataType pDataTypeInput) {
    return new PDatumImpl(expression, pDataTypeInput);
  }

  private static class PDatumImpl implements PDatum {

    private final boolean isNullable;
    private final PDataType dataType;
    private final Integer maxLength;
    private final Integer scale;
    private final SortOrder sortOrder;

    PDatumImpl(Expression expression, PDataType pDataTypeInput) {
      this.isNullable = expression.isNullable();
      this.dataType = pDataTypeInput;
      this.maxLength = expression.getMaxLength();
      this.scale = expression.getScale();
      this.sortOrder = expression.getSortOrder();
    }

    @Override
    public boolean isNullable() {
      return isNullable;
    }

    @Override
    public PDataType getDataType() {
      return dataType;
    }

    @Override
    public Integer getMaxLength() {
      return maxLength;
    }

    @Override
    public Integer getScale() {
      return scale;
    }

    @Override
    public SortOrder getSortOrder() {
      return sortOrder;
    }

  }

  private static class ExpressionDeterminism {
    private ArithmeticParseNode node;
    private List<Expression> children;
    private PDataType theType;
    private Determinism determinism;

    ExpressionDeterminism(ArithmeticParseNode node, List<Expression> children, PDataType theType,
      Determinism determinism) {
      this.node = node;
      this.children = children;
      this.theType = theType;
      this.determinism = determinism;
    }

    PDataType getDataType() {
      return theType;
    }

    Determinism getDeterminism() {
      return determinism;
    }

    public ExpressionDeterminism invoke() throws TypeMismatchException {
      for (Expression e : children) {
        determinism = determinism.combine(e.getDeterminism());
        PDataType type = e.getDataType();
        if (type == null) {
          continue;
        }
        if (type == PDecimal.INSTANCE) {
          theType = PDecimal.INSTANCE;
        } else if (type.isCoercibleTo(PLong.INSTANCE)) {
          if (theType == null) {
            theType = PLong.INSTANCE;
          }
        } else if (type.isCoercibleTo(PDouble.INSTANCE)) {
          if (theType == null) {
            theType = PDouble.INSTANCE;
          }
        } else {
          throw TypeMismatchException.newException(type, node.toString());
        }
      }
      return this;
    }
  }

}
