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

yuqi4733 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 e850494a74 [#8955] feat(lance-rest): Support register and deregister 
table operations in Lance REST server (#9197)
e850494a74 is described below

commit e850494a7463f2dbca0909309ea4aaac1bdf67f8
Author: Junda Yang <[email protected]>
AuthorDate: Thu Nov 20 23:09:48 2025 -0800

    [#8955] feat(lance-rest): Support register and deregister table operations 
in Lance REST server (#9197)
    
    ### What changes were proposed in this pull request?
    
    This PR adds support for registerTable and deregisterTable operations to
    the Lance REST server:
    1. New REST endpoints:
    - POST /lance/v1/table/{id}/register - Register an existing Lance table
    (metadata-only operation)
    - POST /lance/v1/table/{id}/deregister - Deregister a Lance table
    without deleting physical data
    2. Backend implementation:
    - Implemented registerTable() and deregisterTable() in
    GravitinoLanceNamespaceWrapper to handle Gravitino catalog integration
    - Added purgeTable() support in GenericLakehouseCatalogOperations to
    delete metadata without removing physical data
    - Added register property handling in table creation to skip physical
    table creation during registration
    3. REST endpoint improvements:
    - Refactored createTable endpoints to extract headers using @Context
    HttpHeaders instead of individual @HeaderParam annotations
    - Added ServiceConstants class for consistent header key definitions
    - Removed rootCatalog parameter (simplified to 3-level namespace
    requirement)
    4. Bug fixes:
    - Removed @Encoded annotation from all @PathParam in
    LanceNamespaceOperations to allow proper automatic URL decoding by HTTP
    clients
    - Fixed error handling in loadTable() to properly throw
    NoSuchTableException
    - Improved error messages in alterTable() and dropTable()
    - Strengthened namespace level validation (changed from <= 2 to == 2 for
    namespaces, <= 3 to == 3 for tables)
    
    ### Why are the changes needed?
    
    1. Primary need: Users need the ability to register existing Lance
    tables (that were created outside Gravitino) into the Gravitino catalog
    for unified metadata management, and to deregister tables without
    deleting the underlying data.
    2. Secondary improvements:
    - The @Encoded annotation was causing issues with URL encoding/decoding
    - most HTTP clients automatically encode special characters, so
    disabling automatic decoding was incorrect and would break requests with
    encoded namespace delimiters
    - Stricter namespace validation prevents ambiguous table identifiers
    - Better error handling improves debugging experience
    
    Fix: #8955
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, new user-facing changes:
    New APIs:
    - Users can now register existing Lance tables: POST
    /lance/v1/table/{catalog}${schema}${table}/register?mode=create&delimiter=$
    - Users can now deregister tables without data deletion: POST
    /lance/v1/table/{catalog}${schema}${table}/deregister?delimiter=$
    
    ### How was this patch tested?
    
    Test locally.
    
    ---------
    
    Co-authored-by: Mini Yu <[email protected]>
    Co-authored-by: mchades <[email protected]>
---
 .../GenericLakehouseCatalogOperations.java         | 49 ++++++++++++--
 .../GenericLakehouseTablePropertiesMetadata.java   | 12 +++-
 .../catalog/TableOperationDispatcher.java          |  4 ++
 .../lance/common/ops/LanceTableOperations.java     |  8 ++-
 .../gravitino/GravitinoLanceNamespaceWrapper.java  | 79 +++++++++++++++++++---
 .../apache/gravitino/lance/LanceRESTService.java   |  1 +
 .../gravitino/lance/service/ServiceConstants.java} | 22 ++----
 .../service/rest/LanceNamespaceOperations.java     | 11 ++-
 .../lance/service/rest/LanceTableOperations.java   | 79 +++++++++++++++++++---
 9 files changed, 217 insertions(+), 48 deletions(-)

diff --git 
a/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseCatalogOperations.java
 
b/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseCatalogOperations.java
index 29385ff472..812f74c051 100644
--- 
a/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseCatalogOperations.java
+++ 
b/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseCatalogOperations.java
@@ -221,6 +221,8 @@ public class GenericLakehouseCatalogOperations
           .withName(tableEntity.name())
           .withComment(tableEntity.getComment())
           .build();
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchTableException(e, "Table %s does not exist", ident);
     } catch (IOException e) {
       throw new RuntimeException("Failed to list tables under schema " + 
ident.namespace(), e);
     }
@@ -281,6 +283,34 @@ public class GenericLakehouseCatalogOperations
               .withAuditInfo(auditInfo)
               .build();
       store.put(entityToStore);
+
+      // Get the value of register in table properties
+      boolean register =
+          Boolean.parseBoolean(
+              properties.getOrDefault(
+                  GenericLakehouseTablePropertiesMetadata.LAKEHOUSE_REGISTER, 
"false"));
+      if (register) {
+        // Do not need to create the physical table if this is a registration 
operation.
+        // Whether we need to check the existence of the physical table?
+        GenericLakehouseTable.Builder builder = 
GenericLakehouseTable.builder();
+        return builder
+            .withName(ident.name())
+            .withColumns(columns)
+            .withComment(comment)
+            .withProperties(properties)
+            .withDistribution(distribution)
+            .withIndexes(indexes)
+            .withAuditInfo(
+                AuditInfo.builder()
+                    .withCreator(PrincipalUtils.getCurrentUserName())
+                    .withCreateTime(Instant.now())
+                    .build())
+            .withPartitioning(partitions)
+            .withSortOrders(sortOrders)
+            .withFormat(LakehouseTableFormat.LANCE.lowerName())
+            .build();
+      }
+
       LakehouseCatalogOperations lanceCatalogOperations =
           getLakehouseCatalogOperations(newProperties);
       return lanceCatalogOperations.createTable(
@@ -335,7 +365,6 @@ public class GenericLakehouseCatalogOperations
   @Override
   public Table alterTable(NameIdentifier ident, TableChange... changes)
       throws NoSuchTableException, IllegalArgumentException {
-    Namespace namespace = ident.namespace();
     try {
       TableEntity tableEntity = store.get(ident, Entity.EntityType.TABLE, 
TableEntity.class);
       Map<String, String> tableProperties = tableEntity.getProperties();
@@ -343,13 +372,12 @@ public class GenericLakehouseCatalogOperations
           getLakehouseCatalogOperations(tableProperties);
       return lakehouseCatalogOperations.alterTable(ident, changes);
     } catch (IOException e) {
-      throw new RuntimeException("Failed to list tables under schema " + 
namespace, e);
+      throw new RuntimeException("Failed to alter table " + ident, e);
     }
   }
 
   @Override
   public boolean dropTable(NameIdentifier ident) {
-    Namespace namespace = ident.namespace();
     try {
       TableEntity tableEntity = store.get(ident, Entity.EntityType.TABLE, 
TableEntity.class);
       LakehouseCatalogOperations lakehouseCatalogOperations =
@@ -359,7 +387,20 @@ public class GenericLakehouseCatalogOperations
       LOG.warn("Table {} does not exist, skip dropping it.", ident);
       return false;
     } catch (IOException e) {
-      throw new RuntimeException("Failed to list tables under schema " + 
namespace, e);
+      throw new RuntimeException("Failed to drop table: " + ident, e);
+    }
+  }
+
+  @Override
+  public boolean purgeTable(NameIdentifier ident) throws 
UnsupportedOperationException {
+    try {
+      // Only delete the metadata entry here. The physical data will not be 
deleted.
+      if (!tableExists(ident)) {
+        return false;
+      }
+      return store.delete(ident, Entity.EntityType.TABLE);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to purge table " + ident, e);
     }
   }
 
diff --git 
a/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseTablePropertiesMetadata.java
 
b/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseTablePropertiesMetadata.java
index f8ca11b0a0..72c1e5bc57 100644
--- 
a/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseTablePropertiesMetadata.java
+++ 
b/catalogs/catalog-generic-lakehouse/src/main/java/org/apache/gravitino/catalog/lakehouse/GenericLakehouseTablePropertiesMetadata.java
@@ -18,6 +18,7 @@
  */
 package org.apache.gravitino.catalog.lakehouse;
 
+import static 
org.apache.gravitino.connector.PropertyEntry.booleanPropertyEntry;
 import static org.apache.gravitino.connector.PropertyEntry.enumPropertyEntry;
 import static 
org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyEntry;
 
@@ -32,6 +33,7 @@ public class GenericLakehouseTablePropertiesMetadata extends 
BasePropertiesMetad
   public static final String LAKEHOUSE_LOCATION = "location";
   public static final String LAKEHOUSE_FORMAT = "format";
   public static final String LANCE_TABLE_STORAGE_OPTION_PREFIX = 
"lance.storage.";
+  public static final String LAKEHOUSE_REGISTER = "register";
 
   private static final Map<String, PropertyEntry<?>> PROPERTIES_METADATA;
 
@@ -59,7 +61,15 @@ public class GenericLakehouseTablePropertiesMetadata extends 
BasePropertiesMetad
                 false /* immutable */,
                 null /* default value*/,
                 false /* hidden */,
-                false /* reserved */));
+                false /* reserved */),
+            booleanPropertyEntry(
+                LAKEHOUSE_REGISTER,
+                "Whether this is a table registration operation.",
+                false,
+                true /* immutable */,
+                false /* defaultValue */,
+                false /* hidden */,
+                false));
 
     PROPERTIES_METADATA = Maps.uniqueIndex(propertyEntries, 
PropertyEntry::getName);
   }
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
index e549b806d8..6289bd3404 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
@@ -408,6 +408,10 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
                   RuntimeException.class,
                   UnsupportedOperationException.class);
 
