Skip to content

ORC: fill initial defaults for missing id-bound fields - #263

Open
cbb330 wants to merge 5 commits into
chbush/oh120-orc-id-bindingfrom
chbush/oh120-orc-default-fill
Open

ORC: fill initial defaults for missing id-bound fields#263
cbb330 wants to merge 5 commits into
chbush/oh120-orc-id-bindingfrom
chbush/oh120-orc-default-fill

Conversation

@cbb330

@cbb330 cbb330 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fills a field's initial-default when it is absent from an id-bearing ORC file, using the same fallback order as the Parquet reader.

  • buildOrcProjection omits an absent defaulted field only when the file has embedded IDs and the configured reader explicitly declares supportsInitialDefaults().
  • The id-binding StructReader from ORC: bind struct fields by Iceberg field id in generic and Spark 3.1 readers #265 then materializes the declared default as a per-file constant, consuming no column vector.
  • A present column always wins, including explicit null. ID-less/name-mapped files keep legacy null synthesis.
  • Readers that do not opt in retain their previous projection shape and behavior.
  • Spark 3.1 scans projecting a default at any nesting level are routed away from vectorized ORC reads; default materialization remains iterative-only.

Why reader opt-in is required

OrcIterable is shared by Generic, Spark, and Flink row and vectorized readers. Embedded field IDs establish that an absent column was never written, but do not establish that the selected reader can fill it. The new ORC.ReadBuilder.supportsInitialDefaults() capability keeps omission local to readers that implement the missing-field contract.

Generic row reads opt in here. Spark 3.1 row reads opt in in #264. This branch also provides the recursive Spark 3.1 scan-routing guard required before the row-reader implementation is enabled. Other engines and vectorized readers do not opt in.

Provenance

This is net new rather than a backport. Apache Iceberg 1.2 has the schema model for defaults but does not implement ORC default reads. The resolution logic mirrors Parquet's field-reader fallback: physical reader → declared default → null/failure.

Supported scope

Generic ORC row reader; scalar defaults at any struct nesting level, including structs inside list elements and map values.

Vectorized ORC default materialization remains deferred. Predicates on omitted defaulted fields use a conservative ORC SearchArgument so they are evaluated after iterative default materialization.

Stack

  1. ORC: bind struct fields by Iceberg field id in generic and Spark 3.1 readers #265 — ID-based StructReader backport
  2. This PR — Generic ORC initial-default fill, reader capability gate, and Spark 3.1 iterative-routing guard
  3. Spark 3.1: fill ORC initial defaults on the row reader #264 — Spark 3.1 ORC initial-default fill

Testing Done

  • Projection tests cover opted-in embedded-ID omission and non-opted null synthesis
  • Generic ORC end-to-end coverage for scalar, required, nested, list, and map defaults
  • Present values and explicit nulls override defaults
  • ID-less files retain null behavior
  • Top-level and nested projected defaults disable Spark 3.1 vectorized ORC reads
  • Schemas without projected defaults remain vectorization eligible
  • Full iceberg-orc and iceberg-data test suites pass
  • Spotless passes

Fill a field's initial-default when it is absent from an id-bearing ORC
file, mirroring how the Parquet reader resolves defaults.

buildOrcProjection omits an absent field that declares a default when the
file's embedded ids are trustworthy, so the id-binding StructReader finds
no column for it and materializes the declared default as a per-file
constant. This reuses the reader's existing constant path, so a defaulted
field consumes no column vector.

