diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java index dd39d06dabe0..3afa185153ef 100644 --- a/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java @@ -23,6 +23,8 @@ import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Streams; +import com.google.common.math.IntMath; +import io.airlift.slice.Slice; import io.trino.Session; import io.trino.SystemSessionProperties; import io.trino.connector.CatalogName; @@ -85,6 +87,7 @@ import io.trino.spi.type.ArrayType; import io.trino.spi.type.CharType; import io.trino.spi.type.DateType; +import io.trino.spi.type.LongTimestampWithTimeZone; import io.trino.spi.type.MapType; import io.trino.spi.type.RowType; import io.trino.spi.type.TimestampType; @@ -229,6 +232,9 @@ import io.trino.transaction.TransactionManager; import io.trino.type.TypeCoercion; +import java.math.RoundingMode; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -308,7 +314,10 @@ import static io.trino.spi.type.BigintType.BIGINT; import static io.trino.spi.type.BooleanType.BOOLEAN; import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType; +import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND; import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; import static io.trino.sql.NodeUtils.getSortItemsFromOrderBy; import static io.trino.sql.ParsingUtil.createParsingOptions; import static io.trino.sql.analyzer.AggregationAnalyzer.verifyOrderByAggregations; @@ -2367,15 +2376,7 @@ protected Scope visitSampledRelation(SampledRelation relation, Optional s throw semanticException(INVALID_ARGUMENTS, samplePercentage, "Sample percentage cannot be NULL"); } - if (samplePercentageType != DOUBLE) { - ResolvedFunction coercion = metadata.getCoercion(session, samplePercentageType, DOUBLE); - InterpretedFunctionInvoker functionInvoker = new InterpretedFunctionInvoker(plannerContext.getFunctionManager()); - samplePercentageObject = functionInvoker.invoke(coercion, session.toConnectorSession(), samplePercentageObject); - verify(samplePercentageObject != null, "Coercion from %s to %s returned null", samplePercentageType, DOUBLE); - } - - double samplePercentageValue = (double) samplePercentageObject; - + double samplePercentageValue = (double) coerce(samplePercentageType, samplePercentageObject, DOUBLE); if (samplePercentageValue < 0.0) { throw semanticException(NUMERIC_VALUE_OUT_OF_RANGE, samplePercentage, "Sample percentage must be greater than or equal to 0"); } @@ -4586,6 +4587,14 @@ private void validateVersionPointer(QualifiedObjectName tableName, QueryPeriod q if (pointer == null) { throw semanticException(INVALID_ARGUMENTS, queryPeriod, "Pointer value cannot be NULL"); } + Instant pointerInstant = getInstantWithRoundUp((LongTimestampWithTimeZone) coerce(type, pointer, createTimestampWithTimeZoneType(TimestampWithTimeZoneType.MAX_PRECISION))); + if (!pointerInstant.isBefore(session.getStart())) { + String varchar = ((Slice) coerce(type, pointer, createUnboundedVarcharType())).toStringUtf8(); + throw semanticException( + INVALID_ARGUMENTS, + queryPeriod, + format("Pointer value '%s' is not in the past", varchar)); + } } else { if (pointer == null) { @@ -4598,6 +4607,12 @@ private void validateVersionPointer(QualifiedObjectName tableName, QueryPeriod q } } + private Instant getInstantWithRoundUp(LongTimestampWithTimeZone value) + { + return Instant.ofEpochMilli(value.getEpochMillis()) + .plus(IntMath.divide(value.getPicosOfMilli(), PICOSECONDS_PER_NANOSECOND, RoundingMode.CEILING), ChronoUnit.NANOS); + } + private PointerType toPointerType(QueryPeriod.RangeType type) { switch (type) { @@ -4608,6 +4623,16 @@ private PointerType toPointerType(QueryPeriod.RangeType type) } throw new UnsupportedOperationException("Unsupported range type: " + type); } + + private Object coerce(Type sourceType, Object value, Type targetType) + { + if (sourceType.equals(targetType)) { + return value; + } + ResolvedFunction coercion = metadata.getCoercion(session, sourceType, targetType); + InterpretedFunctionInvoker functionInvoker = new InterpretedFunctionInvoker(plannerContext.getFunctionManager()); + return functionInvoker.invoke(coercion, session.toConnectorSession(), value); + } } private Session createViewSession(Optional catalog, Optional schema, Identity identity, SqlPath path) diff --git a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java index 048c44774f72..2035acadc881 100644 --- a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java +++ b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java @@ -386,6 +386,13 @@ public void testTemporalTableVersion() .hasErrorCode(NOT_SUPPORTED) .hasMessage("This connector does not support versioned tables"); + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF CURRENT_TIMESTAMP(12) - INTERVAL '0.001' SECOND") + .hasErrorCode(NOT_SUPPORTED) + .hasMessage("This connector does not support versioned tables"); + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF LOCALTIMESTAMP(12) - INTERVAL '0.001' SECOND") + .hasErrorCode(NOT_SUPPORTED) + .hasMessage("This connector does not support versioned tables"); + // wrong type assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF '2022-01-01'") .hasErrorCode(TYPE_MISMATCH) @@ -418,6 +425,31 @@ public void testTemporalTableVersion() assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF CAST(NULL AS bigint)") .hasErrorCode(TYPE_MISMATCH) .hasMessage("line 1:18: Type bigint invalid. Temporal pointers must be of type Timestamp, Timestamp with Time Zone, or Date."); + + // temporal version pointer in the future -- invalid, because future state can change + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF DATE '2999-01-01'") + .hasErrorCode(INVALID_ARGUMENTS) + .hasMessage("line 1:18: Pointer value '2999-01-01' is not in the past"); + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF TIMESTAMP '2999-01-01'") + .hasErrorCode(INVALID_ARGUMENTS) + .hasMessage("line 1:18: Pointer value '2999-01-01 00:00:00' is not in the past"); + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF TIMESTAMP '2999-01-01 01:02:03.123456789012'") + .hasErrorCode(INVALID_ARGUMENTS) + .hasMessage("line 1:18: Pointer value '2999-01-01 01:02:03.123456789012' is not in the past"); + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF TIMESTAMP '2999-01-01 UTC'") + .hasErrorCode(INVALID_ARGUMENTS) + .hasMessage("line 1:18: Pointer value '2999-01-01 00:00:00 UTC' is not in the past"); + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF TIMESTAMP '2999-01-01 01:02:03.123456789012 Asia/Kathmandu'") + .hasErrorCode(INVALID_ARGUMENTS) + .hasMessage("line 1:18: Pointer value '2999-01-01 01:02:03.123456789012 Asia/Kathmandu' is not in the past"); + + // temporal version pointer at "current moment" -- invalid, because due to time granularity, the current time's state may still change + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF CURRENT_TIMESTAMP(12)") + .hasErrorCode(INVALID_ARGUMENTS) + .hasMessageMatching("line 1:18: Pointer value '.*' is not in the past"); + assertFails("SELECT * FROM t1 FOR TIMESTAMP AS OF LOCALTIMESTAMP(12)") + .hasErrorCode(INVALID_ARGUMENTS) + .hasMessageMatching("line 1:18: Pointer value '.*' is not in the past"); } @Test