DateTimeArithmetic.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.executable;

import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.expressions.ExecFunction;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Interval.TimeUnit;
import org.apache.doris.nereids.trees.expressions.literal.TimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;

/**
 * executable function: date_add/sub, years/quarters/months/week/days/hours/minutes/seconds_add/sub, datediff
 */
public class DateTimeArithmetic {
    private static final long[] POW_10 = { 1, 10, 100, 1000, 10000, 100000, 1000000 };
    private static final long MICROS_PER_SECOND = 1_000_000L;
    private static final long MICROS_PER_MINUTE = Math.multiplyExact(MICROS_PER_SECOND, 60L);
    private static final long MICROS_PER_HOUR = Math.multiplyExact(MICROS_PER_MINUTE, 60L);
    private static final long MICROS_PER_DAY = Math.multiplyExact(MICROS_PER_HOUR, 24L);

    private static long parseYearMonthToMonths(String raw) {
        long[] values = parseIntervalValues(raw, TimeUnit.YEAR_MONTH);
        long months = Math.addExact(Math.multiplyExact(values[0], 12L), values[1]);
        return months;
    }

    private static long parseIntervalToMicros(String raw, TimeUnit unit) {
        if (unit == TimeUnit.YEAR_MONTH) {
            throw new AnalysisException("YEAR_MONTH should be handled via months parsing");
        }
        long[] values = parseIntervalValues(raw, unit);
        try {
            long micros;
            switch (unit) {
                case DAY_HOUR: {
                    long dayMicros = Math.multiplyExact(values[0], MICROS_PER_DAY);
                    long hourMicros = Math.multiplyExact(values[1], MICROS_PER_HOUR);
                    micros = Math.addExact(dayMicros, hourMicros);
                    break;
                }
                case DAY_MINUTE: {
                    long dayMicros = Math.multiplyExact(values[0], MICROS_PER_DAY);
                    long hourMicros = Math.multiplyExact(values[1], MICROS_PER_HOUR);
                    long minuteMicros = Math.multiplyExact(values[2], MICROS_PER_MINUTE);
                    micros = Math.addExact(Math.addExact(dayMicros, hourMicros), minuteMicros);
                    break;
                }
                case DAY_SECOND: {
                    long dayMicros = Math.multiplyExact(values[0], MICROS_PER_DAY);
                    long hourMicros = Math.multiplyExact(values[1], MICROS_PER_HOUR);
                    long minuteMicros = Math.multiplyExact(values[2], MICROS_PER_MINUTE);
                    long secondMicros = Math.multiplyExact(values[3], MICROS_PER_SECOND);
                    micros = Math.addExact(Math.addExact(dayMicros, hourMicros),
                            Math.addExact(minuteMicros, secondMicros));
                    break;
                }
                case DAY_MICROSECOND: {
                    long dayMicros = Math.multiplyExact(values[0], MICROS_PER_DAY);
                    long hourMicros = Math.multiplyExact(values[1], MICROS_PER_HOUR);
                    long minuteMicros = Math.multiplyExact(values[2], MICROS_PER_MINUTE);
                    long secondMicros = Math.multiplyExact(values[3], MICROS_PER_SECOND);
                    micros = Math.addExact(Math.addExact(dayMicros, hourMicros),
                            Math.addExact(Math.addExact(minuteMicros, secondMicros), values[4]));
                    break;
                }
                case HOUR_MINUTE: {
                    long hourMicros = Math.multiplyExact(values[0], MICROS_PER_HOUR);
                    long minuteMicros = Math.multiplyExact(values[1], MICROS_PER_MINUTE);
                    micros = Math.addExact(hourMicros, minuteMicros);
                    break;
                }
                case HOUR_SECOND: {
                    long hourMicros = Math.multiplyExact(values[0], MICROS_PER_HOUR);
                    long minuteMicros = Math.multiplyExact(values[1], MICROS_PER_MINUTE);
                    long secondMicros = Math.multiplyExact(values[2], MICROS_PER_SECOND);
                    micros = Math.addExact(Math.addExact(hourMicros, minuteMicros), secondMicros);
                    break;
                }
                case HOUR_MICROSECOND: {
                    long hourMicros = Math.multiplyExact(values[0], MICROS_PER_HOUR);
                    long minuteMicros = Math.multiplyExact(values[1], MICROS_PER_MINUTE);
                    long secondMicros = Math.multiplyExact(values[2], MICROS_PER_SECOND);
                    micros = Math.addExact(Math.addExact(hourMicros, minuteMicros),
                            Math.addExact(secondMicros, values[3]));
                    break;
                }
                case MINUTE_SECOND: {
                    long minuteMicros = Math.multiplyExact(values[0], MICROS_PER_MINUTE);
                    long secondMicros = Math.multiplyExact(values[1], MICROS_PER_SECOND);
                    micros = Math.addExact(minuteMicros, secondMicros);
                    break;
                }
                case MINUTE_MICROSECOND: {
                    long minuteMicros = Math.multiplyExact(values[0], MICROS_PER_MINUTE);
                    long secondMicros = Math.multiplyExact(values[1], MICROS_PER_SECOND);
                    micros = Math.addExact(Math.addExact(minuteMicros, secondMicros), values[2]);
                    break;
                }
                case SECOND_MICROSECOND: {
                    long secondMicros = Math.multiplyExact(values[0], MICROS_PER_SECOND);
                    micros = Math.addExact(secondMicros, values[1]);
                    break;
                }
                default:
                    throw new AnalysisException("Unsupported time unit: " + unit);
            }
            return micros;
        } catch (ArithmeticException ex) {
            throw new AnalysisException("Interval out of range", ex);
        }
    }