+          if (isManagedTable(catalogIdent)) {
+            return droppedFromCatalog;
+          }
+
           // For unmanaged table, it could happen that the table:
           // 1. Is not found in the catalog (dropped directly from underlying 
sources)
           // 2. Is found in the catalog but not in the store (not managed by 
Gravitino)
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
index b8a967cd30..8a356fb135 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
@@ -19,7 +19,9 @@
 package org.apache.gravitino.lance.common.ops;
 
 import com.lancedb.lance.namespace.model.CreateTableResponse;
+import com.lancedb.lance.namespace.model.DeregisterTableResponse;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
+import com.lancedb.lance.namespace.model.RegisterTableResponse;
 import java.util.Map;
 
 public interface LanceTableOperations {
@@ -32,6 +34,10 @@ public interface LanceTableOperations {
       String delimiter,
       String tableLocation,
       Map<String, String> tableProperties,
-      String rootCatalog,
       byte[] arrowStreamBody);
+
+  RegisterTableResponse registerTable(
+      String tableId, String mode, String delimiter, Map<String, String> 
tableProperties);
+
+  DeregisterTableResponse deregisterTable(String tableId, String delimiter);
 }
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 d3ddbb0ede..865313b31c 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
@@ -35,6 +35,7 @@ import 
com.lancedb.lance.namespace.model.CreateNamespaceRequest;
 import com.lancedb.lance.namespace.model.CreateNamespaceRequest.ModeEnum;
 import com.lancedb.lance.namespace.model.CreateNamespaceResponse;
 import com.lancedb.lance.namespace.model.CreateTableResponse;
