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

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


The following commit(s) were added to refs/heads/main by this push:
     new 1c8ca73  Add NotificationType.VALIDATE which can serve as a dry-run of 
a CREATE without a metadata file (#321)
1c8ca73 is described below

commit 1c8ca73e9d2e736c4ad4fb6e639cef04db54ea68
Author: Dennis Huo <[email protected]>
AuthorDate: Fri Sep 27 15:08:24 2024 -0700

    Add NotificationType.VALIDATE which can serve as a dry-run of a CREATE 
without a metadata file (#321)
    
    If a remote catalog or manual caller wants to ensure that permissions, 
paths, etc., are configured
    correctly to receive CREATE/UPDATE notifications before deciding to 
actually create a table in the
    remote catalog, sending a VALIDATE notification with the prospective table 
metadata path can
    pre-validate basic setup. In a VALIDATE call, no actual entities will be 
mutated or created.
---
 .../service/catalog/BasePolarisCatalog.java        |  52 +++++++-
 .../polaris/service/types/NotificationType.java    |   3 +-
 .../service/catalog/BasePolarisCatalogTest.java    | 136 +++++++++++++++++++++
 .../PolarisCatalogHandlerWrapperAuthzTest.java     | 121 ++++++++++++------
 .../catalog/PolarisRestCatalogIntegrationTest.java |  24 +++-
 spec/rest-catalog-open-api.yaml                    |  12 +-
 6 files changed, 306 insertions(+), 42 deletions(-)

diff --git 
a/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
 
b/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
index 5e46d60..ff5dfef 100644
--- 
a/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
+++ 
b/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
@@ -171,7 +171,7 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
   private CloseableGroup closeableGroup;
   private Map<String, String> catalogProperties;
   private Map<String, String> tableDefaultProperties;
-  private final FileIOFactory fileIOFactory;
+  private FileIOFactory fileIOFactory;
   private PolarisMetaStoreManager metaStoreManager;
 
   /**
@@ -1613,6 +1613,11 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
     return metaStoreManager;
   }
 
+  @VisibleForTesting
+  void setFileIOFactory(FileIOFactory newFactory) {
+    this.fileIOFactory = newFactory;
+  }
+
   @VisibleForTesting
   long getCatalogId() {
     // TODO: Properly handle initialization
@@ -1873,6 +1878,51 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
     if (notificationType == NotificationType.DROP) {
       return dropTableLike(PolarisEntitySubType.TABLE, tableIdentifier, 
Map.of(), false /* purge */)
           .isSuccess();
+    } else if (notificationType == NotificationType.VALIDATE) {
+      // In this mode we don't want to make any mutations, so we won't 
auto-create non-existing
+      // parent namespaces. This means when we want to validate 
allowedLocations for the proposed
+      // table metadata location, we must independently find the deepest 
non-null parent namespace
+      // of the TableIdentifier, which may even be the base CatalogEntity if 
no parent namespaces
+      // actually exist yet. We can then extract the right StorageInfo entity 
via a normal call
+      // to findStorageInfoFromHierarchy.
+      PolarisResolvedPathWrapper resolvedStorageEntity = null;
+      Optional<PolarisEntity> storageInfoEntity = Optional.empty();
+      for (int i = tableIdentifier.namespace().length(); i >= 0; i--) {
+        Namespace nsLevel =
+            Namespace.of(
+                Arrays.stream(tableIdentifier.namespace().levels())
+                    .limit(i)
+                    .toArray(String[]::new));
+        resolvedStorageEntity = resolvedEntityView.getResolvedPath(nsLevel);
+        if (resolvedStorageEntity != null) {
+          storageInfoEntity = 
findStorageInfoFromHierarchy(resolvedStorageEntity);
+          break;
+        }
+      }
+
+      if (resolvedStorageEntity == null || storageInfoEntity.isEmpty()) {
+        throw new BadRequestException(
+            "Failed to find StorageInfo entity for TableIdentifier %s", 
tableIdentifier);
+      }
+
+      // Validate location against the resolvedStorageEntity
+      String metadataLocation =
+          
transformTableLikeLocation(request.getPayload().getMetadataLocation());
+      validateLocationForTableLike(tableIdentifier, metadataLocation, 
resolvedStorageEntity);
+
+      // Validate that we can construct a FileIO
+      String locationDir = metadataLocation.substring(0, 
metadataLocation.lastIndexOf("/"));
+      refreshIOWithCredentials(
+          tableIdentifier,
+          Set.of(locationDir),
+          resolvedStorageEntity,
+          new HashMap<>(tableDefaultProperties),
+          Set.of(PolarisStorageActions.READ));
+
+      LOGGER.debug(
+          "Successful VALIDATE notification for tableIdentifier {}, 
metadataLocation {}",
+          tableIdentifier,
+          metadataLocation);
     } else if (notificationType == NotificationType.CREATE
         || notificationType == NotificationType.UPDATE) {
 
diff --git 
a/polaris-service/src/main/java/org/apache/polaris/service/types/NotificationType.java
 
b/polaris-service/src/main/java/org/apache/polaris/service/types/NotificationType.java
index 3189a59..53d7d47 100644
--- 
a/polaris-service/src/main/java/org/apache/polaris/service/types/NotificationType.java
+++ 
b/polaris-service/src/main/java/org/apache/polaris/service/types/NotificationType.java
@@ -30,7 +30,8 @@ public enum NotificationType {
   UNKNOWN(0, "UNKNOWN"),
   CREATE(1, "CREATE"),
   UPDATE(2, "UPDATE"),
-  DROP(3, "DROP");
+  DROP(3, "DROP"),
+  VALIDATE(4, "VALIDATE");
 
   NotificationType(int id, String displayName) {
     this.id = id;
diff --git 
a/polaris-service/src/test/java/org/apache/polaris/service/catalog/BasePolarisCatalogTest.java
 
b/polaris-service/src/test/java/org/apache/polaris/service/catalog/BasePolarisCatalogTest.java
index 63f859b..1888bde 100644
--- 
a/polaris-service/src/test/java/org/apache/polaris/service/catalog/BasePolarisCatalogTest.java
+++ 
b/polaris-service/src/test/java/org/apache/polaris/service/catalog/BasePolarisCatalogTest.java
@@ -360,6 +360,142 @@ public class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCatalog> {
         .hasMessageContaining("Parent");
   }
 
+  @Test
+  public void testValidateNotificationWhenTableAndNamespacesDontExist() {
+    Assumptions.assumeTrue(
+        requiresNamespaceCreate(),
+        "Only applicable if namespaces must be created before adding 
children");
+    Assumptions.assumeTrue(
+        supportsNestedNamespaces(), "Only applicable if nested namespaces are 
supported");
+    Assumptions.assumeTrue(
+        supportsNotifications(), "Only applicable if notifications are 
supported");
+
+    final String tableLocation = 
"s3://externally-owned-bucket/validate_table/";
+    final String tableMetadataLocation = tableLocation + 
"metadata/v1.metadata.json";
+    BasePolarisCatalog catalog = catalog();
+
+    Namespace namespace = Namespace.of("parent", "child1");
+    TableIdentifier table = TableIdentifier.of(namespace, "table");
+
+    // For a VALIDATE request we can pass in a full metadata JSON filename or 
just the table's
+    // metadata directory; either way the path will be validated to be under 
the allowed locations,
+    // but any actual metadata JSON file will not be accessed.
+    NotificationRequest request = new NotificationRequest();
+    request.setNotificationType(NotificationType.VALIDATE);
+    TableUpdateNotification update = new TableUpdateNotification();
+    update.setMetadataLocation(tableMetadataLocation);
+    update.setTableName(table.name());
+    update.setTableUuid(UUID.randomUUID().toString());
+    update.setTimestamp(230950845L);
+    request.setPayload(update);
+
+    // We should be able to send the notification without creating the 
metadata file since it's
+    // only validating the ability to send the CREATE/UPDATE notification 
possibly before actually
+    // creating the table at all on the remote catalog.
+    Assertions.assertThat(catalog.sendNotification(table, request))
+        .as("Notification should be sent successfully")
+        .isTrue();
+    Assertions.assertThat(catalog.namespaceExists(namespace))
+        .as("Intermediate namespaces should not be created")
+        .isFalse();
+    Assertions.assertThat(catalog.tableExists(table))
+        .as("Table should not be created for a VALIDATE notification")
+        .isFalse();
+
+    // Now also check that despite creating the metadata file, the validation 
call still doesn't
+    // create any namespaces or tables.
+    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    fileIO.addFile(
+        tableMetadataLocation,
+        
TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes(UTF_8));
+
+    Assertions.assertThat(catalog.sendNotification(table, request))
+        .as("Notification should be sent successfully")
+        .isTrue();
+    Assertions.assertThat(catalog.namespaceExists(namespace))
+        .as("Intermediate namespaces should not be created")
+        .isFalse();
+    Assertions.assertThat(catalog.tableExists(table))
+        .as("Table should not be created for a VALIDATE notification")
+        .isFalse();
+  }
+
+  @Test
+  public void testValidateNotificationInDisallowedLocation() {
+    Assumptions.assumeTrue(
+        requiresNamespaceCreate(),
+        "Only applicable if namespaces must be created before adding 
children");
+    Assumptions.assumeTrue(
+        supportsNestedNamespaces(), "Only applicable if nested namespaces are 
supported");
+    Assumptions.assumeTrue(
+        supportsNotifications(), "Only applicable if notifications are 
supported");
+
+    // The location of the metadata JSON file specified in the create will be 
forbidden.
+    // For a VALIDATE call we can pass in the metadata/ prefix itself instead 
of a metadata JSON
+    // filename.
+    final String tableLocation = "s3://forbidden-table-location/table/";
+    final String tableMetadataLocation = tableLocation + "metadata/";
+    BasePolarisCatalog catalog = catalog();
+
+    Namespace namespace = Namespace.of("parent", "child1");
+    TableIdentifier table = TableIdentifier.of(namespace, "table");
+
+    NotificationRequest request = new NotificationRequest();
+    request.setNotificationType(NotificationType.VALIDATE);
+    TableUpdateNotification update = new TableUpdateNotification();
+    update.setMetadataLocation(tableMetadataLocation);
+    update.setTableName(table.name());
+    update.setTableUuid(UUID.randomUUID().toString());
+    update.setTimestamp(230950845L);
+    request.setPayload(update);
+
+    Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, 
request))
+        .isInstanceOf(ForbiddenException.class)
+        .hasMessageContaining("Invalid location");
+  }
+
+  @Test
+  public void testValidateNotificationFailToCreateFileIO() {
+    Assumptions.assumeTrue(
+        requiresNamespaceCreate(),
+        "Only applicable if namespaces must be created before adding 
children");
+    Assumptions.assumeTrue(
+        supportsNestedNamespaces(), "Only applicable if nested namespaces are 
supported");
+    Assumptions.assumeTrue(
+        supportsNotifications(), "Only applicable if notifications are 
supported");
+
+    // The location of the metadata JSON file specified in the create will be 
allowed, but
+    // we'll inject a separate ForbiddenException during FileIO instantiation.
+    // For a VALIDATE call we can pass in the metadata/ prefix itself instead 
of a metadata JSON
+    // filename.
+    final String tableLocation = 
"s3://externally-owned-bucket/validate_table/";
+    final String tableMetadataLocation = tableLocation + "metadata/";
+    BasePolarisCatalog catalog = catalog();
+
+    Namespace namespace = Namespace.of("parent", "child1");
+    TableIdentifier table = TableIdentifier.of(namespace, "table");
+
+    NotificationRequest request = new NotificationRequest();
+    request.setNotificationType(NotificationType.VALIDATE);
+    TableUpdateNotification update = new TableUpdateNotification();
+    update.setMetadataLocation(tableMetadataLocation);
+    update.setTableName(table.name());
+    update.setTableUuid(UUID.randomUUID().toString());
+    update.setTimestamp(230950845L);
+    request.setPayload(update);
+
+    catalog.setFileIOFactory(
+        new FileIOFactory() {
+          @Override
+          public FileIO loadFileIO(String impl, Map<String, String> 
properties) {
+            throw new ForbiddenException("Fake failure applying downscoped 
credentials");
+          }
+        });
+    Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, 
request))
+        .isInstanceOf(ForbiddenException.class)
+        .hasMessageContaining("Fake failure applying downscoped credentials");
+  }
+
   @Test
   public void testUpdateNotificationWhenTableAndNamespacesDontExist() {
     Assumptions.assumeTrue(
diff --git 
a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
 
b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
index 537f585..0f6fc41 100644
--- 
a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
+++ 
b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
@@ -1636,33 +1636,43 @@ public class PolarisCatalogHandlerWrapperAuthzTest 
extends PolarisAuthzTestBase
 
     String tableUuid = UUID.randomUUID().toString();
 
-    NotificationRequest request = new NotificationRequest();
-    request.setNotificationType(NotificationType.CREATE);
-    TableUpdateNotification update = new TableUpdateNotification();
-    update.setMetadataLocation(
+    NotificationRequest createRequest = new NotificationRequest();
+    createRequest.setNotificationType(NotificationType.CREATE);
+    TableUpdateNotification createPayload = new TableUpdateNotification();
+    createPayload.setMetadataLocation(
         String.format("%s/bucket/table/metadata/v1.metadata.json", 
storageLocation));
-    update.setTableName(table.name());
-    update.setTableUuid(tableUuid);
-    update.setTimestamp(230950845L);
-    request.setPayload(update);
-
-    NotificationRequest request2 = new NotificationRequest();
-    request2.setNotificationType(NotificationType.UPDATE);
-    TableUpdateNotification update2 = new TableUpdateNotification();
-    update2.setMetadataLocation(
+    createPayload.setTableName(table.name());
+    createPayload.setTableUuid(tableUuid);
+    createPayload.setTimestamp(230950845L);
+    createRequest.setPayload(createPayload);
+
+    NotificationRequest updateRequest = new NotificationRequest();
+    updateRequest.setNotificationType(NotificationType.UPDATE);
+    TableUpdateNotification updatePayload = new TableUpdateNotification();
+    updatePayload.setMetadataLocation(
         String.format("%s/bucket/table/metadata/v2.metadata.json", 
storageLocation));
-    update2.setTableName(table.name());
-    update2.setTableUuid(tableUuid);
-    update2.setTimestamp(330950845L);
-    request2.setPayload(update2);
-
-    NotificationRequest request3 = new NotificationRequest();
-    request3.setNotificationType(NotificationType.DROP);
-    TableUpdateNotification update3 = new TableUpdateNotification();
-    update3.setTableName(table.name());
-    update3.setTableUuid(tableUuid);
-    update3.setTimestamp(430950845L);
-    request3.setPayload(update3);
+    updatePayload.setTableName(table.name());
+    updatePayload.setTableUuid(tableUuid);
+    updatePayload.setTimestamp(330950845L);
+    updateRequest.setPayload(updatePayload);
+
+    NotificationRequest dropRequest = new NotificationRequest();
+    dropRequest.setNotificationType(NotificationType.DROP);
+    TableUpdateNotification dropPayload = new TableUpdateNotification();
+    dropPayload.setTableName(table.name());
+    dropPayload.setTableUuid(tableUuid);
+    dropPayload.setTimestamp(430950845L);
+    dropRequest.setPayload(dropPayload);
+
+    NotificationRequest validateRequest = new NotificationRequest();
+    validateRequest.setNotificationType(NotificationType.VALIDATE);
+    TableUpdateNotification validatePayload = new TableUpdateNotification();
+    validatePayload.setMetadataLocation(
+        String.format("%s/bucket/table/metadata/v1.metadata.json", 
storageLocation));
+    validatePayload.setTableName(table.name());
+    validatePayload.setTableUuid(tableUuid);
+    validatePayload.setTimestamp(530950845L);
+    validateRequest.setPayload(validatePayload);
 
     PolarisCallContextCatalogFactory factory =
         new PolarisCallContextCatalogFactory(
@@ -1697,13 +1707,14 @@ public class PolarisCatalogHandlerWrapperAuthzTest 
extends PolarisAuthzTestBase
                     .assignUUID()
                     .build();
             TableMetadataParser.overwrite(
-                tableMetadata, 
fileIO.newOutputFile(update.getMetadataLocation()));
+                tableMetadata, 
fileIO.newOutputFile(createPayload.getMetadataLocation()));
             TableMetadataParser.overwrite(
-                tableMetadata, 
fileIO.newOutputFile(update2.getMetadataLocation()));
+                tableMetadata, 
fileIO.newOutputFile(updatePayload.getMetadataLocation()));
             return catalog;
           }
         };
-    doTestSufficientPrivilegeSets(
+
+    List<Set<PolarisPrivilege>> sufficientPrivilegeSets =
         List.of(
             Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT),
             Set.of(PolarisPrivilege.TABLE_FULL_METADATA, 
PolarisPrivilege.NAMESPACE_FULL_METADATA),
@@ -1721,14 +1732,18 @@ public class PolarisCatalogHandlerWrapperAuthzTest 
extends PolarisAuthzTestBase
                 PolarisPrivilege.TABLE_DROP,
                 PolarisPrivilege.TABLE_WRITE_PROPERTIES,
                 PolarisPrivilege.NAMESPACE_CREATE,
-                PolarisPrivilege.NAMESPACE_DROP)),
+                PolarisPrivilege.NAMESPACE_DROP));
+    doTestSufficientPrivilegeSets(
+        sufficientPrivilegeSets,
         () -> {
           newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory)
-              .sendNotification(table, request);
+              .sendNotification(table, createRequest);
           newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory)
-              .sendNotification(table, request2);
+              .sendNotification(table, updateRequest);
           newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory)
-              .sendNotification(table, request3);
+              .sendNotification(table, dropRequest);
+          newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory)
+              .sendNotification(table, validateRequest);
         },
         () -> {
           newWrapper(Set.of(PRINCIPAL_ROLE2), externalCatalog, factory)
@@ -1738,6 +1753,17 @@ public class PolarisCatalogHandlerWrapperAuthzTest 
extends PolarisAuthzTestBase
         },
         PRINCIPAL_NAME,
         externalCatalog);
+
+    // Also test VALIDATE in isolation
+    doTestSufficientPrivilegeSets(
+        sufficientPrivilegeSets,
+        () -> {
+          newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory)
+              .sendNotification(table, validateRequest);
+        },
+        null /* cleanupAction */,
+        PRINCIPAL_NAME,
+        externalCatalog);
   }
 
   @Test
@@ -1746,7 +1772,6 @@ public class PolarisCatalogHandlerWrapperAuthzTest 
extends PolarisAuthzTestBase
     TableIdentifier table = TableIdentifier.of(namespace, "tbl1");
 
     NotificationRequest request = new NotificationRequest();
-    request.setNotificationType(NotificationType.UPDATE);
     TableUpdateNotification update = new TableUpdateNotification();
     
update.setMetadataLocation("file:///tmp/bucket/table/metadata/v1.metadata.json");
     update.setTableName(table.name());
@@ -1754,11 +1779,37 @@ public class PolarisCatalogHandlerWrapperAuthzTest 
extends PolarisAuthzTestBase
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    doTestInsufficientPrivileges(
+    List<PolarisPrivilege> insufficientPrivileges =
         List.of(
             PolarisPrivilege.NAMESPACE_FULL_METADATA,
             PolarisPrivilege.TABLE_FULL_METADATA,
-            PolarisPrivilege.VIEW_FULL_METADATA),
+            PolarisPrivilege.VIEW_FULL_METADATA);
+
+    // Independently test insufficient privileges in isolation.
+    request.setNotificationType(NotificationType.CREATE);
+    doTestInsufficientPrivileges(
+        insufficientPrivileges,
+        () -> {
+          newWrapper(Set.of(PRINCIPAL_ROLE1)).sendNotification(table, request);
+        });
+
+    request.setNotificationType(NotificationType.UPDATE);
+    doTestInsufficientPrivileges(
+        insufficientPrivileges,
+        () -> {
+          newWrapper(Set.of(PRINCIPAL_ROLE1)).sendNotification(table, request);
+        });
+
+    request.setNotificationType(NotificationType.DROP);
+    doTestInsufficientPrivileges(
+        insufficientPrivileges,
+        () -> {
+          newWrapper(Set.of(PRINCIPAL_ROLE1)).sendNotification(table, request);
+        });
+
+    request.setNotificationType(NotificationType.VALIDATE);
+    doTestInsufficientPrivileges(
+        insufficientPrivileges,
         () -> {
           newWrapper(Set.of(PRINCIPAL_ROLE1)).sendNotification(table, request);
         });
diff --git 
a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java
 
b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java
index 7b937fd..a7062bd 100644
--- 
a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java
+++ 
b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java
@@ -759,12 +759,28 @@ public class PolarisRestCatalogIntegrationTest extends 
CatalogTests<RESTCatalog>
             "s3://my-bucket/path/to/metadata.json",
             null));
     restCatalog.createNamespace(Namespace.of("ns1"));
