SqlLikeOperator.java

/*
 * 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.calcite.sql.fun;

import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperandCountRange;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlSpecialOperator;
import org.apache.calcite.sql.SqlWriter;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.parser.SqlParserUtil;
import org.apache.calcite.sql.type.InferTypes;
import org.apache.calcite.sql.type.OperandTypes;
import org.apache.calcite.sql.type.ReturnTypes;
import org.apache.calcite.sql.type.SqlOperandCountRanges;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.Litmus;

/**
 * An operator describing the <code>LIKE</code> and <code>SIMILAR</code>
 * operators.
 *
 * <p>Syntax of the two operators:
 *
 * <ul>
 * <li><code>src-value [NOT] LIKE pattern-value [ESCAPE
 * escape-value]</code></li>
 * <li><code>src-value [NOT] SIMILAR pattern-value [ESCAPE
 * escape-value]</code></li>
 * </ul>
 *
 * <p><b>NOTE</b> If the <code>NOT</code> clause is present the
 * {@link org.apache.calcite.sql.parser.SqlParser parser} will generate a
 * equivalent to <code>NOT (src LIKE pattern ...)</code>
 */
public class SqlLikeOperator extends SqlSpecialOperator {
  //~ Instance fields --------------------------------------------------------

  private final boolean negated;
  private final boolean caseSensitive;

  //~ Constructors -----------------------------------------------------------

  /**
   * Creates a SqlLikeOperator.
   *
   * @param name        Operator name
   * @param kind        Kind
   * @param negated     Whether this is 'NOT LIKE'
   * @param caseSensitive Whether this operator ignores the case of its operands
   */
  SqlLikeOperator(
      String name,
      SqlKind kind,
      boolean negated,
      boolean caseSensitive) {
    // LIKE is right-associative, because that makes it easier to capture
    // dangling ESCAPE clauses: "a like b like c escape d" becomes
    // "a like (b like c escape d)".
    super(
        name,
        kind,
        32,
        false,
        ReturnTypes.BOOLEAN_NULLABLE,
        InferTypes.FIRST_KNOWN,
        OperandTypes.STRING_SAME_SAME_SAME);
    if (!caseSensitive && kind != SqlKind.LIKE) {
      throw new IllegalArgumentException("Only (possibly negated) "
          + SqlKind.LIKE + " can be made case-insensitive, not " + kind);
    }

    this.negated = negated;
    this.caseSensitive = caseSensitive;
  }

  //~ Methods ----------------------------------------------------------------

  /**
   * Returns whether this is the 'NOT LIKE' operator.
   *
   * @return whether this is 'NOT LIKE'
   *
   * @see #not()
   */
  public boolean isNegated() {
    return negated;
  }

  /**
   * Returns whether this operator matches the case of its operands.
   * For example, returns true for {@code LIKE} and false for {@code ILIKE}.
   *
   * @return whether this operator matches the case of its operands
   */
  public boolean isCaseSensitive() {
    return caseSensitive;
  }

  @Override public SqlOperator not() {
    return of(kind, !negated, caseSensitive);
  }

  private static SqlOperator of(SqlKind kind, boolean negated,
      boolean caseSensitive) {
    switch (kind) {
    case SIMILAR:
      return negated
          ? SqlStdOperatorTable.NOT_SIMILAR_TO
          : SqlStdOperatorTable.SIMILAR_TO;
    case RLIKE:
      return negated
          ? SqlLibraryOperators.NOT_RLIKE
          : SqlLibraryOperators.RLIKE;
    case LIKE:
      if (caseSensitive) {
        return negated
            ? SqlStdOperatorTable.NOT_LIKE
            : SqlStdOperatorTable.LIKE;
      } else {
        return negated
            ? SqlLibraryOperators.NOT_ILIKE
            : SqlLibraryOperators.ILIKE;
      }
    default:
      throw new AssertionError("unexpected " + kind);
    }
  }

  @Override public SqlOperandCountRange getOperandCountRange() {
    return SqlOperandCountRanges.between(2, 3);
  }

  @Override public boolean checkOperandTypes(
      SqlCallBinding callBinding,
      boolean throwOnFailure) {
    switch (callBinding.getOperandCount()) {
    case 2:
      if (!OperandTypes.STRING_SAME_SAME.checkOperandTypes(
          callBinding,
          throwOnFailure)) {
        return false;
      }
      break;
    case 3:
      if (!OperandTypes.STRING_SAME_SAME_SAME.checkOperandTypes(
          callBinding,
          throwOnFailure)) {
        return false;
      }

      // calc implementation should
      // enforce the escape character length to be 1
      break;
    default:
      throw new AssertionError("unexpected number of args to "
          + callBinding.getCall() + ": " + callBinding.getOperandCount());
    }

    return SqlTypeUtil.isCharTypeComparable(
        callBinding,
        callBinding.operands(),
        throwOnFailure);
  }

  @Override public void validateCall(SqlCall call, SqlValidator validator,
      SqlValidatorScope scope, SqlValidatorScope operandScope) {
    super.validateCall(call, validator, scope, operandScope);
  }

  @Override public boolean validRexOperands(int count, Litmus litmus) {
    if (negated) {
      litmus.fail("unsupported negated operator {}", this);
    }
    return super.validRexOperands(count, litmus);
  }

  @Override public void unparse(
      SqlWriter writer,
      SqlCall call,
      int leftPrec,
      int rightPrec) {
    final SqlWriter.Frame frame = writer.startList("", "");
    call.operand(0).unparse(writer, getLeftPrec(), getRightPrec());
    writer.sep(getName());

    call.operand(1).unparse(writer, getLeftPrec(), getRightPrec());
    if (call.operandCount() == 3) {
      writer.sep("ESCAPE");
      call.operand(2).unparse(writer, getLeftPrec(), getRightPrec());
    }
    writer.endList(frame);
  }

  @Override public ReduceResult reduceExpr(
      final int opOrdinal,
      TokenSequence list) {
    // Example:
    //   a LIKE b || c ESCAPE d || e AND f
    // |  |    |      |      |      |
    //  exp0    exp1          exp2
    SqlNode exp0 = list.node(opOrdinal - 1);
    SqlOperator op = list.op(opOrdinal);
    assert op instanceof SqlLikeOperator;
    SqlNode exp1 =
        SqlParserUtil.toTreeEx(
            list,
            opOrdinal + 1,
            getRightPrec(),
            SqlKind.ESCAPE);
    SqlNode exp2 = null;
    if ((opOrdinal + 2) < list.size()) {
      if (list.isOp(opOrdinal + 2)) {
        final SqlOperator op2 = list.op(opOrdinal + 2);
        if (op2.getKind() == SqlKind.ESCAPE) {
          exp2 =
              SqlParserUtil.toTreeEx(
                  list,
                  opOrdinal + 3,
                  getRightPrec(),
                  SqlKind.ESCAPE);
        }
      }
    }
    final SqlNode[] operands;
    final int end;
    if (exp2 != null) {
      operands = new SqlNode[]{exp0, exp1, exp2};
      end = opOrdinal + 4;
    } else {
      operands = new SqlNode[]{exp0, exp1};
      end = opOrdinal + 2;
    }
    SqlCall call = createCall(SqlParserPos.sum(operands), operands);
    return new ReduceResult(opOrdinal - 1, end, call);
  }
}