blob: 0b11252e5508c140ce5406171c6a635270ac15ca [file]
// 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.
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.apache.iceberg.CatalogUtil;
import org.apache.iceberg.Schema;
import org.apache.iceberg.Table;
import org.apache.iceberg.UpdateSchema;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.expressions.Literal;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.Types;
import org.apache.iceberg.util.DateTimeUtil;
/** Adds and evolves Iceberg V3 default-value fixtures used by Doris regression tests. */
public final class CreateIcebergInitialDefaultFixtures {
private static final String CATALOG_NAME = "demo";
private static final String SET_INT_WRITE_DEFAULT = "set-int-write-default";
private static final String MISSING_STRUCT_COLUMN = "missing_struct_col";
private static final String MISSING_LIST_COLUMN = "missing_list_col";
private static final String MISSING_MAP_COLUMN = "missing_map_col";
private CreateIcebergInitialDefaultFixtures() {
}
public static void main(String[] args) {
if (args.length == 5 && SET_INT_WRITE_DEFAULT.equals(args[0])) {
Catalog catalog = loadCatalog();
int writeDefault = Integer.parseInt(args[4]);
updateIntWriteDefault(catalog, TableIdentifier.of(args[1], args[2]), writeDefault);
updateIntWriteDefault(catalog, TableIdentifier.of(args[1], args[3]), writeDefault);
return;
}
if (args.length != 3) {
throw new IllegalArgumentException(
"Usage: CreateIcebergInitialDefaultFixtures "
+ "<namespace> <parquet-table> <orc-table> | "
+ SET_INT_WRITE_DEFAULT
+ " <namespace> <parquet-table> <orc-table> <value>");
}
Catalog catalog = loadCatalog();
evolveTable(catalog, TableIdentifier.of(args[0], args[1]));
evolveTable(catalog, TableIdentifier.of(args[0], args[2]));
}
private static Catalog loadCatalog() {
Map<String, String> properties = new HashMap<>();
properties.put("type", "rest");
properties.put("uri", "http://rest:8181");
properties.put("warehouse", "s3://warehouse/wh/");
properties.put("io-impl", "org.apache.iceberg.aws.s3.S3FileIO");
properties.put("s3.endpoint", "http://minio:9000");
properties.put("s3.path-style-access", "true");
properties.put("s3.region", "us-east-1");
return CatalogUtil.buildIcebergCatalog(CATALOG_NAME, properties, null);
}
private static void evolveTable(Catalog catalog, TableIdentifier identifier) {
Table table = catalog.loadTable(identifier);
List<AddedDefault> addedDefaults = new ArrayList<>();
UpdateSchema addColumns = table.updateSchema();
addPrimitiveDefaults(addColumns, null, "default", addedDefaults);
addPrimitiveDefaults(addColumns, "struct_col", "struct_default", addedDefaults);
// Keep the list/map children optional so post-evolution files can distinguish an explicit
// physical NULL from an absent child that must use its initial default.
addDefault(
addColumns,
"list_col",
new DefaultSpec(
"list_default_int",
Types.IntegerType.get(),
Literal.of(101),
Literal.of(201),
false),
addedDefaults);
addDefault(
addColumns,
"map_col",
new DefaultSpec(
"map_default_int",
Types.IntegerType.get(),
Literal.of(103),
Literal.of(203),
false),
addedDefaults);
addMissingComplexColumns(addColumns, addedDefaults);
addColumns.commit();
table = catalog.loadTable(identifier);
UpdateSchema updateWriteDefaults = table.updateSchema();
for (AddedDefault addedDefault : addedDefaults) {
addedDefault.canonicalName = canonicalName(table.schema(), addedDefault);
updateWriteDefaults.updateColumnDefault(
addedDefault.canonicalName, addedDefault.spec.writeDefault);
}
updateWriteDefaults.commit();
table = catalog.loadTable(identifier);
verifyDefaults(table.schema(), addedDefaults);
verifyMissingComplexParentDefaults(table.schema());
System.out.println("Created initial-default fixture: " + identifier);
}
private static void updateIntWriteDefault(
Catalog catalog, TableIdentifier identifier, int writeDefault) {
Table table = catalog.loadTable(identifier);
table.updateSchema()
.updateColumnDefault("default_int", Literal.of(writeDefault))
.commit();
Types.NestedField field = catalog.loadTable(identifier).schema().findField("default_int");
if (field == null || !Objects.equals(writeDefault, field.writeDefault())) {
throw new IllegalStateException(
"Unexpected write-default for " + identifier + ": "
+ (field == null ? "missing field" : field.writeDefault()));
}
}
private static void addMissingComplexColumns(
UpdateSchema update, List<AddedDefault> addedDefaults) {
// Iceberg 1.10.1 Types.NestedField.castDefault rejects every non-null default on a nested
// parent. The strongest fixture its public API can author is a newly added optional parent
// with defaults carried by its children. Old files do not contain the parent at all, so
// readers must preserve a NULL parent instead of reviving it from the child default.
DefaultSpec structChild =
new DefaultSpec(
"missing_struct_default_int",
Types.IntegerType.get(),
Literal.of(107),
Literal.of(207),
false);
update.addColumn(
MISSING_STRUCT_COLUMN,
Types.StructType.of(defaultField(1, structChild)),
"Whole missing struct initial-default fallback fixture");
addedDefaults.add(new AddedDefault(MISSING_STRUCT_COLUMN, structChild));
DefaultSpec listChild =
new DefaultSpec(
"missing_list_default_int",
Types.IntegerType.get(),
Literal.of(109),
Literal.of(209),
false);
update.addColumn(
MISSING_LIST_COLUMN,
Types.ListType.ofOptional(
1, Types.StructType.of(defaultField(2, listChild))),
"Whole missing list initial-default fallback fixture");
addedDefaults.add(new AddedDefault(MISSING_LIST_COLUMN, listChild));
DefaultSpec mapChild =
new DefaultSpec(
"missing_map_default_int",
Types.IntegerType.get(),
Literal.of(111),
Literal.of(211),
false);
update.addColumn(
MISSING_MAP_COLUMN,
Types.MapType.ofOptional(
1,
2,
Types.StringType.get(),
Types.StructType.of(defaultField(3, mapChild))),
"Whole missing map initial-default fallback fixture");
addedDefaults.add(new AddedDefault(MISSING_MAP_COLUMN, mapChild));
}
private static Types.NestedField defaultField(int fieldId, DefaultSpec defaultSpec) {
return Types.NestedField.builder()
.withId(fieldId)
.withName(defaultSpec.name)
.isOptional(!defaultSpec.required)
.ofType(defaultSpec.type)
.withDoc("Iceberg V3 initial-default regression fixture")
.withInitialDefault(defaultSpec.initialDefault)
.withWriteDefault(defaultSpec.writeDefault)
.build();
}
private static void addPrimitiveDefaults(
UpdateSchema update,
String parent,
String prefix,
List<AddedDefault> addedDefaults) {
for (DefaultSpec primitiveDefault : primitiveDefaults()) {
DefaultSpec namedDefault = primitiveDefault.withName(prefix + "_" + primitiveDefault.name);
addDefault(update, parent, namedDefault, addedDefaults);
}
}
private static void addDefault(
UpdateSchema update,
String parent,
DefaultSpec defaultSpec,
List<AddedDefault> addedDefaults) {
String doc = "Iceberg V3 initial-default regression fixture";
if (parent == null) {
if (defaultSpec.required) {
update.addRequiredColumn(
defaultSpec.name,
defaultSpec.type,
doc,
defaultSpec.initialDefault);
} else {
update.addColumn(
defaultSpec.name,
defaultSpec.type,
doc,
defaultSpec.initialDefault);
}
} else if (defaultSpec.required) {
update.addRequiredColumn(
parent,
defaultSpec.name,
defaultSpec.type,
doc,
defaultSpec.initialDefault);
} else {
update.addColumn(
parent,
defaultSpec.name,
defaultSpec.type,
doc,
defaultSpec.initialDefault);
}
addedDefaults.add(new AddedDefault(parent, defaultSpec));
}
private static List<DefaultSpec> primitiveDefaults() {
return Arrays.asList(
new DefaultSpec(
"boolean", Types.BooleanType.get(), Literal.of(true), Literal.of(false), false),
new DefaultSpec(
"int", Types.IntegerType.get(), Literal.of(34), Literal.of(35), false),
new DefaultSpec(
"long",
Types.LongType.get(),
Literal.of(4_900_000_000L),
Literal.of(4_900_000_001L),
false),
new DefaultSpec(
"float", Types.FloatType.get(), Literal.of(12.25F), Literal.of(13.5F), false),
new DefaultSpec(
"double", Types.DoubleType.get(), Literal.of(-123.5D), Literal.of(456.75D), false),
new DefaultSpec(
"decimal",
Types.DecimalType.of(20, 4),
Literal.of(new BigDecimal("12345.6789")),
Literal.of(new BigDecimal("98765.4321")),
false),
new DefaultSpec(
"date",
Types.DateType.get(),
Literal.of(DateTimeUtil.isoDateToDays("2024-12-17")),
Literal.of(DateTimeUtil.isoDateToDays("2025-01-18")),
false),
new DefaultSpec(
"timestamp",
Types.TimestampType.withoutZone(),
Literal.of(DateTimeUtil.isoTimestampToMicros("2024-12-17T23:59:59.123456")),
Literal.of(DateTimeUtil.isoTimestampToMicros("2025-01-18T01:02:03.654321")),
false),
new DefaultSpec(
"timestamptz",
Types.TimestampType.withZone(),
Literal.of(
DateTimeUtil.isoTimestamptzToMicros(
"2024-12-17T23:59:59.123456+00:00")),
Literal.of(
DateTimeUtil.isoTimestamptzToMicros(
"2025-01-18T01:02:03.654321+00:00")),
false),
new DefaultSpec(
"string",
Types.StringType.get(),
Literal.of("initial-default"),
Literal.of("write-default"),
true),
new DefaultSpec(
"uuid",
Types.UUIDType.get(),
Literal.of(UUID.fromString("123e4567-e89b-12d3-a456-426614174000")),
Literal.of(UUID.fromString("123e4567-e89b-12d3-a456-426614174001")),
false),
new DefaultSpec(
"fixed",
Types.FixedType.ofLength(4),
Literal.of(ByteBuffer.wrap(new byte[] {0x0a, 0x0b, 0x0c, 0x0d})),
Literal.of(ByteBuffer.wrap(new byte[] {0x1a, 0x1b, 0x1c, 0x1d})),
false),
new DefaultSpec(
"binary",
Types.BinaryType.get(),
Literal.of(ByteBuffer.wrap(new byte[] {0x2a, 0x2b, 0x2c})),
Literal.of(ByteBuffer.wrap(new byte[] {0x3a, 0x3b, 0x3c})),
false));
}
private static String canonicalName(Schema schema, AddedDefault addedDefault) {
Types.NestedField field;
if (addedDefault.parent == null) {
field = schema.findField(addedDefault.spec.name);
} else {
Types.NestedField parentField = schema.findField(addedDefault.parent);
if (parentField == null) {
throw new IllegalStateException("Missing parent field: " + addedDefault.parent);
}
Type childContainer = parentField.type();
if (childContainer.isListType()) {
childContainer = childContainer.asListType().elementType();
} else if (childContainer.isMapType()) {
childContainer = childContainer.asMapType().valueType();
}
if (!childContainer.isStructType()) {
throw new IllegalStateException(
"Parent is not a struct, list-of-struct, or map-to-struct: "
+ addedDefault.parent);
}
field = null;
for (Types.NestedField child : childContainer.asStructType().fields()) {
if (child.name().equals(addedDefault.spec.name)) {
field = child;
break;
}
}
}
if (field == null) {
throw new IllegalStateException("Missing added field: " + addedDefault.spec.name);
}
return schema.findColumnName(field.fieldId());
}
private static void verifyDefaults(Schema schema, List<AddedDefault> addedDefaults) {
for (AddedDefault addedDefault : addedDefaults) {
Types.NestedField field = schema.findField(addedDefault.canonicalName);
if (field == null) {
throw new IllegalStateException("Missing reloaded field: " + addedDefault.canonicalName);
}
if (field.isRequired() != addedDefault.spec.required) {
throw new IllegalStateException(
"Unexpected requiredness for " + addedDefault.canonicalName);
}
Object expectedInitial = typedValue(addedDefault.spec.initialDefault, field.type());
Object expectedWrite = typedValue(addedDefault.spec.writeDefault, field.type());
if (!Objects.equals(expectedInitial, field.initialDefault())) {
throw new IllegalStateException(
"Unexpected initial-default for "
+ addedDefault.canonicalName
+ ": "
+ field.initialDefault());
}
if (!Objects.equals(expectedWrite, field.writeDefault())) {
throw new IllegalStateException(
"Unexpected write-default for "
+ addedDefault.canonicalName
+ ": "
+ field.writeDefault());
}
if (Objects.equals(field.initialDefault(), field.writeDefault())) {
throw new IllegalStateException(
"initial-default and write-default must differ for "
+ addedDefault.canonicalName);
}
}
}
private static void verifyMissingComplexParentDefaults(Schema schema) {
for (String columnName :
Arrays.asList(
MISSING_STRUCT_COLUMN, MISSING_LIST_COLUMN, MISSING_MAP_COLUMN)) {
Types.NestedField field = schema.findField(columnName);
if (field == null) {
throw new IllegalStateException("Missing complex parent field: " + columnName);
}
if (field.initialDefault() != null || field.writeDefault() != null) {
throw new IllegalStateException(
"Iceberg 1.10.1 nested parent default must remain null: " + columnName);
}
}
}
private static Object typedValue(Literal<?> literal, Type type) {
Literal<?> converted = literal.to(type);
if (converted == null) {
throw new IllegalStateException("Cannot convert default " + literal + " to " + type);
}
return converted.value();
}
private static final class DefaultSpec {
private final String name;
private final Type type;
private final Literal<?> initialDefault;
private final Literal<?> writeDefault;
private final boolean required;
private DefaultSpec(
String name,
Type type,
Literal<?> initialDefault,
Literal<?> writeDefault,
boolean required) {
this.name = name;
this.type = type;
this.initialDefault = initialDefault;
this.writeDefault = writeDefault;
this.required = required;
}
private DefaultSpec withName(String replacementName) {
return new DefaultSpec(
replacementName, type, initialDefault, writeDefault, required);
}
}
private static final class AddedDefault {
private final String parent;
private final DefaultSpec spec;
private String canonicalName;
private AddedDefault(String parent, DefaultSpec spec) {
this.parent = parent;
this.spec = spec;
}
}
}