diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/DomainTranslator.java b/core/trino-main/src/main/java/io/trino/sql/planner/DomainTranslator.java index dc764fbec4f8..7ed160123414 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/DomainTranslator.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/DomainTranslator.java @@ -73,6 +73,7 @@ import javax.annotation.Nullable; import java.lang.invoke.MethodHandle; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -81,6 +82,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Iterators.peekingIterator; @@ -88,12 +90,14 @@ import static io.airlift.slice.SliceUtf8.getCodePointAt; import static io.airlift.slice.SliceUtf8.lengthOfCodePoint; import static io.airlift.slice.SliceUtf8.setCodePointAt; +import static io.airlift.slice.Slices.utf8Slice; import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.NEVER_NULL; import static io.trino.spi.function.InvocationConvention.InvocationReturnConvention.FAIL_ON_NULL; import static io.trino.spi.function.InvocationConvention.simpleConvention; import static io.trino.spi.function.OperatorType.SATURATED_FLOOR_CAST; import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.DateType.DATE; import static io.trino.spi.type.TypeUtils.isFloatingPointNaN; import static io.trino.sql.ExpressionUtils.and; import static io.trino.sql.ExpressionUtils.combineConjuncts; @@ -104,6 +108,7 @@ import static io.trino.sql.tree.ComparisonExpression.Operator.EQUAL; import static io.trino.sql.tree.ComparisonExpression.Operator.GREATER_THAN; import static io.trino.sql.tree.ComparisonExpression.Operator.GREATER_THAN_OR_EQUAL; +import static io.trino.sql.tree.ComparisonExpression.Operator.IS_DISTINCT_FROM; import static io.trino.sql.tree.ComparisonExpression.Operator.LESS_THAN; import static io.trino.sql.tree.ComparisonExpression.Operator.LESS_THAN_OR_EQUAL; import static io.trino.sql.tree.ComparisonExpression.Operator.NOT_EQUAL; @@ -502,6 +507,19 @@ protected ExtractionResult visitComparisonExpression(ComparisonExpression node, } if (symbolExpression instanceof Cast) { Cast castExpression = (Cast) symbolExpression; + // type of expression which is then cast to type of value + Type castSourceType = requireNonNull(expressionTypes.get(NodeRef.of(castExpression.getExpression())), "No type for Cast source expression"); + Type castTargetType = requireNonNull(expressionTypes.get(NodeRef.of(castExpression)), "No type for Cast target expression"); + if (castSourceType instanceof VarcharType && castTargetType == DATE && !castExpression.isSafe()) { + Optional result = createVarcharCastToDateComparisonExtractionResult( + node, + (VarcharType) castSourceType, + normalized.getValue(), + complement); + if (result.isPresent()) { + return result.get(); + } + } if (!isImplicitCoercion(expressionTypes, castExpression)) { // // we cannot use non-coercion cast to literal_type on symbol side to build tuple domain @@ -524,9 +542,6 @@ protected ExtractionResult visitComparisonExpression(ComparisonExpression node, return super.visitComparisonExpression(node, complement); } - // type of expression which is then cast to type of value - Type castSourceType = requireNonNull(expressionTypes.get(NodeRef.of(castExpression.getExpression())), "No type for Cast source expression"); - // we use saturated floor cast value -> castSourceType to rewrite original expression to new one with one cast peeled off the symbol side Optional coercedExpression = coerceComparisonWithRounding( castSourceType, castExpression.getExpression(), normalized.getValue(), normalized.getComparisonOperator()); @@ -588,6 +603,115 @@ private Map, Type> analyzeExpression(Expression expression) return typeAnalyzer.getTypes(session, types, expression); } + private Optional createVarcharCastToDateComparisonExtractionResult( + ComparisonExpression node, + VarcharType sourceType, + NullableValue value, + boolean complement) + { + Cast castExpression = (Cast) node.getLeft(); + Expression sourceExpression = castExpression.getExpression(); + ComparisonExpression.Operator comparisonOperator = node.getOperator(); + requireNonNull(value, "value is null"); + + if (complement || value.isNull()) { + return Optional.empty(); + } + if (!(sourceExpression instanceof SymbolReference)) { + // Calculation is not useful + return Optional.empty(); + } + Symbol sourceSymbol = Symbol.from(sourceExpression); + + if (!sourceType.isUnbounded() && sourceType.getBoundedLength() < 10) { + // too short + return Optional.empty(); + } + + LocalDate date = LocalDate.ofEpochDay(((long) value.getValue())); + if (date.getYear() < 1001 || date.getYear() > 9998) { + // Edge cases. 1-year margin so that we can go to next/prev year for < or > comparisons + return Optional.empty(); + } + + // superset of possible values, for the "normal case" + ValueSet valueSet; + boolean nullAllowed = false; + + switch (comparisonOperator) { + case EQUAL: + valueSet = dateStringRanges(date, sourceType); + break; + case NOT_EQUAL: + case IS_DISTINCT_FROM: + if (date.getDayOfMonth() < 10) { + // TODO: possible to handle but cumbersome + return Optional.empty(); + } + valueSet = ValueSet.all(sourceType).subtract(dateStringRanges(date, sourceType)); + nullAllowed = (comparisonOperator == IS_DISTINCT_FROM); + break; + case LESS_THAN: + case LESS_THAN_OR_EQUAL: + valueSet = ValueSet.ofRanges(Range.lessThan(sourceType, utf8Slice(Integer.toString(date.getYear() + 1)))); + break; + case GREATER_THAN: + case GREATER_THAN_OR_EQUAL: + valueSet = ValueSet.ofRanges(Range.greaterThan(sourceType, utf8Slice(Integer.toString(date.getYear() - 1)))); + break; + default: + return Optional.empty(); + } + + // Date representations starting with whitespace, sign or leading zeroes. + valueSet = valueSet.union(ValueSet.ofRanges( + Range.lessThan(sourceType, utf8Slice("1")), + Range.greaterThan(sourceType, utf8Slice("9")))); + + return Optional.of(new ExtractionResult( + TupleDomain.withColumnDomains(ImmutableMap.of(sourceSymbol, Domain.create(valueSet, nullAllowed))), + node)); + } + + /** + * @return Date representations of the form 2005-09-09, 2005-09-9, 2005-9-09 and 2005-9-9 expanded to ranges: + * {@code [2005-09-09, 2005-09-0:), [2005-09-9, 2005-09-:), [2005-9-09, 2005-9-0:), [2005-9-9, 2005-9-:)} + * (the {@code :} character is the next one after {@code 9}). + */ + private static SortedRangeSet dateStringRanges(LocalDate date, VarcharType domainType) + { + checkArgument(date.getYear() >= 1000 && date.getYear() <= 9999, "Unsupported date: %s", date); + + int month = date.getMonthValue(); + int day = date.getDayOfMonth(); + boolean isMonthSingleDigit = date.getMonthValue() < 10; + boolean isDaySingleDigit = date.getDayOfMonth() < 10; + + // A specific date value like 2005-09-10 can be a result of a CAST for number of various forms, + // as the value can have optional sign, leading zeros for the year, and surrounding whitespace, + // E.g. ' +002005-9-9 '. + + List valueRanges = new ArrayList<>(4); + for (boolean useSingleDigitMonth : List.of(true, false)) { + for (boolean useSingleDigitDay : List.of(true, false)) { + if (useSingleDigitMonth && !isMonthSingleDigit) { + continue; + } + if (useSingleDigitDay && !isDaySingleDigit) { + continue; + } + String dateString = date.getYear() + + ((!useSingleDigitMonth && isMonthSingleDigit) ? "-0" : "-") + month + + ((!useSingleDigitDay && isDaySingleDigit) ? "-0" : "-") + day; + String nextStringPrefix = dateString.substring(0, dateString.length() - 1) + (char) (dateString.charAt(dateString.length() - 1) + 1); // cannot overflow + verify(dateString.length() <= domainType.getLength().orElse(Integer.MAX_VALUE), "dateString length exceeds type bounds"); + verify(dateString.length() == nextStringPrefix.length(), "Next string length mismatch"); + valueRanges.add(Range.range(domainType, utf8Slice(dateString), true, utf8Slice(nextStringPrefix), false)); + } + } + return (SortedRangeSet) ValueSet.ofRanges(valueRanges); + } + private static Optional createComparisonExtractionResult(ComparisonExpression.Operator comparisonOperator, Symbol column, Type type, @Nullable Object value, boolean complement) { if (value == null) { @@ -990,7 +1114,7 @@ private Optional tryVisitLikePredicate(LikePredicate node, Boo VarcharType varcharType = (VarcharType) type; Symbol symbol = Symbol.from(node.getValue()); - Slice pattern = Slices.utf8Slice(((StringLiteral) node.getPattern()).getValue()); + Slice pattern = utf8Slice(((StringLiteral) node.getPattern()).getValue()); Optional escape = node.getEscape() .map(StringLiteral.class::cast) .map(StringLiteral::getValue) @@ -1064,7 +1188,7 @@ private Optional tryVisitStartsWithFunction(FunctionCall node, } Symbol symbol = Symbol.from(target); - Slice constantPrefix = Slices.utf8Slice(((StringLiteral) prefix).getValue()); + Slice constantPrefix = utf8Slice(((StringLiteral) prefix).getValue()); return createRangeDomain(type, constantPrefix).map(domain -> new ExtractionResult(TupleDomain.withColumnDomains(ImmutableMap.of(symbol, domain)), node)); } diff --git a/core/trino-main/src/main/java/io/trino/testing/assertions/TestUtil.java b/core/trino-main/src/main/java/io/trino/testing/assertions/TestUtil.java new file mode 100644 index 000000000000..f705e5b00afe --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/testing/assertions/TestUtil.java @@ -0,0 +1,41 @@ +/* + * Licensed 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 io.trino.testing.assertions; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +public final class TestUtil +{ + private TestUtil() {} + + public static void verifyResultOrFailure(Supplier callback, Consumer verifyResults, Consumer verifyFailure) + { + requireNonNull(callback, "callback is null"); + requireNonNull(verifyResults, "verifyResults is null"); + requireNonNull(verifyFailure, "verifyFailure is null"); + + T result; + try { + result = callback.get(); + } + catch (Throwable t) { + verifyFailure.accept(t); + return; + } + verifyResults.accept(result); + } +} diff --git a/core/trino-main/src/main/java/io/trino/type/DateOperators.java b/core/trino-main/src/main/java/io/trino/type/DateOperators.java index 1d06835b1020..b2f89b2bdd82 100644 --- a/core/trino-main/src/main/java/io/trino/type/DateOperators.java +++ b/core/trino-main/src/main/java/io/trino/type/DateOperators.java @@ -55,6 +55,8 @@ public static Slice castToVarchar(@LiteralParameter("x") long x, @SqlType(Standa @SqlType(StandardTypes.DATE) public static long castFromVarchar(@SqlType("varchar(x)") Slice value) { + // Note: update DomainTranslator.Visitor.createVarcharCastToDateComparisonExtractionResult whenever CAST behavior changes. + try { return parseDate(trim(value).toStringUtf8()); } diff --git a/core/trino-main/src/main/java/io/trino/util/DateTimeUtils.java b/core/trino-main/src/main/java/io/trino/util/DateTimeUtils.java index bb7e57c75303..0d568941d4e7 100644 --- a/core/trino-main/src/main/java/io/trino/util/DateTimeUtils.java +++ b/core/trino-main/src/main/java/io/trino/util/DateTimeUtils.java @@ -61,6 +61,8 @@ private DateTimeUtils() {} public static int parseDate(String value) { + // Note: update DomainTranslator.Visitor.createVarcharCastToDateComparisonExtractionResult whenever varchar->date conversion (CAST) behavior changes. + // in order to follow the standard, we should validate the value: // - the required format is 'YYYY-MM-DD' // - all components should be unsigned numbers diff --git a/core/trino-main/src/test/java/io/trino/sql/planner/TestDomainTranslator.java b/core/trino-main/src/test/java/io/trino/sql/planner/TestDomainTranslator.java index e9dd2757598e..f4963d1c5579 100644 --- a/core/trino-main/src/test/java/io/trino/sql/planner/TestDomainTranslator.java +++ b/core/trino-main/src/test/java/io/trino/sql/planner/TestDomainTranslator.java @@ -1001,6 +1001,90 @@ public void testFromComparisonsWithCoercions() assertPredicateIsAlwaysFalse(not(isDistinctFrom(cast(C_INTEGER, DOUBLE), doubleLiteral(2.1)))); } + @Test + public void testPredicateWithVarcharCastToDate() + { + // = + assertPredicateDerives( + equal(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", " +2005-9-10 \t")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("1")), + Range.range(VARCHAR, utf8Slice("2005-09-10"), true, utf8Slice("2005-09-11"), false), + Range.range(VARCHAR, utf8Slice("2005-9-10"), true, utf8Slice("2005-9-11"), false), + Range.greaterThan(VARCHAR, utf8Slice("9"))), + false))); + // = with day ending with 9 + assertPredicateDerives( + equal(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", "2005-09-09")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("1")), + Range.range(VARCHAR, utf8Slice("2005-09-09"), true, utf8Slice("2005-09-0:"), false), + Range.range(VARCHAR, utf8Slice("2005-09-9"), true, utf8Slice("2005-09-:"), false), + Range.range(VARCHAR, utf8Slice("2005-9-09"), true, utf8Slice("2005-9-0:"), false), + Range.range(VARCHAR, utf8Slice("2005-9-9"), true, utf8Slice("2005-9-:"), false), + Range.greaterThan(VARCHAR, utf8Slice("9"))), + false))); + assertPredicateDerives( + equal(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", "2005-09-19")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("1")), + Range.range(VARCHAR, utf8Slice("2005-09-19"), true, utf8Slice("2005-09-1:"), false), + Range.range(VARCHAR, utf8Slice("2005-9-19"), true, utf8Slice("2005-9-1:"), false), + Range.greaterThan(VARCHAR, utf8Slice("9"))), + false))); + + // != + assertPredicateDerives( + notEqual(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", " +2005-9-10 \t")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("2005-09-10")), + Range.range(VARCHAR, utf8Slice("2005-09-11"), true, utf8Slice("2005-9-10"), false), + Range.greaterThanOrEqual(VARCHAR, utf8Slice("2005-9-11"))), + false))); + + // != with single-digit day + assertUnsupportedPredicate( + notEqual(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", " +2005-9-2 \t"))); + // != with day ending with 9 + assertUnsupportedPredicate( + notEqual(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", "2005-09-09"))); + assertPredicateDerives( + notEqual(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", "2005-09-19")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("2005-09-19")), + Range.range(VARCHAR, utf8Slice("2005-09-1:"), true, utf8Slice("2005-9-19"), false), + Range.greaterThanOrEqual(VARCHAR, utf8Slice("2005-9-1:"))), + false))); + + // < + assertPredicateDerives( + lessThan(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", " +2005-9-10 \t")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("2006")), + Range.greaterThan(VARCHAR, utf8Slice("9"))), + false))); + + // > + assertPredicateDerives( + greaterThan(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", " +2005-9-10 \t")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("1")), + Range.greaterThan(VARCHAR, utf8Slice("2004"))), + false))); + + // BETWEEN + assertPredicateTranslates( + between(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", "2001-01-31"), new GenericLiteral("DATE", "2005-09-10")), + tupleDomain(C_VARCHAR, Domain.create(ValueSet.ofRanges( + Range.lessThan(VARCHAR, utf8Slice("1")), + Range.range(VARCHAR, utf8Slice("2000"), false, utf8Slice("2006"), false), + Range.greaterThan(VARCHAR, utf8Slice("9"))), + false)), + and( + greaterThanOrEqual(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", "2001-01-31")), + lessThanOrEqual(cast(C_VARCHAR, DATE), new GenericLiteral("DATE", "2005-09-10")))); + } + @Test public void testFromUnprocessableInPredicate() { @@ -1911,6 +1995,11 @@ private void assertPredicateTranslates(Expression expression, TupleDomain tupleDomain) + { + assertPredicateTranslates(expression, tupleDomain, expression); + } + private void assertPredicateTranslates(Expression expression, TupleDomain tupleDomain, Expression remainingExpression) { ExtractionResult result = fromPredicate(expression); diff --git a/core/trino-main/src/test/java/io/trino/sql/query/QueryAssertions.java b/core/trino-main/src/test/java/io/trino/sql/query/QueryAssertions.java index 1d84ac176faf..df402d455f47 100644 --- a/core/trino-main/src/test/java/io/trino/sql/query/QueryAssertions.java +++ b/core/trino-main/src/test/java/io/trino/sql/query/QueryAssertions.java @@ -444,7 +444,7 @@ public QueryAssert isFullyPushedDown() if (!skipResultsCorrectnessCheckForPushdown) { // Compare the results with pushdown disabled, so that explicit matches() call is not needed - verifyResultsWithPushdownDisabled(); + hasCorrectResultsRegardlessOfPushdown(); } return this; } @@ -511,17 +511,19 @@ private QueryAssert hasPlan(PlanMatchPattern expectedPlan, Consumer additi if (!skipResultsCorrectnessCheckForPushdown) { // Compare the results with pushdown disabled, so that explicit matches() call is not needed - verifyResultsWithPushdownDisabled(); + hasCorrectResultsRegardlessOfPushdown(); } return this; } - private void verifyResultsWithPushdownDisabled() + @CanIgnoreReturnValue + public QueryAssert hasCorrectResultsRegardlessOfPushdown() { Session withoutPushdown = Session.builder(session) .setSystemProperty("allow_pushdown_into_connectors", "false") .build(); matches(runner.execute(withoutPushdown, query)); + return this; } } diff --git a/core/trino-main/src/test/java/io/trino/type/TestDateTimeOperators.java b/core/trino-main/src/test/java/io/trino/type/TestDateTimeOperators.java index f3aef0373979..34ab7a0f3a12 100644 --- a/core/trino-main/src/test/java/io/trino/type/TestDateTimeOperators.java +++ b/core/trino-main/src/test/java/io/trino/type/TestDateTimeOperators.java @@ -303,6 +303,8 @@ public void testDateLiteral() @Test public void testDateCastFromVarchar() { + // Note: update DomainTranslator.Visitor.createVarcharCastToDateComparisonExtractionResult whenever CAST behavior changes. + assertFunction("CAST('2013-02-02' AS date)", DATE, toDate(new DateTime(2013, 2, 2, 0, 0, 0, 0, UTC))); // one digit for month or day assertFunction("CAST('2013-2-02' AS date)", DATE, toDate(new DateTime(2013, 2, 2, 0, 0, 0, 0, UTC))); diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/TestIcebergMetadataFileOperations.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/TestIcebergMetadataFileOperations.java index 6df4e01a8c96..c6f913a585ab 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/TestIcebergMetadataFileOperations.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/TestIcebergMetadataFileOperations.java @@ -334,6 +334,55 @@ public void testShowStatsForTableWithFilter() .build()); } + @Test + public void testPredicateWithVarcharCastToDate() + { + assertUpdate("CREATE TABLE test_varchar_as_date_predicate(a varchar) WITH (partitioning=ARRAY['truncate(a, 4)'])"); + assertUpdate("INSERT INTO test_varchar_as_date_predicate VALUES '2001-01-31'", 1); + assertUpdate("INSERT INTO test_varchar_as_date_predicate VALUES '2005-09-10'", 1); + + assertFileSystemAccesses("SELECT * FROM test_varchar_as_date_predicate", + ImmutableMultiset.builder() + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_GET_LENGTH), 4) + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_NEW_STREAM), 4) + .addCopies(new FileOperation(METADATA_JSON, INPUT_FILE_NEW_STREAM), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_GET_LENGTH), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_NEW_STREAM), 1) + .build()); + + // CAST to date and comparison + assertFileSystemAccesses("SELECT * FROM test_varchar_as_date_predicate WHERE CAST(a AS date) >= DATE '2005-01-01'", + ImmutableMultiset.builder() + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_GET_LENGTH), 2) // fewer than without filter + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_NEW_STREAM), 2) // fewer than without filter + .addCopies(new FileOperation(METADATA_JSON, INPUT_FILE_NEW_STREAM), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_GET_LENGTH), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_NEW_STREAM), 1) + .build()); + + // CAST to date and BETWEEN + assertFileSystemAccesses("SELECT * FROM test_varchar_as_date_predicate WHERE CAST(a AS date) BETWEEN DATE '2005-01-01' AND DATE '2005-12-31'", + ImmutableMultiset.builder() + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_GET_LENGTH), 2) // fewer than without filter + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_NEW_STREAM), 2) // fewer than without filter + .addCopies(new FileOperation(METADATA_JSON, INPUT_FILE_NEW_STREAM), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_GET_LENGTH), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_NEW_STREAM), 1) + .build()); + + // conversion to date as a date function + assertFileSystemAccesses("SELECT * FROM test_varchar_as_date_predicate WHERE date(a) >= DATE '2005-01-01'", + ImmutableMultiset.builder() + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_GET_LENGTH), 2) // fewer than without filter + .addCopies(new FileOperation(MANIFEST, INPUT_FILE_NEW_STREAM), 2) // fewer than without filter + .addCopies(new FileOperation(METADATA_JSON, INPUT_FILE_NEW_STREAM), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_GET_LENGTH), 1) + .addCopies(new FileOperation(SNAPSHOT, INPUT_FILE_NEW_STREAM), 1) + .build()); + + assertUpdate("DROP TABLE test_varchar_as_date_predicate"); + } + private void assertFileSystemAccesses(@Language("SQL") String query, Multiset expectedAccesses) { resetCounts(); diff --git a/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduConnectorTest.java b/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduConnectorTest.java index a92fad6a8b5e..2d73b6e46c4a 100644 --- a/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduConnectorTest.java +++ b/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduConnectorTest.java @@ -620,6 +620,15 @@ public void testDateYearOfEraPredicate() .hasStackTraceContaining("Cannot apply operator: varchar = date"); } + @Override + public void testVarcharCastToDateInPredicate() + { + assertThatThrownBy(super::testVarcharCastToDateInPredicate) + .hasStackTraceContaining("Table partitioning must be specified using setRangePartitionColumns or addHashPartitions"); + + throw new SkipException("TODO: implement the test for Kudu"); + } + @Test @Override public void testCharVarcharComparison() diff --git a/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java b/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java index 3bb01b32cfd1..1465671d65a4 100644 --- a/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java +++ b/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java @@ -110,6 +110,7 @@ import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_UPDATE; import static io.trino.testing.assertions.Assert.assertEquals; import static io.trino.testing.assertions.Assert.assertEventually; +import static io.trino.testing.assertions.TestUtil.verifyResultOrFailure; import static io.trino.testing.sql.TestTable.randomTableSuffix; import static io.trino.transaction.TransactionBuilder.transaction; import static java.lang.String.format; @@ -503,6 +504,77 @@ public void testSortItemsReflectedInExplain() expectedPattern); } + // CAST(a_varchar AS date) = DATE '...' has a special handling in the DomainTranslator and is worth testing this across connectors + @Test + public void testVarcharCastToDateInPredicate() + { + skipTestUnless(hasBehavior(SUPPORTS_CREATE_TABLE_WITH_DATA)); + + try (TestTable table = new TestTable( + getQueryRunner()::execute, + "varchar_as_date_pred", + "(a varchar)", + List.of( + "'999-09-09'", + "'1005-09-09'", + "'2005-06-06'", "'2005-06-6'", "'2005-6-06'", "'2005-6-6'", "' 2005-06-06'", "'2005-06-06 '", "' +2005-06-06'", "'02005-06-06'", + "'2005-09-06'", "'2005-09-6'", "'2005-9-06'", "'2005-9-6'", "' 2005-09-06'", "'2005-09-06 '", "' +2005-09-06'", "'02005-09-06'", + "'2005-09-09'", "'2005-09-9'", "'2005-9-09'", "'2005-9-9'", "' 2005-09-09'", "'2005-09-09 '", "' +2005-09-09'", "'02005-09-09'", + "'2005-09-10'", "'2005-9-10'", "' 2005-09-10'", "'2005-09-10 '", "' +2005-09-10'", "'02005-09-10'", + "'2005-09-20'", "'2005-9-20'", "' 2005-09-20'", "'2005-09-20 '", "' +2005-09-20'", "'02005-09-20'", + "'9999-09-09'", + "'99999-09-09'"))) { + for (String date : List.of("2005-09-06", "2005-09-09", "2005-09-10")) { + for (String operator : List.of("=", "<=", "<", ">", ">=", "!=", "IS DISTINCT FROM", "IS NOT DISTINCT FROM")) { + assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) %s DATE '%s'".formatted(table.getName(), operator, date))) + .hasCorrectResultsRegardlessOfPushdown(); + } + } + } + + try (TestTable table = new TestTable( + getQueryRunner()::execute, + "varchar_as_date_pred", + "(a varchar)", + List.of("'2005-06-bad-date'", "'2005-09-10'"))) { + assertThatThrownBy(() -> query("SELECT a FROM %s WHERE CAST(a AS date) < DATE '2005-09-10'".formatted(table.getName()))) + .hasMessage("Value cannot be cast to date: 2005-06-bad-date"); + verifyResultOrFailure( + () -> query("SELECT a FROM %s WHERE CAST(a AS date) = DATE '2005-09-10'".formatted(table.getName())), + queryAssert -> assertThat(queryAssert) + .skippingTypesCheck() + .matches("VALUES '2005-09-10'"), + failure -> assertThat(failure) + .hasMessage("Value cannot be cast to date: 2005-06-bad-date")); + // This failure isn't guaranteed: a row may be filtered out on the connector side with a derived predicate on a varchar column. + verifyResultOrFailure( + () -> query("SELECT a FROM %s WHERE CAST(a AS date) != DATE '2005-9-1'".formatted(table.getName())), + queryAssert -> assertThat(queryAssert) + .skippingTypesCheck() + .matches("VALUES '2005-09-10'"), + failure -> assertThat(failure) + .hasMessage("Value cannot be cast to date: 2005-06-bad-date")); + // This failure isn't guaranteed: a row may be filtered out on the connector side with a derived predicate on a varchar column. + verifyResultOrFailure( + () -> query("SELECT a FROM %s WHERE CAST(a AS date) > DATE '2022-08-10'".formatted(table.getName())), + queryAssert -> assertThat(queryAssert) + .skippingTypesCheck() + .returnsEmptyResult(), + failure -> assertThat(failure) + .hasMessage("Value cannot be cast to date: 2005-06-bad-date")); + } + try (TestTable table = new TestTable( + getQueryRunner()::execute, + "varchar_as_date_pred", + "(a varchar)", + List.of("'2005-09-10'"))) { + // 2005-09-01, when written as 2005-09-1, is a prefix of an existing data point: 2005-09-10 + assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) != DATE '2005-09-01'".formatted(table.getName()))) + .skippingTypesCheck() + .matches("VALUES '2005-09-10'"); + } + } + @Test public void testConcurrentScans() {