diff --git a/paimon-common/src/main/java/org/apache/paimon/casting/DateToStringCastRule.java b/paimon-common/src/main/java/org/apache/paimon/casting/DateToStringCastRule.java index 5ec3391eaab8..94eea00a71ec 100644 --- a/paimon-common/src/main/java/org/apache/paimon/casting/DateToStringCastRule.java +++ b/paimon-common/src/main/java/org/apache/paimon/casting/DateToStringCastRule.java @@ -20,23 +20,34 @@ import org.apache.paimon.data.BinaryString; import org.apache.paimon.types.DataType; +import org.apache.paimon.types.DataTypeChecks; import org.apache.paimon.types.DataTypeFamily; import org.apache.paimon.types.DataTypeRoot; +import org.apache.paimon.types.VarCharType; +import org.apache.paimon.utils.BinaryStringUtils; import org.apache.paimon.utils.DateTimeUtils; -import static org.apache.paimon.types.VarCharType.STRING_TYPE; - /** {@link DataTypeRoot#DATE} to {@link DataTypeFamily#CHARACTER_STRING} cast rule. */ class DateToStringCastRule extends AbstractCastRule { static final DateToStringCastRule INSTANCE = new DateToStringCastRule(); private DateToStringCastRule() { - super(CastRulePredicate.builder().input(DataTypeRoot.DATE).target(STRING_TYPE).build()); + super( + CastRulePredicate.builder() + .input(DataTypeRoot.DATE) + .target(DataTypeFamily.CHARACTER_STRING) + .build()); } @Override public CastExecutor create(DataType inputType, DataType targetType) { - return value -> BinaryString.fromString(DateTimeUtils.formatDate(value)); + boolean padOrTrim = + targetType.is(DataTypeRoot.CHAR) + || DataTypeChecks.getLength(targetType) != VarCharType.MAX_LENGTH; + return value -> { + BinaryString result = BinaryString.fromString(DateTimeUtils.formatDate(value)); + return padOrTrim ? BinaryStringUtils.toCharacterString(result, targetType) : result; + }; } } diff --git a/paimon-common/src/main/java/org/apache/paimon/casting/TimeToStringCastRule.java b/paimon-common/src/main/java/org/apache/paimon/casting/TimeToStringCastRule.java index d0f8ba768048..f61f15c74e23 100644 --- a/paimon-common/src/main/java/org/apache/paimon/casting/TimeToStringCastRule.java +++ b/paimon-common/src/main/java/org/apache/paimon/casting/TimeToStringCastRule.java @@ -23,10 +23,10 @@ import org.apache.paimon.types.DataTypeChecks; import org.apache.paimon.types.DataTypeFamily; import org.apache.paimon.types.DataTypeRoot; +import org.apache.paimon.types.VarCharType; +import org.apache.paimon.utils.BinaryStringUtils; import org.apache.paimon.utils.DateTimeUtils; -import static org.apache.paimon.types.VarCharType.STRING_TYPE; - /** * {@link DataTypeRoot#TIME_WITHOUT_TIME_ZONE} to {@link DataTypeFamily#CHARACTER_STRING} cast rule. */ @@ -38,15 +38,20 @@ private TimeToStringCastRule() { super( CastRulePredicate.builder() .input(DataTypeRoot.TIME_WITHOUT_TIME_ZONE) - .target(STRING_TYPE) + .target(DataTypeFamily.CHARACTER_STRING) .build()); } @Override public CastExecutor create(DataType inputType, DataType targetType) { - return value -> - BinaryString.fromString( - DateTimeUtils.formatTimestampMillis( - value, DataTypeChecks.getPrecision(inputType))); + final int precision = DataTypeChecks.getPrecision(inputType); + boolean padOrTrim = + targetType.is(DataTypeRoot.CHAR) + || DataTypeChecks.getLength(targetType) != VarCharType.MAX_LENGTH; + return value -> { + BinaryString result = + BinaryString.fromString(DateTimeUtils.formatTimestampMillis(value, precision)); + return padOrTrim ? BinaryStringUtils.toCharacterString(result, targetType) : result; + }; } } diff --git a/paimon-common/src/main/java/org/apache/paimon/casting/TimestampToStringCastRule.java b/paimon-common/src/main/java/org/apache/paimon/casting/TimestampToStringCastRule.java index 18016edc69a0..f433e559c36f 100644 --- a/paimon-common/src/main/java/org/apache/paimon/casting/TimestampToStringCastRule.java +++ b/paimon-common/src/main/java/org/apache/paimon/casting/TimestampToStringCastRule.java @@ -24,12 +24,12 @@ import org.apache.paimon.types.DataTypeChecks; import org.apache.paimon.types.DataTypeFamily; import org.apache.paimon.types.DataTypeRoot; +import org.apache.paimon.types.VarCharType; +import org.apache.paimon.utils.BinaryStringUtils; import org.apache.paimon.utils.DateTimeUtils; import java.util.TimeZone; -import static org.apache.paimon.types.VarCharType.STRING_TYPE; - /** {@link DataTypeFamily#TIMESTAMP} to {@link DataTypeFamily#CHARACTER_STRING} cast rule. */ class TimestampToStringCastRule extends AbstractCastRule { @@ -39,7 +39,7 @@ private TimestampToStringCastRule() { super( CastRulePredicate.builder() .input(DataTypeFamily.TIMESTAMP) - .target(STRING_TYPE) + .target(DataTypeFamily.CHARACTER_STRING) .build()); } @@ -50,7 +50,14 @@ public CastExecutor create(DataType inputType, DataType inputType.is(DataTypeRoot.TIMESTAMP_WITH_LOCAL_TIME_ZONE) ? TimeZone.getDefault() : DateTimeUtils.UTC_ZONE; - return value -> - BinaryString.fromString(DateTimeUtils.formatTimestamp(value, timeZone, precision)); + boolean padOrTrim = + targetType.is(DataTypeRoot.CHAR) + || DataTypeChecks.getLength(targetType) != VarCharType.MAX_LENGTH; + return value -> { + BinaryString result = + BinaryString.fromString( + DateTimeUtils.formatTimestamp(value, timeZone, precision)); + return padOrTrim ? BinaryStringUtils.toCharacterString(result, targetType) : result; + }; } } diff --git a/paimon-common/src/test/java/org/apache/paimon/casting/DateTimeToCharacterStringCastRuleTest.java b/paimon-common/src/test/java/org/apache/paimon/casting/DateTimeToCharacterStringCastRuleTest.java new file mode 100644 index 000000000000..f6ebb7d36454 --- /dev/null +++ b/paimon-common/src/test/java/org/apache/paimon/casting/DateTimeToCharacterStringCastRuleTest.java @@ -0,0 +1,97 @@ +/* + * 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.paimon.casting; + +import org.apache.paimon.data.BinaryString; +import org.apache.paimon.data.Timestamp; +import org.apache.paimon.types.CharType; +import org.apache.paimon.types.DataType; +import org.apache.paimon.types.DateType; +import org.apache.paimon.types.LocalZonedTimestampType; +import org.apache.paimon.types.TimeType; +import org.apache.paimon.types.TimestampType; +import org.apache.paimon.types.VarCharType; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests casting DATE, TIME and TIMESTAMP to a bounded or non-nullable character string. Those + * targets resolve to no rule until the rules are keyed on the string family, and the unbounded + * cases cover the branch that skips the trim and pad. + */ +public class DateTimeToCharacterStringCastRuleTest { + + @Test + public void testDateToBoundedString() { + DateType date = new DateType(); + + assertThat(cast(date, new VarCharType(4), 0)).isEqualTo("1970"); + assertThat(cast(date, new CharType(12), 0)).isEqualTo("1970-01-01 "); + assertThat(cast(date, VarCharType.STRING_TYPE, 0)).isEqualTo("1970-01-01"); + assertThat(cast(date, VarCharType.stringType(false), 0)).isEqualTo("1970-01-01"); + } + + @Test + public void testTimeToBoundedString() { + assertThat(cast(new TimeType(0), new VarCharType(5), 3661000)).isEqualTo("01:01"); + assertThat(cast(new TimeType(0), new CharType(10), 3661000)).isEqualTo("01:01:01 "); + assertThat(cast(new TimeType(0), VarCharType.STRING_TYPE, 3661000)).isEqualTo("01:01:01"); + + // the input precision has to survive: a bounded target must cut the fraction, not the rule + assertThat(cast(new TimeType(3), VarCharType.STRING_TYPE, 3661123)) + .isEqualTo("01:01:01.123"); + assertThat(cast(new TimeType(3), new VarCharType(8), 3661123)).isEqualTo("01:01:01"); + } + + @Test + public void testTimestampToBoundedString() { + TimestampType timestamp = new TimestampType(3); + Timestamp value = Timestamp.fromEpochMillis(0); + + assertThat(cast(timestamp, new VarCharType(10), value)).isEqualTo("1970-01-01"); + assertThat(cast(timestamp, new CharType(25), value)).isEqualTo("1970-01-01 00:00:00.000 "); + assertThat(cast(timestamp, VarCharType.STRING_TYPE, value)) + .isEqualTo("1970-01-01 00:00:00.000"); + assertThat(cast(timestamp, VarCharType.stringType(false), value)) + .isEqualTo("1970-01-01 00:00:00.000"); + } + + @Test + public void testLocalZonedTimestampToBoundedString() { + // this input keeps the default time zone rather than UTC, so pin the bounded result + // against the unbounded one instead of a fixed instant + LocalZonedTimestampType ltz = new LocalZonedTimestampType(3); + Timestamp value = Timestamp.fromEpochMillis(0); + + String unbounded = cast(ltz, VarCharType.STRING_TYPE, value); + assertThat(unbounded).hasSize(23); + assertThat(cast(ltz, new VarCharType(10), value)).isEqualTo(unbounded.substring(0, 10)); + assertThat(cast(ltz, new CharType(25), value)).isEqualTo(unbounded + " "); + } + + @SuppressWarnings("unchecked") + private static String cast(DataType inputType, DataType targetType, T value) { + CastExecutor executor = + (CastExecutor) CastExecutors.resolve(inputType, targetType); + assertThat(executor).as("no cast rule for %s to %s", inputType, targetType).isNotNull(); + return executor.cast(value).toString(); + } +} diff --git a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/SchemaChangeITCase.java b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/SchemaChangeITCase.java index 43d817477f63..9a0b6cb06647 100644 --- a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/SchemaChangeITCase.java +++ b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/SchemaChangeITCase.java @@ -354,6 +354,37 @@ public void testModifyColumnTypeFromTimestampToString() { + "]"); } + @Test + public void testModifyColumnTypeFromTimestampToBoundedString() { + // a bounded CHAR/VARCHAR target has to resolve to the same rule, then trim or pad + sql( + "CREATE TABLE T (a STRING PRIMARY KEY NOT ENFORCED, b TIMESTAMP(3), d DATE, f TIME, g TIMESTAMP(3) WITH LOCAL TIME ZONE)"); + sql( + "INSERT INTO T VALUES('paimon', TIMESTAMP '2023-06-06 12:00:00', DATE '2023-05-31', TIME '14:30:00', TO_TIMESTAMP_LTZ(4001, 3))"); + + sql("ALTER TABLE T MODIFY (b VARCHAR(10), d CHAR(12), f VARCHAR(5), g VARCHAR(10))"); + List result = sql("SHOW CREATE TABLE T"); + assertThat(result.toString()) + .contains( + "CREATE TABLE `PAIMON`.`default`.`T` (\n" + + " `a` VARCHAR(2147483647) NOT NULL,\n" + + " `b` VARCHAR(10),\n" + + " `d` CHAR(12),\n" + + " `f` VARCHAR(5),\n" + + " `g` VARCHAR(10),"); + String localZoned = + DateTimeUtils.formatTimestamp( + DateTimeUtils.parseTimestampData("1970-01-01 00:00:04.001", 3), + TimeZone.getDefault(), + 3); + result = sql("SELECT * FROM T"); + assertThat(result.stream().map(Objects::toString).collect(Collectors.toList())) + .containsExactly( + "+I[paimon, 2023-06-06, 2023-05-31 , 14:30, " + + localZoned.substring(0, 10) + + "]"); + } + @Test public void testModifyColumnTypeFromStringToString() { sql("CREATE TABLE T (b VARCHAR(10), c VARCHAR(10), d CHAR(5), e CHAR(5))");