This is an automated email from the ASF dual-hosted git repository.
majin1102 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/amoro.git
The following commit(s) were added to refs/heads/master by this push:
new cc0ef839a [Hotfix] improve REST+Lance catalog correctness (#4341)
cc0ef839a is described below
commit cc0ef839a9efc3071c5db5e304efdabc8cd90948
Author: WenLingzhang <[email protected]>
AuthorDate: Tue Sep 8 11:57:28 2026 +0800
[Hotfix] improve REST+Lance catalog correctness (#4341)
* Enforce REST+Lance exclusivity on catalog metadata update
* Fetch all tables and databases across pages via pageToken in
LanceRestCatalog
* keep entered Iceberg storage/auth values when switching to Lance and back
* fixup style
---
.../amoro/server/catalog/CatalogBuilder.java | 19 ++++---
.../server/catalog/DefaultCatalogManager.java | 8 +++
.../catalog/TestDefaultCatalogManagerRecovery.java | 24 +++++++++
.../amoro/formats/lance/AbstractLanceCatalog.java | 18 ++-----
.../formats/lance/LanceDirectoryV1Catalog.java | 25 +++++++++
.../amoro/formats/lance/LanceRestCatalog.java | 60 +++++++++++++++++++---
amoro-web/src/views/catalogs/Detail.vue | 14 -----
7 files changed, 127 insertions(+), 41 deletions(-)
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 5c5594e56..408a8c34a 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
@@ -86,13 +86,7 @@ public class CatalogBuilder {
"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);
+ checkRestLanceExclusivity(type, tableFormats);
switch (type) {
case CATALOG_TYPE_HADOOP:
@@ -108,4 +102,15 @@ public class CatalogBuilder {
throw new IllegalStateException("unsupported catalog type:" + type);
}
}
+
+ /** Enforced on both catalog creation and metadata updates. */
+ static void checkRestLanceExclusivity(String type, Set<TableFormat>
tableFormats) {
+ 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);
+ }
}
diff --git
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
index 6535283da..2ea31ff2a 100644
---
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
+++
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
@@ -24,6 +24,7 @@ import static
org.apache.amoro.properties.CatalogMetaProperties.CLIENT_POOL_SIZE
import org.apache.amoro.AmoroTable;
import org.apache.amoro.Constants;
+import org.apache.amoro.TableFormat;
import org.apache.amoro.api.CatalogMeta;
import org.apache.amoro.config.Configurations;
import org.apache.amoro.exception.AlreadyExistsException;
@@ -49,6 +50,7 @@ import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@@ -273,6 +275,12 @@ public class DefaultCatalogManager extends PersistentBase
implements CatalogMana
if (!oldType.equals(newType)) {
throw new IllegalMetadataException("Cannot update catalog type");
}
+
+ // A REST catalog serves a single protocol per uri, so Lance cannot be
combined with other
+ // table formats. Reject the update before it is persisted.
+ String type = newMeta.getCatalogType();
+ Set<TableFormat> tableFormats = CatalogUtil.tableFormats(newMeta);
+ CatalogBuilder.checkRestLanceExclusivity(type, tableFormats);
}
private void disposeCatalog(String name) {
diff --git
a/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerRecovery.java
b/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerRecovery.java
index 453430309..b0b79bf43 100644
---
a/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerRecovery.java
+++
b/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerRecovery.java
@@ -124,4 +124,28 @@ public class TestDefaultCatalogManagerRecovery extends
AMSManagerTestBase {
"http://localhost:2/also-unreachable",
readMeta.getCatalogProperties().get("uri"));
Assert.assertEquals("new-value",
readMeta.getCatalogProperties().get("new-property"));
}
+
+ /**
+ * Verifies that updating a REST catalog to combine Lance with another table
format is rejected
+ * before the update is persisted — the same invariant CatalogBuilder
enforces at creation time.
+ */
+ @Test
+ public void testUpdateCatalogRejectsLanceWithOtherFormats() {
+ // use iceberg with REST catalog
+ DB.insertCatalog(buildUnreachableRestCatalog());
+ Assert.assertTrue(CATALOG_MANAGER.catalogExist(TEST_CATALOG_NAME));
+
+ // add lance format is invalid
+ CatalogMeta updatedMeta =
CATALOG_MANAGER.getCatalogMeta(TEST_CATALOG_NAME);
+
updatedMeta.getCatalogProperties().put(CatalogMetaProperties.TABLE_FORMATS,
"LANCE,ICEBERG");
+
+ Assert.assertThrows(
+ IllegalStateException.class, () ->
CATALOG_MANAGER.updateCatalog(updatedMeta));
+
+ // The invalid update must not be persisted
+ CatalogMeta readMeta = CATALOG_MANAGER.getCatalogMeta(TEST_CATALOG_NAME);
+ Assert.assertEquals(
+ TableFormat.ICEBERG.name(),
+
readMeta.getCatalogProperties().get(CatalogMetaProperties.TABLE_FORMATS));
+ }
}
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
index 8ba7e1fc0..420299c3a 100644
---
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
@@ -27,11 +27,8 @@ 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;
@@ -91,17 +88,10 @@ public abstract class AbstractLanceCatalog implements
FormatCatalog {
}
@Override
- public List<String> 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());
- }
+ public abstract List<String> listTables(String database);
+
+ @Override
+ public abstract List<String> listDatabases();
protected void validateDatabase(String database) {
if (!databaseExists(database)) {
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 19f8f2b08..fcabb8d71 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
@@ -24,7 +24,10 @@ import org.apache.arrow.util.Preconditions;
import org.apache.iceberg.aliyun.AliyunProperties;
import org.apache.iceberg.aws.s3.S3FileIOProperties;
import org.lance.namespace.LanceNamespace;
+import org.lance.namespace.model.ListTablesRequest;
+import org.lance.namespace.model.ListTablesResponse;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -110,6 +113,28 @@ public class LanceDirectoryV1Catalog extends
AbstractLanceCatalog {
throw new UnsupportedOperationException("Dropping Lance databases is not
supported.");
}
+ @Override
+ public List<String> listTables(String database) {
+ if (!databaseExists(database)) {
+ return Collections.emptyList();
+ }
+ return listTablesFromNamespace();
+ }
+
+ private List<String> 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 static String removeFirstNonNull(Map<String, String> properties,
String... keys) {
String value = null;
for (String key : keys) {
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
index fdf237c12..c665f16f1 100644
---
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
@@ -21,6 +21,7 @@ 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.apache.commons.lang3.StringUtils;
import org.lance.namespace.LanceNamespace;
import org.lance.namespace.errors.NamespaceAlreadyExistsException;
import org.lance.namespace.errors.NamespaceNotFoundException;
@@ -28,13 +29,17 @@ 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.ListTablesRequest;
+import org.lance.namespace.model.ListTablesResponse;
import org.lance.namespace.model.NamespaceExistsRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* REST catalog implementation for Lance.
@@ -95,14 +100,57 @@ public class LanceRestCatalog extends AbstractLanceCatalog
{
return id;
}
+ /**
+ * Lists the tables of a database. The REST service pages results via
pageToken, so follow the
+ * token until it is empty to avoid silently missing tables on large
catalogs.
+ */
+ @Override
+ public List<String> listTables(String database) {
+ validateDatabase(database);
+
+ Set<String> tables = new LinkedHashSet<>();
+ String pageToken = null;
+ do {
+ ListTablesRequest request = new
ListTablesRequest().id(tableIdForListTables(database));
+ if (pageToken != null) {
+ request.pageToken(pageToken);
+ }
+ ListTablesResponse response = namespace.listTables(request);
+ if (response == null) {
+ break;
+ }
+ if (response.getTables() != null) {
+ tables.addAll(response.getTables());
+ }
+
+ pageToken = response.getPageToken();
+ } while (StringUtils.isNotEmpty(pageToken));
+
+ return new ArrayList<>(tables);
+ }
+
+ /** Lists the schemas under the catalog, following the REST pageToken until
it is empty. */
@Override
public List<String> listDatabases() {
- ListNamespacesRequest request = new
ListNamespacesRequest().id(catalogPrefix);
- ListNamespacesResponse response = namespace.listNamespaces(request);
- if (response == null) {
- return Collections.emptyList();
- }
- return new ArrayList<>(response.getNamespaces());
+ Set<String> namespaces = new LinkedHashSet<>();
+ String pageToken = null;
+ do {
+ ListNamespacesRequest request = new
ListNamespacesRequest().id(catalogPrefix);
+ if (pageToken != null) {
+ request.pageToken(pageToken);
+ }
+ ListNamespacesResponse response = namespace.listNamespaces(request);
+ if (response == null) {
+ break;
+ }
+ if (response.getNamespaces() != null) {
+ namespaces.addAll(response.getNamespaces());
+ }
+
+ pageToken = response.getPageToken();
+ } while (StringUtils.isNotEmpty(pageToken));
+
+ return new ArrayList<>(namespaces);
}
@Override
diff --git a/amoro-web/src/views/catalogs/Detail.vue
b/amoro-web/src/views/catalogs/Detail.vue
index 6f78999be..ce6bd5573 100644
--- a/amoro-web/src/views/catalogs/Detail.vue
+++ b/amoro-web/src/views/catalogs/Detail.vue
@@ -436,20 +436,6 @@ watch(
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'
}
},
)