    private static long[] parseIntervalValues(String raw, TimeUnit unit) {
        String str = raw.trim();
        if (str.isEmpty()) {
            throw new AnalysisException("Invalid time format");
        }

        int expectedParts;
        boolean transformMicro = false;
        switch (unit) {
            case YEAR_MONTH:
                expectedParts = 2;
                break;
            case DAY_HOUR:
                expectedParts = 2;
                break;
            case DAY_MINUTE:
                expectedParts = 3;
                break;
            case DAY_SECOND:
                expectedParts = 4;
                break;
            case DAY_MICROSECOND:
                expectedParts = 5;
                transformMicro = true;
                break;
            case HOUR_MINUTE:
                expectedParts = 2;
                break;
            case HOUR_SECOND:
                expectedParts = 3;
                break;
            case HOUR_MICROSECOND:
                expectedParts = 4;
                transformMicro = true;
                break;
            case MINUTE_SECOND:
                expectedParts = 2;
                break;
            case MINUTE_MICROSECOND:
                expectedParts = 3;
                transformMicro = true;
                break;
            case SECOND_MICROSECOND:
                expectedParts = 2;
                transformMicro = true;
                break;
            default:
                throw new AnalysisException("Unsupported time unit: " + unit);
        }

        long[] values = new long[expectedParts];
        boolean negative = false;
        int idx = 0;
        int len = str.length();

        while (idx < len && Character.isWhitespace(str.charAt(idx))) {
            idx++;
        }
        if (idx < len && str.charAt(idx) == '-') {
            negative = true;
            idx++;
        }

        idx = advanceToDigit(str, idx);

        int lastDigits = 0;
        for (int i = 0; i < expectedParts; i++) {
            if (idx >= len || !Character.isDigit(str.charAt(idx))) {
                throw new AnalysisException("Invalid time format");
            }
            long val = 0;
            lastDigits = 0;
            while (idx < len && Character.isDigit(str.charAt(idx))) {
                val = val * 10 + (str.charAt(idx) - '0');
                idx++;
                lastDigits++;
                if (val < 0) {
                    throw new AnalysisException("Invalid time format");
                }
            }
            values[i] = val;

            idx = advanceToDigit(str, idx);
            if (idx == len && i != expectedParts - 1) {
                int filled = i + 1;
                System.arraycopy(values, 0, values, expectedParts - filled, filled);
                Arrays.fill(values, 0, expectedParts - filled, 0L);
                break;
            }
        }

        if (idx != len) {
            throw new AnalysisException("Invalid time format");
        }

        if (transformMicro) {
            int microIndex = expectedParts - 1;
            int digits = lastDigits;
            if (digits > 0 && digits < 6) {
                values[microIndex] = Math.multiplyExact(values[microIndex], POW_10[6 - digits]);
            }
        }

        if (negative) {
            for (int i = 0; i < values.length; i++) {
                values[i] = -values[i];
            }
        }
        return values;
    }

    private static int advanceToDigit(String str, int idx) {
        int len = str.length();
        while (idx < len && !Character.isDigit(str.charAt(idx))) {
            idx++;
        }
        return idx;
    }

    private static Expression applyInterval(DateTimeV2Literal date, VarcharLiteral delta, TimeUnit unit,
            boolean add) {
        if (unit == TimeUnit.YEAR_MONTH) {
            long months = parseYearMonthToMonths(delta.getStringValue());
            months = add ? months : -months;
            return date.plusMonths(months);
        }

        long micros = parseIntervalToMicros(delta.getStringValue(), unit);
        micros = add ? micros : -micros;
        Expression result = date.plusMicroSeconds(micros);
        if (result instanceof DateTimeV2Literal) {
            DateTimeV2Literal dt = (DateTimeV2Literal) result;
            if (dt.getScale() != date.getScale()) {
                // Keep original scale to match existing folding behavior.
                return DateTimeV2Literal.fromJavaDateType(dt.toJavaDateType(), date.getScale());
            }
        }
        return result;
    }

