raminqaf commented on code in PR #28758:
URL: https://github.com/apache/flink/pull/28758#discussion_r3690104002


##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java:
##########
@@ -138,75 +138,203 @@ Stream<TestSetSpec> getTestSetSpecs() {
     }
 
     private static List<TestSetSpec> variantCasts() {
-        // A variant is produced with PARSE_JSON so that the source column 
stays a STRING literal;
-        // there is no VARIANT literal to feed a source column directly. A 
JSON integer is stored in
-        // the smallest integer type that fits (42 -> TINYINT), and numeric 
casts are lenient, so it
-        // still widens to INT.
+        // A variant is produced with PARSE_JSON since there is no VARIANT 
literal. Numeric casts
+        // succeed only when the value is preserved exactly, otherwise CAST 
fails and TRY_CAST
+        // returns NULL.
         return List.of(
                 TestSetSpec.forExpression("Cast a VARIANT produced by 
PARSE_JSON to a primitive")
                         .onFieldsWithData("unused")
                         .andDataTypes(STRING())
+                        // An integer converts to any integer target while the 
value stays in
+                        // range, and to FLOAT or DOUBLE which are approximate 
by definition.
+                        .testResult(
+                                call("PARSE_JSON", "42").cast(TINYINT()),
+                                "CAST(PARSE_JSON('42') AS TINYINT)",
+                                (byte) 42,
+                                TINYINT().notNull())
+                        .testResult(
+                                call("PARSE_JSON", "42").cast(SMALLINT()),
+                                "CAST(PARSE_JSON('42') AS SMALLINT)",
+                                (short) 42,
+                                SMALLINT().notNull())
                         .testResult(
                                 call("PARSE_JSON", "42").cast(INT()),
                                 "CAST(PARSE_JSON('42') AS INT)",
                                 42,
                                 INT().notNull())
-                        // Integer overflow wraps around (Java narrowing), 
like a regular numeric
-                        // cast.
                         .testResult(
-                                call("PARSE_JSON", "40000").cast(SMALLINT()),
-                                "CAST(PARSE_JSON('40000') AS SMALLINT)",
-                                (short) -25536,
+                                call("PARSE_JSON", "42").cast(BIGINT()),
+                                "CAST(PARSE_JSON('42') AS BIGINT)",
+                                42L,
+                                BIGINT().notNull())
+                        .testResult(
+                                call("PARSE_JSON", "42").cast(FLOAT()),
+                                "CAST(PARSE_JSON('42') AS FLOAT)",
+                                42.0f,
+                                FLOAT().notNull())
+                        .testResult(
+                                call("PARSE_JSON", "42").cast(DOUBLE()),
+                                "CAST(PARSE_JSON('42') AS DOUBLE)",
+                                42.0d,
+                                DOUBLE().notNull())
+                        // An out-of-range value is rejected rather than 
wrapped.
+                        .testResult(
+                                call("PARSE_JSON", "1000").cast(SMALLINT()),
+                                "CAST(PARSE_JSON('1000') AS SMALLINT)",
+                                (short) 1000,
                                 SMALLINT().notNull())
+                        .testTableApiRuntimeError(
+                                call("PARSE_JSON", "1000").cast(TINYINT()), 
"overflowed")
+                        .testSqlRuntimeError("CAST(PARSE_JSON('1000') AS 
TINYINT)", "overflowed")
                         .testResult(
-                                call("PARSE_JSON", "128").cast(TINYINT()),
-                                "CAST(PARSE_JSON('128') AS TINYINT)",
-                                (byte) -128,
-                                TINYINT().notNull())
+                                call("PARSE_JSON", "1000").tryCast(TINYINT()),
+                                "TRY_CAST(PARSE_JSON('1000') AS TINYINT)",
+                                null,
+                                TINYINT())
+                        // A decimal is not read as an integer, since that 
would drop digits.
+                        .testTableApiRuntimeError(
+                                call("PARSE_JSON", "123.456").cast(INT()), 
"Cannot cast a VARIANT")
                         .testResult(
-                                call("PARSE_JSON", "2147483648").cast(INT()),
-                                "CAST(PARSE_JSON('2147483648') AS INT)",
-                                -2147483648,
-                                INT().notNull())
+                                call("PARSE_JSON", "123.456").tryCast(INT()),
+                                "TRY_CAST(PARSE_JSON('123.456') AS INT)",
+                                null,
+                                INT())
+                        // A DECIMAL target has to hold the value exactly.
                         .testResult(
-                                call("PARSE_JSON", 
"9223372036854775808").cast(BIGINT()),
-                                "CAST(PARSE_JSON('9223372036854775808') AS 
BIGINT)",
-                                -9223372036854775808L,
-                                BIGINT().notNull())
-                        // An out-of-range floating point value saturates when 
cast to an integer,
-                        // and a fractional value is truncated toward zero.
+                                call("PARSE_JSON", "123.456").cast(DECIMAL(6, 
3)),
+                                "CAST(PARSE_JSON('123.456') AS DECIMAL(6, 3))",
+                                new BigDecimal("123.456"),
+                                DECIMAL(6, 3).notNull())
+                        .testTableApiRuntimeError(
+                                call("PARSE_JSON", "123.456").cast(DECIMAL(6, 
2)), "lose precision")
                         .testResult(
-                                call("PARSE_JSON", "1e20").cast(INT()),
-                                "CAST(PARSE_JSON('1e20') AS INT)",
-                                2147483647,
-                                INT().notNull())
+                                call("PARSE_JSON", 
"123.456").tryCast(DECIMAL(6, 2)),
+                                "TRY_CAST(PARSE_JSON('123.456') AS DECIMAL(6, 
2))",
+                                null,
+                                DECIMAL(6, 2))
+                        .testTableApiRuntimeError(
+                                call("PARSE_JSON", "123.456").cast(DECIMAL(5, 
3)), "overflowed")
                         .testResult(
-                                call("PARSE_JSON", "3.9").cast(INT()),
-                                "CAST(PARSE_JSON('3.9') AS INT)",
-                                3,
-                                INT().notNull())
-                        // A value beyond the FLOAT range becomes infinity.
+                                call("PARSE_JSON", 
"123.456").tryCast(DECIMAL(5, 3)),
+                                "TRY_CAST(PARSE_JSON('123.456') AS DECIMAL(5, 
3))",
+                                null,
+                                DECIMAL(5, 3))
+                        // An integer is exact, so it reaches a DECIMAL that 
has room for it.
+                        .testResult(
+                                call("PARSE_JSON", "42").cast(DECIMAL(5, 2)),
+                                "CAST(PARSE_JSON('42') AS DECIMAL(5, 2))",
+                                new BigDecimal("42.00"),
+                                DECIMAL(5, 2).notNull())
+                        // A decimal reaches an approximate target, where 
losing digits is expected.
                         .testResult(
-                                call("PARSE_JSON", "1e40").cast(FLOAT()),
-                                "CAST(PARSE_JSON('1e40') AS FLOAT)",
-                                Float.POSITIVE_INFINITY,
+                                call("PARSE_JSON", "123.456").cast(FLOAT()),
+                                "CAST(PARSE_JSON('123.456') AS FLOAT)",
+                                123.456f,
                                 FLOAT().notNull())
