This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new f87110c56a [common][flink] Fix missing cast rule for 
DATE/TIME/TIMESTAMP to bounded CHAR/VARCHAR (#9670)
f87110c56a is described below

commit f87110c56a5e5e66bc78d4ec38ac232bc618087d
Author: jackylee <[email protected]>
AuthorDate: Fri Sep 11 14:13:14 2026 +0800

    [common][flink] Fix missing cast rule for DATE/TIME/TIMESTAMP to bounded 
CHAR/VARCHAR (#9670)
---
 .../paimon/casting/DateToStringCastRule.java       | 19 ++++-
 .../paimon/casting/TimeToStringCastRule.java       | 19 +++--
 .../paimon/casting/TimestampToStringCastRule.java  | 17 ++--
 .../DateTimeToCharacterStringCastRuleTest.java     | 97 ++++++++++++++++++++++
 .../apache/paimon/flink/SchemaChangeITCase.java    | 31 +++++++
 5 files changed, 167 insertions(+), 16 deletions(-)

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 5ec3391eaa..94eea00a71 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 @@ package org.apache.paimon.casting;
 
 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<Integer, BinaryString> {
 
     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<Integer, BinaryString> 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 d0f8ba7680..f61f15c74e 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.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#TIME_WITHOUT_TIME_ZONE} to {@link 
DataTypeFamily#CHARACTER_STRING} cast rule.
  */
@@ -38,15 +38,20 @@ class TimeToStringCastRule extends 
AbstractCastRule<Integer, BinaryString> {
         super(
                 CastRulePredicate.builder()
                         .input(DataTypeRoot.TIME_WITHOUT_TIME_ZONE)
-                        .target(STRING_TYPE)
+                        .target(DataTypeFamily.CHARACTER_STRING)
                         .build());
     }
 
     @Override
     public CastExecutor<Integer, BinaryString> 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 18016edc69..f433e559c3 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.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 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<Timestamp, 
BinaryString> {
 
@@ -39,7 +39,7 @@ class TimestampToStringCastRule extends 
AbstractCastRule<Timestamp, BinaryString
         super(
                 CastRulePredicate.builder()
                         .input(DataTypeFamily.TIMESTAMP)
-                        .target(STRING_TYPE)
+                        .target(DataTypeFamily.CHARACTER_STRING)
                         .build());
     }
 
@@ -50,7 +50,14 @@ class TimestampToStringCastRule extends 
AbstractCastRule<Timestamp, BinaryString
                 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 0000000000..f6ebb7d364
--- /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 <T> String cast(DataType inputType, DataType targetType, T 
value) {
+        CastExecutor<T, BinaryString> executor =
+                (CastExecutor<T, BinaryString>) 
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 43d817477f..9a0b6cb066 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 class SchemaChangeITCase extends CatalogITCaseBase {
                                 + "]");
     }
 
+    @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<Row> 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))");

Reply via email to