    private static Expression applyInterval(TimestampTzLiteral date, VarcharLiteral delta, TimeUnit unit,
            boolean add) {
        if (unit == TimeUnit.YEAR_MONTH) {
            long months = parseYearMonthToMonths(delta.getStringValue());
            months = add ? months : -months;
            return date.plusMonths(months);
        }

        long micros = parseIntervalToMicros(delta.getStringValue(), unit);
        micros = add ? micros : -micros;
        Expression result = date.plusMicroSeconds(micros);
        if (result instanceof TimestampTzLiteral) {
            TimestampTzLiteral dt = (TimestampTzLiteral) result;
            if (dt.getScale() != date.getScale()) {
                // Keep original scale to match existing folding behavior.
                return TimestampTzLiteral.fromJavaDateType(dt.toJavaDateType(), date.getScale());
            }
        }
        return result;
    }

    /**
     * datetime arithmetic function date-add.
     */
    @ExecFunction(name = "date_add")
    public static Expression dateAdd(DateV2Literal date, IntegerLiteral day) {
        return daysAdd(date, day);
    }

    @ExecFunction(name = "date_add")
    public static Expression dateAdd(DateTimeV2Literal date, IntegerLiteral day) {
        return daysAdd(date, day);
    }

    @ExecFunction(name = "date_add")
    public static Expression dateAdd(TimestampTzLiteral date, IntegerLiteral day) {
        return daysAdd(date, day);
    }

    /**
     * datetime arithmetic function day_hour-add.
     */
    @ExecFunction(name = "day_hour_add")
    public static Expression dayHourAdd(DateTimeV2Literal date, VarcharLiteral dayHour) {
        return applyInterval(date, dayHour, TimeUnit.DAY_HOUR, true);
    }

    @ExecFunction(name = "day_hour_add")
    public static Expression dayHourAdd(TimestampTzLiteral date, VarcharLiteral dayHour) {
        return applyInterval(date, dayHour, TimeUnit.DAY_HOUR, true);
    }

    @ExecFunction(name = "day_hour_sub")
    public static Expression dayHourSub(DateTimeV2Literal date, VarcharLiteral dayHour) {
        return applyInterval(date, dayHour, TimeUnit.DAY_HOUR, false);
    }

    @ExecFunction(name = "day_hour_sub")
    public static Expression dayHourSub(TimestampTzLiteral date, VarcharLiteral dayHour) {
        return applyInterval(date, dayHour, TimeUnit.DAY_HOUR, false);
    }

    /**
     * datetime arithmetic function minute_second-add.
     */
    @ExecFunction(name = "minute_second_add")
    public static Expression minuteSecondAdd(DateTimeV2Literal date, VarcharLiteral minuteSecond) {
        return applyInterval(date, minuteSecond, TimeUnit.MINUTE_SECOND, true);
    }

    @ExecFunction(name = "minute_second_add")
    public static Expression minuteSecondAdd(TimestampTzLiteral date, VarcharLiteral minuteSecond) {
        return applyInterval(date, minuteSecond, TimeUnit.MINUTE_SECOND, true);
    }

    @ExecFunction(name = "minute_second_sub")
    public static Expression minuteSecondSub(DateTimeV2Literal date, VarcharLiteral minuteSecond) {
        return applyInterval(date, minuteSecond, TimeUnit.MINUTE_SECOND, false);
    }

    @ExecFunction(name = "minute_second_sub")
    public static Expression minuteSecondSub(TimestampTzLiteral date, VarcharLiteral minuteSecond) {
        return applyInterval(date, minuteSecond, TimeUnit.MINUTE_SECOND, false);
    }

    /**
     * datetime arithmetic function second_microsecond-add.
     */
    @ExecFunction(name = "second_microsecond_add")
    public static Expression secondMicrosecondAdd(DateTimeV2Literal date, VarcharLiteral secondMicrosecond) {
        return applyInterval(date, secondMicrosecond, TimeUnit.SECOND_MICROSECOND, true);
    }

    @ExecFunction(name = "second_microsecond_add")
    public static Expression secondMicrosecondAdd(TimestampTzLiteral date, VarcharLiteral secondMicrosecond) {
        return applyInterval(date, secondMicrosecond, TimeUnit.SECOND_MICROSECOND, true);
    }

    @ExecFunction(name = "second_microsecond_sub")
    public static Expression secondMicrosecondSub(DateTimeV2Literal date, VarcharLiteral secondMicrosecond) {
        return applyInterval(date, secondMicrosecond, TimeUnit.SECOND_MICROSECOND, false);
    }

    @ExecFunction(name = "second_microsecond_sub")
    public static Expression secondMicrosecondSub(TimestampTzLiteral date, VarcharLiteral secondMicrosecond) {
        return applyInterval(date, secondMicrosecond, TimeUnit.SECOND_MICROSECOND, false);
    }

