morningman opened a new pull request, #69194:
URL: https://github.com/apache/doris/pull/69194
### What problem does this PR solve?
Issue Number: None
Related PR: #66620, #67323
Problem Summary:
**In short.** Ports branch-4.1 #66620 (Iceberg Variant V2 writes) and #67323
(NULL defaults for VARIANT) to master. On master Doris reads Iceberg VARIANT
columns but rejects every write to a table that has one, and rejects VARIANT in
Iceberg DDL. With this PR Doris writes VARIANT values to format-version 3
Iceberg tables with Parquet data files: top-level and nested values, through
INSERT, INSERT OVERWRITE, UPDATE, MERGE and `rewrite_data_files`. It also
creates and alters top-level VARIANT columns. For every connector sink,
including Paimon, a write is rejected when its source implicitly casts a
VARIANT to a scalar. Before this PR such a write silently stored object and
array values as NULL.
Original author: @hubgeter
**Background.** Iceberg added the VARIANT type in format version 3 and
stores it in Parquet as a group of two binary leaves, `metadata` and `value`.
Master already reads such columns (#66413). For Paimon it already encodes
Variant V2 values to Arrow (#68320, #68883), and it checks the nested shapes a
connector Variant accepts in `CheckCast`. The Iceberg side of the write path
was never ported. Two other parts of #66620 were also missing:
- the plan validator that rejects lossy common-type coercion into a VARIANT
target;
- the binding of a merge sink's output by position.
**The problem, and what it cost.**
| Statement on master | Result |
|---|---|
| `INSERT` / `UPDATE` / `MERGE` (with data) / `rewrite_data_files` on an
Iceberg table with a VARIANT column, at any depth | `Iceberg VARIANT columns
are read-only and cannot be written` |
| `CREATE TABLE ... (payload VARIANT) ENGINE=ICEBERG`, `ADD COLUMN ...
VARIANT`, `MODIFY COLUMN ... VARIANT` | `Unsupported type for Iceberg: VARIANT`
|
| `INSERT INTO paimon_t SELECT IF(TRUE, payload, 1) ...`, `UNION ALL` of a
Variant and a number, the same in `CASE`, a recursive CTE or `LATERAL VIEW` |
The common type is DECIMAL, so an object or array `payload` is written as NULL
with no error |
| `UPDATE iceberg_t SET operation = ...` on a table whose data column is
named `operation` | Merge sink outputs were matched by name, so this column
skipped the cast to its target type |
Even if the FE had let these writes through, the BE could not complete them:
- The BE could not parse `variant` in the Iceberg schema JSON.
- It had no Arrow schema mapping for VARIANT.
- It would have published the Parquet statistics of the two Variant leaves
under the field id `-1`.
**How this PR fixes it.**
- **BE.**
- The Iceberg type system parses `variant`.
- The Iceberg Arrow schema maps VARIANT to the `arrow.parquet.variant`
extension over `struct<metadata: binary, value: binary>`. Master's Variant
encoder already fills it (#68883).
- The Iceberg Parquet writer counts a Variant's values and nulls on the
`metadata` leaf, under the Variant's own field id. It records no column size
and no bounds for the Variant.
- The format v2 Iceberg reader rejects a non-NULL VARIANT initial default.
- **FE, Iceberg connector.**
- A top-level VARIANT maps to Iceberg VARIANT. A VARIANT nested in STRUCT,
ARRAY or MAP DDL is rejected.
- CREATE TABLE and CTAS need an effective format version of 3 and Parquet
data files. The format version comes from the table properties and the
catalog's `table-default` / `table-override`, and so does the file format, with
the `write-format` alias winning as it does for an existing table.
- ADD COLUMN needs the same table settings. MODIFY of a VARIANT column
changes only its comment, nullability and position, so it also works on v3 ORC
tables. A change to or from VARIANT is rejected.
- Writes of data files whose target holds a VARIANT now need format
version 3, Parquet, and no `write.parquet.shred-variants=true`. This replaces
the read-only check. DELETE and delete-only MERGE are unchanged.
- Column metrics are also collected for VARIANT fields.
- VARIANT write defaults, initial defaults and DDL defaults must be NULL
(#67323).
- **FE, engine.** `VariantWritePlanValidator` traces the lineage of each
VARIANT target column through projections, set operations, recursive CTEs,
generators, CTE consumers and scalar subqueries. It rejects an implicit cast
that turns a Variant into a non-Variant value. It is wired once for every
connector:
- in `BindSink`, for INSERT, INSERT OVERWRITE, CTAS, rewrite, and
changelog row-level DML (Paimon UPDATE / MERGE);
- in `BindExpression`, for the row-level merge sink (Iceberg UPDATE /
MERGE).
The merge sink now casts its data columns by position.
- **Differences from branch-4.x.**
- The validator's message has no `Iceberg` / `Paimon` prefix, because one
generic check serves both.
- It does not inspect the condition of an IF or of a CASE branch, or
columns read only by such a condition. Branch-4.x rejects `IF(payload['kind'] =
'event', payload, NULL)`, because comparing a Variant path with a string
implicitly casts the path, even though the written value is unchanged.
- Paimon UPDATE and MERGE are validated too. Branch-4.x validates only
Paimon INSERT.
- A Parquet leaf without a field id now fails a check (`DORIS_CHECK_GE`)
instead of being skipped. Every field of the schema the file is written with
has one.
- **Not ported.** Legacy Variant handling and the `enable_variant_v2` switch
(master is V2 only), the VALUES and MERGE action special cases (master already
casts each value and each branch to the target type), and the extension and
Arrow pool helpers (master has equivalents).
**Results.** These are from a local single-BE cluster with the Iceberg REST
and Paimon docker environments. The tables were created by Spark 4.0 with
Iceberg 1.10.1, or by Doris DDL.
| Case | master | with this PR |
|---|---|---|
| `CREATE TABLE ... (payload VARIANT) ENGINE=ICEBERG` with `format-version`
3 and Parquet | `Unsupported type for Iceberg: VARIANT` | created. With format
version 2: `Iceberg VARIANT writes require table format-version 3, but found
2`. With ORC: `... require Parquet data files, but found ORC`. `STRUCT<...,
payload: VARIANT>`: rejected |
| INSERT of 13 values into that table (objects, arrays, primitives, JSON
null, SQL NULL, `"null"`, `""`, `{}`, `[]`) | read-only | written. Spark reads
each value back through `variant_get` and `to_json`, with SQL NULL and JSON
null kept apart. The data files report 13 values, 1 null, and no bounds for the
column |
| One writer inserts 100,000 rows into `STRUCT<label, payload VARIANT>` |
read-only | One data file with several row groups. Spark's `.files` shows
100,000 values and 10,000 nulls under the nested field id, with no column size
and no bounds |
| Variant nested in STRUCT, ARRAY and MAP, and four levels deep | read-only
| Written. Spark checks the container shapes, Doris reads the nested values
back |
| `UPDATE` / `MERGE` (with `WHEN NOT MATCHED INSERT`) on a Variant column
named `operation` | read-only | Written, and Spark reads the new values |
| MERGE that keeps a Variant in one branch and inserts the number `1` in
another | read-only | The object stays intact and the number is written as the
Variant `1` |
| A table with `write.parquet.shred-variants=true`, an ORC v3 table |
read-only | Rejected with no new snapshot. `MODIFY COLUMN v VARIANT COMMENT
...` on the ORC table still works |
| INSERT into Paimon of `UNION ALL` of a Variant and `1`, `IF(TRUE, payload,
1)`, the same in `CASE`, a recursive CTE, `LATERAL VIEW` | Objects and arrays
are written as NULL (master's own suite asserted this) | All 8 rejected:
`VARIANT write cannot safely convert input column 'payload': source expression
implicitly casts VARIANT to DECIMALV3(38, 9) before sink analysis`. The same 8
statements against Iceberg are rejected too |
| Paimon `UPDATE ... SET payload = IF(TRUE, payload, 1)`, a MERGE whose
source is such a UNION, and the same UPDATE / MERGE on Iceberg | — | Rejected |
| `IF(payload['kind'] = 'object', ..., payload)` in a Paimon UPDATE, and
`CASE WHEN payload['kind'] = ... THEN payload END` in an INSERT | — | Written
(branch-4.x rejects these) |
| Iceberg table that gained a VARIANT column through Spark: the column
omitted, `DEFAULT`, `DEFAULT(payload)` in UPDATE, MERGE and `INSERT ... SELECT`
| read-only | Every such row reads NULL. `ADD COLUMN ... VARIANT DEFAULT NULL`
works. A non-NULL default and `NOT NULL DEFAULT NULL` are rejected, and the
schema is left unchanged |
- **BE UT.** 10 cases are ported from branch-4.x, and
`IcebergBuildsItsOwnSchemaAndMetadata` goes back to its Variant form:
- `ArrowSchemaUtilTest.test_variant_field` / `test_nested_variant_fields`
- `SchemaParserTest.parse_variant`
- `VariantTypeTest.to_string`
- `PrimitiveTypesTest.from_primitive_string`
- `VParquetWriterTest.WritesIcebergVariantAndCollectsLogicalMetrics` /
`WritesNestedIcebergVariant`
- `IcebergV2ReaderTest.AnnotateVariantInitialDefaultMustBeNull` /
`StructInitialDefaultKeepsVariantChildNull`
All 474 cases of the related suites pass under ASAN. Two mutations each
turn only their own two cases red: dropping the Variant metrics rule, and
accepting a non-NULL initial default. clang-tidy reports nothing on the changed
files.
- **FE UT.**
- `VariantWritePlanValidatorTest`: the 13 branch-4.x cases, plus two new
ones. One shows that IF and CASE conditions are not checked. The other shows
that columns read only by a condition are not traced.
- `BindExpressionTest`: the positional merge-output cast, and the
validator on a Variant column named `operation`.
- `ExternalRowLevelMergePlanBuilderTest`: each MERGE branch is cast to the
Variant target before the branch is selected.
- Connector tests:
- `IcebergWritePlanProviderTest`: the write capability matrix; a full
`planWrite` of a v3 Parquet, ORC and shredding table; VARIANT write defaults.
- `IcebergSchemaBuilderTest`: top-level and nested VARIANT DDL, the
effective file format precedence, and DDL defaults.
- `CatalogBackedIcebergCatalogOpsColumnEvolutionTest`: ADD COLUMN on v2,
ORC and Parquet tables, metadata-only MODIFY, and rejected type changes.
- `IcebergConnectorMetadataDdlTest`: CREATE TABLE with VARIANT.
- `IcebergSchemaUtilsTest`: the initial default.
Mutations turn their own cases red:
- in the validator, inspecting conditions, and tracing condition columns;
- in the connector, collecting metrics only for primitive fields, dropping
the CREATE check, dropping the ADD COLUMN check, and dropping the VARIANT
MODIFY branch.
All 1580 fe-connector-iceberg cases pass, and so do 131 cases in 12
fe-core classes, including the existing sink-binding, changelog and merge
builders.
- **Regression.**
- `test_iceberg_variant_read` carries the branch-4.x write block, without
the legacy-Variant cases and the `enable_variant_v2` switch, which master does
not have. Its `.out` gains rows 3–5 of `variant_nested_projection` and
`variant_nested_expressions`, the same rows as branch-4.2.
- The new `test_iceberg_variant_defaults` produces the same `.out` as
branch-4.2.
- In `test_paimon_write_variant_errors`, the 8 common-type cases are back
to `test { exception }`, and Paimon UPDATE / MERGE cases are added.
- All 72 suites under `external_table_p0/paimon/write` and
`external_table_p0/iceberg/write`, and the Iceberg row-lineage DML suites, pass
on the same cluster. The one exception is `test_paimon_write_thread_lifecycle`,
which needs Linux `/proc`.
### Release note
Doris can write VARIANT columns of Iceberg format-version 3 tables with
Parquet data files: INSERT, INSERT OVERWRITE, UPDATE, MERGE and
`rewrite_data_files`. It can also create or add top-level VARIANT columns.
A write into a VARIANT column of an Iceberg or Paimon table now fails when
its source implicitly casts a VARIANT to another type. Examples are `IF(cond,
payload, 1)` and a UNION of a Variant and a number. Before, such a write stored
object and array values as NULL. Write an explicit CAST to keep such a
conversion.
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [x] Regression test
- `test_iceberg_variant_read`, `test_iceberg_variant_defaults`,
`test_paimon_write_variant_errors`, as above
- [x] Unit Test
- BE, fe-core and fe-connector-iceberg, as above
- [x] Manual test (add detailed scripts or steps below)
- The Paimon and Iceberg write regression directories and the
Iceberg row-lineage DML suites on a local cluster, as above
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [ ] No.
- [x] Yes. <!-- Explain the behavior change -->
- Iceberg VARIANT columns become writable on v3 Parquet tables, and
top-level VARIANT DDL works.
- Writes into connector VARIANT columns reject implicit
Variant-to-scalar casts in their source.
- Does this need documentation?
- [ ] No.
- [x] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
- Iceberg VARIANT writes and DDL.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
--
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]