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

yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new eca1b1a447 [#13189] fix(lance): reject empty storage option keys 
(#13190)
eca1b1a447 is described below

commit eca1b1a4474f53778fd6458be606f603d965a453
Author: StormSpirit <[email protected]>
AuthorDate: Thu Sep 17 21:03:30 2026 +0800

    [#13189] fix(lance): reject empty storage option keys (#13190)
    
    ### What changes were proposed in this pull request?
    
    - Reject a table property whose key is exactly `lance.storage.` before
    prefix removal can produce an empty Lance storage-option key.
    - Add regression coverage for the exact-prefix case, normal options,
    arbitrary non-empty provider-specific keys, insertion ordering, and
    catalog-to-table precedence.
    - Keep the change limited to
    `LancePropertiesUtils.getLanceStorageOptions()` and its unit tests;
    reverse conversion and operation call sites are unchanged.
    
    ### Why are the changes needed?
    
    `lance.storage.` currently passes Gravitino's prefix-property validation
    and is converted into the empty Lance option key `""`. A direct
    Docker-based experiment with Lance 6.0.0 and MinIO confirmed that Lance
    can retain this malformed key, so the invalid input can cross the
    Gravitino-to-Lance boundary without an actionable error at the source.
    
    This PR intentionally fixes the extraction boundary only. On current
    `main`, ordinary create can pass the malformed option to dataset
    creation, declare can persist metadata while response-side resolution
    still accepts it, and register can persist the property and return
    success without resolving storage options. After this guard, ordinary
    create fails before dataset creation or metadata persistence; declare
    may persist metadata before response-side resolution returns an error,
    while register still bypasses that resolution. Pre-persistence
    validation for `toTableProperties()` and the relevant catalog/table
    input paths is tracked as a separate follow-up.
    
    Fix: #13189
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. An exact `lance.storage.` property key now raises an
    input-validation error instead of producing an empty Lance
    storage-option key. Existing non-empty options, including
    provider-specific keys, retain their current behavior; no public API or
    property precedence is changed. This guard is evaluated when storage
    options are resolved, not when a property is initially set. Existing
    malformed metadata may therefore cause describe and Lance alter
    operations to fail, while a non-external drop may delete metadata before
    storage cleanup fails, leaving an orphaned dataset.
    
    ### How was this patch tested?
    
    - `./gradlew :lance:lance-common:test --tests
    'org.apache.gravitino.lance.common.utils.TestLancePropertiesUtils'
    -PskipITs` — 3 tests passed with no skips, failures, or errors.
    - `./gradlew :lance:lance-common:spotlessCheck` — passed.
    - `./gradlew rat` — passed.
    - `git diff --check` — passed.
    - A separate Docker-based Lance 6.0.0 probe with MinIO confirmed the
    malformed empty key is retained by Lance; this is supporting evidence,
    not an additional Gravitino CI gate.
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../lance/common/utils/LancePropertiesUtils.java   | 11 ++++++++-
 .../common/utils/TestLancePropertiesUtils.java     | 26 +++++++++++++++++++++-
 2 files changed, 35 insertions(+), 2 deletions(-)

diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
index 39fbe93bf4..4d235afcf8 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.lance.common.utils;
 
 import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
 
+import com.google.common.base.Preconditions;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -37,6 +38,7 @@ public final class LancePropertiesUtils {
    *
    * @param tableProperties the source properties
    * @return the Lance storage options without the `lance.storage.` prefix
+   * @throws IllegalArgumentException if a property key is exactly 
`lance.storage.`
    */
   public static Map<String, String> getLanceStorageOptions(Map<String, String> 
tableProperties) {
     if (tableProperties == null || tableProperties.isEmpty()) {
@@ -47,7 +49,14 @@ public final class LancePropertiesUtils {
         .filter(entry -> 
entry.getKey().startsWith(LANCE_STORAGE_OPTIONS_PREFIX))
         .collect(
             Collectors.toMap(
-                entry -> 
entry.getKey().substring(LANCE_STORAGE_OPTIONS_PREFIX.length()),
+                entry -> {
+                  String propertyKey = entry.getKey();
+                  Preconditions.checkArgument(
+                      !LANCE_STORAGE_OPTIONS_PREFIX.equals(propertyKey),
+                      "Lance storage option key cannot be empty: %s",
+                      propertyKey);
+                  return 
propertyKey.substring(LANCE_STORAGE_OPTIONS_PREFIX.length());
+                },
                 Map.Entry::getValue,
                 (left, right) -> right,
                 LinkedHashMap::new));
diff --git 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
index 55bfa6c347..51de5b37e1 100644
--- 
a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
+++ 
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.java
@@ -19,6 +19,8 @@
 package org.apache.gravitino.lance.common.utils;
 
 import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -31,14 +33,34 @@ public class TestLancePropertiesUtils {
         ImmutableMap.of(
             "lance.storage.endpoint", "http://minio:9000";,
             "lance.storage.access_key_id", "ak",
+            "lance.storage.s3.custom_option", "custom-value",
             "not.storage.key", "ignored");
 
     Map<String, String> storageOptions = 
LancePropertiesUtils.getLanceStorageOptions(properties);
 
-    Assertions.assertEquals(2, storageOptions.size());
+    Assertions.assertEquals(3, storageOptions.size());
     Assertions.assertEquals("http://minio:9000";, 
storageOptions.get("endpoint"));
     Assertions.assertEquals("ak", storageOptions.get("access_key_id"));
+    Assertions.assertEquals("custom-value", 
storageOptions.get("s3.custom_option"));
     Assertions.assertFalse(storageOptions.containsKey("not.storage.key"));
+    Assertions.assertEquals(
+        List.of("endpoint", "access_key_id", "s3.custom_option"),
+        new ArrayList<>(storageOptions.keySet()));
+  }
+
+  /** Verifies that the storage prefix itself cannot become an empty provider 
option key. */
+  @Test
+  public void testGetLanceStorageOptionsRejectsEmptyOptionKey() {
+    String propertyValue = "secret-value-must-not-leak";
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                LancePropertiesUtils.getLanceStorageOptions(
+                    Map.of("lance.storage.", propertyValue)));
+
+    Assertions.assertTrue(exception.getMessage().contains("lance.storage."));
+    Assertions.assertFalse(exception.getMessage().contains(propertyValue));
   }
 
   @Test
@@ -59,5 +81,7 @@ public class TestLancePropertiesUtils {
     Assertions.assertEquals("http://table:9000";, 
storageOptions.get("endpoint"));
     Assertions.assertEquals("us-east-1", storageOptions.get("region"));
     Assertions.assertEquals("table-ak", storageOptions.get("access_key_id"));
+    Assertions.assertEquals(
+        List.of("endpoint", "region", "access_key_id"), new 
ArrayList<>(storageOptions.keySet()));
   }
 }

Reply via email to