    /**
     * datetime arithmetic function date-sub.
     */
    @ExecFunction(name = "date_sub")
    public static Expression dateSub(DateV2Literal date, IntegerLiteral day) {
        return dateAdd(date, new IntegerLiteral(-day.getValue()));
    }

    @ExecFunction(name = "date_sub")
    public static Expression dateSub(DateTimeV2Literal date, IntegerLiteral day) {
        return dateAdd(date, new IntegerLiteral(-day.getValue()));
    }

    @ExecFunction(name = "date_sub")
    public static Expression dateSub(TimestampTzLiteral date, IntegerLiteral day) {
        return dateAdd(date, new IntegerLiteral(-day.getValue()));
    }

    /**
     * datetime arithmetic function years-add.
     */
    @ExecFunction(name = "years_add")
    public static Expression yearsAdd(DateV2Literal date, IntegerLiteral year) {
        return date.plusYears(year.getValue());
    }

    @ExecFunction(name = "years_add")
    public static Expression yearsAdd(DateTimeV2Literal date, IntegerLiteral year) {
        return date.plusYears(year.getValue());
    }

    @ExecFunction(name = "years_add")
    public static Expression yearsAdd(TimestampTzLiteral date, IntegerLiteral year) {
        return date.plusYears(year.getValue());
    }

    /**
     * datetime arithmetic function quarters-add.
     */
    @ExecFunction(name = "quarters_add")
    public static Expression quartersAdd(DateV2Literal date, IntegerLiteral quarter) {
        return date.plusMonths(3 * quarter.getValue());
    }

    @ExecFunction(name = "quarters_add")
    public static Expression quartersAdd(DateTimeV2Literal date, IntegerLiteral quarter) {
        return date.plusMonths(3 * quarter.getValue());
    }

    @ExecFunction(name = "quarters_add")
    public static Expression quartersAdd(TimestampTzLiteral date, IntegerLiteral quarter) {
        return date.plusMonths(3 * quarter.getValue());
    }

    /**
     * datetime arithmetic function months-add.
     */
    @ExecFunction(name = "months_add")
    public static Expression monthsAdd(DateV2Literal date, IntegerLiteral month) {
        return date.plusMonths(month.getValue());
    }

    @ExecFunction(name = "months_add")
    public static Expression monthsAdd(DateTimeV2Literal date, IntegerLiteral month) {
        return date.plusMonths(month.getValue());
    }

    @ExecFunction(name = "months_add")
    public static Expression monthsAdd(TimestampTzLiteral date, IntegerLiteral month) {
        return date.plusMonths(month.getValue());
    }

    /**
     * datetime arithmetic function weeks-add.
     */
    @ExecFunction(name = "weeks_add")
    public static Expression weeksAdd(DateV2Literal date, IntegerLiteral weeks) {
        return date.plusWeeks(weeks.getValue());
    }

    @ExecFunction(name = "weeks_add")
    public static Expression weeksAdd(DateTimeV2Literal date, IntegerLiteral weeks) {
        return date.plusWeeks(weeks.getValue());
    }

    @ExecFunction(name = "weeks_add")
    public static Expression weeksAdd(TimestampTzLiteral date, IntegerLiteral weeks) {
        return date.plusWeeks(weeks.getValue());
    }

    /**
     * datetime arithmetic function days-add.
     */
    @ExecFunction(name = "days_add")
    public static Expression daysAdd(DateV2Literal date, IntegerLiteral day) {
        return date.plusDays(day.getValue());
    }

    @ExecFunction(name = "days_add")
    public static Expression daysAdd(DateTimeV2Literal date, IntegerLiteral day) {
        return date.plusDays(day.getValue());
    }

    @ExecFunction(name = "days_add")
    public static Expression daysAdd(TimestampTzLiteral date, IntegerLiteral day) {
        return date.plusDays(day.getValue());
    }

    /**
     * datetime arithmetic function day_second-add.
     */
    @ExecFunction(name = "day_second_add")
    public static Expression daysAdd(DateTimeV2Literal date, VarcharLiteral daySecond) {
        return applyInterval(date, daySecond, TimeUnit.DAY_SECOND, true);
    }

    @ExecFunction(name = "day_second_add")
    public static Expression daysAdd(TimestampTzLiteral date, VarcharLiteral daySecond) {
        return applyInterval(date, daySecond, TimeUnit.DAY_SECOND, true);
    }

    @ExecFunction(name = "day_second_sub")
    public static Expression daysSub(DateTimeV2Literal date, VarcharLiteral daySecond) {
        return applyInterval(date, daySecond, TimeUnit.DAY_SECOND, false);
    }

    @ExecFunction(name = "day_second_sub")
    public static Expression daysSub(TimestampTzLiteral date, VarcharLiteral daySecond) {
        return applyInterval(date, daySecond, TimeUnit.DAY_SECOND, false);
    }

