-
Notifications
You must be signed in to change notification settings - Fork 45
ORC: fill initial defaults for missing id-bound fields #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0e7b860
6dfb91d
65e1996
ca18b89
37c3c78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,32 @@ public enum LongType { | |
| LONG | ||
| } | ||
|
|
||
| /** | ||
| * Where the Iceberg field IDs in an ORC schema came from. | ||
| * | ||
| * <p>This is the provenance of the IDs, not a statement about the file's contents. It decides | ||
| * whether the absence of an ID may be read as "this column was never written", which in turn | ||
| * decides whether a declared {@code initial-default} may be filled on read. | ||
| */ | ||
| enum FieldIdSource { | ||
| /** | ||
| * The IDs were read from {@code iceberg.id} column attributes written into the file by {@link | ||
| * ORCSchemaUtil#convert(Schema)}. A field with no ID was genuinely never written, so a declared | ||
| * default may be filled. | ||
| */ | ||
| EMBEDDED, | ||
|
|
||
| /** | ||
| * The IDs were derived at read time by {@link ORCSchemaUtil#applyNameMapping} matching column | ||
| * names, because the file carried none of its own -- typically a legacy or Hive-migrated file. | ||
| * 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. Filling a | ||
| * default here would fabricate values over real data, so absent fields are synthesized as null | ||
| * columns instead. | ||
| */ | ||
| NAME_MAPPED | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we adding name mapping? The renamed files from Kyoto? This worries me building in the implicit mapping. If we didn't add the legacy mapping, what happens? Those files are unreadable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a restriction to use this to keep it simple we could require a re-write of the table so it has headers. |
||
| } | ||
|
|
||
| private static class OrcField { | ||
| private final String name; | ||
| private final TypeDescription type; | ||
|
|
@@ -261,18 +287,73 @@ public static Schema convert(TypeDescription orcSchema) { | |
| */ | ||
| public static TypeDescription buildOrcProjection( | ||
| Schema schema, TypeDescription originalOrcSchema) { | ||
| // Callers that cannot establish ID provenance get the conservative behavior: never fill | ||
| // defaults, matching this method's behavior before defaults were supported. | ||
| return buildOrcProjection(schema, originalOrcSchema, FieldIdSource.NAME_MAPPED, false); | ||
| } | ||
|
|
||
| /** | ||
| * Builds the ORC read schema, omitting absent fields that declare an {@code initial-default} so a | ||
| * default-aware reader can fill them. | ||
| * | ||
| * <p>A scalar field at any nesting level is <em>omitted</em> from the read projection when it | ||
| * declares an {@code initial-default}, is absent from the data file, {@code fieldIdSource} is | ||
| * {@link FieldIdSource#EMBEDDED}, and the configured reader supports initial defaults. An | ||
| * id-binding reader then sees no column for that field and fills the declared default as a | ||
| * per-file constant. Otherwise the field is synthesized as a null column, preserving the behavior | ||
| * of readers that have not opted in. | ||
| * | ||
| * @param fieldIdSource where the IDs in {@code originalOrcSchema} came from; see {@link | ||
| * FieldIdSource} | ||
| * @param supportsInitialDefaults whether the configured reader can fill an omitted field's | ||
| * initial default | ||
| */ | ||
| static TypeDescription buildOrcProjection( | ||
| Schema schema, | ||
| TypeDescription originalOrcSchema, | ||
| FieldIdSource fieldIdSource, | ||
| boolean supportsInitialDefaults) { | ||
| 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, | ||
| fieldIdSource, | ||
| supportsInitialDefaults, | ||
| icebergToOrc); | ||
| } | ||
|
|
||
| private static boolean isOmittableDefault( | ||
| Types.NestedField field, | ||
| FieldIdSource fieldIdSource, | ||
| boolean supportsInitialDefaults, | ||
| Map<Integer, OrcField> mapping) { | ||
| // Only scalars reach here with a non-null default: Types.NestedField#castDefault rejects a | ||
| // default on any nested type at construction time. | ||
| return supportsInitialDefaults | ||
| && field.initialDefault() != null | ||
| && !mapping.containsKey(field.fieldId()) | ||
| && fieldIdSource == FieldIdSource.EMBEDDED; | ||
| } | ||
|
|
||
| private static TypeDescription buildOrcProjection( | ||
| Integer fieldId, Type type, boolean isRequired, Map<Integer, OrcField> mapping) { | ||
| Integer fieldId, | ||
| Type type, | ||
| boolean isRequired, | ||
| FieldIdSource fieldIdSource, | ||
| boolean supportsInitialDefaults, | ||
| Map<Integer, OrcField> mapping) { | ||
| final TypeDescription orcType; | ||
|
|
||
| switch (type.typeId()) { | ||
| case STRUCT: | ||
| orcType = TypeDescription.createStruct(); | ||
| for (Types.NestedField nestedField : type.asStructType().fields()) { | ||
| if (isOmittableDefault(nestedField, fieldIdSource, supportsInitialDefaults, mapping)) { | ||
| // The field declares a default, is absent, and the file carries its own Iceberg field | ||
| // IDs. Omit it so a default-aware reader fills it through the existing constant path. | ||
| continue; | ||
| } | ||
| // Using suffix _r to avoid potential underlying issues in ORC reader | ||
| // with reused column names between ORC and Iceberg; | ||
| // e.g. renaming column c -> d and adding new column d | ||
|
|
@@ -285,6 +366,8 @@ private static TypeDescription buildOrcProjection( | |
| nestedField.fieldId(), | ||
| nestedField.type(), | ||
| isRequired && nestedField.isRequired(), | ||
| fieldIdSource, | ||
| supportsInitialDefaults, | ||
| mapping); | ||
| orcType.addField(name, childType); | ||
| } | ||
|
|
@@ -296,16 +379,29 @@ private static TypeDescription buildOrcProjection( | |
| list.elementId(), | ||
| list.elementType(), | ||
| isRequired && list.isElementRequired(), | ||
| fieldIdSource, | ||
| supportsInitialDefaults, | ||
| mapping); | ||
| orcType = TypeDescription.createList(elementType); | ||
| break; | ||
| case MAP: | ||
| Types.MapType map = (Types.MapType) type; | ||
| TypeDescription keyType = | ||
| buildOrcProjection(map.keyId(), map.keyType(), isRequired, mapping); | ||
| buildOrcProjection( | ||
| map.keyId(), | ||
| map.keyType(), | ||
| isRequired, | ||
| fieldIdSource, | ||
| supportsInitialDefaults, | ||
| mapping); | ||
| TypeDescription valueType = | ||
| buildOrcProjection( | ||
| map.valueId(), map.valueType(), isRequired && map.isValueRequired(), mapping); | ||
| map.valueId(), | ||
| map.valueType(), | ||
| isRequired && map.isValueRequired(), | ||
| fieldIdSource, | ||
| supportsInitialDefaults, | ||
| mapping); | ||
| orcType = TypeDescription.createMap(keyType, valueType); | ||
| break; | ||
| default: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,9 +21,12 @@ | |
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.BiFunction; | ||
| import org.apache.iceberg.MetadataColumns; | ||
| import org.apache.iceberg.Schema; | ||
| import org.apache.iceberg.relocated.com.google.common.base.Preconditions; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Maps; | ||
| import org.apache.iceberg.types.Type; | ||
| import org.apache.iceberg.types.Types; | ||
| import org.apache.orc.TypeDescription; | ||
| import org.apache.orc.storage.ql.exec.vector.BytesColumnVector; | ||
|
|
@@ -183,6 +186,28 @@ protected StructReader( | |
| List<OrcValueReader<?>> readers, | ||
| Types.StructType struct, | ||
| Map<Integer, ?> idToConstant) { | ||
| this(orcType, readers, struct, idToConstant, null); | ||
| } | ||
|
|
||
| /** | ||
| * Binds struct fields by Iceberg field id, filling a field's {@code initial-default} when the | ||
| * file has no column for it. | ||
| * | ||
| * <p>A field reaches the default only when the read projection omitted it, which {@link | ||
| * ORCSchemaUtil#buildOrcProjection(Schema, TypeDescription, ORCSchemaUtil.FieldIdSource, | ||
| * boolean)} does only for an absent field that declares a default in a file carrying its own | ||
| * Iceberg field ids. The default is materialized as a per-file constant and consumes no column | ||
| * vector, so a present column -- including one holding an explicit null -- always wins. | ||
| * | ||
| * @param convertConstant converts a default to the engine's in-memory representation, or null | ||
| * to disable default filling and keep the strict missing-reader failure | ||
| */ | ||
| protected StructReader( | ||
| TypeDescription orcType, | ||
| List<OrcValueReader<?>> readers, | ||
| Types.StructType struct, | ||
| Map<Integer, ?> idToConstant, | ||
| BiFunction<Type, Object, Object> convertConstant) { | ||
| List<Types.NestedField> fields = struct.fields(); | ||
| this.readers = new OrcValueReader[fields.size()]; | ||
| this.isConstantOrMetadataField = new boolean[fields.size()]; | ||
|
|
@@ -208,6 +233,10 @@ protected StructReader( | |
| this.isConstantOrMetadataField[pos] = false; | ||
| this.orcFieldIndex[pos] = fieldIdToOrcIndex.getOrDefault(field.fieldId(), -1); | ||
| this.readers[pos] = fileReader; | ||
| } else if (convertConstant != null && field.initialDefault() != null) { | ||
| this.isConstantOrMetadataField[pos] = true; | ||
| this.readers[pos] = | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actual logic |
||
| constants(convertConstant.apply(field.type(), field.initialDefault())); | ||
| } else if (MetadataColumns.isMetadataColumn(field.name())) { | ||
| this.isConstantOrMetadataField[pos] = true; | ||
| this.readers[pos] = constants(null); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we throw an error vs defaulting to null? this would avoid the correctness issue.