RangeInferable.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.trees.expressions.functions;

import org.apache.doris.common.Pair;
import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
import org.apache.doris.nereids.rules.expression.rules.ColumnRange;
import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;

import java.util.Optional;

/** Infer an enclosing result range without requiring the expression to be monotonic. */
public interface RangeInferable extends ExpressionTrait {
    int getRangeInputChildIndex();

    // Replace the range input with a constant while preserving the other arguments.
    Expression withConstantArgs(Expression literal);

    /**
     * Return a range containing every possible result, or empty when no safe range can be inferred.
     * A null lower or upper endpoint means that the input is unbounded on that side.
     */
    Optional<ColumnRange> inferRange(Literal lower, Literal upper, ExpressionRewriteContext context);

    /** Fold the input endpoints, retaining null for an unbounded side, and reject SQL NULL or unfolded results. */
    default Optional<Pair<Literal, Literal>> evaluateRangeEndpoints(
            Literal lower, Literal upper, ExpressionRewriteContext context) {
        Expression lowerValue = lower == null ? null
                : FoldConstantRuleOnFE.evaluate(withConstantArgs(lower), context);
        Expression upperValue = upper == null ? null
                : FoldConstantRuleOnFE.evaluate(withConstantArgs(upper), context);
        if (lowerValue instanceof NullLiteral || upperValue instanceof NullLiteral
                || lowerValue != null && !(lowerValue instanceof Literal)
                || upperValue != null && !(upperValue instanceof Literal)) {
            return Optional.empty();
        }
        return Optional.of(Pair.of((Literal) lowerValue, (Literal) upperValue));
    }

    /** Build inclusive result bounds. Input endpoint exclusions need not hold after applying a function. */
    static Optional<ColumnRange> createRange(Literal lower, Literal upper) {
        ColumnRange range = ColumnRange.all();
        if (lower != null) {
            range = range.withLowerBound(lower);
        }
        if (upper != null) {
            range = range.withUpperBound(upper);
        }
        if (range.isEmptyRange() || !range.span().hasLowerBound() && !range.span().hasUpperBound()) {
            return Optional.empty();
        }
        return Optional.of(range);
    }
}