twalthr commented on code in PR #29092:
URL: https://github.com/apache/flink/pull/29092#discussion_r3934354114


##########
docs/content.zh/docs/sql/reference/data-types.md:
##########
@@ -1614,6 +1614,33 @@ CAST(m AS ARRAY<STRING>)   -- ['1', 'a'], a 
heterogeneous array still renders ea
 CAST(m AS ARRAY<VARIANT>)  -- [1, "a"] as variants, one level shredded
 ```
 
+A variant object casts to `ROW` or `STRUCTURED`, which likewise imposes a 
schema on it. The variant
+must be an object, otherwise the cast fails. Each field is itself a `VARIANT`, 
so it casts to its
+declared type by the same rules, recursively. Fields match by name rather than 
by position, since a
+JSON object is unordered, and name matching is case sensitive. A `ROW` 
declared without field names

Review Comment:
   ```suggestion
   declared type by the same rules, recursively. Fields match by name and name 
matching is case sensitive. A `ROW` declared without field names
   ```



##########
docs/content.zh/docs/sql/reference/data-types.md:
##########
@@ -1614,6 +1614,33 @@ CAST(m AS ARRAY<STRING>)   -- ['1', 'a'], a 
heterogeneous array still renders ea
 CAST(m AS ARRAY<VARIANT>)  -- [1, "a"] as variants, one level shredded
 ```
 
+A variant object casts to `ROW` or `STRUCTURED`, which likewise imposes a 
schema on it. The variant
+must be an object, otherwise the cast fails. Each field is itself a `VARIANT`, 
so it casts to its
+declared type by the same rules, recursively. Fields match by name rather than 
by position, since a
+JSON object is unordered, and name matching is case sensitive. A `ROW` 
declared without field names
+uses the default names `f0`, `f1`, and so on, which must then be present in 
the object.
+
+- A field absent from the object fails the cast, whether the target field is 
nullable or not.
+- A field present but set to a variant null maps to SQL `NULL` when the field 
is nullable and fails
+  the cast when it is `NOT NULL`.

Review Comment:
   ```suggestion
     the cast when the target is `NOT NULL`.
   ```



##########
flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java:
##########
@@ -307,6 +308,23 @@ private static Stream<Arguments> testData() {
                                         
YearMonthIntervalType.YearMonthResolution.MONTH)),
                         false,
                         false),
+                // A variant object casts to ROW or STRUCTURED when every 
field is castable; an
+                // empty
+                // row is vacuously castable and matching is by name
+                Arguments.of(new VariantType(), new RowType(List.of()), false, 
true),
+                Arguments.of(
+                        new VariantType(),
+                        new RowType(
+                                List.of(
+                                        new RowField("f0", new IntType()),
+                                        new RowField("f1", 
VarCharType.STRING_TYPE))),
+                        false,
+                        true),
+                Arguments.of(
+                        new VariantType(),
+                        new RowType(List.of(new RowField("f0", new 
TimeType()))),
+                        false,
+                        false),

Review Comment:
   Isn't this about to fail soon? Rather avoid to risk a red master.



