This is an automated email from the ASF dual-hosted git repository.

etudenhoefner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new d23b0d39bf Core: Include query params into ETag calculation in 
reference IRC (#15057)
d23b0d39bf is described below

commit d23b0d39bfd0a1f914aaa6d24d65906fc99200a3
Author: gaborkaszab <[email protected]>
AuthorDate: Tue Feb 10 11:22:18 2026 +0100

    Core: Include query params into ETag calculation in reference IRC (#15057)
---
 .../java/org/apache/iceberg/rest/ETagProvider.java | 17 ++++-
 .../apache/iceberg/rest/RESTCatalogProperties.java |  1 +
 .../apache/iceberg/rest/RESTSessionCatalog.java    |  3 +-
 .../apache/iceberg/rest/RESTCatalogAdapter.java    | 21 ++++--
 .../org/apache/iceberg/rest/TestETagProvider.java  | 29 ++++++--
 .../iceberg/rest/TestFreshnessAwareLoading.java    | 77 +++++++++++++++++++---
 6 files changed, 127 insertions(+), 21 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/rest/ETagProvider.java 
b/core/src/main/java/org/apache/iceberg/rest/ETagProvider.java
index 82fdc5fcae..bb8e32b0a8 100644
--- a/core/src/main/java/org/apache/iceberg/rest/ETagProvider.java
+++ b/core/src/main/java/org/apache/iceberg/rest/ETagProvider.java
@@ -19,6 +19,9 @@
 package org.apache.iceberg.rest;
 
 import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.iceberg.relocated.com.google.common.base.Joiner;
 import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
 import org.apache.iceberg.relocated.com.google.common.hash.HashFunction;
 import org.apache.iceberg.relocated.com.google.common.hash.Hashing;
@@ -26,12 +29,22 @@ import 
org.apache.iceberg.relocated.com.google.common.hash.Hashing;
 class ETagProvider {
   private static final HashFunction MURMUR3 = Hashing.murmur3_32_fixed();
 
+  private static final Joiner.MapJoiner PARAMS_JOINER = 
Joiner.on(",").withKeyValueSeparator("=");
+  private static final Joiner COMMA = Joiner.on(',');
+
   private ETagProvider() {}
 
-  public static String of(String metadataLocation) {
+  public static String of(String metadataLocation, Map<String, String> params) 
{
     Preconditions.checkArgument(null != metadataLocation, "Invalid metadata 
location: null");
     Preconditions.checkArgument(!metadataLocation.isEmpty(), "Invalid metadata 
location: empty");
 
-    return MURMUR3.hashString(metadataLocation, 
StandardCharsets.UTF_8).toString();
+    String stringToHash = metadataLocation;
+    if (params != null && !params.isEmpty()) {
+      Map<String, String> orderedParams = new TreeMap<>(params);
+
+      stringToHash = COMMA.join(metadataLocation, 
PARAMS_JOINER.join(orderedParams));
+    }
+
+    return MURMUR3.hashString(stringToHash, StandardCharsets.UTF_8).toString();
   }
 }
diff --git 
a/core/src/main/java/org/apache/iceberg/rest/RESTCatalogProperties.java 
b/core/src/main/java/org/apache/iceberg/rest/RESTCatalogProperties.java
index e294bcfebe..7281862481 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTCatalogProperties.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTCatalogProperties.java
@@ -26,6 +26,7 @@ public final class RESTCatalogProperties {
 
   public static final String SNAPSHOT_LOADING_MODE = "snapshot-loading-mode";
   public static final String SNAPSHOT_LOADING_MODE_DEFAULT = 
SnapshotMode.ALL.name();
+  public static final String SNAPSHOTS_QUERY_PARAMETER = "snapshots";
 
   public static final String METRICS_REPORTING_ENABLED = 
"rest-metrics-reporting-enabled";
   public static final boolean METRICS_REPORTING_ENABLED_DEFAULT = true;
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java 
b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
index beb350ef03..cda71fccda 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
@@ -426,7 +426,8 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
   }
 
   private static Map<String, String> snapshotModeToParam(SnapshotMode mode) {
-    return ImmutableMap.of("snapshots", mode.name().toLowerCase(Locale.US));
+    return ImmutableMap.of(
+        RESTCatalogProperties.SNAPSHOTS_QUERY_PARAMETER, 
mode.name().toLowerCase(Locale.US));
   }
 
   private LoadTableResponse loadInternal(
diff --git a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java 
b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
index 5c9e8fe6d4..8ba5daef3f 100644
--- a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
+++ b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
@@ -285,7 +285,8 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
                       CatalogHandlers.createTable(catalog, namespace, request);
                   responseHeaders.accept(
                       ImmutableMap.of(
-                          HttpHeaders.ETAG, 
ETagProvider.of(response.metadataLocation())));
+                          HttpHeaders.ETAG,
+                          ETagProvider.of(response.metadataLocation(), 
defaultQueryParams())));
                   return castResponse(responseType, response);
                 });
           }
@@ -323,7 +324,7 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
           Optional<HTTPHeaders.HTTPHeader> ifNoneMatchHeader =
               httpRequest.headers().firstEntry(HttpHeaders.IF_NONE_MATCH);
 
-          String eTag = ETagProvider.of(response.metadataLocation());
+          String eTag = ETagProvider.of(response.metadataLocation(), 
httpRequest.queryParameters());
 
           if (ifNoneMatchHeader.isPresent() && 
eTag.equals(ifNoneMatchHeader.get().value())) {
             return null;
@@ -383,7 +384,8 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
 
                 responseHeaders.accept(
                     ImmutableMap.of(
-                        HttpHeaders.ETAG, 
ETagProvider.of(response.metadataLocation())));
+                        HttpHeaders.ETAG,
+                        ETagProvider.of(response.metadataLocation(), 
defaultQueryParams())));
 
                 return castResponse(responseType, response);
               });
