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 fb902cccf8 [#11063] fix(lance): manage lance storage config in
Gravitino (#11070)
fb902cccf8 is described below
commit fb902cccf8a95d5839be673ab05e5b55aa63fa64
Author: FANNG <[email protected]>
AuthorDate: Fri May 15 11:48:13 2026 +0800
[#11063] fix(lance): manage lance storage config in Gravitino (#11070)
### What changes were proposed in this pull request?
This PR moves Lance storage configuration into Gravitino-managed catalog
properties and teaches Lance REST to use those catalog defaults when
serving Spark clients. It removes the need to pass
spark.sql.catalog.lance.storage.* in the demo path, while keeping
table-level lance.storage.* overrides supported.
### Why are the changes needed?
Spark currently has to repeat MinIO/S3 settings that are already known
to Gravitino, which makes Lance integration verbose and error-prone.
This change keeps Gravitino as the source of truth for Lance storage
configuration and simplifies Spark usage for Gravitino-managed Lance
catalogs.
Fix: #11063
### Does this PR introduce _any_ user-facing change?
Yes.
- lance.storage.* is now supported as a catalog-level property family
for generic lakehouse catalogs.
- Lance REST responses now resolve storage options from catalog defaults
when table-level values are absent.
- The Spark demo no longer requires explicit Lance storage conf entries.
- Table-level lance.storage.* remains supported as an override.
### How was this patch tested?
- ./gradlew :lance:lance-common:test --tests
org.apache.gravitino.lance.common.utils.TestLancePropertiesUtils
-PskipITs -PskipDockerTests=true
- ./gradlew :catalogs:catalog-lakehouse-generic:test --tests
org.apache.gravitino.catalog.lakehouse.generic.TestPropertiesMetadata
-PskipITs -PskipDockerTests=true
- ./gradlew :lance:lance-rest-server:compileTestJava -PskipITs
-PskipDockerTests=true
---------
Co-authored-by: fanng <“[email protected]”>
---
.../generic/GenericCatalogOperations.java | 16 +++++-
.../generic/GenericCatalogPropertiesMetadata.java | 11 +++-
.../lakehouse/lance/LanceTableOperations.java | 40 +++++++++++---
.../lakehouse/generic/TestPropertiesMetadata.java | 9 +++-
.../lakehouse/lance/TestLanceTableOperations.java | 44 ++++++++++++++-
docs/lakehouse-generic-lance-table.md | 26 ++++++---
docs/lance-rest-integration.md | 35 ++++++++----
.../gravitino/GravitinoLanceTableOperations.java | 9 ++--
.../lance/common/utils/LancePropertiesUtils.java | 38 ++++++++++---
.../common/utils/TestLancePropertiesUtils.java | 63 ++++++++++++++++++++++
.../lance/integration/test/LanceRESTServiceIT.java | 56 ++++++++++++++++++-
11 files changed, 310 insertions(+), 37 deletions(-)
diff --git
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogOperations.java
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogOperations.java
index e31263fadc..1d5876b6dd 100644
---
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogOperations.java
+++
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogOperations.java
@@ -44,6 +44,7 @@ import org.apache.gravitino.Schema;
import org.apache.gravitino.SchemaChange;
import org.apache.gravitino.catalog.ManagedSchemaOperations;
import org.apache.gravitino.catalog.ManagedTableOperations;
+import org.apache.gravitino.catalog.lakehouse.lance.LanceTableOperations;
import org.apache.gravitino.connector.CatalogInfo;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
@@ -77,6 +78,8 @@ public class GenericCatalogOperations implements
CatalogOperations, SupportsSche
private Optional<String> catalogLocation;
+ private Map<String, String> catalogProperties = Map.of();
+
private HasPropertyMetadata propertiesMetadata;
private final Cache<NameIdentifier, String> tableFormatCache;
@@ -125,6 +128,7 @@ public class GenericCatalogOperations implements
CatalogOperations, SupportsSche
public void initialize(
Map<String, String> conf, CatalogInfo info, HasPropertyMetadata
propertiesMetadata)
throws RuntimeException {
+ this.catalogProperties = conf == null ? Map.of() : Maps.newHashMap(conf);
String location =
(String)
propertiesMetadata
@@ -246,7 +250,7 @@ public class GenericCatalogOperations implements
CatalogOperations, SupportsSche
// Get the table operations for the specified table format.
Supplier<ManagedTableOperations> tableOpsSupplier =
tableOpsCache.get(format);
Preconditions.checkArgument(tableOpsSupplier != null, "Unsupported table
format: %s", format);
- ManagedTableOperations tableOps = tableOpsSupplier.get();
+ ManagedTableOperations tableOps =
configureTableOps(tableOpsSupplier.get());
Table createdTable =
tableOps.createTable(
@@ -339,7 +343,7 @@ public class GenericCatalogOperations implements
CatalogOperations, SupportsSche
return format.toLowerCase(Locale.ROOT);
});
- ManagedTableOperations ops = tableOpsCache.get(tableFormat).get();
+ ManagedTableOperations ops =
configureTableOps(tableOpsCache.get(tableFormat).get());
Preconditions.checkArgument(
ops != null, "No table operations found for table format %s",
tableFormat);
return ops;
@@ -360,4 +364,12 @@ public class GenericCatalogOperations implements
CatalogOperations, SupportsSche
}
}
}
+
+ private ManagedTableOperations configureTableOps(ManagedTableOperations ops)
{
+ if (ops instanceof LanceTableOperations) {
+ ((LanceTableOperations) ops).setCatalogProperties(catalogProperties);
+ }
+
+ return ops;
+ }
}
diff --git
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogPropertiesMetadata.java
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogPropertiesMetadata.java
index 71daef161b..dd805181f8 100644
---
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogPropertiesMetadata.java
+++
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogPropertiesMetadata.java
@@ -20,6 +20,8 @@
package org.apache.gravitino.catalog.lakehouse.generic;
import static
org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyEntry;
+import static
org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyPrefixEntry;
+import static
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
@@ -41,7 +43,14 @@ public class GenericCatalogPropertiesMetadata extends
BaseCatalogPropertiesMetad
"The root directory of the generic catalog.",
false /* immutable */,
null, /* defaultValue */
- false /* hidden */));
+ false /* hidden */),
+ stringOptionalPropertyPrefixEntry(
+ LANCE_STORAGE_OPTIONS_PREFIX,
+ "The Lance storage options managed by the catalog.",
+ false /* immutable */,
+ null, /* defaultValue */
+ false /* hidden */,
+ false /* reserved */));
PROPERTIES_METADATA = Maps.uniqueIndex(propertyEntries,
PropertyEntry::getName);
}
diff --git
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
index a1342e1cc9..dccbfe8c04 100644
---
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
+++
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
@@ -53,6 +53,7 @@ import
org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.indexes.Index;
import org.apache.gravitino.storage.IdGenerator;
import org.lance.Dataset;
+import org.lance.ReadOptions;
import org.lance.WriteParams;
import org.lance.index.DistanceType;
import org.lance.index.IndexOptions;
@@ -78,6 +79,8 @@ public class LanceTableOperations extends
ManagedTableOperations {
private final IdGenerator idGenerator;
+ private volatile Map<String, String> catalogProperties = Map.of();
+
public LanceTableOperations(
EntityStore store, ManagedSchemaOperations schemaOps, IdGenerator
idGenerator) {
this.store = store;
@@ -100,6 +103,16 @@ public class LanceTableOperations extends
ManagedTableOperations {
return idGenerator;
}
+ /**
+ * Sets the catalog properties used to resolve Lance storage defaults at
runtime.
+ *
+ * @param catalogProperties the catalog properties
+ */
+ public void setCatalogProperties(Map<String, String> catalogProperties) {
+ this.catalogProperties =
+ catalogProperties == null ? Map.of() :
ImmutableMap.copyOf(catalogProperties);
+ }
+
@Override
public Table createTable(
NameIdentifier ident,
@@ -197,7 +210,9 @@ public class LanceTableOperations extends
ManagedTableOperations {
// Otherwise, we should not delete the dataset.
if (purged) {
// Delete the Lance dataset at the location
- Dataset.drop(location,
LancePropertiesUtils.getLanceStorageOptions(table.properties()));
+ Dataset.drop(
+ location,
+ LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties,
table.properties()));
LOG.info("Deleted Lance dataset at location {}", location);
}
@@ -231,7 +246,9 @@ public class LanceTableOperations extends
ManagedTableOperations {
String location = table.properties().get(Table.PROPERTY_LOCATION);
// Delete the Lance dataset at the location
- Dataset.drop(location,
LancePropertiesUtils.getLanceStorageOptions(table.properties()));
+ Dataset.drop(
+ location,
+ LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties,
table.properties()));
LOG.info("Deleted Lance dataset at location {}", location);
}
@@ -277,7 +294,8 @@ public class LanceTableOperations extends
ManagedTableOperations {
ident, columns, comment, properties, partitions, distribution,
sortOrders, indexes);
}
- Map<String, String> storageProps =
LancePropertiesUtils.getLanceStorageOptions(properties);
+ Map<String, String> storageProps =
+ LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties,
properties);
try (Dataset ignored =
Dataset.write()
.allocator(new RootAllocator())
@@ -308,7 +326,7 @@ public class LanceTableOperations extends
ManagedTableOperations {
} catch (TableAlreadyExistsException e) {
// If the table metadata already exists, but the underlying lance table
was just created
// successfully, we need to clean up the created lance table to avoid
orphaned datasets.
- Dataset.drop(location,
LancePropertiesUtils.getLanceStorageOptions(properties));
+ Dataset.drop(location, storageProps);
throw e;
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("Dataset already exists")) {
@@ -344,7 +362,9 @@ public class LanceTableOperations extends
ManagedTableOperations {
*/
long handleLanceTableChange(Table table, TableChange[] changes) {
String location = table.properties().get(Table.PROPERTY_LOCATION);
- try (Dataset dataset = openDataset(location)) {
+ Map<String, String> storageOptions =
+ LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties,
table.properties());
+ try (Dataset dataset = openDataset(location, storageOptions)) {
for (TableChange change : changes) {
if (change instanceof TableChange.DeleteColumn deleteColumn) {
dataset.dropColumns(List.of(String.join(".",
deleteColumn.fieldName())));
@@ -385,7 +405,15 @@ public class LanceTableOperations extends
ManagedTableOperations {
}
Dataset openDataset(String location) {
- return Dataset.open().allocator(new RootAllocator()).uri(location).build();
+ return openDataset(location, Map.of());
+ }
+
+ Dataset openDataset(String location, Map<String, String> storageOptions) {
+ return Dataset.open()
+ .allocator(new RootAllocator())
+ .uri(location)
+ .readOptions(new
ReadOptions.Builder().setStorageOptions(storageOptions).build())
+ .build();
}
private IndexParams getIndexParamsByIndexType(IndexType indexType) {
diff --git
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestPropertiesMetadata.java
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestPropertiesMetadata.java
index 75eb613dc5..5371a63f5a 100644
---
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestPropertiesMetadata.java
+++
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestPropertiesMetadata.java
@@ -41,13 +41,20 @@ public class TestPropertiesMetadata {
PropertiesMetadata catalogPropertiesMetadata =
genericCatalog.catalogPropertiesMetadata();
Assertions.assertNotNull(catalogPropertiesMetadata);
- Map<String, String> catalogProperties = ImmutableMap.of("location",
"/tmp/test1");
+ Map<String, String> catalogProperties =
+ ImmutableMap.of(
+ "location", "/tmp/test1",
+ "lance.storage.endpoint", "http://minio:9000");
String catalogLocation =
(String)
catalogPropertiesMetadata.getOrDefault(
catalogProperties, GenericCatalog.PROPERTY_LOCATION);
Assertions.assertEquals("/tmp/test1", catalogLocation);
+
Assertions.assertTrue(catalogPropertiesMetadata.containsProperty("lance.storage.endpoint"));
+ Assertions.assertEquals(
+ "http://minio:9000",
+ catalogPropertiesMetadata.getOrDefault(catalogProperties,
"lance.storage.endpoint"));
}
@Test
diff --git
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
index c102d321e2..7362f903cb 100644
---
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
+++
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.catalog.lakehouse.lance;
import static
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_CREATION_MODE;
+import static
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
@@ -108,7 +109,7 @@ public class TestLanceTableOperations {
Version version = mock(Version.class);
when(dataset.getVersion()).thenReturn(version);
when(version.getId()).thenReturn(7L);
- Mockito.doReturn(dataset).when(lanceTableOps).openDataset("location");
+ Mockito.doReturn(dataset).when(lanceTableOps).openDataset("location",
Map.of());
TableChange[] changes =
new TableChange[] {
@@ -126,4 +127,45 @@ public class TestLanceTableOperations {
inOrder.verify(dataset).dropColumns(anyList());
inOrder.verify(dataset).getVersion();
}
+
+ @Test
+ public void testHandleLanceTableChangeUsesCatalogStorageOptions() {
+ lanceTableOps.setCatalogProperties(
+ Map.of(
+ LANCE_STORAGE_OPTIONS_PREFIX + "endpoint",
"http://catalog-endpoint",
+ LANCE_STORAGE_OPTIONS_PREFIX + "secret_access_key",
"catalog-secret"));
+
+ Table table = mock(Table.class);
+ when(table.properties())
+ .thenReturn(
+ Map.of(
+ Table.PROPERTY_LOCATION,
+ "location",
+ LANCE_STORAGE_OPTIONS_PREFIX + "access_key_id",
+ "table-key"));
+
+ Dataset dataset = mock(Dataset.class);
+ Version version = mock(Version.class);
+ when(dataset.getVersion()).thenReturn(version);
+ when(version.getId()).thenReturn(9L);
+ Mockito.doReturn(dataset)
+ .when(lanceTableOps)
+ .openDataset(
+ "location",
+ Map.of(
+ "endpoint",
+ "http://catalog-endpoint",
+ "secret_access_key",
+ "catalog-secret",
+ "access_key_id",
+ "table-key"));
+
+ TableChange[] changes =
+ new TableChange[] {TableChange.deleteColumn(new String[] {"col1"},
false)};
+ long returnedVersion = lanceTableOps.handleLanceTableChange(table,
changes);
+
+ Assertions.assertEquals(9L, returnedVersion);
+ Mockito.verify(dataset).dropColumns(anyList());
+ Mockito.verify(dataset).getVersion();
+ }
}
diff --git a/docs/lakehouse-generic-lance-table.md
b/docs/lakehouse-generic-lance-table.md
index 12e853fee6..d42e34377a 100644
--- a/docs/lakehouse-generic-lance-table.md
+++ b/docs/lakehouse-generic-lance-table.md
@@ -303,9 +303,26 @@ done
Other table operations (load, alter, drop, truncate) follow standard
relational catalog patterns. See [Table
Operations](./manage-relational-metadata-using-gravitino.md#table-operations)
for details.
### Using Lance table with MinIO
-To use Lance tables stored in MinIO with Gravitino, ensure that the MinIO
storage backend is properly configured. Below is an example of how to set up
and use Lance tables with MinIO.
+To use Lance tables stored in MinIO with Gravitino, configure the MinIO
storage backend once on the Lance catalog. Gravitino will then return those
storage options to Lance clients and Spark does not need to repeat them.
```shell
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "lance_catalog",
+ "type": "RELATIONAL",
+ "provider": "lakehouse-generic",
+ "comment": "catalog for Lance tables on MinIO",
+ "properties": {
+ "location": "s3://bucket1/lance",
+ "lance.storage.endpoint": "http://minio:9000",
+ "lance.storage.access_key_id": "ak",
+ "lance.storage.secret_access_key": "sk",
+ "lance.storage.allow_http": "true",
+ "lance.storage.region": "us-east-1"
+ }
+}' http://localhost:8090/api/metalakes/test/catalogs
+
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" -d '{
"name": "lance_orders",
@@ -320,13 +337,10 @@ curl -X POST -H "Accept:
application/vnd.gravitino.v1+json" \
],
"properties": {
"format": "lance",
- "location": "s3://bucket1/lance_orders",
- "lance.storage.access_key_id": "ak",
- "lance.storage.endpoint": "http://minio:9000",
- "lance.storage.secret_access_key": "sk",
- "lance.storage.allow_http": "true"
+ "location": "s3://bucket1/lance_orders"
}
}'
http://localhost:8090/api/metalakes/test/catalogs/lance_catalog/schemas/sales/tables
```
+If you need to override storage on a single table, `lance.storage.*` table
properties are still supported.
diff --git a/docs/lance-rest-integration.md b/docs/lance-rest-integration.md
index 101213dfcd..66fbaff4f3 100644
--- a/docs/lance-rest-integration.md
+++ b/docs/lance-rest-integration.md
@@ -126,7 +126,28 @@ spark.sql("SELECT * FROM sales.orders").show()
The `LOCATION` clause in the `CREATE TABLE` statement is optional. When
omitted, lance-spark automatically determines an appropriate storage location
based on catalog properties.
For detailed information on location resolution logic, refer to the [Lakehouse
Generic Catalog
documentation](./lakehouse-generic-catalog.md#key-property-location).
-For cloud storage backends such as Amazon S3 or MinIO, specify credentials and
endpoint configuration in the table properties:
+For Gravitino-managed Lance catalogs, put the storage configuration in the
Gravitino catalog properties so Spark does not need to repeat it.
+
+For example, create the Gravitino catalog with catalog-level Lance storage
properties:
+
+```shell
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "lance_catalog",
+ "type": "RELATIONAL",
+ "provider": "lakehouse-generic",
+ "comment": "catalog for Lance tables on MinIO",
+ "properties": {
+ "location": "s3://bucket/tmp",
+ "lance.storage.endpoint": "http://minio:9000",
+ "lance.storage.access_key_id": "ak",
+ "lance.storage.secret_access_key": "sk",
+ "lance.storage.allow_http": "true",
+ "lance.storage.region": "us-east-1"
+ }
+}' http://localhost:8090/api/metalakes/test/catalogs
+```
```python
spark.sql("""
@@ -136,16 +157,12 @@ spark.sql("""
)
USING lance
LOCATION 's3://bucket/tmp/sales/orders.lance/'
- TBLPROPERTIES (
- 'format' = 'lance',
- 'lance.storage.access_key_id' = 'your_access_key',
- 'lance.storage.secret_access_key' = 'your_secret_key',
- 'lance.storage.endpoint' = 'http://minio:9000',
- 'lance.storage.allow_http' = 'true'
- )
+ TBLPROPERTIES ('format' = 'lance')
""")
```
+If you need a per-table override, `lance.storage.*` table properties are still
supported and take precedence over catalog defaults.
+
## Ray Integration
### Installation
@@ -230,4 +247,4 @@ Refer to each engine's specific documentation for detailed
configuration paramet
- [Lance REST Service Documentation](./lance-rest-service)
- [Lance Format Specification](https://lance.org/)
- [Apache Gravitino Documentation](https://gravitino.apache.org/)
-- [Lakehouse Generic Catalog Guide](./lakehouse-generic-catalog.md)
\ No newline at end of file
+- [Lakehouse Generic Catalog Guide](./lakehouse-generic-catalog.md)
diff --git
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
index d43aad8f0c..a1ec2ee9bb 100644
---
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
+++
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
@@ -131,7 +131,8 @@ public class GravitinoLanceTableOperations implements
LanceTableOperations {
Optional.ofNullable(table.properties().get(LANCE_TABLE_VERSION))
.map(Long::valueOf)
.orElse(null));
-
response.setStorageOptions(LancePropertiesUtils.getLanceStorageOptions(table.properties()));
+ response.setStorageOptions(
+ LancePropertiesUtils.resolveLanceStorageOptions(catalog.properties(),
table.properties()));
return response;
}
@@ -179,11 +180,11 @@ public class GravitinoLanceTableOperations implements
LanceTableOperations {
.createTable(
tableIdentifier, columns.toArray(new Column[0]), null,
createTableProperties);
Map<String, String> properties = t.properties();
+ Map<String, String> effectiveStorageOptions =
+ LancePropertiesUtils.resolveLanceStorageOptions(catalog.properties(),
properties);
CreateTableResponse response = new CreateTableResponse();
- // Extract storage options from table properties. All storage options
stores in table
- // properties.
-
response.setStorageOptions(LancePropertiesUtils.getLanceStorageOptions(properties));
+ response.setStorageOptions(effectiveStorageOptions);
response.setVersion(
Optional.ofNullable(properties.get(LANCE_TABLE_VERSION)).map(Long::valueOf).orElse(null));
response.setLocation(properties.get(LANCE_LOCATION));
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 e674a7266a..3555fa3f99 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,25 +21,51 @@ package org.apache.gravitino.lance.common.utils;
import static
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
-public class LancePropertiesUtils {
+/** Utility methods for Lance storage properties. */
+public final class LancePropertiesUtils {
private LancePropertiesUtils() {
- // Private constructor to prevent instantiation
+ // Utility class.
}
+ /**
+ * Extracts Lance storage options from a property map.
+ *
+ * @param tableProperties the source properties
+ * @return the Lance storage options without the `lance.storage.` prefix
+ */
public static Map<String, String> getLanceStorageOptions(Map<String, String>
tableProperties) {
- if (tableProperties == null) {
+ if (tableProperties == null || tableProperties.isEmpty()) {
return Map.of();
}
return tableProperties.entrySet().stream()
- .filter(e -> e.getKey().startsWith(LANCE_STORAGE_OPTIONS_PREFIX))
+ .filter(entry ->
entry.getKey().startsWith(LANCE_STORAGE_OPTIONS_PREFIX))
.collect(
Collectors.toMap(
- e ->
e.getKey().substring(LANCE_STORAGE_OPTIONS_PREFIX.length()),
- Map.Entry::getValue));
+ entry ->
entry.getKey().substring(LANCE_STORAGE_OPTIONS_PREFIX.length()),
+ Map.Entry::getValue,
+ (left, right) -> right,
+ LinkedHashMap::new));
+ }
+
+ /**
+ * Resolves the effective Lance storage options using table properties first
and catalog
+ * properties as defaults.
+ *
+ * @param catalogProperties the catalog properties
+ * @param tableProperties the table properties
+ * @return the effective storage options
+ */
+ public static Map<String, String> resolveLanceStorageOptions(
+ Map<String, String> catalogProperties, Map<String, String>
tableProperties) {
+ Map<String, String> effectiveStorageOptions =
+ new LinkedHashMap<>(getLanceStorageOptions(catalogProperties));
+ effectiveStorageOptions.putAll(getLanceStorageOptions(tableProperties));
+ return effectiveStorageOptions;
}
}
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
new file mode 100644
index 0000000000..55bfa6c347
--- /dev/null
+++
b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/utils/TestLancePropertiesUtils.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.gravitino.lance.common.utils;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestLancePropertiesUtils {
+
+ @Test
+ public void testGetLanceStorageOptions() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ "lance.storage.endpoint", "http://minio:9000",
+ "lance.storage.access_key_id", "ak",
+ "not.storage.key", "ignored");
+
+ Map<String, String> storageOptions =
LancePropertiesUtils.getLanceStorageOptions(properties);
+
+ Assertions.assertEquals(2, storageOptions.size());
+ Assertions.assertEquals("http://minio:9000",
storageOptions.get("endpoint"));
+ Assertions.assertEquals("ak", storageOptions.get("access_key_id"));
+ Assertions.assertFalse(storageOptions.containsKey("not.storage.key"));
+ }
+
+ @Test
+ public void testResolveLanceStorageOptionsPrefersTableProperties() {
+ Map<String, String> catalogProperties =
+ ImmutableMap.of(
+ "lance.storage.endpoint", "http://catalog:9000",
+ "lance.storage.region", "us-east-1");
+ Map<String, String> tableProperties =
+ ImmutableMap.of(
+ "lance.storage.endpoint", "http://table:9000",
+ "lance.storage.access_key_id", "table-ak");
+
+ Map<String, String> storageOptions =
+ LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties,
tableProperties);
+
+ Assertions.assertEquals(3, storageOptions.size());
+ 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"));
+ }
+}
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
index 501f1394e7..3c0661971d 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
@@ -95,6 +95,10 @@ public class LanceRESTServiceIT extends BaseIT {
private static final String CATALOG_NAME =
GravitinoITUtils.genRandomName("lance_rest_catalog");
private static final String SCHEMA_NAME =
GravitinoITUtils.genRandomName("lance_rest_schema");
private static final String DELIMITER = ".";
+ private static final String MINIO_ENDPOINT = "http://127.0.0.1:9000";
+ private static final String MINIO_REGION = "us-east-1";
+ private static final String MINIO_ACCESS_KEY = "minioadmin";
+ private static final String MINIO_SECRET_KEY = "minioadmin";
private GravitinoMetalake metalake;
private Catalog catalog;
@@ -661,6 +665,46 @@ public class LanceRESTServiceIT extends BaseIT {
.contains("Column non_existing_column does not exist in the
dataset"));
}
+ @Test
+ void testCreateTableUsesCatalogStorageOptions() throws IOException {
+ catalog =
createCatalog(GravitinoITUtils.genRandomName("lance_rest_catalog"));
+ createSchema();
+
+ String location = tempDir.resolve("catalog_storage_table").toString() +
"/";
+ List<String> ids = List.of(catalog.name(), SCHEMA_NAME,
"catalog_storage_table");
+ org.apache.arrow.vector.types.pojo.Schema schema =
+ new org.apache.arrow.vector.types.pojo.Schema(
+ Arrays.asList(
+ Field.nullable("id", new ArrowType.Int(32, true)),
+ Field.nullable("value", new ArrowType.Utf8())));
+ byte[] body = ArrowUtils.generateIpcStream(schema);
+
+ CreateTableResponse response =
+ createTable(ids, location, ImmutableMap.of("key1", "v1"), body, /*
mode= */ null);
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(location, response.getLocation());
+ Assertions.assertEquals(MINIO_ENDPOINT,
response.getStorageOptions().get("endpoint"));
+ Assertions.assertEquals("true",
response.getStorageOptions().get("allow_http"));
+ Assertions.assertEquals(MINIO_ACCESS_KEY,
response.getStorageOptions().get("access_key_id"));
+ Assertions.assertEquals(
+ MINIO_SECRET_KEY,
response.getStorageOptions().get("secret_access_key"));
+ Assertions.assertEquals(MINIO_REGION,
response.getStorageOptions().get("region"));
+
+ DescribeTableRequest describeTableRequest = new DescribeTableRequest();
+ describeTableRequest.setId(ids);
+ DescribeTableResponse loadTable = ns.describeTable(describeTableRequest);
+ Assertions.assertNotNull(loadTable);
+ Assertions.assertEquals(MINIO_ENDPOINT,
loadTable.getStorageOptions().get("endpoint"));
+ Assertions.assertEquals("true",
loadTable.getStorageOptions().get("allow_http"));
+ Assertions.assertEquals(MINIO_ACCESS_KEY,
loadTable.getStorageOptions().get("access_key_id"));
+ Assertions.assertEquals(
+ MINIO_SECRET_KEY,
loadTable.getStorageOptions().get("secret_access_key"));
+ Assertions.assertEquals(MINIO_REGION,
loadTable.getStorageOptions().get("region"));
+
Assertions.assertFalse(loadTable.getMetadata().containsKey("lance.storage.endpoint"));
+
Assertions.assertFalse(loadTable.getMetadata().containsKey("lance.storage.access_key_id"));
+
Assertions.assertFalse(loadTable.getMetadata().containsKey("lance.storage.secret_access_key"));
+ }
+
@Test
void testAlterColumns() throws Exception {
catalog = createCatalog(CATALOG_NAME);
@@ -1025,12 +1069,22 @@ public class LanceRESTServiceIT extends BaseIT {
}
private Catalog createCatalog(String catalogName) {
+ Map<String, String> catalogProperties =
+ ImmutableMap.<String, String>builder()
+ .putAll(properties)
+ .put("lance.storage.endpoint", MINIO_ENDPOINT)
+ .put("lance.storage.allow_http", "true")
+ .put("lance.storage.access_key_id", MINIO_ACCESS_KEY)
+ .put("lance.storage.secret_access_key", MINIO_SECRET_KEY)
+ .put("lance.storage.region", MINIO_REGION)
+ .build();
+
return metalake.createCatalog(
catalogName,
Catalog.Type.RELATIONAL,
"lakehouse-generic",
"catalog for lance rest service tests",
- properties);
+ catalogProperties);
}
private void createSchema() {