ConvertTz.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.common.DdlException;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
import org.apache.doris.nereids.trees.expressions.functions.Monotonic;
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.literal.DateLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
import org.apache.doris.nereids.trees.expressions.shape.TernaryExpression;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
import org.apache.doris.nereids.types.DateTimeV2Type;
import org.apache.doris.nereids.types.VarcharType;
import org.apache.doris.nereids.util.DateUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
/**
* ScalarFunction 'convert_tz'.
*/
public class ConvertTz extends ScalarFunction
implements TernaryExpression, ExplicitlyCastableSignature, PropagateNullLiteral, PropagateNullable, Monotonic {
public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
FunctionSignature.ret(DateTimeV2Type.WILDCARD)
.args(DateTimeV2Type.WILDCARD, VarcharType.SYSTEM_DEFAULT, VarcharType.SYSTEM_DEFAULT)
);
/**
* constructor with 3 arguments.
*/
public ConvertTz(Expression arg0, Expression arg1, Expression arg2) {
super("convert_tz", castDateTime(arg0), arg1, arg2);
}
/** constructor for withChildren and reuse signature */
private ConvertTz(ScalarFunctionParams functionParams) {
super(functionParams);
}
private static Expression castDateTime(Expression arg0) {
try {
return arg0 instanceof StringLikeLiteral ? new Cast(arg0, DateTimeV2Type.forTypeFromString(
((StringLikeLiteral) arg0).getStringValue()), true) : arg0;
} catch (Exception e) {
return new NullLiteral(DateTimeV2Type.SYSTEM_DEFAULT);
}
}
/**
* withChildren.
*/
@Override
public ConvertTz withChildren(List<Expression> children) {
Preconditions.checkArgument(children.size() == 3);
return new ConvertTz(getFunctionParams(children));
}
@Override
public List<FunctionSignature> getSignatures() {
return SIGNATURES;
}
@Override
public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
return visitor.visitConvertTz(this, context);
}
@Override
public boolean isMonotonic(Literal lower, Literal upper) {
if (!(child(1) instanceof StringLikeLiteral) || !(child(2) instanceof StringLikeLiteral)) {
return false;
}
ZoneId fromZone = parseZoneId((StringLikeLiteral) child(1));
ZoneId toZone = parseZoneId((StringLikeLiteral) child(2));
if (fromZone == null || toZone == null) {
return false;
}
if (toZone.getRules().isFixedOffset()) {
return true;
}
if (lower == null || upper == null) {
return false;
}
LocalDateTime lowerDateTime = toLocalDateTime(lower);
LocalDateTime upperDateTime = toLocalDateTime(upper);
if (lowerDateTime == null || upperDateTime == null) {
return false;
}
if (lowerDateTime.equals(upperDateTime)) {
return true;
}
if (upperDateTime.isBefore(lowerDateTime)) {
return false;
}
/*
* convert_tz can be treated as a composition of two mappings:
*
* source local time x -> instant by from_tz -> target local time by to_tz.
*
* After PR #64029, the first mapping is monotonic non-decreasing. A spring gap in from_tz
* flattens skipped local times to the transition instant, and a fall-back overlap uses the
* pre-transition offset before jumping forward at the overlap end. Neither case makes the
* instant move backward as x increases.
*
* The second mapping, instant -> to_tz local time, is also monotonic non-decreasing except
* at a to_tz fall-back transition, where the displayed local time jumps backward. Therefore
* convert_tz is non-monotonic for a partition only when the instant interval obtained from
* interpreting the partition bounds in from_tz crosses a to_tz fall-back transition instant.
*
* Partition pruning folds both endpoints before deriving the function range, so a fall-back
* instant inside (fromInstant(lower), fromInstant(upper)] disables the monotonic shortcut.
*/
Instant lowerInstant = DateTimeLiteral.convertLocalToInstant(lowerDateTime, fromZone);
Instant upperInstant = DateTimeLiteral.convertLocalToInstant(upperDateTime, fromZone);
if (upperInstant.isBefore(lowerInstant)) {
return false;
}
return !DateUtils.hasFallbackTransitionInInstantRange(toZone, lowerInstant, upperInstant);
}
@Override
public boolean isPositive() {
return true;
}
@Override
public int getMonotonicFunctionChildIndex() {
return 0;
}
@Override
public Expression withConstantArgs(Expression literal) {
return new ConvertTz(literal, child(1), child(2));
}
private ZoneId parseZoneId(StringLikeLiteral timeZone) {
try {
String standardizedTimeZone = TimeUtils.checkTimeZoneValidAndStandardize(timeZone.getStringValue());
return ZoneId.of(standardizedTimeZone, TimeUtils.timeZoneAliasMap);
} catch (DdlException | DateTimeException e) {
return null;
}
}
private LocalDateTime toLocalDateTime(Literal literal) {
return literal instanceof DateLiteral ? ((DateLiteral) literal).toJavaDateType() : null;
}
}