Skip to content

Commit fa33047

Browse files
author
fanng
committed
[#11063] fix(lance): manage storage config in Gravitino
1 parent c3a24b5 commit fa33047

10 files changed

Lines changed: 209 additions & 31 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import org.apache.gravitino.exceptions.NonEmptySchemaException;
5656
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
5757
import org.apache.gravitino.exceptions.TableAlreadyExistsException;
58+
import org.apache.gravitino.lance.common.utils.LanceConstants;
59+
import org.apache.gravitino.lance.common.utils.LancePropertiesUtils;
5860
import org.apache.gravitino.meta.TableEntity;
5961
import org.apache.gravitino.rel.Column;
6062
import org.apache.gravitino.rel.Table;
@@ -77,6 +79,8 @@ public class GenericCatalogOperations implements CatalogOperations, SupportsSche
7779

7880
private Optional<String> catalogLocation;
7981

82+
private Map<String, String> catalogProperties = Map.of();
83+
8084
private HasPropertyMetadata propertiesMetadata;
8185

8286
private final Cache<NameIdentifier, String> tableFormatCache;
@@ -125,6 +129,7 @@ protected EntityStore store() {
125129
public void initialize(
126130
Map<String, String> conf, CatalogInfo info, HasPropertyMetadata propertiesMetadata)
127131
throws RuntimeException {
132+
this.catalogProperties = conf == null ? Map.of() : Maps.newHashMap(conf);
128133
String location =
129134
(String)
130135
propertiesMetadata
@@ -242,6 +247,13 @@ public Table createTable(
242247
Map<String, String> newProperties = Maps.newHashMap(properties);
243248
newProperties.put(Table.PROPERTY_LOCATION, tableLocation);
244249
newProperties.put(Table.PROPERTY_TABLE_FORMAT, format);
250+
if ("lance".equals(format)) {
251+
LancePropertiesUtils.getLanceStorageOptions(catalogProperties)
252+
.forEach(
253+
(key, value) ->
254+
newProperties.putIfAbsent(
255+
LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX + key, value));
256+
}
245257

246258
// Get the table operations for the specified table format.
247259
Supplier<ManagedTableOperations> tableOpsSupplier = tableOpsCache.get(format);

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/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

docs/lakehouse-generic-lance-table.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ 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
309309
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
@@ -318,15 +318,12 @@ curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
318318
"nullable": false
319319
}
320320
],
321-
"properties": {
321+
"properties": {
322322
"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"
328-
}
323+
"location": "s3://bucket1/lance_orders"
324+
}
329325
}' http://localhost:8090/api/metalakes/test/catalogs/lance_catalog/schemas/sales/tables
330326

331327
```
332328

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

docs/lance-rest-integration.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ 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.
130130

131131
```python
132132
spark.sql("""
@@ -136,16 +136,12 @@ spark.sql("""
136136
)
137137
USING lance
138138
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-
)
139+
TBLPROPERTIES ('format' = 'lance')
146140
""")
147141
```
148142

143+
If you need a per-table override, `lance.storage.*` table properties are still supported and take precedence over catalog defaults.
144+
149145
## Ray Integration
150146

151147
### Installation
@@ -230,4 +226,4 @@ Refer to each engine's specific documentation for detailed configuration paramet
230226
- [Lance REST Service Documentation](./lance-rest-service)
231227
- [Lance Format Specification](https://lance.org/)
232228
- [Apache Gravitino Documentation](https://gravitino.apache.org/)
233-
- [Lakehouse Generic Catalog Guide](./lakehouse-generic-catalog.md)
229+
- [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: 6 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

@@ -171,6 +172,9 @@ public CreateTableResponse createTable(
171172

172173
// Pass creation mode as property to delegate handling to LanceTableOperations
173174
createTableProperties.put(LANCE_CREATION_MODE, normalizeCreateMode(mode, tableId));
175+
Map<String, String> effectiveStorageOptions =
176+
LancePropertiesUtils.resolveLanceStorageOptions(
177+
catalog.properties(), createTableProperties);
174178

175179
// Single call - mode is handled server-side
176180
Table t =
@@ -181,9 +185,7 @@ public CreateTableResponse createTable(
181185
Map<String, String> properties = t.properties();
182186

183187
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));
188+
response.setStorageOptions(effectiveStorageOptions);
187189
response.setVersion(
188190
Optional.ofNullable(properties.get(LANCE_TABLE_VERSION)).map(Long::valueOf).orElse(null));
189191
response.setLocation(properties.get(LANCE_LOCATION));

lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/LancePropertiesUtils.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,51 @@
2121

2222
import static org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_STORAGE_OPTIONS_PREFIX;
2323

24+
import java.util.LinkedHashMap;
2425
import java.util.Map;
2526
import java.util.stream.Collectors;
2627

27-
public class LancePropertiesUtils {
28+
/** Utility methods for Lance storage properties. */
29+
public final class LancePropertiesUtils {
2830

2931
private LancePropertiesUtils() {
30-
// Private constructor to prevent instantiation
32+
// Utility class.
3133
}
3234

35+
/**
36+
* Extracts Lance storage options from a property map.
37+
*
38+
* @param tableProperties the source properties
39+
* @return the Lance storage options without the `lance.storage.` prefix
40+
*/
3341
public static Map<String, String> getLanceStorageOptions(Map<String, String> tableProperties) {
34-
if (tableProperties == null) {
42+
if (tableProperties == null || tableProperties.isEmpty()) {
3543
return Map.of();
3644
}
3745

3846
return tableProperties.entrySet().stream()
39-
.filter(e -> e.getKey().startsWith(LANCE_STORAGE_OPTIONS_PREFIX))
47+
.filter(entry -> entry.getKey().startsWith(LANCE_STORAGE_OPTIONS_PREFIX))
4048
.collect(
4149
Collectors.toMap(
42-
e -> e.getKey().substring(LANCE_STORAGE_OPTIONS_PREFIX.length()),
43-
Map.Entry::getValue));
50+
entry -> entry.getKey().substring(LANCE_STORAGE_OPTIONS_PREFIX.length()),
51+
Map.Entry::getValue,
52+
(left, right) -> right,
53+
LinkedHashMap::new));
54+
}
55+
56+
/**
57+
* Resolves the effective Lance storage options using table properties first and catalog
58+
* properties as defaults.
59+
*
60+
* @param catalogProperties the catalog properties
61+
* @param tableProperties the table properties
62+
* @return the effective storage options
63+
*/
64+
public static Map<String, String> resolveLanceStorageOptions(
65+
Map<String, String> catalogProperties, Map<String, String> tableProperties) {
66+
Map<String, String> effectiveStorageOptions =
67+
new LinkedHashMap<>(getLanceStorageOptions(catalogProperties));
68+
effectiveStorageOptions.putAll(getLanceStorageOptions(tableProperties));
69+
return effectiveStorageOptions;
4470
}
4571
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.gravitino.lance.common.utils;
20+
21+
import com.google.common.collect.ImmutableMap;
22+
import java.util.Map;
23+
import org.junit.jupiter.api.Assertions;
24+
import org.junit.jupiter.api.Test;
25+
26+
public class TestLancePropertiesUtils {
27+
28+
@Test
29+
public void testGetLanceStorageOptions() {
30+
Map<String, String> properties =
31+
ImmutableMap.of(
32+
"lance.storage.endpoint", "http://minio:9000",
33+
"lance.storage.access_key_id", "ak",
34+
"not.storage.key", "ignored");
35+
36+
Map<String, String> storageOptions = LancePropertiesUtils.getLanceStorageOptions(properties);
37+
38+
Assertions.assertEquals(2, storageOptions.size());
39+
Assertions.assertEquals("http://minio:9000", storageOptions.get("endpoint"));
40+
Assertions.assertEquals("ak", storageOptions.get("access_key_id"));
41+
Assertions.assertFalse(storageOptions.containsKey("not.storage.key"));
42+
}
43+
44+
@Test
45+
public void testResolveLanceStorageOptionsPrefersTableProperties() {
46+
Map<String, String> catalogProperties =
47+
ImmutableMap.of(
48+
"lance.storage.endpoint", "http://catalog:9000",
49+
"lance.storage.region", "us-east-1");
50+
Map<String, String> tableProperties =
51+
ImmutableMap.of(
52+
"lance.storage.endpoint", "http://table:9000",
53+
"lance.storage.access_key_id", "table-ak");
54+
55+
Map<String, String> storageOptions =
56+
LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, tableProperties);
57+
58+
Assertions.assertEquals(3, storageOptions.size());
59+
Assertions.assertEquals("http://table:9000", storageOptions.get("endpoint"));
60+
Assertions.assertEquals("us-east-1", storageOptions.get("region"));
61+
Assertions.assertEquals("table-ak", storageOptions.get("access_key_id"));
62+
}
63+
}

lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ public class LanceRESTServiceIT extends BaseIT {
9595
private static final String CATALOG_NAME = GravitinoITUtils.genRandomName("lance_rest_catalog");
9696
private static final String SCHEMA_NAME = GravitinoITUtils.genRandomName("lance_rest_schema");
9797
private static final String DELIMITER = ".";
98+
private static final String MINIO_ENDPOINT = "http://127.0.0.1:9000";
99+
private static final String MINIO_REGION = "us-east-1";
100+
private static final String MINIO_ACCESS_KEY = "minioadmin";
101+
private static final String MINIO_SECRET_KEY = "minioadmin";
98102

99103
private GravitinoMetalake metalake;
100104
private Catalog catalog;
@@ -661,6 +665,43 @@ void testCreateTable() throws IOException {
661665
.contains("Column non_existing_column does not exist in the dataset"));
662666
}
663667

668+
@Test
669+
void testCreateTableUsesCatalogStorageOptions() throws IOException {
670+
catalog = createCatalog(GravitinoITUtils.genRandomName("lance_rest_catalog"));
671+
createSchema();
672+
673+
String location = tempDir + "/" + "catalog_storage_table/";
674+
List<String> ids = List.of(catalog.name(), SCHEMA_NAME, "catalog_storage_table");
675+
org.apache.arrow.vector.types.pojo.Schema schema =
676+
new org.apache.arrow.vector.types.pojo.Schema(
677+
Arrays.asList(
678+
Field.nullable("id", new ArrowType.Int(32, true)),
679+
Field.nullable("value", new ArrowType.Utf8())));
680+
byte[] body = ArrowUtils.generateIpcStream(schema);
681+
682+
CreateTableResponse response =
683+
createTable(ids, location, ImmutableMap.of("key1", "v1"), body, /* mode= */ null);
684+
Assertions.assertNotNull(response);
685+
Assertions.assertEquals(location, response.getLocation());
686+
Assertions.assertEquals(MINIO_ENDPOINT, response.getStorageOptions().get("endpoint"));
687+
Assertions.assertEquals("true", response.getStorageOptions().get("allow_http"));
688+
Assertions.assertEquals(MINIO_ACCESS_KEY, response.getStorageOptions().get("access_key_id"));
689+
Assertions.assertEquals(
690+
MINIO_SECRET_KEY, response.getStorageOptions().get("secret_access_key"));
691+
Assertions.assertEquals(MINIO_REGION, response.getStorageOptions().get("region"));
692+
693+
DescribeTableRequest describeTableRequest = new DescribeTableRequest();
694+
describeTableRequest.setId(ids);
695+
DescribeTableResponse loadTable = ns.describeTable(describeTableRequest);
696+
Assertions.assertNotNull(loadTable);
697+
Assertions.assertEquals(MINIO_ENDPOINT, loadTable.getStorageOptions().get("endpoint"));
698+
Assertions.assertEquals("true", loadTable.getStorageOptions().get("allow_http"));
699+
Assertions.assertEquals(MINIO_ACCESS_KEY, loadTable.getStorageOptions().get("access_key_id"));
700+
Assertions.assertEquals(
701+
MINIO_SECRET_KEY, loadTable.getStorageOptions().get("secret_access_key"));
702+
Assertions.assertEquals(MINIO_REGION, loadTable.getStorageOptions().get("region"));
703+
}
704+
664705
@Test
665706
void testAlterColumns() throws Exception {
666707
catalog = createCatalog(CATALOG_NAME);
@@ -1025,12 +1066,23 @@ private GravitinoMetalake createMetalake(String metalakeName) {
10251066
}
10261067

10271068
private Catalog createCatalog(String catalogName) {
1069+
Map<String, String> catalogProperties =
1070+
ImmutableMap.<String, String>builder()
1071+
.putAll(properties)
1072+
.put(Catalog.PROPERTY_LOCATION, tempDir.toString())
1073+
.put("lance.storage.endpoint", MINIO_ENDPOINT)
1074+
.put("lance.storage.allow_http", "true")
1075+
.put("lance.storage.access_key_id", MINIO_ACCESS_KEY)
1076+
.put("lance.storage.secret_access_key", MINIO_SECRET_KEY)
1077+
.put("lance.storage.region", MINIO_REGION)
1078+
.build();
1079+
10281080
return metalake.createCatalog(
10291081
catalogName,
10301082
Catalog.Type.RELATIONAL,
10311083
"lakehouse-generic",
10321084
"catalog for lance rest service tests",
1033-
properties);
1085+
catalogProperties);
10341086
}
10351087

10361088
private void createSchema() {

0 commit comments

Comments
 (0)