From f6550c8ad4f4dfa71bcad6af94a237da9395861f Mon Sep 17 00:00:00 2001 From: Eduard Tudenhoefner Date: Wed, 31 May 2023 08:07:16 +0200 Subject: [PATCH] Core: Move UpdateRequirement out of rest package --- .palantir/revapi.yml | 5 + .../org/apache/iceberg/UpdateRequirement.java | 371 ++++++++++++++++++ .../iceberg/UpdateRequirementParser.java | 264 +++++++++++++ .../apache/iceberg/rest/CatalogHandlers.java | 6 +- .../apache/iceberg/rest/RESTSerializers.java | 48 +++ .../iceberg/rest/RESTTableOperations.java | 20 +- .../requests/CommitTransactionRequest.java | 2 +- .../CommitTransactionRequestParser.java | 60 +-- .../requests/UpdateRequirementParser.java | 5 + .../rest/requests/UpdateTableRequest.java | 36 +- .../requests/UpdateTableRequestParser.java | 110 ++++++ .../TestUpdateRequirementParser.java | 3 +- .../TestCommitTransactionRequestParser.java | 10 +- .../TestUpdateTableRequestParser.java | 222 +++++++++++ 14 files changed, 1083 insertions(+), 79 deletions(-) create mode 100644 core/src/main/java/org/apache/iceberg/UpdateRequirement.java create mode 100644 core/src/main/java/org/apache/iceberg/UpdateRequirementParser.java create mode 100644 core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequestParser.java rename core/src/test/java/org/apache/iceberg/{rest/requests => }/TestUpdateRequirementParser.java (99%) create mode 100644 core/src/test/java/org/apache/iceberg/rest/requests/TestUpdateTableRequestParser.java diff --git a/.palantir/revapi.yml b/.palantir/revapi.yml index ef039675ac03..67f8ff3367fd 100644 --- a/.palantir/revapi.yml +++ b/.palantir/revapi.yml @@ -737,6 +737,11 @@ acceptedBreaks: old: "method void org.apache.iceberg.rest.auth.OAuth2Util.AuthSession::(java.util.Map, java.lang.String, java.lang.String)" justification: "Removing deprecations for 1.3.0" + - code: "java.method.returnTypeTypeParametersChanged" + old: "method java.util.List\ + \ org.apache.iceberg.rest.requests.UpdateTableRequest::requirements()" + new: "method java.util.List org.apache.iceberg.rest.requests.UpdateTableRequest::requirements()" + justification: "Accepted src API break by moving UpdateTableRequest out of REST" apache-iceberg-0.14.0: org.apache.iceberg:iceberg-api: - code: "java.class.defaultSerializationChanged" diff --git a/core/src/main/java/org/apache/iceberg/UpdateRequirement.java b/core/src/main/java/org/apache/iceberg/UpdateRequirement.java new file mode 100644 index 000000000000..e480877962cd --- /dev/null +++ b/core/src/main/java/org/apache/iceberg/UpdateRequirement.java @@ -0,0 +1,371 @@ +/* + * 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; + +import java.util.List; +import java.util.Set; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.CommitFailedException; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; +import org.apache.iceberg.relocated.com.google.common.collect.Lists; +import org.apache.iceberg.relocated.com.google.common.collect.Sets; + +/** Represents a requirement for a {@link MetadataUpdate} */ +public interface UpdateRequirement { + void validate(TableMetadata base); + + static Builder builderForCreate() { + return new Builder(null, false).requireCreate(); + } + + static Builder builderForReplace(TableMetadata base) { + Preconditions.checkNotNull(base, "Cannot create a builder from table metadata: null"); + return new Builder(base, true).requireTableUUID(base.uuid()); + } + + static Builder builderFor(TableMetadata base) { + Preconditions.checkNotNull(base, "Cannot create a builder from table metadata: null"); + return new Builder(base, false).requireTableUUID(base.uuid()); + } + + class AssertTableDoesNotExist implements UpdateRequirement { + public AssertTableDoesNotExist() {} + + @Override + public void validate(TableMetadata base) { + if (base != null) { + throw new CommitFailedException("Requirement failed: table already exists"); + } + } + } + + class AssertTableUUID implements UpdateRequirement { + private final String uuid; + + public AssertTableUUID(String uuid) { + this.uuid = uuid; + } + + public String uuid() { + return uuid; + } + + @Override + public void validate(TableMetadata base) { + if (!uuid.equalsIgnoreCase(base.uuid())) { + throw new CommitFailedException( + "Requirement failed: UUID does not match: expected %s != %s", base.uuid(), uuid); + } + } + } + + class AssertRefSnapshotID implements UpdateRequirement { + private final String name; + private final Long snapshotId; + + public AssertRefSnapshotID(String name, Long snapshotId) { + this.name = name; + this.snapshotId = snapshotId; + } + + public String refName() { + return name; + } + + public Long snapshotId() { + return snapshotId; + } + + @Override + public void validate(TableMetadata base) { + SnapshotRef ref = base.ref(name); + if (ref != null) { + String type = ref.isBranch() ? "branch" : "tag"; + if (snapshotId == null) { + // a null snapshot ID means the ref should not exist already + throw new CommitFailedException( + "Requirement failed: %s %s was created concurrently", type, name); + } else if (snapshotId != ref.snapshotId()) { + throw new CommitFailedException( + "Requirement failed: %s %s has changed: expected id %s != %s", + type, name, snapshotId, ref.snapshotId()); + } + } else if (snapshotId != null) { + throw new CommitFailedException( + "Requirement failed: branch or tag %s is missing, expected %s", name, snapshotId); + } + } + } + + class AssertLastAssignedFieldId implements UpdateRequirement { + private final int lastAssignedFieldId; + + public AssertLastAssignedFieldId(int lastAssignedFieldId) { + this.lastAssignedFieldId = lastAssignedFieldId; + } + + public int lastAssignedFieldId() { + return lastAssignedFieldId; + } + + @Override + public void validate(TableMetadata base) { + if (base != null && base.lastColumnId() != lastAssignedFieldId) { + throw new CommitFailedException( + "Requirement failed: last assigned field id changed: expected id %s != %s", + lastAssignedFieldId, base.lastColumnId()); + } + } + } + + class AssertCurrentSchemaID implements UpdateRequirement { + private final int schemaId; + + public AssertCurrentSchemaID(int schemaId) { + this.schemaId = schemaId; + } + + public int schemaId() { + return schemaId; + } + + @Override + public void validate(TableMetadata base) { + if (schemaId != base.currentSchemaId()) { + throw new CommitFailedException( + "Requirement failed: current schema changed: expected id %s != %s", + schemaId, base.currentSchemaId()); + } + } + } + + class AssertLastAssignedPartitionId implements UpdateRequirement { + private final int lastAssignedPartitionId; + + public AssertLastAssignedPartitionId(int lastAssignedPartitionId) { + this.lastAssignedPartitionId = lastAssignedPartitionId; + } + + public int lastAssignedPartitionId() { + return lastAssignedPartitionId; + } + + @Override + public void validate(TableMetadata base) { + if (base != null && base.lastAssignedPartitionId() != lastAssignedPartitionId) { + throw new CommitFailedException( + "Requirement failed: last assigned partition id changed: expected id %s != %s", + lastAssignedPartitionId, base.lastAssignedPartitionId()); + } + } + } + + class AssertDefaultSpecID implements UpdateRequirement { + private final int specId; + + public AssertDefaultSpecID(int specId) { + this.specId = specId; + } + + public int specId() { + return specId; + } + + @Override + public void validate(TableMetadata base) { + if (specId != base.defaultSpecId()) { + throw new CommitFailedException( + "Requirement failed: default partition spec changed: expected id %s != %s", + specId, base.defaultSpecId()); + } + } + } + + class AssertDefaultSortOrderID implements UpdateRequirement { + private final int sortOrderId; + + public AssertDefaultSortOrderID(int sortOrderId) { + this.sortOrderId = sortOrderId; + } + + public int sortOrderId() { + return sortOrderId; + } + + @Override + public void validate(TableMetadata base) { + if (sortOrderId != base.defaultSortOrderId()) { + throw new CommitFailedException( + "Requirement failed: default sort order changed: expected id %s != %s", + sortOrderId, base.defaultSortOrderId()); + } + } + } + + class Builder { + private final TableMetadata base; + private final ImmutableList.Builder requirements = ImmutableList.builder(); + private final List updates = Lists.newArrayList(); + private final Set changedRefs = Sets.newHashSet(); + private final boolean isReplace; + private TableIdentifier identifier = null; + private boolean addedSchema = false; + private boolean setSchemaId = false; + private boolean addedSpec = false; + private boolean setSpecId = false; + private boolean setOrderId = false; + + public Builder(TableMetadata base, boolean isReplace) { + this.base = base; + this.isReplace = isReplace; + } + + public Builder forTable(TableIdentifier ident) { + Preconditions.checkArgument(null != ident, "Invalid table identifier: null"); + this.identifier = ident; + return this; + } + + private Builder require(UpdateRequirement requirement) { + Preconditions.checkArgument(requirement != null, "Invalid requirement: null"); + requirements.add(requirement); + return this; + } + + private Builder requireCreate() { + return require(new UpdateRequirement.AssertTableDoesNotExist()); + } + + private Builder requireTableUUID(String uuid) { + Preconditions.checkArgument(uuid != null, "Invalid required UUID: null"); + return require(new UpdateRequirement.AssertTableUUID(uuid)); + } + + private Builder requireRefSnapshotId(String ref, Long snapshotId) { + return require(new UpdateRequirement.AssertRefSnapshotID(ref, snapshotId)); + } + + private Builder requireLastAssignedFieldId(int fieldId) { + return require(new UpdateRequirement.AssertLastAssignedFieldId(fieldId)); + } + + private Builder requireCurrentSchemaId(int schemaId) { + return require(new UpdateRequirement.AssertCurrentSchemaID(schemaId)); + } + + private Builder requireLastAssignedPartitionId(int partitionId) { + return require(new UpdateRequirement.AssertLastAssignedPartitionId(partitionId)); + } + + private Builder requireDefaultSpecId(int specId) { + return require(new UpdateRequirement.AssertDefaultSpecID(specId)); + } + + private Builder requireDefaultSortOrderId(int orderId) { + return require(new UpdateRequirement.AssertDefaultSortOrderID(orderId)); + } + + public Builder update(MetadataUpdate update) { + Preconditions.checkArgument(update != null, "Invalid update: null"); + updates.add(update); + + // add requirements based on the change + if (update instanceof MetadataUpdate.SetSnapshotRef) { + update((MetadataUpdate.SetSnapshotRef) update); + } else if (update instanceof MetadataUpdate.AddSchema) { + update((MetadataUpdate.AddSchema) update); + } else if (update instanceof MetadataUpdate.SetCurrentSchema) { + update((MetadataUpdate.SetCurrentSchema) update); + } else if (update instanceof MetadataUpdate.AddPartitionSpec) { + update((MetadataUpdate.AddPartitionSpec) update); + } else if (update instanceof MetadataUpdate.SetDefaultPartitionSpec) { + update((MetadataUpdate.SetDefaultPartitionSpec) update); + } else if (update instanceof MetadataUpdate.SetDefaultSortOrder) { + update((MetadataUpdate.SetDefaultSortOrder) update); + } + + return this; + } + + private void update(MetadataUpdate.SetSnapshotRef setRef) { + // require that the ref is unchanged from the base + String name = setRef.name(); + // add returns true the first time the ref name is added + boolean added = changedRefs.add(name); + if (added && base != null && !isReplace) { + SnapshotRef baseRef = base.ref(name); + // require that the ref does not exist (null) or is the same as the base snapshot + requireRefSnapshotId(name, baseRef != null ? baseRef.snapshotId() : null); + } + } + + private void update(MetadataUpdate.AddSchema update) { + if (!addedSchema) { + if (base != null) { + requireLastAssignedFieldId(base.lastColumnId()); + } + this.addedSchema = true; + } + } + + private void update(MetadataUpdate.SetCurrentSchema update) { + if (!setSchemaId) { + if (base != null && !isReplace) { + // require that the current schema has not changed + requireCurrentSchemaId(base.currentSchemaId()); + } + this.setSchemaId = true; + } + } + + private void update(MetadataUpdate.AddPartitionSpec update) { + if (!addedSpec) { + if (base != null) { + requireLastAssignedPartitionId(base.lastAssignedPartitionId()); + } + this.addedSpec = true; + } + } + + private void update(MetadataUpdate.SetDefaultPartitionSpec update) { + if (!setSpecId) { + if (base != null && !isReplace) { + // require that the default spec has not changed + requireDefaultSpecId(base.defaultSpecId()); + } + this.setSpecId = true; + } + } + + private void update(MetadataUpdate.SetDefaultSortOrder update) { + if (!setOrderId) { + if (base != null && !isReplace) { + // require that the default write order has not changed + requireDefaultSortOrderId(base.defaultSortOrderId()); + } + this.setOrderId = true; + } + } + + public List build() { + return requirements.build(); + } + } +} diff --git a/core/src/main/java/org/apache/iceberg/UpdateRequirementParser.java b/core/src/main/java/org/apache/iceberg/UpdateRequirementParser.java new file mode 100644 index 000000000000..091d4f1fc58c --- /dev/null +++ b/core/src/main/java/org/apache/iceberg/UpdateRequirementParser.java @@ -0,0 +1,264 @@ +/* + * 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; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; +import org.apache.iceberg.util.JsonUtil; + +public class UpdateRequirementParser { + + private UpdateRequirementParser() {} + + private static final String TYPE = "type"; + + // assertion types + static final String ASSERT_TABLE_UUID = "assert-table-uuid"; + static final String ASSERT_TABLE_DOES_NOT_EXIST = "assert-create"; + static final String ASSERT_REF_SNAPSHOT_ID = "assert-ref-snapshot-id"; + static final String ASSERT_LAST_ASSIGNED_FIELD_ID = "assert-last-assigned-field-id"; + static final String ASSERT_CURRENT_SCHEMA_ID = "assert-current-schema-id"; + static final String ASSERT_LAST_ASSIGNED_PARTITION_ID = "assert-last-assigned-partition-id"; + static final String ASSERT_DEFAULT_SPEC_ID = "assert-default-spec-id"; + static final String ASSERT_DEFAULT_SORT_ORDER_ID = "assert-default-sort-order-id"; + + // AssertTableUUID + private static final String UUID = "uuid"; + + // AssertRefSnapshotID + private static final String NAME = "ref"; + private static final String SNAPSHOT_ID = "snapshot-id"; + + // AssertLastAssignedFieldId + private static final String LAST_ASSIGNED_FIELD_ID = "last-assigned-field-id"; + + // AssertCurrentSchemaID + private static final String SCHEMA_ID = "current-schema-id"; + + // AssertLastAssignedPartitionId + private static final String LAST_ASSIGNED_PARTITION_ID = "last-assigned-partition-id"; + + // AssertDefaultSpecID + private static final String SPEC_ID = "default-spec-id"; + + // AssertDefaultSortOrderID + private static final String SORT_ORDER_ID = "default-sort-order-id"; + + private static final Map, String> TYPES = + ImmutableMap., String>builder() + .put(UpdateRequirement.AssertTableUUID.class, ASSERT_TABLE_UUID) + .put(UpdateRequirement.AssertTableDoesNotExist.class, ASSERT_TABLE_DOES_NOT_EXIST) + .put(UpdateRequirement.AssertRefSnapshotID.class, ASSERT_REF_SNAPSHOT_ID) + .put(UpdateRequirement.AssertLastAssignedFieldId.class, ASSERT_LAST_ASSIGNED_FIELD_ID) + .put(UpdateRequirement.AssertCurrentSchemaID.class, ASSERT_CURRENT_SCHEMA_ID) + .put( + UpdateRequirement.AssertLastAssignedPartitionId.class, + ASSERT_LAST_ASSIGNED_PARTITION_ID) + .put(UpdateRequirement.AssertDefaultSpecID.class, ASSERT_DEFAULT_SPEC_ID) + .put(UpdateRequirement.AssertDefaultSortOrderID.class, ASSERT_DEFAULT_SORT_ORDER_ID) + .buildOrThrow(); + + public static String toJson(UpdateRequirement updateRequirement) { + return toJson(updateRequirement, false); + } + + public static String toJson(UpdateRequirement updateRequirement, boolean pretty) { + return JsonUtil.generate(gen -> toJson(updateRequirement, gen), pretty); + } + + public static void toJson(UpdateRequirement updateRequirement, JsonGenerator generator) + throws IOException { + String requirementType = TYPES.get(updateRequirement.getClass()); + + generator.writeStartObject(); + generator.writeStringField(TYPE, requirementType); + + switch (requirementType) { + case ASSERT_TABLE_DOES_NOT_EXIST: + // No fields beyond the requirement itself + break; + case ASSERT_TABLE_UUID: + writeAssertTableUUID((UpdateRequirement.AssertTableUUID) updateRequirement, generator); + break; + case ASSERT_REF_SNAPSHOT_ID: + writeAssertRefSnapshotId( + (UpdateRequirement.AssertRefSnapshotID) updateRequirement, generator); + break; + case ASSERT_LAST_ASSIGNED_FIELD_ID: + writeAssertLastAssignedFieldId( + (UpdateRequirement.AssertLastAssignedFieldId) updateRequirement, generator); + break; + case ASSERT_LAST_ASSIGNED_PARTITION_ID: + writeAssertLastAssignedPartitionId( + (UpdateRequirement.AssertLastAssignedPartitionId) updateRequirement, generator); + break; + case ASSERT_CURRENT_SCHEMA_ID: + writeAssertCurrentSchemaId( + (UpdateRequirement.AssertCurrentSchemaID) updateRequirement, generator); + break; + case ASSERT_DEFAULT_SPEC_ID: + writeAssertDefaultSpecId( + (UpdateRequirement.AssertDefaultSpecID) updateRequirement, generator); + break; + case ASSERT_DEFAULT_SORT_ORDER_ID: + writeAssertDefaultSortOrderId( + (UpdateRequirement.AssertDefaultSortOrderID) updateRequirement, generator); + break; + default: + throw new IllegalArgumentException( + String.format( + "Cannot convert update requirement to json. Unrecognized type: %s", + requirementType)); + } + + generator.writeEndObject(); + } + + /** + * Read MetadataUpdate from a JSON string. + * + * @param json a JSON string of a MetadataUpdate + * @return a MetadataUpdate object + */ + public static UpdateRequirement fromJson(String json) { + return JsonUtil.parse(json, UpdateRequirementParser::fromJson); + } + + public static UpdateRequirement fromJson(JsonNode jsonNode) { + Preconditions.checkArgument( + jsonNode != null && jsonNode.isObject(), + "Cannot parse update requirement from non-object value: %s", + jsonNode); + Preconditions.checkArgument( + jsonNode.hasNonNull(TYPE), "Cannot parse update requirement. Missing field: type"); + String type = JsonUtil.getString(TYPE, jsonNode).toLowerCase(Locale.ROOT); + + switch (type) { + case ASSERT_TABLE_DOES_NOT_EXIST: + return readAssertTableDoesNotExist(jsonNode); + case ASSERT_TABLE_UUID: + return readAssertTableUUID(jsonNode); + case ASSERT_REF_SNAPSHOT_ID: + return readAssertRefSnapshotId(jsonNode); + case ASSERT_LAST_ASSIGNED_FIELD_ID: + return readAssertLastAssignedFieldId(jsonNode); + case ASSERT_LAST_ASSIGNED_PARTITION_ID: + return readAssertLastAssignedPartitionId(jsonNode); + case ASSERT_CURRENT_SCHEMA_ID: + return readAssertCurrentSchemaId(jsonNode); + case ASSERT_DEFAULT_SPEC_ID: + return readAssertDefaultSpecId(jsonNode); + case ASSERT_DEFAULT_SORT_ORDER_ID: + return readAssertDefaultSortOrderId(jsonNode); + default: + throw new UnsupportedOperationException( + String.format("Unrecognized update requirement. Cannot convert to json: %s", type)); + } + } + + private static void writeAssertTableUUID( + UpdateRequirement.AssertTableUUID requirement, JsonGenerator gen) throws IOException { + gen.writeStringField(UUID, requirement.uuid()); + } + + private static void writeAssertRefSnapshotId( + UpdateRequirement.AssertRefSnapshotID requirement, JsonGenerator gen) throws IOException { + gen.writeStringField(NAME, requirement.refName()); + if (requirement.snapshotId() != null) { + gen.writeNumberField(SNAPSHOT_ID, requirement.snapshotId()); + } else { + gen.writeNullField(SNAPSHOT_ID); + } + } + + private static void writeAssertLastAssignedFieldId( + UpdateRequirement.AssertLastAssignedFieldId requirement, JsonGenerator gen) + throws IOException { + gen.writeNumberField(LAST_ASSIGNED_FIELD_ID, requirement.lastAssignedFieldId()); + } + + private static void writeAssertLastAssignedPartitionId( + UpdateRequirement.AssertLastAssignedPartitionId requirement, JsonGenerator gen) + throws IOException { + gen.writeNumberField(LAST_ASSIGNED_PARTITION_ID, requirement.lastAssignedPartitionId()); + } + + private static void writeAssertCurrentSchemaId( + UpdateRequirement.AssertCurrentSchemaID requirement, JsonGenerator gen) throws IOException { + gen.writeNumberField(SCHEMA_ID, requirement.schemaId()); + } + + private static void writeAssertDefaultSpecId( + UpdateRequirement.AssertDefaultSpecID requirement, JsonGenerator gen) throws IOException { + gen.writeNumberField(SPEC_ID, requirement.specId()); + } + + private static void writeAssertDefaultSortOrderId( + UpdateRequirement.AssertDefaultSortOrderID requirement, JsonGenerator gen) + throws IOException { + gen.writeNumberField(SORT_ORDER_ID, requirement.sortOrderId()); + } + + @SuppressWarnings( + "unused") // Keep same signature in case this requirement class evolves and gets fields + private static UpdateRequirement readAssertTableDoesNotExist(JsonNode node) { + return new UpdateRequirement.AssertTableDoesNotExist(); + } + + private static UpdateRequirement readAssertTableUUID(JsonNode node) { + String uuid = JsonUtil.getString(UUID, node); + return new UpdateRequirement.AssertTableUUID(uuid); + } + + private static UpdateRequirement readAssertRefSnapshotId(JsonNode node) { + String name = JsonUtil.getString(NAME, node); + Long snapshotId = JsonUtil.getLongOrNull(SNAPSHOT_ID, node); + return new UpdateRequirement.AssertRefSnapshotID(name, snapshotId); + } + + private static UpdateRequirement readAssertLastAssignedFieldId(JsonNode node) { + int lastAssignedFieldId = JsonUtil.getInt(LAST_ASSIGNED_FIELD_ID, node); + return new UpdateRequirement.AssertLastAssignedFieldId(lastAssignedFieldId); + } + + private static UpdateRequirement readAssertCurrentSchemaId(JsonNode node) { + int schemaId = JsonUtil.getInt(SCHEMA_ID, node); + return new UpdateRequirement.AssertCurrentSchemaID(schemaId); + } + + private static UpdateRequirement readAssertLastAssignedPartitionId(JsonNode node) { + int lastAssignedPartitionId = JsonUtil.getInt(LAST_ASSIGNED_PARTITION_ID, node); + return new UpdateRequirement.AssertLastAssignedPartitionId(lastAssignedPartitionId); + } + + private static UpdateRequirement readAssertDefaultSpecId(JsonNode node) { + int specId = JsonUtil.getInt(SPEC_ID, node); + return new UpdateRequirement.AssertDefaultSpecID(specId); + } + + private static UpdateRequirement readAssertDefaultSortOrderId(JsonNode node) { + int sortOrderId = JsonUtil.getInt(SORT_ORDER_ID, node); + return new UpdateRequirement.AssertDefaultSortOrderID(sortOrderId); + } +} diff --git a/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java b/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java index d297fc738317..8073b05a8139 100644 --- a/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java +++ b/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java @@ -287,15 +287,15 @@ private static boolean isCreate(UpdateTableRequest request) { boolean isCreate = request.requirements().stream() .anyMatch( - UpdateTableRequest.UpdateRequirement.AssertTableDoesNotExist.class::isInstance); + org.apache.iceberg.UpdateRequirement.AssertTableDoesNotExist.class::isInstance); if (isCreate) { - List invalidRequirements = + List invalidRequirements = request.requirements().stream() .filter( req -> !(req - instanceof UpdateTableRequest.UpdateRequirement.AssertTableDoesNotExist)) + instanceof org.apache.iceberg.UpdateRequirement.AssertTableDoesNotExist)) .collect(Collectors.toList()); Preconditions.checkArgument( invalidRequirements.isEmpty(), "Invalid create requirements: %s", invalidRequirements); diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java b/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java index 246b6c92602e..9beb68b63289 100644 --- a/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java +++ b/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java @@ -48,7 +48,9 @@ import org.apache.iceberg.rest.requests.ReportMetricsRequest; import org.apache.iceberg.rest.requests.ReportMetricsRequestParser; import org.apache.iceberg.rest.requests.UpdateRequirementParser; +import org.apache.iceberg.rest.requests.UpdateTableRequest; import org.apache.iceberg.rest.requests.UpdateTableRequest.UpdateRequirement; +import org.apache.iceberg.rest.requests.UpdateTableRequestParser; import org.apache.iceberg.rest.responses.ErrorResponse; import org.apache.iceberg.rest.responses.ErrorResponseParser; import org.apache.iceberg.rest.responses.OAuthTokenResponse; @@ -79,6 +81,8 @@ public static void registerAll(ObjectMapper mapper) { .addDeserializer(TableMetadata.class, new TableMetadataDeserializer()) .addSerializer(UpdateRequirement.class, new UpdateRequirementSerializer()) .addDeserializer(UpdateRequirement.class, new UpdateRequirementDeserializer()) + .addSerializer(org.apache.iceberg.UpdateRequirement.class, new UpdateReqSerializer()) + .addDeserializer(org.apache.iceberg.UpdateRequirement.class, new UpdateReqDeserializer()) .addSerializer(OAuthTokenResponse.class, new OAuthTokenResponseSerializer()) .addDeserializer(OAuthTokenResponse.class, new OAuthTokenResponseDeserializer()) .addSerializer(ReportMetricsRequest.class, new ReportMetricsRequestSerializer<>()) @@ -92,6 +96,8 @@ public static void registerAll(ObjectMapper mapper) { mapper.registerModule(module); } + /** @deprecated will be removed in 1.4.0, use {@link UpdateReqDeserializer} instead. */ + @Deprecated public static class UpdateRequirementDeserializer extends JsonDeserializer { @Override public UpdateRequirement deserialize(JsonParser p, DeserializationContext ctxt) @@ -101,6 +107,8 @@ public UpdateRequirement deserialize(JsonParser p, DeserializationContext ctxt) } } + /** @deprecated will be removed in 1.4.0, use {@link UpdateReqSerializer} instead. */ + @Deprecated public static class UpdateRequirementSerializer extends JsonSerializer { @Override public void serialize( @@ -110,6 +118,28 @@ public void serialize( } } + public static class UpdateReqDeserializer + extends JsonDeserializer { + @Override + public org.apache.iceberg.UpdateRequirement deserialize( + JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + return org.apache.iceberg.UpdateRequirementParser.fromJson(node); + } + } + + public static class UpdateReqSerializer + extends JsonSerializer { + @Override + public void serialize( + org.apache.iceberg.UpdateRequirement value, + JsonGenerator gen, + SerializerProvider serializers) + throws IOException { + org.apache.iceberg.UpdateRequirementParser.toJson(value, gen); + } + } + public static class TableMetadataDeserializer extends JsonDeserializer { @Override public TableMetadata deserialize(JsonParser p, DeserializationContext context) @@ -305,4 +335,22 @@ public CommitTransactionRequest deserialize(JsonParser p, DeserializationContext return CommitTransactionRequestParser.fromJson(jsonNode); } } + + public static class UpdateTableRequestSerializer extends JsonSerializer { + @Override + public void serialize( + UpdateTableRequest request, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + UpdateTableRequestParser.toJson(request, gen); + } + } + + public static class UpdateTableRequestDeserializer extends JsonDeserializer { + @Override + public UpdateTableRequest deserialize(JsonParser p, DeserializationContext context) + throws IOException { + JsonNode jsonNode = p.getCodec().readTree(p); + return UpdateTableRequestParser.fromJson(jsonNode); + } + } } diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTTableOperations.java b/core/src/main/java/org/apache/iceberg/rest/RESTTableOperations.java index 0841de763bf1..330286ee61c9 100644 --- a/core/src/main/java/org/apache/iceberg/rest/RESTTableOperations.java +++ b/core/src/main/java/org/apache/iceberg/rest/RESTTableOperations.java @@ -28,6 +28,7 @@ import org.apache.iceberg.TableMetadata; import org.apache.iceberg.TableOperations; import org.apache.iceberg.TableProperties; +import org.apache.iceberg.UpdateRequirement; import org.apache.iceberg.encryption.EncryptionManager; import org.apache.iceberg.io.FileIO; import org.apache.iceberg.io.LocationProvider; @@ -101,14 +102,14 @@ public TableMetadata refresh() { @Override public void commit(TableMetadata base, TableMetadata metadata) { - UpdateTableRequest.Builder requestBuilder; + UpdateRequirement.Builder requestBuilder; List baseChanges; Consumer errorHandler; switch (updateType) { case CREATE: Preconditions.checkState( base == null, "Invalid base metadata for create transaction, expected null: %s", base); - requestBuilder = UpdateTableRequest.builderForCreate(); + requestBuilder = UpdateRequirement.builderForCreate(); baseChanges = createChanges; errorHandler = ErrorHandlers.tableErrorHandler(); // throws NoSuchTableException break; @@ -116,14 +117,14 @@ public void commit(TableMetadata base, TableMetadata metadata) { case REPLACE: Preconditions.checkState(base != null, "Invalid base metadata: null"); // use the original replace base metadata because the transaction will refresh - requestBuilder = UpdateTableRequest.builderForReplace(replaceBase); + requestBuilder = UpdateRequirement.builderForReplace(replaceBase); baseChanges = createChanges; errorHandler = ErrorHandlers.tableCommitHandler(); break; case SIMPLE: Preconditions.checkState(base != null, "Invalid base metadata: null"); - requestBuilder = UpdateTableRequest.builderFor(base); + requestBuilder = UpdateRequirement.builderFor(base); baseChanges = ImmutableList.of(); errorHandler = ErrorHandlers.tableCommitHandler(); break; @@ -133,9 +134,14 @@ public void commit(TableMetadata base, TableMetadata metadata) { String.format("Update type %s is not supported", updateType)); } - baseChanges.forEach(requestBuilder::update); - metadata.changes().forEach(requestBuilder::update); - UpdateTableRequest request = requestBuilder.build(); + List updates = + ImmutableList.builder() + .addAll(baseChanges) + .addAll(metadata.changes()) + .build(); + updates.forEach(requestBuilder::update); + List requirements = requestBuilder.build(); + UpdateTableRequest request = new UpdateTableRequest(requirements, updates); // the error handler will throw necessary exceptions like CommitFailedException and // UnknownCommitStateException diff --git a/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequest.java b/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequest.java index 34e3acc67579..ef41664a078b 100644 --- a/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequest.java +++ b/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequest.java @@ -41,7 +41,7 @@ public void validate() { Preconditions.checkArgument(!tableChanges.isEmpty(), "Invalid table changes: empty"); for (UpdateTableRequest tableChange : tableChanges) { Preconditions.checkArgument( - null != tableChange.identifier(), "Invalid table changes: table identifier required"); + null != tableChange.identifier(), "Invalid table changes: table identifier is required"); } } } diff --git a/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequestParser.java b/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequestParser.java index 8a191821aa8d..a2519ed0f570 100644 --- a/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequestParser.java +++ b/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequestParser.java @@ -22,19 +22,12 @@ import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; import java.util.List; -import org.apache.iceberg.MetadataUpdate; -import org.apache.iceberg.MetadataUpdateParser; -import org.apache.iceberg.catalog.TableIdentifier; -import org.apache.iceberg.catalog.TableIdentifierParser; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.relocated.com.google.common.collect.Lists; import org.apache.iceberg.util.JsonUtil; public class CommitTransactionRequestParser { private static final String TABLE_CHANGES = "table-changes"; - private static final String IDENTIFIER = "identifier"; - private static final String REQUIREMENTS = "requirements"; - private static final String UPDATES = "updates"; private CommitTransactionRequestParser() {} @@ -48,31 +41,14 @@ public static String toJson(CommitTransactionRequest request, boolean pretty) { public static void toJson(CommitTransactionRequest request, JsonGenerator gen) throws IOException { - Preconditions.checkArgument(null != request, "Invalid commit tx request: null"); + Preconditions.checkArgument(null != request, "Invalid commit transaction request: null"); gen.writeStartObject(); gen.writeFieldName(TABLE_CHANGES); gen.writeStartArray(); for (UpdateTableRequest tableChange : request.tableChanges()) { - gen.writeStartObject(); - - gen.writeFieldName(IDENTIFIER); - TableIdentifierParser.toJson(tableChange.identifier(), gen); - - gen.writeArrayFieldStart(REQUIREMENTS); - for (UpdateTableRequest.UpdateRequirement updateRequirement : tableChange.requirements()) { - UpdateRequirementParser.toJson(updateRequirement, gen); - } - gen.writeEndArray(); - - gen.writeArrayFieldStart(UPDATES); - for (MetadataUpdate metadataUpdate : tableChange.updates()) { - MetadataUpdateParser.toJson(metadataUpdate, gen); - } - gen.writeEndArray(); - - gen.writeEndObject(); + UpdateTableRequestParser.toJson(tableChange, gen); } gen.writeEndArray(); @@ -84,41 +60,17 @@ public static CommitTransactionRequest fromJson(String json) { } public static CommitTransactionRequest fromJson(JsonNode json) { - Preconditions.checkArgument(null != json, "Cannot parse commit tx request from null object"); + Preconditions.checkArgument( + null != json, "Cannot parse commit transaction request from null object"); List tableChanges = Lists.newArrayList(); JsonNode changes = JsonUtil.get(TABLE_CHANGES, json); Preconditions.checkArgument( - changes.isArray(), "Cannot parse commit tx request from non-array: %s", changes); + changes.isArray(), "Cannot parse commit transaction request from non-array: %s", changes); for (JsonNode node : changes) { - TableIdentifier identifier = null; - List requirements = Lists.newArrayList(); - List updates = Lists.newArrayList(); - - if (node.hasNonNull(IDENTIFIER)) { - identifier = TableIdentifierParser.fromJson(JsonUtil.get(IDENTIFIER, node)); - } - - if (node.hasNonNull(REQUIREMENTS)) { - JsonNode requirementsNode = JsonUtil.get(REQUIREMENTS, node); - Preconditions.checkArgument( - requirementsNode.isArray(), - "Cannot parse requirements from non-array: %s", - requirementsNode); - requirementsNode.forEach(req -> requirements.add(UpdateRequirementParser.fromJson(req))); - } - - if (node.hasNonNull(UPDATES)) { - JsonNode updatesNode = JsonUtil.get(UPDATES, node); - Preconditions.checkArgument( - updatesNode.isArray(), "Cannot parse metadata updates from non-array: %s", updatesNode); - - updatesNode.forEach(update -> updates.add(MetadataUpdateParser.fromJson(update))); - } - - tableChanges.add(new UpdateTableRequest(identifier, requirements, updates)); + tableChanges.add(UpdateTableRequestParser.fromJson(node)); } return new CommitTransactionRequest(tableChanges); diff --git a/core/src/main/java/org/apache/iceberg/rest/requests/UpdateRequirementParser.java b/core/src/main/java/org/apache/iceberg/rest/requests/UpdateRequirementParser.java index 443ae87b9eee..8ee4012ff204 100644 --- a/core/src/main/java/org/apache/iceberg/rest/requests/UpdateRequirementParser.java +++ b/core/src/main/java/org/apache/iceberg/rest/requests/UpdateRequirementParser.java @@ -28,6 +28,11 @@ import org.apache.iceberg.rest.requests.UpdateTableRequest.UpdateRequirement; import org.apache.iceberg.util.JsonUtil; +/** + * @deprecated will be removed in 1.4.0 - use {@link org.apache.iceberg.UpdateRequirementParser} + * instead. + */ +@Deprecated public class UpdateRequirementParser { private UpdateRequirementParser() {} diff --git a/core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequest.java b/core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequest.java index cac3a99dcd17..3fc52a62f126 100644 --- a/core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequest.java +++ b/core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequest.java @@ -35,21 +35,22 @@ public class UpdateTableRequest implements RESTRequest { private TableIdentifier identifier; - private List requirements; + private List requirements; private List updates; public UpdateTableRequest() { // needed for Jackson deserialization } - public UpdateTableRequest(List requirements, List updates) { + public UpdateTableRequest( + List requirements, List updates) { this.requirements = requirements; this.updates = updates; } UpdateTableRequest( TableIdentifier identifier, - List requirements, + List requirements, List updates) { this(requirements, updates); this.identifier = identifier; @@ -58,7 +59,7 @@ public UpdateTableRequest(List requirements, List requirements() { + public List requirements() { return requirements != null ? requirements : ImmutableList.of(); } @@ -78,23 +79,41 @@ public String toString() { .toString(); } + /** + * @deprecated will be removed in 1.4.0, use {@link org.apache.iceberg.UpdateRequirement} instead. + */ + @Deprecated public static Builder builderForCreate() { return new Builder(null, false).requireCreate(); } + /** + * @deprecated will be removed in 1.4.0, use {@link org.apache.iceberg.UpdateRequirement} instead. + */ + @Deprecated public static Builder builderForReplace(TableMetadata base) { Preconditions.checkNotNull(base, "Cannot create a builder from table metadata: null"); return new Builder(base, true).requireTableUUID(base.uuid()); } + /** + * @deprecated will be removed in 1.4.0, use {@link org.apache.iceberg.UpdateRequirement} instead. + */ + @Deprecated public static Builder builderFor(TableMetadata base) { Preconditions.checkNotNull(base, "Cannot create a builder from table metadata: null"); return new Builder(base, false).requireTableUUID(base.uuid()); } + /** + * @deprecated will be removed in 1.4.0, use {@link org.apache.iceberg.UpdateRequirement.Builder} + * instead. + */ + @Deprecated public static class Builder { private final TableMetadata base; - private final ImmutableList.Builder requirements = ImmutableList.builder(); + private final ImmutableList.Builder requirements = + ImmutableList.builder(); private final List updates = Lists.newArrayList(); private final Set changedRefs = Sets.newHashSet(); private final boolean isReplace; @@ -235,8 +254,11 @@ public UpdateTableRequest build() { } } - public interface UpdateRequirement { - void validate(TableMetadata base); + /** + * @deprecated will be removed in 1.4.0, use {@link org.apache.iceberg.UpdateRequirement} instead. + */ + @Deprecated + public interface UpdateRequirement extends org.apache.iceberg.UpdateRequirement { class AssertTableDoesNotExist implements UpdateRequirement { AssertTableDoesNotExist() {} diff --git a/core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequestParser.java b/core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequestParser.java new file mode 100644 index 000000000000..043921f2fa93 --- /dev/null +++ b/core/src/main/java/org/apache/iceberg/rest/requests/UpdateTableRequestParser.java @@ -0,0 +1,110 @@ +/* + * 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.rest.requests; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.util.List; +import org.apache.iceberg.MetadataUpdate; +import org.apache.iceberg.MetadataUpdateParser; +import org.apache.iceberg.UpdateRequirement; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.catalog.TableIdentifierParser; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.relocated.com.google.common.collect.Lists; +import org.apache.iceberg.util.JsonUtil; + +public class UpdateTableRequestParser { + + private static final String IDENTIFIER = "identifier"; + private static final String REQUIREMENTS = "requirements"; + private static final String UPDATES = "updates"; + + private UpdateTableRequestParser() {} + + public static String toJson(UpdateTableRequest request) { + return toJson(request, false); + } + + public static String toJson(UpdateTableRequest request, boolean pretty) { + return JsonUtil.generate(gen -> toJson(request, gen), pretty); + } + + public static void toJson(UpdateTableRequest request, JsonGenerator gen) throws IOException { + Preconditions.checkArgument(null != request, "Invalid update table request: null"); + + gen.writeStartObject(); + + if (null != request.identifier()) { + gen.writeFieldName(IDENTIFIER); + TableIdentifierParser.toJson(request.identifier(), gen); + } + + gen.writeArrayFieldStart(REQUIREMENTS); + for (UpdateRequirement updateRequirement : request.requirements()) { + org.apache.iceberg.UpdateRequirementParser.toJson(updateRequirement, gen); + } + gen.writeEndArray(); + + gen.writeArrayFieldStart(UPDATES); + for (MetadataUpdate metadataUpdate : request.updates()) { + MetadataUpdateParser.toJson(metadataUpdate, gen); + } + gen.writeEndArray(); + + gen.writeEndObject(); + } + + public static UpdateTableRequest fromJson(String json) { + return JsonUtil.parse(json, UpdateTableRequestParser::fromJson); + } + + public static UpdateTableRequest fromJson(JsonNode json) { + Preconditions.checkArgument(null != json, "Cannot parse update table request from null object"); + + TableIdentifier identifier = null; + List requirements = Lists.newArrayList(); + List updates = Lists.newArrayList(); + + if (json.hasNonNull(IDENTIFIER)) { + identifier = TableIdentifierParser.fromJson(JsonUtil.get(IDENTIFIER, json)); + } + + if (json.hasNonNull(REQUIREMENTS)) { + JsonNode requirementsNode = JsonUtil.get(REQUIREMENTS, json); + Preconditions.checkArgument( + requirementsNode.isArray(), + "Cannot parse requirements from non-array: %s", + requirementsNode); + requirementsNode.forEach( + req -> requirements.add(org.apache.iceberg.UpdateRequirementParser.fromJson(req))); + } + + if (json.hasNonNull(UPDATES)) { + JsonNode updatesNode = JsonUtil.get(UPDATES, json); + Preconditions.checkArgument( + updatesNode.isArray(), "Cannot parse metadata updates from non-array: %s", updatesNode); + + updatesNode.forEach(update -> updates.add(MetadataUpdateParser.fromJson(update))); + } + + return new UpdateTableRequest(identifier, requirements, updates); + } +} diff --git a/core/src/test/java/org/apache/iceberg/rest/requests/TestUpdateRequirementParser.java b/core/src/test/java/org/apache/iceberg/TestUpdateRequirementParser.java similarity index 99% rename from core/src/test/java/org/apache/iceberg/rest/requests/TestUpdateRequirementParser.java rename to core/src/test/java/org/apache/iceberg/TestUpdateRequirementParser.java index 9af2b1d6588f..23f5c0c42598 100644 --- a/core/src/test/java/org/apache/iceberg/rest/requests/TestUpdateRequirementParser.java +++ b/core/src/test/java/org/apache/iceberg/TestUpdateRequirementParser.java @@ -16,11 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.iceberg.rest.requests; +package org.apache.iceberg; import java.util.List; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; -import org.apache.iceberg.rest.requests.UpdateTableRequest.UpdateRequirement; import org.assertj.core.api.Assertions; import org.junit.Assert; import org.junit.Test; diff --git a/core/src/test/java/org/apache/iceberg/rest/requests/TestCommitTransactionRequestParser.java b/core/src/test/java/org/apache/iceberg/rest/requests/TestCommitTransactionRequestParser.java index 776a091f821d..6b61efbf176e 100644 --- a/core/src/test/java/org/apache/iceberg/rest/requests/TestCommitTransactionRequestParser.java +++ b/core/src/test/java/org/apache/iceberg/rest/requests/TestCommitTransactionRequestParser.java @@ -23,9 +23,9 @@ import com.fasterxml.jackson.databind.JsonNode; import org.apache.iceberg.MetadataUpdate; +import org.apache.iceberg.UpdateRequirement; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; -import org.apache.iceberg.rest.requests.UpdateTableRequest.UpdateRequirement; import org.junit.jupiter.api.Test; public class TestCommitTransactionRequestParser { @@ -34,11 +34,11 @@ public class TestCommitTransactionRequestParser { public void nullAndEmptyCheck() { assertThatThrownBy(() -> CommitTransactionRequestParser.toJson(null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid commit tx request: null"); + .hasMessage("Invalid commit transaction request: null"); assertThatThrownBy(() -> CommitTransactionRequestParser.fromJson((JsonNode) null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot parse commit tx request from null object"); + .hasMessage("Cannot parse commit transaction request from null object"); assertThatThrownBy(() -> CommitTransactionRequestParser.fromJson("{}")) .isInstanceOf(IllegalArgumentException.class) @@ -46,7 +46,7 @@ public void nullAndEmptyCheck() { assertThatThrownBy(() -> CommitTransactionRequestParser.fromJson("{\"table-changes\":{}}")) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot parse commit tx request from non-array: {}"); + .hasMessage("Cannot parse commit transaction request from non-array: {}"); assertThatThrownBy(() -> CommitTransactionRequestParser.fromJson("{\"table-changes\":[]}")) .isInstanceOf(IllegalArgumentException.class) @@ -60,7 +60,7 @@ public void invalidTableIdentifier() { CommitTransactionRequestParser.fromJson( "{\"table-changes\":[{\"ns1.table1\" : \"ns1.table1\"}]}")) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid table changes: table identifier required"); + .hasMessage("Invalid table changes: table identifier is required"); assertThatThrownBy( () -> diff --git a/core/src/test/java/org/apache/iceberg/rest/requests/TestUpdateTableRequestParser.java b/core/src/test/java/org/apache/iceberg/rest/requests/TestUpdateTableRequestParser.java new file mode 100644 index 000000000000..94599f9d1d23 --- /dev/null +++ b/core/src/test/java/org/apache/iceberg/rest/requests/TestUpdateTableRequestParser.java @@ -0,0 +1,222 @@ +/* + * 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.rest.requests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.iceberg.MetadataUpdate; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.Test; + +public class TestUpdateTableRequestParser { + + @Test + public void nullAndEmptyCheck() { + assertThatThrownBy(() -> UpdateTableRequestParser.toJson(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid update table request: null"); + + assertThatThrownBy(() -> UpdateTableRequestParser.fromJson((JsonNode) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse update table request from null object"); + + UpdateTableRequest request = UpdateTableRequestParser.fromJson("{}"); + assertThat(request.identifier()).isNull(); + assertThat(request.updates()).isEmpty(); + assertThat(request.requirements()).isEmpty(); + } + + @Test + public void invalidTableIdentifier() { + // table identifier is optional + UpdateTableRequest request = + UpdateTableRequestParser.fromJson("{\"requirements\" : [], \"updates\" : []}"); + assertThat(request.identifier()).isNull(); + + assertThatThrownBy(() -> UpdateTableRequestParser.fromJson("{\"identifier\" : {}}}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse missing string: name"); + + assertThatThrownBy( + () -> UpdateTableRequestParser.fromJson("{\"identifier\" : { \"name\": 23}}}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse to a string value: name: 23"); + } + + @Test + public void invalidRequirements() { + assertThatThrownBy( + () -> + UpdateTableRequestParser.fromJson( + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"}," + + "\"requirements\":[23],\"updates\":[]}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse update requirement from non-object value: 23"); + + assertThatThrownBy( + () -> + UpdateTableRequestParser.fromJson( + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"}," + + "\"requirements\":[{}],\"updates\":[]}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse update requirement. Missing field: type"); + + assertThatThrownBy( + () -> + UpdateTableRequestParser.fromJson( + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"}," + + "\"requirements\":[{\"type\":\"assert-table-uuid\"}],\"updates\":[]}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse missing string: uuid"); + } + + @Test + public void invalidMetadataUpdates() { + assertThatThrownBy( + () -> + UpdateTableRequestParser.fromJson( + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"}," + + "\"requirements\":[],\"updates\":[23]}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse metadata update from non-object value: 23"); + + assertThatThrownBy( + () -> + UpdateTableRequestParser.fromJson( + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"}," + + "\"requirements\":[],\"updates\":[{}]}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse metadata update. Missing field: action"); + + assertThatThrownBy( + () -> + UpdateTableRequestParser.fromJson( + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"}," + + "\"requirements\":[],\"updates\":[{\"action\":\"assign-uuid\"}]}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse missing string: uuid"); + } + + @Test + public void roundTripSerde() { + String uuid = "2cc52516-5e73-41f2-b139-545d41a4e151"; + UpdateTableRequest request = + new UpdateTableRequest( + TableIdentifier.of("ns1", "table1"), + ImmutableList.of( + new org.apache.iceberg.UpdateRequirement.AssertTableUUID(uuid), + new org.apache.iceberg.UpdateRequirement.AssertTableDoesNotExist()), + ImmutableList.of( + new MetadataUpdate.AssignUUID(uuid), new MetadataUpdate.SetCurrentSchema(23))); + + String expectedJson = + "{\n" + + " \"identifier\" : {\n" + + " \"namespace\" : [ \"ns1\" ],\n" + + " \"name\" : \"table1\"\n" + + " },\n" + + " \"requirements\" : [ {\n" + + " \"type\" : \"assert-table-uuid\",\n" + + " \"uuid\" : \"2cc52516-5e73-41f2-b139-545d41a4e151\"\n" + + " }, {\n" + + " \"type\" : \"assert-create\"\n" + + " } ],\n" + + " \"updates\" : [ {\n" + + " \"action\" : \"assign-uuid\",\n" + + " \"uuid\" : \"2cc52516-5e73-41f2-b139-545d41a4e151\"\n" + + " }, {\n" + + " \"action\" : \"set-current-schema\",\n" + + " \"schema-id\" : 23\n" + + " } ]\n" + + "}"; + + String json = UpdateTableRequestParser.toJson(request, true); + assertThat(json).isEqualTo(expectedJson); + + // can't do an equality comparison on UpdateTableRequest because updates/requirements + // don't implement equals/hashcode + assertThat(UpdateTableRequestParser.toJson(UpdateTableRequestParser.fromJson(json), true)) + .isEqualTo(expectedJson); + } + + @Test + public void roundTripSerdeWithoutIdentifier() { + String uuid = "2cc52516-5e73-41f2-b139-545d41a4e151"; + UpdateTableRequest request = + new UpdateTableRequest( + ImmutableList.of( + new org.apache.iceberg.UpdateRequirement.AssertTableUUID(uuid), + new org.apache.iceberg.UpdateRequirement.AssertTableDoesNotExist()), + ImmutableList.of( + new MetadataUpdate.AssignUUID(uuid), new MetadataUpdate.SetCurrentSchema(23))); + + String expectedJson = + "{\n" + + " \"requirements\" : [ {\n" + + " \"type\" : \"assert-table-uuid\",\n" + + " \"uuid\" : \"2cc52516-5e73-41f2-b139-545d41a4e151\"\n" + + " }, {\n" + + " \"type\" : \"assert-create\"\n" + + " } ],\n" + + " \"updates\" : [ {\n" + + " \"action\" : \"assign-uuid\",\n" + + " \"uuid\" : \"2cc52516-5e73-41f2-b139-545d41a4e151\"\n" + + " }, {\n" + + " \"action\" : \"set-current-schema\",\n" + + " \"schema-id\" : 23\n" + + " } ]\n" + + "}"; + + String json = UpdateTableRequestParser.toJson(request, true); + assertThat(json).isEqualTo(expectedJson); + + // can't do an equality comparison on UpdateTableRequest because updates/requirements + // don't implement equals/hashcode + assertThat(UpdateTableRequestParser.toJson(UpdateTableRequestParser.fromJson(json), true)) + .isEqualTo(expectedJson); + } + + @Test + public void emptyRequirementsAndUpdates() { + UpdateTableRequest request = + new UpdateTableRequest( + TableIdentifier.of("ns1", "table1"), ImmutableList.of(), ImmutableList.of()); + + String json = + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"},\"requirements\":[],\"updates\":[]}"; + + assertThat(UpdateTableRequestParser.toJson(request)).isEqualTo(json); + // can't do an equality comparison on UpdateTableRequest because updates/requirements + // don't implement equals/hashcode + assertThat(UpdateTableRequestParser.toJson(UpdateTableRequestParser.fromJson(json))) + .isEqualTo(json); + + assertThat(UpdateTableRequestParser.toJson(request)).isEqualTo(json); + // can't do an equality comparison on UpdateTableRequest because updates/requirements + // don't implement equals/hashcode + assertThat( + UpdateTableRequestParser.toJson( + UpdateTableRequestParser.fromJson( + "{\"identifier\":{\"namespace\":[\"ns1\"],\"name\":\"table1\"}}"))) + .isEqualTo(json); + } +}