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))");