+import com.lancedb.lance.namespace.model.DeregisterTableResponse;
 import com.lancedb.lance.namespace.model.DescribeNamespaceResponse;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
 import com.lancedb.lance.namespace.model.DropNamespaceRequest;
@@ -42,6 +43,8 @@ import 
com.lancedb.lance.namespace.model.DropNamespaceResponse;
 import com.lancedb.lance.namespace.model.JsonArrowSchema;
 import com.lancedb.lance.namespace.model.ListNamespacesResponse;
 import com.lancedb.lance.namespace.model.ListTablesResponse;
+import com.lancedb.lance.namespace.model.RegisterTableRequest;
+import com.lancedb.lance.namespace.model.RegisterTableResponse;
 import com.lancedb.lance.namespace.util.CommonUtil;
 import com.lancedb.lance.namespace.util.JsonArrowSchemaConverter;
 import com.lancedb.lance.namespace.util.PageUtil;
@@ -492,7 +495,7 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
       String namespaceId, String delimiter, String pageToken, Integer limit) {
     ObjectIdentifier nsId = ObjectIdentifier.of(namespaceId, 
Pattern.quote(delimiter));
     Preconditions.checkArgument(
-        nsId.levels() <= 2, "Expected at most 2-level namespace but got: %s", 
nsId.levels());
+        nsId.levels() == 2, "Expected 2-level namespace but got: %s", 
nsId.levels());
     String catalogName = nsId.levelAtListPos(0);
     Catalog catalog = loadAndValidateLakehouseCatalog(catalogName);
     String schemaName = nsId.levelAtListPos(1);
