snuyanzin commented on code in PR #28066:
URL: https://github.com/apache/flink/pull/28066#discussion_r3303431934
##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CoalesceFunctionITCase.java:
##########
@@ -19,21 +19,302 @@
package org.apache.flink.table.planner.functions;
import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
+import org.apache.flink.table.functions.ScalarFunction;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.types.Row;
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Period;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.stream.Stream;
+import static org.apache.flink.table.api.DataTypes.ARRAY;
import static org.apache.flink.table.api.DataTypes.BIGINT;
+import static org.apache.flink.table.api.DataTypes.BINARY;
+import static org.apache.flink.table.api.DataTypes.BOOLEAN;
+import static org.apache.flink.table.api.DataTypes.BYTES;
+import static org.apache.flink.table.api.DataTypes.CHAR;
+import static org.apache.flink.table.api.DataTypes.DATE;
+import static org.apache.flink.table.api.DataTypes.DECIMAL;
+import static org.apache.flink.table.api.DataTypes.DOUBLE;
+import static org.apache.flink.table.api.DataTypes.FIELD;
+import static org.apache.flink.table.api.DataTypes.FLOAT;
import static org.apache.flink.table.api.DataTypes.INT;
+import static org.apache.flink.table.api.DataTypes.INTERVAL;
+import static org.apache.flink.table.api.DataTypes.MAP;
+import static org.apache.flink.table.api.DataTypes.MONTH;
+import static org.apache.flink.table.api.DataTypes.ROW;
+import static org.apache.flink.table.api.DataTypes.SECOND;
+import static org.apache.flink.table.api.DataTypes.SMALLINT;
+import static org.apache.flink.table.api.DataTypes.STRING;
+import static org.apache.flink.table.api.DataTypes.TIME;
+import static org.apache.flink.table.api.DataTypes.TIMESTAMP;
+import static org.apache.flink.table.api.DataTypes.TIMESTAMP_LTZ;
+import static org.apache.flink.table.api.DataTypes.TINYINT;
+import static org.apache.flink.table.api.DataTypes.VARBINARY;
+import static org.apache.flink.table.api.DataTypes.VARCHAR;
import static org.apache.flink.table.api.Expressions.$;
+import static org.apache.flink.table.api.Expressions.call;
import static org.apache.flink.table.api.Expressions.coalesce;
+import static org.apache.flink.util.CollectionUtil.entry;
+import static org.apache.flink.util.CollectionUtil.map;
/** Test {@link BuiltInFunctionDefinitions#COALESCE} and its return type. */
class CoalesceFunctionITCase extends BuiltInFunctionTestBase {
@Override
Stream<TestSetSpec> getTestSetSpecs() {
- return Stream.of(
- TestSetSpec.forFunction(BuiltInFunctionDefinitions.COALESCE)
+ final List<TestSetSpec> specs = new ArrayList<>();
+ specs.addAll(allTypesBasic());
+ specs.addAll(typePromotion());
+ specs.addAll(lazyEvaluation());
+ specs.addAll(constants());
+ return specs.stream();
+ }
+
+ private static List<TestSetSpec> allTypesBasic() {
+ return List.of(
+ basicSpec("BOOLEAN", BOOLEAN(), true, false),
+ basicSpec("TINYINT", TINYINT(), (byte) 5, (byte) 10),
+ basicSpec("SMALLINT", SMALLINT(), (short) 100, (short) 200),
+ basicSpec("INT", INT(), 1, 2),
+ basicSpec("BIGINT", BIGINT(), 100L, 200L),
+ basicSpec("FLOAT", FLOAT(), 1.5f, 2.5f),
+ basicSpec("DOUBLE", DOUBLE(), 1.5d, 2.5d),
+ basicSpec(
+ "DECIMAL",
+ DECIMAL(5, 2),
+ new BigDecimal("123.45"),
+ new BigDecimal("234.56")),
+ basicSpec("CHAR", CHAR(5), "hello", "world"),
+ basicSpec("VARCHAR", VARCHAR(10), "hello", "world"),
+ basicSpec("STRING", STRING(), "hello", "world"),
+ basicSpec("BINARY", BINARY(2), new byte[] {0, 1}, new byte[]
{2, 3}),
+ basicSpec("VARBINARY", VARBINARY(5), new byte[] {0, 1, 2}, new
byte[] {3, 4}),
+ basicSpec("BYTES", BYTES(), new byte[] {0, 1, 2}, new byte[]
{3, 4, 5}),
+ basicSpec("DATE", DATE(), LocalDate.of(2026, 1, 1),
LocalDate.of(2026, 12, 31)),
+ basicSpec("TIME", TIME(), LocalTime.of(12, 34, 56),
LocalTime.of(23, 59, 59)),
+ basicSpec(
+ "TIMESTAMP",
+ TIMESTAMP(),
+ LocalDateTime.of(2026, 1, 1, 12, 0, 0),
+ LocalDateTime.of(2026, 12, 31, 23, 59, 59)),
+ basicSpec(
+ "TIMESTAMP_LTZ",
+ TIMESTAMP_LTZ(),
+ Instant.parse("2026-01-01T12:00:00Z"),
+ Instant.parse("2026-12-31T23:59:59Z")),
+ basicSpec(
+ "INTERVAL_MONTH",
+ INTERVAL(MONTH()),
+ Period.ofMonths(18),
+ Period.ofMonths(27)),
+ basicSpec(
+ "INTERVAL_SECOND",
+ INTERVAL(SECOND(3)),
+ Duration.ofMillis(12345),
+ Duration.ofMillis(67890)),
+ basicSpec("ARRAY", ARRAY(INT()), new Integer[] {1, 2, 3}, new
Integer[] {4, 5, 6}),
+ basicSpec(
+ "MAP",
+ MAP(STRING(), INT()),
+ map(entry("a", 1), entry("b", 2)),
+ map(entry("c", 3), entry("d", 4))),
+ rowSpec());
+ }
+
+ private static TestSetSpec basicSpec(String name, DataType type, Object
value1, Object value2) {
+ return TestSetSpec.forFunction(BuiltInFunctionDefinitions.COALESCE,
name)
+ .onFieldsWithData(null, value1, value2)
+ .andDataTypes(type.nullable(), type.notNull(), type.notNull())
+ .testResult(coalesce($("f0"), $("f1")), "COALESCE(f0, f1)",
value1, type.notNull())
+ .testResult(coalesce($("f1"), $("f2")), "COALESCE(f1, f2)",
value1, type.notNull())
+ .testResult(
+ coalesce($("f0"), $("f0"), $("f2")),
+ "COALESCE(f0, f0, f2)",
+ value2,
+ type.notNull());
+ }
+
+ private static TestSetSpec rowSpec() {
+ DataType rowType = ROW(FIELD("a", INT()), FIELD("b", STRING()));
+ return TestSetSpec.forFunction(BuiltInFunctionDefinitions.COALESCE,
"ROW")
+ .onFieldsWithData(null, Row.of(1, "hello"), Row.of(2, "world"))
+ .andDataTypes(rowType.nullable(), rowType.notNull(),
rowType.notNull())
+ .testResult(
+ coalesce($("f0"), $("f1")),
+ "COALESCE(f0, f1)",
+ Row.of(1, "hello"),
+ rowType.notNull())
+ .testResult(
+ coalesce($("f1"), $("f2")),
+ "COALESCE(f1, f2)",
+ Row.of(1, "hello"),
+ rowType.notNull());
+ }
+
+ /**
+ * Verifies the LEAST_RESTRICTIVE return-type inference combined with
LEAST_NULLABLE: mixing
+ * compatible operand types yields the widest type, and nullability is
dropped if any operand is
+ * NOT NULL.
+ */
+ private static List<TestSetSpec> typePromotion() {
+ return List.of(
+ TestSetSpec.forFunction(BuiltInFunctionDefinitions.COALESCE,
"INT and BIGINT")
+ .onFieldsWithData(null, 1, 2L)
+ .andDataTypes(INT().nullable(), INT().nullable(),
BIGINT().notNull())
+ .testResult(
+ coalesce($("f0"), $("f1"), $("f2")),
+ "COALESCE(f0, f1, f2)",
+ 1L,
+ BIGINT().notNull()),
+ TestSetSpec.forFunction(BuiltInFunctionDefinitions.COALESCE,
"TINYINT and INT")
+ .onFieldsWithData(null, (byte) 7, 42)
+ .andDataTypes(TINYINT().nullable(),
TINYINT().nullable(), INT().notNull())
+ .testResult(
+ coalesce($("f0"), $("f1"), $("f2")),
+ "COALESCE(f0, f1, f2)",
+ 7,
+ INT().notNull()),
+
+ // TIMESTAMP precision widening: TIMESTAMP(0) < TIMESTAMP(3) →
declared
+ // TIMESTAMP(3). Calcite stores TIMESTAMP as a Long millis
value, so widening
+ // precision does not change the underlying value.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE,
+ "TIMESTAMP(0) and TIMESTAMP(3)")
+ .onFieldsWithData(
+ null,
+ LocalDateTime.parse("2026-01-01T00:00:00"),
+ LocalDateTime.parse("2026-01-01T00:00:00.123"))
+ .andDataTypes(
+ TIMESTAMP(0).nullable(),
+ TIMESTAMP(0).nullable(),
+ TIMESTAMP(3).notNull())
+ .testResult(
+ coalesce($("f0"), $("f1"), $("f2")),
+ "COALESCE(f0, f1, f2)",
+ LocalDateTime.parse("2026-01-01T00:00:00"),
+ TIMESTAMP(3).notNull()),
+
+ // DECIMAL precision widening, same scale: DECIMAL(5,2) <
DECIMAL(10,2)
+ // → declared DECIMAL(10, 2).
+ // Same scale → underlying BigDecimal representation pre/post
simplify is
+ // identical.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE,
+ "DECIMAL(5,2) and DECIMAL(10,2) (same scale)")
+ .onFieldsWithData(
+ null, new BigDecimal("1.23"), new
BigDecimal("9876543.21"))
+ .andDataTypes(
+ DECIMAL(5, 2).nullable(),
+ DECIMAL(5, 2).nullable(),
+ DECIMAL(10, 2).notNull())
+ .testResult(
+ coalesce($("f0"), $("f1"), $("f2")),
+ "COALESCE(f0, f1, f2)",
+ new BigDecimal("1.23"),
+ DECIMAL(10, 2).notNull()),
+
+ // DECIMAL precision and scale widening: DECIMAL(5,2) <
DECIMAL(10,4)
+ // → declared DECIMAL(10, 4) (Calcite widening rule:
+ // d = max(p1-s1, p2-s2) = max(3, 6) = 6, scale = max(2,4)
= 4, precision = 10).
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE,
+ "DECIMAL(5,2) and DECIMAL(10,4) (different
scale)")
+ .onFieldsWithData(null, new BigDecimal("1.23"), new
BigDecimal("4.5678"))
+ .andDataTypes(
+ DECIMAL(5, 2).nullable(),
+ DECIMAL(5, 2).nullable(),
+ DECIMAL(10, 4).notNull())
+ .testResult(
+ coalesce($("f0"), $("f1"), $("f2")),
+ "COALESCE(f0, f1, f2)",
+ new BigDecimal("1.2300"),
+ DECIMAL(10, 4).notNull()),
+
+ // INTERVAL YEAR TO MONTH — same shape on every operand.
Stored as a single int
+ // (months); precision is metadata, the underlying value is
unchanged.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE, "INTERVAL
MONTH precision")
+ .onFieldsWithData(null, Period.ofMonths(2),
Period.ofMonths(5))
+ .andDataTypes(
+ INTERVAL(MONTH()).nullable(),
+ INTERVAL(MONTH()).nullable(),
+ INTERVAL(MONTH()).notNull())
+ .testResult(
+ coalesce($("f0"), $("f1"), $("f2")),
+ "COALESCE(f0, f1, f2)",
+ Period.ofMonths(2),
+ INTERVAL(MONTH()).notNull()),
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE,
+ "INTERVAL SECOND same precision")
+ .onFieldsWithData(null, Duration.ofSeconds(10),
Duration.ofMillis(15000))
+ .andDataTypes(
+ INTERVAL(SECOND(3)).nullable(),
+ INTERVAL(SECOND(3)).nullable(),
+ INTERVAL(SECOND(3)).notNull())
+ .testResult(
+ coalesce($("f0"), $("f1"), $("f2")),
+ "COALESCE(f0, f1, f2)",
+ Duration.ofSeconds(10),
+ INTERVAL(SECOND(3)).notNull()));
+ }
+
+ /** Lazy evaluation: a non-null operand short-circuits the rest. */
+ private static List<TestSetSpec> lazyEvaluation() {
+ return List.of(
+ // First arg non-null at runtime: subsequent ThrowingFunction
must NOT be called.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE,
+ "lazy: first operand non-null skips remainder")
+ .onFieldsWithData(1, 100)
+ .andDataTypes(INT().nullable(), INT().notNull())
+ .withFunction(ThrowingFunction.class)
+ .testResult(
+ coalesce($("f0"), call("ThrowingFunction",
$("f1"))),
+ "COALESCE(f0, ThrowingFunction(f1))",
+ 1,
+ INT().notNull()),
+ // Middle arg non-null at runtime: ThrowingFunction in the
third slot must NOT be
+ // called.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE,
+ "lazy: middle operand non-null skips
remainder")
+ .onFieldsWithData(null, 5, 100)
+ .andDataTypes(INT().nullable(), INT().nullable(),
INT().notNull())
+ .withFunction(ThrowingFunction.class)
+ .testResult(
+ coalesce($("f0"), $("f1"),
call("ThrowingFunction", $("f2"))),
+ "COALESCE(f0, f1, ThrowingFunction(f2))",
+ 5,
+ INT().notNull()),
+ // Negative control: the previous operand IS null at runtime,
so ThrowingFunction
+ // must be reached and must throw.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.COALESCE,
+ "negative control: throwing UDF fires when
reached")
+ .onFieldsWithData(null, 100)
+ .andDataTypes(INT().nullable(), INT().notNull())
+ .withFunction(ThrowingFunction.class)
+ .testTableApiRuntimeError(
Review Comment:
sure, added
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]