    /**
     * datetime arithmetic function days-sub
     */
    @ExecFunction(name = "days_sub")
    public static Expression daysSub(DateV2Literal date, IntegerLiteral day) {
        return daysAdd(date, new IntegerLiteral(-day.getValue()));
    }

    @ExecFunction(name = "days_sub")
    public static Expression daysSub(DateTimeV2Literal date, IntegerLiteral day) {
        return daysAdd(date, new IntegerLiteral(-day.getValue()));
    }

    @ExecFunction(name = "days_sub")
    public static Expression daysSub(TimestampTzLiteral date, IntegerLiteral day) {
        return daysAdd(date, new IntegerLiteral(-day.getValue()));
    }

    @ExecFunction(name = "day_minute_add")
    public static Expression dayMinuteAdd(DateTimeV2Literal date, VarcharLiteral dayMinute) {
        return applyInterval(date, dayMinute, TimeUnit.DAY_MINUTE, true);
    }

    @ExecFunction(name = "day_minute_add")
    public static Expression dayMinuteAdd(TimestampTzLiteral date, VarcharLiteral dayMinute) {
        return applyInterval(date, dayMinute, TimeUnit.DAY_MINUTE, true);
    }

    @ExecFunction(name = "day_minute_sub")
    public static Expression dayMinuteSub(DateTimeV2Literal date, VarcharLiteral dayMinute) {
        return applyInterval(date, dayMinute, TimeUnit.DAY_MINUTE, false);
    }

    @ExecFunction(name = "day_minute_sub")
    public static Expression dayMinuteSub(TimestampTzLiteral date, VarcharLiteral dayMinute) {
        return applyInterval(date, dayMinute, TimeUnit.DAY_MINUTE, false);
    }

    @ExecFunction(name = "day_microsecond_add")
    public static Expression dayMicrosecondAdd(DateTimeV2Literal date, VarcharLiteral dayMicrosecond) {
        return applyInterval(date, dayMicrosecond, TimeUnit.DAY_MICROSECOND, true);
    }

    @ExecFunction(name = "day_microsecond_add")
    public static Expression dayMicrosecondAdd(TimestampTzLiteral date, VarcharLiteral dayMicrosecond) {
        return applyInterval(date, dayMicrosecond, TimeUnit.DAY_MICROSECOND, true);
    }

    @ExecFunction(name = "day_microsecond_sub")
    public static Expression dayMicrosecondSub(DateTimeV2Literal date, VarcharLiteral dayMicrosecond) {
        return applyInterval(date, dayMicrosecond, TimeUnit.DAY_MICROSECOND, false);
    }

    @ExecFunction(name = "day_microsecond_sub")
    public static Expression dayMicrosecondSub(TimestampTzLiteral date, VarcharLiteral dayMicrosecond) {
        return applyInterval(date, dayMicrosecond, TimeUnit.DAY_MICROSECOND, false);
    }

    @ExecFunction(name = "hour_minute_add")
    public static Expression hourMinuteAdd(DateTimeV2Literal date, VarcharLiteral hourMinute) {
        return applyInterval(date, hourMinute, TimeUnit.HOUR_MINUTE, true);
    }

    @ExecFunction(name = "hour_minute_add")
    public static Expression hourMinuteAdd(TimestampTzLiteral date, VarcharLiteral hourMinute) {
        return applyInterval(date, hourMinute, TimeUnit.HOUR_MINUTE, true);
    }

    @ExecFunction(name = "hour_minute_sub")
    public static Expression hourMinuteSub(DateTimeV2Literal date, VarcharLiteral hourMinute) {
        return applyInterval(date, hourMinute, TimeUnit.HOUR_MINUTE, false);
    }

    @ExecFunction(name = "hour_minute_sub")
    public static Expression hourMinuteSub(TimestampTzLiteral date, VarcharLiteral hourMinute) {
        return applyInterval(date, hourMinute, TimeUnit.HOUR_MINUTE, false);
    }

    @ExecFunction(name = "hour_second_add")
    public static Expression hourSecondAdd(DateTimeV2Literal date, VarcharLiteral hourSecond) {
        return applyInterval(date, hourSecond, TimeUnit.HOUR_SECOND, true);
    }

    @ExecFunction(name = "hour_second_add")
    public static Expression hourSecondAdd(TimestampTzLiteral date, VarcharLiteral hourSecond) {
        return applyInterval(date, hourSecond, TimeUnit.HOUR_SECOND, true);
    }

    @ExecFunction(name = "hour_second_sub")
    public static Expression hourSecondSub(DateTimeV2Literal date, VarcharLiteral hourSecond) {
        return applyInterval(date, hourSecond, TimeUnit.HOUR_SECOND, false);
    }

    @ExecFunction(name = "hour_second_sub")
    public static Expression hourSecondSub(TimestampTzLiteral date, VarcharLiteral hourSecond) {
        return applyInterval(date, hourSecond, TimeUnit.HOUR_SECOND, false);
    }

