morningman opened a new pull request, #69350:
URL: https://github.com/apache/doris/pull/69350

   ### What problem does this PR solve?
   
   Issue Number: None
   
   Related PR: #69194
   
   Problem Summary:
   
   **In short.** The Variant write check added in #69194 also rejects writes 
that lose nothing: a scalar field of a composite target that is computed from a 
Variant, and a Variant that is only compared or only picks an element. This PR 
traces only the parts of a value that reach a Variant leaf of the target 
column, and skips the arguments that only compare or select. A lossy cast on a 
value that is written into a Variant leaf is still rejected. This is the 
follow-up promised in two review threads of #69194.
   
   **Background.** Common-type analysis runs before the sink is bound. When a 
Variant meets a number in a UNION, IF or CASE, the Variant side is implicitly 
cast to DECIMAL, and an object or array Variant becomes SQL NULL; the sink then 
encodes that NULL or scalar back into the Variant column. 
`VariantWritePlanValidator` (#69194), which every connector's INSERT / 
OVERWRITE / CTAS / row-level DML goes through, follows the lineage of each 
target column that contains a Variant and rejects an implicit 
Variant-to-non-Variant cast on the way. It already skips the positions that 
only choose rows or values: IF and CASE conditions, window partition and order 
keys, the ORDER BY of an aggregate function, and the later inputs of EXCEPT and 
INTERSECT.
   
   **The problem, and what it cost.** Two shapes of correct writes fail with 
"VARIANT write cannot safely convert input column ...":
   
   1. As soon as a target column contains one Variant leaf, the whole column is 
traced, so an implicit cast in a field that is written as a BOOLEAN is treated 
as a Variant loss.
   2. Every argument of every expression is treated as a written value, 
including arguments that are only compared or only select an element.
   
   | Write (`v` is a VARIANT) | Target | Before | After |
   |---|---|---|---|
   | `named_struct('matched', COALESCE(v['n'], 0) > 1, 'payload', v)` | 
`STRUCT<matched BOOLEAN, payload VARIANT>` | rejected | accepted |
   | the same struct built from subquery columns, from a struct column's 
fields, through `struct()`, a cast between structs, an IF or a UNION | same | 
rejected | accepted |
   | `map(IF(TRUE, v['k'], 1), v)` | `MAP<STRING, VARIANT>` | rejected | 
accepted |
   | `v['n'] > 0`, `v['n'] IN (1, 2)`, `v['s'] LIKE 'a%'`, `NOT v['flag']` | 
`VARIANT` | rejected | accepted |
   | `NULLIF(1, IF(TRUE, v, 1))`, `element_at(ARRAY(v), IF(TRUE, v['n'], 1))` | 
`VARIANT` | rejected | accepted |
   | `named_struct('matched', TRUE, 'payload', IF(TRUE, v, 1))` | 
`STRUCT<matched BOOLEAN, payload VARIANT>` | rejected | rejected |
   | `IF(TRUE, v, 1) > 0`, `NULLIF(IF(TRUE, v, 1), 1)`, 
`element_at(ARRAY(IF(TRUE, v, 1)), 1)` | `VARIANT` | rejected | rejected |
   
   The only workaround was an explicit CAST around the sibling field or the 
comparison.
   
   **How this PR fixes it.**
   
   1. The validator traces a demand per ExprId instead of a set of ExprIds. A 
demand says which parts of a value reach a Variant leaf: none, all of it, or 
the parts of each field of a STRUCT, of the element of an ARRAY, or of the key 
and the value of a MAP. The demand of a target column comes from its type and 
is aligned position by position to the source type, as the sink's struct cast 
is; when the shapes differ, all of the value is traced, as before.
   2. A closed set of structural expressions passes the demand on: 
`named_struct` / `struct` (field i to its argument), `array` and `map` 
constructors, `element_at` (a struct field chosen by its constant name or 
1-based position, an array element, a map value), casts (part by part between 
values of the same shape), the results of IF / CASE / NULLIF / NVL 
(`ExpressionUtils.getCaseWhenLikeBranchResults`, the model 
`JoinExtractOrFromCaseWhen` already relies on), aliases, window functions, set 
operation inputs by ordinal, recursive CTEs, CTE outputs and scalar subqueries. 
Any other expression traces all of every argument, as before, so an expression 
the validator does not model can only reject more. A cast is lossy only if it 
loses a Variant leaf in a demanded part.
   3. Three kinds of argument are not traced, because they never become the 
written value: the second argument of NULLIF, the index or key of `element_at`, 
and the implicit cast that a comparison, IN, LIKE / REGEXP, AND / OR or NOT 
applies directly to an operand. Only that one cast is skipped; the operand 
under it is still traced, so `IF(TRUE, v, 1) > 0` is still rejected. Functions 
whose arguments may be returned are not listed one by one: `ELT(v['n'], 'a', 
'b')`, `SUBSTRING('abc', v['n'])` and `MAX_BY(1, IF(TRUE, v, 1))` are still 
rejected and need an explicit CAST.
   4. A CTE output is traced once per demand rather than once: it is traced 
again only when a consumer requests a part not traced yet, at most once per 
nested field of its type, so the work stays proportional to the plan (the two 
existing exponential-plan tests still run in milliseconds). The recursive CTE 
fixpoint works the same way per output ordinal.
   5. The error message now ends with "use an explicit CAST if the conversion 
is intended".
   
   branch-4.1 and branch-4.2 scan the whole write expression for such casts, 
including IF and CASE conditions, so they reject more than master; this PR does 
not change them.
   
   **Results.**
   
   - `VariantWritePlanValidatorTest`: six new tests (struct targets in seven 
shapes, crossed struct fields by name and by position, ARRAY<STRUCT> and MAP 
targets, values of another shape, compared and selecting arguments, a CTE 
output traced again for more of it). Each accepted statement was checked to be 
rejected before this change, and each rejected one to stay rejected. 19 
mutations of the new rules (dropping each structural rule, swapping map keys 
and values, an off-by-one struct position, tracing the skipped arguments, 
reusing a CTE trace for any part, dropping the demand on a shape mismatch, and 
others) each fail at least one test. The 21 existing tests pass unchanged.
   - `test_paimon_write_variant_errors`: a Paimon table with `STRUCT<matched: 
BOOLEAN, payload: VARIANT>` and a top-level VARIANT column. The scalar field 
computed with `COALESCE(payload['n'], 0) > 1`, in place and from a subquery, 
and the comparisons `payload['n'] > 1` and `payload['kind'] LIKE 'object%'` are 
written and read back (Spark reads the same values); a loss in 
`flagged.payload` or under a comparison operand is still rejected. Before this 
change the first of these writes fails with the error above.
   - The other Variant write suites, all six `test_paimon_write_variant*` and 
`test_iceberg_variant_read`, pass locally with this change.
   
   ### Classes and call graph
   
   ```
   BindSink / BindExpression
     -> VariantWritePlanValidator.validateNoLossyCoercion(targets, plan[, 
sourceOrdinals][, cteContext])
          Demand.of(target type).alignTo(source type)
          -> traceOutputLineage(plan, ExprId -> Demand)
               set operation / recursive CTE / generate / CTE consumer: 
per-ordinal demands
               other plans -> validateExpression(named expression, demand)
                    -> valueNodes(expression, demand)          structural 
dispatch, skipped arguments
                    -> validateLossyCast(nodes)                
losesVariantLeaf(source, target, demand)
                    -> slots and scalar subqueries -> demands on the child plans
   ```
   
   ### Release note
   
   Writes into an external table column that contains a VARIANT no longer fail 
when a Variant is implicitly cast only to compute a non-Variant field of the 
target (for example the BOOLEAN field of a STRUCT), only to be compared 
(comparison, IN, LIKE, REGEXP, AND, OR, NOT), or only to choose a value (the 
second argument of NULLIF, the index or key of element_at). The error for a 
lossy cast now suggests an explicit CAST.
   
   ### Check List (For Author)
   
   - Test
       - [x] Regression test
       - [x] Unit Test
       - [ ] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
   
   - Behavior changed:
       - [ ] No.
       - [x] Yes. Writes described in the release note are accepted instead of 
rejected; the error message gains a hint.
   
   - Does this need documentation?
       - [x] No.
       - [ ] Yes.
   
   ### Check List (For Reviewer who merge this PR)
   
   - [ ] Confirm the release note
   - [ ] Confirm test cases
   - [ ] Confirm document
   - [ ] Add branch pick label
   


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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to