Skip to content

Commit fb902cc

Browse files
FANNG1fanng
andauthored
[#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 <“fanng@apache.org”>
1 parent 6402759 commit fb902cc

11 files changed

Lines changed: 310 additions & 37 deletions

File tree

catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogOperations.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.apache.gravitino.SchemaChange;
4545
import org.apache.gravitino.catalog.ManagedSchemaOperations;
4646
import org.apache.gravitino.catalog.ManagedTableOperations;
47+
import org.apache.gravitino.catalog.lakehouse.lance.LanceTableOperations;
4748
import org.apache.gravitino.connector.CatalogInfo;
4849
import org.apache.gravitino.connector.CatalogOperations;
4950
import org.apache.gravitino.connector.HasPropertyMetadata;
@@ -77,6 +78,8 @@ public class GenericCatalogOperations implements CatalogOperations, SupportsSche
7778

7879
private Optional<String> catalogLocation;
7980

81+
private Map<String, String> catalogProperties = Map.of();
82+
8083
private HasPropertyMetadata propertiesMetadata;
8184

8285
private final Cache<NameIdentifier, String> tableFormatCache;
@@ -125,6 +128,7 @@ protected EntityStore store() {
125128
public void initialize(
126129
Map<String, String> conf, CatalogInfo info, HasPropertyMetadata propertiesMetadata)
127130
throws RuntimeException {
131+
this.catalogProperties = conf == null ? Map.of() : Maps.newHashMap(conf);
128132
String location =
129133
(String)
130134
propertiesMetadata
@@ -246,7 +250,7 @@ public Table createTable(
246250
// Get the table operations for the specified table format.
247251
Supplier<ManagedTableOperations> tableOpsSupplier = tableOpsCache.get(format);
248252
Preconditions.checkArgument(tableOpsSupplier != null, "Unsupported table format: %s", format);
249-
ManagedTableOperations tableOps = tableOpsSupplier.get();
253+
ManagedTableOperations tableOps = configureTableOps(tableOpsSupplier.get());
250254

251255
Table createdTable =
252256
tableOps.createTable(
@@ -339,7 +343,7 @@ private ManagedTableOperations tableOps(NameIdentifier tableIdent) {
339343
return format.toLowerCase(Locale.ROOT);
340344
});
341345

342-
ManagedTableOperations ops = tableOpsCache.get(tableFormat).get();
346+
ManagedTableOperations ops = configureTableOps(tableOpsCache.get(tableFormat).get());
343347
Preconditions.checkArgument(
344348
ops != null, "No table operations found for table format %s", tableFormat);
345349
return ops;
@@ -360,4 +364,12 @@ private ManagedTableOperations tableOps(NameIdentifier tableIdent) {
360364
}
361365
}
362366
}
367+
368+
private ManagedTableOperations configureTableOps(ManagedTableOperations ops) {
369+
if (ops instanceof LanceTableOperations) {
370+
((LanceTableOperations) ops).setCatalogProperties(catalogProperties);
371+
}
372+
373+
return ops;
374+
}
363375
}

catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/generic/GenericCatalogPropertiesMetadata.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
package org.apache.gravitino.catalog.lakehouse.generic;
2121

2222
import static org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyEntry;
23+
import static org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyPrefixEntry;
24+
import static org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
2325

2426
import com.google.common.collect.ImmutableList;
2527
import com.google.common.collect.Maps;
@@ -41,7 +43,14 @@ public class GenericCatalogPropertiesMetadata extends BaseCatalogPropertiesMetad
4143
"The root directory of the generic catalog.",
4244
false /* immutable */,
4345
null, /* defaultValue */
44-
false /* hidden */));
46+
false /* hidden */),
47+
stringOptionalPropertyPrefixEntry(
48+
LANCE_STORAGE_OPTIONS_PREFIX,
49+
"The Lance storage options managed by the catalog.",
50+
false /* immutable */,
51+
null, /* defaultValue */
52+
false /* hidden */,
53+
false /* reserved */));
4554

4655
PROPERTIES_METADATA = Maps.uniqueIndex(propertyEntries, PropertyEntry::getName);
4756
}

catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.apache.gravitino.rel.indexes.Index;
5454
import org.apache.gravitino.storage.IdGenerator;
5555
import org.lance.Dataset;
56+
import org.lance.ReadOptions;
5657
import org.lance.WriteParams;
5758
import org.lance.index.DistanceType;
5859
import org.lance.index.IndexOptions;
@@ -78,6 +79,8 @@ public enum CreationMode {
7879

7980
private final IdGenerator idGenerator;
8081

82+
private volatile Map<String, String> catalogProperties = Map.of();
83+
8184
public LanceTableOperations(
8285
EntityStore store, ManagedSchemaOperations schemaOps, IdGenerator idGenerator) {
8386
this.store = store;
@@ -100,6 +103,16 @@ protected IdGenerator idGenerator() {
100103
return idGenerator;
101104
}
102105

106+
/**
107+
* Sets the catalog properties used to resolve Lance storage defaults at runtime.
108+
*
109+
* @param catalogProperties the catalog properties
110+
*/
111+
public void setCatalogProperties(Map<String, String> catalogProperties) {
112+
this.catalogProperties =
113+
catalogProperties == null ? Map.of() : ImmutableMap.copyOf(catalogProperties);
114+
}
115+
103116
@Override
104117
public Table createTable(
105118
NameIdentifier ident,
@@ -197,7 +210,9 @@ public boolean purgeTable(NameIdentifier ident) {
197210
// Otherwise, we should not delete the dataset.
198211
if (purged) {
199212
// Delete the Lance dataset at the location
200-
Dataset.drop(location, LancePropertiesUtils.getLanceStorageOptions(table.properties()));
213+
Dataset.drop(
214+
location,
215+
LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, table.properties()));
201216
LOG.info("Deleted Lance dataset at location {}", location);
202217
}
203218

@@ -231,7 +246,9 @@ public boolean dropTable(NameIdentifier ident) {
231246
String location = table.properties().get(Table.PROPERTY_LOCATION);
232247

233248
// Delete the Lance dataset at the location
234-
Dataset.drop(location, LancePropertiesUtils.getLanceStorageOptions(table.properties()));
249+
Dataset.drop(
250+
location,
251+
LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, table.properties()));
235252
LOG.info("Deleted Lance dataset at location {}", location);
236253
}
237254

@@ -277,7 +294,8 @@ Table createTableInternal(
277294
ident, columns, comment, properties, partitions, distribution, sortOrders, indexes);
278295
}
279296

280-
Map<String, String> storageProps = LancePropertiesUtils.getLanceStorageOptions(properties);
297+
Map<String, String> storageProps =
298+
LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, properties);
281299
try (Dataset ignored =
282300
Dataset.write()
283301
.allocator(new RootAllocator())
@@ -308,7 +326,7 @@ Table createTableInternal(
308326
} catch (TableAlreadyExistsException e) {
309327
// If the table metadata already exists, but the underlying lance table was just created
310328
// successfully, we need to clean up the created lance table to avoid orphaned datasets.
311-
Dataset.drop(location, LancePropertiesUtils.getLanceStorageOptions(properties));
329+
Dataset.drop(location, storageProps);
312330
throw e;
313331
} catch (IllegalArgumentException e) {
314332
if (e.getMessage().contains("Dataset already exists")) {
@@ -344,7 +362,9 @@ private org.apache.arrow.vector.types.pojo.Schema convertColumnsToArrowSchema(Co
344362
*/
345363
long handleLanceTableChange(Table table, TableChange[] changes) {
346364
String location = table.properties().get(Table.PROPERTY_LOCATION);
347-
try (Dataset dataset = openDataset(location)) {
365+
Map<String, String> storageOptions =
366+
LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, table.properties());
367+
try (Dataset dataset = openDataset(location, storageOptions)) {
348368
for (TableChange change : changes) {
349369
if (change instanceof TableChange.DeleteColumn deleteColumn) {
350370
dataset.dropColumns(List.of(String.join(".", deleteColumn.fieldName())));
@@ -385,7 +405,15 @@ long handleLanceTableChange(Table table, TableChange[] changes) {
385405
}
386406

387407
Dataset openDataset(String location) {
388-
return Dataset.open().allocator(new RootAllocator()).uri(location).build();
408+
return openDataset(location, Map.of());
409+
}
410+
411+
Dataset openDataset(String location, Map<String, String> storageOptions) {
412+
return Dataset.open()
413+
.allocator(new RootAllocator())
414+
.uri(location)
415+
.readOptions(new ReadOptions.Builder().setStorageOptions(storageOptions).build())
416+
.build();
389417
}
390418

391419
private IndexParams getIndexParamsByIndexType(IndexType indexType) {

catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestPropertiesMetadata.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,20 @@ void testCatalogPropertiesMetadata() {
4141
PropertiesMetadata catalogPropertiesMetadata = genericCatalog.catalogPropertiesMetadata();
4242
Assertions.assertNotNull(catalogPropertiesMetadata);
4343

44-
Map<String, String> catalogProperties = ImmutableMap.of("location", "/tmp/test1");
44+
Map<String, String> catalogProperties =
45+
ImmutableMap.of(
46+
"location", "/tmp/test1",
47+
"lance.storage.endpoint", "http://minio:9000");
4548

4649
String catalogLocation =
4750
(String)
4851
catalogPropertiesMetadata.getOrDefault(
4952
catalogProperties, GenericCatalog.PROPERTY_LOCATION);
5053
Assertions.assertEquals("/tmp/test1", catalogLocation);
54+
Assertions.assertTrue(catalogPropertiesMetadata.containsProperty("lance.storage.endpoint"));
55+
Assertions.assertEquals(
56+
"http://minio:9000",
57+
catalogPropertiesMetadata.getOrDefault(catalogProperties, "lance.storage.endpoint"));
5158
}
5259

5360
@Test

catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.gravitino.catalog.lakehouse.lance;
2020

2121
import static org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_CREATION_MODE;
22+
import static org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
2223
import static org.mockito.ArgumentMatchers.any;
2324
import static org.mockito.ArgumentMatchers.anyList;
2425
import static org.mockito.Mockito.mock;
@@ -108,7 +109,7 @@ public void testHandleLanceTableChangeRespectsOrder() {
108109
Version version = mock(Version.class);
109110
when(dataset.getVersion()).thenReturn(version);
110111
when(version.getId()).thenReturn(7L);
111-
Mockito.doReturn(dataset).when(lanceTableOps).openDataset("location");
112+
Mockito.doReturn(dataset).when(lanceTableOps).openDataset("location", Map.of());
112113

113114
TableChange[] changes =
114115
new TableChange[] {
@@ -126,4 +127,45 @@ public void testHandleLanceTableChangeRespectsOrder() {
126127
inOrder.verify(dataset).dropColumns(anyList());
127128
inOrder.verify(dataset).getVersion();
128129
}
130+
131+
@Test
132+
public void testHandleLanceTableChangeUsesCatalogStorageOptions() {
133+
lanceTableOps.setCatalogProperties(
134+
Map.of(
135+
LANCE_STORAGE_OPTIONS_PREFIX + "endpoint", "http://catalog-endpoint",
136+
LANCE_STORAGE_OPTIONS_PREFIX + "secret_access_key", "catalog-secret"));
137+
138+
Table table = mock(Table.class);
139+
when(table.properties())
140+
.thenReturn(
141+
Map.of(
142+
Table.PROPERTY_LOCATION,
143+
"location",
144+
LANCE_STORAGE_OPTIONS_PREFIX + "access_key_id",
145+
"table-key"));
146+
147+
Dataset dataset = mock(Dataset.class);
148+
Version version = mock(Version.class);
149+
when(dataset.getVersion()).thenReturn(version);
150+
when(version.getId()).thenReturn(9L);
151+
Mockito.doReturn(dataset)
152+
.when(lanceTableOps)
153+
.openDataset(
154+
"location",
155+
Map.of(
156+
"endpoint",
157+
"http://catalog-endpoint",
158+
"secret_access_key",
159+
"catalog-secret",
160+
"access_key_id",
161+
"table-key"));
162+
163+
TableChange[] changes =
164+
new TableChange[] {TableChange.deleteColumn(new String[] {"col1"}, false)};
165+
long returnedVersion = lanceTableOps.handleLanceTableChange(table, changes);
166+
167+
Assertions.assertEquals(9L, returnedVersion);
168+
Mockito.verify(dataset).dropColumns(anyList());
169+
Mockito.verify(dataset).getVersion();
170+
}
129171
}

docs/lakehouse-generic-lance-table.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,26 @@ done
303303
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.
304304

305305
### Using Lance table with MinIO
306-
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.
306+
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.
307307

308308
```shell
309+
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
310+
-H "Content-Type: application/json" \
311+
-d '{
312+
"name": "lance_catalog",
313+
"type": "RELATIONAL",
314+
"provider": "lakehouse-generic",
315+
"comment": "catalog for Lance tables on MinIO",
316+
"properties": {
317+
"location": "s3://bucket1/lance",
318+
"lance.storage.endpoint": "http://minio:9000",
319+
"lance.storage.access_key_id": "ak",
320+
"lance.storage.secret_access_key": "sk",
321+
"lance.storage.allow_http": "true",
322+
"lance.storage.region": "us-east-1"
323+
}
324+
}' http://localhost:8090/api/metalakes/test/catalogs
325+
309326
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
310327
-H "Content-Type: application/json" -d '{
311328
"name": "lance_orders",
@@ -320,13 +337,10 @@ curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
320337
],
321338
"properties": {
322339
"format": "lance",
323-
"location": "s3://bucket1/lance_orders",
324-
"lance.storage.access_key_id": "ak",
325-
"lance.storage.endpoint": "http://minio:9000",
326-
"lance.storage.secret_access_key": "sk",
327-
"lance.storage.allow_http": "true"
340+
"location": "s3://bucket1/lance_orders"
328341
}
329342
}' http://localhost:8090/api/metalakes/test/catalogs/lance_catalog/schemas/sales/tables
330343

331344
```
332345

346+
If you need to override storage on a single table, `lance.storage.*` table properties are still supported.

docs/lance-rest-integration.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,28 @@ spark.sql("SELECT * FROM sales.orders").show()
126126
The `LOCATION` clause in the `CREATE TABLE` statement is optional. When omitted, lance-spark automatically determines an appropriate storage location based on catalog properties.
127127
For detailed information on location resolution logic, refer to the [Lakehouse Generic Catalog documentation](./lakehouse-generic-catalog.md#key-property-location).
128128

129-
For cloud storage backends such as Amazon S3 or MinIO, specify credentials and endpoint configuration in the table properties:
129+
For Gravitino-managed Lance catalogs, put the storage configuration in the Gravitino catalog properties so Spark does not need to repeat it.
130+
131+
For example, create the Gravitino catalog with catalog-level Lance storage properties:
132+
133+
```shell
134+
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
135+
-H "Content-Type: application/json" \
136+
-d '{
137+
"name": "lance_catalog",
138+
"type": "RELATIONAL",
139+
"provider": "lakehouse-generic",
140+
"comment": "catalog for Lance tables on MinIO",
141+
"properties": {
142+
"location": "s3://bucket/tmp",
143+
"lance.storage.endpoint": "http://minio:9000",
144+
"lance.storage.access_key_id": "ak",
145+
"lance.storage.secret_access_key": "sk",
146+
"lance.storage.allow_http": "true",
147+
"lance.storage.region": "us-east-1"
148+
}
149+
}' http://localhost:8090/api/metalakes/test/catalogs
150+
```
130151

131152
```python
132153
spark.sql("""
@@ -136,16 +157,12 @@ spark.sql("""
136157
)
137158
USING lance
138159
LOCATION 's3://bucket/tmp/sales/orders.lance/'
139-
TBLPROPERTIES (
140-
'format' = 'lance',
141-
'lance.storage.access_key_id' = 'your_access_key',
142-
'lance.storage.secret_access_key' = 'your_secret_key',
143-
'lance.storage.endpoint' = 'http://minio:9000',
144-
'lance.storage.allow_http' = 'true'
145-
)
160+
TBLPROPERTIES ('format' = 'lance')
146161
""")
147162
```
148163

164+
If you need a per-table override, `lance.storage.*` table properties are still supported and take precedence over catalog defaults.
165+
149166
## Ray Integration
150167

151168
### Installation
@@ -230,4 +247,4 @@ Refer to each engine's specific documentation for detailed configuration paramet
230247
- [Lance REST Service Documentation](./lance-rest-service)
231248
- [Lance Format Specification](https://lance.org/)
232249
- [Apache Gravitino Documentation](https://gravitino.apache.org/)
233-
- [Lakehouse Generic Catalog Guide](./lakehouse-generic-catalog.md)
250+
- [Lakehouse Generic Catalog Guide](./lakehouse-generic-catalog.md)

lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ public DescribeTableResponse describeTable(
131131
Optional.ofNullable(table.properties().get(LANCE_TABLE_VERSION))
132132
.map(Long::valueOf)
133133
.orElse(null));
134-
response.setStorageOptions(LancePropertiesUtils.getLanceStorageOptions(table.properties()));
134+
response.setStorageOptions(
135+
LancePropertiesUtils.resolveLanceStorageOptions(catalog.properties(), table.properties()));
135136
return response;
136137
}
137138

@@ -179,11 +180,11 @@ public CreateTableResponse createTable(
179180
.createTable(
180181
tableIdentifier, columns.toArray(new Column[0]), null, createTableProperties);
181182
Map<String, String> properties = t.properties();
183+
Map<String, String> effectiveStorageOptions =
184+
LancePropertiesUtils.resolveLanceStorageOptions(catalog.properties(), properties);
182185

183186
CreateTableResponse response = new CreateTableResponse();
184-
// Extract storage options from table properties. All storage options stores in table
185-
// properties.
186-
response.setStorageOptions(LancePropertiesUtils.getLanceStorageOptions(properties));
187+
response.setStorageOptions(effectiveStorageOptions);
187188
response.setVersion(
188189
Optional.ofNullable(properties.get(LANCE_TABLE_VERSION)).map(Long::valueOf).orElse(null));
189190
response.setLocation(properties.get(LANCE_LOCATION));

0 commit comments

Comments
 (0)