    @ExecFunction(name = "hour_microsecond_add")
    public static Expression hourMicrosecondAdd(DateTimeV2Literal date, VarcharLiteral hourMicrosecond) {
        return applyInterval(date, hourMicrosecond, TimeUnit.HOUR_MICROSECOND, true);
    }

    @ExecFunction(name = "hour_microsecond_add")
    public static Expression hourMicrosecondAdd(TimestampTzLiteral date, VarcharLiteral hourMicrosecond) {
        return applyInterval(date, hourMicrosecond, TimeUnit.HOUR_MICROSECOND, true);
    }

    @ExecFunction(name = "hour_microsecond_sub")
    public static Expression hourMicrosecondSub(DateTimeV2Literal date, VarcharLiteral hourMicrosecond) {
        return applyInterval(date, hourMicrosecond, TimeUnit.HOUR_MICROSECOND, false);
    }

    @ExecFunction(name = "hour_microsecond_sub")
    public static Expression hourMicrosecondSub(TimestampTzLiteral date, VarcharLiteral hourMicrosecond) {
        return applyInterval(date, hourMicrosecond, TimeUnit.HOUR_MICROSECOND, false);
    }

    @ExecFunction(name = "minute_microsecond_add")
    public static Expression minuteMicrosecondAdd(DateTimeV2Literal date, VarcharLiteral minuteMicrosecond) {
        return applyInterval(date, minuteMicrosecond, TimeUnit.MINUTE_MICROSECOND, true);
    }

    @ExecFunction(name = "minute_microsecond_add")
    public static Expression minuteMicrosecondAdd(TimestampTzLiteral date, VarcharLiteral minuteMicrosecond) {
        return applyInterval(date, minuteMicrosecond, TimeUnit.MINUTE_MICROSECOND, true);
    }

    @ExecFunction(name = "minute_microsecond_sub")
    public static Expression minuteMicrosecondSub(DateTimeV2Literal date, VarcharLiteral minuteMicrosecond) {
        return applyInterval(date, minuteMicrosecond, TimeUnit.MINUTE_MICROSECOND, false);
    }

    @ExecFunction(name = "minute_microsecond_sub")
    public static Expression minuteMicrosecondSub(TimestampTzLiteral date, VarcharLiteral minuteMicrosecond) {
        return applyInterval(date, minuteMicrosecond, TimeUnit.MINUTE_MICROSECOND, false);
    }

    @ExecFunction(name = "year_month_add")
    public static Expression yearMonthAdd(DateTimeV2Literal date, VarcharLiteral yearMonth) {
        return applyInterval(date, yearMonth, TimeUnit.YEAR_MONTH, true);
    }

    @ExecFunction(name = "year_month_add")
    public static Expression yearMonthAdd(TimestampTzLiteral date, VarcharLiteral yearMonth) {
        return applyInterval(date, yearMonth, TimeUnit.YEAR_MONTH, true);
    }

    @ExecFunction(name = "year_month_sub")
    public static Expression yearMonthSub(DateTimeV2Literal date, VarcharLiteral yearMonth) {
        return applyInterval(date, yearMonth, TimeUnit.YEAR_MONTH, false);
    }

    @ExecFunction(name = "year_month_sub")
    public static Expression yearMonthSub(TimestampTzLiteral date, VarcharLiteral yearMonth) {
        return applyInterval(date, yearMonth, TimeUnit.YEAR_MONTH, false);
    }

    /**
     * datetime arithmetic function hours-add.
     */
    @ExecFunction(name = "hours_add")
    public static Expression hoursAdd(DateTimeV2Literal date, IntegerLiteral hour) {
        return date.plusHours(hour.getValue());
    }

    @ExecFunction(name = "hours_add")
    public static Expression hoursAdd(TimestampTzLiteral date, IntegerLiteral hour) {
        return date.plusHours(hour.getValue());
    }

    /**
     * datetime arithmetic function minutes-add.
     */
    @ExecFunction(name = "minutes_add")
    public static Expression minutesAdd(DateTimeV2Literal date, BigIntLiteral minute) {
        return date.plusMinutes(minute.getValue());
    }

    @ExecFunction(name = "minutes_add")
    public static Expression minutesAdd(TimestampTzLiteral date, BigIntLiteral minute) {
        return date.plusMinutes(minute.getValue());
    }

    /**
     * datetime arithmetic function seconds-add.
     */
    @ExecFunction(name = "seconds_add")
    public static Expression secondsAdd(DateTimeV2Literal date, BigIntLiteral second) {
        return date.plusSeconds(second.getValue());
    }

    @ExecFunction(name = "seconds_add")
    public static Expression secondsAdd(TimestampTzLiteral date, BigIntLiteral second) {
        return date.plusSeconds(second.getValue());
    }

