Ramin Gharib created FLINK-40910:
------------------------------------
Summary: 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
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)