This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 1a0a5e9c2e [Cherry-pick to branch-1.3] [#12794] feat(catalog): Support
connection tests with proposed changes (#12798) (#12884)
1a0a5e9c2e is described below
commit 1a0a5e9c2e9ac002e4fed76aec2d34b3c365c19e
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Sep 4 09:43:35 2026 +0800
[Cherry-pick to branch-1.3] [#12794] feat(catalog): Support connection
tests with proposed changes (#12798) (#12884)
**Cherry-pick Information:**
- Original commit: 2325b412a29e40221d8c3b729542f6d5f4e995be
- Original PR: #12798
- Target branch: `branch-1.3`
- Status: ✅ Conflicts resolved manually
**Resolution:**
- Rebuilt the #12798 change instead of retaining the bot-generated
conflict markers.
- Preserved the no-body behavior for stored-configuration probes and
added temporary rename, comment, set-property, and remove-property
changes without persistence.
- Kept property validation, catalog read locking, temporary-wrapper
cleanup, authorization, REST error handling, and Java/Python client
behavior.
- Omitted the main-only `SecretAlterChanges` and `SecretManager` changes
and secret-specific tests because `branch-1.3` does not contain the
Secret API or subsystem.
**Validation:**
- Compiled the affected API, Core, Server, and Java client production
and test sources.
- Ran Core, Server, and Java client unit tests.
- Ran the targeted Hive Docker integration test covering temporary
failure, non-persistence, and subsequent stored-configuration success.
- Python client: 1018 unit tests passed; Black 26.3.1 and Pylint 4.0.5
passed.
- `./gradlew spotlessApply`
- `./gradlew :docs:build :api:javadoc -PskipITs -PskipDockerTests=true`
- Conflict-marker scan and `git diff --check`
Co-authored-by: mchades <[email protected]>
---
.../org/apache/gravitino/SupportsCatalogs.java | 22 ++++++
.../apache/gravitino/client/GravitinoClient.java | 12 ++++
.../apache/gravitino/client/GravitinoMetalake.java | 40 +++++++++++
.../gravitino/client/TestGravitinoAdminClient.java | 30 ++++++++
.../client/integration/test/CatalogIT.java | 13 ++++
.../gravitino/client/gravitino_client.py | 6 +-
.../gravitino/client/gravitino_metalake.py | 17 ++++-
.../client-python/tests/unittests/test_metalake.py | 21 ++++++
.../apache/gravitino/catalog/CatalogManager.java | 71 +++++++++++++++++++
.../catalog/CatalogNormalizeDispatcher.java | 13 ++++
.../apache/gravitino/catalog/SupportsCatalogs.java | 10 +++
.../gravitino/hook/CatalogHookDispatcher.java | 5 ++
.../gravitino/listener/CatalogEventDispatcher.java | 6 ++
.../gravitino/catalog/TestCatalogManager.java | 81 ++++++++++++++++++++++
.../catalog/TestCatalogNormalizeDispatcher.java | 14 ++++
docs/open-api/catalogs.yaml | 20 ++++--
.../server/web/rest/CatalogOperations.java | 14 +++-
.../server/web/rest/TestCatalogOperations.java | 42 +++++++++++
18 files changed, 423 insertions(+), 14 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
b/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
index 650d60fdbb..1009a1f332 100644
--- a/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
+++ b/api/src/main/java/org/apache/gravitino/SupportsCatalogs.java
@@ -226,4 +226,26 @@ public interface SupportsCatalogs {
throw new UnsupportedOperationException(
String.format("Catalog %s does not support connection testing",
catalogName));
}
+
+ /**
+ * Test the connection of an existing catalog with proposed changes without
persisting them.
+ *
+ * <p>The default implementation preserves the existing connection-test
behavior when no changes
+ * are supplied and rejects non-empty changes. Implementations that support
testing proposed
+ * changes should override this method.
+ *
+ * @param catalogName the name of the existing catalog.
+ * @param changes the proposed changes to apply temporarily.
+ * @throws NoSuchCatalogException if the catalog does not exist.
+ * @throws Exception if the test failed.
+ */
+ default void testConnection(String catalogName, CatalogChange... changes)
throws Exception {
+ if (changes.length == 0) {
+ testConnection(catalogName);
+ return;
+ }
+ throw new UnsupportedOperationException(
+ String.format(
+ "Catalog %s does not support connection testing with proposed
changes", catalogName));
+ }
}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
index 25837678e4..24ca818415 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
@@ -547,6 +547,18 @@ public class GravitinoClient extends GravitinoClientBase
getMetalake().testConnection(catalogName);
}
+ /**
+ * Test the connection of an existing catalog with proposed changes without
persisting them.
+ *
+ * @param catalogName the name of the existing catalog.
+ * @param changes the proposed changes to apply temporarily.
+ * @throws Exception if the test failed.
+ */
+ @Override
+ public void testConnection(String catalogName, CatalogChange... changes)
throws Exception {
+ getMetalake().testConnection(catalogName, changes);
+ }
+
@Override
public String[] listTags() throws NoSuchMetalakeException {
return getMetalake().listTags();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
index 70b15c329c..5887abbedf 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
@@ -448,6 +448,46 @@ public class GravitinoMetalake extends MetalakeDTO
ErrorHandlers.catalogErrorHandler().accept(resp);
}
+ /**
+ * Test the connection of an existing catalog with proposed changes without
persisting them.
+ *
+ * @param catalogName the name of the existing catalog.
+ * @param changes the proposed changes to apply temporarily.
+ * @throws Exception if the test failed.
+ */
+ @Override
+ public void testConnection(String catalogName, CatalogChange... changes)
throws Exception {
+ Preconditions.checkArgument(changes != null, "changes must not be null");
+ if (changes.length == 0) {
+ testConnection(catalogName);
+ return;
+ }
+
+ List<CatalogUpdateRequest> requests =
+ Arrays.stream(changes)
+ .map(DTOConverters::toCatalogUpdateRequest)
+ .collect(Collectors.toList());
+ CatalogUpdatesRequest updatesRequest = new CatalogUpdatesRequest(requests);
+ updatesRequest.validate();
+
+ ErrorResponse resp =
+ restClient.post(
+ String.format(
+ API_METALAKES_CATALOGS_PATH + "/testConnection",
+ RESTUtils.encodeString(this.name()),
+ RESTUtils.encodeString(catalogName)),
+ updatesRequest,
+ ErrorResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.catalogErrorHandler());
+
+ if (resp.getCode() == 0) {
+ return;
+ }
+
+ ErrorHandlers.catalogErrorHandler().accept(resp);
+ }
+
@Override
public SupportsRoles supportsRoles() {
return this;
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoAdminClient.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoAdminClient.java
index 0bcf28d0d0..4073bca3be 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoAdminClient.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoAdminClient.java
@@ -25,11 +25,14 @@ import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.CatalogChange;
import org.apache.gravitino.MetalakeChange;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.dto.AuditDTO;
import org.apache.gravitino.dto.MetalakeDTO;
import org.apache.gravitino.dto.requests.CatalogCreateRequest;
+import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
+import org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
import org.apache.gravitino.dto.requests.MetalakeCreateRequest;
import org.apache.gravitino.dto.requests.MetalakeUpdatesRequest;
import org.apache.gravitino.dto.responses.BaseResponse;
@@ -324,6 +327,33 @@ public class TestGravitinoAdminClient extends TestBase {
HttpStatus.SC_OK);
Assertions.assertDoesNotThrow(() -> metaLake.testConnection("catalog"));
+ buildMockResource(
+ Method.POST,
+ "/api/metalakes/mock/catalogs/catalog/testConnection",
+ null,
+ new BaseResponse(),
+ HttpStatus.SC_OK);
+ Assertions.assertDoesNotThrow(() -> metaLake.testConnection("catalog", new
CatalogChange[0]));
+
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> metaLake.testConnection("catalog", (CatalogChange[]) null));
+ Assertions.assertTrue(exception.getMessage().contains("changes must not be
null"));
+
+ CatalogUpdatesRequest updatesRequest =
+ new CatalogUpdatesRequest(
+ Collections.singletonList(
+ new CatalogUpdateRequest.SetCatalogPropertyRequest("key",
"value")));
+ buildMockResource(
+ Method.POST,
+ "/api/metalakes/mock/catalogs/catalog/testConnection",
+ updatesRequest,
+ new BaseResponse(),
+ HttpStatus.SC_OK);
+ Assertions.assertDoesNotThrow(
+ () -> metaLake.testConnection("catalog",
CatalogChange.setProperty("key", "value")));
+
buildMockResource(
Method.POST,
"/api/metalakes/mock/catalogs/catalog/testConnection",
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/CatalogIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/CatalogIT.java
index f3df2f4c9d..21e46b1ebe 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/CatalogIT.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/CatalogIT.java
@@ -40,6 +40,7 @@ import org.apache.gravitino.client.GravitinoMetalake;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
import org.apache.gravitino.exceptions.CatalogInUseException;
import org.apache.gravitino.exceptions.CatalogNotInUseException;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.file.FilesetCatalog;
import org.apache.gravitino.file.FilesetChange;
@@ -125,6 +126,18 @@ public class CatalogIT extends BaseIT {
Assertions.assertTrue(catalog.properties().containsKey("metastore.uris"));
Assertions.assertDoesNotThrow(() -> metalake.testConnection(catalogName));
+ Assertions.assertThrows(
+ ConnectionFailedException.class,
+ () ->
+ metalake.testConnection(
+ catalogName,
+ CatalogChange.updateComment("temporary comment"),
+ CatalogChange.setProperty("metastore.uris",
"thrift://127.0.0.1:1")));
+ Catalog unchangedCatalog = metalake.loadCatalog(catalogName);
+ Assertions.assertEquals("catalog comment", unchangedCatalog.comment());
+ Assertions.assertEquals(hmsUri,
unchangedCatalog.properties().get("metastore.uris"));
+ Assertions.assertDoesNotThrow(() -> metalake.testConnection(catalogName));
+
metalake.dropCatalog(catalogName, true);
}
diff --git a/clients/client-python/gravitino/client/gravitino_client.py
b/clients/client-python/gravitino/client/gravitino_client.py
index 55b384939b..58cc9251d2 100644
--- a/clients/client-python/gravitino/client/gravitino_client.py
+++ b/clients/client-python/gravitino/client/gravitino_client.py
@@ -120,9 +120,9 @@ class GravitinoClient(GravitinoClientBase, SupportsJobs,
TagOperations):
def disable_catalog(self, name: str):
return self.get_metalake().disable_catalog(name)
- def test_connection(self, name: str) -> None:
- """Test an existing catalog connection using its stored
configuration."""
- self.get_metalake().test_connection(name)
+ def test_connection(self, name: str, *changes: CatalogChange) -> None:
+ """Test an existing catalog connection with optional proposed
changes."""
+ self.get_metalake().test_connection(name, *changes)
def list_job_templates(self) -> List[JobTemplate]:
"""Lists all job templates in the current metalake.
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 3dd2d30462..bdd557e3b3 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -353,11 +353,12 @@ class GravitinoMetalake(
url, json=catalog_disable_request,
error_handler=CATALOG_ERROR_HANDLER
)
- def test_connection(self, name: str) -> None:
- """Test an existing catalog connection using its stored configuration.
+ def test_connection(self, name: str, *changes: CatalogChange) -> None:
+ """Test an existing catalog connection with optional proposed changes.
Args:
name: The name of the existing catalog.
+ changes: Proposed catalog changes to apply temporarily without
persisting.
Raises:
NoSuchCatalogException: If the catalog does not exist.
@@ -370,7 +371,17 @@ class GravitinoMetalake(
)
+ "/testConnection"
)
- response = self.rest_client.post(url,
error_handler=CATALOG_ERROR_HANDLER)
+ if changes:
+ requests = [
+ DTOConverters.to_catalog_update_request(change) for change in
changes
+ ]
+ updates_request = CatalogUpdatesRequest(requests)
+ updates_request.validate()
+ response = self.rest_client.post(
+ url, json=updates_request, error_handler=CATALOG_ERROR_HANDLER
+ )
+ else:
+ response = self.rest_client.post(url,
error_handler=CATALOG_ERROR_HANDLER)
base_response = BaseResponse.from_json(response.body,
infer_missing=True)
base_response.validate()
if base_response.code() == 0:
diff --git a/clients/client-python/tests/unittests/test_metalake.py
b/clients/client-python/tests/unittests/test_metalake.py
index 674539389c..1a00985b13 100644
--- a/clients/client-python/tests/unittests/test_metalake.py
+++ b/clients/client-python/tests/unittests/test_metalake.py
@@ -18,9 +18,12 @@
import unittest
from unittest.mock import MagicMock
+from gravitino.api.catalog_change import CatalogChange
from gravitino.client.gravitino_metalake import GravitinoMetalake
from gravitino.constants.error import ErrorConstants
from gravitino.dto.metalake_dto import MetalakeDTO
+from gravitino.dto.requests.catalog_update_request import CatalogUpdateRequest
+from gravitino.dto.requests.catalog_updates_request import
CatalogUpdatesRequest
from gravitino.dto.responses.metalake_response import MetalakeResponse
from gravitino.exceptions.base import (
ConnectionFailedException,
@@ -44,6 +47,24 @@ class TestMetalake(unittest.TestCase):
error_handler=CATALOG_ERROR_HANDLER,
)
+ def test_existing_catalog_connection_with_changes(self):
+ rest_client = MagicMock()
+ rest_client.post.return_value.body = b'{"code":0}'
+ metalake = GravitinoMetalake(
+ MetalakeDTO("metalake", None, {}, None), rest_client
+ )
+
+ metalake.test_connection("catalog", CatalogChange.set_property("key",
"value"))
+
+ expected_request = CatalogUpdatesRequest(
+ [CatalogUpdateRequest.SetCatalogPropertyRequest("key", "value")]
+ )
+ rest_client.post.assert_called_once_with(
+ "api/metalakes/metalake/catalogs/catalog/testConnection",
+ json=expected_request,
+ error_handler=CATALOG_ERROR_HANDLER,
+ )
+
def test_existing_catalog_connection_failure(self):
rest_client = MagicMock()
rest_client.post.return_value.body = (
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index a974dfb486..d7e2739f07 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -702,6 +702,77 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
});
}
+ /**
+ * Test the connection of an existing catalog with proposed changes without
persisting them.
+ *
+ * @param ident The identifier of the existing catalog.
+ * @param changes The proposed changes to apply temporarily.
+ */
+ @Override
+ public void testConnection(NameIdentifier ident, CatalogChange... changes) {
+ Preconditions.checkArgument(changes != null, "changes must not be null");
+ if (changes.length == 0) {
+ testConnection(ident);
+ return;
+ }
+
+ TreeLockUtils.doWithTreeLock(
+ ident,
+ LockType.READ,
+ () -> {
+ CatalogWrapper storedWrapper = loadCatalogAndWrap(ident);
+ BaseCatalog<?> storedCatalog = storedWrapper.catalog();
+ storedCatalog.checkMetalakeAndCatalogInUse();
+ try {
+ storedWrapper.doWithPropertiesMeta(
+ metadata -> {
+ Pair<Map<String, String>, Map<String, String>> alterProperty
=
+ getCatalogAlterProperty(changes);
+ validatePropertyForAlter(
+ metadata.catalogPropertiesMetadata(),
+ alterProperty.getLeft(),
+ alterProperty.getRight());
+ return null;
+ });
+
+ CatalogEntity storedEntity = storedCatalog.entity();
+ Map<String, String> effectiveProperties =
+ storedEntity.getProperties() == null
+ ? new HashMap<>()
+ : new HashMap<>(storedEntity.getProperties());
+ CatalogEntity effectiveEntity =
+ updateEntity(
+ newCatalogBuilder(storedEntity.namespace(),
storedEntity),
+ effectiveProperties,
+ changes)
+ .build();
+ effectiveEntity = convertFilesetCatalogEntity(effectiveEntity);
+
+ CatalogWrapper temporaryWrapper =
createCatalogWrapper(effectiveEntity, null);
+ try {
+ NameIdentifier effectiveIdent = effectiveEntity.nameIdentifier();
+ temporaryWrapper.doWithCatalogOps(
+ operations -> {
+ operations.testConnection(effectiveIdent);
+ return null;
+ });
+ } finally {
+ temporaryWrapper.close();
+ }
+ } catch (UnsupportedOperationException e) {
+ throw e;
+ } catch (Exception e) {
+ LOG.warn(
+ "Failed to test existing catalog connection {} with proposed
changes", ident, e);
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ }
+ throw new RuntimeException(e);
+ }
+ return null;
+ });
+ }
+
@Override
public void enableCatalog(NameIdentifier ident)
throws NoSuchCatalogException, CatalogNotInUseException {
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
index 85ba013ba2..14f7db422f 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/CatalogNormalizeDispatcher.java
@@ -130,6 +130,19 @@ public class CatalogNormalizeDispatcher implements
CatalogDispatcher {
dispatcher.testConnection(ident);
}
+ @Override
+ public void testConnection(NameIdentifier ident, CatalogChange... changes)
throws Exception {
+ validateCatalogName(ident.name());
+ Arrays.stream(changes)
+ .forEach(
+ change -> {
+ if (change instanceof CatalogChange.RenameCatalog) {
+ validateCatalogName(((CatalogChange.RenameCatalog)
change).getNewName());
+ }
+ });
+ dispatcher.testConnection(ident, changes);
+ }
+
@Override
public void enableCatalog(NameIdentifier ident) throws
NoSuchCatalogException {
dispatcher.enableCatalog(ident);
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
b/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
index a6f6ccb11e..84cd066792 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/SupportsCatalogs.java
@@ -185,6 +185,16 @@ public interface SupportsCatalogs {
*/
void testConnection(NameIdentifier ident) throws Exception;
+ /**
+ * Test the connection of an existing catalog with proposed changes without
persisting them.
+ *
+ * @param ident The identifier of the existing catalog.
+ * @param changes The proposed changes to apply temporarily.
+ * @throws NoSuchCatalogException If the catalog does not exist.
+ * @throws Exception If the connection test fails.
+ */
+ void testConnection(NameIdentifier ident, CatalogChange... changes) throws
Exception;
+
/**
* Enable a catalog. If the catalog is already enabled, this method does
nothing.
*
diff --git
a/core/src/main/java/org/apache/gravitino/hook/CatalogHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/CatalogHookDispatcher.java
index 577c71dfd8..5852b18cb1 100644
--- a/core/src/main/java/org/apache/gravitino/hook/CatalogHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/CatalogHookDispatcher.java
@@ -172,6 +172,11 @@ public class CatalogHookDispatcher implements
CatalogDispatcher {
dispatcher.testConnection(ident);
}
+ @Override
+ public void testConnection(NameIdentifier ident, CatalogChange... changes)
throws Exception {
+ dispatcher.testConnection(ident, changes);
+ }
+
@Override
public void enableCatalog(NameIdentifier ident)
throws NoSuchCatalogException, CatalogNotInUseException {
diff --git
a/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
b/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
index bd738380b7..cc64bdf4b7 100644
---
a/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
@@ -209,6 +209,12 @@ public class CatalogEventDispatcher implements
CatalogDispatcher {
dispatcher.testConnection(ident);
}
+ @Override
+ public void testConnection(NameIdentifier ident, CatalogChange... changes)
throws Exception {
+ // TODO(#12566): Support event dispatching for testConnection
+ dispatcher.testConnection(ident, changes);
+ }
+
@Override
public void enableCatalog(NameIdentifier ident)
throws NoSuchCatalogException, CatalogNotInUseException {
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
index f54722c330..4424dc848f 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
@@ -57,6 +57,7 @@ import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.Schema;
import org.apache.gravitino.connector.BaseCatalog;
+import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.connector.capability.CapabilityResult;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
@@ -80,6 +81,7 @@ import
org.apache.gravitino.storage.relational.SupportsEntityChangeLog;
import org.apache.gravitino.storage.relational.po.cache.EntityChangeRecord;
import org.apache.gravitino.storage.relational.po.cache.OperateType;
import org.apache.gravitino.utils.PrincipalUtils;
+import org.apache.gravitino.utils.ThrowableFunction;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
@@ -1284,6 +1286,85 @@ public class TestCatalogManager {
}
}
+ @Test
+ void testExistingCatalogConnectionWithProposedChanges() throws Exception {
+ NameIdentifier ident = NameIdentifier.of("metalake",
"connection_changes_test");
+ Map<String, String> properties =
+ ImmutableMap.<String, String>builder()
+ .put("provider", "test")
+ .put(PROPERTY_KEY1, "value1")
+ .put(PROPERTY_KEY2, "value2")
+ .put(PROPERTY_KEY5_PREFIX + "1", "value3")
+ .put("removable", "stored")
+ .build();
+ catalogManager.createCatalog(
+ ident, Catalog.Type.RELATIONAL, provider, "stored comment",
properties);
+ CatalogEntity storedBefore = entityStore.get(ident, EntityType.CATALOG,
CatalogEntity.class);
+ CatalogManager.CatalogWrapper cachedBefore =
+ catalogManager.getCatalogCache().getIfPresent(ident);
+
+ CatalogManager.CatalogWrapper temporaryWrapper =
+ Mockito.mock(CatalogManager.CatalogWrapper.class);
+ CatalogOperations temporaryOperations =
Mockito.mock(CatalogOperations.class);
+ AtomicReference<CatalogEntity> effectiveEntity = new AtomicReference<>();
+ Mockito.doAnswer(
+ invocation -> {
+ effectiveEntity.set(invocation.getArgument(0));
+ return temporaryWrapper;
+ })
+ .when(catalogManager)
+ .createCatalogWrapper(any(CatalogEntity.class), eq(null));
+ Mockito.doAnswer(
+ invocation -> {
+ ThrowableFunction<CatalogOperations, Object> operation =
invocation.getArgument(0);
+ return operation.apply(temporaryOperations);
+ })
+ .when(temporaryWrapper)
+ .doWithCatalogOps(any());
+
+ NameIdentifier renamedIdent = NameIdentifier.of("metalake",
"connection_changes_renamed");
+ try {
+ catalogManager.testConnection(
+ ident,
+ CatalogChange.rename(renamedIdent.name()),
+ CatalogChange.updateComment("temporary comment"),
+ CatalogChange.setProperty(PROPERTY_KEY2, "temporary value"),
+ CatalogChange.removeProperty("removable"));
+
+ CatalogEntity effective = effectiveEntity.get();
+ Assertions.assertNotNull(effective);
+ Assertions.assertEquals(renamedIdent.name(), effective.name());
+ Assertions.assertEquals("temporary comment", effective.getComment());
+ Assertions.assertEquals("temporary value",
effective.getProperties().get(PROPERTY_KEY2));
+
Assertions.assertFalse(effective.getProperties().containsKey("removable"));
+ Mockito.verify(temporaryOperations).testConnection(renamedIdent);
+ Mockito.verify(temporaryWrapper).close();
+
+ CatalogEntity storedAfter = entityStore.get(ident, EntityType.CATALOG,
CatalogEntity.class);
+ Assertions.assertEquals(storedBefore.name(), storedAfter.name());
+ Assertions.assertEquals(storedBefore.getComment(),
storedAfter.getComment());
+ Assertions.assertEquals(storedBefore.getProperties(),
storedAfter.getProperties());
+ Assertions.assertFalse(entityStore.exists(renamedIdent,
EntityType.CATALOG));
+ Assertions.assertSame(cachedBefore,
catalogManager.getCatalogCache().getIfPresent(ident));
+
+ Mockito.doThrow(new IOException("probe failed"))
+ .when(temporaryOperations)
+ .testConnection(any(NameIdentifier.class));
+ RuntimeException failure =
+ Assertions.assertThrows(
+ RuntimeException.class,
+ () ->
+ catalogManager.testConnection(
+ ident, CatalogChange.setProperty(PROPERTY_KEY2, "another
value")));
+ Assertions.assertInstanceOf(IOException.class, failure.getCause());
+ Mockito.verify(temporaryWrapper, Mockito.times(2)).close();
+ } finally {
+ Mockito.doCallRealMethod()
+ .when(catalogManager)
+ .createCatalogWrapper(any(CatalogEntity.class), eq(null));
+ }
+ }
+
@Test
public void testCatalogCacheRemoveListener() throws IOException {
NameIdentifier ident = NameIdentifier.of(metalake, "catalog");
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
index 9babbe7ce6..9a0810b7db 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
@@ -28,6 +28,7 @@ import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.CatalogChange;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.EntityStore;
@@ -169,4 +170,17 @@ public class TestCatalogNormalizeDispatcher {
"The catalog name '" + illegalName + "' is illegal.",
exception.getMessage());
}
}
+
+ @Test
+ void testConnectionChangesValidateRenamedCatalogName() {
+ NameIdentifier catalogIdent = NameIdentifier.of(metalake, "catalog");
+ Mockito.clearInvocations(catalogManager);
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalogNormalizeDispatcher.testConnection(
+ catalogIdent, CatalogChange.rename("invalid/name")));
+ Mockito.verify(catalogManager, Mockito.never())
+ .testConnection(Mockito.eq(catalogIdent),
Mockito.any(CatalogChange[].class));
+ }
}
diff --git a/docs/open-api/catalogs.yaml b/docs/open-api/catalogs.yaml
index c9972aa600..6fe23fcdec 100644
--- a/docs/open-api/catalogs.yaml
+++ b/docs/open-api/catalogs.yaml
@@ -151,13 +151,21 @@ paths:
summary: Test an existing catalog connection
description: >-
Runs the provider's smallest meaningful, read-only catalog-level
operation using the
- catalog's stored configuration and effective credentials. This
fail-fast preflight
- surfaces configuration and connectivity failures before regular
catalog operations, but
- does not guarantee that every object-level or mutating operation will
succeed. Fileset
- catalogs test all catalog-level `location` and `location-*` targets.
Model and Generic
- catalogs do not support connection testing. Expected test failures are
returned as
- application error codes in an HTTP 200 response.
+ catalog's stored configuration and effective credentials. Optional
proposed catalog
+ changes are applied to a temporary effective configuration for this
probe and are not
+ persisted. This fail-fast preflight surfaces configuration and
connectivity failures before
+ regular catalog operations, but does not guarantee that every
object-level or mutating
+ operation will succeed. Fileset catalogs test all catalog-level
`location` and `location-*`
+ targets. Model and Generic catalogs do not support connection testing.
Expected test
+ failures are returned as application error codes in an HTTP 200
response.
operationId: testExistingCatalogConnection
+ requestBody:
+ required: false
+ description: Optional catalog changes to apply only to this connection
test
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CatalogUpdatesRequest"
responses:
"200":
description: Connection test completed, including expected test
failures
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
index 1e026b96f2..43226b156e 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
@@ -232,14 +232,24 @@ public class CatalogOperations {
@PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
String metalake,
@PathParam("catalog") @AuthorizationMetadata(type =
Entity.EntityType.CATALOG)
- String catalogName) {
+ String catalogName,
+ CatalogUpdatesRequest request) {
LOG.info("Received test connection request for existing catalog: {}.{}",
metalake, catalogName);
try {
return Utils.doAs(
httpRequest,
() -> {
NameIdentifier ident = NameIdentifierUtil.ofCatalog(metalake,
catalogName);
- catalogDispatcher.testConnection(ident);
+ if (request == null) {
+ catalogDispatcher.testConnection(ident);
+ } else {
+ request.validate();
+ CatalogChange[] changes =
+ request.getUpdates().stream()
+ .map(CatalogUpdateRequest::catalogChange)
+ .toArray(CatalogChange[]::new);
+ catalogDispatcher.testConnection(ident, changes);
+ }
LOG.info(
"Successfully tested connection for existing catalog: {}.{}",
metalake,
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
index 1fa527630b..54b91fdad8 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
@@ -47,6 +47,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.CatalogChange;
import org.apache.gravitino.Config;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.NameIdentifier;
@@ -81,6 +82,7 @@ import org.glassfish.jersey.test.TestProperties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
public class TestCatalogOperations extends BaseOperationsTest {
@@ -399,6 +401,46 @@ public class TestCatalogOperations extends
BaseOperationsTest {
Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
Assertions.assertEquals(0,
response.readEntity(BaseResponse.class).getCode());
+ CatalogUpdatesRequest proposedChanges =
+ new CatalogUpdatesRequest(
+ ImmutableList.of(
+ new CatalogUpdateRequest.RenameCatalogRequest("catalog2"),
+ new CatalogUpdateRequest.UpdateCatalogCommentRequest("new
comment"),
+ new CatalogUpdateRequest.SetCatalogPropertyRequest("key", "new
value"),
+ new
CatalogUpdateRequest.RemoveCatalogPropertyRequest("old-key")));
+ doNothing().when(manager).testConnection(any(NameIdentifier.class),
any(CatalogChange[].class));
+ Response changedResponse =
+ target("/metalakes/metalake1/catalogs/catalog1/testConnection")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(proposedChanges,
MediaType.APPLICATION_JSON_TYPE));
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
changedResponse.getStatus());
+ Assertions.assertEquals(0,
changedResponse.readEntity(BaseResponse.class).getCode());
+ ArgumentCaptor<CatalogChange[]> changesCaptor =
ArgumentCaptor.forClass(CatalogChange[].class);
+ Mockito.verify(manager).testConnection(any(NameIdentifier.class),
changesCaptor.capture());
+ Assertions.assertArrayEquals(
+ new CatalogChange[] {
+ CatalogChange.rename("catalog2"),
+ CatalogChange.updateComment("new comment"),
+ CatalogChange.setProperty("key", "new value"),
+ CatalogChange.removeProperty("old-key")
+ },
+ changesCaptor.getValue());
+
+ doThrow(new IllegalArgumentException("invalid proposed change"))
+ .when(manager)
+ .testConnection(any(NameIdentifier.class), any(CatalogChange[].class));
+ Response invalidChangesResponse =
+ target("/metalakes/metalake1/catalogs/catalog1/testConnection")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(proposedChanges,
MediaType.APPLICATION_JSON_TYPE));
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
invalidChangesResponse.getStatus());
+ ErrorResponse invalidChanges =
invalidChangesResponse.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
invalidChanges.getCode());
+ Assertions.assertEquals("invalid proposed change",
invalidChanges.getMessage());
+ Assertions.assertNull(invalidChanges.getStack());
+
doThrow(new ConnectionFailedException("sanitized failure"))
.when(manager)
.testConnection(any(NameIdentifier.class));