@@ -516,7 +519,7 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
   public DescribeTableResponse describeTable(String tableId, String delimiter) 
{
     ObjectIdentifier nsId = ObjectIdentifier.of(tableId, 
Pattern.quote(delimiter));
     Preconditions.checkArgument(
-        nsId.levels() <= 3, "Expected at most 3-level namespace but got: %s", 
nsId.levels());
+        nsId.levels() == 3, "Expected at 3-level namespace but got: %s", 
nsId.levels());
 
     String catalogName = nsId.levelAtListPos(0);
     Catalog catalog = loadAndValidateLakehouseCatalog(catalogName);
@@ -538,17 +541,10 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
       String delimiter,
       String tableLocation,
       Map<String, String> tableProperties,
-      String rootCatalog,
       byte[] arrowStreamBody) {
     ObjectIdentifier nsId = ObjectIdentifier.of(tableId, 
Pattern.quote(delimiter));
     Preconditions.checkArgument(
-        nsId.levels() <= 3, "Expected at most 3-level namespace but got: %s", 
nsId.levels());
-    if (rootCatalog != null) {
-      List<String> levels = nsId.listStyleId();
-      List<String> newLevels = Lists.newArrayList(rootCatalog);
-      newLevels.addAll(levels);
-      nsId = ObjectIdentifier.of(newLevels);
-    }
+        nsId.levels() == 3, "Expected at 3-level namespace but got: %s", 
nsId.levels());
 
     // Parser column information.
     List<Column> columns = Lists.newArrayList();
@@ -614,6 +610,68 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
     return response;
   }
 