-                        // DECIMAL overflow yields NULL instead of wrapping; 
TRY_CAST surfaces it.
                         .testResult(
-                                call("PARSE_JSON", 
"123.456").tryCast(DECIMAL(4, 2)),
-                                "TRY_CAST(PARSE_JSON('123.456') AS DECIMAL(4, 
2))",
+                                call("PARSE_JSON", "123.456").cast(DOUBLE()),
+                                "CAST(PARSE_JSON('123.456') AS DOUBLE)",
+                                123.456d,
+                                DOUBLE().notNull())
+                        // A magnitude the target cannot represent is still 
rejected.
+                        .testTableApiRuntimeError(
+                                call("PARSE_JSON", "1e40").cast(FLOAT()), 
"overflowed")
+                        .testResult(
+                                call("PARSE_JSON", "1e40").tryCast(FLOAT()),
+                                "TRY_CAST(PARSE_JSON('1e40') AS FLOAT)",
                                 null,
-                                DECIMAL(4, 2))
+                                FLOAT())
                         .testResult(
-                                call("PARSE_JSON", "42").cast(BIGINT()),
-                                "CAST(PARSE_JSON('42') AS BIGINT)",
-                                42L,
-                                BIGINT().notNull())
+                                call("PARSE_JSON", "1e20").cast(DOUBLE()),
+                                "CAST(PARSE_JSON('1e20') AS DOUBLE)",
+                                1e20,
+                                DOUBLE().notNull())
                         .testResult(
                                 call("PARSE_JSON", "true").cast(BOOLEAN()),
                                 "CAST(PARSE_JSON('true') AS BOOLEAN)",
                                 true,
                                 BOOLEAN().notNull())
