ORC: bind struct fields by Iceberg field id in generic and Spark 3.5 readers - #257
ORC: bind struct fields by Iceberg field id in generic and Spark 3.5 readers#257cbb330 wants to merge 1 commit into
Conversation
…readers Backport the upstream id-based StructReader binding (apache/iceberg 77e7dbb, PR #15776) to the openhouse-1.5.2 ORC reader, adapted for the 1.5.x type system. Companion to the 1.2.x backport in linkedin/iceberg PR #256. The ORC StructReader binds fields positionally, consuming column vectors in projection order. This replaces that with id-based binding: each expected field is matched to its ORC column by Iceberg field id, resilient to field reordering and schema evolution. Only GenericOrcReaders and Spark 3.5 SparkOrcValueReaders are converted; the positional constructor/overloads are retained and marked @deprecated because other engine readers still use them. Row-lineage, UNKNOWN, and VARIANT constructs from the upstream commit are omitted as they do not exist on 1.5.x. Behavior-preserving for well-formed reads: id-based binding produces identical results to positional binding on all current inputs. Co-authored-by: Cursor <cursoragent@cursor.com>
Divergence vs
|
Reviewer orientationPlease review #256 (the 1.2.x backport) first. The ORC core of this PR is byte-identical to it, so the per-line backport / adapted / net-new classification lives there as inline review annotations: #256 (review) For this PR, the only genuinely new surface to review is the Spark 3.5 wiring ( Everything else (the shared ORC core + the equivalence test) is identical to #256: cbb330/iceberg-apache@compare/orc-id-binding-shared-core...compare/orc-id-binding-shared-core-1.5.x |
Summary
Backports the upstream id-based ORC
StructReaderbinding (apache/iceberg77e7dbb24, PR #15776) to theopenhouse-1.5.2line. This is the 1.5.x companion to the 1.2.x backport in #256, targeting Spark 3.5.What changes: the ORC reader used to bind struct fields positionally — consuming ORC column vectors in projection order, assuming projected field order matches the file's physical column order. This PR switches the converted readers to id-based binding: each expected field is matched to its ORC column by its Iceberg field id, robust to field reordering and schema evolution.
Why it's safe:
buildOrcProjectionalready normalizes column order, so id-based binding produces identical results to positional binding on all current inputs. It is behavior-preserving — and the prerequisite for the follow-up work (see Next steps).Review this after #256 — it is nearly the same change
The ORC core of this PR is byte-identical to #256. Proof (renders "the branches are identical"): cbb330/iceberg-apache@compare/orc-id-binding-shared-core...compare/orc-id-binding-shared-core-1.5.x
So the only thing that genuinely needs fresh review here is the Spark 3.5 wiring. Everything else mirrors #256 (whose inline annotations classify each change).
How id-based binding works (quick primer)
An ORC file stores columns in a physical order. Iceberg has an expected schema, and every field has a permanent numeric field id. Positional binding = "the Nth expected field is the Nth file column" (trusts order); id-based binding = "match each expected field to the file column with the same field id" (order-independent). The real logic lives in the shared
OrcValueReaders.StructReaderbase class; the per-engine reader edits just thread the ORC file schema (TypeDescription) down to it and opt in.Provenance of each change
OrcValueReaders,GenericOrcReaders,GenericOrcReaderSparkOrcValueReaders/SparkOrcReader(v3.5)@Deprecatedpositional overload upstream deleted. NotablySparkOrcReader's one-liner is exactly what upstreammainalready ships for Spark 3.5 (see divergence comment).TestGenericOrcReaderIdBindingTestSparkOrcReadMetadataColumns/TestGenericReadProjectionAdaptations vs
apache/main1.5.x lacks the constructs the upstream commit references, so these are intentionally omitted (they would not compile on 1.5.x):
ROW_ID/LAST_UPDATED_SEQUENCE_NUMBERbranches,handleRowIdField/handleLastUpdatedSeqField,RowIdReader/LastUpdatedSeqReader.UNKNOWNtype clause in the metadata fallback.VARIANTreader/visitor.Everything else is aligned to upstream verbatim.
Scope
Only
GenericOrcReadersand Spark 3.5SparkOrcValueReadersare converted. The positional constructor/overloads are retained and@Deprecatedbecause the unconverted readers (Spark 3.3/3.4, Flink) still call them.Reviewing the divergence from upstream
77e7dbb24apache/main: apache/iceberg@main...cbb330:iceberg-apache:compare/orc-id-binding-1.5.x-vs-apache-main (hit Retry if it says "taking too long to generate")apache/main" comment on this PR.Testing Done
Validated on JDK 11:
./gradlew :iceberg-data:test --tests TestGenericOrcReaderIdBinding— pass (type promotion, reordered projection,_pos/_deleted,idToConstant)../gradlew -DsparkVersions=3.5 -DscalaVersion=2.12 :iceberg-spark:iceberg-spark-3.5_2.12:test(incl.TestSparkOrcReader,TestSparkOrcReadMetadataColumns) — pass../gradlew :iceberg-orc:revapi— pass; no.palantir/revapi.ymlchange needed.spotlessApplyclean;iceberg-orc/iceberg-data/iceberg-spark-3.5_2.12compile.Next steps (this is step 1 of 2)
This PR lands only the binding mechanism and does not change read output. The motivating follow-up is column default values: once fields are bound by id, a subsequent change can use those bound ids to resolve and apply an Iceberg field's initial-default value for fields present in the expected schema but absent from the ORC file (which today read as
null). Defaults are only correct once binding is keyed on field id rather than position, so this PR is the prerequisite. Defaults will land as a separate PR on top of this one.