From 4cf923be642eac9ca4f299ad298aac8b5520099d Mon Sep 17 00:00:00 2001 From: laskoviymishka Date: Wed, 29 Jul 2026 00:30:01 +0200 Subject: [PATCH 1/5] Core: add labels to the load table/view read path Reference client implementation for the IRC labels read-path spec change (apache/iceberg#15750). Without it, RESTObjectMapper (which sets FAIL_ON_UNKNOWN_PROPERTIES = false) silently drops the labels field on deserialization, so labels returned by a catalog are invisible to the Java client. New rest.labels package (mirroring rest.credentials): Labels (object + fields sub-scopes, with a shared empty instance) and FieldLabels (per-field, keyed by field-id) value types as immutables interfaces, each with a JSON parser. FieldLabels validates field-id >= 1 and a non-empty labels map, matching Credential. Wire an optional labels field into LoadTableResponse / LoadViewResponse and their parsers. labels() never returns null (empty instance when absent), on LoadViewResponse via a @Value.Default default method so no interface API break is introduced. Labels are omitted from the wire when absent (or empty), so the change is additive and backward compatible. --- .../java/org/apache/iceberg/FieldLabel.java | 30 ++++ .../org/apache/iceberg/FieldLabelParser.java | 64 ++++++++ .../main/java/org/apache/iceberg/Labels.java | 40 +++++ .../java/org/apache/iceberg/LabelsParser.java | 81 ++++++++++ .../rest/responses/LoadTableResponse.java | 19 ++- .../responses/LoadTableResponseParser.java | 11 ++ .../rest/responses/LoadViewResponse.java | 6 + .../responses/LoadViewResponseParser.java | 11 ++ .../apache/iceberg/TestFieldLabelParser.java | 85 +++++++++++ .../org/apache/iceberg/TestLabelsParser.java | 142 ++++++++++++++++++ .../rest/responses/TestLoadTableResponse.java | 46 ++++++ .../TestLoadTableResponseParser.java | 117 +++++++++++++++ .../responses/TestLoadViewResponseParser.java | 116 ++++++++++++++ 13 files changed, 766 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/apache/iceberg/FieldLabel.java create mode 100644 core/src/main/java/org/apache/iceberg/FieldLabelParser.java create mode 100644 core/src/main/java/org/apache/iceberg/Labels.java create mode 100644 core/src/main/java/org/apache/iceberg/LabelsParser.java create mode 100644 core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java create mode 100644 core/src/test/java/org/apache/iceberg/TestLabelsParser.java diff --git a/core/src/main/java/org/apache/iceberg/FieldLabel.java b/core/src/main/java/org/apache/iceberg/FieldLabel.java new file mode 100644 index 000000000000..b75c9b99453f --- /dev/null +++ b/core/src/main/java/org/apache/iceberg/FieldLabel.java @@ -0,0 +1,30 @@ +/* + * 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.Map; +import org.immutables.value.Value; + +/** Labels attached to a single field, identified by its field id. See {@link Labels}. */ +@Value.Immutable +public interface FieldLabel { + int fieldId(); + + Map labels(); +} diff --git a/core/src/main/java/org/apache/iceberg/FieldLabelParser.java b/core/src/main/java/org/apache/iceberg/FieldLabelParser.java new file mode 100644 index 000000000000..a8e53140b8d6 --- /dev/null +++ b/core/src/main/java/org/apache/iceberg/FieldLabelParser.java @@ -0,0 +1,64 @@ +/* + * 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 org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.util.JsonUtil; + +public class FieldLabelParser { + private static final String FIELD_ID = "field-id"; + private static final String LABELS = "labels"; + + private FieldLabelParser() {} + + public static String toJson(FieldLabel fieldLabels) { + return toJson(fieldLabels, false); + } + + public static String toJson(FieldLabel fieldLabels, boolean pretty) { + return JsonUtil.generate(gen -> toJson(fieldLabels, gen), pretty); + } + + public static void toJson(FieldLabel fieldLabels, JsonGenerator gen) throws IOException { + Preconditions.checkArgument(null != fieldLabels, "Invalid field labels: null"); + + gen.writeStartObject(); + + gen.writeNumberField(FIELD_ID, fieldLabels.fieldId()); + JsonUtil.writeStringMap(LABELS, fieldLabels.labels(), gen); + + gen.writeEndObject(); + } + + public static FieldLabel fromJson(String json) { + return JsonUtil.parse(json, FieldLabelParser::fromJson); + } + + public static FieldLabel fromJson(JsonNode json) { + Preconditions.checkArgument(null != json, "Cannot parse field labels from null object"); + + return ImmutableFieldLabel.builder() + .fieldId(JsonUtil.getInt(FIELD_ID, json)) + .labels(JsonUtil.getStringMap(LABELS, json)) + .build(); + } +} diff --git a/core/src/main/java/org/apache/iceberg/Labels.java b/core/src/main/java/org/apache/iceberg/Labels.java new file mode 100644 index 000000000000..73d4bf1d07b4 --- /dev/null +++ b/core/src/main/java/org/apache/iceberg/Labels.java @@ -0,0 +1,40 @@ +/* + * 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.Map; +import org.immutables.value.Value; + +/** Optional catalog-provided labels returned on a load response. */ +@Value.Immutable +public interface Labels { + Labels EMPTY = ImmutableLabels.builder().build(); + + /** Object-level labels. */ + Map objectLabels(); + + /** Field-level labels */ + List fields(); + + /** Returns true when there are neither object-level nor field-level labels. */ + default boolean isEmpty() { + return objectLabels().isEmpty() && fields().isEmpty(); + } +} diff --git a/core/src/main/java/org/apache/iceberg/LabelsParser.java b/core/src/main/java/org/apache/iceberg/LabelsParser.java new file mode 100644 index 000000000000..01cf473b751f --- /dev/null +++ b/core/src/main/java/org/apache/iceberg/LabelsParser.java @@ -0,0 +1,81 @@ +/* + * 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 org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.util.JsonUtil; + +public class LabelsParser { + private static final String OBJECT_LABELS = "object-labels"; + private static final String FIELDS = "fields"; + + private LabelsParser() {} + + public static String toJson(Labels labels) { + return toJson(labels, false); + } + + public static String toJson(Labels labels, boolean pretty) { + return JsonUtil.generate(gen -> toJson(labels, gen), pretty); + } + + public static void toJson(Labels labels, JsonGenerator gen) throws IOException { + Preconditions.checkArgument(null != labels, "Invalid labels: null"); + + gen.writeStartObject(); + + if (!labels.objectLabels().isEmpty()) { + JsonUtil.writeStringMap(OBJECT_LABELS, labels.objectLabels(), gen); + } + + if (!labels.fields().isEmpty()) { + gen.writeArrayFieldStart(FIELDS); + for (FieldLabel fieldLabels : labels.fields()) { + FieldLabelParser.toJson(fieldLabels, gen); + } + + gen.writeEndArray(); + } + + gen.writeEndObject(); + } + + public static Labels fromJson(String json) { + return JsonUtil.parse(json, LabelsParser::fromJson); + } + + public static Labels fromJson(JsonNode json) { + Preconditions.checkArgument(null != json, "Cannot parse labels from null object"); + + ImmutableLabels.Builder builder = ImmutableLabels.builder(); + + if (json.hasNonNull(OBJECT_LABELS)) { + builder.objectLabels(JsonUtil.getStringMap(OBJECT_LABELS, json)); + } + + if (json.hasNonNull(FIELDS)) { + builder.fields(JsonUtil.getObjectList(FIELDS, json, FieldLabelParser::fromJson)); + } + + return builder.build(); + } +} diff --git a/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java b/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java index a60310236dce..a1eecdfd3302 100644 --- a/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java +++ b/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.apache.iceberg.Labels; import org.apache.iceberg.TableMetadata; import org.apache.iceberg.relocated.com.google.common.base.MoreObjects; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; @@ -47,6 +48,7 @@ public class LoadTableResponse implements RESTResponse { private TableMetadata metadataWithLocation; private List credentials; private RemoteSigningConfig remoteSigningConfig; + private Labels labels; public LoadTableResponse() { // Required for Jackson deserialization @@ -57,12 +59,14 @@ private LoadTableResponse( TableMetadata metadata, Map config, List credentials, - RemoteSigningConfig remoteSigningConfig) { + RemoteSigningConfig remoteSigningConfig, + Labels labels) { this.metadataLocation = metadataLocation; this.metadata = metadata; this.config = config; this.credentials = credentials; this.remoteSigningConfig = remoteSigningConfig; + this.labels = labels; } @Override @@ -95,12 +99,17 @@ public RemoteSigningConfig remoteSigningConfig() { return remoteSigningConfig != null ? remoteSigningConfig : RemoteSigningConfig.EMPTY; } + public Labels labels() { + return labels != null ? labels : Labels.EMPTY; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) .add("metadataLocation", metadataLocation) .add("metadata", metadata) .add("config", config) + .add("labels", labels) .toString(); } @@ -114,6 +123,7 @@ public static class Builder { private final Map config = Maps.newHashMap(); private final List credentials = Lists.newArrayList(); private RemoteSigningConfig remoteSigningConfig = RemoteSigningConfig.EMPTY; + private Labels labels; private Builder() {} @@ -148,10 +158,15 @@ public Builder withRemoteSigningConfig(RemoteSigningConfig signingConfig) { return this; } + public Builder withLabels(Labels tableLabels) { + this.labels = tableLabels; + return this; + } + public LoadTableResponse build() { Preconditions.checkNotNull(metadata, "Invalid metadata: null"); return new LoadTableResponse( - metadataLocation, metadata, config, credentials, remoteSigningConfig); + metadataLocation, metadata, config, credentials, remoteSigningConfig, labels); } } } diff --git a/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponseParser.java b/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponseParser.java index d53d879beeab..404928bc2a46 100644 --- a/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponseParser.java +++ b/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponseParser.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; +import org.apache.iceberg.LabelsParser; import org.apache.iceberg.TableMetadata; import org.apache.iceberg.TableMetadataParser; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; @@ -36,6 +37,7 @@ public class LoadTableResponseParser { private static final String CONFIG = "config"; private static final String STORAGE_CREDENTIALS = "storage-credentials"; private static final String REMOTE_SIGNING_CONFIG = "remote-signing-config"; + private static final String LABELS = "labels"; private LoadTableResponseParser() {} @@ -77,6 +79,11 @@ public static void toJson(LoadTableResponse response, JsonGenerator gen) throws RemoteSigningConfigParser.toJson(response.remoteSigningConfig(), gen); } + if (!response.labels().isEmpty()) { + gen.writeFieldName(LABELS); + LabelsParser.toJson(response.labels(), gen); + } + gen.writeEndObject(); } @@ -113,6 +120,10 @@ public static LoadTableResponse fromJson(JsonNode json) { RemoteSigningConfigParser.fromJson(JsonUtil.get(REMOTE_SIGNING_CONFIG, json))); } + if (json.hasNonNull(LABELS)) { + builder.withLabels(LabelsParser.fromJson(JsonUtil.get(LABELS, json))); + } + return builder.build(); } } diff --git a/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponse.java b/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponse.java index d07ba872fdaa..5d47fbbff9ad 100644 --- a/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponse.java +++ b/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponse.java @@ -19,6 +19,7 @@ package org.apache.iceberg.rest.responses; import java.util.Map; +import org.apache.iceberg.Labels; import org.apache.iceberg.rest.RESTResponse; import org.apache.iceberg.view.ViewMetadata; import org.immutables.value.Value; @@ -31,6 +32,11 @@ public interface LoadViewResponse extends RESTResponse { Map config(); + @Value.Default + default Labels labels() { + return Labels.EMPTY; + } + @Override default void validate() { // nothing to validate as it's not possible to create an invalid instance diff --git a/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponseParser.java b/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponseParser.java index 7964a8455a52..fabcfc34351d 100644 --- a/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponseParser.java +++ b/core/src/main/java/org/apache/iceberg/rest/responses/LoadViewResponseParser.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; +import org.apache.iceberg.LabelsParser; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.util.JsonUtil; import org.apache.iceberg.view.ViewMetadata; @@ -31,6 +32,7 @@ public class LoadViewResponseParser { private static final String METADATA_LOCATION = "metadata-location"; private static final String METADATA = "metadata"; private static final String CONFIG = "config"; + private static final String LABELS = "labels"; private LoadViewResponseParser() {} @@ -56,6 +58,11 @@ public static void toJson(LoadViewResponse response, JsonGenerator gen) throws I JsonUtil.writeStringMap(CONFIG, response.config(), gen); } + if (!response.labels().isEmpty()) { + gen.writeFieldName(LABELS); + LabelsParser.toJson(response.labels(), gen); + } + gen.writeEndObject(); } @@ -80,6 +87,10 @@ public static LoadViewResponse fromJson(JsonNode json) { builder.config(JsonUtil.getStringMap(CONFIG, json)); } + if (json.hasNonNull(LABELS)) { + builder.labels(LabelsParser.fromJson(JsonUtil.get(LABELS, json))); + } + return builder.build(); } } diff --git a/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java b/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java new file mode 100644 index 000000000000..fd7039a261e1 --- /dev/null +++ b/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java @@ -0,0 +1,85 @@ +/* + * 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 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.relocated.com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; + +public class TestFieldLabelParser { + + @Test + public void nullCheck() { + assertThatThrownBy(() -> FieldLabelParser.toJson(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid field labels: null"); + + assertThatThrownBy(() -> FieldLabelParser.fromJson((JsonNode) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse field labels from null object"); + } + + @Test + public void roundTrip() { + FieldLabel fieldLabels = + ImmutableFieldLabel.builder().fieldId(3).labels(ImmutableMap.of("pii", "true")).build(); + + String expectedJson = + """ + { + "field-id" : 3, + "labels" : { + "pii" : "true" + } + }"""; + + assertThat(FieldLabelParser.toJson(fieldLabels, true)).isEqualTo(expectedJson); + assertThat(FieldLabelParser.fromJson(expectedJson)).isEqualTo(fieldLabels); + } + + @Test + public void emptyLabels() { + assertThat(ImmutableFieldLabel.builder().fieldId(1).build().labels()).isEmpty(); + } + + @Test + public void emptyLabelsFromJson() { + FieldLabel fieldLabels = FieldLabelParser.fromJson("{\"field-id\": 1, \"labels\": {}}"); + + assertThat(fieldLabels.fieldId()).isEqualTo(1); + assertThat(fieldLabels.labels()).isEmpty(); + } + + @Test + public void missingLabelsFromJson() { + assertThatThrownBy(() -> FieldLabelParser.fromJson("{\"field-id\": 1}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse missing map: labels"); + } + + @Test + public void emptyJson() { + assertThatThrownBy(() -> FieldLabelParser.fromJson("{}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse missing int: field-id"); + } +} diff --git a/core/src/test/java/org/apache/iceberg/TestLabelsParser.java b/core/src/test/java/org/apache/iceberg/TestLabelsParser.java new file mode 100644 index 000000000000..010d43bf3780 --- /dev/null +++ b/core/src/test/java/org/apache/iceberg/TestLabelsParser.java @@ -0,0 +1,142 @@ +/* + * 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 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.relocated.com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; + +public class TestLabelsParser { + + @Test + public void nullCheck() { + assertThatThrownBy(() -> LabelsParser.toJson(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid labels: null"); + + assertThatThrownBy(() -> LabelsParser.fromJson((JsonNode) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse labels from null object"); + } + + @Test + public void emptyLabels() { + Labels labels = ImmutableLabels.builder().build(); + assertThat(labels.isEmpty()).isTrue(); + + String expectedJson = "{ }"; + assertThat(LabelsParser.toJson(labels, true)).isEqualTo(expectedJson); + + Labels roundTrip = LabelsParser.fromJson(expectedJson); + assertThat(roundTrip.objectLabels()).isEmpty(); + assertThat(roundTrip.fields()).isEmpty(); + + Labels nonEmpty = ImmutableLabels.builder().objectLabels(ImmutableMap.of("k", "v")).build(); + assertThat(nonEmpty.isEmpty()).isFalse(); + } + + @Test + public void objectLabelsOnly() { + Labels labels = + ImmutableLabels.builder() + .objectLabels(ImmutableMap.of("owner", "team-a", "sensitivity", "high")) + .build(); + + String expectedJson = + """ + { + "object-labels" : { + "owner" : "team-a", + "sensitivity" : "high" + } + }"""; + + assertThat(LabelsParser.toJson(labels, true)).isEqualTo(expectedJson); + assertThat(LabelsParser.fromJson(expectedJson)).isEqualTo(labels); + } + + @Test + public void fieldLabelsOnly() { + Labels labels = + ImmutableLabels.builder() + .addFields( + ImmutableFieldLabel.builder() + .fieldId(3) + .labels(ImmutableMap.of("pii", "true")) + .build()) + .build(); + + String expectedJson = + """ + { + "fields" : [ { + "field-id" : 3, + "labels" : { + "pii" : "true" + } + } ] + }"""; + + assertThat(LabelsParser.toJson(labels, true)).isEqualTo(expectedJson); + assertThat(LabelsParser.fromJson(expectedJson)).isEqualTo(labels); + } + + @Test + public void objectAndFieldLabel() { + Labels labels = + ImmutableLabels.builder() + .objectLabels(ImmutableMap.of("owner", "team-a")) + .addFields( + ImmutableFieldLabel.builder() + .fieldId(1) + .labels(ImmutableMap.of("classification", "pii")) + .build()) + .addFields( + ImmutableFieldLabel.builder() + .fieldId(2) + .labels(ImmutableMap.of("classification", "public")) + .build()) + .build(); + + String expectedJson = + """ + { + "object-labels" : { + "owner" : "team-a" + }, + "fields" : [ { + "field-id" : 1, + "labels" : { + "classification" : "pii" + } + }, { + "field-id" : 2, + "labels" : { + "classification" : "public" + } + } ] + }"""; + + assertThat(LabelsParser.toJson(labels, true)).isEqualTo(expectedJson); + assertThat(LabelsParser.fromJson(expectedJson)).isEqualTo(labels); + } +} diff --git a/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponse.java b/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponse.java index 96854fe64c08..14f740dc305d 100644 --- a/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponse.java +++ b/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponse.java @@ -26,6 +26,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; +import org.apache.iceberg.ImmutableFieldLabel; +import org.apache.iceberg.ImmutableLabels; +import org.apache.iceberg.Labels; import org.apache.iceberg.NullOrder; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; @@ -162,6 +165,48 @@ public void testRoundTripSerdeWithV3TableMetadata() throws Exception { assertRoundTripSerializesEquallyFrom(json, resp); } + @Test + public void testRoundTripSerdeWithLabels() throws Exception { + String tableMetadataJson = readTableMetadataInputFile("TableMetadataV2Valid.json"); + TableMetadata metadata = + TableMetadataParser.fromJson(TEST_METADATA_LOCATION, tableMetadataJson); + Labels labels = + ImmutableLabels.builder() + .objectLabels(ImmutableMap.of("owner", "team-a")) + .addFields( + ImmutableFieldLabel.builder() + .fieldId(1) + .labels(ImmutableMap.of("classification", "pii")) + .build()) + .build(); + String json = + String.format( + """ + { + "metadata-location":"%s", + "metadata":%s, + "config":{"foo":"bar"}, + "labels":{ + "object-labels":{"owner":"team-a"}, + "fields":[{"field-id":1,"labels":{"classification":"pii"}}] + } + }""", + TEST_METADATA_LOCATION, TableMetadataParser.toJson(metadata)); + + // exercise the full RESTObjectMapper (Jackson) path, not just the parser + LoadTableResponse actual = deserialize(json); + assertThat(actual.labels()).isEqualTo(labels); + + LoadTableResponse resp = + LoadTableResponse.builder() + .withTableMetadata(metadata) + .addAllConfig(CONFIG) + .withLabels(labels) + .build(); + // compare as JSON trees so the readable (pretty) input above is whitespace-insensitive + assertThat(mapper().readTree(serialize(resp))).isEqualTo(mapper().readTree(json)); + } + @Test public void testCanDeserializeWithoutDefaultValues() throws Exception { String metadataJson = readTableMetadataInputFile("TableMetadataV1Valid.json"); @@ -187,6 +232,7 @@ public void assertEquals(LoadTableResponse actual, LoadTableResponse expected) { assertThat(actual.metadataLocation()) .as("Should have the same metadata location") .isEqualTo(expected.metadataLocation()); + assertThat(actual.labels()).as("Should have the same labels").isEqualTo(expected.labels()); } private void assertEqualTableMetadata(TableMetadata actual, TableMetadata expected) { diff --git a/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponseParser.java b/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponseParser.java index 3c03f24cf7de..7d6f028b79ea 100644 --- a/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponseParser.java +++ b/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadTableResponseParser.java @@ -25,6 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.List; import java.util.Map; +import org.apache.iceberg.ImmutableFieldLabel; +import org.apache.iceberg.ImmutableLabels; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; import org.apache.iceberg.SortOrder; @@ -557,4 +559,119 @@ public void roundTripSerdeWithCredentials() { assertThat(LoadTableResponseParser.toJson(LoadTableResponseParser.fromJson(json), true)) .isEqualTo(expectedJson); } + + @Test + public void roundTripSerdeWithLabels() { + String uuid = "386b9f01-002b-4d8c-b77f-42c3fd3b7c9b"; + TableMetadata metadata = + TableMetadata.buildFromEmpty() + .assignUUID(uuid) + .setLocation("location") + .setCurrentSchema( + new Schema(Types.NestedField.required(1, "x", Types.LongType.get())), 1) + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .discardChanges() + .withMetadataLocation("metadata-location") + .build(); + + LoadTableResponse response = + LoadTableResponse.builder() + .withTableMetadata(metadata) + .withLabels( + ImmutableLabels.builder() + .objectLabels(ImmutableMap.of("owner", "team-a")) + .addFields( + ImmutableFieldLabel.builder() + .fieldId(1) + .labels(ImmutableMap.of("classification", "pii")) + .build()) + .build()) + .build(); + + String expectedJson = + String.format( + """ + { + "metadata-location" : "metadata-location", + "metadata" : { + "format-version" : 2, + "table-uuid" : "386b9f01-002b-4d8c-b77f-42c3fd3b7c9b", + "location" : "location", + "last-sequence-number" : 0, + "last-updated-ms" : %d, + "last-column-id" : 1, + "current-schema-id" : 0, + "schemas" : [ { + "type" : "struct", + "schema-id" : 0, + "fields" : [ { + "id" : 1, + "name" : "x", + "required" : true, + "type" : "long" + } ] + } ], + "default-spec-id" : 0, + "partition-specs" : [ { + "spec-id" : 0, + "fields" : [ ] + } ], + "last-partition-id" : 999, + "default-sort-order-id" : 0, + "sort-orders" : [ { + "order-id" : 0, + "fields" : [ ] + } ], + "properties" : { }, + "current-snapshot-id" : -1, + "refs" : { }, + "snapshots" : [ ], + "statistics" : [ ], + "partition-statistics" : [ ], + "snapshot-log" : [ ], + "metadata-log" : [ ] + }, + "labels" : { + "object-labels" : { + "owner" : "team-a" + }, + "fields" : [ { + "field-id" : 1, + "labels" : { + "classification" : "pii" + } + } ] + } + }""", + metadata.lastUpdatedMillis()); + + String json = LoadTableResponseParser.toJson(response, true); + assertThat(json).isEqualTo(expectedJson); + // can't do an equality comparison because Schema doesn't implement equals/hashCode + assertThat(LoadTableResponseParser.toJson(LoadTableResponseParser.fromJson(json), true)) + .isEqualTo(expectedJson); + } + + @Test + public void labelsAreOptional() { + String uuid = "386b9f01-002b-4d8c-b77f-42c3fd3b7c9b"; + TableMetadata metadata = + TableMetadata.buildFromEmpty() + .assignUUID(uuid) + .setLocation("location") + .setCurrentSchema( + new Schema(Types.NestedField.required(1, "x", Types.LongType.get())), 1) + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .discardChanges() + .withMetadataLocation("metadata-location") + .build(); + + LoadTableResponse response = LoadTableResponse.builder().withTableMetadata(metadata).build(); + + String json = LoadTableResponseParser.toJson(response, true); + assertThat(json).doesNotContain("labels"); + assertThat(LoadTableResponseParser.fromJson(json).labels().isEmpty()).isTrue(); + } } diff --git a/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadViewResponseParser.java b/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadViewResponseParser.java index b4ae60b17424..bd398822900f 100644 --- a/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadViewResponseParser.java +++ b/core/src/test/java/org/apache/iceberg/rest/responses/TestLoadViewResponseParser.java @@ -22,6 +22,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.fasterxml.jackson.databind.JsonNode; +import org.apache.iceberg.ImmutableFieldLabel; +import org.apache.iceberg.ImmutableLabels; import org.apache.iceberg.Schema; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; @@ -279,4 +281,118 @@ public void nullConfig() { assertThat(parsed.config()).isEmpty(); assertThat(LoadViewResponseParser.toJson(parsed, true)).isEqualTo(jsonWithoutConfig); } + + @Test + public void roundTripSerdeWithLabels() { + String uuid = "386b9f01-002b-4d8c-b77f-42c3fd3b7c9b"; + ViewMetadata viewMetadata = + ViewMetadata.builder() + .assignUUID(uuid) + .setLocation("location") + .addSchema(new Schema(Types.NestedField.required(1, "x", Types.LongType.get()))) + .addVersion( + ImmutableViewVersion.builder() + .schemaId(0) + .versionId(1) + .timestampMillis(23L) + .defaultNamespace(Namespace.of("ns1")) + .build()) + .setCurrentVersionId(1) + .build(); + + LoadViewResponse response = + ImmutableLoadViewResponse.builder() + .metadata(viewMetadata) + .metadataLocation("custom-location") + .labels( + ImmutableLabels.builder() + .objectLabels(ImmutableMap.of("owner", "team-a")) + .addFields( + ImmutableFieldLabel.builder() + .fieldId(1) + .labels(ImmutableMap.of("classification", "pii")) + .build()) + .build()) + .build(); + + String expectedJson = + """ + { + "metadata-location" : "custom-location", + "metadata" : { + "view-uuid" : "386b9f01-002b-4d8c-b77f-42c3fd3b7c9b", + "format-version" : 1, + "location" : "location", + "schemas" : [ { + "type" : "struct", + "schema-id" : 0, + "fields" : [ { + "id" : 1, + "name" : "x", + "required" : true, + "type" : "long" + } ] + } ], + "current-version-id" : 1, + "versions" : [ { + "version-id" : 1, + "timestamp-ms" : 23, + "schema-id" : 0, + "summary" : { }, + "default-namespace" : [ "ns1" ], + "representations" : [ ] + } ], + "version-log" : [ { + "timestamp-ms" : 23, + "version-id" : 1 + } ] + }, + "labels" : { + "object-labels" : { + "owner" : "team-a" + }, + "fields" : [ { + "field-id" : 1, + "labels" : { + "classification" : "pii" + } + } ] + } + }"""; + + String json = LoadViewResponseParser.toJson(response, true); + assertThat(json).isEqualTo(expectedJson); + // can't do an equality comparison because Schema doesn't implement equals/hashCode + assertThat(LoadViewResponseParser.toJson(LoadViewResponseParser.fromJson(json), true)) + .isEqualTo(expectedJson); + } + + @Test + public void labelsAreOptional() { + String uuid = "386b9f01-002b-4d8c-b77f-42c3fd3b7c9b"; + ViewMetadata viewMetadata = + ViewMetadata.builder() + .assignUUID(uuid) + .setLocation("location") + .addSchema(new Schema(Types.NestedField.required(1, "x", Types.LongType.get()))) + .addVersion( + ImmutableViewVersion.builder() + .schemaId(0) + .versionId(1) + .timestampMillis(23L) + .defaultNamespace(Namespace.of("ns1")) + .build()) + .setCurrentVersionId(1) + .build(); + + LoadViewResponse response = + ImmutableLoadViewResponse.builder() + .metadata(viewMetadata) + .metadataLocation("custom-location") + .build(); + + String json = LoadViewResponseParser.toJson(response, true); + assertThat(json).doesNotContain("labels"); + assertThat(LoadViewResponseParser.fromJson(json).labels().isEmpty()).isTrue(); + } } From f2c011926f74c10d44b625c99d0f6e0870cafd42 Mon Sep 17 00:00:00 2001 From: laskoviymishka Date: Tue, 15 Sep 2026 13:56:18 +0200 Subject: [PATCH 2/5] Core: rename fieldLabels to fieldLabel for single FieldLabel values Address review nit: a single FieldLabel value was named `fieldLabels` (plural). Rename the parameter in FieldLabelParser, the loop variable in LabelsParser, and the test locals, and singularize the related precondition messages. Co-authored-by: Isaac --- .../org/apache/iceberg/FieldLabelParser.java | 18 +++++++++--------- .../java/org/apache/iceberg/LabelsParser.java | 4 ++-- .../apache/iceberg/TestFieldLabelParser.java | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/org/apache/iceberg/FieldLabelParser.java b/core/src/main/java/org/apache/iceberg/FieldLabelParser.java index a8e53140b8d6..cc8123a0f4b9 100644 --- a/core/src/main/java/org/apache/iceberg/FieldLabelParser.java +++ b/core/src/main/java/org/apache/iceberg/FieldLabelParser.java @@ -30,21 +30,21 @@ public class FieldLabelParser { private FieldLabelParser() {} - public static String toJson(FieldLabel fieldLabels) { - return toJson(fieldLabels, false); + public static String toJson(FieldLabel fieldLabel) { + return toJson(fieldLabel, false); } - public static String toJson(FieldLabel fieldLabels, boolean pretty) { - return JsonUtil.generate(gen -> toJson(fieldLabels, gen), pretty); + public static String toJson(FieldLabel fieldLabel, boolean pretty) { + return JsonUtil.generate(gen -> toJson(fieldLabel, gen), pretty); } - public static void toJson(FieldLabel fieldLabels, JsonGenerator gen) throws IOException { - Preconditions.checkArgument(null != fieldLabels, "Invalid field labels: null"); + public static void toJson(FieldLabel fieldLabel, JsonGenerator gen) throws IOException { + Preconditions.checkArgument(null != fieldLabel, "Invalid field label: null"); gen.writeStartObject(); - gen.writeNumberField(FIELD_ID, fieldLabels.fieldId()); - JsonUtil.writeStringMap(LABELS, fieldLabels.labels(), gen); + gen.writeNumberField(FIELD_ID, fieldLabel.fieldId()); + JsonUtil.writeStringMap(LABELS, fieldLabel.labels(), gen); gen.writeEndObject(); } @@ -54,7 +54,7 @@ public static FieldLabel fromJson(String json) { } public static FieldLabel fromJson(JsonNode json) { - Preconditions.checkArgument(null != json, "Cannot parse field labels from null object"); + Preconditions.checkArgument(null != json, "Cannot parse field label from null object"); return ImmutableFieldLabel.builder() .fieldId(JsonUtil.getInt(FIELD_ID, json)) diff --git a/core/src/main/java/org/apache/iceberg/LabelsParser.java b/core/src/main/java/org/apache/iceberg/LabelsParser.java index 01cf473b751f..32ce968e135a 100644 --- a/core/src/main/java/org/apache/iceberg/LabelsParser.java +++ b/core/src/main/java/org/apache/iceberg/LabelsParser.java @@ -49,8 +49,8 @@ public static void toJson(Labels labels, JsonGenerator gen) throws IOException { if (!labels.fields().isEmpty()) { gen.writeArrayFieldStart(FIELDS); - for (FieldLabel fieldLabels : labels.fields()) { - FieldLabelParser.toJson(fieldLabels, gen); + for (FieldLabel fieldLabel : labels.fields()) { + FieldLabelParser.toJson(fieldLabel, gen); } gen.writeEndArray(); diff --git a/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java b/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java index fd7039a261e1..e6facd4e88ae 100644 --- a/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java +++ b/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java @@ -40,7 +40,7 @@ public void nullCheck() { @Test public void roundTrip() { - FieldLabel fieldLabels = + FieldLabel fieldLabel = ImmutableFieldLabel.builder().fieldId(3).labels(ImmutableMap.of("pii", "true")).build(); String expectedJson = @@ -52,8 +52,8 @@ public void roundTrip() { } }"""; - assertThat(FieldLabelParser.toJson(fieldLabels, true)).isEqualTo(expectedJson); - assertThat(FieldLabelParser.fromJson(expectedJson)).isEqualTo(fieldLabels); + assertThat(FieldLabelParser.toJson(fieldLabel, true)).isEqualTo(expectedJson); + assertThat(FieldLabelParser.fromJson(expectedJson)).isEqualTo(fieldLabel); } @Test @@ -63,10 +63,10 @@ public void emptyLabels() { @Test public void emptyLabelsFromJson() { - FieldLabel fieldLabels = FieldLabelParser.fromJson("{\"field-id\": 1, \"labels\": {}}"); + FieldLabel fieldLabel = FieldLabelParser.fromJson("{\"field-id\": 1, \"labels\": {}}"); - assertThat(fieldLabels.fieldId()).isEqualTo(1); - assertThat(fieldLabels.labels()).isEmpty(); + assertThat(fieldLabel.fieldId()).isEqualTo(1); + assertThat(fieldLabel.labels()).isEmpty(); } @Test From 1161a1ad7cc17a37f71dc2f4cd77ccee9fb00389 Mon Sep 17 00:00:00 2001 From: laskoviymishka Date: Tue, 15 Sep 2026 14:40:53 +0200 Subject: [PATCH 3/5] Core: preserve original null-check messages The previous commit singularized the precondition messages in FieldLabelParser, which broke TestFieldLabelParser.nullCheck (it asserts the exact text). Keep the identifier rename fieldLabels -> fieldLabel but restore the original messages ("Invalid field labels: null", "Cannot parse field labels from null object"). Co-authored-by: Isaac --- core/src/main/java/org/apache/iceberg/FieldLabelParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/iceberg/FieldLabelParser.java b/core/src/main/java/org/apache/iceberg/FieldLabelParser.java index cc8123a0f4b9..e15c4b00931a 100644 --- a/core/src/main/java/org/apache/iceberg/FieldLabelParser.java +++ b/core/src/main/java/org/apache/iceberg/FieldLabelParser.java @@ -39,7 +39,7 @@ public static String toJson(FieldLabel fieldLabel, boolean pretty) { } public static void toJson(FieldLabel fieldLabel, JsonGenerator gen) throws IOException { - Preconditions.checkArgument(null != fieldLabel, "Invalid field label: null"); + Preconditions.checkArgument(null != fieldLabel, "Invalid field labels: null"); gen.writeStartObject(); @@ -54,7 +54,7 @@ public static FieldLabel fromJson(String json) { } public static FieldLabel fromJson(JsonNode json) { - Preconditions.checkArgument(null != json, "Cannot parse field label from null object"); + Preconditions.checkArgument(null != json, "Cannot parse field labels from null object"); return ImmutableFieldLabel.builder() .fieldId(JsonUtil.getInt(FIELD_ID, json)) From 44d81802976b4d2b98f91480a8317c24f1fe324c Mon Sep 17 00:00:00 2001 From: laskoviymishka Date: Wed, 16 Sep 2026 17:47:39 +0200 Subject: [PATCH 4/5] Core: address review comments on label tests and Labels.isEmpty - Make TestFieldLabelParser and TestLabelsParser (and their test methods) package private. - Annotate Labels.isEmpty() with @Value.Derived, matching the pattern in RemoteSigningConfig. Co-authored-by: Isaac --- core/src/main/java/org/apache/iceberg/Labels.java | 1 + .../org/apache/iceberg/TestFieldLabelParser.java | 14 +++++++------- .../java/org/apache/iceberg/TestLabelsParser.java | 12 ++++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/apache/iceberg/Labels.java b/core/src/main/java/org/apache/iceberg/Labels.java index 73d4bf1d07b4..20f41f4ea069 100644 --- a/core/src/main/java/org/apache/iceberg/Labels.java +++ b/core/src/main/java/org/apache/iceberg/Labels.java @@ -34,6 +34,7 @@ public interface Labels { List fields(); /** Returns true when there are neither object-level nor field-level labels. */ + @Value.Derived default boolean isEmpty() { return objectLabels().isEmpty() && fields().isEmpty(); } diff --git a/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java b/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java index e6facd4e88ae..72c1a49a0342 100644 --- a/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java +++ b/core/src/test/java/org/apache/iceberg/TestFieldLabelParser.java @@ -25,10 +25,10 @@ import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.junit.jupiter.api.Test; -public class TestFieldLabelParser { +class TestFieldLabelParser { @Test - public void nullCheck() { + void nullCheck() { assertThatThrownBy(() -> FieldLabelParser.toJson(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Invalid field labels: null"); @@ -39,7 +39,7 @@ public void nullCheck() { } @Test - public void roundTrip() { + void roundTrip() { FieldLabel fieldLabel = ImmutableFieldLabel.builder().fieldId(3).labels(ImmutableMap.of("pii", "true")).build(); @@ -57,12 +57,12 @@ public void roundTrip() { } @Test - public void emptyLabels() { + void emptyLabels() { assertThat(ImmutableFieldLabel.builder().fieldId(1).build().labels()).isEmpty(); } @Test - public void emptyLabelsFromJson() { + void emptyLabelsFromJson() { FieldLabel fieldLabel = FieldLabelParser.fromJson("{\"field-id\": 1, \"labels\": {}}"); assertThat(fieldLabel.fieldId()).isEqualTo(1); @@ -70,14 +70,14 @@ public void emptyLabelsFromJson() { } @Test - public void missingLabelsFromJson() { + void missingLabelsFromJson() { assertThatThrownBy(() -> FieldLabelParser.fromJson("{\"field-id\": 1}")) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot parse missing map: labels"); } @Test - public void emptyJson() { + void emptyJson() { assertThatThrownBy(() -> FieldLabelParser.fromJson("{}")) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Cannot parse missing int: field-id"); diff --git a/core/src/test/java/org/apache/iceberg/TestLabelsParser.java b/core/src/test/java/org/apache/iceberg/TestLabelsParser.java index 010d43bf3780..bb331fcf444b 100644 --- a/core/src/test/java/org/apache/iceberg/TestLabelsParser.java +++ b/core/src/test/java/org/apache/iceberg/TestLabelsParser.java @@ -25,10 +25,10 @@ import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.junit.jupiter.api.Test; -public class TestLabelsParser { +class TestLabelsParser { @Test - public void nullCheck() { + void nullCheck() { assertThatThrownBy(() -> LabelsParser.toJson(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Invalid labels: null"); @@ -39,7 +39,7 @@ public void nullCheck() { } @Test - public void emptyLabels() { + void emptyLabels() { Labels labels = ImmutableLabels.builder().build(); assertThat(labels.isEmpty()).isTrue(); @@ -55,7 +55,7 @@ public void emptyLabels() { } @Test - public void objectLabelsOnly() { + void objectLabelsOnly() { Labels labels = ImmutableLabels.builder() .objectLabels(ImmutableMap.of("owner", "team-a", "sensitivity", "high")) @@ -75,7 +75,7 @@ public void objectLabelsOnly() { } @Test - public void fieldLabelsOnly() { + void fieldLabelsOnly() { Labels labels = ImmutableLabels.builder() .addFields( @@ -101,7 +101,7 @@ public void fieldLabelsOnly() { } @Test - public void objectAndFieldLabel() { + void objectAndFieldLabel() { Labels labels = ImmutableLabels.builder() .objectLabels(ImmutableMap.of("owner", "team-a")) From ee93ef599fb0120eef7b763e9952c94fdf07743f Mon Sep 17 00:00:00 2001 From: laskoviymishka Date: Wed, 16 Sep 2026 22:57:50 +0200 Subject: [PATCH 5/5] Core: default LoadTableResponse builder labels to Labels.EMPTY Address review: default the builder's labels field to Labels.EMPTY, matching remoteSigningConfig, so build() never passes null. Co-authored-by: Isaac --- .../org/apache/iceberg/rest/responses/LoadTableResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java b/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java index a1eecdfd3302..24b3ad65fc86 100644 --- a/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java +++ b/core/src/main/java/org/apache/iceberg/rest/responses/LoadTableResponse.java @@ -123,7 +123,7 @@ public static class Builder { private final Map config = Maps.newHashMap(); private final List credentials = Lists.newArrayList(); private RemoteSigningConfig remoteSigningConfig = RemoteSigningConfig.EMPTY; - private Labels labels; + private Labels labels = Labels.EMPTY; private Builder() {}