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

jerryshao 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 fc85710032 [Cherry-pick to branch-1.3] [#12468] fix(lance): validate 
the identifier in ListNamespaces at schema level (#12469) (#12496)
fc85710032 is described below

commit fc857100321e2b64f7940d9945960dad1f686017
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Aug 19 20:52:07 2026 +0800

    [Cherry-pick to branch-1.3] [#12468] fix(lance): validate the identifier in 
ListNamespaces at schema level (#12469) (#12496)
    
    **Cherry-pick Information:**
    - Original commit: 101e3475a3706cb2a903c695bdfd8664bc5d1250
    - Target branch: `branch-1.3`
    - Status: ✅ **Conflicts resolved**
    
    **Conflict resolution:**
    
    The cherry-pick conflicted in
    `GravitinoLanceNameSpaceOperations.namespaceExists`.
    On `main` the schema check goes through
    `namespaceWrapper.schemaExists(catalog, name)`,
    a helper backed by `SchemaDispatcher` that was added after 1.3 was cut;
    `branch-1.3` has no such method and calls
    `catalog.asSchemas().schemaExists(name)`
    directly.
    
    Resolved by keeping the `validateSchemaExists` extraction from the
    original commit
    but having it call `catalog.asSchemas().schemaExists`, which is what
    `branch-1.3`
    uses everywhere else. The new unit test stubs the same path
    (`Catalog.asSchemas()` -> mocked `SupportsSchemas`) instead of stubbing
    the
    wrapper method that does not exist on this branch. The `listNamespaces`
    change and
    the `LanceRESTServiceIT` additions applied cleanly and are unchanged.
    
    The `SchemaDispatcher`-backed wrapper helpers were deliberately not
    back-ported —
    they pull in an embedded-server code path that does not exist on
    `branch-1.3` and
    are well outside the scope of this fix.
    
    **Verification:**
    - `./gradlew :lance:lance-rest-server:test --tests
    '*TestGravitinoLanceNameSpaceOperations*' -PskipITs` — 3/3 pass
    - Reverting the level-2 validation makes 2 of the 3 fail, matching the
    failure profile described in the original PR
    - `spotlessApply` produces no further changes
    
    ---------
    
    Co-authored-by: FANNG <[email protected]>
---
 .../GravitinoLanceNameSpaceOperations.java         |  19 +++-
 .../TestGravitinoLanceNameSpaceOperations.java     | 101 +++++++++++++++++++++
 .../lance/integration/test/LanceRESTServiceIT.java |  24 +++++
 3 files changed, 139 insertions(+), 5 deletions(-)

diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
index a51fc15d49..5152ee8b69 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNameSpaceOperations.java
@@ -108,6 +108,12 @@ public class GravitinoLanceNameSpaceOperations implements 
LanceNamespaceOperatio
         break;
 
       case 2:
+        // A schema has no child namespaces, only tables, so the result is 
always empty. The
+        // identifier still has to be validated, otherwise a nonexistent path 
would be reported
+        // as an existing but empty namespace.
+        Catalog schemaCatalog =
+            
namespaceWrapper.loadAndValidateLakehouseCatalog(nsId.levelAtListPos(0));
+        validateSchemaExists(schemaCatalog, nsId.levelAtListPos(1));
         namespaces = Lists.newArrayList();
         break;
 
@@ -217,11 +223,14 @@ public class GravitinoLanceNameSpaceOperations implements 
LanceNamespaceOperatio
 
     Catalog catalog = 
namespaceWrapper.loadAndValidateLakehouseCatalog(nsId.levelAtListPos(0));
     if (nsId.levels() == 2) {
-      String schemaName = nsId.levelAtListPos(1);
-      if (!catalog.asSchemas().schemaExists(schemaName)) {
-        throw new NamespaceNotFoundException(
-            "Schema not found: " + schemaName, 
CommonUtil.formatCurrentStackTrace(), schemaName);
-      }
+      validateSchemaExists(catalog, nsId.levelAtListPos(1));
+    }
+  }
+
+  private void validateSchemaExists(Catalog catalog, String schemaName) {
+    if (!catalog.asSchemas().schemaExists(schemaName)) {
+      throw new NamespaceNotFoundException(
+          "Schema not found: " + schemaName, 
CommonUtil.formatCurrentStackTrace(), schemaName);
     }
   }
 
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNameSpaceOperations.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNameSpaceOperations.java
new file mode 100644
index 0000000000..7c5e9ff87a
--- /dev/null
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNameSpaceOperations.java
@@ -0,0 +1,101 @@
+/*
+ * 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.common.ops.gravitino;
+
+import static org.mockito.Mockito.when;
+
+import java.util.regex.Pattern;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.SupportsSchemas;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.model.ListNamespacesResponse;
+import org.mockito.Mockito;
+
+class TestGravitinoLanceNameSpaceOperations {
+
+  private static final String DELIMITER = Pattern.quote(".");
+
+  @Test
+  void testListNamespacesOnSchemaReturnsEmptyList() {
+    GravitinoLanceNamespaceWrapper namespaceWrapper =
+        Mockito.mock(GravitinoLanceNamespaceWrapper.class);
+    Catalog catalog = mockCatalogWithSchema("schema", true);
+    
when(namespaceWrapper.loadAndValidateLakehouseCatalog("catalog")).thenReturn(catalog);
+    GravitinoLanceNameSpaceOperations operations =
+        new GravitinoLanceNameSpaceOperations(namespaceWrapper);
+
+    ListNamespacesResponse response =
+        operations.listNamespaces("catalog.schema", DELIMITER, null, null);
+
+    Assertions.assertTrue(response.getNamespaces().isEmpty());
+    Assertions.assertNull(response.getPageToken());
+  }
+
+  @Test
+  void testListNamespacesOnNonExistentSchemaThrows() {
+    GravitinoLanceNamespaceWrapper namespaceWrapper =
+        Mockito.mock(GravitinoLanceNamespaceWrapper.class);
+    Catalog catalog = mockCatalogWithSchema("bogus_schema", false);
+    
when(namespaceWrapper.loadAndValidateLakehouseCatalog("catalog")).thenReturn(catalog);
+    GravitinoLanceNameSpaceOperations operations =
+        new GravitinoLanceNameSpaceOperations(namespaceWrapper);
+
+    NamespaceNotFoundException exception =
+        Assertions.assertThrows(
+            NamespaceNotFoundException.class,
+            () -> operations.listNamespaces("catalog.bogus_schema", DELIMITER, 
null, null));
+    Assertions.assertEquals("Schema not found: bogus_schema", 
exception.getMessage());
+    Assertions.assertEquals("bogus_schema", exception.getInstance());
+  }
+
+  @Test
+  void testListNamespacesOnNonExistentCatalogThrows() {
+    GravitinoLanceNamespaceWrapper namespaceWrapper =
+        Mockito.mock(GravitinoLanceNamespaceWrapper.class);
+    when(namespaceWrapper.loadAndValidateLakehouseCatalog("bogus_catalog"))
+        .thenThrow(
+            new NamespaceNotFoundException(
+                "Catalog not found: bogus_catalog", "", "bogus_catalog"));
+    GravitinoLanceNameSpaceOperations operations =
+        new GravitinoLanceNameSpaceOperations(namespaceWrapper);
+
+    // A nonexistent catalog must be reported at every depth, not only for its 
own level.
+    NamespaceNotFoundException exception =
+        Assertions.assertThrows(
+            NamespaceNotFoundException.class,
+            () -> operations.listNamespaces("bogus_catalog.bogus_schema", 
DELIMITER, null, null));
+    Assertions.assertEquals("Catalog not found: bogus_catalog", 
exception.getMessage());
+
+    exception =
+        Assertions.assertThrows(
+            NamespaceNotFoundException.class,
+            () -> operations.listNamespaces("bogus_catalog", DELIMITER, null, 
null));
+    Assertions.assertEquals("Catalog not found: bogus_catalog", 
exception.getMessage());
+  }
+
+  private static Catalog mockCatalogWithSchema(String schemaName, boolean 
exists) {
+    Catalog catalog = Mockito.mock(Catalog.class);
+    SupportsSchemas schemas = Mockito.mock(SupportsSchemas.class);
+    when(catalog.asSchemas()).thenReturn(schemas);
+    when(schemas.schemaExists(schemaName)).thenReturn(exists);
+    return catalog;
+  }
+}
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
index 1450d9813d..dcaa97c7f2 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
@@ -186,6 +186,30 @@ public class LanceRESTServiceIT extends BaseIT {
     listNamespacesResp = ns.listNamespaces(listNamespacesReq);
 
     Assertions.assertEquals(Sets.newHashSet(schema1.name()), 
listNamespacesResp.getNamespaces());
+
+    // a schema has no child namespaces, only tables, so the result is empty
+    listNamespacesReq.addIdItem(schema1.name());
+    listNamespacesResp = ns.listNamespaces(listNamespacesReq);
+
+    Assertions.assertTrue(listNamespacesResp.getNamespaces().isEmpty());
+
+    // listing under a non-existent schema should fail instead of returning an 
empty list
+    ListNamespacesRequest nonExistentSchemaReq = new ListNamespacesRequest();
+    nonExistentSchemaReq.addIdItem(catalog1.name());
+    nonExistentSchemaReq.addIdItem("non_existent_schema");
+    RuntimeException exception =
+        Assertions.assertThrows(
+            RuntimeException.class, () -> 
ns.listNamespaces(nonExistentSchemaReq));
+    assertLanceErrorCode(exception, ErrorCode.NAMESPACE_NOT_FOUND);
+
+    // a non-existent catalog should be reported at every depth
+    ListNamespacesRequest nonExistentCatalogReq = new ListNamespacesRequest();
+    nonExistentCatalogReq.addIdItem("non_existent_catalog");
+    nonExistentCatalogReq.addIdItem("non_existent_schema");
+    exception =
+        Assertions.assertThrows(
+            RuntimeException.class, () -> 
ns.listNamespaces(nonExistentCatalogReq));
+    assertLanceErrorCode(exception, ErrorCode.NAMESPACE_NOT_FOUND);
   }
 
   @Test

Reply via email to