##########
docs/content.zh/docs/sql/reference/data-types.md:
##########
@@ -1614,6 +1614,33 @@ CAST(m AS ARRAY<STRING>)   -- ['1', 'a'], a 
heterogeneous array still renders ea
 CAST(m AS ARRAY<VARIANT>)  -- [1, "a"] as variants, one level shredded
 ```
 
+A variant object casts to `ROW` or `STRUCTURED`, which likewise imposes a 
schema on it. The variant
+must be an object, otherwise the cast fails. Each field is itself a `VARIANT`, 
so it casts to its
+declared type by the same rules, recursively. Fields match by name rather than 
by position, since a
+JSON object is unordered, and name matching is case sensitive. A `ROW` 
declared without field names
+uses the default names `f0`, `f1`, and so on, which must then be present in 
the object.
+
+- A field absent from the object fails the cast, whether the target field is 
nullable or not.
+- A field present but set to a variant null maps to SQL `NULL` when the field 
is nullable and fails
+  the cast when it is `NOT NULL`.
+- Object fields the target does not name are dropped, so the row is a 
projection.
+- A `ROW` or `STRUCTURED` whose fields are `VARIANT` is the identity on those 
fields: it shreds one
+  level and keeps the rest semi-structured. A field set to a variant null 
stays a variant null
+  rather than becoming SQL `NULL`.
+
+If any field cast fails, the whole cast fails, and `TRY_CAST` returns `NULL` 
for the entire value
+rather than a partial result, exactly as for an array target.

Review Comment:
   ```suggestion
   rather than a partial result.
   ```



##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractVariantToConstructedCastRule.java:
##########
@@ -30,9 +30,9 @@
  * a shape check at each level, so the recursion bottoms out at the same 
scalar cast the primitive
  * and string rules perform and no new leaf semantics are introduced.
  *
- * <p>A constructed cast can always fail, on a shape mismatch, an unreadable 
leaf, or a missing
- * {@code NOT NULL} field, so {@code TRY_CAST} wraps the whole value and 
returns {@code NULL} for
- * any failure rather than a partial result.
+ * <p>A constructed cast can always fail, on a shape mismatch, an unreadable 
leaf, a missing field,
+ * or a JSON {@code null} in a {@code NOT NULL} position, so {@code TRY_CAST} 
wraps the whole value

Review Comment:
   ```suggestion
    * or a VARIANT {@code null} in a {@code NOT NULL} position, so {@code 
TRY_CAST} wraps the whole value
   ```



##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java:
##########
@@ -267,6 +333,16 @@ class CastRulesTest {
                                                     "d", 
ARRAY(STRING()).getLogicalType())))
                             .build());
 
+    // The variant a VARIANT row field round-trips to. A ROW cast serializes 
each VARIANT field via
+    // BinaryRowWriter.writeVariant, which stores the field's sliced value and 
the object metadata
+    // but
+    // drops the position, so the read-back variant equals neither the 
original field view (non-zero
+    // pos) nor a freshly built scalar (empty metadata). This rebuilds that 
exact form at pos 0.

Review Comment:
   remove this comment? You just create a deep copy right?



##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToRowCastRule.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.planner.functions.casting;
+
+import org.apache.flink.table.api.TableRuntimeException;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.writer.BinaryRowWriter;
+import org.apache.flink.table.planner.codegen.CodeGeneratorContext;
+import org.apache.flink.table.runtime.functions.VariantCastUtils;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
+import org.apache.flink.table.types.logical.utils.LogicalTypeChecks;
+import org.apache.flink.types.variant.Variant;
+
+import java.util.List;
+
+import static org.apache.flink.table.planner.codegen.CodeGenUtils.className;
+import static org.apache.flink.table.planner.codegen.CodeGenUtils.newName;
+import static 
org.apache.flink.table.planner.functions.casting.CastRuleUtils.binaryWriterWriteField;
+import static 
org.apache.flink.table.planner.functions.casting.CastRuleUtils.binaryWriterWriteNull;
+import static 
org.apache.flink.table.planner.functions.casting.CastRuleUtils.constructorCall;
+import static 
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
+import static 
org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall;
+import static 
org.apache.flink.table.planner.functions.casting.CastRuleUtils.strLiteral;
+
+/**
+ * {@link LogicalTypeRoot#VARIANT} to {@link LogicalTypeRoot#ROW} and {@link
+ * LogicalTypeRoot#STRUCTURED_TYPE} cast rule.
+ *
+ * <p>The variant must be an object, otherwise the cast fails. Fields match 
<b>by name</b> rather
+ * than by position, because a JSON object is unordered. Name matching is case 
sensitive. A target

Review Comment:
   ```suggestion
    * than by position, because a VARIANT object is unordered. Name matching is 
case sensitive. A target
   ```



##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRuleProviderTest.java:
##########
@@ -157,4 +157,17 @@ void testResolveVariantToArray() {
         // MULTISET has no variant counterpart
         assertThat(CastRuleProvider.exists(VARIANT, 
MULTISET(STRING()).getLogicalType())).isFalse();
     }
+
+    @Test
+    void testResolveVariantToRow() {
+        assertThat(CastRuleProvider.resolve(VARIANT, ROW(FIELD("f0", 
INT())).getLogicalType()))
+                .isSameAs(VariantToRowCastRule.INSTANCE);
+        // a structured target shares the ROW rule
+        assertThat(CastRuleProvider.resolve(VARIANT, STRUCTURED))
+                .isSameAs(VariantToRowCastRule.INSTANCE);
+
+        // a field with no variant counterpart makes the whole cast 
unresolvable
+        assertThat(CastRuleProvider.exists(VARIANT, ROW(FIELD("f0", 
TIME())).getLogicalType()))

Review Comment:
   same comment as above



##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java:
##########
@@ -1991,7 +2067,99 @@ Stream<CastTestSpecBuilder> testCases() {
                                             
VARIANT_INT_ARRAY_WITH_NULL.getElement(0),
                                             
VARIANT_INT_ARRAY_WITH_NULL.getElement(1),
                                             
VARIANT_INT_ARRAY_WITH_NULL.getElement(2)
-                                        })));
+                                        })),
+                CastTestSpecBuilder.testCastTo(ROW(FIELD("id", INT()), 
FIELD("name", STRING())))
+                        .fromCase(VARIANT(), null, null)
+                        .fromCase(
+                                VARIANT(), VARIANT_RECORD, 
GenericRowData.of(7, fromString("ada")))
+                        // an array or a scalar is not an object
+                        .fail(VARIANT(), VARIANT_INT_ARRAY, 
TableRuntimeException.class),

Review Comment:
   add expected error message substring



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