TimeStampNsLiteral.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.analysis;

import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.TableIf.TableType;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.foundation.format.FormatOptions;
import org.apache.doris.thrift.TDateLiteral;
import org.apache.doris.thrift.TExprNode;
import org.apache.doris.thrift.TExprNodeType;

import com.google.common.base.Preconditions;
import com.google.gson.annotations.SerializedName;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.ZoneOffset;

/**
 * Legacy literal for the fixed nanosecond-precision TIMESTAMP_NS type.
 *
 * <p>TIMESTAMP_NS owns its value and range logic instead of inheriting the unrelated calendar
 * encodings and microsecond rules in {@link DateLiteral}.</p>
 */
public final class TimeStampNsLiteral extends LiteralExpr {
    private static final long NANOSECONDS_PER_SECOND = 1_000_000_000L;
    private static final long MAX_NANOSECOND = NANOSECONDS_PER_SECOND - 1;
    private static final int MIN_YEAR = 1677;
    private static final int MAX_YEAR = 2262;
    private static final LocalDateTime MIN_VALUE
            = LocalDateTime.of(MIN_YEAR, 9, 21, 0, 12, 43, 145224192);
    private static final LocalDateTime MAX_VALUE
            = LocalDateTime.of(MAX_YEAR, 4, 11, 23, 47, 16, 854775807);

    @SerializedName("y")
    private long year;
    @SerializedName("m")
    private long month;
    @SerializedName("d")
    private long day;
    @SerializedName("h")
    private long hour;
    @SerializedName("M")
    private long minute;
    @SerializedName("s")
    private long second;
    @SerializedName("ns")
    private long nanosecond;
    @SerializedName("inf")
    private boolean isMinInfinity;

    public TimeStampNsLiteral() {
        type = Type.TIMESTAMP_NS;
    }

    @Override
    public boolean isNullable() {
        return false;
    }

    public TimeStampNsLiteral(boolean isMax) {
        this(isMax ? MAX_VALUE : MIN_VALUE);
        isMinInfinity = !isMax;
    }

    public TimeStampNsLiteral(long year, long month, long day, long hour, long minute, long second,
            long nanosecond) {
        this();
        this.year = year;
        this.month = month;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
        this.second = second;
        this.nanosecond = nanosecond;
        this.isMinInfinity = false;
    }

    public TimeStampNsLiteral(LocalDateTime value) {
        this(value.getYear(), value.getMonthValue(), value.getDayOfMonth(), value.getHour(),
                value.getMinute(), value.getSecond(), value.getNano());
    }

    private TimeStampNsLiteral(TimeStampNsLiteral other) {
        super(other);
        year = other.year;
        month = other.month;
        day = other.day;
        hour = other.hour;
        minute = other.minute;
        second = other.second;
        nanosecond = other.nanosecond;
        isMinInfinity = other.isMinInfinity;
        type = Type.TIMESTAMP_NS;
    }

    public static TimeStampNsLiteral createMinValue() {
        return new TimeStampNsLiteral(false);
    }

    @Override
    public Expr clone() {
        return new TimeStampNsLiteral(this);
    }

    @Override
    public boolean isMinValue() {
        return isMinInfinity;
    }

    @Override
    public Object getRealValue() {
        LocalDateTime value = toLocalDateTime();
        return BigInteger.valueOf(value.toEpochSecond(ZoneOffset.UTC))
                .multiply(BigInteger.valueOf(NANOSECONDS_PER_SECOND))
                .add(BigInteger.valueOf(nanosecond))
                .longValueExact();
    }