    /**
     * datetime arithmetic function microseconds-add.
     */
    @ExecFunction(name = "microseconds_add")
    public static Expression microSecondsAdd(DateTimeV2Literal date, BigIntLiteral microSecond) {
        return date.plusMicroSeconds(microSecond.getValue());
    }

    @ExecFunction(name = "microseconds_add")
    public static Expression microSecondsAdd(TimestampTzLiteral date, BigIntLiteral microSecond) {
        return date.plusMicroSeconds(microSecond.getValue());
    }

    /**
     * datetime arithmetic function years-sub.
     */
    @ExecFunction(name = "years_sub")
    public static Expression yearsSub(DateV2Literal date, IntegerLiteral year) {
        return yearsAdd(date, new IntegerLiteral(-year.getValue()));
    }

    @ExecFunction(name = "years_sub")
    public static Expression yearsSub(DateTimeV2Literal date, IntegerLiteral year) {
        return yearsAdd(date, new IntegerLiteral(-year.getValue()));
    }

    @ExecFunction(name = "years_sub")
    public static Expression yearsSub(TimestampTzLiteral date, IntegerLiteral year) {
        return yearsAdd(date, new IntegerLiteral(-year.getValue()));
    }

    /**
     * datetime arithmetic function quarters-sub.
     */
    @ExecFunction(name = "quarters_sub")
    public static Expression quartersSub(DateV2Literal date, IntegerLiteral quarter) {
        return quartersAdd(date, new IntegerLiteral(-quarter.getValue()));
    }

    @ExecFunction(name = "quarters_sub")
    public static Expression quartersSub(DateTimeV2Literal date, IntegerLiteral quarter) {
        return quartersAdd(date, new IntegerLiteral(-quarter.getValue()));
    }

    @ExecFunction(name = "quarters_sub")
    public static Expression quartersSub(TimestampTzLiteral date, IntegerLiteral quarter) {
        return quartersAdd(date, new IntegerLiteral(-quarter.getValue()));
    }

    /**
     * datetime arithmetic function months-sub
     */
    @ExecFunction(name = "months_sub")
    public static Expression monthsSub(DateV2Literal date, IntegerLiteral month) {
        return monthsAdd(date, new IntegerLiteral(-month.getValue()));
    }

    @ExecFunction(name = "months_sub")
    public static Expression monthsSub(DateTimeV2Literal date, IntegerLiteral month) {
        return monthsAdd(date, new IntegerLiteral(-month.getValue()));
    }

    @ExecFunction(name = "months_sub")
    public static Expression monthsSub(TimestampTzLiteral date, IntegerLiteral month) {
        return monthsAdd(date, new IntegerLiteral(-month.getValue()));
    }

    /**
     * datetime arithmetic function weeks-sub.
     */
    @ExecFunction(name = "weeks_sub")
    public static Expression weeksSub(DateV2Literal date, IntegerLiteral weeks) {
        return date.plusWeeks(-weeks.getValue());
    }

    @ExecFunction(name = "weeks_sub")
    public static Expression weeksSub(DateTimeV2Literal date, IntegerLiteral weeks) {
        return date.plusWeeks(-weeks.getValue());
    }

    @ExecFunction(name = "weeks_sub")
    public static Expression weeksSub(TimestampTzLiteral date, IntegerLiteral weeks) {
        return date.plusWeeks(-weeks.getValue());
    }

    /**
     * datetime arithmetic function hours-sub
     */
    @ExecFunction(name = "hours_sub")
    public static Expression hoursSub(DateTimeV2Literal date, IntegerLiteral hour) {
        return hoursAdd(date, new IntegerLiteral(-hour.getValue()));
    }

    @ExecFunction(name = "hours_sub")
    public static Expression hoursSub(TimestampTzLiteral date, IntegerLiteral hour) {
        return hoursAdd(date, new IntegerLiteral(-hour.getValue()));
    }

    /**
     * datetime arithmetic function minutes-sub
     */
    @ExecFunction(name = "minutes_sub")
    public static Expression minutesSub(DateTimeV2Literal date, BigIntLiteral minute) {
        return minutesAdd(date, new BigIntLiteral(-minute.getValue()));
    }

    @ExecFunction(name = "minutes_sub")
    public static Expression minutesSub(TimestampTzLiteral date, BigIntLiteral minute) {
        return minutesAdd(date, new BigIntLiteral(-minute.getValue()));
    }

    /**
     * datetime arithmetic function seconds-sub
     */
    @ExecFunction(name = "seconds_sub")
    public static Expression secondsSub(DateTimeV2Literal date, BigIntLiteral second) {
        return secondsAdd(date, new BigIntLiteral(-second.getValue()));
    }

    @ExecFunction(name = "seconds_sub")
    public static Expression secondsSub(TimestampTzLiteral date, BigIntLiteral second) {
        return secondsAdd(date, new BigIntLiteral(-second.getValue()));
    }

