diff --git a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java index fba03ce4215c..7e644863b974 100644 --- a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java +++ b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java @@ -99,6 +99,7 @@ import io.trino.sql.tree.StringLiteral; import io.trino.sql.tree.TableElement; import io.trino.sql.tree.Values; +import io.trino.util.DateTimeUtils; import java.util.ArrayList; import java.util.Collection; @@ -561,7 +562,8 @@ private Query showCreateMaterializedView(ShowCreate node) query, false, false, - Optional.empty(), // TODO support GRACE PERIOD + viewDefinition.get().getGracePeriod() + .map(DateTimeUtils::formatDayTimeInterval), Optional.empty(), // TODO support WHEN STALE propertyNodes, viewDefinition.get().getComment())).trim(); 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 c5aabbf40656..affafb444b75 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 @@ -18,10 +18,12 @@ import io.trino.client.IntervalYearMonth; import io.trino.spi.TrinoException; import io.trino.spi.type.TimeZoneKey; +import io.trino.sql.tree.IntervalLiteral; import org.joda.time.DateTime; import org.joda.time.DurationFieldType; import org.joda.time.MutablePeriod; import org.joda.time.Period; +import org.joda.time.PeriodType; import org.joda.time.ReadWritablePeriod; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -34,6 +36,7 @@ import org.joda.time.format.PeriodParser; import java.time.DateTimeException; +import java.time.Duration; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -46,6 +49,10 @@ import static com.google.common.base.Preconditions.checkArgument; import static io.trino.spi.StandardErrorCode.INVALID_LITERAL; import static io.trino.sql.tree.IntervalLiteral.IntervalField; +import static io.trino.sql.tree.IntervalLiteral.IntervalField.DAY; +import static io.trino.sql.tree.IntervalLiteral.IntervalField.SECOND; +import static io.trino.sql.tree.IntervalLiteral.Sign.NEGATIVE; +import static io.trino.sql.tree.IntervalLiteral.Sign.POSITIVE; import static io.trino.util.DateTimeZoneIndex.getChronology; import static io.trino.util.DateTimeZoneIndex.packDateTimeWithZone; import static java.lang.Math.toIntExact; @@ -254,6 +261,19 @@ public static long parseDayTimeInterval(String value, IntervalField startField, throw invalidQualifier(startField, endField.orElse(startField)); } + public static IntervalLiteral formatDayTimeInterval(Duration duration) + { + long millis = duration.toMillis(); + IntervalLiteral.Sign sign = millis < 0 ? NEGATIVE : POSITIVE; + Period period = new Period(Math.abs(millis)).normalizedStandard(PeriodType.dayTime()); + // Always use INTERVAL DAY TO SECOND. The output is more verbose + // (e.g., "1 0:00:00" instead of "1"), but this avoids the need to + // determine the minimal field range and choose a specialized formatter. + String value = INTERVAL_DAY_SECOND_FORMATTER.print(period); + + return new IntervalLiteral(value, sign, DAY, Optional.of(SECOND)); + } + private static long parsePeriodMillis(PeriodFormatter periodFormatter, String value) { Period period = parsePeriod(periodFormatter, value); @@ -337,7 +357,12 @@ private static PeriodFormatter createPeriodFormatter(IntervalField startField, I List parsers = new ArrayList<>(); - PeriodFormatterBuilder builder = new PeriodFormatterBuilder(); + PeriodFormatterBuilder builder = new PeriodFormatterBuilder() + // Ensures zero-valued fields are printed instead of omitted. This affects printing only, not parsing. + // Example for INTERVAL HOUR TO SECOND: + // With printZeroIfSupported(): "2:00:45" + // Without printZeroIfSupported(): "2::45" + .printZeroIfSupported(); switch (startField) { case YEAR: builder.appendYears(); @@ -372,6 +397,12 @@ private static PeriodFormatter createPeriodFormatter(IntervalField startField, I break; } builder.appendLiteral(":"); + // Ensures fixed-width, zero-padded minutes. This affects printing only, not parsing. + // Applies to the next appended field (minutes). + // Example for INTERVAL HOUR TO MINUTE: + // With minimumPrintedDigits(2): "2:05" + // Without minimumPrintedDigits(2): "2:5" + builder.minimumPrintedDigits(2); // fall through case MINUTE: @@ -381,6 +412,12 @@ private static PeriodFormatter createPeriodFormatter(IntervalField startField, I break; } builder.appendLiteral(":"); + // Ensures fixed-width, zero-padded seconds. This affects printing only, not parsing. + // Applies to the next appended field (seconds). + // Example for INTERVAL HOUR TO SECOND: + // With minimumPrintedDigits(2): "2:05:07" + // Without minimumPrintedDigits(2): "2:05:7" + builder.minimumPrintedDigits(2); // fall through case SECOND: diff --git a/core/trino-main/src/test/java/io/trino/type/TestIntervalDayTime.java b/core/trino-main/src/test/java/io/trino/type/TestIntervalDayTime.java index 815cc6190a25..921bfd5c8be8 100644 --- a/core/trino-main/src/test/java/io/trino/type/TestIntervalDayTime.java +++ b/core/trino-main/src/test/java/io/trino/type/TestIntervalDayTime.java @@ -13,13 +13,19 @@ */ package io.trino.type; +import io.trino.sql.ExpressionFormatter; import io.trino.sql.query.QueryAssertions; +import io.trino.sql.query.QueryAssertions.ExpressionAssertProvider.Result; +import io.trino.sql.tree.IntervalLiteral; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.parallel.Execution; +import java.time.Duration; + import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; import static io.trino.spi.function.OperatorType.ADD; import static io.trino.spi.function.OperatorType.DIVIDE; @@ -32,6 +38,7 @@ import static io.trino.spi.function.OperatorType.SUBTRACT; import static io.trino.spi.type.VarcharType.VARCHAR; import static io.trino.testing.assertions.TrinoExceptionAssert.assertTrinoExceptionThrownBy; +import static io.trino.util.DateTimeUtils.formatDayTimeInterval; import static java.util.concurrent.TimeUnit.DAYS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; @@ -523,4 +530,74 @@ public void testIndeterminate() assertThat(assertions.operator(INDETERMINATE, "INTERVAL '45' MINUTE TO SECOND")) .isEqualTo(false); } + + @Test + void testIntervalDayTimeRoundTrip() + { + testIntervalDayTimeRoundTrip("INTERVAL '0' SECOND", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'0' SECOND", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '0.000' SECOND", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '0.4' SECOND", "INTERVAL '0 0:00:00.400' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '0.04' SECOND", "INTERVAL '0 0:00:00.040' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '0.040' SECOND", "INTERVAL '0 0:00:00.040' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '45' SECOND", "INTERVAL '0 0:00:45' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'45' SECOND", "INTERVAL -'0 0:00:45' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '0.555' SECOND", "INTERVAL '0 0:00:00.555' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '59.999' SECOND", "INTERVAL '0 0:00:59.999' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '60' SECOND", "INTERVAL '0 0:01:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '61' SECOND", "INTERVAL '0 0:01:01' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '3661' SECOND", "INTERVAL '0 1:01:01' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '90061' SECOND", "INTERVAL '1 1:01:01' DAY TO SECOND"); + + testIntervalDayTimeRoundTrip("INTERVAL '0' MINUTE", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'0' MINUTE", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '25' MINUTE", "INTERVAL '0 0:25:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'25' MINUTE", "INTERVAL -'0 0:25:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '15:30' MINUTE TO SECOND", "INTERVAL '0 0:15:30' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '59:00.999' MINUTE TO SECOND", "INTERVAL '0 0:59:00.999' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '60' MINUTE", "INTERVAL '0 1:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '61' MINUTE", "INTERVAL '0 1:01:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1500' MINUTE", "INTERVAL '1 1:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1501' MINUTE", "INTERVAL '1 1:01:00' DAY TO SECOND"); + + testIntervalDayTimeRoundTrip("INTERVAL '0' HOUR", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'0' HOUR", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '8' HOUR", "INTERVAL '0 8:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'8' HOUR", "INTERVAL -'0 8:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '2:45' HOUR TO MINUTE", "INTERVAL '0 2:45:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '2:00:45' HOUR TO SECOND", "INTERVAL '0 2:00:45' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1:30:45' HOUR TO SECOND", "INTERVAL '0 1:30:45' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1:00:00.999' HOUR TO SECOND", "INTERVAL '0 1:00:00.999' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '24' HOUR", "INTERVAL '1 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '25' HOUR", "INTERVAL '1 1:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '17520' HOUR", "INTERVAL '730 0:00:00' DAY TO SECOND"); + + testIntervalDayTimeRoundTrip("INTERVAL '0' DAY", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'0' DAY", "INTERVAL '0 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '340' DAY", "INTERVAL '340 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL -'340' DAY", "INTERVAL -'340 0:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '2 6' DAY TO HOUR", "INTERVAL '2 6:00:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '3 0:30' DAY TO MINUTE", "INTERVAL '3 0:30:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '3 12:30' DAY TO MINUTE", "INTERVAL '3 12:30:00' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1 0:00:15' DAY TO SECOND", "INTERVAL '1 0:00:15' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1 4:20:15' DAY TO SECOND", "INTERVAL '1 4:20:15' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1 0:00:00.999' DAY TO SECOND", "INTERVAL '1 0:00:00.999' DAY TO SECOND"); + testIntervalDayTimeRoundTrip("INTERVAL '1 23:59:59.999' DAY TO SECOND", "INTERVAL '1 23:59:59.999' DAY TO SECOND"); + } + + private void testIntervalDayTimeRoundTrip(@Language("SQL") String input, @Language("SQL") String expectedFormatted) + { + Result evaluatedResult = assertions.expression(input).evaluate(); + assertThat(evaluatedResult.type()).isEqualTo(IntervalDayTimeType.INTERVAL_DAY_TIME); + SqlIntervalDayTime originalInterval = (SqlIntervalDayTime) evaluatedResult.value(); + + Duration duration = Duration.ofMillis(originalInterval.getMillis()); + IntervalLiteral formattedLiteral = formatDayTimeInterval(duration); + String formatted = ExpressionFormatter.formatExpression(formattedLiteral); + assertThat(formatted).isEqualTo(expectedFormatted); + + Result reparsedResult = assertions.expression(formatted).evaluate(); + SqlIntervalDayTime reparsedInterval = (SqlIntervalDayTime) reparsedResult.value(); + assertThat(reparsedInterval).isEqualTo(originalInterval); + } } 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 e1afcf56b66d..7378b28de8fc 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 @@ -1532,6 +1532,8 @@ public void testMaterializedViewGracePeriod() assertUpdate("CREATE MATERIALIZED VIEW " + viewName + " " + "GRACE PERIOD INTERVAL '1' HOUR " + "AS SELECT DISTINCT regionkey, format('%s', name) name FROM " + table.getName()); + assertThat((String) computeScalar("SHOW CREATE MATERIALIZED VIEW " + viewName)) + .matches("(?sm).*^GRACE PERIOD INTERVAL '0 1:00:00' DAY TO SECOND$.*"); String initialResults = "SELECT DISTINCT regionkey, CAST(name AS varchar) FROM region";