+    String notificationUrl =
+        String.format(
+            
"http://localhost:%d/api/catalog/v1/%s/namespaces/ns1/tables/tbl1/notifications";,
+            EXT.getLocalPort(), currentCatalogName);
     try (Response response =
         EXT.client()
-            .target(
-                String.format(
-                    
"http://localhost:%d/api/catalog/v1/%s/namespaces/ns1/tables/tbl1/notifications";,
-                    EXT.getLocalPort(), currentCatalogName))
+            .target(notificationUrl)
+            .request("application/json")
+            .header("Authorization", "Bearer " + userToken)
+            .header(REALM_PROPERTY_KEY, realm)
+            .post(Entity.json(notification))) {
+      assertThat(response)
+          .returns(Response.Status.BAD_REQUEST.getStatusCode(), 
Response::getStatus)
+          .extracting(r -> r.readEntity(ErrorResponse.class))
+          .returns("Cannot update internal catalog via notifications", 
ErrorResponse::message);
+    }
+
+    // NotificationType.VALIDATE should also surface the same error.
+    notification.setNotificationType(NotificationType.VALIDATE);
+    try (Response response =
+        EXT.client()
+            .target(notificationUrl)
             .request("application/json")
             .header("Authorization", "Bearer " + userToken)
             .header(REALM_PROPERTY_KEY, realm)
diff --git a/spec/rest-catalog-open-api.yaml b/spec/rest-catalog-open-api.yaml
index 6bea02a..7c22d5b 100644
--- a/spec/rest-catalog-open-api.yaml
+++ b/spec/rest-catalog-open-api.yaml
@@ -984,6 +984,15 @@ paths:
           The responsibility of ensuring the correct order of timestamps for a 
sequence of notifications 
           lies with the caller of the API. This includes managing potential 
clock skew or inconsistencies 
           when notifications are sent from multiple sources.
+
+          A VALIDATE request behaves like a dry-run of a CREATE or UPDATE 
request up to but not including
+          loading the contents of a metadata file; this includes validations 
of permissions, the specified
+          metadata path being within ALLOWED_LOCATIONS, having an EXTERNAL 
catalog, etc. The intended use
+          case for a VALIDATE notification is to allow a remote catalog to 
pre-validate the general
+          settings of a receiving catalog against an intended new table 
location before possibly creating
+          a table intended for sending notifcations in the remote catalog at 
all. For a VALIDATE request,
+          the specified metadata-location can either be a prospective full 
metadata file path, or a
+          relevant parent directory of the intended table to validate against 
ALLOWED_LOCATIONS.
         content:
           application/json:
             schema:
@@ -3244,6 +3253,7 @@ components:
         - CREATE
         - UPDATE
         - DROP
+        - VALIDATE
 
     TableUpdateNotification:
       type: object
@@ -4181,4 +4191,4 @@ components:
             catalog: Allows interacting with the Config and Catalog APIs
     BearerAuth:
       type: http
-      scheme: bearer
\ No newline at end of file
+      scheme: bearer

Reply via email to