    /**
     * datetime arithmetic function microseconds_sub.
     */
    @ExecFunction(name = "microseconds_sub")
    public static Expression microSecondsSub(DateTimeV2Literal date, BigIntLiteral microSecond) {
        return date.plusMicroSeconds(-microSecond.getValue());
    }

    @ExecFunction(name = "microseconds_sub")
    public static Expression microSecondsSub(TimestampTzLiteral date, BigIntLiteral microSecond) {
        return date.plusMicroSeconds(-microSecond.getValue());
    }

    /**
     * datetime arithmetic function milliseconds_add.
     */
    @ExecFunction(name = "milliseconds_add")
    public static Expression milliSecondsAdd(DateTimeV2Literal date, BigIntLiteral milliSecond) {
        return date.plusMilliSeconds(milliSecond.getValue());
    }

    @ExecFunction(name = "milliseconds_add")
    public static Expression milliSecondsAdd(TimestampTzLiteral date, BigIntLiteral milliSecond) {
        return date.plusMilliSeconds(milliSecond.getValue());
    }

    /**
     * datetime arithmetic function milliseconds_sub.
     */
    @ExecFunction(name = "milliseconds_sub")
    public static Expression milliSecondsSub(DateTimeV2Literal date, BigIntLiteral milliSecond) {
        return date.plusMilliSeconds(-milliSecond.getValue());
    }

    @ExecFunction(name = "milliseconds_sub")
    public static Expression milliSecondsSub(TimestampTzLiteral date, BigIntLiteral milliSecond) {
        return date.plusMilliSeconds(-milliSecond.getValue());
    }

    /**
     * datetime arithmetic function datediff
     */
    @ExecFunction(name = "datediff")
    public static Expression dateDiff(DateV2Literal date1, DateV2Literal date2) {
        return new IntegerLiteral(dateDiff(date1.toJavaDateType(), date2.toJavaDateType()));
    }

    @ExecFunction(name = "datediff")
    public static Expression dateDiff(DateV2Literal date1, DateTimeV2Literal date2) {
        return new IntegerLiteral(dateDiff(date1.toJavaDateType(), date2.toJavaDateType()));
    }

    @ExecFunction(name = "datediff")
    public static Expression dateDiff(DateTimeV2Literal date1, DateV2Literal date2) {
        return new IntegerLiteral(dateDiff(date1.toJavaDateType(), date2.toJavaDateType()));
    }

    @ExecFunction(name = "datediff")
    public static Expression dateDiff(DateTimeV2Literal date1, DateTimeV2Literal date2) {
        return new IntegerLiteral(dateDiff(date1.toJavaDateType(), date2.toJavaDateType()));
    }

    private static int dateDiff(LocalDateTime date1, LocalDateTime date2) {
        return ((int) ChronoUnit.DAYS.between(date2.toLocalDate(), date1.toLocalDate()));
    }

    @ExecFunction(name = "to_days")
    public static Expression toDays(DateV2Literal date) {
        return new IntegerLiteral((int) date.getDay());
    }

    @ExecFunction(name = "to_days")
    public static Expression toDays(DateTimeV2Literal date) {
        return new IntegerLiteral(((int) date.getDay()));
    }

    /**
     * datetime arithmetic function time.
     */
    @ExecFunction(name = "time")
    public static Expression time(DateTimeV2Literal date) {
        return new TimeV2Literal((int) date.getHour(), (int) date.getMinute(), (int) date.getSecond(),
                (int) date.getMicroSecond(), (int) date.getScale(), false);
    }

    /**
     * datetime arithmetic function add_time.
     */
    @ExecFunction(name = "add_time")
    public static Expression addTime(DateTimeV2Literal date, TimeV2Literal time) {
        return date.plusMicroSeconds((long) time.getValue());
    }

    @ExecFunction(name = "add_time")
    public static Expression addTime(TimeV2Literal t1, TimeV2Literal t2) {
        return t1.plusMicroSeconds((long) t2.getValue());
    }

    @ExecFunction(name = "add_time")
    public static Expression addTime(TimestampTzLiteral date, TimeV2Literal time) {
        return date.plusMicroSeconds((long) time.getValue());
    }

    /**
     * datetime arithmetic function sub_time.
     */
    @ExecFunction(name = "sub_time")
    public static Expression subTime(DateTimeV2Literal date, TimeV2Literal time) {
        return date.plusMicroSeconds(-(long) time.getValue());
    }

    @ExecFunction(name = "sub_time")
    public static Expression subTime(TimeV2Literal t1, TimeV2Literal t2) {
        return t1.plusMicroSeconds(-(long) t2.getValue());
    }

    @ExecFunction(name = "sub_time")
    public static Expression subTime(TimestampTzLiteral date, TimeV2Literal time) {
        return date.plusMicroSeconds(-(long) time.getValue());
    }
}