ORC: Fill initial defaults for absent scalar columns on read#252
ORC: Fill initial defaults for absent scalar columns on read#252cbb330 wants to merge 10 commits into
Conversation
Fill a field's initial-default as a per-file constant when its column is absent from an ORC data file, for the generic, Spark (2.4/3.1/3.2/3.3, row + vectorized) and Flink (1.14/1.15/1.16) readers. buildOrcProjection omits an absent top-level scalar field that declares an initial-default; each reader's record() step then injects the converted default into idToConstant so the existing partition-constant path fills it for every row (consuming no column vector). The omit is gated on applyDefaults, which OrcIterable sets to hasIds(fileSchema): id-less (name-mapped/migrated) files synthesize a null column instead and read NULL, so a default is never name-matched onto a file lacking embedded field ids. Scope is top-level scalar defaults; nested defaults are synthesized as null (read NULL) and remain a follow-up. Present-column binding is unchanged (ORC name-evolution + positional assembly), so existing reads do not regress. Co-authored-by: Cursor <cursoragent@cursor.com>
cbb330
left a comment
There was a problem hiding this comment.
Inline provenance annotations as requested, so the split is visible per-hunk.
Legend: 🟣 PRIOR ART (LI fork #76, same positional reader generation) · 🔵 BACKPORTED (decision/semantics carried from my apache/main ORC PR, re-implemented for the positional reader) · 🟢 NET NEW (did not exist on main or in #76).
Mental model: #76 = the how (omit + idToConstant inject on a positional reader); main = the what/why (top-level scalar scope, hasIds gate, NULL fallbacks); the shared idToConstantWithDefaults helper + all-engine wiring = the net-new glue.
cbb330
left a comment
There was a problem hiding this comment.
Stamping the remaining files so no file is left to interpretation. These are the per-version copies of the spots already annotated on the v3.3 / v1.16 representatives, plus the tests. Same legend: 🟣 PRIOR ART (#76) · 🔵 BACKPORTED (from apache/main) · 🟢 NET NEW.
Add an opt-in applyColumnDefaults flag on the ORC read builder (off by default). Only the row SparkOrcReader (Spark 3.1) and generic GenericOrcReader read paths enable it; OrcIterable then omits an absent top-level scalar default only when opted in (and the file has ids), so the reader can inject it via idToConstant. Every other ORC reader leaves the flag off, keeping the column synthesized as a null column (reads NULL) and therefore positionally aligned -- so it cannot misalign. Defers vectorized ORC and Flink defaults to a follow-up, and drops the Spark 2.4/3.2/3.3 wiring (those versions read NULL, unchanged). Co-authored-by: Cursor <cursoragent@cursor.com>
cbb330
left a comment
There was a problem hiding this comment.
Re-stamped provenance for the now-scoped file set (generic + Spark 3.1 row + the opt-in gate). Supersedes my earlier two reviews, which were partly anchored to files that have since been reverted.
Legend: 🟣 PRIOR ART (LI fork #76, positional reader) · 🔵 BACKPORTED (decision/semantics from my apache/main ORC PR) · 🟢 NET NEW (did not exist on main or #76 — includes the opt-in gate introduced in this revision).
…th-default Add typed-default cases (boolean/int/long/float/double/string/decimal) and a required-field-with-default case (end-to-end + projection-level), confirming the fill reuses the partition-constant channel correctly across scalar types and that an absent required field with a default is omitted+filled rather than rejected by the required-missing check. Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the top-level-only gate in buildOrcProjection so an absent scalar field declaring an initial-default is omitted (and filled via idToConstant) at any nesting level, not just the top level. The reader path already keys idToConstant by global field id and consults it at every struct level, so no reader change is needed. Add a safeguard so a non-root struct is never emitted empty (which ORC can reject): if every subfield of a nested struct is absent + defaulted, keep one synthesized null column (it reads NULL). The root struct may still be empty (select-only-default). Nested fill scenarios (struct/map/list) are ported from upstream BaseFormatModelTests. Co-authored-by: Cursor <cursoragent@cursor.com>
…ed structs) ORC accepts an empty nested read struct (as it already does for the empty root struct in select-only-default), so the "keep one synthesized null column" safeguard was unnecessary and introduced a surprising case where one subfield read NULL instead of its default. Remove it: an absent scalar default is now omitted and filled at any nesting level with no exceptions, including when every subfield of a nested struct is absent + defaulted (covered by a new end-to-end test). Also drops the now-unused topLevel parameter from buildOrcProjection. Co-authored-by: Cursor <cursoragent@cursor.com>
Add a row with a null nested struct to testNestedStructScalarDefault: a null parent struct stays null on read (the absent-only default is not fabricated), locking the positional reader's null-struct branch. Upstream's nested-default tests only cover this incidentally via random data, and via the id-binding reader rather than this branch. Co-authored-by: Cursor <cursoragent@cursor.com>
cbb330
left a comment
There was a problem hiding this comment.
Provenance re-stamp for the current state (now includes nested-scalar defaults + the opt-in gate). Supersedes earlier reviews.
Legend: 🟣 PRIOR ART (LI fork #76, positional reader) · 🔵 BACKPORTED (decision/semantics from my apache/main ORC PR) · 🟢 NET NEW (did not exist on main or #76 — chiefly the opt-in gate and the single shared helper).
Remove the reader-level enable flag so declared initial defaults are applied only when the field is absent and the file carries trustworthy IDs. Add regression coverage for id-less files, present values, and nested Spark rows. Co-authored-by: Cursor <cursoragent@cursor.com>
cbb330
left a comment
There was a problem hiding this comment.
Provenance annotations refreshed for the schema-driven implementation. Labels describe code origin, not correctness.
|
|
||
| private static boolean isOmittableDefault( | ||
| Types.NestedField field, boolean hasTrustedIds, Map<Integer, OrcField> mapping) { | ||
| return field.initialDefault() != null && !mapping.containsKey(field.fieldId()) && hasTrustedIds; |
There was a problem hiding this comment.
[ADAPTED BACKPORT + PRIOR ART] This is the complete activation invariant: the field declares initialDefault, the field ID is absent from the file mapping, and the file carries trustworthy embedded IDs. The omission behavior follows the newer implementation; transporting the value through idToConstant follows #76 for this positional reader generation.
| * @param convertConstant converts an internal default value to the engine's in-memory form | ||
| * @return {@code idToConstant} unchanged when no defaults apply, otherwise a new merged map | ||
| */ | ||
| public static Map<Integer, ?> idToConstantWithDefaults( |
There was a problem hiding this comment.
[NET NEW + PRIOR ART] Shared engine-neutral adapter around the #76 constant mechanism. It reads the value from initialDefault, injects only fields omitted after trusted absence detection, preserves existing partition constants, and leaves engine-specific conversion to the caller.
| if (hasTrustedIds) { | ||
| // Embedded IDs make field identity trustworthy, so absent fields that declare an initial | ||
| // default can be omitted from the projection and filled by a default-aware reader. | ||
| readOrcSchema = ORCSchemaUtil.buildOrcProjection(schema, fileSchema, hasTrustedIds); |
There was a problem hiding this comment.
[ADAPTED BACKPORT] File identity is derived from the existing embedded-ID check. Trusted files may omit absent defaulted fields; id-less/name-mapped files take the separate fail-closed path and synthesize NULL. There is no reader-level enable flag.
| return GenericOrcReaders.struct( | ||
| fields, | ||
| expected, | ||
| ORCSchemaUtil.idToConstantWithDefaults( |
There was a problem hiding this comment.
[NET NEW BREADTH + PRIOR ART] #76 covered Spark only. This applies the same positional constant mechanism to the generic reader, using its existing partition-constant converter.
| return SparkOrcValueReaders.struct( | ||
| fields, | ||
| expected, | ||
| ORCSchemaUtil.idToConstantWithDefaults( |
There was a problem hiding this comment.
[PRIOR ART: #76] Reuses the deployed positional invariant: an absent field becomes a per-file constant, and the unchanged StructReader consumes no ORC vector for that field. The source value is now initialDefault.
| } | ||
|
|
||
| protected static Object convertConstant(Type type, Object value) { | ||
| public static Object convertConstant(Type type, Object value) { |
| Sets.union(constantFieldIds, metadataFieldIds); | ||
| Schema schemaWithoutConstantAndMetadataFields = | ||
| TypeUtil.selectNot(expectedSchema, constantAndMetadataFieldIds); | ||
| // Follow-up: wire initial-default constants into VectorizedSparkOrcReaders. Tables whose |
There was a problem hiding this comment.
[DOCUMENTED LIMITATION] Vectorized ORC is intentionally deferred. The table-level overlay must route opted-in tables to the supported row reader until this path can materialize declared defaults.
| } | ||
|
|
||
| @Test | ||
| public void testTopLevelScalarDefaultOmittedWhenFileIdentityIsTrustworthy() { |
There was a problem hiding this comment.
[ADAPTED BACKPORT] Projection scenarios are adapted from the newer behavior for this positional reader: trusted absence omits, untrusted identity synthesizes NULL, and nested/required defaults follow the same invariant.
| } | ||
|
|
||
| @Test | ||
| public void testReadDoesNotApplyDefaultToIdLessFile() throws IOException { |
There was a problem hiding this comment.
[NET NEW SAFETY COVERAGE] End-to-end proof that an id-less file remains NULL even when the in-memory field declares a default. The adjacent test also proves present values and explicit NULLs are never replaced.
| } | ||
|
|
||
| @Test | ||
| public void testRowReadFillsDeclaredDefault() throws IOException { |
There was a problem hiding this comment.
[ADAPTED BACKPORT + NET NEW COVERAGE] Adapts the Spark row-fill scenario and proves the field declaration is the trigger: declared absent defaults fill, absent fields without a default remain NULL, and nested declared defaults fill through the same path.
Keep the pre-existing two-argument projection call for files with embedded IDs, introducing the explicit trust argument only for the id-less name-mapped branch. Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve the existing two-argument projection behavior for all callers and activate default omission only in the embedded-ID branch. This leaves the id-less name-mapped path unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove unrelated filter commentary so OrcIterable changes only at the trusted-ID projection call. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Fills a scalar column's
initial-defaultwhen the field is absent from an ORC data file whose embedded Iceberg field IDs are trustworthy. Support is intentionally limited to the generic reader and the Spark 3.1 row reader. Scalar fields may be top-level or nested inside structs, map values, or list elements.The table-level read-time overlay is the master gate. Tables that are not opted in never carry
initialDefaultin their in-memory schema and follow the unchanged read path. The default is never persisted to table metadata.This is an adapted backport for Iceberg 1.2's positional ORC reader. It uses the default semantics from the newer implementation and the positional
idToConstantpattern previously deployed in LinkedIn Iceberg #76. It does not backport the newer id-binding reader.Review annotation legend
Inline annotations classify each change by provenance:
These labels describe provenance, not correctness.
Activation invariant
A default is applied exactly when:
initialDefault;There is no ORC-level enable flag.
OrcIterablederives file trust directly fromORCSchemaUtil.hasIds(fileSchema). Id-less/name-mapped files fail closed and readNULL.How it works
buildOrcProjection(..., hasTrustedIds)omits a field satisfying the activation invariant.record()step reads the value frominitialDefaultand adds it to the per-fileidToConstantmap.idToConstantis the transport required by the 1.2 positional reader;initialDefaultremains the semantic source of the value.Supported routing
Opted-in tables must use one of the wired readers:
GenericOrcReaderSparkOrcReaderVectorized ORC, Flink, MR, and other Spark versions are not wired. The table-level overlay must not route opted-in tables to those paths until their reader support is added.
Deferred work and limitations
writeDefaultis not handled.Testing Done
TestBuildOrcProjection: trusted-identity omission, untrusted-identity null synthesis, required fields with defaults, nested omission, and empty nested structs.TestOrcDefaultValues: generic end-to-end fill, default-only projection, representative scalar types, required fields, nested struct/map/list scenarios, empty and null parent structs, id-less fail-closed behavior, and preservation of present values and explicitNULLs.TestSparkOrcReaderDefaults: declared default fill, absent field without a default remainsNULL, and nested declared default fill.