    @Override
    public ByteBuffer getHashValue(PrimitiveType primitiveType) {
        Preconditions.checkArgument(primitiveType == PrimitiveType.TIMESTAMP_NS,
                "Expected TIMESTAMP_NS hash type, but got %s", primitiveType);
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES).order(ByteOrder.LITTLE_ENDIAN);
        buffer.putLong((long) getRealValue());
        return buffer;
    }

    @Override
    public int compareLiteral(LiteralExpr expr) {
        if (expr instanceof PlaceHolderExpr) {
            return compareLiteral(((PlaceHolderExpr) expr).getLiteral());
        }
        if (expr instanceof NullLiteral) {
            return 1;
        }
        if (expr == MaxLiteral.MAX_VALUE) {
            return -1;
        }
        if (expr instanceof TimeStampNsLiteral) {
            return compareDateTime((TimeStampNsLiteral) expr);
        }
        if (expr instanceof DateLiteral) {
            if (isMinValue()) {
                return -1;
            }
            DateLiteral other = (DateLiteral) expr;
            int result = compareDateTime(other.getYear(), other.getMonth(), other.getDay(),
                    other.getHour(), other.getMinute(), other.getSecond(), other.getMicrosecond() * 1000);
            if (result == 0 && other.isDateType()) {
                // DateLiteral orders DATE/DATEV2 before datetime-like literals on the same day.
                // Preserve that ordering and comparison symmetry across the two literal classes.
                return 1;
            }
            return result;
        }
        return Integer.signum(getStringValue().compareTo(expr.getStringValue()));
    }

    private int compareDateTime(TimeStampNsLiteral other) {
        if (isMinValue() != other.isMinValue()) {
            return isMinValue() ? -1 : 1;
        }
        return compareDateTime(other.year, other.month, other.day, other.hour, other.minute,
                other.second, other.nanosecond);
    }

    private int compareDateTime(long otherYear, long otherMonth, long otherDay, long otherHour,
            long otherMinute, long otherSecond, long otherNanosecond) {
        int result = Long.compare(year, otherYear);
        if (result == 0) {
            result = Long.compare(month, otherMonth);
        }
        if (result == 0) {
            result = Long.compare(day, otherDay);
        }
        if (result == 0) {
            result = Long.compare(hour, otherHour);
        }
        if (result == 0) {
            result = Long.compare(minute, otherMinute);
        }
        if (result == 0) {
            result = Long.compare(second, otherSecond);
        }
        if (result == 0) {
            result = Long.compare(nanosecond, otherNanosecond);
        }
        return result;
    }

    @Override
    public String getStringValue() {
        return String.format("%04d-%02d-%02d %02d:%02d:%02d.%09d",
                year, month, day, hour, minute, second, nanosecond);
    }

    public void roundFloor(int newScale) {
        Preconditions.checkArgument(newScale == ScalarType.TIMESTAMP_NS_SCALE,
                "TIMESTAMP_NS has fixed scale %s, but got %s",
                ScalarType.TIMESTAMP_NS_SCALE, newScale);
    }

    @Override
    public void checkValueValid() throws AnalysisException {
        if (year < MIN_YEAR || year > MAX_YEAR) {
            throw new AnalysisException("TimeStampNsLiteral has invalid year value: " + year);
        }
        if (month < 1 || month > 12) {
            throw new AnalysisException("TimeStampNsLiteral has invalid month value: " + month);
        }
        if (day < 1 || day > daysInMonth(year, month)) {
            throw new AnalysisException("TimeStampNsLiteral has invalid day value: " + day);
        }
        if (hour < 0 || hour > 23) {
            throw new AnalysisException("TimeStampNsLiteral has invalid hour value: " + hour);
        }
        if (minute < 0 || minute > 59) {
            throw new AnalysisException("TimeStampNsLiteral has invalid minute value: " + minute);
        }
        if (second < 0 || second > 59) {
            throw new AnalysisException("TimeStampNsLiteral has invalid second value: " + second);
        }
        if (nanosecond < 0 || nanosecond > MAX_NANOSECOND) {
            throw new AnalysisException("TimeStampNsLiteral has invalid nanosecond value: " + nanosecond);
        }
        if (checkRange()) {
            throw new AnalysisException("TimeStampNsLiteral is outside Int64 epoch nanosecond range: "
                    + getStringValue());
        }
    }

    boolean checkRange() {
        if (year < MIN_YEAR || year > MAX_YEAR || month < 1 || month > 12
                || hour < 0 || hour > 23 || minute < 0 || minute > 59
                || second < 0 || second > 59) {
            return true;
        }
        if (day < 1 || day > daysInMonth(year, month)) {
            return true;
        }
        if (nanosecond < 0 || nanosecond > MAX_NANOSECOND) {
            return true;
        }
        return compareBoundary(MIN_VALUE) < 0 || compareBoundary(MAX_VALUE) > 0;
    }

    private int compareBoundary(LocalDateTime boundary) {
        return compareDateTime(boundary.getYear(), boundary.getMonthValue(), boundary.getDayOfMonth(),
                boundary.getHour(), boundary.getMinute(), boundary.getSecond(), boundary.getNano());
    }

    private static int daysInMonth(long year, long month) {
        switch ((int) month) {
            case 2:
                return Year.isLeap(year) ? 29 : 28;
            case 4:
            case 6:
            case 9:
            case 11:
                return 30;
            default:
                return 31;
        }
    }

    public LocalDateTime toLocalDateTime() {
        try {
            return LocalDateTime.of((int) year, (int) month, (int) day, (int) hour,
                    (int) minute, (int) second, (int) nanosecond);
        } catch (DateTimeException e) {
            throw new IllegalStateException("Invalid TIMESTAMP_NS literal: " + getStringValue(), e);
        }
    }

    public TimeStampNsLiteral plusYears(long years) {
        return new TimeStampNsLiteral(toLocalDateTime().plusYears(years));
    }

    public TimeStampNsLiteral plusMonths(long months) {
        return new TimeStampNsLiteral(toLocalDateTime().plusMonths(months));
    }

    public TimeStampNsLiteral plusDays(long days) {
        return new TimeStampNsLiteral(toLocalDateTime().plusDays(days));
    }

    public TimeStampNsLiteral plusHours(long hours) {
        return new TimeStampNsLiteral(toLocalDateTime().plusHours(hours));
    }

    public TimeStampNsLiteral plusMinutes(long minutes) {
        return new TimeStampNsLiteral(toLocalDateTime().plusMinutes(minutes));
    }

    public TimeStampNsLiteral plusSeconds(long seconds) {
        return new TimeStampNsLiteral(toLocalDateTime().plusSeconds(seconds));
    }

    public long getYear() {
        return year;
    }

    public long getMonth() {
        return month;
    }

    public long getDay() {
        return day;
    }

    public long getHour() {
        return hour;
    }

    public long getMinute() {
        return minute;
    }

    public long getSecond() {
        return second;
    }

    public long getMicrosecond() {
        return nanosecond / 1000;
    }

    public long getNanosecond() {
        return nanosecond;
    }

    @Override
    public long getLongValue() {
        return (year * 10000 + month * 100 + day) * 1000000L + hour * 10000 + minute * 100 + second;
    }

    @Override
    public double getDoubleValue() {
        return getLongValue();
    }

    public double getDoubleValueAsDateTime() {
        return getDoubleValue();
    }

    @Override
    public int hashCode() {
        int legacyHash = Long.hashCode(getLongValue());
        if (nanosecond % 1000 == 0) {
            return isMinValue() ? 31 * legacyHash + 1 : legacyHash;
        }
        int hash = 31 * legacyHash + Long.hashCode(nanosecond);
        return isMinValue() ? 31 * hash + 1 : hash;
    }

    @Override
    protected String toSqlImpl() {
        return "'" + getStringValue() + "'";
    }

    @Override
    protected String toSqlImpl(boolean disableTableName, boolean needExternalSql,
            TableType tableType, TableIf table) {
        return toSqlImpl();
    }

    @Override
    protected void toThrift(TExprNode msg) {
        msg.node_type = TExprNodeType.DATE_LITERAL;
        msg.date_literal = new TDateLiteral(getStringValue());
    }

    @Override
    protected String getStringValueInComplexTypeForQuery(FormatOptions options) {
        return options.getNestedStringWrapper() + getStringValue() + options.getNestedStringWrapper();
    }
}