@@ -402,7 +404,8 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
 
                 responseHeaders.accept(
                     ImmutableMap.of(
-                        HttpHeaders.ETAG, 
ETagProvider.of(response.metadataLocation())));
+                        HttpHeaders.ETAG,
+                        ETagProvider.of(response.metadataLocation(), 
defaultQueryParams())));
 
                 return castResponse(responseType, response);
               });
@@ -537,6 +540,12 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
     return null;
   }
 
+  private static Map<String, String> defaultQueryParams() {
+    return Map.of(
+        RESTCatalogProperties.SNAPSHOTS_QUERY_PARAMETER,
+        SnapshotMode.ALL.toString().toLowerCase(Locale.US));
+  }
+
   /**
    * This is a very simplistic approach that only validates the requirements 
for each table and does
    * not do any other conflict detection. Therefore, it does not guarantee 
true transactional
@@ -735,7 +744,9 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
   private static SnapshotMode snapshotModeFromQueryParams(Map<String, String> 
queryParams) {
     return SnapshotMode.valueOf(
         queryParams
-            .getOrDefault("snapshots", 
RESTCatalogProperties.SNAPSHOT_LOADING_MODE_DEFAULT)
+            .getOrDefault(
+                RESTCatalogProperties.SNAPSHOTS_QUERY_PARAMETER,
+                RESTCatalogProperties.SNAPSHOT_LOADING_MODE_DEFAULT)
             .toUpperCase(Locale.US));
   }
 }
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestETagProvider.java 
b/core/src/test/java/org/apache/iceberg/rest/TestETagProvider.java
index 1a3971492d..aaf5c3442e 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestETagProvider.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestETagProvider.java
@@ -21,30 +21,49 @@ package org.apache.iceberg.rest;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.util.Map;
 import org.junit.jupiter.api.Test;
 
 public class TestETagProvider {
+  static final String METADATA_LOCATION =
+      
"/var/folders/20/290st0_52y5fyjcj2mlg49500000gn/T/junit-3064022805908958416/db_name/tbl_name/metadata/00000-f7a7956e-61d0-499b-be60-b141283f8229.metadata.json";
+
   @Test
   public void testNullInput() {
-    assertThatThrownBy(() -> ETagProvider.of(null))
+    assertThatThrownBy(() -> ETagProvider.of(null, null))
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessageContaining("Invalid metadata location: null");
   }
 
   @Test
   public void testEmptyInput() {
-    assertThatThrownBy(() -> ETagProvider.of(""))
+    assertThatThrownBy(() -> ETagProvider.of("", null))
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessageContaining("Invalid metadata location: empty");
   }
 
   @Test
   public void testETagContent() {
-    assertThat("1f865717")
+    assertThat("90b8ad4e")
+        .isEqualTo(
+            ETagProvider.of(METADATA_LOCATION, Map.of("param1", "value1", 
"param2", "value2")));
+
+    assertThat("cb787e6a")
         .isEqualTo(
             ETagProvider.of(
-                
"/var/folders/20/290st0_52y5fyjcj2mlg49500000gn/T/junit-3064022805908958416/db_name/tbl_name/metadata/00000-f7a7956e-61d0-499b-be60-b141283f8229.metadata.json"));
+                METADATA_LOCATION, Map.of("param1", "other_value1", "param2", 
"other_value2")));
+
+    assertThat("55faa5d9").isEqualTo(ETagProvider.of("/short/path", null));
+
+    assertThat("55faa5d9").isEqualTo(ETagProvider.of("/short/path", Map.of()));
 
-    assertThat("55faa5d9").isEqualTo(ETagProvider.of("/short/path"));
+    assertThat("8adf3766").isEqualTo(ETagProvider.of("/short/path", 
Map.of("param", "some_value")));
+  }
+
+  @Test
+  public void testDifferentParameterOrderGiveSameETag() {
+    assertThat(ETagProvider.of(METADATA_LOCATION, Map.of("param1", "value1", 
"param2", "value2")))
+        .isEqualTo(
+            ETagProvider.of(METADATA_LOCATION, Map.of("param2", "value2", 
"param1", "value1")));
   }
 }
diff --git 
a/core/src/test/java/org/apache/iceberg/rest/TestFreshnessAwareLoading.java 
b/core/src/test/java/org/apache/iceberg/rest/TestFreshnessAwareLoading.java
index 0652bb378b..1cee12dfec 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestFreshnessAwareLoading.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestFreshnessAwareLoading.java
@@ -169,14 +169,68 @@ public class TestFreshnessAwareLoading extends 
TestBaseWithRESTServer {
     assertThat(respHeaders).containsEntry(HttpHeaders.ETAG, eTag);
   }
 
+  @Test
+  public void differentETagForDifferentSnapshotMode() {
+    Map<String, String> responseHeaders = Maps.newHashMap();
+    RESTCatalogAdapter adapter = 
adapterCapturingResponseHeaders(responseHeaders);
+    RESTCatalog catalog =
+        new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), config -> 
adapter);
+    catalog.initialize(
+        "test",
+        ImmutableMap.of(
+            RESTCatalogProperties.SNAPSHOT_LOADING_MODE,
+            RESTCatalogProperties.SnapshotMode.REFS.name()));
+
+    catalog.createNamespace(TABLE.namespace());
+    catalog.createTable(TABLE, SCHEMA);
+
+    assertThat(responseHeaders).containsKey(HttpHeaders.ETAG);
+    String eTagForCreateTable = responseHeaders.get(HttpHeaders.ETAG);
+    responseHeaders.clear();
+
+    catalog.loadTable(TABLE);
+
+    assertThat(responseHeaders).containsKey(HttpHeaders.ETAG);
+    
assertThat(eTagForCreateTable).isNotEqualTo(responseHeaders.get(HttpHeaders.ETAG));
+
+    // Verify that table load used the refs query parameter
+    verify(adapter, times(1))
+        .execute(
+            matches(
+                HTTPRequest.HTTPMethod.GET,
+                RESOURCE_PATHS.table(TABLE),
+                Map.of(),
+                Map.of("snapshots", "refs")),
+            eq(LoadTableResponse.class),
+            any(),
+            any());
+  }
+
   @Test
   public void notModifiedResponse() {
+    // Capture the response headers from createTable to get an ETag.
+    Map<String, String> responseHeaders = Maps.newHashMap();
+    Mockito.doAnswer(
+            invocation ->
+                adapterForRESTServer.execute(
+                    invocation.getArgument(0),
+                    invocation.getArgument(1),
+                    invocation.getArgument(2),
+                    responseHeaders::putAll,
+                    ParserContext.builder().build()))
+        .when(adapterForRESTServer)
+        .execute(
+            matches(HTTPRequest.HTTPMethod.POST, 
RESOURCE_PATHS.tables(TABLE.namespace())),
+            eq(LoadTableResponse.class),
+            any(),
+            any());
+
     restCatalog.createNamespace(TABLE.namespace());
     restCatalog.createTable(TABLE, SCHEMA);
-    Table table = restCatalog.loadTable(TABLE);
+    restCatalog.loadTable(TABLE);
 
-    String eTag =
-        ETagProvider.of(((BaseTable) 
table).operations().current().metadataFileLocation());
+    assertThat(responseHeaders).containsKeys(HttpHeaders.ETAG);
+    String eTag = responseHeaders.get(HttpHeaders.ETAG);
 
     Mockito.doAnswer(
             invocation -> {
@@ -730,8 +784,8 @@ public class TestFreshnessAwareLoading extends 
TestBaseWithRESTServer {
     assertThat(table.operations()).isInstanceOf(CustomTableOps.class);
   }
 
-  private RESTCatalog catalogWithResponseHeaders(Map<String, String> 
respHeaders) {
-    RESTCatalogAdapter adapter =
+  private RESTCatalogAdapter adapterCapturingResponseHeaders(Map<String, 
String> respHeaders) {
+    return Mockito.spy(
         new RESTCatalogAdapter(backendCatalog) {
           @Override
           public <T extends RESTResponse> T execute(
@@ -739,11 +793,18 @@ public class TestFreshnessAwareLoading extends 
TestBaseWithRESTServer {
               Class<T> responseType,
               Consumer<ErrorResponse> errorHandler,
               Consumer<Map<String, String>> responseHeaders) {
-            return super.execute(request, responseType, errorHandler, 
respHeaders::putAll);
+            Consumer<Map<String, String>> compositeConsumer =
+                headers -> {
+                  responseHeaders.accept(headers);
+                  respHeaders.putAll(headers);
+                };
+            return super.execute(request, responseType, errorHandler, 
compositeConsumer);
           }
-        };
+        });
+  }
 
-    return catalog(adapter);
+  private RESTCatalog catalogWithResponseHeaders(Map<String, String> 
respHeaders) {
+    return catalog(adapterCapturingResponseHeaders(respHeaders));
   }
 
   private RESTCatalog catalog(RESTCatalogAdapter adapter) {

Reply via email to