TemporalConversionProperties.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.util;

import org.apache.doris.nereids.trees.expressions.literal.TimeStampNsLiteral;
import org.apache.doris.nereids.types.ArrayType;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.DateTimeV2Type;
import org.apache.doris.nereids.types.MapType;
import org.apache.doris.nereids.types.StructField;
import org.apache.doris.nereids.types.StructType;
import org.apache.doris.nereids.types.TimeStampNsType;
import org.apache.doris.nereids.types.TimeStampTzType;

import java.util.List;

/**
 * Properties of a temporal conversion with a given source and target: calendar range,
 * literal exactness and nullable output layout. Shared by implicit coercion and explicit CAST.
 *
 * <p>This class does not choose target types, admit function overloads or select a cast mode.
 * {@link TypeCoercionUtils} chooses common types, applies implicit conversion and constructs casts.
 * Rewrite rules also consume these properties.
 * Explicit CAST legality remains in CheckCast, and
 * timezone value conversion is implemented by {@link DateUtils}.
 */
public final class TemporalConversionProperties {
    private TemporalConversionProperties() {
    }

    /**
     * Whether a date-like source is converted to the narrower TIMESTAMP_NS calendar range,
     * including aligned ARRAY, MAP and STRUCT components. This describes the type ranges,
     * not whether a particular value overflows or which cast mode the caller should use.
     */
    public static boolean narrowsCalendarRangeToTimeStampNs(DataType sourceType, DataType targetType) {
        if (sourceType instanceof ArrayType && targetType instanceof ArrayType) {
            return narrowsCalendarRangeToTimeStampNs(((ArrayType) sourceType).getItemType(),
                    ((ArrayType) targetType).getItemType());
        } else if (sourceType instanceof MapType && targetType instanceof MapType) {
            MapType sourceMap = (MapType) sourceType;
            MapType targetMap = (MapType) targetType;
            return narrowsCalendarRangeToTimeStampNs(sourceMap.getKeyType(), targetMap.getKeyType())
                    || narrowsCalendarRangeToTimeStampNs(sourceMap.getValueType(), targetMap.getValueType());
        } else if (sourceType instanceof StructType && targetType instanceof StructType) {
            List<StructField> sourceFields = ((StructType) sourceType).getFields();
            List<StructField> targetFields = ((StructType) targetType).getFields();
            // CheckCast rejects different field counts; such structs have no aligned conversion to inspect.
            if (sourceFields.size() != targetFields.size()) {
                return false;
            }
            for (int i = 0; i < sourceFields.size(); i++) {
                if (narrowsCalendarRangeToTimeStampNs(sourceFields.get(i).getDataType(),
                        targetFields.get(i).getDataType())) {
                    return true;
                }
            }
            return false;
        }
        return sourceType.isDateLikeType() && !sourceType.isTimeStampNsType() && targetType.isTimeStampNsType();
    }

    /**
     * Whether an already evaluated NS value is exactly representable by another temporal type.
     * Callers must evaluate any enclosing CAST before checking the result: a loss of precision
     * explicitly requested by that CAST is distinct from the subsequent implicit conversion.
     * This property neither evaluates expressions nor determines explicit CAST legality.
     */
    public static boolean isTimeStampNsLiteralExactlyRepresentable(
            TimeStampNsLiteral literal, DataType targetType) {
        if (targetType.isDateType() || targetType.isDateV2Type()) {
            return literal.isMidnight();
        }
        if (targetType.isDateTimeType()) {
            return literal.getNanoSecond() == 0;
        }
        if (targetType instanceof DateTimeV2Type) {
            int scale = ((DateTimeV2Type) targetType).getScale();
            if (scale < 0) {
                scale = DateTimeV2Type.MAX_SCALE;
            }
            return isFractionExactlyRepresentable(literal.getNanoSecond(), scale);
        }
        if (targetType instanceof TimeStampTzType) {
            int scale = ((TimeStampTzType) targetType).getScale();
            if (scale < 0) {
                scale = TimeStampTzType.MAX_SCALE;
            }
            return isFractionExactlyRepresentable(literal.getNanoSecond(), scale);
        }
        return false;
    }

    /** Exact fractional representability; unlike CAST, this must not round the value. */
    private static boolean isFractionExactlyRepresentable(long nanos, int scale) {
        long factor = (long) Math.pow(10, TimeStampNsType.SCALE - scale);
        return nanos % factor == 0;
    }

    /** Nullable output layout required by temporal CAST kernels, independent of strict mode. */
    public static boolean hasNullableResult(DataType source, DataType target) {
        if (source.equals(target)) {
            return false;
        }
        if ((source.isDateLikeType() || source.isTimeType()) && target.isTimeStampNsType()) {
            // Nanosecond precision comes with a narrower signed epoch-nanosecond range.
            return true;
        }
        // Scale reduction may overflow at the calendar boundary; zoned conversions can fail
        // while resolving a local datetime. Preserve the kernels' nullable layout in either mode.
        return (source.isDateTimeV2Type() && target.isDateTimeV2Type())
                || (source.isTimeStampTzType() && target.isDateTimeV2Type())
                || ((source.isDateTimeV2Type() || source.isTimeStampNsType() || source.isTimeStampTzType())
                    && target.isTimeStampTzType());
    }
}