| // 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; |
| } |
| } |
| } |