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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 8375559ce88 [refactor](lance) improve lance catalog implementation in 
FE (#66581)
8375559ce88 is described below

commit 8375559ce889f20f208a2a68df711bf622aa390f
Author: zhangstar333 <[email protected]>
AuthorDate: Sat Aug 8 01:42:02 2026 +0800

    [refactor](lance) improve lance catalog implementation in FE (#66581)
    
    ### What problem does this PR solve?
    Problem Summary:
     refactor some code including:
      - Adds dedicated properties for filesystem and REST catalogs.
      - Uses the Lance `tableExists` API for table existence checks.
      - Refactors metadata loading and snapshot version resolution.
      - Improves method naming and documentation for readability.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [x] No need to test or manual test. Explain why:
    - [x] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../iceberg/scripts/lance_rest_server.py           |  27 ++
 .../datasource/lance/LanceExternalCatalog.java     | 340 ++++++---------------
 .../datasource/lance/LanceMetadataLoader.java      |  66 ++--
 .../doris/datasource/lance/LanceNamespaceName.java |  81 +++--
 .../doris/datasource/lance/LanceReadOptions.java   |  43 +++
 .../datasource/lance/LanceSnapshotResolver.java    |  20 ++
 .../doris/datasource/lance/LanceVectorQuery.java   |  75 +++--
 .../metastore/AbstractLanceProperties.java         | 104 +++++++
 .../LanceFileSystemMetastoreProperties.java        | 104 +++++++
 .../property/metastore/LancePropertiesFactory.java |  45 +++
 .../metastore/LanceRestMetastoreProperties.java    | 229 ++++++++++++++
 .../property/metastore/MetastoreProperties.java    |   2 +-
 .../doris/tablefunction/S3TableValuedFunction.java |   2 +-
 .../VectorSearchTableValuedFunction.java           |  14 +-
 .../lance/LanceFilesystemCatalogTest.java          |  52 ++--
 .../datasource/lance/LanceRestCatalogTest.java     |  51 ++++
 .../datasource/lance/LanceVectorQueryTest.java     |  25 +-
 .../property/metastore/LancePropertiesTest.java    |  63 ++++
 18 files changed, 965 insertions(+), 378 deletions(-)

diff --git 
a/docker/thirdparties/docker-compose/iceberg/scripts/lance_rest_server.py 
b/docker/thirdparties/docker-compose/iceberg/scripts/lance_rest_server.py
index 7ef594a5c42..8aef278c024 100644
--- a/docker/thirdparties/docker-compose/iceberg/scripts/lance_rest_server.py
+++ b/docker/thirdparties/docker-compose/iceberg/scripts/lance_rest_server.py
@@ -72,6 +72,16 @@ def _decode_identifier(identifier: str) -> tuple[str, ...]:
     return tuple(part for part in identifier.split(DELIMITER) if part)
 
 
+def _namespace_exists(namespace: tuple[str, ...]) -> bool:
+    if not namespace:
+        return True
+    return any(
+        len(identifier) > len(namespace)
+        and identifier[: len(namespace)] == namespace
+        for identifier in TABLES
+    )
+
+
 class LanceRestHandler(BaseHTTPRequestHandler):
     server_version = "DorisLanceRestFixture/1.0"
 
@@ -147,6 +157,18 @@ class LanceRestHandler(BaseHTTPRequestHandler):
             )
             return
 
+        table_exists_match = re.fullmatch(r"/v1/table/(.+)/exists", path)
+        if table_exists_match:
+            identifier = _decode_identifier(table_exists_match.group(1))
+            if identifier in TABLES:
+                self._write_empty(200)
+                return
+            if not _namespace_exists(identifier[:-1]):
+                self._write_json(404, {"error": "namespace not found", "code": 
1})
+                return
+            self._write_json(404, {"error": "table not found", "code": 4})
+            return
+
         self._write_json(404, {"error": "not found", "code": 4})
 
     def _authorized(self) -> bool:
@@ -168,6 +190,11 @@ class LanceRestHandler(BaseHTTPRequestHandler):
         self.end_headers()
         self.wfile.write(response)
 
+    def _write_empty(self, status: int) -> None:
+        self.send_response(status)
+        self.send_header("Content-Length", "0")
+        self.end_headers()
+
     def log_message(self, message: str, *args: object) -> None:
         print(f"{self.address_string()} - {message % args}", flush=True)
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceExternalCatalog.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceExternalCatalog.java
index d943b185e7f..4339a863993 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceExternalCatalog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceExternalCatalog.java
@@ -24,22 +24,25 @@ import org.apache.doris.datasource.CatalogProperty;
 import org.apache.doris.datasource.ExternalCatalog;
 import org.apache.doris.datasource.InitCatalogLog;
 import org.apache.doris.datasource.SessionContext;
+import org.apache.doris.datasource.property.metastore.AbstractLanceProperties;
+import 
org.apache.doris.datasource.property.metastore.LanceFileSystemMetastoreProperties;
+import 
org.apache.doris.datasource.property.metastore.LanceRestMetastoreProperties;
 
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.lance.namespace.LanceNamespace;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.TableNotFoundException;
 import org.lance.namespace.model.DescribeTableRequest;
 import org.lance.namespace.model.DescribeTableResponse;
 import org.lance.namespace.model.ListNamespacesRequest;
 import org.lance.namespace.model.ListNamespacesResponse;
 import org.lance.namespace.model.ListTablesRequest;
 import org.lance.namespace.model.ListTablesResponse;
+import org.lance.namespace.model.TableExistsRequest;
 
-import java.net.URI;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -47,35 +50,27 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Queue;
 import java.util.Set;
-import java.util.regex.Pattern;
 
 /** Read-only Lance Directory or REST Namespace catalog. */
 public class LanceExternalCatalog extends ExternalCatalog {
-    public static final String LANCE_CATALOG_TYPE = "lance.catalog.type";
-    public static final String LANCE_FILESYSTEM = "filesystem";
-    public static final String LANCE_REST = "rest";
-    public static final String WAREHOUSE = "warehouse";
-    public static final String NAMESPACE_PARENT = "lance.namespace.parent";
-    public static final String NAMESPACE_DELIMITER = 
"lance.namespace.delimiter";
-    public static final String ROOT_DATABASE = "lance.namespace.root_database";
-    public static final String REST_URI = "lance.rest.uri";
-    public static final String REST_SECURITY_TYPE = "lance.rest.security.type";
-    public static final String REST_BEARER_TOKEN = "lance.rest.bearer-token";
-    public static final String REST_API_KEY = "lance.rest.api-key";
-    public static final String REST_HEADER_PREFIX = "lance.rest.header.";
-
-    private static final String DEFAULT_DELIMITER = "$";
-    private static final String DEFAULT_ROOT_DATABASE = "default";
+    public static final String LANCE_CATALOG_TYPE = 
AbstractLanceProperties.LANCE_CATALOG_TYPE;
+    public static final String LANCE_FILESYSTEM = 
AbstractLanceProperties.LANCE_FILESYSTEM;
+    public static final String LANCE_REST = AbstractLanceProperties.LANCE_REST;
+    public static final String WAREHOUSE = 
LanceFileSystemMetastoreProperties.WAREHOUSE;
+    public static final String NAMESPACE_PARENT = 
AbstractLanceProperties.NAMESPACE_PARENT;
+    public static final String NAMESPACE_DELIMITER = 
AbstractLanceProperties.NAMESPACE_DELIMITER;
+    public static final String ROOT_DATABASE = 
AbstractLanceProperties.ROOT_DATABASE;
+    public static final String REST_URI = 
LanceRestMetastoreProperties.REST_URI;
+    public static final String REST_SECURITY_TYPE = 
LanceRestMetastoreProperties.REST_SECURITY_TYPE;
+    public static final String REST_BEARER_TOKEN = 
LanceRestMetastoreProperties.REST_BEARER_TOKEN;
+    public static final String REST_API_KEY = 
LanceRestMetastoreProperties.REST_API_KEY;
+    public static final String REST_HEADER_PREFIX = 
LanceRestMetastoreProperties.REST_HEADER_PREFIX;
+
     private static final String DATABASE_NAMESPACE_DELIMITER = ".";
-    private static final String REST_SECURITY_NONE = "none";
-    private static final String REST_SECURITY_BEARER = "bearer";
-    private static final String REST_SECURITY_API_KEY = "api_key";
-    private static final Pattern HTTP_HEADER_NAME = 
Pattern.compile("^[!#$%&'*+.^_`|~0-9A-Za-z-]+$");
     private static final int PAGE_SIZE = 1000;
     private static final long ALLOCATOR_LIMIT = 256L * 1024 * 1024;
 
@@ -83,7 +78,6 @@ public class LanceExternalCatalog extends ExternalCatalog {
     private transient BufferAllocator allocator;
     private transient List<String> parentNamespace = Collections.emptyList();
     private transient String catalogType;
-    private transient String namespaceDelimiter;
     private transient String rootDatabase;
     private transient Map<String, String> javaStorageOptions = 
Collections.emptyMap();
     private transient Map<String, String> backendStorageOptions = 
Collections.emptyMap();
@@ -99,20 +93,19 @@ public class LanceExternalCatalog extends ExternalCatalog {
     protected void initLocalObjectsImpl() {
         try {
             namespaceLock = new Object();
-            catalogType = normalizedCatalogType();
-            namespaceDelimiter = 
catalogProperty.getOrDefault(NAMESPACE_DELIMITER, DEFAULT_DELIMITER);
-            rootDatabase = catalogProperty.getOrDefault(ROOT_DATABASE, 
DEFAULT_ROOT_DATABASE);
-            parentNamespace = LanceNamespaceName.parseParent(
-                    catalogProperty.getOrDefault(NAMESPACE_PARENT, ""), 
namespaceDelimiter);
+            AbstractLanceProperties properties = getLanceProperties();
+            catalogType = properties.getLanceCatalogType();
+            rootDatabase = properties.getRootDatabase();
+            parentNamespace = LanceNamespaceName.parseParentNamespace(
+                    properties.getNamespaceParent(), 
properties.getNamespaceDelimiter());
             backendStorageOptions = 
catalogProperty.getBackendStorageProperties();
             javaStorageOptions = 
LanceStorageOptions.forJavaSdk(backendStorageOptions);
 
             allocator = new RootAllocator(ALLOCATOR_LIMIT);
-            namespace = connectNamespace(catalogType, allocator, 
javaStorageOptions);
+            namespace = properties.createNamespace(allocator, 
javaStorageOptions);
         } catch (Exception e) {
             closeLanceObjects();
-            throw new RuntimeException("Failed to initialize Lance " + 
normalizedCatalogType() + " catalog '"
-                    + getName()
+            throw new RuntimeException("Failed to initialize Lance catalog '" 
+ getName()
                     + "': " + sanitizedRootCauseMessage(e), safeCause(e));
         }
     }
@@ -126,14 +119,14 @@ public class LanceExternalCatalog extends ExternalCatalog 
{
             return;
         }
 
+        AbstractLanceProperties properties = getLanceProperties();
         Map<String, String> storageOptions = LanceStorageOptions.forJavaSdk(
                 catalogProperty.getBackendStorageProperties());
-        String delimiter = catalogProperty.getOrDefault(NAMESPACE_DELIMITER, 
DEFAULT_DELIMITER);
-        List<String> parent = LanceNamespaceName.parseParent(
-                catalogProperty.getOrDefault(NAMESPACE_PARENT, ""), delimiter);
-        String type = normalizedCatalogType();
+        List<String> parent = LanceNamespaceName.parseParentNamespace(
+                properties.getNamespaceParent(), 
properties.getNamespaceDelimiter());
+        String type = properties.getLanceCatalogType();
         try (BufferAllocator testAllocator = new 
RootAllocator(ALLOCATOR_LIMIT)) {
-            LanceNamespace testNamespace = connectNamespace(type, 
testAllocator, storageOptions);
+            LanceNamespace testNamespace = 
properties.createNamespace(testAllocator, storageOptions);
             try {
                 testNamespace.listTables(new 
ListTablesRequest().id(parent).limit(1));
                 testNamespace.listNamespaces(new 
ListNamespacesRequest().id(parent).limit(1));
@@ -146,226 +139,32 @@ public class LanceExternalCatalog extends 
ExternalCatalog {
         }
     }
 
-    private LanceNamespace connectNamespace(String type, BufferAllocator 
namespaceAllocator,
-            Map<String, String> storageOptions) {
-        if (LANCE_FILESYSTEM.equals(type)) {
-            return connectDirectoryNamespace(namespaceAllocator, 
storageOptions);
-        }
-        if (LANCE_REST.equals(type)) {
-            return connectRestNamespace(namespaceAllocator);
-        }
-        throw new IllegalArgumentException("Unsupported Lance catalog type '" 
+ type + "'");
-    }
-
-    private LanceNamespace connectDirectoryNamespace(BufferAllocator 
namespaceAllocator,
-            Map<String, String> storageOptions) {
-        Map<String, String> namespaceProperties = new HashMap<>();
-        namespaceProperties.put("root", 
catalogProperty.getOrDefault(WAREHOUSE, ""));
-        storageOptions.forEach((key, value) -> 
namespaceProperties.put("storage." + key, value));
-        return LanceNamespace.connect("dir", namespaceProperties, 
namespaceAllocator);
-    }
-
-    private LanceNamespace connectRestNamespace(BufferAllocator 
namespaceAllocator) {
-        Map<String, String> namespaceProperties = new HashMap<>();
-        namespaceProperties.put("uri", normalizedRestUri());
-        namespaceProperties.put("delimiter",
-                catalogProperty.getOrDefault(NAMESPACE_DELIMITER, 
DEFAULT_DELIMITER));
-
-        Map<String, String> properties = catalogProperty.getProperties();
-        properties.forEach((key, value) -> {
-            if (key.startsWith(REST_HEADER_PREFIX)) {
-                namespaceProperties.put("header." + 
key.substring(REST_HEADER_PREFIX.length()), value);
-            }
-        });
-        String securityType = properties.getOrDefault(REST_SECURITY_TYPE, 
REST_SECURITY_NONE)
-                .trim().toLowerCase(Locale.ROOT);
-        if (REST_SECURITY_BEARER.equals(securityType)) {
-            namespaceProperties.put("header.Authorization", "Bearer " + 
properties.get(REST_BEARER_TOKEN));
-        } else if (REST_SECURITY_API_KEY.equals(securityType)) {
-            namespaceProperties.put("header.x-api-key", 
properties.get(REST_API_KEY));
-        }
-        return LanceNamespace.connect("rest", namespaceProperties, 
namespaceAllocator);
-    }
-
     @Override
     public void checkProperties() throws DdlException {
         super.checkProperties();
-        Map<String, String> properties = catalogProperty.getProperties();
-        String type = normalizedCatalogType();
-        if (!LANCE_FILESYSTEM.equals(type) && !LANCE_REST.equals(type)) {
-            throw new DdlException("Property '" + LANCE_CATALOG_TYPE
-                    + "' must be 'filesystem' or 'rest', but was '" + type + 
"'");
-        }
-        validateCommonProperties(properties);
-        if (LANCE_FILESYSTEM.equals(type)) {
-            validateFilesystemProperties(properties);
-        } else {
-            validateRestProperties(properties);
-        }
-    }
-
-    private void validateCommonProperties(Map<String, String> properties) 
throws DdlException {
-        String delimiter = properties.getOrDefault(NAMESPACE_DELIMITER, 
DEFAULT_DELIMITER);
-        if (delimiter.isEmpty() || delimiter.indexOf('\\') >= 0) {
-            throw new DdlException("Property '" + NAMESPACE_DELIMITER
-                    + "' cannot be empty or contain the escape character 
'\\'");
-        }
-        String rootDb = properties.getOrDefault(ROOT_DATABASE, 
DEFAULT_ROOT_DATABASE);
-        if (StringUtils.isBlank(rootDb)) {
-            throw new DdlException("Property '" + ROOT_DATABASE
-                    + "' must be non-empty");
-        }
-        
LanceNamespaceName.parseParent(properties.getOrDefault(NAMESPACE_PARENT, ""), 
delimiter);
-    }
-
-    private void validateFilesystemProperties(Map<String, String> properties) 
throws DdlException {
-        String warehouse = properties.get(WAREHOUSE);
-        if (StringUtils.isBlank(warehouse)) {
-            throw new DdlException("Missing required property 'warehouse' for 
Lance filesystem catalog");
-        }
-        validateWarehouse(warehouse);
-        for (String key : properties.keySet()) {
-            if (key.startsWith("lance.rest.")) {
-                throw new DdlException("Property '" + key + "' is not valid 
for Lance filesystem catalog");
-            }
-        }
-    }
-
-    private void validateRestProperties(Map<String, String> properties) throws 
DdlException {
-        if (properties.containsKey(WAREHOUSE)) {
-            throw new DdlException("Property 'warehouse' is not valid for 
Lance REST catalog");
-        }
-        String restUri = properties.get(REST_URI);
-        if (StringUtils.isBlank(restUri)) {
-            throw new DdlException("Missing required property '" + REST_URI + 
"' for Lance REST catalog");
-        }
-        validateRestUri(restUri);
-
-        String securityType = properties.getOrDefault(REST_SECURITY_TYPE, 
REST_SECURITY_NONE)
-                .trim().toLowerCase(Locale.ROOT);
-        boolean bearerTokenConfigured = 
properties.containsKey(REST_BEARER_TOKEN);
-        boolean apiKeyConfigured = properties.containsKey(REST_API_KEY);
-        boolean hasBearerToken = 
StringUtils.isNotBlank(properties.get(REST_BEARER_TOKEN));
-        boolean hasApiKey = 
StringUtils.isNotBlank(properties.get(REST_API_KEY));
-        switch (securityType) {
-            case REST_SECURITY_NONE:
-                if (bearerTokenConfigured || apiKeyConfigured) {
-                    throw new DdlException("Lance REST security type 'none' 
cannot configure '"
-                            + REST_BEARER_TOKEN + "' or '" + REST_API_KEY + 
"'");
-                }
-                break;
-            case REST_SECURITY_BEARER:
-                if (!hasBearerToken || apiKeyConfigured) {
-                    throw new DdlException("Lance REST security type 'bearer' 
requires only '"
-                            + REST_BEARER_TOKEN + "'");
-                }
-                validateCredentialHeaderValue(REST_BEARER_TOKEN, 
properties.get(REST_BEARER_TOKEN));
-                break;
-            case REST_SECURITY_API_KEY:
-                if (!hasApiKey || bearerTokenConfigured) {
-                    throw new DdlException("Lance REST security type 'api_key' 
requires only '"
-                            + REST_API_KEY + "'");
-                }
-                validateCredentialHeaderValue(REST_API_KEY, 
properties.get(REST_API_KEY));
-                break;
-            default:
-                throw new DdlException("Property '" + REST_SECURITY_TYPE
-                        + "' must be 'none', 'bearer', or 'api_key'");
-        }
-
-        Set<String> supportedKeys = new HashSet<>();
-        Collections.addAll(supportedKeys, REST_URI, REST_SECURITY_TYPE, 
REST_BEARER_TOKEN, REST_API_KEY);
-        for (Map.Entry<String, String> entry : properties.entrySet()) {
-            String key = entry.getKey();
-            if (!key.startsWith("lance.rest.") || supportedKeys.contains(key)) 
{
-                continue;
-            }
-            if (!key.startsWith(REST_HEADER_PREFIX)) {
-                throw new DdlException("Unsupported Lance REST property '" + 
key + "'");
-            }
-            validateRestHeader(key.substring(REST_HEADER_PREFIX.length()), 
entry.getValue());
-        }
-    }
-
-    private void validateRestHeader(String headerName, String headerValue) 
throws DdlException {
-        if (StringUtils.isBlank(headerName) || 
!HTTP_HEADER_NAME.matcher(headerName).matches()) {
-            throw new DdlException("Invalid HTTP header name in property '" + 
REST_HEADER_PREFIX
-                    + headerName + "'");
-        }
-        if ("authorization".equalsIgnoreCase(headerName) || 
"x-api-key".equalsIgnoreCase(headerName)) {
-            throw new DdlException("Authentication header '" + headerName + "' 
must be configured through '"
-                    + REST_SECURITY_TYPE + "'");
-        }
-        if (headerValue == null || headerValue.indexOf('\r') >= 0 || 
headerValue.indexOf('\n') >= 0) {
-            throw new DdlException("Invalid HTTP header value in property '" + 
REST_HEADER_PREFIX
-                    + headerName + "'");
-        }
-    }
-
-    private void validateCredentialHeaderValue(String propertyName, String 
value) throws DdlException {
-        if (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0) {
-            throw new DdlException("Invalid HTTP credential value in property 
'" + propertyName + "'");
-        }
-    }
-
-    private void validateRestUri(String value) throws DdlException {
-        URI uri;
         try {
-            uri = URI.create(value.trim());
+            AbstractLanceProperties properties = getLanceProperties();
+            LanceNamespaceName.parseParentNamespace(
+                    properties.getNamespaceParent(), 
properties.getNamespaceDelimiter());
         } catch (IllegalArgumentException e) {
-            throw new DdlException("Invalid Lance REST URI in property '" + 
REST_URI + "'", e);
-        }
-        String scheme = uri.getScheme();
-        if (scheme == null || (!("http".equalsIgnoreCase(scheme)) && 
!("https".equalsIgnoreCase(scheme)))) {
-            throw new DdlException("Property '" + REST_URI + "' must use http 
or https");
-        }
-        if (StringUtils.isBlank(uri.getRawAuthority()) || uri.getRawUserInfo() 
!= null
-                || uri.getRawQuery() != null || uri.getRawFragment() != null) {
-            throw new DdlException("Property '" + REST_URI
-                    + "' must contain an authority and cannot contain 
user-info, query, or fragment");
-        }
-    }
-
-    private String normalizedCatalogType() {
-        return catalogProperty.getOrDefault(LANCE_CATALOG_TYPE, 
LANCE_FILESYSTEM)
-                .trim().toLowerCase(Locale.ROOT);
-    }
-
-    private String normalizedRestUri() {
-        String uri = catalogProperty.getOrDefault(REST_URI, "").trim();
-        while (uri.endsWith("/")) {
-            uri = uri.substring(0, uri.length() - 1);
+            throw new DdlException(e.getMessage(), e);
         }
-        return uri;
     }
 
-    private void validateWarehouse(String warehouse) throws DdlException {
-        URI uri;
-        try {
-            uri = URI.create(warehouse);
-        } catch (IllegalArgumentException e) {
-            throw new DdlException("Invalid Lance warehouse URI: " + 
warehouse, e);
-        }
-        if (uri.getScheme() == null) {
-            Path path = Paths.get(warehouse);
-            if (!path.isAbsolute()) {
-                throw new DdlException("Local Lance warehouse must be an 
absolute path: " + warehouse);
-            }
-            return;
-        }
-        String scheme = uri.getScheme().toLowerCase();
-        if (!"file".equals(scheme) && !"s3".equals(scheme)) {
-            throw new DdlException("Unsupported Lance filesystem warehouse 
scheme '" + scheme
-                    + "'; first phase supports local/file and s3");
-        }
+    private AbstractLanceProperties getLanceProperties() {
+        return (AbstractLanceProperties) 
catalogProperty.getMetastoreProperties();
     }
 
     @Override
     protected List<String> listDatabaseNames() {
         makeSureInitialized();
+
+        // The configured root database represents the empty relative Lance 
namespace.
         LinkedHashSet<String> databases = new LinkedHashSet<>();
         databases.add(rootDatabase);
 
+        // Breadth-first traversal starts at the catalog's configured parent 
namespace.
+        // Queue entries remain relative so they can be exposed as Doris 
database names.
         Queue<List<String>> queue = new ArrayDeque<>();
         queue.add(Collections.emptyList());
         Set<List<String>> visited = new HashSet<>();
@@ -374,17 +173,30 @@ public class LanceExternalCatalog extends ExternalCatalog 
{
             if (!visited.add(relativeParent)) {
                 continue;
             }
-            for (String child : 
listChildNamespaces(fullNamespace(relativeParent))) {
+
+            // The Lance API expects a full namespace, including the 
configured parent.
+            List<String> fullParentNamespace = 
buildFullNamespace(relativeParent);
+            for (String child : listChildNamespaces(fullParentNamespace)) {
                 List<String> relativeChild = new ArrayList<>(relativeParent);
                 relativeChild.add(child);
-                databases.add(LanceNamespaceName.encode(
+
+                // Doris exposes each hierarchical relative namespace as one 
flat database name.
+                databases.add(LanceNamespaceName.namespaceToDorisDatabaseName(
                         relativeChild, DATABASE_NAMESPACE_DELIMITER, 
rootDatabase));
+
+                // Visit this child later to discover namespaces nested below 
it.
                 queue.add(relativeChild);
             }
         }
         return new ArrayList<>(databases);
     }
 
+    /**
+     * Lists all direct child namespace names under the given full Lance 
namespace.
+     *
+     * <p>Each request asks for at most {@link #PAGE_SIZE} children. If Lance 
returns a
+     * page token, this method keeps requesting subsequent pages until all 
children are collected.
+     */
     private List<String> listChildNamespaces(List<String> namespaceId) {
         List<String> result = new ArrayList<>();
         String pageToken = null;
@@ -410,8 +222,9 @@ public class LanceExternalCatalog extends ExternalCatalog {
     protected List<String> listTableNamesFromRemote(SessionContext ctx, String 
dbName) {
         makeSureInitialized();
         try {
-            List<String> namespaceId = fullNamespace(
-                    LanceNamespaceName.decode(dbName, 
DATABASE_NAMESPACE_DELIMITER, rootDatabase));
+            List<String> relativeNamespace = 
LanceNamespaceName.dorisDatabaseNameToNamespace(
+                    dbName, DATABASE_NAMESPACE_DELIMITER, rootDatabase);
+            List<String> namespaceId = buildFullNamespace(relativeNamespace);
             List<String> result = new ArrayList<>();
             String pageToken = null;
             Set<String> consumedTokens = new HashSet<>();
@@ -437,11 +250,21 @@ public class LanceExternalCatalog extends ExternalCatalog 
{
 
     @Override
     public boolean tableExist(SessionContext ctx, String dbName, String 
tblName) {
+        makeSureInitialized();
         try {
-            describeTable(dbName, tblName);
+            List<String> relativeNamespace = 
LanceNamespaceName.dorisDatabaseNameToNamespace(
+                    dbName, DATABASE_NAMESPACE_DELIMITER, rootDatabase);
+            List<String> tableId = buildFullNamespace(relativeNamespace);
+            tableId.add(tblName);
+            TableExistsRequest request = new TableExistsRequest().id(tableId);
+            synchronized (namespaceLock) {
+                namespace.tableExists(request);
+            }
             return true;
-        } catch (RuntimeException e) {
+        } catch (TableNotFoundException | NamespaceNotFoundException e) {
             return false;
+        } catch (DdlException e) {
+            throw new RuntimeException(e);
         }
     }
 
@@ -480,13 +303,13 @@ public class LanceExternalCatalog extends ExternalCatalog 
{
                         throw new IllegalArgumentException(
                                 "Cannot parse Lance FOR TIME AS OF value '" + 
snapshot.getValue() + "'");
                     }
-                    version = LanceMetadataLoader.resolveVersionAtOrBefore(
+                    version = LanceSnapshotResolver.getVersionAtOrBefore(
                             datasetUri, storageOptions, timestamp, allocator);
                 }
-                return LanceMetadataLoader.load(datasetUri, storageOptions,
+                return LanceMetadataLoader.loadVersion(datasetUri, 
storageOptions,
                         tableBackendStorageOptions, version, allocator);
             }
-            return LanceMetadataLoader.load(datasetUri, storageOptions,
+            return LanceMetadataLoader.loadLatest(datasetUri, storageOptions,
                     tableBackendStorageOptions, allocator);
         } catch (Exception e) {
             throw new RuntimeException("Failed to load Lance table metadata 
for " + dbName + "." + tableName
@@ -496,8 +319,9 @@ public class LanceExternalCatalog extends ExternalCatalog {
 
     private DescribeTableResponse describeTable(String dbName, String 
tableName) {
         try {
-            List<String> tableId = fullNamespace(
-                    LanceNamespaceName.decode(dbName, 
DATABASE_NAMESPACE_DELIMITER, rootDatabase));
+            List<String> relativeNamespace = 
LanceNamespaceName.dorisDatabaseNameToNamespace(
+                    dbName, DATABASE_NAMESPACE_DELIMITER, rootDatabase);
+            List<String> tableId = buildFullNamespace(relativeNamespace);
             tableId.add(tableName);
             DescribeTableRequest request = new 
DescribeTableRequest().id(tableId).withTableUri(true)
                     .vendCredentials(LANCE_REST.equals(catalogType));
@@ -509,7 +333,15 @@ public class LanceExternalCatalog extends ExternalCatalog {
         }
     }
 
-    private List<String> fullNamespace(List<String> relativeNamespace) {
+    /**
+     * Prepends the configured parent namespace to a namespace relative to 
this catalog.
+     *
+     * <p>For example, if {@code parentNamespace} is {@code [company, 
analytics]} and
+     * {@code relativeNamespace} is {@code [sales, daily]}, this method returns
+     * {@code [company, analytics, sales, daily]}. The returned list is a new 
mutable list;
+     * neither input list is modified.
+     */
+    private List<String> buildFullNamespace(List<String> relativeNamespace) {
         List<String> result = new ArrayList<>(parentNamespace.size() + 
relativeNamespace.size());
         result.addAll(parentNamespace);
         result.addAll(relativeNamespace);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceMetadataLoader.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceMetadataLoader.java
index 76a59e5f318..a0502fee873 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceMetadataLoader.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceMetadataLoader.java
@@ -21,7 +21,6 @@ import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
 import org.lance.Dataset;
 import org.lance.Fragment;
-import org.lance.ReadOptions;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -31,54 +30,58 @@ import java.util.OptionalLong;
 /** Loads one fixed Lance dataset snapshot through the Lance Java SDK. */
 public final class LanceMetadataLoader {
     private static final long ALLOCATOR_LIMIT = 256L * 1024 * 1024;
-    private static final long METADATA_CACHE_SIZE = 64L * 1024 * 1024;
 
     private LanceMetadataLoader() {
     }
 
     /**
-     * Load metadata for a directly-addressed dataset, using Doris backend 
storage properties.
-     * This overload owns a short-lived allocator and is suitable for an S3 
TVF.
+     * Loads the latest metadata for a directly addressed dataset and owns a 
short-lived allocator.
+     *
+     * <p>Called by {@link 
org.apache.doris.tablefunction.S3TableValuedFunction} when reading a
+     * Lance dataset through an S3 TVF.
      */
-    public static LanceTableMetadata load(String datasetUri, Map<String, 
String> backendStorageOptions)
+    public static LanceTableMetadata loadLatestForTvf(
+            String datasetUri, Map<String, String> backendStorageOptions)
             throws Exception {
         try (BufferAllocator allocator = new RootAllocator(ALLOCATOR_LIMIT)) {
-            return load(datasetUri, 
LanceStorageOptions.forJavaSdk(backendStorageOptions),
+            return loadLatest(datasetUri, 
LanceStorageOptions.forJavaSdk(backendStorageOptions),
                     backendStorageOptions, allocator);
         }
     }
 
     /**
-     * Load metadata with an allocator and Java storage options already owned 
by a catalog.
-     * Schema, version and fragments are all read from the same opened dataset 
snapshot.
+     * Loads the latest metadata with the allocator and storage options owned 
by a catalog.
+     *
+     * <p>Called by
+     * {@link LanceExternalCatalog#loadTableMetadata(String, String, 
java.util.Optional)} when no
+     * time-travel version is requested. Schema, version, and fragments are 
read from the same
+     * opened dataset snapshot.
      */
-    public static LanceTableMetadata load(String datasetUri, Map<String, 
String> javaStorageOptions,
+    public static LanceTableMetadata loadLatest(String datasetUri, Map<String, 
String> javaStorageOptions,
             Map<String, String> backendStorageOptions, BufferAllocator 
allocator) throws Exception {
-        return load(datasetUri, javaStorageOptions, backendStorageOptions, 
OptionalLong.empty(), allocator);
+        return loadInternal(
+                datasetUri, javaStorageOptions, backendStorageOptions, 
OptionalLong.empty(), allocator);
     }
 
-    /** Load metadata from an explicitly selected Lance version. */
-    public static LanceTableMetadata load(String datasetUri, Map<String, 
String> javaStorageOptions,
+    /**
+     * Loads metadata from an explicitly selected Lance version.
+     *
+     * <p>Called by
+     * {@link LanceExternalCatalog#loadTableMetadata(String, String, 
java.util.Optional)} for both
+     * {@code FOR VERSION AS OF} and the version resolved from {@code FOR TIME 
AS OF}.
+     */
+    public static LanceTableMetadata loadVersion(String datasetUri, 
Map<String, String> javaStorageOptions,
             Map<String, String> backendStorageOptions, long version, 
BufferAllocator allocator) throws Exception {
-        return load(datasetUri, javaStorageOptions, backendStorageOptions, 
OptionalLong.of(version), allocator);
+        return loadInternal(
+                datasetUri, javaStorageOptions, backendStorageOptions, 
OptionalLong.of(version), allocator);
     }
 
-    /** Resolve the latest Lance version whose commit time does not exceed the 
requested timestamp. */
-    public static long resolveVersionAtOrBefore(String datasetUri, Map<String, 
String> javaStorageOptions,
-            long timestampMillis, BufferAllocator allocator) throws Exception {
-        ReadOptions readOptions = readOptions(javaStorageOptions, 
OptionalLong.empty());
-        try (Dataset dataset = 
Dataset.open().allocator(allocator).uri(datasetUri)
-                .readOptions(readOptions).build()) {
-            return 
LanceSnapshotResolver.versionAtOrBefore(dataset.listVersions(), 
timestampMillis);
-        }
-    }
-
-    private static LanceTableMetadata load(String datasetUri, Map<String, 
String> javaStorageOptions,
+    /** Shared implementation for the latest-version and explicit-version 
public entry points. */
+    private static LanceTableMetadata loadInternal(String datasetUri, 
Map<String, String> javaStorageOptions,
             Map<String, String> backendStorageOptions, OptionalLong version,
             BufferAllocator allocator) throws Exception {
-        ReadOptions readOptions = readOptions(javaStorageOptions, version);
         try (Dataset dataset = 
Dataset.open().allocator(allocator).uri(datasetUri)
-                .readOptions(readOptions).build()) {
+                .readOptions(LanceReadOptions.build(javaStorageOptions, 
version)).build()) {
             long resolvedVersion = dataset.version();
             List<LanceTableMetadata.LanceFragmentInfo> fragments = new 
ArrayList<>();
             for (Fragment fragment : dataset.getFragments()) {
@@ -89,15 +92,4 @@ public final class LanceMetadataLoader {
                     backendStorageOptions);
         }
     }
-
-    private static ReadOptions readOptions(Map<String, String> 
javaStorageOptions, OptionalLong version) {
-        ReadOptions.Builder builder = new ReadOptions.Builder()
-                .setStorageOptions(javaStorageOptions)
-                .setIndexCacheSizeBytes(0)
-                .setMetadataCacheSizeBytes(METADATA_CACHE_SIZE);
-        if (version.isPresent()) {
-            builder.setVersion(version.getAsLong());
-        }
-        return builder.build();
-    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceNamespaceName.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceNamespaceName.java
index bea8f09ad94..caa014afb1f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceNamespaceName.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceNamespaceName.java
@@ -23,14 +23,32 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
-/** Reversible mapping between a hierarchical Lance namespace and one Doris 
database name. */
+/**
+ * Converts between a hierarchical Lance namespace and the flat database name 
exposed by Doris.
+ *
+ * <p>The input namespace is relative to the optional parent namespace 
configured on the catalog.
+ * An empty relative namespace maps to {@code rootDatabase}. Other namespace 
components are joined
+ * with {@code delimiter}; delimiter occurrences and backslashes inside a 
component are escaped so
+ * the conversion remains reversible. A one-level namespace equal to {@code 
rootDatabase} receives
+ * an additional leading escape to distinguish it from the empty root 
namespace.
+ *
+ * <p>This class only converts names. It does not access the remote Lance 
namespace service.
+ */
 final class LanceNamespaceName {
     private static final char ESCAPE = '\\';
 
     private LanceNamespaceName() {
     }
 
-    static String encode(List<String> relativeNamespace, String delimiter, 
String rootDatabase) {
+    /**
+     * Converts relative Lance namespace components to one Doris database name.
+     *
+     * <p>For example, with {@code "."} as the delimiter,
+     * {@code [company.analytics, raw]} becomes {@code company\.analytics.raw}.
+     * An empty namespace becomes {@code rootDatabase}.
+     */
+    static String namespaceToDorisDatabaseName(
+            List<String> relativeNamespace, String delimiter, String 
rootDatabase) {
         if (relativeNamespace.isEmpty()) {
             return rootDatabase;
         }
@@ -39,32 +57,58 @@ final class LanceNamespaceName {
             if (i > 0) {
                 result.append(delimiter);
             }
-            result.append(escape(relativeNamespace.get(i), delimiter));
+            result.append(escapeNamespaceComponent(relativeNamespace.get(i), 
delimiter));
         }
         // The local root database represents the empty Lance namespace. Escape
         // the rare, colliding one-level namespace so the mapping stays 
reversible.
         return rootDatabase.contentEquals(result) ? ESCAPE + result.toString() 
: result.toString();
     }
 
-    static List<String> decode(String database, String delimiter, String 
rootDatabase) throws DdlException {
-        if (rootDatabase.equals(database)) {
+    /** Escapes backslashes and delimiters occurring inside one namespace 
component. */
+    private static String escapeNamespaceComponent(String component, String 
delimiter) {
+        return component.replace("\\", "\\\\").replace(delimiter, "\\" + 
delimiter);
+    }
+
+    /**
+     * Restores the relative Lance namespace represented by one Doris database 
name.
+     *
+     * <p>For example, with {@code "."} as the delimiter,
+     * {@code company\.analytics.raw} becomes {@code [company.analytics, raw]}.
+     * A database name equal to {@code rootDatabase} becomes an empty 
namespace.
+     */
+    static List<String> dorisDatabaseNameToNamespace(
+            String databaseName, String delimiter, String rootDatabase) throws 
DdlException {
+        if (rootDatabase.equals(databaseName)) {
             return Collections.emptyList();
         }
-        if (database.length() > 1 && database.charAt(0) == ESCAPE
-                && rootDatabase.equals(database.substring(1))) {
-            return splitEscaped(database.substring(1), delimiter);
+        if (databaseName.length() > 1 && databaseName.charAt(0) == ESCAPE
+                && rootDatabase.equals(databaseName.substring(1))) {
+            return parseEscapedNamespace(databaseName.substring(1), delimiter);
         }
-        return splitEscaped(database, delimiter);
+        return parseEscapedNamespace(databaseName, delimiter);
     }
 
-    static List<String> parseParent(String value, String delimiter) throws 
DdlException {
+    /**
+     * Parses the escaped parent namespace configured in the catalog 
properties.
+     *
+     * <p>For example, with {@code "."} as the delimiter,
+     * {@code company.analytics} becomes {@code [company, analytics]}.
+     */
+    static List<String> parseParentNamespace(String value, String delimiter) 
throws DdlException {
         if (value == null || value.isEmpty()) {
             return Collections.emptyList();
         }
-        return splitEscaped(value, delimiter);
+        return parseEscapedNamespace(value, delimiter);
     }
 
-    private static List<String> splitEscaped(String value, String delimiter) 
throws DdlException {
+    /**
+     * Splits an escaped namespace and rejects invalid escapes or empty 
components.
+     *
+     * <p>For example, with {@code "."} as the delimiter, {@code 
company\.analytics.raw}
+     * is parsed as {@code [company.analytics, raw]}.
+     */
+    private static List<String> parseEscapedNamespace(String value, String 
delimiter)
+            throws DdlException {
         List<String> result = new ArrayList<>();
         StringBuilder current = new StringBuilder();
         for (int i = 0; i < value.length();) {
@@ -80,22 +124,19 @@ final class LanceNamespaceName {
                     throw new DdlException("Invalid escape in Lance namespace 
'" + value + "'");
                 }
             } else if (value.startsWith(delimiter, i)) {
-                addComponent(result, current, value);
+                addNamespaceComponent(result, current, value);
                 i += delimiter.length();
             } else {
                 current.append(value.charAt(i++));
             }
         }
-        addComponent(result, current, value);
+        addNamespaceComponent(result, current, value);
         return result;
     }
 
-    private static String escape(String component, String delimiter) {
-        return component.replace("\\", "\\\\").replace(delimiter, "\\" + 
delimiter);
-    }
-
-    private static void addComponent(List<String> result, StringBuilder 
current, String encoded)
-            throws DdlException {
+    /** Adds one parsed component while enforcing that Lance namespace 
components are non-empty. */
+    private static void addNamespaceComponent(
+            List<String> result, StringBuilder current, String encoded) throws 
DdlException {
         if (current.length() == 0) {
             throw new DdlException("Empty Lance namespace component in '" + 
encoded + "'");
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceReadOptions.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceReadOptions.java
new file mode 100644
index 00000000000..432426b2c95
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceReadOptions.java
@@ -0,0 +1,43 @@
+// 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.doris.datasource.lance;
+
+import org.lance.ReadOptions;
+
+import java.util.Map;
+import java.util.OptionalLong;
+
+/** Builds consistently configured SDK read options for Lance dataset metadata 
access. */
+final class LanceReadOptions {
+    private static final long METADATA_CACHE_SIZE = 64L * 1024 * 1024;
+
+    private LanceReadOptions() {
+    }
+
+    /** Called by metadata loading and timestamp-based snapshot resolution. */
+    static ReadOptions build(Map<String, String> javaStorageOptions, 
OptionalLong version) {
+        ReadOptions.Builder builder = new ReadOptions.Builder()
+                .setStorageOptions(javaStorageOptions)
+                .setIndexCacheSizeBytes(0)
+                .setMetadataCacheSizeBytes(METADATA_CACHE_SIZE);
+        if (version.isPresent()) {
+            builder.setVersion(version.getAsLong());
+        }
+        return builder.build();
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceSnapshotResolver.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceSnapshotResolver.java
index bf194ad0292..7398367c98a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceSnapshotResolver.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceSnapshotResolver.java
@@ -17,11 +17,15 @@
 
 package org.apache.doris.datasource.lance;
 
+import org.apache.arrow.memory.BufferAllocator;
+import org.lance.Dataset;
 import org.lance.Version;
 
 import java.time.Instant;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
+import java.util.OptionalLong;
 
 /** Resolves Doris time-travel selectors to immutable Lance version IDs. */
 final class LanceSnapshotResolver {
@@ -43,6 +47,22 @@ final class LanceSnapshotResolver {
         return version;
     }
 
+    /**
+     * Gets the latest Lance version whose commit time does not exceed the 
requested timestamp.
+     *
+     * <p>Called by
+     * {@link LanceExternalCatalog#loadTableMetadata(String, String, 
java.util.Optional)} to resolve
+     * a {@code FOR TIME AS OF} clause before loading the selected metadata 
snapshot.
+     */
+    static long getVersionAtOrBefore(String datasetUri, Map<String, String> 
javaStorageOptions,
+            long timestampMillis, BufferAllocator allocator) throws Exception {
+        try (Dataset dataset = 
Dataset.open().allocator(allocator).uri(datasetUri)
+                .readOptions(LanceReadOptions.build(javaStorageOptions, 
OptionalLong.empty())).build()) {
+            return versionAtOrBefore(dataset.listVersions(), timestampMillis);
+        }
+    }
+
+    /** Selects a version from the version list fetched by {@link 
#getVersionAtOrBefore}. */
     static long versionAtOrBefore(List<Version> versions, long 
timestampMillis) {
         Instant requestedTime = Instant.ofEpochMilli(timestampMillis);
         return versions.stream()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceVectorQuery.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceVectorQuery.java
index 27bf7c67767..5b5ebd8f88c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceVectorQuery.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceVectorQuery.java
@@ -44,10 +44,10 @@ public final class LanceVectorQuery {
     }
 
     /**
-     * Resolve a column using Doris' case-insensitive identifier behavior 
while preserving the
+     * Find a vector column using Doris' case-insensitive identifier behavior 
while preserving the
      * physical Lance field name sent to the backend.
      */
-    public static Field resolveVectorField(Schema schema, String column) 
throws AnalysisException {
+    public static Field findVectorColumnField(Schema schema, String column) 
throws AnalysisException {
         Field match = null;
         for (Field field : schema.getFields()) {
             if (field.getName().equalsIgnoreCase(column)) {
@@ -64,7 +64,19 @@ public final class LanceVectorQuery {
         return match;
     }
 
-    public static TSearchVector encode(Field field, String json) throws 
AnalysisException {
+    public static TSearchVector parseAndEncodeQueryVector(Field field, String 
json)
+            throws AnalysisException {
+        VectorEncodingSpec encodingSpec = analyzeVectorField(field);
+        JsonArray values = parseQueryVector(json, field, 
encodingSpec.dimension);
+        byte[] encodedValues = encodeQueryVectorValues(field, values, 
encodingSpec);
+
+        return new TSearchVector()
+                .setElementType(encodingSpec.elementType)
+                .setDimension(encodingSpec.dimension)
+                .setValues(encodedValues);
+    }
+
+    private static VectorEncodingSpec analyzeVectorField(Field field) throws 
AnalysisException {
         if (hasExtension(field) || field.getDictionary() != null
                 || field.getType().getTypeID() != 
ArrowType.ArrowTypeID.FixedSizeList
                 || field.getChildren().size() != 1) {
@@ -81,30 +93,36 @@ public final class LanceVectorQuery {
         if (hasExtension(elementField) || elementField.getDictionary() != 
null) {
             throw unsupportedVectorType(field);
         }
-        ElementEncoding encoding = elementEncoding(field, 
elementField.getType());
+        return determineVectorEncoding(field, elementField.getType(), 
dimension);
+    }
 
-        JsonArray values;
+    private static JsonArray parseQueryVector(String json, Field field, int 
dimension)
+            throws AnalysisException {
         try {
             JsonElement root = JsonParser.parseString(json);
             if (!root.isJsonArray()) {
                 throw new AnalysisException("'query_vector' must be a JSON 
array");
             }
-            values = root.getAsJsonArray();
+            JsonArray values = root.getAsJsonArray();
+            if (values.size() != dimension) {
+                throw new AnalysisException("Query vector dimension " + 
values.size()
+                        + " does not match Lance column '" + field.getName()
+                        + "' dimension " + dimension);
+            }
+            return values;
         } catch (AnalysisException e) {
             throw e;
         } catch (RuntimeException e) {
             throw new AnalysisException("Invalid 'query_vector' JSON: " + 
e.getMessage(), e);
         }
-        if (values.size() != dimension) {
-            throw new AnalysisException("Query vector dimension " + 
values.size()
-                    + " does not match Lance column '" + field.getName()
-                    + "' dimension " + dimension);
-        }
+    }
 
-        long encodedSize = (long) dimension * encoding.byteWidth;
+    private static byte[] encodeQueryVectorValues(Field field, JsonArray 
values,
+            VectorEncodingSpec encodingSpec) throws AnalysisException {
+        long encodedSize = (long) encodingSpec.dimension * 
encodingSpec.byteWidth;
         if (encodedSize > Integer.MAX_VALUE) {
             throw new AnalysisException("Lance vector column '" + 
field.getName()
-                    + "' is too large to encode: " + dimension + " elements");
+                    + "' is too large to encode: " + encodingSpec.dimension + 
" elements");
         }
         ByteBuffer buffer = ByteBuffer.allocate((int) 
encodedSize).order(ByteOrder.LITTLE_ENDIAN);
         for (int i = 0; i < values.size(); ++i) {
@@ -112,34 +130,35 @@ public final class LanceVectorQuery {
             if (!value.isJsonPrimitive() || 
!value.getAsJsonPrimitive().isNumber()) {
                 throw new AnalysisException("Query vector element " + i + " 
must be a number");
             }
-            encodeElement(value.getAsJsonPrimitive(), i, encoding.elementType, 
buffer);
+            writeQueryVectorElement(
+                    value.getAsJsonPrimitive(), i, encodingSpec.elementType, 
buffer);
         }
-        return new TSearchVector()
-                .setElementType(encoding.elementType)
-                .setDimension(dimension)
-                .setValues(buffer.array());
+        return buffer.array();
     }
 
-    private static ElementEncoding elementEncoding(Field vectorField, 
ArrowType elementType)
-            throws AnalysisException {
+    private static VectorEncodingSpec determineVectorEncoding(
+            Field vectorField, ArrowType elementType, int dimension) throws 
AnalysisException {
         switch (elementType.getTypeID()) {
             case FloatingPoint:
                 FloatingPointPrecision precision =
                         ((ArrowType.FloatingPoint) elementType).getPrecision();
                 switch (precision) {
                     case HALF:
-                        return new ElementEncoding(TVectorElementType.FLOAT16, 
Short.BYTES);
+                        return new VectorEncodingSpec(
+                                dimension, TVectorElementType.FLOAT16, 
Short.BYTES);
                     case SINGLE:
-                        return new ElementEncoding(TVectorElementType.FLOAT32, 
Float.BYTES);
+                        return new VectorEncodingSpec(
+                                dimension, TVectorElementType.FLOAT32, 
Float.BYTES);
                     case DOUBLE:
-                        return new ElementEncoding(TVectorElementType.FLOAT64, 
Double.BYTES);
+                        return new VectorEncodingSpec(
+                                dimension, TVectorElementType.FLOAT64, 
Double.BYTES);
                     default:
                         throw unsupportedVectorType(vectorField);
                 }
             case Int:
                 ArrowType.Int integer = (ArrowType.Int) elementType;
                 if (integer.getBitWidth() == Byte.SIZE) {
-                    return new ElementEncoding(integer.getIsSigned()
+                    return new VectorEncodingSpec(dimension, 
integer.getIsSigned()
                             ? TVectorElementType.INT8 : 
TVectorElementType.UINT8, Byte.BYTES);
                 }
                 throw unsupportedVectorType(vectorField);
@@ -148,7 +167,7 @@ public final class LanceVectorQuery {
         }
     }
 
-    private static void encodeElement(JsonPrimitive value, int index,
+    private static void writeQueryVectorElement(JsonPrimitive value, int index,
             TVectorElementType elementType, ByteBuffer buffer) throws 
AnalysisException {
         try {
             switch (elementType) {
@@ -226,11 +245,13 @@ public final class LanceVectorQuery {
                 + " is not representable as " + type.name().toLowerCase());
     }
 
-    private static class ElementEncoding {
+    private static class VectorEncodingSpec {
+        private final int dimension;
         private final TVectorElementType elementType;
         private final int byteWidth;
 
-        private ElementEncoding(TVectorElementType elementType, int byteWidth) 
{
+        private VectorEncodingSpec(int dimension, TVectorElementType 
elementType, int byteWidth) {
+            this.dimension = dimension;
             this.elementType = elementType;
             this.byteWidth = byteWidth;
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/AbstractLanceProperties.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/AbstractLanceProperties.java
new file mode 100644
index 00000000000..c82b74fa3b0
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/AbstractLanceProperties.java
@@ -0,0 +1,104 @@
+// 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.doris.datasource.property.metastore;
+
+import org.apache.doris.foundation.property.ConnectorProperty;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.commons.lang3.StringUtils;
+import org.lance.namespace.LanceNamespace;
+
+import java.util.Map;
+
+/** Common properties shared by Lance filesystem and REST namespace catalogs. 
*/
+public abstract class AbstractLanceProperties extends MetastoreProperties {
+    public static final String LANCE_CATALOG_TYPE = "lance.catalog.type";
+    public static final String LANCE_FILESYSTEM = "filesystem";
+    public static final String LANCE_REST = "rest";
+    public static final String NAMESPACE_PARENT = "lance.namespace.parent";
+    public static final String NAMESPACE_DELIMITER = 
"lance.namespace.delimiter";
+    public static final String ROOT_DATABASE = "lance.namespace.root_database";
+
+    public static final String DEFAULT_DELIMITER = "$";
+    public static final String DEFAULT_ROOT_DATABASE = "default";
+
+    @ConnectorProperty(
+            names = {NAMESPACE_PARENT},
+            required = false,
+            description = "The optional Lance namespace under which Doris 
databases are exposed.")
+    private String namespaceParent = "";
+
+    @ConnectorProperty(
+            names = {NAMESPACE_DELIMITER},
+            required = false,
+            description = "The delimiter used in an escaped parent namespace. 
Default: $."
+    )
+    private String namespaceDelimiter = DEFAULT_DELIMITER;
+
+    @ConnectorProperty(
+            names = {ROOT_DATABASE},
+            required = false,
+            description = "The Doris database name representing the catalog's 
root namespace. Default: default."
+    )
+    private String rootDatabase = DEFAULT_ROOT_DATABASE;
+
+    protected AbstractLanceProperties(Map<String, String> props) {
+        super(Type.LANCE, props);
+    }
+
+    @Override
+    public final void initNormalizeAndCheckProps() {
+        super.initNormalizeAndCheckProps();
+        // Preserve namespace whitespace and explicit empty values because 
both are meaningful
+        // to the escaping rules and must be validated rather than silently 
replaced by defaults.
+        namespaceParent = origProps.getOrDefault(NAMESPACE_PARENT, "");
+        namespaceDelimiter = origProps.getOrDefault(NAMESPACE_DELIMITER, 
DEFAULT_DELIMITER);
+        rootDatabase = origProps.getOrDefault(ROOT_DATABASE, 
DEFAULT_ROOT_DATABASE);
+        validateCommonProperties();
+        validateCatalogProperties();
+    }
+
+    public abstract String getLanceCatalogType();
+
+    public abstract LanceNamespace createNamespace(
+            BufferAllocator allocator, Map<String, String> javaStorageOptions);
+
+    protected abstract void validateCatalogProperties();
+
+    public String getNamespaceParent() {
+        return namespaceParent;
+    }
+
+    public String getNamespaceDelimiter() {
+        return namespaceDelimiter;
+    }
+
+    public String getRootDatabase() {
+        return rootDatabase;
+    }
+
+    private void validateCommonProperties() {
+        if (namespaceDelimiter.isEmpty() || namespaceDelimiter.indexOf('\\') 
>= 0) {
+            throw new IllegalArgumentException("Property '" + 
NAMESPACE_DELIMITER
+                    + "' cannot be empty or contain the escape character 
'\\'");
+        }
+        if (StringUtils.isBlank(rootDatabase)) {
+            throw new IllegalArgumentException("Property '" + ROOT_DATABASE + 
"' must be non-empty");
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LanceFileSystemMetastoreProperties.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LanceFileSystemMetastoreProperties.java
new file mode 100644
index 00000000000..c6518f07f7e
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LanceFileSystemMetastoreProperties.java
@@ -0,0 +1,104 @@
+// 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.doris.datasource.property.metastore;
+
+import org.apache.doris.foundation.property.ConnectorProperty;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.commons.lang3.StringUtils;
+import org.lance.namespace.LanceNamespace;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/** Properties for a Lance directory namespace backed by a filesystem 
warehouse. */
+public class LanceFileSystemMetastoreProperties extends 
AbstractLanceProperties {
+    public static final String WAREHOUSE = "warehouse";
+
+    @ConnectorProperty(
+            names = {WAREHOUSE},
+            required = false,
+            description = "The local, file, or S3 warehouse containing Lance 
datasets."
+    )
+    private String warehouse;
+
+    public LanceFileSystemMetastoreProperties(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public String getLanceCatalogType() {
+        return LANCE_FILESYSTEM;
+    }
+
+    @Override
+    public LanceNamespace createNamespace(
+            BufferAllocator allocator, Map<String, String> javaStorageOptions) 
{
+        Map<String, String> namespaceProperties = new HashMap<>();
+        namespaceProperties.put("root", warehouse);
+        javaStorageOptions.forEach(
+                (key, value) -> namespaceProperties.put("storage." + key, 
value));
+        return LanceNamespace.connect("dir", namespaceProperties, allocator);
+    }
+
+    public String getWarehouse() {
+        return warehouse;
+    }
+
+    @Override
+    protected void validateCatalogProperties() {
+        warehouse = origProps.get(WAREHOUSE);
+        if (StringUtils.isBlank(warehouse)) {
+            throw new IllegalArgumentException(
+                    "Missing required property 'warehouse' for Lance 
filesystem catalog");
+        }
+        validateWarehouse(warehouse);
+        for (String key : origProps.keySet()) {
+            if (key.startsWith("lance.rest.")) {
+                throw new IllegalArgumentException(
+                        "Property '" + key + "' is not valid for Lance 
filesystem catalog");
+            }
+        }
+    }
+
+    private static void validateWarehouse(String warehouse) {
+        final URI uri;
+        try {
+            uri = URI.create(warehouse);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException("Invalid Lance warehouse URI: " 
+ warehouse, e);
+        }
+        if (uri.getScheme() == null) {
+            Path path = Paths.get(warehouse);
+            if (!path.isAbsolute()) {
+                throw new IllegalArgumentException(
+                        "Local Lance warehouse must be an absolute path: " + 
warehouse);
+            }
+            return;
+        }
+        String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
+        if (!"file".equals(scheme) && !"s3".equals(scheme)) {
+            throw new IllegalArgumentException("Unsupported Lance filesystem 
warehouse scheme '" + scheme
+                    + "'; first phase supports local/file and s3");
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LancePropertiesFactory.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LancePropertiesFactory.java
new file mode 100644
index 00000000000..3a3669b83da
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LancePropertiesFactory.java
@@ -0,0 +1,45 @@
+// 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.doris.datasource.property.metastore;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/** Selects Lance filesystem or REST properties from {@code 
lance.catalog.type}. */
+public class LancePropertiesFactory extends AbstractMetastorePropertiesFactory 
{
+    public LancePropertiesFactory() {
+        register(AbstractLanceProperties.LANCE_FILESYSTEM, 
LanceFileSystemMetastoreProperties::new);
+        register(AbstractLanceProperties.LANCE_REST, 
LanceRestMetastoreProperties::new);
+    }
+
+    @Override
+    public MetastoreProperties create(Map<String, String> props) {
+        String type = 
props.getOrDefault(AbstractLanceProperties.LANCE_CATALOG_TYPE,
+                
AbstractLanceProperties.LANCE_FILESYSTEM).trim().toLowerCase(Locale.ROOT);
+        if (!AbstractLanceProperties.LANCE_FILESYSTEM.equals(type)
+                && !AbstractLanceProperties.LANCE_REST.equals(type)) {
+            throw new IllegalArgumentException("Property '" + 
AbstractLanceProperties.LANCE_CATALOG_TYPE
+                    + "' must be 'filesystem' or 'rest', but was '" + type + 
"'");
+        }
+        Map<String, String> normalizedProperties = new HashMap<>(props);
+        normalizedProperties.put(AbstractLanceProperties.LANCE_CATALOG_TYPE, 
type);
+        return createInternal(normalizedProperties, 
AbstractLanceProperties.LANCE_CATALOG_TYPE,
+                AbstractLanceProperties.LANCE_FILESYSTEM);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LanceRestMetastoreProperties.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LanceRestMetastoreProperties.java
new file mode 100644
index 00000000000..e989b0e6f21
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/LanceRestMetastoreProperties.java
@@ -0,0 +1,229 @@
+// 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.doris.datasource.property.metastore;
+
+import org.apache.doris.foundation.property.ConnectorProperty;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.commons.lang3.StringUtils;
+import org.lance.namespace.LanceNamespace;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/** Properties for a Lance REST namespace catalog. */
+public class LanceRestMetastoreProperties extends AbstractLanceProperties {
+    public static final String REST_URI = "lance.rest.uri";
+    public static final String REST_SECURITY_TYPE = "lance.rest.security.type";
+    public static final String REST_BEARER_TOKEN = "lance.rest.bearer-token";
+    public static final String REST_API_KEY = "lance.rest.api-key";
+    public static final String REST_HEADER_PREFIX = "lance.rest.header.";
+
+    private static final String REST_SECURITY_NONE = "none";
+    private static final String REST_SECURITY_BEARER = "bearer";
+    private static final String REST_SECURITY_API_KEY = "api_key";
+    private static final Pattern HTTP_HEADER_NAME =
+            Pattern.compile("^[!#$%&'*+.^_`|~0-9A-Za-z-]+$");
+
+    @ConnectorProperty(
+            names = {REST_URI},
+            required = false,
+            description = "The HTTP or HTTPS endpoint of the Lance REST 
namespace service."
+    )
+    private String restUri;
+
+    @ConnectorProperty(
+            names = {REST_SECURITY_TYPE},
+            required = false,
+            description = "REST authentication type: none, bearer, or api_key. 
Default: none."
+    )
+    private String securityType = REST_SECURITY_NONE;
+
+    @ConnectorProperty(
+            names = {REST_BEARER_TOKEN},
+            required = false,
+            sensitive = true,
+            description = "Bearer token used when lance.rest.security.type is 
bearer."
+    )
+    private String bearerToken;
+
+    @ConnectorProperty(
+            names = {REST_API_KEY},
+            required = false,
+            sensitive = true,
+            description = "API key used when lance.rest.security.type is 
api_key."
+    )
+    private String apiKey;
+
+    public LanceRestMetastoreProperties(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public String getLanceCatalogType() {
+        return LANCE_REST;
+    }
+
+    @Override
+    public LanceNamespace createNamespace(
+            BufferAllocator allocator, Map<String, String> javaStorageOptions) 
{
+        Map<String, String> namespaceProperties = new HashMap<>();
+        namespaceProperties.put("uri", normalizedRestUri());
+        namespaceProperties.put("delimiter", getNamespaceDelimiter());
+
+        origProps.forEach((key, value) -> {
+            if (key.startsWith(REST_HEADER_PREFIX)) {
+                namespaceProperties.put(
+                        "header." + 
key.substring(REST_HEADER_PREFIX.length()), value);
+            }
+        });
+        if (REST_SECURITY_BEARER.equals(securityType)) {
+            namespaceProperties.put("header.Authorization", "Bearer " + 
bearerToken);
+        } else if (REST_SECURITY_API_KEY.equals(securityType)) {
+            namespaceProperties.put("header.x-api-key", apiKey);
+        }
+        return LanceNamespace.connect("rest", namespaceProperties, allocator);
+    }
+
+    public String getRestUri() {
+        return normalizedRestUri();
+    }
+
+    public String getSecurityType() {
+        return securityType;
+    }
+
+    @Override
+    protected void validateCatalogProperties() {
+        restUri = origProps.get(REST_URI);
+        securityType = origProps.getOrDefault(REST_SECURITY_TYPE, 
REST_SECURITY_NONE);
+        bearerToken = origProps.get(REST_BEARER_TOKEN);
+        apiKey = origProps.get(REST_API_KEY);
+        if 
(origProps.containsKey(LanceFileSystemMetastoreProperties.WAREHOUSE)) {
+            throw new IllegalArgumentException(
+                    "Property 'warehouse' is not valid for Lance REST 
catalog");
+        }
+        if (StringUtils.isBlank(restUri)) {
+            throw new IllegalArgumentException(
+                    "Missing required property '" + REST_URI + "' for Lance 
REST catalog");
+        }
+        validateRestUri(restUri);
+
+        securityType = securityType.trim().toLowerCase(Locale.ROOT);
+        boolean bearerTokenConfigured = 
origProps.containsKey(REST_BEARER_TOKEN);
+        boolean apiKeyConfigured = origProps.containsKey(REST_API_KEY);
+        boolean hasBearerToken = StringUtils.isNotBlank(bearerToken);
+        boolean hasApiKey = StringUtils.isNotBlank(apiKey);
+        switch (securityType) {
+            case REST_SECURITY_NONE:
+                if (bearerTokenConfigured || apiKeyConfigured) {
+                    throw new IllegalArgumentException("Lance REST security 
type 'none' cannot configure '"
+                            + REST_BEARER_TOKEN + "' or '" + REST_API_KEY + 
"'");
+                }
+                break;
+            case REST_SECURITY_BEARER:
+                if (!hasBearerToken || apiKeyConfigured) {
+                    throw new IllegalArgumentException("Lance REST security 
type 'bearer' requires only '"
+                            + REST_BEARER_TOKEN + "'");
+                }
+                validateCredentialHeaderValue(REST_BEARER_TOKEN, bearerToken);
+                break;
+            case REST_SECURITY_API_KEY:
+                if (!hasApiKey || bearerTokenConfigured) {
+                    throw new IllegalArgumentException("Lance REST security 
type 'api_key' requires only '"
+                            + REST_API_KEY + "'");
+                }
+                validateCredentialHeaderValue(REST_API_KEY, apiKey);
+                break;
+            default:
+                throw new IllegalArgumentException("Property '" + 
REST_SECURITY_TYPE
+                        + "' must be 'none', 'bearer', or 'api_key'");
+        }
+
+        Set<String> supportedKeys = new HashSet<>(Arrays.asList(
+                REST_URI, REST_SECURITY_TYPE, REST_BEARER_TOKEN, 
REST_API_KEY));
+        for (Map.Entry<String, String> entry : origProps.entrySet()) {
+            String key = entry.getKey();
+            if (!key.startsWith("lance.rest.") || supportedKeys.contains(key)) 
{
+                continue;
+            }
+            if (!key.startsWith(REST_HEADER_PREFIX)) {
+                throw new IllegalArgumentException("Unsupported Lance REST 
property '" + key + "'");
+            }
+            validateRestHeader(key.substring(REST_HEADER_PREFIX.length()), 
entry.getValue());
+        }
+    }
+
+    private static void validateRestHeader(String headerName, String 
headerValue) {
+        if (StringUtils.isBlank(headerName) || 
!HTTP_HEADER_NAME.matcher(headerName).matches()) {
+            throw new IllegalArgumentException("Invalid HTTP header name in 
property '"
+                    + REST_HEADER_PREFIX + headerName + "'");
+        }
+        if ("authorization".equalsIgnoreCase(headerName)
+                || "x-api-key".equalsIgnoreCase(headerName)) {
+            throw new IllegalArgumentException("Authentication header '" + 
headerName
+                    + "' must be configured through '" + REST_SECURITY_TYPE + 
"'");
+        }
+        if (headerValue == null || headerValue.indexOf('\r') >= 0 || 
headerValue.indexOf('\n') >= 0) {
+            throw new IllegalArgumentException("Invalid HTTP header value in 
property '"
+                    + REST_HEADER_PREFIX + headerName + "'");
+        }
+    }
+
+    private static void validateCredentialHeaderValue(String propertyName, 
String value) {
+        if (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0) {
+            throw new IllegalArgumentException(
+                    "Invalid HTTP credential value in property '" + 
propertyName + "'");
+        }
+    }
+
+    private static void validateRestUri(String value) {
+        final URI uri;
+        try {
+            uri = URI.create(value.trim());
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(
+                    "Invalid Lance REST URI in property '" + REST_URI + "'", 
e);
+        }
+        String scheme = uri.getScheme();
+        if (scheme == null
+                || (!("http".equalsIgnoreCase(scheme)) && 
!("https".equalsIgnoreCase(scheme)))) {
+            throw new IllegalArgumentException(
+                    "Property '" + REST_URI + "' must use http or https");
+        }
+        if (StringUtils.isBlank(uri.getRawAuthority()) || uri.getRawUserInfo() 
!= null
+                || uri.getRawQuery() != null || uri.getRawFragment() != null) {
+            throw new IllegalArgumentException("Property '" + REST_URI
+                    + "' must contain an authority and cannot contain 
user-info, query, or fragment");
+        }
+    }
+
+    private String normalizedRestUri() {
+        String uri = restUri.trim();
+        while (uri.endsWith("/")) {
+            uri = uri.substring(0, uri.length() - 1);
+        }
+        return uri;
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/MetastoreProperties.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/MetastoreProperties.java
index 0c9b030a20f..9d4b02fa8f9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/MetastoreProperties.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/MetastoreProperties.java
@@ -88,7 +88,7 @@ public class MetastoreProperties extends ConnectionProperties 
{
         register(Type.ICEBERG, new IcebergPropertiesFactory());
         register(Type.PAIMON, new PaimonPropertiesFactory());
         register(Type.TRINO_CONNECTOR, new TrinoConnectorPropertiesFactory());
-        register(Type.LANCE, props -> new MetastoreProperties(Type.LANCE, 
props));
+        register(Type.LANCE, new LancePropertiesFactory());
     }
 
     public static void register(Type type, MetastorePropertiesFactory factory) 
{
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/S3TableValuedFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/S3TableValuedFunction.java
index d461e82f1d3..61be8f0af22 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/S3TableValuedFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/S3TableValuedFunction.java
@@ -65,7 +65,7 @@ public class S3TableValuedFunction extends 
ExternalFileTableValuedFunction {
         if (isLanceFormat()) {
             try {
                 LanceTableMetadata metadata =
-                        LanceMetadataLoader.load(filePath, 
backendConnectProperties);
+                        LanceMetadataLoader.loadLatestForTvf(filePath, 
backendConnectProperties);
                 setLanceTableMetadata(metadata);
             } catch (Exception e) {
                 throw new AnalysisException(
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/VectorSearchTableValuedFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/VectorSearchTableValuedFunction.java
index 1829d91e329..50ffc053d4c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/VectorSearchTableValuedFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/VectorSearchTableValuedFunction.java
@@ -96,8 +96,7 @@ public class VectorSearchTableValuedFunction extends 
TableValuedFunctionIf {
             throws AnalysisException {
         Map<String, String> params = normalizeProperties(properties);
         sourceTableName = parseTableName(required(params, TABLE));
-        checkSelectPrivilege(sourceTableName);
-        sourceTable = resolveLanceTable(sourceTableName);
+        sourceTable = findLanceExternalTable(sourceTableName);
         try {
             metadata = sourceTable.loadMetadata();
         } catch (RuntimeException e) {
@@ -108,9 +107,9 @@ public class VectorSearchTableValuedFunction extends 
TableValuedFunctionIf {
             throw new AnalysisException("Lance vector search requires a fixed 
positive dataset version");
         }
 
-        Field vectorField = LanceVectorQuery.resolveVectorField(
+        Field vectorField = LanceVectorQuery.findVectorColumnField(
                 metadata.getSchema(), required(params, COLUMN));
-        TSearchVector queryVector = LanceVectorQuery.encode(
+        TSearchVector queryVector = LanceVectorQuery.parseAndEncodeQueryVector(
                 vectorField, required(params, QUERY_VECTOR));
         long topK = parseLong(params.getOrDefault(TOP_K, "10"), TOP_K, 1, 
Long.MAX_VALUE);
         long offset = parseLong(params.getOrDefault(OFFSET, "0"), OFFSET, 0, 
Long.MAX_VALUE);
@@ -236,7 +235,8 @@ public class VectorSearchTableValuedFunction extends 
TableValuedFunctionIf {
         return new TableName(names.get(0), names.get(1), names.get(2));
     }
 
-    private static void checkSelectPrivilege(TableName tableName) throws 
AnalysisException {
+    private static LanceExternalTable findLanceExternalTable(TableName 
tableName)
+            throws AnalysisException {
         ConnectContext context = ConnectContext.get();
         if (!Env.getCurrentEnv().getAccessManager()
                 .checkTblPriv(context, tableName, PrivPredicate.SELECT)) {
@@ -244,10 +244,6 @@ public class VectorSearchTableValuedFunction extends 
TableValuedFunctionIf {
                     context.getQualifiedUser(), context.getRemoteIP(),
                     tableName.getDb() + ": " + tableName.getTbl());
         }
-    }
-
-    private static LanceExternalTable resolveLanceTable(TableName tableName)
-            throws AnalysisException {
         CatalogIf<?> catalog = 
Env.getCurrentEnv().getCatalogMgr().getCatalog(tableName.getCtl());
         if (!(catalog instanceof LanceExternalCatalog)) {
             throw new AnalysisException("Catalog '" + tableName.getCtl()
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceFilesystemCatalogTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceFilesystemCatalogTest.java
index d169a1d303a..6b4a9f9c2bc 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceFilesystemCatalogTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceFilesystemCatalogTest.java
@@ -46,34 +46,48 @@ public class LanceFilesystemCatalogTest {
 
     @Test
     public void testNamespaceNameRoundTrip() throws Exception {
-        Assert.assertEquals(Collections.emptyList(), LanceNamespaceName.decode(
-                LanceNamespaceName.encode(Collections.emptyList(), ".", 
"default"), ".", "default"));
+        Assert.assertEquals(Collections.emptyList(), 
LanceNamespaceName.dorisDatabaseNameToNamespace(
+                LanceNamespaceName.namespaceToDorisDatabaseName(
+                        Collections.emptyList(), ".", "default"),
+                ".", "default"));
         Assert.assertEquals("doris",
-                LanceNamespaceName.encode(Collections.singletonList("doris"), 
".", "default"));
+                LanceNamespaceName.namespaceToDorisDatabaseName(
+                        Collections.singletonList("doris"), ".", "default"));
         Assert.assertEquals("company.analytics",
-                LanceNamespaceName.encode(java.util.Arrays.asList("company", 
"analytics"), ".", "default"));
-        Assert.assertEquals(java.util.Arrays.asList("company", "analytics"), 
LanceNamespaceName.decode(
-                LanceNamespaceName.encode(java.util.Arrays.asList("company", 
"analytics"), ".", "default"),
-                ".", "default"));
-        Assert.assertEquals(java.util.Arrays.asList("a.b", "c"), 
LanceNamespaceName.decode(
-                LanceNamespaceName.encode(java.util.Arrays.asList("a.b", "c"), 
".", "default"),
-                ".", "default"));
+                LanceNamespaceName.namespaceToDorisDatabaseName(
+                        java.util.Arrays.asList("company", "analytics"), ".", 
"default"));
+        Assert.assertEquals(java.util.Arrays.asList("company", "analytics"),
+                LanceNamespaceName.dorisDatabaseNameToNamespace(
+                        LanceNamespaceName.namespaceToDorisDatabaseName(
+                                java.util.Arrays.asList("company", 
"analytics"), ".", "default"),
+                        ".", "default"));
+        Assert.assertEquals(java.util.Arrays.asList("a.b", "c"),
+                LanceNamespaceName.dorisDatabaseNameToNamespace(
+                        LanceNamespaceName.namespaceToDorisDatabaseName(
+                                java.util.Arrays.asList("a.b", "c"), ".", 
"default"),
+                        ".", "default"));
 
         java.util.List<String> delimiterAtEnd = java.util.Arrays.asList("a.", 
"b");
         java.util.List<String> delimiterAtStart = java.util.Arrays.asList("a", 
".b");
-        String encodedAtEnd = LanceNamespaceName.encode(delimiterAtEnd, ".", 
"default");
-        String encodedAtStart = LanceNamespaceName.encode(delimiterAtStart, 
".", "default");
+        String encodedAtEnd =
+                
LanceNamespaceName.namespaceToDorisDatabaseName(delimiterAtEnd, ".", "default");
+        String encodedAtStart =
+                
LanceNamespaceName.namespaceToDorisDatabaseName(delimiterAtStart, ".", 
"default");
         Assert.assertNotEquals(encodedAtEnd, encodedAtStart);
-        Assert.assertEquals(delimiterAtEnd, 
LanceNamespaceName.decode(encodedAtEnd, ".", "default"));
-        Assert.assertEquals(delimiterAtStart, 
LanceNamespaceName.decode(encodedAtStart, ".", "default"));
-        Assert.assertEquals(java.util.Arrays.asList("a\\b", "c"), 
LanceNamespaceName.decode(
-                LanceNamespaceName.encode(java.util.Arrays.asList("a\\b", 
"c"), ".", "default"),
-                ".", "default"));
+        Assert.assertEquals(delimiterAtEnd,
+                LanceNamespaceName.dorisDatabaseNameToNamespace(encodedAtEnd, 
".", "default"));
+        Assert.assertEquals(delimiterAtStart,
+                
LanceNamespaceName.dorisDatabaseNameToNamespace(encodedAtStart, ".", 
"default"));
+        Assert.assertEquals(java.util.Arrays.asList("a\\b", "c"),
+                LanceNamespaceName.dorisDatabaseNameToNamespace(
+                        LanceNamespaceName.namespaceToDorisDatabaseName(
+                                java.util.Arrays.asList("a\\b", "c"), ".", 
"default"),
+                        ".", "default"));
 
-        String rootCollision = LanceNamespaceName.encode(
+        String rootCollision = LanceNamespaceName.namespaceToDorisDatabaseName(
                 Collections.singletonList("default"), ".", "default");
         Assert.assertEquals("\\default", rootCollision);
         Assert.assertEquals(Collections.singletonList("default"),
-                LanceNamespaceName.decode(rootCollision, ".", "default"));
+                LanceNamespaceName.dorisDatabaseNameToNamespace(rootCollision, 
".", "default"));
     }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceRestCatalogTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceRestCatalogTest.java
index 510153b5b97..46ece3f2ebd 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceRestCatalogTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceRestCatalogTest.java
@@ -41,6 +41,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public class LanceRestCatalogTest {
     private static final String BEARER_TOKEN = "test-bearer-token";
+    private static final String EXISTING_TABLE = "existing_table";
+    private static final String MISSING_TABLE = "missing_table";
+    private static final String MISSING_NAMESPACE = "missing_namespace";
 
     private final List<RequestRecord> requests = new CopyOnWriteArrayList<>();
     private HttpServer server;
@@ -156,6 +159,25 @@ public class LanceRestCatalogTest {
         assertInvalid(filesystemWithRest, "not valid for Lance filesystem 
catalog");
     }
 
+    @Test
+    public void testTableExist() {
+        LanceExternalCatalog catalog = new LanceExternalCatalog(
+                201, "lance_rest_table_exists", null, restProperties(), "");
+        try {
+            Assertions.assertTrue(catalog.tableExist(null, "default", 
EXISTING_TABLE));
+            Assertions.assertFalse(catalog.tableExist(null, "default", 
MISSING_TABLE));
+            Assertions.assertFalse(catalog.tableExist(null, MISSING_NAMESPACE, 
MISSING_TABLE));
+        } finally {
+            catalog.onClose();
+        }
+    }
+
+    @Test
+    public void testTableExistDoesNotHideServiceErrors() {
+        assertTableExistThrows(202, "lance_rest_table_exists_unauthorized", 
"fail/");
+        assertTableExistThrows(203, "lance_rest_table_exists_unavailable", 
"service-error/");
+    }
+
     private Map<String, String> restProperties() {
         Map<String, String> properties = restPropertiesWithoutUri();
         properties.put(LanceExternalCatalog.REST_URI, restUri);
@@ -177,6 +199,20 @@ public class LanceRestCatalogTest {
         
Assertions.assertTrue(exception.getMessage().contains(expectedMessage), 
exception.getMessage());
     }
 
+    private void assertTableExistThrows(long catalogId, String catalogName, 
String endpointPrefix) {
+        Map<String, String> properties = restProperties();
+        properties.put(LanceExternalCatalog.REST_URI, restUri + 
endpointPrefix);
+        LanceExternalCatalog catalog = new LanceExternalCatalog(
+                catalogId, catalogName, null, properties, "");
+        try {
+            Assertions.assertThrows(
+                    RuntimeException.class,
+                    () -> catalog.tableExist(null, "default", EXISTING_TABLE));
+        } finally {
+            catalog.onClose();
+        }
+    }
+
     private void handleRequest(HttpExchange exchange) throws IOException {
         ByteStreams.toByteArray(exchange.getRequestBody());
         String path = exchange.getRequestURI().getPath();
@@ -189,6 +225,21 @@ public class LanceRestCatalogTest {
         if (path.startsWith("/fail/")) {
             status = 401;
             response = "{\"error\":\"invalid token " + BEARER_TOKEN + 
"\",\"code\":16}";
+        } else if (path.startsWith("/service-error/")) {
+            status = 503;
+            response = "{\"error\":\"service unavailable\",\"code\":17}";
+        } else if ("POST".equals(exchange.getRequestMethod())
+                && path.matches("/v1/table/[^/]+/exists")) {
+            String tableId = path.substring("/v1/table/".length(), 
path.length() - "/exists".length());
+            if (EXISTING_TABLE.equals(tableId)) {
+                exchange.sendResponseHeaders(200, -1);
+                exchange.close();
+                return;
+            }
+            status = 404;
+            response = tableId.startsWith(MISSING_NAMESPACE + "$")
+                    ? "{\"error\":\"namespace not found\",\"code\":1}"
+                    : "{\"error\":\"table not found\",\"code\":4}";
         } else if (path.endsWith("/table/list")) {
             response = "{\"tables\":[]}";
         } else if (path.endsWith("/list")) {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceVectorQueryTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceVectorQueryTest.java
index 0a9f1821b0d..1ccd5fa7db6 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceVectorQueryTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LanceVectorQueryTest.java
@@ -41,7 +41,8 @@ public class LanceVectorQueryTest {
     public void testEncodesFloat32AsLittleEndianTypedVector() throws Exception 
{
         Field field = vectorField("embedding",
                 new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE), 3);
-        TSearchVector vector = LanceVectorQuery.encode(field, "[0.25, -1.5, 
3]");
+        TSearchVector vector =
+                LanceVectorQuery.parseAndEncodeQueryVector(field, "[0.25, 
-1.5, 3]");
 
         Assertions.assertEquals(TVectorElementType.FLOAT32, 
vector.getElementType());
         Assertions.assertEquals(3, vector.getDimension());
@@ -54,24 +55,24 @@ public class LanceVectorQueryTest {
 
     @Test
     public void testEncodesAllLanceCElementTypes() throws Exception {
-        TSearchVector float16 = LanceVectorQuery.encode(vectorField("f16",
+        TSearchVector float16 = 
LanceVectorQuery.parseAndEncodeQueryVector(vectorField("f16",
                 new ArrowType.FloatingPoint(FloatingPointPrecision.HALF), 1), 
"[1.5]");
         Assertions.assertEquals(TVectorElementType.FLOAT16, 
float16.getElementType());
         Assertions.assertEquals(1.5F, Float16.toFloat(
                 
float16.bufferForValues().order(ByteOrder.LITTLE_ENDIAN).getShort()));
 
-        TSearchVector float64 = LanceVectorQuery.encode(vectorField("f64",
+        TSearchVector float64 = 
LanceVectorQuery.parseAndEncodeQueryVector(vectorField("f64",
                 new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE), 
1), "[1.25]");
         Assertions.assertEquals(TVectorElementType.FLOAT64, 
float64.getElementType());
         Assertions.assertEquals(1.25,
                 
float64.bufferForValues().order(ByteOrder.LITTLE_ENDIAN).getDouble());
 
-        TSearchVector uint8 = LanceVectorQuery.encode(
+        TSearchVector uint8 = LanceVectorQuery.parseAndEncodeQueryVector(
                 vectorField("u8", new ArrowType.Int(8, false), 2), "[0, 255]");
         Assertions.assertEquals(TVectorElementType.UINT8, 
uint8.getElementType());
         Assertions.assertArrayEquals(new byte[] {0, (byte) 255}, 
uint8.getValues());
 
-        TSearchVector int8 = LanceVectorQuery.encode(
+        TSearchVector int8 = LanceVectorQuery.parseAndEncodeQueryVector(
                 vectorField("i8", new ArrowType.Int(8, true), 2), "[-128, 
127]");
         Assertions.assertEquals(TVectorElementType.INT8, 
int8.getElementType());
         Assertions.assertArrayEquals(new byte[] {(byte) -128, 127}, 
int8.getValues());
@@ -82,12 +83,14 @@ public class LanceVectorQueryTest {
         Field float32 = vectorField("embedding",
                 new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE), 3);
         AnalysisException dimension = Assertions.assertThrows(
-                AnalysisException.class, () -> 
LanceVectorQuery.encode(float32, "[1, 2]"));
+                AnalysisException.class,
+                () -> LanceVectorQuery.parseAndEncodeQueryVector(float32, "[1, 
2]"));
         Assertions.assertTrue(dimension.getMessage().contains("dimension"));
 
         Field uint8 = vectorField("embedding", new ArrowType.Int(8, false), 1);
         AnalysisException range = Assertions.assertThrows(
-                AnalysisException.class, () -> LanceVectorQuery.encode(uint8, 
"[256]"));
+                AnalysisException.class,
+                () -> LanceVectorQuery.parseAndEncodeQueryVector(uint8, 
"[256]"));
         Assertions.assertTrue(range.getMessage().contains("uint8"));
     }
 
@@ -97,7 +100,8 @@ public class LanceVectorQueryTest {
                 Collections.singletonList(Field.nullable("item",
                         new 
ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE))));
         Assertions.assertThrows(
-                AnalysisException.class, () -> LanceVectorQuery.encode(list, 
"[1]"));
+                AnalysisException.class,
+                () -> LanceVectorQuery.parseAndEncodeQueryVector(list, "[1]"));
 
         Field bfloat16Item = new Field("item",
                 new FieldType(true, new ArrowType.FixedSizeBinary(2), null,
@@ -107,7 +111,8 @@ public class LanceVectorQueryTest {
                 FieldType.nullable(new ArrowType.FixedSizeList(1)),
                 Collections.singletonList(bfloat16Item));
         Assertions.assertThrows(
-                AnalysisException.class, () -> 
LanceVectorQuery.encode(bfloat16Vector, "[1]"));
+                AnalysisException.class,
+                () -> 
LanceVectorQuery.parseAndEncodeQueryVector(bfloat16Vector, "[1]"));
     }
 
     @Test
@@ -120,7 +125,7 @@ public class LanceVectorQueryTest {
 
         AnalysisException ambiguous = Assertions.assertThrows(
                 AnalysisException.class,
-                () -> LanceVectorQuery.resolveVectorField(schema, 
"embedding"));
+                () -> LanceVectorQuery.findVectorColumnField(schema, 
"embedding"));
         Assertions.assertTrue(ambiguous.getMessage().contains("ambiguous"));
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/property/metastore/LancePropertiesTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/property/metastore/LancePropertiesTest.java
new file mode 100644
index 00000000000..1702d583f4f
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/property/metastore/LancePropertiesTest.java
@@ -0,0 +1,63 @@
+// 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.doris.datasource.property.metastore;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class LancePropertiesTest {
+    @Test
+    public void testDefaultFilesystemProperties() throws Exception {
+        Map<String, String> properties = new HashMap<>();
+        properties.put("type", "lance");
+        properties.put(LanceFileSystemMetastoreProperties.WAREHOUSE, 
"/tmp/lance");
+
+        AbstractLanceProperties lanceProperties =
+                (AbstractLanceProperties) 
MetastoreProperties.create(properties);
+
+        Assertions.assertInstanceOf(LanceFileSystemMetastoreProperties.class, 
lanceProperties);
+        Assertions.assertEquals(AbstractLanceProperties.LANCE_FILESYSTEM,
+                lanceProperties.getLanceCatalogType());
+        Assertions.assertEquals(AbstractLanceProperties.DEFAULT_DELIMITER,
+                lanceProperties.getNamespaceDelimiter());
+        Assertions.assertEquals(AbstractLanceProperties.DEFAULT_ROOT_DATABASE,
+                lanceProperties.getRootDatabase());
+    }
+
+    @Test
+    public void testRestProperties() throws Exception {
+        Map<String, String> properties = new HashMap<>();
+        properties.put("type", "lance");
+        properties.put(AbstractLanceProperties.LANCE_CATALOG_TYPE,
+                AbstractLanceProperties.LANCE_REST);
+        properties.put(LanceRestMetastoreProperties.REST_URI, 
"http://localhost:8080/";);
+        properties.put(LanceRestMetastoreProperties.REST_SECURITY_TYPE, 
"bearer");
+        properties.put(LanceRestMetastoreProperties.REST_BEARER_TOKEN, 
"token");
+
+        AbstractLanceProperties lanceProperties =
+                (AbstractLanceProperties) 
MetastoreProperties.create(properties);
+
+        Assertions.assertInstanceOf(LanceRestMetastoreProperties.class, 
lanceProperties);
+        LanceRestMetastoreProperties restProperties = 
(LanceRestMetastoreProperties) lanceProperties;
+        Assertions.assertEquals("http://localhost:8080";, 
restProperties.getRestUri());
+        Assertions.assertEquals("bearer", restProperties.getSecurityType());
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to