+  @Override
+  public RegisterTableResponse registerTable(
+      String tableId, String mode, String delimiter, Map<String, String> 
tableProperties) {
+    ObjectIdentifier nsId = ObjectIdentifier.of(tableId, 
Pattern.quote(delimiter));
+    Preconditions.checkArgument(
+        nsId.levels() == 3, "Expected at 3-level namespace but got: %s", 
nsId.levels());
+
+    String catalogName = nsId.levelAtListPos(0);
+    Catalog catalog = loadAndValidateLakehouseCatalog(catalogName);
+    NameIdentifier tableIdentifier =
+        NameIdentifier.of(nsId.levelAtListPos(1), nsId.levelAtListPos(2));
+
+    // TODO Support real register API
+    RegisterTableRequest.ModeEnum createMode =
+        RegisterTableRequest.ModeEnum.fromValue(mode.toUpperCase());
+    if (createMode == RegisterTableRequest.ModeEnum.CREATE
+        && catalog.asTableCatalog().tableExists(tableIdentifier)) {
+      throw LanceNamespaceException.conflict(
+          "Table already exists: " + tableId,
+          SchemaAlreadyExistsException.class.getSimpleName(),
+          tableId,
+          CommonUtil.formatCurrentStackTrace());
+    }
+
+    if (createMode == RegisterTableRequest.ModeEnum.OVERWRITE
+        && catalog.asTableCatalog().tableExists(tableIdentifier)) {
+      LOG.info("Overwriting existing table: {}", tableId);
+      catalog.asTableCatalog().dropTable(tableIdentifier);
+    }
+
+    Table t =
+        catalog.asTableCatalog().createTable(tableIdentifier, new Column[] {}, 
"", tableProperties);
+
+    RegisterTableResponse response = new RegisterTableResponse();
+    response.setProperties(t.properties());
+    response.setLocation(t.properties().get("location"));
+    return response;
+  }
+
+  @Override
+  public DeregisterTableResponse deregisterTable(String tableId, String 
delimiter) {
+
+    ObjectIdentifier nsId = ObjectIdentifier.of(tableId, 
Pattern.quote(delimiter));
+    Preconditions.checkArgument(
+        nsId.levels() == 3, "Expected at 3-level namespace but got: %s", 
nsId.levels());
+
+    String catalogName = nsId.levelAtListPos(0);
+    Catalog catalog = loadAndValidateLakehouseCatalog(catalogName);
+
+    NameIdentifier tableIdentifier =
+        NameIdentifier.of(nsId.levelAtListPos(1), nsId.levelAtListPos(2));
+    Table t = catalog.asTableCatalog().loadTable(tableIdentifier);
+    Map<String, String> properties = t.properties();
+    // TODO Support real deregister API.
+    catalog.asTableCatalog().purgeTable(tableIdentifier);
+
+    DeregisterTableResponse response = new DeregisterTableResponse();
+    response.setProperties(properties);
+    response.setLocation(properties.get("location"));
+    return response;
+  }
+
   private JsonArrowSchema toJsonArrowSchema(Column[] columns) {
     List<Field> fields =
         Arrays.stream(columns)
@@ -627,7 +685,6 @@ public class GravitinoLanceNamespaceWrapper extends 
NamespaceWrapper
   @VisibleForTesting
   org.apache.arrow.vector.types.pojo.Schema parseArrowIpcStream(byte[] stream) 
{
     org.apache.arrow.vector.types.pojo.Schema schema;
-
     try (BufferAllocator allocator = new RootAllocator();
         ByteArrayInputStream bais = new ByteArrayInputStream(stream);
         ArrowStreamReader reader = new ArrowStreamReader(bais, allocator)) {
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 8c800e49d6..afab3ab2a7 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
@@ -127,6 +127,7 @@ public class LanceRESTService implements 
GravitinoAuxiliaryService {
     try {
       Constructor<? extends NamespaceWrapper> constructor =
           
lanceNamespaceBackend.getWrapperClass().getConstructor(LanceConfig.class);
+
       return constructor.newInstance(lanceConfig);
     } catch (Exception e) {
       LOG.error("Error loading namespace implementation for backend type: {}", 
backendType, e);
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/ServiceConstants.java
similarity index 60%
copy from 
lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
copy to 
lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/ServiceConstants.java
index b8a967cd30..f39ea2e684 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/ServiceConstants.java
@@ -16,22 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.lance.common.ops;
 
-import com.lancedb.lance.namespace.model.CreateTableResponse;
-import com.lancedb.lance.namespace.model.DescribeTableResponse;
-import java.util.Map;
+package org.apache.gravitino.lance.service;
 
-public interface LanceTableOperations {
+public class ServiceConstants {
+  public static final String LANCE_HTTP_HEADER_PREFIX = "x-lance-";
 
-  DescribeTableResponse describeTable(String tableId, String delimiter);
-
-  CreateTableResponse createTable(
-      String tableId,
-      String mode,
-      String delimiter,
-      String tableLocation,
-      Map<String, String> tableProperties,
-      String rootCatalog,
-      byte[] arrowStreamBody);
+  public static final String LANCE_TABLE_LOCATION_HEADER =
+      LANCE_HTTP_HEADER_PREFIX + "table-location";
+  public static final String LANCE_TABLE_PROPERTIES_PREFIX_HEADER =
+      LANCE_HTTP_HEADER_PREFIX + "table-properties";
 }
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 1ae9637b20..464201b851 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
@@ -33,7 +33,6 @@ 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;
@@ -63,7 +62,7 @@ public class LanceNamespaceOperations {
   @Timed(name = "list-namespaces." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "list-namespaces", absolute = true)
   public Response listNamespaces(
-      @Encoded @PathParam("id") String namespaceId,
+      @PathParam("id") String namespaceId,
       @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter,
       @QueryParam("page_token") String pageToken,
       @QueryParam("limit") Integer limit) {
@@ -83,7 +82,7 @@ public class LanceNamespaceOperations {
   @Timed(name = "describe-namespaces." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "describe-namespaces", absolute = true)
   public Response describeNamespace(
-      @Encoded @PathParam("id") String namespaceId,
+      @PathParam("id") String namespaceId,
       @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter) {
     try {
       DescribeNamespaceResponse response =
@@ -99,7 +98,7 @@ public class LanceNamespaceOperations {
   @Timed(name = "create-namespaces." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "create-namespaces", absolute = true)
   public Response createNamespace(
-      @Encoded @PathParam("id") String namespaceId,
+      @PathParam("id") String namespaceId,
       @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter,
       CreateNamespaceRequest request) {
     try {
@@ -124,7 +123,7 @@ public class LanceNamespaceOperations {
   @Timed(name = "drop-namespaces." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "drop-namespaces", absolute = true)
   public Response dropNamespace(
-      @Encoded @PathParam("id") String namespaceId,
+      @PathParam("id") String namespaceId,
       @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter,
       DropNamespaceRequest request) {
     try {
@@ -151,7 +150,7 @@ public class LanceNamespaceOperations {
   @Timed(name = "namespace-exists." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "namespace-exists", absolute = true)
   public Response namespaceExists(
-      @Encoded @PathParam("id") String namespaceId,
+      @PathParam("id") String namespaceId,
       @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") 
String delimiter) {
     try {
       lanceNamespace.asNamespaceOps().namespaceExists(namespaceId, 
Pattern.quote(delimiter));
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
index 690cb8759e..5590eef9bd 100644
--- 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
@@ -19,24 +19,33 @@
 package org.apache.gravitino.lance.service.rest;
 
 import static 
org.apache.gravitino.lance.common.ops.NamespaceWrapper.NAMESPACE_DELIMITER_DEFAULT;
+import static 
org.apache.gravitino.lance.service.ServiceConstants.LANCE_TABLE_LOCATION_HEADER;
+import static 
org.apache.gravitino.lance.service.ServiceConstants.LANCE_TABLE_PROPERTIES_PREFIX_HEADER;
 
 import com.codahale.metrics.annotation.ResponseMetered;
 import com.codahale.metrics.annotation.Timed;
 import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.collect.Maps;
 import com.lancedb.lance.namespace.model.CreateTableResponse;
+import com.lancedb.lance.namespace.model.DeregisterTableRequest;
+import com.lancedb.lance.namespace.model.DeregisterTableResponse;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
+import com.lancedb.lance.namespace.model.RegisterTableRequest;
+import com.lancedb.lance.namespace.model.RegisterTableResponse;
 import com.lancedb.lance.namespace.util.JsonUtil;
 import java.util.Map;
 import javax.inject.Inject;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DefaultValue;
-import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.lance.common.ops.NamespaceWrapper;
@@ -81,18 +90,20 @@ public class LanceTableOperations {
       @PathParam("id") String tableId,
       @QueryParam("mode") @DefaultValue("create") String mode, // create, 
exist_ok, overwrite
       @QueryParam("delimiter") @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) 
String delimiter,
-      @HeaderParam("x-lance-table-location") String tableLocation,
-      @HeaderParam("x-lance-table-properties") String tableProperties,
-      @HeaderParam("x-lance-root-catalog") String rootCatalog,
+      @Context HttpHeaders headers,
       byte[] arrowStreamBody) {
     try {
+      // Extract table properties from header
+      MultivaluedMap<String, String> headersMap = headers.getRequestHeaders();
+      String tableLocation = headersMap.getFirst(LANCE_TABLE_LOCATION_HEADER);
+      String tableProperties = 
headersMap.getFirst(LANCE_TABLE_PROPERTIES_PREFIX_HEADER);
+
       Map<String, String> props =
           JsonUtil.mapper().readValue(tableProperties, new TypeReference<>() 
{});
       CreateTableResponse response =
           lanceNamespace
               .asTableOps()
-              .createTable(
-                  tableId, mode, delimiter, tableLocation, props, rootCatalog, 
arrowStreamBody);
+              .createTable(tableId, mode, delimiter, tableLocation, props, 
arrowStreamBody);
       return Response.ok(response).build();
     } catch (Exception e) {
       return LanceExceptionMapper.toRESTResponse(tableId, e);
@@ -107,10 +118,13 @@ public class LanceTableOperations {
       @PathParam("id") String tableId,
       @QueryParam("mode") @DefaultValue("create") String mode, // create, 
exist_ok, overwrite
       @QueryParam("delimiter") @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) 
String delimiter,
-      @HeaderParam("x-lance-table-location") String tableLocation,
-      @HeaderParam("x-lance-root-catalog") String rootCatalog,
-      @HeaderParam("x-lance-table-properties") String tableProperties) {
+      @Context HttpHeaders headers) {
     try {
+      // Extract table properties from header
+      MultivaluedMap<String, String> headersMap = headers.getRequestHeaders();
+      String tableLocation = headersMap.getFirst(LANCE_TABLE_LOCATION_HEADER);
+      String tableProperties = 
headersMap.getFirst(LANCE_TABLE_PROPERTIES_PREFIX_HEADER);
+
       Map<String, String> props =
           StringUtils.isBlank(tableProperties)
               ? Map.of()
@@ -118,7 +132,52 @@ public class LanceTableOperations {
       CreateTableResponse response =
           lanceNamespace
               .asTableOps()
-              .createTable(tableId, mode, delimiter, tableLocation, props, 
rootCatalog, null);
+              .createTable(tableId, mode, delimiter, tableLocation, props, 
null);
+      return Response.ok(response).build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(tableId, e);
+    }
+  }
+
+  @POST
+  @Path("/register")
+  @Timed(name = "register-table." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "register-table", absolute = true)
+  public Response registerTable(
+      @PathParam("id") String tableId,
+      @QueryParam("mode") @DefaultValue("create") String mode, // overwrite or
+      @QueryParam("delimiter") @DefaultValue("$") String delimiter,
+      @Context HttpHeaders headers,
+      RegisterTableRequest registerTableRequest) {
+    try {
+      Map<String, String> props =
+          registerTableRequest.getProperties() == null
+              ? Maps.newHashMap()
+              : Maps.newHashMap(registerTableRequest.getProperties());
+      props.put("register", "true");
+      props.put("location", registerTableRequest.getLocation());
+      props.put("format", "lance");
+
+      RegisterTableResponse response =
+          lanceNamespace.asTableOps().registerTable(tableId, mode, delimiter, 
props);
+      return Response.ok(response).build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(tableId, e);
+    }
+  }
+
+  @POST
+  @Path("/deregister")
+  @Timed(name = "deregister-table." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "deregister-table", absolute = true)
+  public Response deregisterTable(
+      @PathParam("id") String tableId,
+      @QueryParam("delimiter") @DefaultValue("$") String delimiter,
+      @Context HttpHeaders headers,
+      DeregisterTableRequest deregisterTableRequest) {
+    try {
+      DeregisterTableResponse response =
+          lanceNamespace.asTableOps().deregisterTable(tableId, delimiter);
       return Response.ok(response).build();
     } catch (Exception e) {
       return LanceExceptionMapper.toRESTResponse(tableId, e);

Reply via email to