Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion api/src/main/java/org/apache/iceberg/types/Types.java
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,10 @@ private NestedField(
Literal<?> writeDefault) {
Preconditions.checkNotNull(name, "Name cannot be null");
Preconditions.checkNotNull(type, "Type cannot be null");
Preconditions.checkArgument(
isOptional || !type.equals(UnknownType.get()),
"Cannot create required field with unknown type: %s",
name);
this.isOptional = isOptional;
this.id = id;
this.name = name;
Expand All @@ -694,7 +698,10 @@ private static Literal<?> castDefault(Literal<?> defaultValue, Type type) {
throw new IllegalArgumentException(
String.format("Invalid default value for %s: %s (must be null)", type, defaultValue));
} else if (defaultValue != null) {
return defaultValue.to(type);
Literal<?> typedDefault = defaultValue.to(type);
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking of it, would a NoneLiteral make sense 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now, we make a lot of assumptions that Literal does not contain null or NaN. That way, we don't need to implement 3-value boolean logic or worry about how to handle NaN in comparisons.

If you want to have a null default, then you don't set a default value for an optional column. And this also passes through null without a problem if the Literal was null.

Preconditions.checkArgument(
typedDefault != null, "Cannot cast default value to %s: %s", type, defaultValue);
return typedDefault;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class TestPartitionSpecValidation {
NestedField.required(5, "another_d", Types.TimestampType.withZone()),
NestedField.required(6, "s", Types.StringType.get()),
NestedField.required(7, "v", Types.VariantType.get()),
NestedField.required(8, "u", Types.UnknownType.get()));
NestedField.optional(8, "u", Types.UnknownType.get()));

@Test
public void testMultipleTimestampPartitions() {
Expand Down
54 changes: 52 additions & 2 deletions api/src/test/java/org/apache/iceberg/TestSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.Types;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.FieldSource;
Expand All @@ -41,8 +42,7 @@ public class TestSchema {
ImmutableList.of(
Types.TimestampNanoType.withoutZone(),
Types.TimestampNanoType.withZone(),
Types.VariantType.get(),
Types.UnknownType.get());
Types.VariantType.get());

private static final Schema INITIAL_DEFAULT_SCHEMA =
new Schema(
Expand Down Expand Up @@ -122,6 +122,56 @@ private static Stream<Arguments> supportedTypes() {
.mapToObj(supportedVersion -> Arguments.of(type, supportedVersion)));
}

@Test
public void testUnknownSupport() {
// this needs a different schema because it cannot be used in required fields
Schema schemaWithUnknown =
new Schema(
Types.NestedField.required(1, "id", Types.LongType.get()),
Types.NestedField.optional(2, "top", Types.UnknownType.get()),
Types.NestedField.optional(
3, "arr", Types.ListType.ofOptional(4, Types.UnknownType.get())),
Types.NestedField.required(
5,
"struct",
Types.StructType.of(
Types.NestedField.optional(6, "inner_op", Types.UnknownType.get()),
Types.NestedField.optional(
7,
"inner_map",
Types.MapType.ofOptional(
8, 9, Types.StringType.get(), Types.UnknownType.get())),
Types.NestedField.optional(
10,
"struct_arr",
Types.StructType.of(
Types.NestedField.optional(11, "deep", Types.UnknownType.get()))))));

assertThatThrownBy(() -> Schema.checkCompatibility(schemaWithUnknown, 2))
.isInstanceOf(IllegalStateException.class)
.hasMessage(
"Invalid schema for v%s:\n"
+ "- Invalid type for top: %s is not supported until v%s\n"
+ "- Invalid type for arr.element: %s is not supported until v%s\n"
+ "- Invalid type for struct.inner_op: %s is not supported until v%s\n"
+ "- Invalid type for struct.inner_map.value: %s is not supported until v%s\n"
+ "- Invalid type for struct.struct_arr.deep: %s is not supported until v%s",
2,
Types.UnknownType.get(),
MIN_FORMAT_VERSIONS.get(Type.TypeID.UNKNOWN),
Types.UnknownType.get(),
MIN_FORMAT_VERSIONS.get(Type.TypeID.UNKNOWN),
Types.UnknownType.get(),
MIN_FORMAT_VERSIONS.get(Type.TypeID.UNKNOWN),
Types.UnknownType.get(),
MIN_FORMAT_VERSIONS.get(Type.TypeID.UNKNOWN),
Types.UnknownType.get(),
MIN_FORMAT_VERSIONS.get(Type.TypeID.UNKNOWN));

assertThatCode(() -> Schema.checkCompatibility(schemaWithUnknown, 3))
.doesNotThrowAnyException();
}

@ParameterizedTest
@MethodSource("supportedTypes")
public void testTypeSupported(Type type, int supportedVersion) {
Expand Down
43 changes: 43 additions & 0 deletions core/src/test/java/org/apache/iceberg/TestSchemaUpdate.java
Original file line number Diff line number Diff line change
Expand Up @@ -2439,4 +2439,47 @@ public void testMoveDeletedNestedStructFieldToFirst() {

assertThat(actual.asStruct()).isEqualTo(expected.asStruct());
}

@Test
public void testAddUnknown() {
Schema schema = new Schema(required(1, "id", Types.LongType.get()));
Schema expected =
new Schema(
required(1, "id", Types.LongType.get()), optional(2, "unk", Types.UnknownType.get()));

Schema actual =
new SchemaUpdate(schema, schema.highestFieldId())
.addColumn("unk", Types.UnknownType.get())
.apply();

assertThat(actual.asStruct()).isEqualTo(expected.asStruct());
}

@Test
public void testAddUnknownNonNullDefault() {
Schema schema = new Schema(required(1, "id", Types.LongType.get()));

assertThatThrownBy(
() ->
new SchemaUpdate(schema, schema.highestFieldId())
.allowIncompatibleChanges()
.addColumn("unk", Types.UnknownType.get(), Literal.of("string!"))
.apply())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Cannot cast default value to unknown: \"string!\"");
}

@Test
public void testAddRequiredUnknown() {
Schema schema = new Schema(required(1, "id", Types.LongType.get()));

assertThatThrownBy(
() ->
new SchemaUpdate(schema, schema.highestFieldId())
.allowIncompatibleChanges()
.addRequiredColumn("unk", Types.UnknownType.get())
.apply())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Cannot create required field with unknown type: unk");
}
}