DateTrunc.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.scalar;

import org.apache.doris.catalog.FunctionSignature;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.functions.CustomSignature;
import org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
import org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic;
import org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.format.DateTimeChecker;
import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.DateTimeV2Type;
import org.apache.doris.nereids.types.TimeStampTzType;
import org.apache.doris.nereids.types.VarcharType;
import org.apache.doris.nereids.util.ExpressionUtils;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

import java.util.List;
import java.util.Optional;

/**
 * ScalarFunction 'date_trunc'. This class is generated by GenerateFunction.
 */
public class DateTrunc extends ScalarFunction
        implements BinaryExpression, PropagateNullLiteral, PropagateNullable, RoundingMonotonic, CustomSignature {
    private static final List<String> LEGAL_TIME_UNIT =
            ImmutableList.of("year", "quarter", "month", "week", "day", "hour", "minute", "second");

    /**
     * constructor with 2 arguments.
     */
    public DateTrunc(Expression arg0, Expression arg1) {
        super("date_trunc", arg0, arg1);
    }

    /** constructor for withChildren and reuse signature */
    private DateTrunc(ScalarFunctionParams functionParams) {
        super(functionParams);
    }

    @Override
    public Expression prepareBeforeTypeCoercion() {
        // When an argument is a date, the other one is the time unit, and the signature does not need its value.
        if (getArgument(0).getDataType().isDateLikeType() || getArgument(1).getDataType().isDateLikeType()) {
            return this;
        }
        // Otherwise customSignature tells the time unit from the date value by the literal time unit, so fold a
        // constant string that evaluates to a time unit, unless the other argument already is one. A string date
        // value is kept unfolded, because folding it would change the derived return type.
        return withChildren((argument, index) -> {
            if (!argument.getDataType().isStringLikeType() || isTimeUnit(getArgument(1 - index))) {
                return argument;
            }
            Expression folded = ExpressionUtils.foldConstantArgument(argument);
            return isTimeUnit(folded) ? folded : argument;
        });
    }

    private static boolean isTimeUnit(Expression expression) {
        return expression instanceof StringLikeLiteral
                && LEGAL_TIME_UNIT.contains(((StringLikeLiteral) expression).getStringValue().toLowerCase());
    }

    private static boolean isConstantString(Expression expression) {
        return expression.isConstant() && expression.getDataType().isStringLikeType();
    }

    /**
     * Tells the date value from the time unit. Returns the index of the date argument, or -1 when neither
     * argument identifies its role on FE, e.g. two string constants only BE can evaluate.
     */
    private int dateArgumentIndex() {
        for (int i = 0; i < 2; i++) {
            if (getArgument(i).getDataType().isDateLikeType()) {
                return i;
            }
        }
        // A nonconstant argument, e.g. a VARCHAR column, can only be the date value.
        for (int i = 0; i < 2; i++) {
            if (!getArgument(i).isConstant()) {
                return i;
            }
        }
        // prepareBeforeTypeCoercion folded the constant that evaluates to a time unit, so a literal time unit
        // tells that the other argument is the date value.
        for (int i = 0; i < 2; i++) {
            if (isTimeUnit(getArgument(i))) {
                return 1 - i;
            }
        }
        // A string FE evaluates to anything else is the date value, and the other argument is then the time unit,
        // which BE validates when FE cannot evaluate it.
        for (int i = 0; i < 2; i++) {
            if (ExpressionUtils.foldConstantArgument(getArgument(i)) instanceof StringLikeLiteral) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public void checkLegalityBeforeTypeCoercion() {
        int dateIndex = dateArgumentIndex();
        if (dateIndex < 0 || !isConstantString(getArgument(1 - dateIndex))) {
            throw new AnalysisException("the time unit parameter of "
                    + getName() + " function must be a string constant: " + toSql());
        }
        // Validate the time unit value FE can evaluate here, because constant folding may remove this function
        // before any later check. BE validates a time unit FE cannot evaluate.
        Expression timeUnit = ExpressionUtils.foldConstantArgument(getArgument(1 - dateIndex));
        if (timeUnit instanceof StringLikeLiteral && !isTimeUnit(timeUnit)) {
            throw new AnalysisException("date_trunc function time unit param only support argument is "
                    + String.join("|", LEGAL_TIME_UNIT));
        }
    }

    /**
     * withChildren.
     */
    @Override
    public DateTrunc withChildren(List<Expression> children) {
        Preconditions.checkArgument(children.size() == 2);
        return new DateTrunc(getFunctionParams(children));
    }

    @Override
    public FunctionSignature customSignature() {
        // should never return V1 Type
        int dateIndex = dateArgumentIndex();
        if (dateIndex < 0) {
            // checkLegalityBeforeTypeCoercion rejects the call, so just return a signature here
            return FunctionSignature.ret(DateTimeV2Type.WILDCARD)
                    .args(VarcharType.SYSTEM_DEFAULT, DateTimeV2Type.WILDCARD);
        }
        Expression date = getArgument(dateIndex);
        DataType type;
        if (date.getDataType() instanceof TimeStampTzType) {
            // Handle TimeStampTzType first, because getCurrentType() would convert it to DateTimeV2Type
            type = date.getDataType();
        } else if (date.getDataType().isDateLikeType()) {
            type = DataType.getCurrentType(date.getDataType());
        } else if (date instanceof StringLikeLiteral
                && DateTimeChecker.hasTimeZone(((StringLikeLiteral) date).getStringValue())) {
            // a literal datetime string with timezone information
            type = TimeStampTzType.SYSTEM_DEFAULT;
        } else {
            type = DateTimeV2Type.WILDCARD;
        }
        return dateIndex == 0 ? FunctionSignature.ret(type).args(type, VarcharType.SYSTEM_DEFAULT)
                : FunctionSignature.ret(type).args(VarcharType.SYSTEM_DEFAULT, type);
    }

    @Override
    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
        return visitor.visitDateTrunc(this, context);
    }

    @Override
    public boolean isPositive() {
        return true;
    }

    @Override
    public RoundingType getRoundingType() {
        return RoundingType.FLOOR;
    }

    @Override
    public Optional<Expression> nextBucketBoundary(Literal value) {
        int dateArgumentIndex = getMonotonicFunctionChildIndex();
        Expression unit = child(dateArgumentIndex == 0 ? 1 : 0);
        if (!(unit instanceof StringLikeLiteral)) {
            return Optional.empty();
        }
        IntegerLiteral one = new IntegerLiteral(1);
        switch (((StringLikeLiteral) unit).getStringValue().toLowerCase()) {
            case "year":
                return Optional.of(new YearsAdd(value, one));
            case "quarter":
                return Optional.of(new QuartersAdd(value, one));
            case "month":
                return Optional.of(new MonthsAdd(value, one));
            case "week":
                return Optional.of(new WeeksAdd(value, one));
            case "day":
                return Optional.of(new DaysAdd(value, one));
            case "hour":
                return Optional.of(new HoursAdd(value, one));
            case "minute":
                return Optional.of(new MinutesAdd(value, one));
            case "second":
                return Optional.of(new SecondsAdd(value, one));
            default:
                return Optional.empty();
        }
    }

    @Override
    public boolean isRoundingRelationGuaranteed() {
        int dateArgumentIndex = getMonotonicFunctionChildIndex();
        Expression unit = child(dateArgumentIndex == 0 ? 1 : 0);
        return unit instanceof StringLikeLiteral
                && LEGAL_TIME_UNIT.contains(((StringLikeLiteral) unit).getStringValue().toLowerCase());
    }

    @Override
    public int getMonotonicFunctionChildIndex() {
        return getArgument(0).getDataType().isDateLikeType() ? 0 : 1;
    }

    @Override
    public Expression withConstantArgs(Expression literal) {
        return getArgument(0).getDataType().isDateLikeType()
                ? new DateTrunc(literal, child(1)) : new DateTrunc(child(0), literal);
    }
}