diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java index af39ba5330..5c5594e56e 100644 --- a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java +++ b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java @@ -59,7 +59,7 @@ public class CatalogBuilder { CATALOG_TYPE_GLUE, Sets.newHashSet(TableFormat.ICEBERG, TableFormat.MIXED_ICEBERG), CATALOG_TYPE_REST, - Sets.newHashSet(TableFormat.ICEBERG, TableFormat.MIXED_ICEBERG), + Sets.newHashSet(TableFormat.ICEBERG, TableFormat.MIXED_ICEBERG, TableFormat.LANCE), CATALOG_TYPE_CUSTOM, Sets.newHashSet(TableFormat.ICEBERG, TableFormat.MIXED_ICEBERG), CATALOG_TYPE_HIVE, @@ -86,6 +86,13 @@ public static ServerCatalog buildServerCatalog( "Table format %s is not supported for metastore type: %s", tableFormats, type); + Preconditions.checkState( + !(CATALOG_TYPE_REST.equals(type) + && tableFormats.contains(TableFormat.LANCE) + && tableFormats.size() > 1), + "REST catalog serves a single protocol per uri," + + " Lance cannot be combined with other table formats: %s", + tableFormats); switch (type) { case CATALOG_TYPE_HADOOP: diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/CatalogController.java b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/CatalogController.java index 172b2a0c8f..b7dcd5651b 100644 --- a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/CatalogController.java +++ b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/CatalogController.java @@ -208,6 +208,15 @@ public class CatalogController { CatalogDescriptor.of(CATALOG_TYPE_REST, STORAGE_CONFIGS_VALUE_TYPE_OSS, ICEBERG)); VALIDATE_CATALOGS.add( CatalogDescriptor.of(CATALOG_TYPE_REST, STORAGE_CONFIGS_VALUE_TYPE_OSS, MIXED_ICEBERG)); + + // Lance REST is a data proxy: the client never talks to object storage, so the storage config + // is not consumed, but it still reuses the standard REST storage types. + VALIDATE_CATALOGS.add( + CatalogDescriptor.of(CATALOG_TYPE_REST, STORAGE_CONFIGS_VALUE_TYPE_S3, LANCE)); + VALIDATE_CATALOGS.add( + CatalogDescriptor.of(CATALOG_TYPE_REST, STORAGE_CONFIGS_VALUE_TYPE_HADOOP, LANCE)); + VALIDATE_CATALOGS.add( + CatalogDescriptor.of(CATALOG_TYPE_REST, STORAGE_CONFIGS_VALUE_TYPE_OSS, LANCE)); } private final PlatformFileManager platformFileInfoService; @@ -473,8 +482,11 @@ private CatalogMeta constructCatalogMeta(CatalogRegisterInfo info, CatalogMeta o catalogMeta.putToCatalogProperties( CatalogProperties.CATALOG_IMPL, GlueCatalog.class.getName()); } else if (CatalogMetaProperties.CATALOG_TYPE_REST.equals(metastoreType)) { - catalogMeta.putToCatalogProperties( - CatalogProperties.CATALOG_IMPL, RESTCatalog.class.getName()); + // Only the Iceberg-family formats need the Iceberg RESTCatalog implementation. + if (!info.getTableFormatList().contains(LANCE.name())) { + catalogMeta.putToCatalogProperties( + CatalogProperties.CATALOG_IMPL, RESTCatalog.class.getName()); + } } catalogMeta.putToCatalogProperties( diff --git a/amoro-common/src/main/java/org/apache/amoro/utils/CatalogUtil.java b/amoro-common/src/main/java/org/apache/amoro/utils/CatalogUtil.java index e35fbc65a8..46285b9d0d 100644 --- a/amoro-common/src/main/java/org/apache/amoro/utils/CatalogUtil.java +++ b/amoro-common/src/main/java/org/apache/amoro/utils/CatalogUtil.java @@ -121,6 +121,13 @@ public static Map mergeCatalogPropertiesToTable( /** Build {@link TableMetaStore} from catalog meta. */ public static TableMetaStore buildMetaStore(CatalogMeta catalogMeta) { + // REST + Lance manages storage/auth server-side, so a client-side TableMetaStore is + // unnecessary. Set a local configuration so build() returns a disabled-auth store. + if (CatalogMetaProperties.CATALOG_TYPE_REST.equalsIgnoreCase(catalogMeta.getCatalogType()) + && isLanceOnly(catalogMeta)) { + return TableMetaStore.builder().withConfiguration(new Configuration()).build(); + } + // load storage configs TableMetaStore.Builder builder = TableMetaStore.builder(); boolean isLocalStorage = false; @@ -215,6 +222,11 @@ public static TableMetaStore buildMetaStore(CatalogMeta catalogMeta) { return builder.build(); } + private static boolean isLanceOnly(CatalogMeta catalogMeta) { + Set formats = tableFormats(catalogMeta); + return formats.size() == 1 && formats.contains(TableFormat.LANCE); + } + public static TableIdentifier tableId(TableMeta tableMeta) { return TableIdentifier.of( tableMeta.getTableIdentifier().getCatalog(), diff --git a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/AbstractLanceCatalog.java b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/AbstractLanceCatalog.java new file mode 100644 index 0000000000..8ba7e1fc04 --- /dev/null +++ b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/AbstractLanceCatalog.java @@ -0,0 +1,111 @@ +/* + * 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.amoro.formats.lance; + +import org.apache.amoro.AmoroTable; +import org.apache.amoro.FormatCatalog; +import org.apache.amoro.NoSuchDatabaseException; +import org.apache.amoro.NoSuchTableException; +import org.apache.amoro.table.TableIdentifier; +import org.lance.Dataset; +import org.lance.namespace.LanceNamespace; +import org.lance.namespace.errors.TableNotFoundException; +import org.lance.namespace.model.DropTableRequest; +import org.lance.namespace.model.ListTablesRequest; +import org.lance.namespace.model.ListTablesResponse; +import org.lance.namespace.model.TableExistsRequest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Base implementation of {@link FormatCatalog} for Lance. */ +public abstract class AbstractLanceCatalog implements FormatCatalog { + + protected final String catalogName; + protected final LanceNamespace namespace; + + protected AbstractLanceCatalog(String catalogName, LanceNamespace namespace) { + this.catalogName = catalogName; + this.namespace = namespace; + } + + /** Composes the namespace table id used to reference a single table. */ + protected abstract List tableId(String database, String table); + + /** Composes the namespace id used to list tables of a database. */ + protected abstract List tableIdForListTables(String database); + + @Override + public boolean tableExists(String database, String table) { + if (!databaseExists(database)) { + return false; + } + + try { + namespace.tableExists(new TableExistsRequest().id(tableId(database, table))); + return true; + } catch (TableNotFoundException e) { + return false; + } + } + + @Override + public AmoroTable loadTable(String database, String tableName) { + if (!tableExists(database, tableName)) { + throw new NoSuchTableException("Table: " + database + "." + tableName + " does not exist"); + } + + TableIdentifier identifier = TableIdentifier.of(catalogName, database, tableName); + Dataset dataset = + Dataset.open().namespaceClient(namespace).tableId(tableId(database, tableName)).build(); + return new LanceTable(identifier, dataset, Collections.emptyMap()); + } + + @Override + public boolean dropTable(String database, String table, boolean purge) { + validateDatabase(database); + + try { + namespace.dropTable(new DropTableRequest().id(tableId(database, table))); + return true; + } catch (TableNotFoundException e) { + return false; + } + } + + @Override + public List listTables(String database) { + if (!databaseExists(database)) { + return Collections.emptyList(); + } + ListTablesRequest request = new ListTablesRequest().id(tableIdForListTables(database)); + ListTablesResponse response = namespace.listTables(request); + if (response == null) { + return Collections.emptyList(); + } + return new ArrayList<>(response.getTables()); + } + + protected void validateDatabase(String database) { + if (!databaseExists(database)) { + throw new NoSuchDatabaseException("Database: " + database + " does not exist"); + } + } +} diff --git a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java index a78fa1ae35..a3faa8b2de 100755 --- a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java +++ b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java @@ -21,6 +21,7 @@ import org.apache.amoro.FormatCatalog; import org.apache.amoro.FormatCatalogFactory; import org.apache.amoro.TableFormat; +import org.apache.amoro.properties.CatalogMetaProperties; import org.apache.amoro.table.TableMetaStore; import java.util.HashMap; @@ -38,6 +39,9 @@ public FormatCatalog create( String metastoreType, Map properties, TableMetaStore metaStore) { + if (CatalogMetaProperties.CATALOG_TYPE_REST.equals(metastoreType)) { + return new LanceRestCatalog(catalogName, properties); + } return new LanceDirectoryV1Catalog(catalogName, properties); } diff --git a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceDirectoryV1Catalog.java b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceDirectoryV1Catalog.java index 926d91ec85..19f8f2b085 100755 --- a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceDirectoryV1Catalog.java +++ b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceDirectoryV1Catalog.java @@ -18,26 +18,13 @@ package org.apache.amoro.formats.lance; -import org.apache.amoro.AmoroTable; -import org.apache.amoro.FormatCatalog; -import org.apache.amoro.NoSuchDatabaseException; -import org.apache.amoro.NoSuchTableException; import org.apache.amoro.properties.CatalogMetaProperties; -import org.apache.amoro.table.TableIdentifier; -import org.apache.arrow.memory.BufferAllocator; import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.util.Preconditions; import org.apache.iceberg.aliyun.AliyunProperties; import org.apache.iceberg.aws.s3.S3FileIOProperties; -import org.lance.Dataset; import org.lance.namespace.LanceNamespace; -import org.lance.namespace.errors.TableNotFoundException; -import org.lance.namespace.model.DropTableRequest; -import org.lance.namespace.model.ListTablesRequest; -import org.lance.namespace.model.ListTablesResponse; -import org.lance.namespace.model.TableExistsRequest; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -50,22 +37,22 @@ * root directory, each immediate subdirectory whose name ends with ".lance" is treated as a Lance * dataset. All tables live in a single logical database named "default". */ -public class LanceDirectoryV1Catalog implements FormatCatalog { +public class LanceDirectoryV1Catalog extends AbstractLanceCatalog { private static final String DEFAULT_DATABASE = "default"; private static final String STORAGE_ACCESS_KEY_ID = "storage.access_key_id"; private static final String STORAGE_SECRET_ACCESS_KEY = "storage.secret_access_key"; private static final String STORAGE_ENDPOINT = "storage.endpoint"; - private final String catalogName; - private final Map namespaceProperties; - private final LanceNamespace namespace; public LanceDirectoryV1Catalog(String catalogName, Map catalogProperties) { + super(catalogName, buildNamespace(catalogProperties)); + } + + private static LanceNamespace buildNamespace(Map catalogProperties) { Preconditions.checkArgument( catalogProperties != null && !catalogProperties.isEmpty(), "Catalog properties must be set."); - this.catalogName = catalogName; - this.namespaceProperties = new HashMap<>(catalogProperties); + Map namespaceProperties = new HashMap<>(catalogProperties); String root = namespaceProperties.remove(CatalogMetaProperties.KEY_WAREHOUSE); String storageAccessKey = removeFirstNonNull( @@ -80,42 +67,37 @@ public LanceDirectoryV1Catalog(String catalogName, Map catalogPr Preconditions.checkArgument( root != null && !root.isEmpty(), "Warehouse must be set in catalogProperties."); - this.namespaceProperties.put("manifest_enabled", "false"); - this.namespaceProperties.put("vend_input_storage_options", "true"); - this.namespaceProperties.put("root", root); + namespaceProperties.put("manifest_enabled", "false"); + namespaceProperties.put("vend_input_storage_options", "true"); + namespaceProperties.put("root", root); if (storageAccessKey != null) { - this.namespaceProperties.put(STORAGE_ACCESS_KEY_ID, storageAccessKey); + namespaceProperties.put(STORAGE_ACCESS_KEY_ID, storageAccessKey); } if (storageSecretKey != null) { - this.namespaceProperties.put(STORAGE_SECRET_ACCESS_KEY, storageSecretKey); + namespaceProperties.put(STORAGE_SECRET_ACCESS_KEY, storageSecretKey); } putStorageEndpointIfPresent(namespaceProperties); - this.namespace = initializeNamespace(new RootAllocator(Long.MAX_VALUE)); + return LanceNamespace.connect("dir", namespaceProperties, new RootAllocator(Long.MAX_VALUE)); } @Override - public List listDatabases() { - return Collections.singletonList(DEFAULT_DATABASE); + protected List tableId(String database, String table) { + return Collections.singletonList(table); } @Override - public boolean databaseExists(String database) { - return DEFAULT_DATABASE.equals(database); + protected List tableIdForListTables(String database) { + return Collections.emptyList(); } @Override - public boolean tableExists(String database, String table) { - if (!databaseExists(database)) { - return false; - } + public List listDatabases() { + return Collections.singletonList(DEFAULT_DATABASE); + } - try { - TableExistsRequest request = new TableExistsRequest().id(Collections.singletonList(table)); - namespace.tableExists(request); - return true; - } catch (TableNotFoundException e) { - return false; - } + @Override + public boolean databaseExists(String database) { + return DEFAULT_DATABASE.equals(database); } @Override @@ -128,59 +110,6 @@ public void dropDatabase(String database) { throw new UnsupportedOperationException("Dropping Lance databases is not supported."); } - @Override - public AmoroTable loadTable(String database, String tableName) { - if (!databaseExists(database) || !tableExists(database, tableName)) { - throw new NoSuchTableException("Table: " + database + "." + tableName + " does not exist"); - } - - TableIdentifier identifier = TableIdentifier.of(catalogName, database, tableName); - Dataset dataset = - Dataset.open() - .namespaceClient(namespace) - .tableId(Collections.singletonList(tableName)) - .build(); - return new LanceTable(identifier, dataset, Collections.emptyMap()); - } - - @Override - public boolean dropTable(String database, String table, boolean purge) { - validateDatabase(database); - - try { - namespace.dropTable(new DropTableRequest().id(Collections.singletonList(table))); - return true; - } catch (TableNotFoundException e) { - return false; - } - } - - @Override - public List listTables(String database) { - if (!databaseExists(database)) { - return Collections.emptyList(); - } - return listTablesFromNamespace(); - } - - private List listTablesFromNamespace() { - if (namespace == null) { - return Collections.emptyList(); - } - - ListTablesRequest request = new ListTablesRequest().id(Collections.emptyList()); - ListTablesResponse response = namespace.listTables(request); - if (response == null) { - return Collections.emptyList(); - } - - return new ArrayList<>(response.getTables()); - } - - private LanceNamespace initializeNamespace(BufferAllocator allocator) { - return LanceNamespace.connect("dir", namespaceProperties, allocator); - } - private static String removeFirstNonNull(Map properties, String... keys) { String value = null; for (String key : keys) { @@ -205,10 +134,4 @@ private static void putStorageEndpointIfPresent(Map properties) properties.put(STORAGE_ENDPOINT, endpoint); } } - - private void validateDatabase(String database) { - if (!databaseExists(database)) { - throw new NoSuchDatabaseException("Database: " + database + " does not exist"); - } - } } diff --git a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceRestCatalog.java b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceRestCatalog.java new file mode 100644 index 0000000000..fdf237c127 --- /dev/null +++ b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceRestCatalog.java @@ -0,0 +1,134 @@ +/* + * 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.amoro.formats.lance; + +import org.apache.amoro.AlreadyExistsException; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.util.Preconditions; +import org.lance.namespace.LanceNamespace; +import org.lance.namespace.errors.NamespaceAlreadyExistsException; +import org.lance.namespace.errors.NamespaceNotFoundException; +import org.lance.namespace.model.CreateNamespaceRequest; +import org.lance.namespace.model.DropNamespaceRequest; +import org.lance.namespace.model.ListNamespacesRequest; +import org.lance.namespace.model.ListNamespacesResponse; +import org.lance.namespace.model.NamespaceExistsRequest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * REST catalog implementation for Lance. + * + *

This catalog connects to a Lance Namespace REST service via {@code + * LanceNamespace.connect("rest", ...)} and maps the REST namespace hierarchy to Amoro's + * database/table model. + */ +public class LanceRestCatalog extends AbstractLanceCatalog { + + private static final String URI_PROPERTY = "uri"; + + /** + * Optional catalog name prepended to the REST namespace id, mapping Amoro's database/table onto + * the service's schema/table levels (catalog/schema/table hierarchy). Empty when the service has + * no catalog level. + */ + private static final String CATALOG_PROPERTY = "catalog-name"; + + private final List catalogPrefix; + + public LanceRestCatalog(String catalogName, Map catalogProperties) { + super(catalogName, buildNamespace(catalogProperties)); + this.catalogPrefix = catalogPrefix(catalogProperties); + } + + private static List catalogPrefix(Map catalogProperties) { + String catalog = catalogProperties.get(CATALOG_PROPERTY); + return catalog == null || catalog.isEmpty() + ? Collections.emptyList() + : Collections.singletonList(catalog); + } + + private static LanceNamespace buildNamespace(Map namespaceProperties) { + Preconditions.checkArgument( + namespaceProperties != null && !namespaceProperties.isEmpty(), + "Catalog properties must be set."); + Preconditions.checkArgument( + namespaceProperties.containsKey(URI_PROPERTY), + "URI must be set in namespaceProperties for REST metastore type."); + Map restProperties = new HashMap<>(namespaceProperties); + return LanceNamespace.connect("rest", restProperties, new RootAllocator(Long.MAX_VALUE)); + } + + @Override + protected List tableId(String database, String table) { + return concat(catalogPrefix, database, table); + } + + @Override + protected List tableIdForListTables(String database) { + return concat(catalogPrefix, database); + } + + private static List concat(List prefix, String... parts) { + List id = new ArrayList<>(prefix); + Collections.addAll(id, parts); + return id; + } + + @Override + public List listDatabases() { + ListNamespacesRequest request = new ListNamespacesRequest().id(catalogPrefix); + ListNamespacesResponse response = namespace.listNamespaces(request); + if (response == null) { + return Collections.emptyList(); + } + return new ArrayList<>(response.getNamespaces()); + } + + @Override + public boolean databaseExists(String database) { + try { + NamespaceExistsRequest request = + new NamespaceExistsRequest().id(concat(catalogPrefix, database)); + namespace.namespaceExists(request); + return true; + } catch (NamespaceNotFoundException e) { + return false; + } + } + + @Override + public void createDatabase(String database) { + try { + namespace.createNamespace(new CreateNamespaceRequest().id(concat(catalogPrefix, database))); + } catch (NamespaceAlreadyExistsException e) { + throw new AlreadyExistsException("Database: " + database + " already exists", e); + } + } + + @Override + public void dropDatabase(String database) { + validateDatabase(database); + namespace.dropNamespace(new DropNamespaceRequest().id(concat(catalogPrefix, database))); + } +} diff --git a/amoro-web/src/views/catalogs/Detail.vue b/amoro-web/src/views/catalogs/Detail.vue index e088b2f395..6f78999be1 100644 --- a/amoro-web/src/views/catalogs/Detail.vue +++ b/amoro-web/src/views/catalogs/Detail.vue @@ -119,6 +119,7 @@ const tableFormatMap = { MIXED_ICEBERG: 'MIXED_ICEBERG', PAIMON: 'PAIMON', HUDI: 'HUDI', + LANCE: 'LANCE', } const tableFormatText = { @@ -127,6 +128,7 @@ const tableFormatText = { [tableFormatMap.MIXED_ICEBERG]: 'Mixed Iceberg', [tableFormatMap.PAIMON]: 'Paimon', [tableFormatMap.HUDI]: 'Hudi', + [tableFormatMap.LANCE]: 'Lance', } const storageConfigFileNameMap = { @@ -248,7 +250,12 @@ async function loadMetastoreCapabilities(loadTableFormats: boolean) { const options = (formats || []) as string[] tableFormatOptions.value = options if (loadTableFormats) { - formState.tableFormatList = [...options] + // REST catalog: Lance and Iceberg use different protocols (one protocol per uri), + // so they can't coexist. Default to Iceberg family; Lance stays unchecked (opt-in). + const defaultFormats = type === 'rest' + ? options.filter(format => format !== tableFormatMap.LANCE) + : options + formState.tableFormatList = defaultFormats.length > 0 ? [...defaultFormats] : [...options] } const storageTypes = await getMetastoreStorageTypes(type) storageTypeOptions.value = (storageTypes || []).map((val: string) => ({ @@ -394,6 +401,11 @@ const authTypeOptions = computed(() => { const isAuthDisabled = computed(() => formState.storageConfig['storage.type'] === 'Local') +// REST + Lance needs no client-side object storage/auth; storage is server-managed. +const isRestLance = computed(() => { + return formState.catalog.type === 'rest' && formState.tableFormatList.includes(tableFormatMap.LANCE) +}) + watch( () => formState.storageConfig['storage.type'], (storageType) => { @@ -409,7 +421,37 @@ watch( formState.authConfig[simpleUsernameKey] = 'local' } } - } + }, +) + +watch( + () => formState.tableFormatList, + (formats, oldFormats) => { + const isRest = formState.catalog.type === 'rest' + const hasLance = formats.includes(tableFormatMap.LANCE) + + if (isRest && hasLance && formats.length > 1) { + // If Lance was just added, keep only Lance; otherwise the user switched away from Lance. + const hadLance = oldFormats?.includes(tableFormatMap.LANCE) + formState.tableFormatList = hadLance + ? formats.filter(format => format !== tableFormatMap.LANCE) + : [tableFormatMap.LANCE] + return + } + + // REST + Lance needs no client-side storage/auth; drop them on selection . + if (isRest && hasLance) { + Object.keys(formState.storageConfig).forEach((key) => { + delete formState.storageConfig[key] + }) + Object.keys(formState.authConfig).forEach((key) => { + delete formState.authConfig[key] + }) + } + else if (isRest && !formState.storageConfig['storage.type']) { + formState.storageConfig['storage.type'] = 'S3' + } + }, ) async function changeMetastore() { @@ -488,7 +530,7 @@ function handleSave() { formRef.value .validateFields() .then(async () => { - const { catalog, tableFormatList, storageConfig, authConfig } = formState + const { catalog, tableFormatList } = formState const properties = await propertiesRef.value.getProperties() const tableProperties = await tablePropertiesRef.value.getProperties() if (!properties) { @@ -500,6 +542,14 @@ function handleSave() { loading.value = true const catalogParams = catalog getFileIdParams() + let storageConfig = formState.storageConfig + let authConfig = formState.authConfig + if (isRestLance.value) { + // Lance REST manages storage server-side; send a neutral S3 storage type + // (accepted by the backend REST+S3+LANCE whitelist) and no auth config. + storageConfig = { 'storage.type': 'S3' } + authConfig = {} + } await saveCatalogsSetting({ isCreate: isNewCatalog.value, ...catalogParams, @@ -641,126 +691,128 @@ onMounted(() => { /> {{ formState.catalog.optimizerGroup }} - -

- {{ $t('storageConfigName') }} -

- - - - {{ formState.storageConfig['storage.type'] }} - - - - {{ formState.storageConfig['storage.s3.endpoint'] }} - - - - {{ formState.storageConfig['storage.s3.region'] }} - - + +

+ {{ $t('storageConfigName') }} +

+
+ + + {{ formState.storageConfig['storage.type'] }} + + + + {{ formState.storageConfig['storage.s3.endpoint'] }} + + + + {{ formState.storageConfig['storage.s3.region'] }} + + - - {{ formState.storageConfig['storage.oss.endpoint'] }} - -
- - + {{ formState.storageConfig['storage.oss.endpoint'] }} + +
+ - - {{ $t('upload') - }} - - - {{ config.fileName - }} + + + {{ $t('upload') + }} + + + {{ config.fileName + }} + +
+ +

+ {{ $t('authenticationConfig') }} +

+
+ + + {{ formState.authConfig['auth.type'] }} -
- -

- {{ $t('authenticationConfig') }} -

-
- - - {{ formState.authConfig['auth.type'] }} - - - - {{ formState.authConfig['auth.simple.hadoop_username'] }} - - - - {{ formState.authConfig['auth.kerberos.principal'] }} - -
- + {{ formState.authConfig['auth.simple.hadoop_username'] }} + + + + {{ formState.authConfig['auth.kerberos.principal'] }} + +
+ - - {{ $t('upload') - }} - - - {{ config.fileName }} + + + {{ $t('upload') + }} + + + {{ config.fileName }} + +
+ + + {{ formState.authConfig['auth.ak_sk.access_key'] }} -
- - - {{ formState.authConfig['auth.ak_sk.access_key'] }} - - - - {{ formState.authConfig['auth.ak_sk.secret_key'] }} - + + + {{ formState.authConfig['auth.ak_sk.secret_key'] }} + +

{{ $t('properties') }}