Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions api/src/main/java/org/apache/iceberg/types/ReplaceTypeById.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.iceberg.types;

import java.util.List;
import java.util.Map;
import org.apache.iceberg.Schema;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;

class ReplaceTypeById extends TypeUtil.SchemaVisitor<Type> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor: I think it might be worth pulling this out into a separate PR in order to reduce complexity in this one and harden the impl + have more extensive tests (especially around default value handling)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You are correct. That would have been a better choice. Keeping it here because the code has been reviewed. Will add the tests.

private final Map<Integer, Type> replacementsById;

ReplaceTypeById(Map<Integer, Type> replacementsById) {
this.replacementsById = replacementsById;
}

@Override
public Type schema(Schema schema, Type structResult) {
return structResult;
}

@Override
public Type struct(Types.StructType struct, List<Type> fieldResults) {
List<Types.NestedField> fields = struct.fields();
List<Types.NestedField> newFields = Lists.newArrayListWithExpectedSize(fields.size());
boolean hasChanged = false;

for (int i = 0; i < fields.size(); i += 1) {
Type fieldReplacement = fieldResults.get(i);
Types.NestedField field = fields.get(i);
if (field.type() != fieldReplacement) {
hasChanged = true;
newFields.add(Types.NestedField.from(field).ofType(fieldReplacement).build());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need to handle initial/write defaults here when we're changing the type? What happens if the original type is int with some defaults but we change the type to string? I think we should have a test for this scenario

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I checked, and defaults are already handled correctly because it turns out NestedField re-validates them against the new type. Added tests for both: testReplaceFieldTypesPreservesDefaultsOnCompatibleType (default survives an int to long promotion) and testReplaceFieldTypesRejectsDefaultIncompatibleWithType (int to string throws).

} else {
newFields.add(field);
}
}

if (hasChanged) {
return Types.StructType.of(newFields);
}

return struct;
}

@Override
public Type field(Types.NestedField field, Type fieldResult) {
return replacementsById.getOrDefault(field.fieldId(), fieldResult);
}

@Override
public Type list(Types.ListType list, Type elementResult) {
Type elementReplacement = replacementsById.getOrDefault(list.elementId(), elementResult);
if (list.elementType() != elementReplacement) {
if (list.isElementRequired()) {
return Types.ListType.ofRequired(list.elementId(), elementReplacement);
} else {
return Types.ListType.ofOptional(list.elementId(), elementReplacement);
}
}

return list;
}

@Override
public Type map(Types.MapType map, Type keyResult, Type valueResult) {
Comment thread
anoopj marked this conversation as resolved.
Type keyReplacement = replacementsById.getOrDefault(map.keyId(), keyResult);
Type valueReplacement = replacementsById.getOrDefault(map.valueId(), valueResult);
if (map.keyType() != keyReplacement || map.valueType() != valueReplacement) {
if (map.isValueRequired()) {
return Types.MapType.ofRequired(
map.keyId(), map.valueId(), keyReplacement, valueReplacement);
} else {
return Types.MapType.ofOptional(
map.keyId(), map.valueId(), keyReplacement, valueReplacement);
}
}

return map;
}

@Override
public Type variant(Types.VariantType variant) {
return variant;
}

@Override
public Type primitive(Type.PrimitiveType primitive) {
return primitive;
}
}
18 changes: 18 additions & 0 deletions api/src/main/java/org/apache/iceberg/types/TypeUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@
return project(schema, projectedIds);
}

/**
* Returns a copy of the schema with the type of each field in {@code replacementsById} replaced
* by its mapped type. Fields not in the map are unchanged.
*
* @param schema a schema
* @param replacementsById a map from field ID to the type that should replace the field's type
* @return a schema with the replaced field types
*/
public static Schema replaceFieldTypes(Schema schema, Map<Integer, Type> replacementsById) {
Types.StructType struct = visit(schema, new ReplaceTypeById(replacementsById)).asStructType();
if (struct.equals(schema.asStruct())) {
return schema;
}

return new Schema(
schema.schemaId(), struct.fields(), schema.getAliases(), schema.identifierFieldIds());
Comment thread
anoopj marked this conversation as resolved.
}

