[ 
https://issues.apache.org/jira/browse/FLINK-40910?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

ASF GitHub Bot updated FLINK-40910:
-----------------------------------
    Labels: pull-request-available  (was: )

> Reject VARIANT where SQL needs equality. Byte comparison gives wrong results 
> in GROUP BY, DISTINCT, joins and =
> ---------------------------------------------------------------------------------------------------------------
>
>                 Key: FLINK-40910
>                 URL: https://issues.apache.org/jira/browse/FLINK-40910
>             Project: Flink
>          Issue Type: Bug
>          Components: Table SQL / Planner, Table SQL / Runtime
>            Reporter: Ramin Gharib
>            Assignee: Ramin Gharib
>            Priority: Major
>              Labels: pull-request-available
>
> h2. Problem
> Flink compares VARIANT values by their binary encoding. One logical value has 
> many encodings. Queries that group, deduplicate, join or compare on VARIANT 
> therefore return wrong results, without any error.
> {code:sql}
> -- t(s STRING) has three rows:
> --   row 1: \{"a":1,"b":2}
> --   row 2: \{"b":2,"a":1}
> --   row 3: \{"a":1,"c":3}
> SELECT a, COUNT(*) FROM (SELECT PARSE_JSON(s)['a'] AS a FROM t) GROUP BY a;
> {code}
> {noformat}
> Query                                                        Expected         
>    Actual
> GROUP BY PARSE_JSON(s)['a']                                  one group, count 
> 3  three groups, count 1 each
> COUNT(DISTINCT PARSE_JSON(s)) over row 1 and row 2           1                
>    2
> WHERE PARSE_JSON(s)['a'] = PARSE_JSON('1')                   3 rows           
>    0 rows
> l JOIN r ON PARSE_JSON(l.s)['id'] = PARSE_JSON(r.s)['id']    1 row            
>    0 rows
>   with l = \{"id":7,"x":1} and r = \{"y":2,"id":7}
> {noformat}
> The actual results come from VariantSemanticTest on master 5afe863af62. 
> Example output for the GROUP BY:
> {noformat}
> Expecting actual:
>   ["+I[1, 1]", "+I[1, 1]", "+I[1, 1]"]
> to contain exactly in any order:
>   ["+I[1, 1]", "-U[1, 1]", "+U[1, 2]", "-U[1, 2]", "+U[1, 3]"]
> {noformat}
> h2. Cause
> A VARIANT is two byte arrays. The metadata is a dictionary of key names. The 
> value refers to keys by their position in that dictionary. Flink treats two 
> VARIANTs as equal only if both arrays are byte-equal:
> * Grouping, DISTINCT and join keys are BinaryRowData. 
> \{{AbstractBinaryWriter#writeVariant}} writes the dictionary and the value 
> bytes into the key, and BinaryRowData hashes and compares those bytes.
> * \{{=}} calls \{{BinaryVariant#equals}}, which compares the value, the 
> dictionary and the position.
> One value has many valid encodings:
> * The dictionary lists keys in the order they first appear. Row 1 and row 2 
> get different dictionaries.
> * A field taken by field access keeps the full dictionary of its parent 
> object. The number 1 from row 1 differs from the number 1 from row 3 and from 
> PARSE_JSON('1').
> * Numbers keep their storage width. PARSE_JSON('1') is a TINYINT, CAST(CAST(1 
> AS BIGINT) AS VARIANT) is a BIGINT, and PARSE_JSON('1.0') is a DECIMAL. All 
> three print as 1.
> The variant encoding spec allows all of these. For the same reason, the 
> Iceberg spec defines no hash for variant.
> h2. Proposal
> Reject VARIANT where SQL needs equality, like ORDER BY on VARIANT is rejected 
> today (\{{TypeCheckUtils#isComparable}}). Users cast to a concrete type 
> first. That already works:
> {code:sql}
> SELECT a, COUNT(*) FROM (SELECT CAST(PARSE_JSON(s)['a'] AS INT) AS a FROM t) 
> GROUP BY a;
> -- one group, count 3
> {code}
> Reject VARIANT, also when nested in ROW, ARRAY or MAP, in:
> * GROUP BY keys, including GROUPING SETS, ROLLUP, CUBE and window aggregations
> * DISTINCT aggregates and SELECT DISTINCT
> * equi-join keys
> * =, <>, IN, IS DISTINCT FROM and IS NOT DISTINCT FROM between VARIANT 
> operands
> * UNION, INTERSECT and EXCEPT without ALL
> * PARTITION BY of OVER windows, Top-N, deduplication, process table functions 
> and MATCH_RECOGNIZE
> Still allowed: VARIANT as an aggregate argument such as COUNT(v), 
> FIRST_VALUE(v) and LAST_VALUE(v), IS NULL, IS NOT NULL, field access and 
> casts.
> The error should name the clause and suggest a cast, for example:
> {noformat}
> VARIANT cannot be used as a grouping key because VARIANT values have no 
> equality. Cast the value to a concrete type first, for example CAST(v['a'] AS 
> INT).
> {noformat}
> Open question: VARIANT as a MAP key. MAP lookups use the same byte equality.
> h2. Other systems
> ||System||Behavior||
> |Spark / Databricks|VARIANT is not comparable since SPARK-47569. GROUP BY 
> fails with GROUP_EXPRESSION_TYPE_IS_NOT_ORDERABLE. DISTINCT and set 
> operations fail. PARTITION BY fails with PARTITION_BY_VARIANT. Databricks 
> docs: "The VARIANT data type cannot be used for comparisons, grouping, 
> ordering, and set operations."|
> |BigQuery|JSON is neither groupable nor comparable, and cannot be a partition 
> or cluster key. Users extract with JSON_VALUE first.|
> |Apache Iceberg|The spec defines no hash for variant, because equivalent 
> values have several representations.|
> Sources:
> * https://docs.databricks.com/aws/en/semi-structured/variant
> * 
> https://github.com/apache/spark/blob/master/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ExprUtils.scala
>  (checkValidGroupingExprs)
> * 
> https://github.com/apache/spark/blob/master/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckAnalysis.scala
>  (variantColumnInSetOperation, PARTITION_BY_VARIANT)
> * https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
> * https://github.com/apache/iceberg/blob/main/format/spec.md
> h2. Compatibility
> * Flink 2.1 to 2.3 accept these queries. After the change they fail at 
> validation. This needs a release note.
> * Existing tests assert the old behavior and must change: VariantSemanticTest 
> VARIANT_AS_AGG_KEY, and the COUNT(DISTINCT v) in BUILTIN_AGG and 
> BUILTIN_AGG_WITH_RETRACTION.
> * Open question: a job restored from a compiled plan skips SQL validation. 
> Should it fail too, or keep running?
> * Supporting equality later stays compatible, because it only accepts more 
> queries.
> h2. Alternatives considered
> * Write a field value with only the keys it uses. This fixes scalar fields, 
> but not objects or number widths.
> * A canonical encoding: sorted dictionary, no unused keys, smallest number 
> width. This changes the bytes of existing state, and data from other writers 
> still differs because the spec allows several encodings.
> * Semantic equality and hashing on decoded values. Correct, but a larger 
> design: how 1 compares to 1.0, NaN, and the timestamp kinds. It can follow 
> later.
> h2. Tests
> Four VariantSemanticTest programs reproduce the bug and fail on master as 
> shown above: variant-field-as-agg-key, variant-object-as-distinct-key, 
> variant-field-equals-variant and variant-field-as-join-key. With the fix they 
> become failing-SQL checks for the new error. A fifth program, 
> variant-field-cast-as-agg-key, checks the CAST workaround and passes today.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to