ORC/Spark: forward-port default-value reads via idToConstant (LI #76) - #267
ORC/Spark: forward-port default-value reads via idToConstant (LI #76)#267cbb330 wants to merge 7 commits into
Conversation
cbb330
left a comment
There was a problem hiding this comment.
Inline annotations for the reviewer guide: forward-ported as-is vs forward-ported with changes. See the updated tables in the PR description for links back to each comment.
| * from an ORC file. Disabled by default so existing readers retain null-synthesizing projection | ||
| * behavior. Spark ORC readers opt in (forward-port of LI #76). | ||
| */ | ||
| public ReadBuilder supportsInitialDefaults() { |
There was a problem hiding this comment.
Raymond always omitted defaulted columns from the ORC projection, for every reader. This fork shares ORCSchemaUtil with Generic, so omit is opt-in via supportsInitialDefaults() — only the Spark row reader calls it.
There was a problem hiding this comment.
"Raymond's omit was always-on." what does this mean?
There was a problem hiding this comment.
sorry, the inline comments were confusing before, fixed them.
basically this PR is a "forward port" of Raymond Zhang's previous previous implementation which was utilized for hive. Bc the previous impl landed in production, we can feel safer to implement his work as is.
but his work as is has a couple gotchas. One is that it activated for every type of reader, not just spark's row reader. Because vectorized reads introduce another level of complexity, and I have found 0 tables in catalog with the vectorized read table property present*, I'm leaving it out of scope for the initial phase we are implementing (which is behind a feature gate).
- there are two ways to activate vectorized reads: one is table property. other is spark session property. so maybe a few people are using spark session property, but I code searched "spark.sql.iceberg.vectorization.enabled" and didn't find any user repos that have enabled.
There was a problem hiding this comment.
One is that it activated for every type of reader, not just spark's row reader.
Yeah, the blast radius for this is all tables vs just those that are enabled. The code seems safe though one sxisting codepath in the orc dir(famous last words)
| private static <T> T visitRecord( | ||
| /** | ||
| * Visits a struct. Overridden by Spark to inject {@code initial-default} values into {@code | ||
| * idToConstant} for fields omitted from the ORC projection (forward-port of LI #76 / {@code |
There was a problem hiding this comment.
do we need this forward port or can we consider this a reasonable limitation? Those datasets must rewrite themselves but are unblocked to migrate essentially.
There was a problem hiding this comment.
I think this might be mixing two things.
This isn’t a compatibility path for files without field IDs. Those we don’t handle at all ( so name-mapped / incomplete IDs stay on the null-synthesizing projection as-is current status quo)
The production case we are targetting is the opposite: the file has Iceberg IDs, so we can omit an absent defaulted column from the ORC projection. This visitRecord looks at the Iceberg schema, notices the hole, and puts the default in idToConstant. Without that, fill-on-read does not work.
3302e57 to
8c8d99b
Compare
| * Visits a struct. Overridden by Spark to inject {@code initial-default} values into {@code | ||
| * idToConstant} for fields omitted from the ORC projection. | ||
| */ | ||
| protected T visitRecord( |
There was a problem hiding this comment.
As-is. Protected visitRecord so Spark can inject defaults for omitted fields.
| * Checks whether {@code list1} contains all integers from {@code list2} in the same relative | ||
| * order. {@code list1} may contain extra integers that {@code list2} does not. | ||
| */ | ||
| private static boolean containsInOrder(List<Integer> list1, List<Integer> list2) { |
There was a problem hiding this comment.
As-is. Iceberg/ORC field-id alignment check.
|
|
||
| private static class ReadBuilder extends OrcSchemaWithTypeVisitor<OrcValueReader<?>> { | ||
| private final Map<Integer, ?> idToConstant; | ||
| private static class ReadBuilder extends OrcSchemaWithTypeVisitorSpark<OrcValueReader<?>> { |
There was a problem hiding this comment.
As-is. Row reader extends the Spark visitor to pick up inject.
|
|
||
| private static class ReadBuilder extends OrcSchemaWithTypeVisitor<Converter> { | ||
| private final Map<Integer, ?> idToConstant; | ||
| private static class ReadBuilder extends OrcSchemaWithTypeVisitorSpark<Converter> { |
There was a problem hiding this comment.
As-is wiring. Vectorized reader still extends the Spark visitor. Defaulted scans are forced onto the row reader, so this path does not fill.
There was a problem hiding this comment.
Dropped. Vectorized fill leftovers from the #76 forward-port; this path is unchanged now. Defaulted scans still go through the row reader via SparkBatchScan.
| import org.apache.spark.unsafe.types.UTF8String; | ||
|
|
||
| /** Constant array column vector. Unused until nested-typed defaults are legal in the API. */ | ||
| public class ConstantArrayColumnVector extends ConstantColumnVector { |
There was a problem hiding this comment.
As-is (unused). Nested constant vector kept; castDefault still rejects nested-typed defaults.
There was a problem hiding this comment.
Dropped. Vectorized fill leftovers from the #76 forward-port; this path is unchanged now. Defaulted scans still go through the row reader via SparkBatchScan.
| orcType = TypeDescription.createStruct(); | ||
| for (Types.NestedField nestedField : type.asStructType().fields()) { | ||
| // Omit so the reader fills via idToConstant instead of a synthetic null column. | ||
| if (supportsInitialDefaults |
There was a problem hiding this comment.
Changed. Same omit, but initialDefault() != null and gated by supportsInitialDefaults because this util is shared with Generic.
| if (MetadataColumns.nonMetadataColumn(iField.name()) | ||
| && !idToConstant.containsKey(iField.fieldId()) | ||
| && iField.initialDefault() != null) { | ||
| idToConstant.put( |
There was a problem hiding this comment.
Changed. Same id-mismatch inject, but initialDefault() and skip nulls.
| List<String> names, | ||
| List<OrcValueReader<?>> fields) { | ||
| return SparkOrcValueReaders.struct(record, fields, expected, idToConstant); | ||
| return SparkOrcValueReaders.struct(record, fields, expected, getIdToConstant()); |
There was a problem hiding this comment.
Changed. Id-bound struct(record, ...) from #265, not positional.
| .split(task.start(), task.length()) | ||
| .createReaderFunc( | ||
| readOrcSchema -> new SparkOrcReader(readSchema, readOrcSchema, idToConstant)) | ||
| .supportsInitialDefaults() |
There was a problem hiding this comment.
Changed. Spark row reader opts into omit+fill; Generic does not.
| boolean hasNoInitialDefaults = hasNoInitialDefaults(expectedSchema); | ||
|
|
||
| // Defaulted projections stay on the row reader; batched ORC does not fill initial-defaults. | ||
| boolean batchReadOrc = hasNoDeleteFiles && allOrcFileScanTasks && hasNoInitialDefaults; |
There was a problem hiding this comment.
Changed. Defaulted projections stay on the row reader; batched ORC does not fill.
|
@mkuchenbecker carrying over your notes from #264 (and the Generic fill site on #263). Those PRs are closed; this is where the answers live. Vectorized: no fill PR. Defaulted projections are forced onto the row reader ( Spark 3.1 vs 3.5: this stack is
Silent failure / is null an error: if we cannot prove the column was never written, we do not fill — we keep null (status quo). We do not throw and we do not invent a default. That is intentional. The case you wanted to avoid (silently overlaying a default on real data) is handled by refusing to omit, not by failing the read. All data types: current coverage is int/string and a nested scalar ( Generic |
Restore the production Spark ORC initial-default path from f200623: omit absent defaulted fields from the ORC projection and inject them into idToConstant so row and vectorized readers fill via existing constant readers. Adapted to the upstream initial-default API and gated behind supportsInitialDefaults so shared ORCSchemaUtil does not break Generic.
Vectorized ORC default-fill is out of scope. SparkBatchScan disables columnar ORC when the projection includes an initial-default, and the batched reader no longer opts into omission.
Keep the why (opt-in omit, row-only fill, nested defaults not API-legal). Drop stack TODOs, commit SHAs, and forward-port narration.
526e934 to
95fbc27
Compare
| /** | ||
| * Signals that the configured reader can fill {@code initial-default} values for fields omitted | ||
| * from an ORC file. Disabled by default so existing readers retain null-synthesizing projection | ||
| * behavior. |
There was a problem hiding this comment.
what is a "null-synthesizing projection" ?
There was a problem hiding this comment.
Projection here means the columns this read asked for (the read schema), not the columns physically in the file.
If country is in the projection but missing from an old ORC file, the reader still has to return a country field. Today when it does that, it fills null. That's a null-synthesizing projection. It honors the requested schema by inventing nulls for columns the file doesn't have.
supportsInitialDefaults() is the new path. The scan still carries country, but we don't read that stream from this ORC file and we don't fill null. We omit it from this file's ORC columns and put initial-default in the scan's country slot.
The same existing behavior stays by default and returns nulls. Only the Spark row reader opts in, and only fields with initial-default take the new path.
mkuchenbecker
left a comment
There was a problem hiding this comment.
why is the vectorized dir needed?
Vectorized fill is out of scope; SparkBatchScan already keeps defaulted projections on the row reader. Restore the batched visitor and constant vectors to the pre-#76 path.
@mkuchenbecker good catch, i removed them. they are dead code in this PR since I disabled vectorized. these came forward from the original work from RZ from the forward port. but we aren't using it any time soon |
Summary
Forward-port of the Spark ORC default-value read path from LI #76 onto
openhouse-1.2.0, adapted toinitial-defaultand stacked on id-bound ORC structs (#265).Absent defaulted fields are omitted from the per-file ORC projection and injected into
idToConstant. The Spark row reader fills them. Omit is opt-in (supportsInitialDefaults) becauseORCSchemaUtilis shared with Generic.Vectorized ORC default-fill is out of scope.
SparkBatchScandisables columnar ORC when the projection includes aninitial-default. The vectorized-dir leftovers from #76 were dropped from this PR.Mechanism (same as #76):
idToConstantConstantReadermaterializes themInline notes mark each site as as-is or changed vs #76 (review).
Stack #270: this PR → #268 → #269.
Reviewer guide
As-is
visitRecordOrcSchemaWithTypeVisitorcontainsInOrderSparkOrcReaderconvertConstantSTRUCT/LIST/MAPconvertConstantChanged
initialDefault(); omit gatedORCSchemaUtilis shared with GenericinitialDefault() != nullstruct(record, ...)castDefaultrejects non-null nested defaults (apache/iceberg#14611). Nested scalar defaults (e.g.loc.country) are coveredFollow-ups
castDefault+ nested-typed defaultsReplaces #263/#264 for the Spark fill path.
Testing Done
testOrcScalarDefaultValues— passtestOrcNestedScalarDefaultValues— passTestSparkBatchScanInitialDefaults— pass