public static Schema join(Schema left, Schema right) {
List<Types.NestedField> joinedColumns = Lists.newArrayList(left.columns());
for (Types.NestedField rightColumn : right.columns()) {
Expand Down Expand Up @@ -457,7 +475,7 @@
return true;
}

switch (from.typeId()) {

Check warning on line 478 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / revapi

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 478 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / build-checks (17, pull_request)

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 478 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / check-runtime-deps

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch
case INTEGER:
return to.typeId() == Type.TypeID.LONG;

Expand Down Expand Up @@ -561,7 +579,7 @@
}

private static int estimateSize(Type type) {
switch (type.typeId()) {

Check warning on line 582 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / revapi

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 582 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / build-checks (17, pull_request)

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 582 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / check-runtime-deps

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch
case BOOLEAN:
// the size of a boolean variable is virtual machine dependent
// it is common to believe booleans occupy 1 byte in most JVMs
Expand Down Expand Up @@ -745,7 +763,7 @@
}

public static <T> T visit(Type type, SchemaVisitor<T> visitor) {
switch (type.typeId()) {

Check warning on line 766 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / revapi

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 766 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / build-checks (17, pull_request)

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 766 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / check-runtime-deps

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch
case STRUCT:
Types.StructType struct = type.asNestedType().asStructType();
List<T> results = Lists.newArrayListWithExpectedSize(struct.fields().size());
Expand Down Expand Up @@ -885,7 +903,7 @@
* @return the result of traversing the given type with the visitor
*/
public static <T> T visit(Type type, CustomOrderSchemaVisitor<T> visitor) {
switch (type.typeId()) {

Check warning on line 906 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / revapi

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 906 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / build-checks (17, pull_request)

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch

Check warning on line 906 in api/src/main/java/org/apache/iceberg/types/TypeUtil.java

View workflow job for this annotation

GitHub Actions / check-runtime-deps

[StatementSwitchToExpressionSwitch] This statement switch can be converted to a new-style arrow switch
case STRUCT:
Types.StructType struct = type.asNestedType().asStructType();
List<VisitFieldFuture<T>> results =
Expand Down
130 changes: 130 additions & 0 deletions api/src/test/java/org/apache/iceberg/types/TestTypeUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.stream.Stream;
import org.apache.iceberg.Schema;
import org.apache.iceberg.expressions.Literal;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
import org.apache.iceberg.relocated.com.google.common.collect.Sets;
import org.apache.iceberg.types.Types.IntegerType;
Expand Down Expand Up @@ -1037,4 +1038,133 @@ public void testIndexStatsNames() {
.containsEntry(24, "addresses_value") // the leaf takes precedence
.hasSize(22);
}

@Test
public void testReplaceFieldTypes() {
Comment thread
danielcweeks marked this conversation as resolved.
Types.StructType replacement = Types.StructType.of(required(10, "x", IntegerType.get()));
Schema schema =
new Schema(
required(1, "id", IntegerType.get()),
required(2, "s", Types.StructType.of(required(3, "a", Types.LongType.get()))));

Schema result = TypeUtil.replaceFieldTypes(schema, ImmutableMap.of(2, replacement));

assertThat(result.findField(1).type()).isEqualTo(IntegerType.get());
assertThat(result.findField(2).type()).isEqualTo(replacement);
}

@Test
public void testReplaceFieldTypesPrimitive() {
Schema schema =
new Schema(required(1, "id", IntegerType.get()), required(2, "count", IntegerType.get()));

Schema result = TypeUtil.replaceFieldTypes(schema, ImmutableMap.of(2, Types.LongType.get()));

assertThat(result.findField(1).type()).isEqualTo(IntegerType.get());
assertThat(result.findField(2).type()).isEqualTo(Types.LongType.get());
}

@Test
public void testReplaceFieldTypesListElement() {
Schema schema =
new Schema(required(1, "list", Types.ListType.ofRequired(2, Types.StructType.of())));

Schema result =
TypeUtil.replaceFieldTypes(
schema, ImmutableMap.of(2, Types.StructType.of(required(3, "x", IntegerType.get()))));

Types.ListType list = (Types.ListType) result.findField(1).type();
assertThat(list.elementType().asStructType().field(3).name()).isEqualTo("x");
assertThat(list.isElementRequired()).isTrue();
}

@Test
public void testReplaceFieldTypesMapKeyAndValue() {
Schema schema =
new Schema(
required(1, "m", Types.MapType.ofRequired(2, 3, IntegerType.get(), IntegerType.get())));

Schema result =
TypeUtil.replaceFieldTypes(
schema, ImmutableMap.of(2, Types.LongType.get(), 3, Types.StringType.get()));

Types.MapType map = (Types.MapType) result.findField(1).type();
assertThat(map.keyType()).isEqualTo(Types.LongType.get());
assertThat(map.valueType()).isEqualTo(Types.StringType.get());
assertThat(map.isValueRequired()).isTrue();
}

@Test
public void testReplaceFieldTypesPreservesDefaultsOnCompatibleType() {
Types.NestedField field =
Types.NestedField.optional("count")
.withId(1)
.ofType(IntegerType.get())
.withInitialDefault(Literal.of(5))
.withWriteDefault(Literal.of(7))
.build();
Schema schema = new Schema(field);

Schema result = TypeUtil.replaceFieldTypes(schema, ImmutableMap.of(1, Types.LongType.get()));

Types.NestedField replaced = result.findField(1);
assertThat(replaced.type()).isEqualTo(Types.LongType.get());
assertThat(replaced.initialDefault()).isEqualTo(5L);
assertThat(replaced.writeDefault()).isEqualTo(7L);
}

@Test
public void testReplaceFieldTypesRejectsDefaultIncompatibleWithType() {
Types.NestedField field =
Types.NestedField.optional("count")
.withId(1)
.ofType(IntegerType.get())
.withInitialDefault(Literal.of(5))
.build();
Schema schema = new Schema(field);

assertThatThrownBy(
() -> TypeUtil.replaceFieldTypes(schema, ImmutableMap.of(1, Types.StringType.get())))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot cast default value to string");
}

@Test
public void testReplaceFieldTypesPreservesIdentifierField() {
Schema schema =
new Schema(
Lists.newArrayList(
required(1, "id", IntegerType.get()), required(2, "data", Types.StringType.get())),
Sets.newHashSet(1));

Schema result = TypeUtil.replaceFieldTypes(schema, ImmutableMap.of(1, Types.LongType.get()));

assertThat(result.findField(1).type()).isEqualTo(Types.LongType.get());
assertThat(result.identifierFieldIds()).containsExactly(1);
}

@Test
public void testReplaceFieldTypesRejectsNonPrimitiveIdentifierField() {
Schema schema =
new Schema(
Lists.newArrayList(
required(1, "id", IntegerType.get()), required(2, "data", Types.StringType.get())),
Sets.newHashSet(1));

// an identifier field must be primitive, so replacing it with a struct is rejected
assertThatThrownBy(
() ->
TypeUtil.replaceFieldTypes(
schema,
ImmutableMap.of(1, Types.StructType.of(required(3, "x", IntegerType.get())))))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("not a primitive type field");
}

@Test
public void testReplaceFieldTypesNoMatchReturnsSameSchema() {
Schema schema = new Schema(required(1, "id", IntegerType.get()));
Schema result = TypeUtil.replaceFieldTypes(schema, ImmutableMap.of(99, Types.LongType.get()));
assertThat(result).isSameAs(schema);
}
}
19 changes: 19 additions & 0 deletions core/src/main/java/org/apache/iceberg/Partitioning.java
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ public static StructType partitionType(Table table) {
"table partition", specs, allActiveFieldIds(table.schema(), specs));
}

/**
* Builds a unified partition type containing all partition fields from the given specs, including
* fields whose source columns are no longer present in the table schema.
*
* @param specs the partition specs to unify
Comment thread
anoopj marked this conversation as resolved.
* @return the constructed unified partition type
*/
static StructType unionPartitionTypes(Collection<PartitionSpec> specs) {
return buildPartitionProjectionType("union partition", specs, allFieldIds(specs));
}

/**
* Checks if any of the specs in a table is partitioned.
*
Expand Down Expand Up @@ -347,6 +358,14 @@ private static boolean compatibleTransforms(Transform<?, ?> t1, Transform<?, ?>
|| t2.equals(Transforms.alwaysNull());
}

// collects IDs of all partition fields used across specs
private static Set<Integer> allFieldIds(Collection<PartitionSpec> specs) {
return FluentIterable.from(specs)
.transformAndConcat(PartitionSpec::fields)
.transform(PartitionField::fieldId)
.toSet();
}

// collects IDs of all partition field used across specs that are in the current schema
private static Set<Integer> allActiveFieldIds(Schema schema, Collection<PartitionSpec> specs) {
return FluentIterable.from(specs)
Expand Down
24 changes: 19 additions & 5 deletions core/src/main/java/org/apache/iceberg/TrackedFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.Types;

/** A file tracked by a manifest. */
Expand Down Expand Up @@ -95,9 +96,14 @@ interface TrackedFile {
Types.ListType.ofRequired(136, Types.IntegerType.get()),
"Field ids used to determine row equality in equality delete files");

static Types.StructType schemaWithContentStats(
Types.StructType partitionType, Types.StructType contentStatsType) {
return Types.StructType.of(
/**
* Returns the schema for the given partition and content stats types.
*
* <p>The partition and content stats fields use {@link Types.UnknownType} when their types have
* no fields, so that they are not stored in manifest files.
*/
static Schema schema(Types.StructType partitionType, Types.StructType contentStatsType) {
return new Schema(
TRACKING,
CONTENT_TYPE,
FORMAT_VERSION,
Expand All @@ -106,9 +112,13 @@ static Types.StructType schemaWithContentStats(
RECORD_COUNT,
FILE_SIZE_IN_BYTES,
SPEC_ID,
Types.NestedField.optional(PARTITION_ID, PARTITION_NAME, partitionType, PARTITION_DOC),
Types.NestedField.optional(
CONTENT_STATS_ID, CONTENT_STATS_NAME, contentStatsType, CONTENT_STATS_DOC),
PARTITION_ID, PARTITION_NAME, typeOrUnknown(partitionType), PARTITION_DOC),
Types.NestedField.optional(
CONTENT_STATS_ID,
CONTENT_STATS_NAME,
typeOrUnknown(contentStatsType),
CONTENT_STATS_DOC),
SORT_ORDER_ID,
DELETION_VECTOR,
MANIFEST_INFO,
Expand All @@ -117,6 +127,10 @@ static Types.StructType schemaWithContentStats(
EQUALITY_IDS);
}

private static Type typeOrUnknown(Types.StructType structType) {
return structType.fields().isEmpty() ? Types.UnknownType.get() : structType;
}

/** Returns the tracking information for this entry. */
Tracking tracking();

Expand Down
12 changes: 6 additions & 6 deletions core/src/main/java/org/apache/iceberg/TrackedFileStruct.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ class TrackedFileStruct extends SupportsIndexProjection implements TrackedFile,
/** Used by internal readers to instantiate this class with a projection schema. */
TrackedFileStruct(Types.StructType projection) {
super(BASE_TYPE, projection);
// partition type may be null if the field was not projected
// partition type may be null if the field was not projected, or unknown for unpartitioned
// manifests
Type partType = projection.fieldType("partition");
if (partType != null) {
this.partitionData = new PartitionData(partType.asNestedType().asStructType());
if (partType != null && partType.isStructType()) {
this.partitionData = new PartitionData(partType.asStructType());
}
}

Expand All @@ -101,10 +102,10 @@ class TrackedFileStruct extends SupportsIndexProjection implements TrackedFile,
int formatVersion,
String location,
FileFormat fileFormat,
PartitionData partition,
long recordCount,
long fileSizeInBytes,
Integer specId,
PartitionData partition,
ContentStats contentStats,
Integer sortOrderId,
DeletionVector deletionVector,
Expand All @@ -120,9 +121,8 @@ class TrackedFileStruct extends SupportsIndexProjection implements TrackedFile,
this.fileFormat = fileFormat;
this.recordCount = recordCount;
this.fileSizeInBytes = fileSizeInBytes;
this.partitionData = partition;

this.specId = specId;
this.partitionData = partition;
this.contentStats = contentStats;
this.sortOrderId = sortOrderId;
this.deletionVector = deletionVector;
Expand Down
Loading
Loading