blob: b577afd59a8867bed2fd483e23c67248b9e80795 [file] [log] [blame]
/*
* 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);
}
}