StringEmptyToLengthRule.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.doris.nereids.rules.expression.rules;
import org.apache.doris.nereids.rules.expression.ExpressionPatternMatcher;
import org.apache.doris.nereids.rules.expression.ExpressionPatternRuleFactory;
import org.apache.doris.nereids.rules.expression.ExpressionRuleType;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.Not;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Length;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import com.google.common.collect.ImmutableList;
import java.util.List;
/**
* Rewrites comparisons with empty strings to equivalent length()-based expressions so that
* the NestedColumnPruning OFFSET optimization can apply.
*
* <ul>
* <li>{@code str_col = ''} → {@code length(str_col) = 0}</li>
* <li>{@code str_col <> ''} → {@code length(str_col) != 0}
* (represented as {@code NOT(length(str_col) = 0)})</li>
* </ul>
*
* This is a semantics-preserving rewrite: for any non-NULL string, {@code s = ''} is equivalent
* to {@code length(s) = 0}; for NULL, both sides evaluate to NULL.
*
* Only applies when the compared expression is a direct {@link SlotReference} of string-like type,
* so that the resulting {@code length(slot)} call can benefit from OFFSET-only column reading.
*/
public class StringEmptyToLengthRule implements ExpressionPatternRuleFactory {
public static final StringEmptyToLengthRule INSTANCE = new StringEmptyToLengthRule();
@Override
public List<ExpressionPatternMatcher<? extends Expression>> buildRules() {
return ImmutableList.of(
// str_col = '' → length(str_col) = 0
matchesType(EqualTo.class)
.thenApply(ctx -> rewriteEqualToEmpty(ctx.expr))
.toRule(ExpressionRuleType.STRING_EMPTY_TO_LENGTH),
// NOT(str_col = '') → NOT(length(str_col) = 0) (i.e. str_col <> '')
matchesType(Not.class)
.thenApply(ctx -> {
Not not = ctx.expr;
if (!(not.child() instanceof EqualTo)) {
return not;
}
Expression rewritten = rewriteEqualToEmpty((EqualTo) not.child());
if (rewritten == not.child()) {
return not;
}
return new Not(rewritten);
})
.toRule(ExpressionRuleType.STRING_EMPTY_TO_LENGTH)
);
}
/**
* If {@code equalTo} compares a string-typed SlotReference against an empty-string literal,
* rewrites it to {@code length(slot) = 0}. Returns the original expression unchanged otherwise.
*/
private static Expression rewriteEqualToEmpty(EqualTo equalTo) {
Expression left = equalTo.left();
Expression right = equalTo.right();
SlotReference slot = null;
if (isStringSlot(left) && isEmptyStringLiteral(right)) {
slot = (SlotReference) left;
} else if (isStringSlot(right) && isEmptyStringLiteral(left)) {
slot = (SlotReference) right;
}
if (slot == null) {
return equalTo;
}
return new EqualTo(new Length(slot), new IntegerLiteral(0));
}
private static boolean isStringSlot(Expression expr) {
return expr instanceof SlotReference && expr.getDataType().isStringLikeType();
}
private static boolean isEmptyStringLiteral(Expression expr) {
return expr instanceof Literal
&& expr.getDataType().isStringLikeType()
&& ((Literal) expr).getStringValue().isEmpty();
}
}