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