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

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


The following commit(s) were added to refs/heads/main by this push:
     new 74667849e4 [#13186] fix(lance): enforce table format boundary (#13187)
74667849e4 is described below

commit 74667849e4f8564e379d729efc7d086e5c89f231
Author: StormSpirit <[email protected]>
AuthorDate: Thu Sep 17 23:29:54 2026 +0800

    [#13186] fix(lance): enforce table format boundary (#13187)
    
    ### What changes were proposed in this pull request?
    
    This change enforces the stored table format at the Lance namespace
    boundary. Lance REST direct table operations validate that the existing
    entity is a Lance table, treat known non-Lance entities as absent for
    `tableExists`, and reject non-Lance metadata before describe, drop,
    deregister, or alter behavior is delegated to a format-specific
    operation. The Generic Catalog Lance delegator applies the same check to
    `EXIST_OK`, create `OVERWRITE`, register `OVERWRITE`, `purgeTable`, and
    `dropTable` paths before metadata or dataset deletion. The catalog-side
    guard uses `IllegalArgumentException`, and the REST adapter uses Lance
    `InvalidInputException` so the intended HTTP `400` response is
    preserved. Generic Catalog `ListTables` behavior is unchanged.
    
    ### Why are the changes needed?
    
    Lance requests select the Lance delegator from request properties, while
    existing table operations select a delegator from the stored entity
    format. With a mixed-format Generic Catalog, this mismatch allowed a
    Lance request to handle an existing Delta entity as if it were a Lance
    table. In particular, create `OVERWRITE` could purge non-Lance metadata
    and pass its stored location to Lance dataset deletion; register
    `OVERWRITE` could remove the existing metadata. Lance REST describe,
    existence, deregister, and alter calls also returned misleading results
    or format-dependent errors. These paths must fail closed at the Lance
    boundary.
    
    Fix: #13186
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. For a known non-Lance entity, Lance REST direct table operations
    now return HTTP `400 INVALID_INPUT`, and `tableExists` presents the
    entity as absent with the normal table-not-found response. Lance create
    `EXIST_OK`, create `OVERWRITE`, and register `OVERWRITE` now return HTTP
    `400 INVALID_INPUT` without changing the existing entity. Plain `CREATE`
    retains the normal existing-name conflict response. Valid Lance
    operations and the Generic Catalog's format-agnostic `ListTables`
    behavior remain unchanged.
    
    ### How was this patch tested?
    
    - Added unit coverage for the shared format predicate, REST adapter
    guards, Generic Catalog `EXIST_OK` and overwrite guards, metadata
    deletion prevention, and `IllegalArgumentException` propagation.
    - Added a mixed-format integration test that creates external Delta
    metadata with a sentinel file, exercises Lance REST and direct
    `TableCatalog` paths, and verifies metadata, location, and physical data
    preservation.
    - Added authorization coverage proving that an unauthorized caller
    receives `403` before non-Lance format validation.
    - Ran `./gradlew :lance:lance-rest-server:test
    :catalogs:catalog-lakehouse-generic:test -PtestMode=embedded
    -PlanceSparkBundleVersions=0.4.0 -PskipWeb=true`.
    - Ran `./gradlew :lance:lance-rest-server:test -PtestMode=embedded
    -PlanceSparkBundleVersions=0.4.0 -PskipWeb=true --tests
    '*LanceFormatBoundaryIT'`.
    - The final embedded regression completed 161 Lance REST server tests
    and 74 Generic Catalog tests with no failures or errors.
    - Deploy-mode validation was not run because the changed paths are
    covered by the embedded auxiliary-server and direct `TableCatalog`
    paths, and no deploy-only implementation was introduced.
    
    ---------
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../lakehouse/lance/LanceTableOperations.java      |  17 +-
 .../lakehouse/lance/TestLanceTableOperations.java  | 105 ++++++
 docs/lakehouse-generic-lance-table.md              |  16 +
 docs/lance-rest-integration.md                     |  25 ++
 .../gravitino/GravitinoLanceTableOperations.java   |  30 +-
 .../lance/common/utils/LancePropertiesUtils.java   |  11 +
 .../common/utils/TestLancePropertiesUtils.java     |  11 +
 .../gravitino/TestGravitinoLanceModeParsing.java   |   3 +
 .../TestGravitinoLanceTableOperations.java         |  80 ++++-
 .../integration/test/LanceFormatBoundaryIT.java    | 364 +++++++++++++++++++++
 .../test/LanceTableAuthorizationIT.java            |  31 ++
 11 files changed, 687 insertions(+), 6 deletions(-)

diff --git 
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
 
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
index 696e6c2da0..c4e31e196e 100644
--- 
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
+++ 
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
@@ -213,7 +213,9 @@ public class LanceTableOperations extends 
ManagedTableOperations {
           !register, "EXIST_OK mode is not supported for register operation");
 
       try {
-        return super.loadTable(ident);
+        Table table = super.loadTable(ident);
+        validateLanceTable(ident, table);
+        return table;
       } catch (NoSuchTableException e) {
         // Table doesn't exist, proceed with creation
       }
@@ -277,6 +279,7 @@ public class LanceTableOperations extends 
ManagedTableOperations {
       // Use super.loadTable to avoid triggering an unnecessary schema-refresh 
(which may open the
       // dataset) for a table that is about to be deleted anyway.
       Table table = super.loadTable(ident);
+      validateLanceTable(ident, table);
       boolean external =
           Optional.ofNullable(table.properties().get(Table.PROPERTY_EXTERNAL))
               .map(Boolean::parseBoolean)
@@ -303,6 +306,8 @@ public class LanceTableOperations extends 
ManagedTableOperations {
 
     } catch (NoSuchTableException e) {
       return false;
+    } catch (IllegalArgumentException e) {
+      throw e;
     } catch (Exception e) {
       throw ExceptionMessages.wrap("Failed to purge Lance dataset for table " 
+ ident, e);
     }
@@ -313,6 +318,7 @@ public class LanceTableOperations extends 
ManagedTableOperations {
     try {
       // Use super.loadTable to skip schema-refresh overhead when dropping.
       Table table = super.loadTable(ident);
+      validateLanceTable(ident, table);
       boolean external =
           Optional.ofNullable(table.properties().get(Table.PROPERTY_EXTERNAL))
               .map(Boolean::parseBoolean)
@@ -337,6 +343,8 @@ public class LanceTableOperations extends 
ManagedTableOperations {
 
     } catch (NoSuchTableException e) {
       return false;
+    } catch (IllegalArgumentException e) {
+      throw e;
     } catch (Exception e) {
       throw ExceptionMessages.wrap("Failed to drop Lance dataset for table " + 
ident, e);
     }
@@ -361,6 +369,13 @@ public class LanceTableOperations extends 
ManagedTableOperations {
     }
   }
 
+  private static void validateLanceTable(NameIdentifier ident, Table table) {
+    if (!LancePropertiesUtils.isLanceTableFormat(
+        table.properties().get(Table.PROPERTY_TABLE_FORMAT))) {
+      throw new IllegalArgumentException("Table is not a Lance table: " + 
ident);
+    }
+  }
+
   // Package-private for testing
   Table createTableInternal(
       NameIdentifier ident,
diff --git 
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
 
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
index 5d158a1665..53e5c2d919 100644
--- 
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
+++ 
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
@@ -125,6 +125,98 @@ public class TestLanceTableOperations {
                 new Index[0]));
   }
 
+  /** Verifies EXIST_OK does not return a non-Lance entity as a Lance table. */
+  @Test
+  public void testExistOkRejectsNonLanceTable() throws IOException {
+    NameIdentifier ident = NameIdentifier.of("schema", "table");
+    String location = tempDir.resolve("delta-exist-ok").toString();
+    when(store.get(eq(ident), eq(Entity.EntityType.TABLE), 
eq(TableEntity.class)))
+        .thenReturn(nonLanceTableEntity(ident, location));
+    Map<String, String> properties =
+        Map.of(
+            Table.PROPERTY_LOCATION,
+            location,
+            LANCE_CREATION_MODE,
+            "EXIST_OK",
+            Table.PROPERTY_TABLE_FORMAT,
+            "lance");
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                lanceTableOps.createTable(
+                    ident,
+                    new Column[0],
+                    null,
+                    properties,
+                    new Transform[0],
+                    null,
+                    new SortOrder[0],
+                    new Index[0]));
+
+    Assertions.assertTrue(exception.getMessage().contains("not a Lance 
table"));
+    verify(lanceTableOps, never()).openDataset(anyString(), any());
+    verify(store, never()).delete(any(), any());
+  }
+
+  /** Verifies both overwrite paths reject a non-Lance entity before mutation. 
*/
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  public void testOverwriteRejectsNonLanceTableBeforeMutation(boolean 
register) throws IOException {
+    NameIdentifier ident = NameIdentifier.of("schema", "table");
+    String location =
+        tempDir.resolve(register ? "delta-register-overwrite" : 
"delta-overwrite").toString();
+    when(store.get(eq(ident), eq(Entity.EntityType.TABLE), 
eq(TableEntity.class)))
+        .thenReturn(nonLanceTableEntity(ident, location));
+    Map<String, String> properties = Maps.newHashMap();
+    properties.put(Table.PROPERTY_LOCATION, location);
+    properties.put(Table.PROPERTY_TABLE_FORMAT, "lance");
+    properties.put(LANCE_CREATION_MODE, "OVERWRITE");
+    if (register) {
+      properties.put(LANCE_TABLE_REGISTER, "true");
+    }
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                lanceTableOps.createTable(
+                    ident,
+                    new Column[0],
+                    null,
+                    properties,
+                    new Transform[0],
+                    null,
+                    new SortOrder[0],
+                    new Index[0]));
+
+    Assertions.assertTrue(exception.getMessage().contains("not a Lance 
table"));
+    verify(store, never()).delete(any(), any());
+  }
+
+  /** Verifies the purge guard preserves the original 
IllegalArgumentException. */
+  @Test
+  public void testPurgeTablePropagatesNonLanceGuard() throws IOException {
+    NameIdentifier ident = NameIdentifier.of("schema", "table");
+    when(store.get(eq(ident), eq(Entity.EntityType.TABLE), 
eq(TableEntity.class)))
+        .thenReturn(nonLanceTableEntity(ident, 
tempDir.resolve("delta-purge").toString()));
+
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
lanceTableOps.purgeTable(ident));
+    verify(store, never()).delete(any(), any());
+  }
+
+  /** Verifies the drop guard preserves the original IllegalArgumentException. 
*/
+  @Test
+  public void testDropTablePropagatesNonLanceGuard() throws IOException {
+    NameIdentifier ident = NameIdentifier.of("schema", "table");
+    when(store.get(eq(ident), eq(Entity.EntityType.TABLE), 
eq(TableEntity.class)))
+        .thenReturn(nonLanceTableEntity(ident, 
tempDir.resolve("delta-drop").toString()));
+
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
lanceTableOps.dropTable(ident));
+    verify(store, never()).delete(any(), any());
+  }
+
   @Test
   public void testLoadDeclaredTableSchemaFromLocation() throws Exception {
     NameIdentifier ident = NameIdentifier.of("schema", "table");
@@ -1116,6 +1208,19 @@ public class TestLanceTableOperations {
         .build();
   }
 
+  private static TableEntity nonLanceTableEntity(NameIdentifier ident, String 
location) {
+    return tableEntity(
+        ident,
+        List.of(),
+        Map.of(
+            Table.PROPERTY_LOCATION,
+            location,
+            Table.PROPERTY_TABLE_FORMAT,
+            "delta",
+            Table.PROPERTY_EXTERNAL,
+            "true"));
+  }
+
   private void stubMutableTable(NameIdentifier ident, 
AtomicReference<TableEntity> storedTable)
       throws IOException {
     when(store.get(eq(ident), eq(Entity.EntityType.TABLE), 
eq(TableEntity.class)))
diff --git a/docs/lakehouse-generic-lance-table.md 
b/docs/lakehouse-generic-lance-table.md
index 2961527486..68d1a28dfc 100644
--- a/docs/lakehouse-generic-lance-table.md
+++ b/docs/lakehouse-generic-lance-table.md
@@ -114,6 +114,22 @@ Required and optional properties for tables in a Generic 
Lakehouse Catalog:
 - `EXIST_OK`: Create a new table if it does not exist, otherwise do nothing.
 - `OVERWRITE`: Create a new table, overwrite if the table already exists, it 
will delete the existing data directory first if the table is not a registered 
table and then create a new one.
 
+### Format boundary
+
+The Generic Catalog is format-agnostic for its general table APIs, but a Lance 
table operation must
+target an entity whose `format` property is `lance` (case-insensitive). Lance 
REST direct
+operations such as describe, drop, deregister, and alter reject a known 
non-Lance entity with
+HTTP `400 INVALID_INPUT`; `tableExists` presents it as absent. These checks 
preserve the existing
+metadata and location.
+
+For create requests dispatched to the Lance table delegator, an existing 
non-Lance entity remains
+a normal name conflict for `CREATE` (`409`). `EXIST_OK`, create `OVERWRITE`, 
and register
+`OVERWRITE` are rejected with `IllegalArgumentException` by the direct 
Gravitino API, which is
+returned as HTTP `400` by the Lance REST service. In particular, overwrite 
validation happens
+before metadata removal or a Lance dataset delete. Generic Catalog 
`ListTables` behavior is
+unchanged; it continues to list all formats. Format-specific listing is a 
separate follow-up
+concern.
+
 **Location Requirement:** Must be specified at catalog, schema, or table 
level. See [Location 
Resolution](./lakehouse-generic-catalog.md#key-property-location).
 
 Also set additional properties specific to your lakehouse format or custom 
requirements.
diff --git a/docs/lance-rest-integration.md b/docs/lance-rest-integration.md
index 18f581f522..39ab9f6c11 100644
--- a/docs/lance-rest-integration.md
+++ b/docs/lance-rest-integration.md
@@ -57,6 +57,31 @@ The following table outlines the tested compatibility 
between Gravitino versions
 - The Lance ecosystem is changing quickly, so some versions may introduce 
breaking changes.
 :::
 
+## Format boundary
+
+The Lance REST service is a Lance table namespace, even when its metadata 
backend is a
+format-agnostic Generic Catalog. The REST table operations therefore validate 
the stored
+`format` property before returning Lance metadata or applying a table mutation.
+
+When an identifier is occupied by a known non-Lance table, direct Lance table 
operations fail
+with HTTP `400` and an `INVALID_INPUT` error. `TableExists` presents that 
entry as absent and
+returns the normal table-not-found response. The underlying Generic Catalog 
metadata and storage
+location remain unchanged.
+
+The same boundary applies to create requests that target an existing entity 
through the Lance
+delegator:
+
+| Request mode         | Existing non-Lance entity                            |
+| -------------------- | ---------------------------------------------------- |
+| `CREATE`             | `409` conflict, as for any existing table name       |
+| `EXIST_OK`           | `400 INVALID_INPUT`                                  |
+| `OVERWRITE`          | `400 INVALID_INPUT`; metadata and data are preserved |
+| Register `OVERWRITE` | `400 INVALID_INPUT`; metadata and data are preserved |
+
+The validation is performed after the normal authorization checks. It does not 
convert existing
+Generic Catalog unknown-format loading errors or change the Generic Catalog's 
format-agnostic
+`ListTables` behavior.
+
 ### Reproducing the matrix locally
 
 Both connectors ship with a multi-version integration test driver so the
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
index 49bd407e7e..300e678246 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
@@ -50,6 +50,7 @@ import 
org.apache.gravitino.lance.common.utils.LancePropertiesUtils;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.TableChange;
+import org.lance.namespace.errors.InvalidInputException;
 import org.lance.namespace.errors.TableNotFoundException;
 import org.lance.namespace.model.AlterTableAlterColumnsRequest;
 import org.lance.namespace.model.AlterTableDropColumnsRequest;
@@ -129,7 +130,7 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
 
     Table table;
     try {
-      table = 
namespaceWrapper.asTableCatalog(catalog).loadTable(tableIdentifier);
+      table = loadAndValidateLanceTable(catalog, tableIdentifier, tableId);
     } catch (NoSuchTableException e) {
       throw new TableNotFoundException(
           "Table not found: " + tableId, CommonUtil.formatCurrentStackTrace(), 
tableId);
@@ -277,7 +278,7 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
         NameIdentifier.of(nsId.levelAtListPos(1), nsId.levelAtListPos(2));
     Table t;
     try {
-      t = namespaceWrapper.asTableCatalog(catalog).loadTable(tableIdentifier);
+      t = loadAndValidateLanceTable(catalog, tableIdentifier, tableId);
     } catch (NoSuchTableException e) {
       throw new TableNotFoundException(
           "Table not found: " + tableId, CommonUtil.formatCurrentStackTrace(), 
tableId);
@@ -323,7 +324,16 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
     NameIdentifier tableIdentifier =
         NameIdentifier.of(nsId.levelAtListPos(1), nsId.levelAtListPos(2));
 
-    return 
namespaceWrapper.asTableCatalog(catalog).tableExists(tableIdentifier);
+    try {
+      return LancePropertiesUtils.isLanceTableFormat(
+          namespaceWrapper
+              .asTableCatalog(catalog)
+              .loadTable(tableIdentifier)
+              .properties()
+              .get(Table.PROPERTY_TABLE_FORMAT));
+    } catch (NoSuchTableException e) {
+      return false;
+    }
   }
 
   @Override
@@ -340,7 +350,7 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
 
     Table table;
     try {
-      table = 
namespaceWrapper.asTableCatalog(catalog).loadTable(tableIdentifier);
+      table = loadAndValidateLanceTable(catalog, tableIdentifier, tableId);
     } catch (NoSuchTableException e) {
       throw new TableNotFoundException(
           "Table not found: " + tableId, CommonUtil.formatCurrentStackTrace(), 
tableId);
@@ -378,6 +388,7 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
     }
     TableChange[] changes = handler.buildGravitinoTableChange(request);
 
+    loadAndValidateLanceTable(catalog, tableIdentifier, tableId);
     Table table = 
namespaceWrapper.asTableCatalog(catalog).alterTable(tableIdentifier, changes);
 
     return handler.handle(table, request);
@@ -389,6 +400,17 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
     return (GravitinoLanceTableAlterHandler<REQUEST, RESPONSE>) 
ALTER_HANDLERS.get(requestClass);
   }
 
+  private Table loadAndValidateLanceTable(
+      Catalog catalog, NameIdentifier tableIdentifier, String tableId) {
+    Table table = 
namespaceWrapper.asTableCatalog(catalog).loadTable(tableIdentifier);
+    if (!LancePropertiesUtils.isLanceTableFormat(
+        table.properties().get(Table.PROPERTY_TABLE_FORMAT))) {
+      throw new InvalidInputException(
+          "Table is not a Lance table: " + tableId, 
CommonUtil.formatCurrentStackTrace(), tableId);
+    }
+    return table;
+  }
+
   private List<Column> 
extractColumns(org.apache.arrow.vector.types.pojo.Schema arrowSchema) {
     List<Column> columns = new ArrayList<>();
 
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
index 4d235afcf8..aba4087aa2 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.lance.common.utils;
 
 import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
+import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_TABLE_FORMAT;
 
 import com.google.common.base.Preconditions;
 import java.util.LinkedHashMap;
@@ -33,6 +34,16 @@ public final class LancePropertiesUtils {
     // Utility class.
   }
 
+  /**
+   * Returns whether the supplied table format identifies a Lance table.
+   *
+   * @param tableFormat the table format, which may be null
+   * @return true when the format is Lance, ignoring case
+   */
+  public static boolean isLanceTableFormat(String tableFormat) {
+    return LANCE_TABLE_FORMAT.equalsIgnoreCase(tableFormat);
+  }
+
   /**
    * Extracts Lance storage options from a property map.
    *
diff --git 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
index 51de5b37e1..3a862c7c06 100644
--- 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
+++ 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
@@ -18,6 +18,8 @@
  */
 package org.apache.gravitino.lance.common.utils;
 
+import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_TABLE_FORMAT;
+
 import com.google.common.collect.ImmutableMap;
 import java.util.ArrayList;
 import java.util.List;
@@ -27,6 +29,15 @@ import org.junit.jupiter.api.Test;
 
 public class TestLancePropertiesUtils {
 
+  /** Verifies the binary Lance format predicate's case and null handling. */
+  @Test
+  public void testIsLanceTableFormatIsCaseInsensitiveAndNullSafe() {
+    
Assertions.assertTrue(LancePropertiesUtils.isLanceTableFormat(LANCE_TABLE_FORMAT));
+    Assertions.assertTrue(LancePropertiesUtils.isLanceTableFormat("LANCE"));
+    Assertions.assertFalse(LancePropertiesUtils.isLanceTableFormat("delta"));
+    Assertions.assertFalse(LancePropertiesUtils.isLanceTableFormat(null));
+  }
+
   @Test
   public void testGetLanceStorageOptions() {
     Map<String, String> properties =
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
index e18820bb9e..f2c390add5 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceModeParsing.java
@@ -22,6 +22,7 @@ import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_CREAT
 import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_LOCATION;
 import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
 import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_TABLE_DECLARED;
+import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_TABLE_FORMAT;
 import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_TABLE_VERSION;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyMap;
@@ -206,6 +207,8 @@ class TestGravitinoLanceModeParsing {
             Map.of(
                 LANCE_LOCATION,
                 "/tmp/table",
+                Table.PROPERTY_TABLE_FORMAT,
+                LANCE_TABLE_FORMAT,
                 LANCE_TABLE_DECLARED,
                 "true",
                 LANCE_STORAGE_OPTIONS_PREFIX + "region",
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceTableOperations.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceTableOperations.java
index a291e80ce1..079a3a1cd5 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceTableOperations.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceTableOperations.java
@@ -33,6 +33,7 @@ import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.TableChange;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.lance.namespace.errors.InvalidInputException;
 import org.lance.namespace.model.AlterColumnsEntry;
 import org.lance.namespace.model.AlterTableAlterColumnsRequest;
 import org.lance.namespace.model.AlterTableAlterColumnsResponse;
@@ -101,7 +102,8 @@ class TestGravitinoLanceTableOperations {
   void testDeregisterTableRejectsManagedTable() {
     // Mock a managed table (no PROPERTY_EXTERNAL=true)
     Table managedTable = Mockito.mock(Table.class);
-    Mockito.when(managedTable.properties()).thenReturn(new HashMap<>());
+    Mockito.when(managedTable.properties())
+        .thenReturn(new HashMap<>(Map.of(Table.PROPERTY_TABLE_FORMAT, 
"lance")));
 
     TableCatalog tableCatalog = Mockito.mock(TableCatalog.class);
     Mockito.when(tableCatalog.loadTable(Mockito.any(NameIdentifier.class)))
@@ -126,4 +128,80 @@ class TestGravitinoLanceTableOperations {
     // Verify dropTable was never called — the guard must reject before 
reaching the catalog layer.
     Mockito.verify(tableCatalog, Mockito.never()).dropTable(Mockito.any());
   }
+
+  @Test
+  void testDescribeTableRejectsNonLanceTable() {
+    TableCatalog tableCatalog = tableCatalogWithTable("delta");
+    GravitinoLanceTableOperations ops = operations(tableCatalog);
+
+    InvalidInputException exception =
+        Assertions.assertThrows(
+            InvalidInputException.class,
+            () ->
+                ops.describeTable(
+                    "catalog.schema.table", ".", java.util.Optional.empty(), 
false, false));
+
+    Assertions.assertTrue(exception.getMessage().contains("not a Lance 
table"));
+  }
+
+  @Test
+  void testTableExistsTreatsNonLanceTableAsAbsent() {
+    TableCatalog tableCatalog = tableCatalogWithTable("delta");
+    GravitinoLanceTableOperations ops = operations(tableCatalog);
+
+    Assertions.assertFalse(ops.tableExists("catalog.schema.table", "."));
+    Mockito.verify(tableCatalog, Mockito.never()).tableExists(Mockito.any());
+  }
+
+  @Test
+  void testDropTableRejectsNonLanceTableBeforePurge() {
+    TableCatalog tableCatalog = tableCatalogWithTable("delta");
+    GravitinoLanceTableOperations ops = operations(tableCatalog);
+
+    Assertions.assertThrows(
+        InvalidInputException.class, () -> 
ops.dropTable("catalog.schema.table", "."));
+
+    Mockito.verify(tableCatalog, Mockito.never()).purgeTable(Mockito.any());
+  }
+
+  @Test
+  void testDeregisterTableRejectsNonLanceTableBeforeDrop() {
+    TableCatalog tableCatalog = tableCatalogWithTable("delta");
+    GravitinoLanceTableOperations ops = operations(tableCatalog);
+
+    Assertions.assertThrows(
+        InvalidInputException.class, () -> 
ops.deregisterTable("catalog.schema.table", "."));
+
+    Mockito.verify(tableCatalog, Mockito.never()).dropTable(Mockito.any());
+  }
+
+  @Test
+  void testAlterTableRejectsNonLanceTableBeforeAlter() {
+    TableCatalog tableCatalog = tableCatalogWithTable("delta");
+    GravitinoLanceTableOperations ops = operations(tableCatalog);
+    AlterTableDropColumnsRequest request = new AlterTableDropColumnsRequest();
+    request.setColumns(List.of("col1"));
+
+    Assertions.assertThrows(
+        InvalidInputException.class, () -> 
ops.alterTable("catalog.schema.table", ".", request));
+
+    Mockito.verify(tableCatalog, Mockito.never()).alterTable(Mockito.any(), 
Mockito.any());
+  }
+
+  private static TableCatalog tableCatalogWithTable(String format) {
+    Table table = Mockito.mock(Table.class);
+    Mockito.when(table.properties())
+        .thenReturn(Map.of(Table.PROPERTY_TABLE_FORMAT, format, 
Table.PROPERTY_EXTERNAL, "true"));
+    TableCatalog tableCatalog = Mockito.mock(TableCatalog.class);
+    
Mockito.when(tableCatalog.loadTable(Mockito.any(NameIdentifier.class))).thenReturn(table);
+    return tableCatalog;
+  }
+
+  private static GravitinoLanceTableOperations operations(TableCatalog 
tableCatalog) {
+    Catalog catalog = Mockito.mock(Catalog.class);
+    GravitinoLanceNamespaceWrapper wrapper = 
Mockito.mock(GravitinoLanceNamespaceWrapper.class);
+    
Mockito.when(wrapper.loadAndValidateLakehouseCatalog(Mockito.anyString())).thenReturn(catalog);
+    Mockito.when(wrapper.asTableCatalog(catalog)).thenReturn(tableCatalog);
+    return new GravitinoLanceTableOperations(wrapper);
+  }
 }
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceFormatBoundaryIT.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceFormatBoundaryIT.java
new file mode 100644
index 0000000000..c23e63dc7a
--- /dev/null
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceFormatBoundaryIT.java
@@ -0,0 +1,364 @@
+/*
+ * 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.gravitino.lance.integration.test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.commons.io.FileUtils;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.lance.common.utils.ArrowUtils;
+import org.apache.gravitino.lance.common.utils.LanceConstants;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.rel.types.Types;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.lance.namespace.LanceNamespace;
+import org.lance.namespace.client.apache.ApiClient;
+import org.lance.namespace.client.apache.ApiException;
+import org.lance.namespace.client.apache.api.TableApi;
+import org.lance.namespace.errors.ErrorCode;
+import org.lance.namespace.errors.LanceNamespaceException;
+import org.lance.namespace.model.AlterTableDropColumnsRequest;
+import org.lance.namespace.model.DeclareTableRequest;
+import org.lance.namespace.model.DeregisterTableRequest;
+import org.lance.namespace.model.DescribeTableRequest;
+import org.lance.namespace.model.DropTableRequest;
+import org.lance.namespace.model.RegisterTableRequest;
+import org.lance.namespace.model.TableExistsRequest;
+
+/** Integration coverage for the Lance/non-Lance table format boundary. */
+public class LanceFormatBoundaryIT extends BaseIT {
+  private static final String CATALOG_NAME =
+      GravitinoITUtils.genRandomName("lance_boundary_catalog");
+  private static final String SCHEMA_NAME = 
GravitinoITUtils.genRandomName("lance_boundary_schema");
+  private static final String DELIMITER = ".";
+
+  private GravitinoMetalake metalake;
+  private Catalog catalog;
+  private LanceNamespace namespace;
+  private final BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE);
+  private Path tempDir;
+
+  /** Starts an embedded Gravitino server with the Lance auxiliary service. */
+  @BeforeAll
+  public void startIntegrationTest() throws Exception {
+    ignoreLanceAuxRestService = false;
+    super.startIntegrationTest();
+    metalake =
+        client.createMetalake(getLanceRESTServerMetalakeName(), "Lance format 
boundary IT", null);
+    catalog =
+        metalake.createCatalog(
+            CATALOG_NAME,
+            Catalog.Type.RELATIONAL,
+            "lakehouse-generic",
+            "Lance format boundary catalog",
+            ImmutableMap.of());
+    catalog.asSchemas().createSchema(SCHEMA_NAME, "Lance format boundary 
schema", null);
+    namespace =
+        LanceNamespace.connect(
+            "rest",
+            ImmutableMap.of("uri", getLanceRestServiceUrl(), "delimiter", 
DELIMITER),
+            allocator);
+    tempDir = Files.createTempDirectory("lance_format_boundary_it_");
+  }
+
+  /** Stops the test services and removes test metadata and temporary files. */
+  @AfterAll
+  public void clean() throws Exception {
+    Exception failure = null;
+    try {
+      if (client != null) {
+        client.dropMetalake(getLanceRESTServerMetalakeName(), true);
+      }
+    } catch (Exception e) {
+      failure = e;
+    }
+
+    try {
+      if (tempDir != null) {
+        FileUtils.deleteDirectory(tempDir.toFile());
+      }
+    } catch (Exception e) {
+      if (failure == null) {
+        failure = e;
+      } else {
+        failure.addSuppressed(e);
+      }
+    }
+
+    try {
+      allocator.close();
+    } catch (Exception e) {
+      if (failure == null) {
+        failure = e;
+      } else {
+        failure.addSuppressed(e);
+      }
+    }
+
+    try {
+      super.stopIntegrationTest();
+    } catch (Exception e) {
+      if (failure == null) {
+        failure = e;
+      } else {
+        failure.addSuppressed(e);
+      }
+    }
+
+    if (failure != null) {
+      throw failure;
+    }
+  }
+
+  /** Verifies that non-Lance metadata and data are preserved by all rejected 
Lance operations. */
+  @Test
+  public void testNonLanceTablesFailClosedWithoutDataLoss() throws IOException 
{
+    String deltaTableName = "delta_boundary_table";
+    String deltaLocation = tempDir.resolve(deltaTableName).toString();
+    Path sentinel = 
Files.createDirectories(Path.of(deltaLocation)).resolve("sentinel");
+    Files.writeString(sentinel, "must survive");
+    NameIdentifier deltaIdentifier = NameIdentifier.of(SCHEMA_NAME, 
deltaTableName);
+    catalog
+        .asTableCatalog()
+        .createTable(
+            deltaIdentifier,
+            new Column[] {Column.of("id", Types.IntegerType.get(), "id")},
+            null,
+            ImmutableMap.of(
+                Table.PROPERTY_LOCATION,
+                deltaLocation,
+                Table.PROPERTY_TABLE_FORMAT,
+                "delta",
+                Table.PROPERTY_EXTERNAL,
+                "true"));
+
+    List<String> deltaIds = List.of(CATALOG_NAME, SCHEMA_NAME, deltaTableName);
+    DescribeTableRequest describeRequest = new DescribeTableRequest();
+    describeRequest.setId(deltaIds);
+    LanceNamespaceException describeException =
+        Assertions.assertThrows(
+            LanceNamespaceException.class, () -> 
namespace.describeTable(describeRequest));
+    assertLanceErrorCode(describeException, ErrorCode.INVALID_INPUT);
+
+    Table deltaTable = catalog.asTableCatalog().loadTable(deltaIdentifier);
+    String originalDeltaLocation = 
deltaTable.properties().get(Table.PROPERTY_LOCATION);
+    Assertions.assertEquals("delta", 
deltaTable.properties().get(Table.PROPERTY_TABLE_FORMAT));
+
+    TableCatalog tableCatalog = catalog.asTableCatalog();
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            tableCatalog.createTable(
+                deltaIdentifier, new Column[0], null, 
overwriteProperties(deltaLocation, false)));
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            tableCatalog.createTable(
+                deltaIdentifier, new Column[0], null, 
overwriteProperties(deltaLocation, true)));
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    ApiException existOkException =
+        Assertions.assertThrows(
+            ApiException.class,
+            () ->
+                createTableApi()
+                    .createTable(
+                        String.join(DELIMITER, deltaIds),
+                        arrowBody(),
+                        DELIMITER,
+                        "exist_ok",
+                        null,
+                        null,
+                        Map.of(
+                            LanceConstants.LANCE_TABLE_LOCATION_HEADER,
+                            tempDir.resolve("rest_exist_ok").toString())));
+    Assertions.assertEquals(400, existOkException.getCode());
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    ApiException overwriteException =
+        Assertions.assertThrows(
+            ApiException.class,
+            () ->
+                createTableApi()
+                    .createTable(
+                        String.join(DELIMITER, deltaIds),
+                        arrowBody(),
+                        DELIMITER,
+                        "overwrite",
+                        null,
+                        null,
+                        Map.of(
+                            LanceConstants.LANCE_TABLE_LOCATION_HEADER,
+                            tempDir.resolve("rest_overwrite").toString())));
+    Assertions.assertEquals(400, overwriteException.getCode());
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    RegisterTableRequest registerRequest = new RegisterTableRequest();
+    registerRequest.setId(deltaIds);
+    
registerRequest.setLocation(tempDir.resolve("rest_register_overwrite").toString());
+    registerRequest.setMode("overwrite");
+    LanceNamespaceException registerException =
+        Assertions.assertThrows(
+            LanceNamespaceException.class, () -> 
namespace.registerTable(registerRequest));
+    assertLanceErrorCode(registerException, ErrorCode.INVALID_INPUT);
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    ApiException createException =
+        Assertions.assertThrows(
+            ApiException.class,
+            () ->
+                createTableApi()
+                    .createTable(
+                        String.join(DELIMITER, deltaIds),
+                        arrowBody(),
+                        DELIMITER,
+                        "create",
+                        null,
+                        null,
+                        Map.of(
+                            LanceConstants.LANCE_TABLE_LOCATION_HEADER,
+                            tempDir.resolve("rest_create").toString())));
+    Assertions.assertEquals(409, createException.getCode());
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    LanceNamespaceException existsException =
+        Assertions.assertThrows(
+            LanceNamespaceException.class,
+            () -> namespace.tableExists(tableExistsRequest(deltaIds)));
+    assertLanceErrorCode(existsException, ErrorCode.TABLE_NOT_FOUND);
+    Table remainingDeltaTable = tableCatalog.loadTable(deltaIdentifier);
+    Assertions.assertEquals(
+        "delta", 
remainingDeltaTable.properties().get(Table.PROPERTY_TABLE_FORMAT));
+    Assertions.assertEquals(
+        originalDeltaLocation, 
remainingDeltaTable.properties().get(Table.PROPERTY_LOCATION));
+
+    LanceNamespaceException dropException =
+        Assertions.assertThrows(
+            LanceNamespaceException.class, () -> 
namespace.dropTable(dropTableRequest(deltaIds)));
+    assertLanceErrorCode(dropException, ErrorCode.INVALID_INPUT);
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    LanceNamespaceException deregisterException =
+        Assertions.assertThrows(
+            LanceNamespaceException.class,
+            () -> namespace.deregisterTable(deregisterTableRequest(deltaIds)));
+    assertLanceErrorCode(deregisterException, ErrorCode.INVALID_INPUT);
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    AlterTableDropColumnsRequest alterRequest = new 
AlterTableDropColumnsRequest();
+    alterRequest.setId(deltaIds);
+    alterRequest.setColumns(List.of("id"));
+    ApiException alterException =
+        Assertions.assertThrows(
+            ApiException.class,
+            () ->
+                createTableApi()
+                    .alterTableDropColumns(
+                        String.join(DELIMITER, deltaIds), alterRequest, 
DELIMITER));
+    Assertions.assertEquals(400, alterException.getCode());
+    Assertions.assertTrue(Files.exists(sentinel));
+
+    String lanceTableName = "lance_boundary_table";
+    List<String> lanceIds = List.of(CATALOG_NAME, SCHEMA_NAME, lanceTableName);
+    DeclareTableRequest declareRequest = new DeclareTableRequest();
+    declareRequest.setId(lanceIds);
+    declareRequest.setLocation(tempDir.resolve(lanceTableName).toString());
+    Assertions.assertDoesNotThrow(() -> 
namespace.declareTable(declareRequest));
+
+    DescribeTableRequest lanceDescribeRequest = new DescribeTableRequest();
+    lanceDescribeRequest.setId(lanceIds);
+    Assertions.assertEquals(
+        "lance",
+        namespace
+            .describeTable(lanceDescribeRequest)
+            .getMetadata()
+            .get(Table.PROPERTY_TABLE_FORMAT));
+    Assertions.assertDoesNotThrow(() -> 
namespace.tableExists(tableExistsRequest(lanceIds)));
+  }
+
+  private Map<String, String> overwriteProperties(String location, boolean 
register) {
+    Map<String, String> properties = Maps.newHashMap();
+    properties.put(Table.PROPERTY_LOCATION, location);
+    properties.put(Table.PROPERTY_TABLE_FORMAT, "lance");
+    properties.put(Table.PROPERTY_EXTERNAL, "true");
+    properties.put(LanceConstants.LANCE_CREATION_MODE, "OVERWRITE");
+    if (register) {
+      properties.put(LanceConstants.LANCE_TABLE_REGISTER, "true");
+    }
+    return properties;
+  }
+
+  private TableExistsRequest tableExistsRequest(List<String> ids) {
+    TableExistsRequest request = new TableExistsRequest();
+    request.setId(ids);
+    return request;
+  }
+
+  private DropTableRequest dropTableRequest(List<String> ids) {
+    DropTableRequest request = new DropTableRequest();
+    request.setId(ids);
+    return request;
+  }
+
+  private DeregisterTableRequest deregisterTableRequest(List<String> ids) {
+    DeregisterTableRequest request = new DeregisterTableRequest();
+    request.setId(ids);
+    return request;
+  }
+
+  private TableApi createTableApi() {
+    return new TableApi(new ApiClient().setBasePath(getLanceRestServiceUrl()));
+  }
+
+  private static byte[] arrowBody() throws IOException {
+    return ArrowUtils.generateIpcStream(
+        new Schema(List.of(Field.nullable("id", new ArrowType.Int(32, 
true)))));
+  }
+
+  private static void assertLanceErrorCode(
+      RuntimeException exception, ErrorCode expectedErrorCode) {
+    Assertions.assertInstanceOf(LanceNamespaceException.class, exception);
+    Assertions.assertEquals(
+        expectedErrorCode.getCode(), ((LanceNamespaceException) 
exception).getCode());
+  }
+
+  private String getLanceRestServiceUrl() {
+    return String.format("http://%s:%d/lance";, "localhost", 
getLanceRESTServerPort());
+  }
+}
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
index c7722b0523..36f41c8b3e 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
@@ -28,12 +28,15 @@ import java.util.ArrayList;
 import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import org.apache.arrow.vector.types.pojo.ArrowType;
 import org.apache.arrow.vector.types.pojo.Field;
 import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.authorization.Owner;
 import org.apache.gravitino.authorization.Privileges;
@@ -43,6 +46,9 @@ import org.apache.gravitino.client.GravitinoMetalake;
 import org.apache.gravitino.integration.test.util.BaseIT;
 import org.apache.gravitino.lance.common.utils.ArrowUtils;
 import org.apache.gravitino.lance.common.utils.LanceConstants;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.types.Types;
 import org.apache.gravitino.server.web.ObjectMapperProvider;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
@@ -85,6 +91,7 @@ public class LanceTableAuthorizationIT extends BaseIT {
   private static final String DROP_TABLE = "g_drop_table";
   private static final String OWNED_SCHEMA_TABLE = "h_owned_schema_table";
   private static final String LONE_SCHEMA_TABLE = "i_lone_schema_table";
+  private static final String NON_LANCE_TABLE = "j_non_lance_table";
   private static final String DELIMITER = ".";
 
   @TempDir private static Path tempDir;
@@ -117,6 +124,20 @@ public class LanceTableAuthorizationIT extends BaseIT {
     registerTable(VISIBLE_TABLE);
     registerTable(HIDDEN_TABLE);
     createTable(WRITE_SCHEMA, MUTABLE_TABLE);
+    Catalog catalog = metalake.loadCatalog(CATALOG);
+    catalog
+        .asTableCatalog()
+        .createTable(
+            NameIdentifier.of(WRITE_SCHEMA, NON_LANCE_TABLE),
+            new Column[] {Column.of("id", Types.IntegerType.get(), "id")},
+            null,
+            Map.of(
+                Table.PROPERTY_TABLE_FORMAT,
+                "delta",
+                Table.PROPERTY_LOCATION,
+                location(NON_LANCE_TABLE),
+                Table.PROPERTY_EXTERNAL,
+                "true"));
     // Registered by the admin, so neither owner below owns the table itself.
     assertStatus(
         200, register(ADMIN, OWNED_SCHEMA, OWNED_SCHEMA_TABLE, null, 
location(OWNED_SCHEMA_TABLE)));
@@ -230,6 +251,16 @@ public class LanceTableAuthorizationIT extends BaseIT {
     assertStatus(200, table(ADMIN, HIDDEN_TABLE, "describe"));
   }
 
+  /** Verifies authorization rejects an inaccessible non-Lance table before 
format validation. */
+  @Test
+  public void testAuthorizationRunsBeforeFormatValidation() throws Exception {
+    HttpResponse<String> denied = table(READER, WRITE_SCHEMA, NON_LANCE_TABLE, 
"describe");
+    assertStatus(403, denied);
+    Assertions.assertFalse(denied.body().contains(location(NON_LANCE_TABLE)), 
denied.body());
+
+    assertStatus(400, table(ADMIN, WRITE_SCHEMA, NON_LANCE_TABLE, "describe"));
+  }
+
   @Test
   public void testCreateTablePrivilegeProbesButDoesNotRead() throws Exception {
     // Clients probe before creating, so CREATE_TABLE authorizes the probe but 
not a read.

Reply via email to