+                        // CAST returns the raw scalar value (string unquoted)
+                        .testResult(
+                                call("PARSE_JSON", "\"foo\"").cast(STRING()),
+                                "CAST(PARSE_JSON('\"foo\"') AS STRING)",
+                                "foo",
+                                STRING().notNull())
+                        .testResult(
+                                call("PARSE_JSON", "123.456").cast(STRING()),
+                                "CAST(PARSE_JSON('123.456') AS STRING)",
+                                "123.456",
+                                STRING().notNull())
+                        .testResult(
+                                call("PARSE_JSON", "true").cast(STRING()),
+                                "CAST(PARSE_JSON('true') AS STRING)",
+                                "true",
+                                STRING().notNull())

Review Comment:
   Now the cast to string produces SQL string.



##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java:
##########
@@ -0,0 +1,257 @@
+/*
+ * 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.flink.table.runtime.functions;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.table.api.TableRuntimeException;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.types.variant.Variant;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Instant;
+import java.time.LocalDateTime;
+
+/**
+ * Runtime helpers for casting a {@code VARIANT} value to a SQL type.
+ *
+ * <p>A cast succeeds only when the target holds the stored value without 
altering it, so a value is
+ * never wrapped, rounded, truncated, or padded to make it fit. {@code FLOAT} 
and {@code DOUBLE} are
+ * the exception: they are approximate by definition, so they accept any 
numeric kind and reject
+ * only a magnitude they cannot represent at all.
+ */
+@Internal
+public final class VariantCastUtils {
+
+    private VariantCastUtils() {}
+
+    /**
+     * Reads an integer variant as a {@code long} and checks it against the 
target range. Only the
+     * integer kinds are accepted, so an approximate or decimal value is 
rejected rather than
+     * rounded.
+     */
+    public static long toIntegral(Variant variant, long min, long max, String 
targetType) {
+        switch (variant.getType()) {
+            case TINYINT:
+            case SMALLINT:
+            case INT:
+            case BIGINT:
+                break;
+            default:
+                throw unsupportedKind(variant, targetType);
+        }
+        final long value = ((Number) variant.get()).longValue();
+        if (value < min || value > max) {
+            throw overflow(value, targetType);
+        }
+        return value;
+    }
+
+    /**
+     * Reads any numeric variant as a {@code float}. Dropping decimal digits 
is expected of an
+     * approximate type, but a magnitude outside the {@code FLOAT} range is 
rejected.
+     */
+    public static float toFloat(Variant variant) {
+        final float value = numeric(variant, "FLOAT").floatValue();
+        if (!Float.isFinite(value)) {
+            throw overflow(variant.get(), "FLOAT");
+        }
+        return value;
+    }
+
+    /** Reads any numeric variant as a {@code double}. See {@link 
#toFloat(Variant)}. */
+    public static double toDouble(Variant variant) {
+        final double value = numeric(variant, "DOUBLE").doubleValue();
+        if (!Double.isFinite(value)) {
+            throw overflow(variant.get(), "DOUBLE");
+        }
+        return value;
+    }
+
+    /**
+     * Reads an integer or decimal variant as the target {@code DECIMAL}. The 
value has to fit the
+     * precision and scale without rounding, although trailing zeros may be 
appended to reach the
+     * scale.
+     */
+    public static DecimalData toDecimal(Variant variant, int precision, int 
scale) {
+        final String targetType = String.format("DECIMAL(%d, %d)", precision, 
scale);

Review Comment:
   Done



-- 
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]

Reply via email to