content: /*
   * 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);
    }
  }
  