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 980a7addda1a..1cde2b8ad43c 100644 --- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java +++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java @@ -444,12 +444,15 @@ public void renameTable(SessionContext context, TableIdentifier from, TableIdent @Override public boolean tableExists(SessionContext context, TableIdentifier identifier) { - Endpoint.check(endpoints, Endpoint.V1_TABLE_EXISTS); - try { checkIdentifierIsValid(identifier); - client.head(paths.table(identifier), headers(context), ErrorHandlers.tableErrorHandler()); - return true; + if (endpoints.contains(Endpoint.V1_TABLE_EXISTS)) { + client.head(paths.table(identifier), headers(context), ErrorHandlers.tableErrorHandler()); + return true; + } else { + // fallback in order to work with 1.7.x and older servers + return super.tableExists(context, identifier); + } } catch (NoSuchTableException e) { return false; } @@ -665,13 +668,16 @@ public List listNamespaces(SessionContext context, Namespace namespac @Override public boolean namespaceExists(SessionContext context, Namespace namespace) { - Endpoint.check(endpoints, Endpoint.V1_NAMESPACE_EXISTS); - try { checkNamespaceIsValid(namespace); - client.head( - paths.namespace(namespace), headers(context), ErrorHandlers.namespaceErrorHandler()); - return true; + if (endpoints.contains(Endpoint.V1_NAMESPACE_EXISTS)) { + client.head( + paths.namespace(namespace), headers(context), ErrorHandlers.namespaceErrorHandler()); + return true; + } else { + // fallback in order to work with 1.7.x and older servers + return super.namespaceExists(context, namespace); + } } catch (NoSuchNamespaceException e) { return false; } @@ -1239,12 +1245,15 @@ public List listViews(SessionContext context, Namespace namespa @Override public boolean viewExists(SessionContext context, TableIdentifier identifier) { - Endpoint.check(endpoints, Endpoint.V1_VIEW_EXISTS); - try { checkViewIdentifierIsValid(identifier); - client.head(paths.view(identifier), headers(context), ErrorHandlers.viewErrorHandler()); - return true; + if (endpoints.contains(Endpoint.V1_VIEW_EXISTS)) { + client.head(paths.view(identifier), headers(context), ErrorHandlers.viewErrorHandler()); + return true; + } else { + // fallback in order to work with 1.7.x and older servers + return super.viewExists(context, identifier); + } } catch (NoSuchViewException e) { return false; } diff --git a/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java b/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java index 6a5f22075c6e..696240bb6da2 100644 --- a/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java +++ b/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java @@ -66,6 +66,7 @@ import org.apache.iceberg.exceptions.ServiceFailureException; import org.apache.iceberg.expressions.Expressions; import org.apache.iceberg.inmemory.InMemoryCatalog; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.apache.iceberg.relocated.com.google.common.collect.Lists; import org.apache.iceberg.relocated.com.google.common.collect.Maps; @@ -2505,6 +2506,127 @@ public void testNamespaceExistsViaHEADRequest() { any()); } + @Test + public void testNamespaceExistsFallbackToGETRequest() { + RESTCatalogAdapter adapter = + Mockito.spy( + new RESTCatalogAdapter(backendCatalog) { + @Override + public T execute( + HTTPRequest request, + Class responseType, + Consumer errorHandler, + Consumer> responseHeaders) { + if ("v1/config".equals(request.path())) { + return castResponse( + responseType, + ConfigResponse.builder() + // server indicates support of loading a namespace only via GET, which is + // what older REST servers would send back too + .withEndpoints(ImmutableList.of(Endpoint.V1_LOAD_NAMESPACE)) + .build()); + } + + return super.execute(request, responseType, errorHandler, responseHeaders); + } + }); + + RESTCatalog catalog = + new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter); + catalog.initialize("test", ImmutableMap.of()); + + assertThat(catalog.namespaceExists(Namespace.of("non-existing"))).isFalse(); + + Mockito.verify(adapter) + .execute( + reqMatcher(HTTPMethod.GET, "v1/config", Map.of(), Map.of()), + eq(ConfigResponse.class), + any(), + any()); + + // verifies that the namespace is loaded via a GET instead of HEAD (V1_NAMESPACE_EXISTS) + Mockito.verify(adapter) + .execute( + reqMatcher(HTTPMethod.GET, "v1/namespaces/non-existing", Map.of(), Map.of()), + any(), + any(), + any()); + } + + @Test + public void testTableExistsViaHEADRequest() { + RESTCatalogAdapter adapter = Mockito.spy(new RESTCatalogAdapter(backendCatalog)); + RESTCatalog catalog = + new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter); + catalog.initialize("test", ImmutableMap.of()); + + assertThat(catalog.tableExists(TABLE)).isFalse(); + + Mockito.verify(adapter) + .execute( + reqMatcher(HTTPMethod.GET, "v1/config", Map.of(), Map.of()), + eq(ConfigResponse.class), + any(), + any()); + Mockito.verify(adapter) + .execute( + reqMatcher(HTTPMethod.HEAD, "v1/namespaces/newdb/tables/table", Map.of(), Map.of()), + any(), + any(), + any()); + } + + @Test + public void testTableExistsFallbackToGETRequest() { + RESTCatalogAdapter adapter = + Mockito.spy( + new RESTCatalogAdapter(backendCatalog) { + @Override + public T execute( + HTTPRequest request, + Class responseType, + Consumer errorHandler, + Consumer> responseHeaders) { + if ("v1/config".equals(request.path())) { + return castResponse( + responseType, + ConfigResponse.builder() + // server indicates support of loading a table only via GET, which is + // what older REST servers would send back too + .withEndpoints(ImmutableList.of(Endpoint.V1_LOAD_TABLE)) + .build()); + } + + return super.execute(request, responseType, errorHandler, responseHeaders); + } + }); + + RESTCatalog catalog = + new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter); + catalog.initialize("test", ImmutableMap.of()); + + assertThat(catalog.tableExists(TABLE)).isFalse(); + + Mockito.verify(adapter) + .execute( + reqMatcher(HTTPMethod.GET, "v1/config", Map.of(), Map.of()), + eq(ConfigResponse.class), + any(), + any()); + + // verifies that the table is loaded via a GET instead of HEAD (V1_LOAD_TABLE) + Mockito.verify(adapter) + .execute( + reqMatcher( + HTTPMethod.GET, + "v1/namespaces/newdb/tables/table", + Map.of(), + Map.of("snapshots", "all")), + any(), + any(), + any()); + } + private RESTCatalog catalog(RESTCatalogAdapter adapter) { RESTCatalog catalog = new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter); diff --git a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java index 8dfbf0df6dd7..73ff5f052a11 100644 --- a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java +++ b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java @@ -38,6 +38,7 @@ import org.apache.iceberg.catalog.SessionCatalog; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.inmemory.InMemoryCatalog; +import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.apache.iceberg.rest.HTTPRequest.HTTPMethod; import org.apache.iceberg.rest.responses.ConfigResponse; @@ -243,6 +244,51 @@ public void viewExistsViaHEADRequest() { any()); } + @Test + public void viewExistsFallbackToGETRequest() { + RESTCatalogAdapter adapter = + Mockito.spy( + new RESTCatalogAdapter(backendCatalog) { + @Override + public T execute( + HTTPRequest request, + Class responseType, + Consumer errorHandler, + Consumer> responseHeaders) { + if ("v1/config".equals(request.path())) { + return castResponse( + responseType, + ConfigResponse.builder() + // server indicates support of loading a view only via GET, which is + // what older REST servers would send back too + .withEndpoints(ImmutableList.of(Endpoint.V1_LOAD_VIEW)) + .build()); + } + + return super.execute(request, responseType, errorHandler, responseHeaders); + } + }); + + RESTCatalog catalog = + new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter); + catalog.initialize("test", ImmutableMap.of()); + + assertThat(catalog.viewExists(TableIdentifier.of("ns", "view"))).isFalse(); + + Mockito.verify(adapter) + .execute( + reqMatcher(HTTPMethod.GET, "v1/config", Map.of(), Map.of()), + eq(ConfigResponse.class), + any(), + any()); + Mockito.verify(adapter) + .execute( + reqMatcher(HTTPMethod.GET, "v1/namespaces/ns/views/view", Map.of(), Map.of()), + any(), + any(), + any()); + } + @Override protected RESTCatalog catalog() { return restCatalog;