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

mchades pushed a commit to branch branch-lance-namepspace-dev
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-lance-namepspace-dev by 
this push:
     new 662097ad51 [#8892] feat(Lance-REST-Server): implement namespace 
operation APIs for LRS (#8902)
662097ad51 is described below

commit 662097ad51cd44bc031fe02ecff2d16c613c89ba
Author: mchades <[email protected]>
AuthorDate: Sat Oct 25 00:30:04 2025 +0800

    [#8892] feat(Lance-REST-Server): implement namespace operation APIs for LRS 
(#8902)
    
    ### What changes were proposed in this pull request?
    
    implement namespace operation APIs for LRS
    
    ### Why are the changes needed?
    
    Fix: #8892
    
    ### Does this PR introduce _any_ user-facing change?
    
    yes, new REST APIs added
    
    ### How was this patch tested?
    
    not now
---
 conf/gravitino-lance-rest-server.conf.template     |   9 +-
 conf/gravitino.conf.template                       |  17 +-
 .../gravitino/lance/common/config/LanceConfig.java |  17 +-
 .../lance/common/ops/LanceNamespaceOperations.java |   8 +-
 .../gravitino/GravitinoLanceNamespaceWrapper.java  | 332 +++++++++++++++++++--
 .../lance/common/config/TestLanceConfig.java       |  13 +-
 .../apache/gravitino/lance/LanceRESTService.java   |   2 +-
 .../service/rest/LanceNamespaceOperations.java     |  94 +++++-
 .../apache/gravitino/server/TestServerConfig.java  |   3 +-
 9 files changed, 447 insertions(+), 48 deletions(-)

diff --git a/conf/gravitino-lance-rest-server.conf.template 
b/conf/gravitino-lance-rest-server.conf.template
index 32609bffca..137daf145d 100644
--- a/conf/gravitino-lance-rest-server.conf.template
+++ b/conf/gravitino-lance-rest-server.conf.template
@@ -40,6 +40,9 @@ gravitino.lance-rest.requestHeaderSize = 131072
 # The response header size of the built-in web server
 gravitino.lance-rest.responseHeaderSize = 131072
 
-# THE CONFIGURATION FOR Lance CATALOG
-# The logical Lance catalog served by this REST endpoint
-gravitino.lance-rest.catalog-name = default
+# THE CONFIGURATION FOR Lance namespace backend
+# The backend Lance namespace for Lance REST service, it's recommended to use 
Gravitino
+gravitino.lance-rest.namespace-backend = gravitino
+gravitino.lance-rest.uri = http://localhost:8090
+# replace metalake with your metalake name in Gravitino
+# gravitino.lance-rest.metalake-name = metalake
diff --git a/conf/gravitino.conf.template b/conf/gravitino.conf.template
index 418d14f14c..a1fdb005ca 100644
--- a/conf/gravitino.conf.template
+++ b/conf/gravitino.conf.template
@@ -81,8 +81,9 @@ gravitino.authorization.enable = false
 gravitino.authorization.serviceAdmins = anonymous
 
 # THE CONFIGURATION FOR AUXILIARY SERVICE
-# Auxiliary service names, separate by ','
+# Auxiliary service names, separate by ',' such as iceberg-rest,lance-rest
 gravitino.auxService.names = iceberg-rest
+
 # Iceberg REST service classpath
 gravitino.iceberg-rest.classpath = iceberg-rest-server/libs, 
iceberg-rest-server/conf
 # Iceberg REST service host
@@ -93,3 +94,17 @@ gravitino.iceberg-rest.httpPort = 9001
 gravitino.iceberg-rest.catalog-backend = memory
 # The warehouse directory of Iceberg catalog for Iceberg REST service
 gravitino.iceberg-rest.warehouse = /tmp/
+
+# Lance REST service classpath
+gravitino.lance-rest.classpath = lance-rest-server/libs
+# Lance REST service host
+gravitino.lance-rest.host = 0.0.0.0
+# Lance REST service http port
+gravitino.lance-rest.httpPort = 9101
+
+# THE CONFIGURATION FOR Lance namespace backend
+# The backend Lance namespace for Lance REST service, it's recommended to use 
Gravitino
+gravitino.lance-rest.namespace-backend = gravitino
+gravitino.lance-rest.uri = http://localhost:8090
+# replace metalake with your metalake name in Gravitino
+# gravitino.lance-rest.metalake-name = metalake
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/config/LanceConfig.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/config/LanceConfig.java
index b6614c87ee..3703189ba8 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/config/LanceConfig.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/config/LanceConfig.java
@@ -39,29 +39,22 @@ public class LanceConfig extends Config implements 
OverwriteDefaultConfig {
   public static final String DEFAULT_NAMESPACE_BACKEND = "gravitino";
   public static final String DEFAULT_URI = "http://localhost:8090";;
 
-  public static final ConfigEntry<String> CATALOG_NAME =
-      new ConfigBuilder(LANCE_CONFIG_PREFIX + "catalog-name")
-          .doc("Logical Lance catalog served by the REST endpoint")
-          .version(ConfigConstants.VERSION_0_1_0)
-          .stringConf()
-          .createWithDefault("default");
-
   public static final ConfigEntry<String> NAMESPACE_BACKEND =
-      new ConfigBuilder(LANCE_CONFIG_PREFIX + CONFIG_NAMESPACE_BACKEND)
+      new ConfigBuilder(CONFIG_NAMESPACE_BACKEND)
           .doc("The backend implementation for namespace operations")
           .version(ConfigConstants.VERSION_0_1_0)
           .stringConf()
           .createWithDefault(DEFAULT_NAMESPACE_BACKEND);
 
   public static final ConfigEntry<String> METALAKE_NAME =
-      new ConfigBuilder(LANCE_CONFIG_PREFIX + CONFIG_METALAKE)
+      new ConfigBuilder(CONFIG_METALAKE)
           .doc("The Metalake name for Gravitino namespace backend")
           .version(ConfigConstants.VERSION_0_1_0)
           .stringConf()
           .create();
 
   public static final ConfigEntry<String> NAMESPACE_URI =
-      new ConfigBuilder(LANCE_CONFIG_PREFIX + CONFIG_URI)
+      new ConfigBuilder(CONFIG_URI)
           .doc("The URI for the namespace backend, e.g., Gravitino server URI")
           .version(ConfigConstants.VERSION_0_1_0)
           .stringConf()
@@ -76,8 +69,8 @@ public class LanceConfig extends Config implements 
OverwriteDefaultConfig {
     super(false);
   }
 
-  public String getCatalogName() {
-    return get(CATALOG_NAME);
+  public String getNamespaceBackend() {
+    return get(NAMESPACE_BACKEND);
   }
 
   public String getNamespaceUri() {
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceNamespaceOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceNamespaceOperations.java
index 1b5da98ec0..226de4dbd7 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceNamespaceOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceNamespaceOperations.java
@@ -32,19 +32,19 @@ public interface LanceNamespaceOperations {
   ListNamespacesResponse listNamespaces(
       String namespaceId, String delimiter, String pageToken, Integer limit);
 
-  DescribeNamespaceResponse describeNamespace(String id, String delimiter);
+  DescribeNamespaceResponse describeNamespace(String namespaceId, String 
delimiter);
 
   CreateNamespaceResponse createNamespace(
-      String id,
+      String namespaceId,
       String delimiter,
       CreateNamespaceRequest.ModeEnum mode,
       Map<String, String> properties);
 
   DropNamespaceResponse dropNamespace(
-      String id,
+      String namespaceId,
       String delimiter,
       DropNamespaceRequest.ModeEnum mode,
       DropNamespaceRequest.BehaviorEnum behavior);
 
-  void namespaceExists(String id, String delimiter) throws 
LanceNamespaceException;
+  void namespaceExists(String namespaceId, String delimiter) throws 
LanceNamespaceException;
 }
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
index 59f637b5a1..cb1b85752a 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java
@@ -23,6 +23,7 @@ import static 
org.apache.gravitino.lance.common.config.LanceConfig.NAMESPACE_URI
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.lancedb.lance.namespace.LanceNamespaceException;
 import com.lancedb.lance.namespace.ObjectIdentifier;
@@ -33,16 +34,30 @@ import 
com.lancedb.lance.namespace.model.DropNamespaceRequest;
 import com.lancedb.lance.namespace.model.DropNamespaceResponse;
 import com.lancedb.lance.namespace.model.ListNamespacesResponse;
 import com.lancedb.lance.namespace.model.ListTablesResponse;
+import com.lancedb.lance.namespace.util.CommonUtil;
 import com.lancedb.lance.namespace.util.PageUtil;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.IntFunction;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Catalog;
+import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.SchemaChange;
 import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NonEmptyCatalogException;
+import org.apache.gravitino.exceptions.NonEmptySchemaException;
+import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.apache.gravitino.lance.common.config.LanceConfig;
 import org.apache.gravitino.lance.common.ops.LanceNamespaceOperations;
 import org.apache.gravitino.lance.common.ops.LanceTableOperations;
@@ -102,7 +117,6 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
     List<String> namespaces;
     switch (nsId.levels()) {
       case 0:
-        // List catalogs of type relational and provider generic-lakehouse
         namespaces =
             Arrays.stream(client.listCatalogsInfo())
                 .filter(this::isLakehouseCatalog)
@@ -111,16 +125,14 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
         break;
 
       case 1:
-        // List schemas under the catalog
-        String catalogName = nsId.levelAtListPos(0);
-        Catalog catalog = client.loadCatalog(catalogName);
-        if (!isLakehouseCatalog(catalog)) {
-          throw new NoSuchCatalogException("Catalog not found: %s", 
catalogName);
-        }
-
+        Catalog catalog = 
loadAndValidateLakehouseCatalog(nsId.levelAtListPos(0));
         namespaces = Lists.newArrayList(catalog.asSchemas().listSchemas());
         break;
 
+      case 2:
+        namespaces = Lists.newArrayList();
+        break;
+
       default:
         throw new IllegalArgumentException(
             "Expected at most 2-level namespace but got: " + namespaceId);
@@ -136,34 +148,101 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
   }
 
   @Override
-  public DescribeNamespaceResponse describeNamespace(String id, String 
delimiter) {
-    throw new UnsupportedOperationException("Not implemented yet");
+  public DescribeNamespaceResponse describeNamespace(String namespaceId, 
String delimiter) {
+    ObjectIdentifier nsId = ObjectIdentifier.of(namespaceId, delimiter);
+    Preconditions.checkArgument(
+        nsId.levels() <= 2 && nsId.levels() > 0,
+        "Expected at most 2-level and at least 1-level namespace but got: %s",
+        namespaceId);
+
+    Catalog catalog = loadAndValidateLakehouseCatalog(nsId.levelAtListPos(0));
+    Map<String, String> properties = Maps.newHashMap();
+
+    switch (nsId.levels()) {
+      case 1:
+        
Optional.ofNullable(catalog.properties()).ifPresent(properties::putAll);
+        break;
+      case 2:
+        String schemaName = nsId.levelAtListPos(1);
+        Schema schema = catalog.asSchemas().loadSchema(schemaName);
+        Optional.ofNullable(schema.properties()).ifPresent(properties::putAll);
+        break;
+      default:
+        throw new IllegalArgumentException(
+            "Expected at most 2-level and at least 1-level namespace but got: 
" + namespaceId);
+    }
+
+    DescribeNamespaceResponse response = new DescribeNamespaceResponse();
+    response.setProperties(properties);
+    return response;
   }
 
   @Override
   public CreateNamespaceResponse createNamespace(
-      String id,
+      String namespaceId,
       String delimiter,
       CreateNamespaceRequest.ModeEnum mode,
       Map<String, String> properties) {
-    throw new UnsupportedOperationException("Not implemented yet");
+    ObjectIdentifier nsId = ObjectIdentifier.of(namespaceId, delimiter);
+    Preconditions.checkArgument(
+        nsId.levels() <= 2 && nsId.levels() > 0,
+        "Expected at most 2-level and at least 1-level namespace but got: %s",
+        namespaceId);
+
+    switch (nsId.levels()) {
+      case 1:
+        return createOrUpdateCatalog(nsId.levelAtListPos(0), mode, properties);
+      case 2:
+        return createOrUpdateSchema(
+            nsId.levelAtListPos(0), nsId.levelAtListPos(1), mode, properties);
+      default:
+        throw new IllegalArgumentException(
+            "Expected at most 2-level and at least 1-level namespace but got: 
" + namespaceId);
+    }
   }
 
   @Override
   public DropNamespaceResponse dropNamespace(
-      String id,
+      String namespaceId,
       String delimiter,
       DropNamespaceRequest.ModeEnum mode,
       DropNamespaceRequest.BehaviorEnum behavior) {
-    throw new UnsupportedOperationException("Not implemented yet");
+    ObjectIdentifier nsId = ObjectIdentifier.of(namespaceId, delimiter);
+    Preconditions.checkArgument(
+        nsId.levels() <= 2 && nsId.levels() > 0,
+        "Expected at most 2-level and at least 1-level namespace but got: %s",
+        namespaceId);
+
+    switch (nsId.levels()) {
+      case 1:
+        return dropCatalog(nsId.levelAtListPos(0), mode, behavior);
+      case 2:
+        return dropSchema(nsId.levelAtListPos(0), nsId.levelAtListPos(1), 
mode, behavior);
+      default:
+        throw new IllegalArgumentException(
+            "Expected at most 2-level and at least 1-level namespace but got: 
" + namespaceId);
+    }
   }
 
   @Override
-  public void namespaceExists(String id, String delimiter) throws 
LanceNamespaceException {}
+  public void namespaceExists(String namespaceId, String delimiter) throws 
LanceNamespaceException {
+    ObjectIdentifier nsId = ObjectIdentifier.of(namespaceId, delimiter);
+    Preconditions.checkArgument(
+        nsId.levels() <= 2 && nsId.levels() > 0,
+        "Expected at most 2-level and at least 1-level namespace but got: %s",
+        namespaceId);
 
-  private boolean isLakehouseCatalog(Catalog catalog) {
-    return catalog.type().equals(Catalog.Type.RELATIONAL)
-        && "generic-lakehouse".equals(catalog.provider());
+    Catalog catalog = loadAndValidateLakehouseCatalog(nsId.levelAtListPos(0));
+    if (nsId.levels() == 2) {
+      String schemaName = nsId.levelAtListPos(1);
+      if (!catalog.asSchemas().schemaExists(schemaName)) {
+        throw LanceNamespaceException.notFound(
+            "Schema not found: " + schemaName,
+            NoSuchSchemaException.class.getSimpleName(),
+            schemaName,
+            CommonUtil.formatCurrentStackTrace());
+      }
+    }
   }
 
   @Override
@@ -171,4 +250,221 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
       String id, String delimiter, String pageToken, Integer limit) {
     throw new UnsupportedOperationException("Not implemented yet");
   }
+
+  private boolean isLakehouseCatalog(Catalog catalog) {
+    return catalog.type().equals(Catalog.Type.RELATIONAL)
+        && "generic-lakehouse".equals(catalog.provider());
+  }
+
+  private Catalog loadAndValidateLakehouseCatalog(String catalogName) {
+    Catalog catalog;
+    try {
+      catalog = client.loadCatalog(catalogName);
+    } catch (NoSuchCatalogException e) {
+      throw LanceNamespaceException.notFound(
+          "Catalog not found: " + catalogName,
+          NoSuchCatalogException.class.getSimpleName(),
+          catalogName,
+          CommonUtil.formatCurrentStackTrace());
+    }
+    if (!isLakehouseCatalog(catalog)) {
+      throw LanceNamespaceException.notFound(
+          "Catalog is not a lakehouse catalog: " + catalogName,
+          NoSuchCatalogException.class.getSimpleName(),
+          catalogName,
+          CommonUtil.formatCurrentStackTrace());
+    }
+    return catalog;
+  }
+
+  private CreateNamespaceResponse createOrUpdateCatalog(
+      String catalogName, CreateNamespaceRequest.ModeEnum mode, Map<String, 
String> properties) {
+    CreateNamespaceResponse response = new CreateNamespaceResponse();
+
+    Catalog catalog;
+    try {
+      catalog = client.loadCatalog(catalogName);
+    } catch (NoSuchCatalogException e) {
+      // Catalog does not exist, create it
+      Catalog createdCatalog =
+          client.createCatalog(
+              catalogName,
+              Catalog.Type.RELATIONAL,
+              "generic-lakehouse",
+              "created by Lance REST server",
+              properties);
+      response.setProperties(
+          createdCatalog.properties() == null ? Maps.newHashMap() : 
createdCatalog.properties());
+      return response;
+    }
+
+    // Catalog exists, validate type
+    if (!isLakehouseCatalog(catalog)) {
+      throw LanceNamespaceException.conflict(
+          "Catalog already exists but is not a lakehouse catalog: " + 
catalogName,
+          CatalogAlreadyExistsException.class.getSimpleName(),
+          catalogName,
+          CommonUtil.formatCurrentStackTrace());
+    }
+
+    // Catalog exists, handle based on mode
+    switch (mode) {
+      case EXIST_OK:
+        response.setProperties(Maps.newHashMap());
+        return response;
+      case CREATE:
+        throw LanceNamespaceException.conflict(
+            "Catalog already exists: " + catalogName,
+            CatalogAlreadyExistsException.class.getSimpleName(),
+            catalogName,
+            CommonUtil.formatCurrentStackTrace());
+      case OVERWRITE:
+        CatalogChange[] changes =
+            buildChanges(
+                properties,
+                catalog.properties(),
+                CatalogChange::setProperty,
+                CatalogChange::removeProperty,
+                CatalogChange[]::new);
+        Catalog alteredCatalog = client.alterCatalog(catalogName, changes);
+        
Optional.ofNullable(alteredCatalog.properties()).ifPresent(response::setProperties);
+        return response;
+      default:
+        throw new IllegalArgumentException("Unknown mode: " + mode);
+    }
+  }
+
+  private CreateNamespaceResponse createOrUpdateSchema(
+      String catalogName,
+      String schemaName,
+      CreateNamespaceRequest.ModeEnum mode,
+      Map<String, String> properties) {
+    CreateNamespaceResponse response = new CreateNamespaceResponse();
+    Catalog loadedCatalog = loadAndValidateLakehouseCatalog(catalogName);
+
+    Schema schema;
+    try {
+      schema = loadedCatalog.asSchemas().loadSchema(schemaName);
+    } catch (NoSuchSchemaException e) {
+      // Schema does not exist, create it
+      Schema createdSchema = 
loadedCatalog.asSchemas().createSchema(schemaName, null, properties);
+      response.setProperties(
+          createdSchema.properties() == null ? Maps.newHashMap() : 
createdSchema.properties());
+      return response;
+    }
+
+    // Schema exists, handle based on mode
+    switch (mode) {
+      case EXIST_OK:
+        response.setProperties(Maps.newHashMap());
+        return response;
+      case CREATE:
+        throw LanceNamespaceException.conflict(
+            "Schema already exists: " + schemaName,
+            SchemaAlreadyExistsException.class.getSimpleName(),
+            schemaName,
+            CommonUtil.formatCurrentStackTrace());
+      case OVERWRITE:
+        SchemaChange[] changes =
+            buildChanges(
+                properties,
+                schema.properties(),
+                SchemaChange::setProperty,
+                SchemaChange::removeProperty,
+                SchemaChange[]::new);
+        Schema alteredSchema = 
loadedCatalog.asSchemas().alterSchema(schemaName, changes);
+        
Optional.ofNullable(alteredSchema.properties()).ifPresent(response::setProperties);
+        return response;
+      default:
+        throw new IllegalArgumentException("Unknown mode: " + mode);
+    }
+  }
+
+  private DropNamespaceResponse dropCatalog(
+      String catalogName,
+      DropNamespaceRequest.ModeEnum mode,
+      DropNamespaceRequest.BehaviorEnum behavior) {
+    try {
+      boolean dropped =
+          client.dropCatalog(catalogName, behavior == 
DropNamespaceRequest.BehaviorEnum.CASCADE);
+      if (dropped) {
+        return new DropNamespaceResponse();
+      } else {
+        // Catalog did not exist
+        if (mode == DropNamespaceRequest.ModeEnum.FAIL) {
+          throw LanceNamespaceException.notFound(
+              "Catalog not found: " + catalogName,
+              NoSuchCatalogException.class.getSimpleName(),
+              catalogName,
+              CommonUtil.formatCurrentStackTrace());
+        }
+        return new DropNamespaceResponse(); // SKIP mode
+      }
+    } catch (NonEmptyCatalogException e) {
+      throw LanceNamespaceException.badRequest(
+          String.format("Catalog %s is not empty.", catalogName),
+          NonEmptyCatalogException.class.getSimpleName(),
+          catalogName,
+          CommonUtil.formatCurrentStackTrace());
+    }
+  }
+
+  private DropNamespaceResponse dropSchema(
+      String catalogName,
+      String schemaName,
+      DropNamespaceRequest.ModeEnum mode,
+      DropNamespaceRequest.BehaviorEnum behavior) {
+    try {
+      boolean dropped =
+          client
+              .loadCatalog(catalogName)
+              .asSchemas()
+              .dropSchema(schemaName, behavior == 
DropNamespaceRequest.BehaviorEnum.CASCADE);
+      if (dropped) {
+        return new DropNamespaceResponse();
+      } else {
+        // Schema did not exist
+        if (mode == DropNamespaceRequest.ModeEnum.FAIL) {
+          throw LanceNamespaceException.notFound(
+              "Schema not found: " + schemaName,
+              NoSuchSchemaException.class.getSimpleName(),
+              schemaName,
+              CommonUtil.formatCurrentStackTrace());
+        }
+        return new DropNamespaceResponse(); // SKIP mode
+      }
+    } catch (NoSuchCatalogException e) {
+      throw LanceNamespaceException.notFound(
+          "Catalog not found: " + catalogName,
+          NoSuchCatalogException.class.getSimpleName(),
+          catalogName,
+          CommonUtil.formatCurrentStackTrace());
+    } catch (NonEmptySchemaException e) {
+      throw LanceNamespaceException.badRequest(
+          String.format("Schema %s is not empty.", schemaName),
+          NonEmptySchemaException.class.getSimpleName(),
+          schemaName,
+          CommonUtil.formatCurrentStackTrace());
+    }
+  }
+
+  private <T> T[] buildChanges(
+      Map<String, String> newProps,
+      Map<String, String> oldProps,
+      BiFunction<String, String, T> setPropertyFunc,
+      Function<String, T> removePropertyFunc,
+      IntFunction<T[]> arrayCreator) {
+    Stream<T> setPropertiesStream =
+        newProps.entrySet().stream()
+            .map(entry -> setPropertyFunc.apply(entry.getKey(), 
entry.getValue()));
+
+    Stream<T> removePropertiesStream =
+        oldProps == null
+            ? Stream.empty()
+            : oldProps.keySet().stream()
+                .filter(key -> !newProps.containsKey(key))
+                .map(removePropertyFunc);
+
+    return Stream.concat(setPropertiesStream, 
removePropertiesStream).toArray(arrayCreator);
+  }
 }
diff --git 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/config/TestLanceConfig.java
 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/config/TestLanceConfig.java
index 176634f309..44577a2dfa 100644
--- 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/config/TestLanceConfig.java
+++ 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/config/TestLanceConfig.java
@@ -29,22 +29,22 @@ public class TestLanceConfig {
   @Test
   public void testLoadLanceConfig() {
     Map<String, String> properties =
-        ImmutableMap.of("gravitino.lance-rest.catalog-name", "test_catalog");
+        ImmutableMap.of("gravitino.lance-rest.namespace-backend", 
"test_catalog");
 
     LanceConfig lanceConfig = new LanceConfig();
     lanceConfig.loadFromMap(properties, k -> 
k.startsWith("gravitino.lance-rest."));
-    Assertions.assertEquals("test_catalog", lanceConfig.getCatalogName());
+    Assertions.assertEquals("gravitino", lanceConfig.getNamespaceBackend());
 
     LanceConfig lanceConfig2 = new LanceConfig(properties);
-    Assertions.assertEquals("test_catalog", lanceConfig2.getCatalogName());
+    Assertions.assertEquals("gravitino", lanceConfig2.getNamespaceBackend());
   }
 
   @Test
   public void testDefaultCatalogName() {
-    // Test default catalog name when not specified
+    // Test default namespace backend name when not specified
     Map<String, String> properties = ImmutableMap.of();
     LanceConfig lanceConfig = new LanceConfig(properties);
-    Assertions.assertEquals("default", lanceConfig.getCatalogName());
+    Assertions.assertEquals("gravitino", lanceConfig.getNamespaceBackend());
   }
 
   @Test
@@ -94,7 +94,6 @@ public class TestLanceConfig {
     // Test all configurations together for auxiliary mode
     Map<String, String> properties =
         ImmutableMap.<String, String>builder()
-            .put(LanceConfig.CATALOG_NAME.getKey(), "lance_catalog")
             .put(LanceConfig.NAMESPACE_URI.getKey(), 
"http://gravitino-prod:8090";)
             .put(LanceConfig.METALAKE_NAME.getKey(), "production")
             .put(LanceConfig.NAMESPACE_BACKEND.getKey(), "gravitino")
@@ -104,7 +103,7 @@ public class TestLanceConfig {
     LanceConfig lanceConfig = new LanceConfig(properties);
 
     // Verify all config values
-    Assertions.assertEquals("lance_catalog", lanceConfig.getCatalogName());
+    Assertions.assertEquals("gravitino", lanceConfig.getNamespaceBackend());
     Assertions.assertEquals("http://gravitino-prod:8090";, 
lanceConfig.getNamespaceUri());
     Assertions.assertEquals("production", lanceConfig.getGravitinoMetalake());
 
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
index d1409c8e12..2d9f3e8823 100644
--- 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
@@ -90,7 +90,7 @@ public class LanceRESTService implements 
GravitinoAuxiliaryService {
     server.addCustomFilters(LANCE_SPEC);
     server.addSystemFilters(LANCE_SPEC);
 
-    LOG.info("Initialized Lance REST service for catalog {}", 
lanceConfig.getCatalogName());
+    LOG.info("Initialized Lance REST service for backend {}", 
lanceConfig.getNamespaceBackend());
   }
 
   @Override
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceNamespaceOperations.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceNamespaceOperations.java
index 2d07357f30..dd548541ad 100644
--- 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceNamespaceOperations.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceNamespaceOperations.java
@@ -22,12 +22,19 @@ import static 
org.apache.gravitino.lance.common.ops.NamespaceWrapper.NAMESPACE_D
 
 import com.codahale.metrics.annotation.ResponseMetered;
 import com.codahale.metrics.annotation.Timed;
+import com.lancedb.lance.namespace.model.CreateNamespaceRequest;
+import com.lancedb.lance.namespace.model.CreateNamespaceResponse;
+import com.lancedb.lance.namespace.model.DescribeNamespaceResponse;
+import com.lancedb.lance.namespace.model.DropNamespaceRequest;
+import com.lancedb.lance.namespace.model.DropNamespaceResponse;
 import com.lancedb.lance.namespace.model.ListNamespacesResponse;
+import java.util.regex.Pattern;
 import javax.inject.Inject;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DefaultValue;
 import javax.ws.rs.Encoded;
 import javax.ws.rs.GET;
+import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -61,10 +68,95 @@ public class LanceNamespaceOperations {
       @QueryParam("limit") Integer limit) {
     try {
       ListNamespacesResponse response =
-          lanceNamespace.asNamespaceOps().listNamespaces(namespaceId, 
delimiter, pageToken, limit);
+          lanceNamespace
+              .asNamespaceOps()
+              .listNamespaces(namespaceId, Pattern.quote(delimiter), 
pageToken, limit);
       return Response.ok(response).build();
     } catch (Exception e) {
       return LanceExceptionMapper.toRESTResponse(namespaceId, e);
     }
   }
+
+  @POST
+  @Path("/{id}/describe")
+  @Timed(name = "describe-namespaces." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "describe-namespaces", absolute = true)
+  public Response describeNamespace(
+      @Encoded @PathParam("id") String namespaceId,
+      @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter) {
+    try {
+      DescribeNamespaceResponse response =
+          lanceNamespace.asNamespaceOps().describeNamespace(namespaceId, 
Pattern.quote(delimiter));
+      return Response.ok(response).build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(namespaceId, e);
+    }
+  }
+
+  @POST
+  @Path("/{id}/create")
+  @Timed(name = "create-namespaces." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "create-namespaces", absolute = true)
+  public Response createNamespace(
+      @Encoded @PathParam("id") String namespaceId,
+      @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter,
+      CreateNamespaceRequest request) {
+    try {
+      CreateNamespaceResponse response =
+          lanceNamespace
+              .asNamespaceOps()
+              .createNamespace(
+                  namespaceId,
+                  Pattern.quote(delimiter),
+                  request.getMode() == null
+                      ? CreateNamespaceRequest.ModeEnum.CREATE
+                      : request.getMode(),
+                  request.getProperties());
+      return Response.ok(response).build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(namespaceId, e);
+    }
+  }
+
+  @POST
+  @Path("/{id}/drop")
+  @Timed(name = "drop-namespaces." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "drop-namespaces", absolute = true)
+  public Response dropNamespace(
+      @Encoded @PathParam("id") String namespaceId,
+      @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter,
+      DropNamespaceRequest request) {
+    try {
+      DropNamespaceResponse response =
+          lanceNamespace
+              .asNamespaceOps()
+              .dropNamespace(
+                  namespaceId,
+                  Pattern.quote(delimiter),
+                  request.getMode() == null
+                      ? DropNamespaceRequest.ModeEnum.FAIL
+                      : request.getMode(),
+                  request.getBehavior() == null
+                      ? DropNamespaceRequest.BehaviorEnum.RESTRICT
+                      : request.getBehavior());
+      return Response.ok(response).build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(namespaceId, e);
+    }
+  }
+
+  @POST
+  @Path("/{id}/exists")
+  @Timed(name = "namespace-exists." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "namespace-exists", absolute = true)
+  public Response namespaceExists(
+      @Encoded @PathParam("id") String namespaceId,
+      @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter) {
+    try {
+      lanceNamespace.asNamespaceOps().namespaceExists(namespaceId, 
Pattern.quote(delimiter));
+      return Response.ok().build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(namespaceId, e);
+    }
+  }
 }
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/TestServerConfig.java 
b/server-common/src/test/java/org/apache/gravitino/server/TestServerConfig.java
index fc9193e012..e46e27b807 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/TestServerConfig.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/TestServerConfig.java
@@ -67,7 +67,8 @@ public class TestServerConfig {
     for (Map.Entry<Object, Object> entry : properties.entrySet()) {
       String propKey = (String) entry.getKey();
       if 
(propKey.startsWith(AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX)
-          || propKey.startsWith("gravitino.iceberg-rest.")) {
+          || propKey.startsWith("gravitino.iceberg-rest.")
+          || propKey.startsWith("gravitino.lance-rest.")) {
         continue;
       }
       Assertions.assertTrue(

Reply via email to