A present column always wins, including one holding an explicit null.
Id-less and name-mapped files keep legacy null synthesis, so a default is
never name-matched onto a legacy or migrated file. Engines that have not
opted into id binding are unaffected: default filling is enabled only by
passing a constant converter, and the positional path keeps its strict
missing-reader failure.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cbb330
cbb330 force-pushed the chbush/oh120-orc-default-fill branch from 1115244 to 0e7b860 Compare July 28, 2026 07:13
final Map<Integer, OrcField> icebergToOrc = icebergToOrcMapping("root", originalOrcSchema);
return buildOrcProjection(Integer.MIN_VALUE, schema.asStruct(), true, icebergToOrc);
return buildOrcProjection(
Integer.MIN_VALUE, schema.asStruct(), true, hasTrustedIds, icebergToOrc);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are trusted IDs?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is a bit vague so I changed it to a two-valued enum, which names where the field ids in the schema came from.

  • EMBEDDED: the ids were read from iceberg.id column attributes persisted in the ORC file. When it is present it is great, a field with no id was genuinely never written, so its declared default can be filled safely.
  • NAME_MAPPED: the file carried no ids of its own (e.g. was renamed from kafka ETL's Hive-migration), so an earlier step derived them at read time by matching column names. When file carries no ids, "absent" is not proof since e.g. an iceberg column can rename and the name mapping no longer covers.but it's data is physically in the file. Filling a default there would fabricate values over real data, so we don't fill the default on this path. We keep the original behavior here, in order to focus only on the important case: iceberg created files.

this.readers[pos] = fileReader;
} else if (convertConstant != null && field.initialDefault() != null) {
this.isConstantOrMetadataField[pos] = true;
this.readers[pos] =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actual logic

@mkuchenbecker mkuchenbecker left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall makes sense, datatypes tested, where there is a default and the default is not specified, then populate.

An intergration test in li-openhouse for trino and spark will be useful to capture actual behaviour.

Replace buildOrcProjection's boolean hasTrustedIds parameter with a
FieldIdSource enum naming where a schema's Iceberg field ids came from:
EMBEDDED for ids read from iceberg.id column attributes written into the
file, NAME_MAPPED for ids derived at read time by matching column names.

"Trusted" asserted a judgment without saying who trusts the ids or why,
which a reviewer had to resolve by reading OrcIterable. The provenance is
the checkable fact, and it is what decides whether an absent field may be
read as "never written" and so have its declared default filled. Naming
it at the call site also makes the two branches in OrcIterable read as
the matched pair they are, rather than one passing a bare true and the
other omitting the argument entirely.

Each constant documents its own hazard: under NAME_MAPPED a field can
look absent merely because its name did not match, for example after a
rename the name mapping no longer covers, while its data is physically
present in the file, so filling a default would fabricate values over
real data.

No behavior change. Also record why isOmittableDefault need not check
that the field is scalar: Types.NestedField#castDefault already rejects a
default on any nested type at construction time.
@github-actions github-actions Bot added the DATA label Aug 2, 2026
@cbb330

cbb330 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

I added an explicit supportsInitialDefaults() flag to the ORC row-reader path.

Previously, the shared ORC projection omitted missing defaulted columns whenever a file had embedded field IDs. That inadvertently affected every Spark/Flink row and vectorized reader, even though only Generic and Spark 3.1 iterative readers know how to fill the omitted column today. This could break non-supported engines by giving their positional readers a shorter column list.

Now a field is omitted only when all four conditions hold:

  supportsInitialDefaults
      && field.initialDefault() != null
      && !mapping.containsKey(field.fieldId())
      && fieldIdSource == FieldIdSource.EMBEDDED

Generic and Spark 3.1 iterative readers explicitly opt in. Other engines retain the previous projection and behavior. Spark 3.1 also avoids vectorized ORC reads when the projected schema contains
initial defaults; vectorized support can be added separately.

This keeps default filling isolated to readers that implement it, while preserving backward compatibility everywhere else.

@cbb330
cbb330 force-pushed the chbush/oh120-orc-default-fill branch from 3fd044f to 5175788 Compare August 3, 2026 01:58
@cbb330
cbb330 force-pushed the chbush/oh120-orc-default-fill branch from 5175788 to 540604c Compare August 3, 2026 02:03
@github-actions github-actions Bot removed the DOCS label Aug 3, 2026
@cbb330
cbb330 force-pushed the chbush/oh120-orc-default-fill branch 2 times, most recently from 60115f6 to d5893e8 Compare August 3, 2026 07:10
@cbb330
cbb330 force-pushed the chbush/oh120-orc-default-fill branch from d5893e8 to ca18b89 Compare August 3, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants