This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 0684d44383 [core] Skip snapshot loader for catalogs without version
management (#10594)
0684d44383 is described below
commit 0684d44383ac783a2f88b935f80e4320497e88a0
Author: junmuz <[email protected]>
AuthorDate: Fri Oct 16 06:07:41 2026 +0100
[core] Skip snapshot loader for catalogs without version management (#10594)
---
.../apache/paimon/table/CatalogEnvironment.java | 4 +-
.../jdbc/JdbcCatalogExecutorConnectionsTest.java | 318 +++++++++++++++++++++
.../org/apache/paimon/jdbc/JdbcCatalogTest.java | 28 ++
.../paimon/table/CatalogEnvironmentTest.java | 17 ++
4 files changed, 366 insertions(+), 1 deletion(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
index 9b575bc203..5e85a884ec 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
@@ -220,7 +220,9 @@ public class CatalogEnvironment implements Serializable {
@Nullable
public SnapshotLoader snapshotLoader() {
- if (catalogLoader == null) {
+ // Catalogs without version management cannot load snapshots, avoid
loading the catalog
+ // only to fall back to the file system.
+ if (catalogLoader == null || !supportsVersionManagement) {
return null;
}
return new SnapshotLoaderImpl(catalogLoader, identifier, uuid);
diff --git
a/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogExecutorConnectionsTest.java
b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogExecutorConnectionsTest.java
new file mode 100644
index 0000000000..61f2e1dea1
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogExecutorConnectionsTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.paimon.jdbc;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.CatalogFactory;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.index.HashBucketAssigner;
+import org.apache.paimon.options.CatalogOptions;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.table.source.ReadBuilder;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.utils.InstantiationUtil;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.Serializable;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.DriverPropertyInfo;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Simulates Spark executors working on tables of a {@link JdbcCatalog} and
counts the physical JDBC
+ * connections they open.
+ *
+ * <p>The driver creates the table and commits data, then table objects are
shipped to the
+ * "executor" through Java serialization, like a Spark task closure. Resetting
{@link
+ * CachedJdbcClientPool} beforehand stands in for a fresh executor JVM, so
every connection opened
+ * afterwards is attributed to executor-side work.
+ */
+public class JdbcCatalogExecutorConnectionsTest {
+
+ private static final String DATABASE = "db";
+
+ @TempDir Path tempDir;
+
+ private Catalog driverCatalog;
+
+ @BeforeAll
+ static void registerDriver() throws SQLException {
+ DriverManager.registerDriver(new CountingDriver());
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ Options options = new Options();
+ options.set(CatalogOptions.METASTORE, "jdbc");
+ options.set(CatalogOptions.URI, "jdbc:counting:sqlite:" +
tempDir.resolve("catalog.db"));
+ options.set(CatalogOptions.WAREHOUSE,
tempDir.resolve("warehouse").toUri().toString());
+ options.set(JdbcCatalogOptions.CATALOG_KEY, "executor-test");
+ options.set(JdbcCatalogOptions.LOCK_ENABLED, false);
+ driverCatalog =
CatalogFactory.createCatalog(CatalogContext.create(options));
+ driverCatalog.createDatabase(DATABASE, false);
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ driverCatalog.close();
+ CachedJdbcClientPool.resetCache();
+ }
+
+ @Test
+ public void testFixedBucketWriteOnExecutor() throws Exception {
+ FileStoreTable table = createFixedBucketTable();
+ commitOnDriver(table, row(1, 1, "a"), row(1, 2, "b"));
+
+ BatchWriteBuilder writeBuilder =
shipToExecutor(table.newBatchWriteBuilder());
+ try (BatchTableWrite write = writeBuilder.newWrite()) {
+ // same partition and buckets as the driver commit, so the writer
restores files
+ write.write(row(1, 1, "c"));
+ write.write(row(1, 2, "d"));
+ write.write(row(2, 3, "e"));
+ write.prepareCommit();
+ }
+
+ assertNoExecutorConnections();
+ }
+
+ @Test
+ public void testFixedBucketWriteWithFullCompactionOnExecutor() throws
Exception {
+ FileStoreTable table = createFixedBucketTable();
+ commitOnDriver(table, row(1, 1, "a"));
+ commitOnDriver(table, row(1, 1, "b"));
+
+ BatchWriteBuilder writeBuilder =
shipToExecutor(table.newBatchWriteBuilder());
+ try (BatchTableWrite write = writeBuilder.newWrite()) {
+ write.write(row(1, 1, "c"));
+ write.compact(BinaryRow.singleColumn(1), bucketOf(table, 1, 1),
true);
+ write.prepareCommit();
+ }
+
+ assertNoExecutorConnections();
+ }
+
+ @Test
+ public void testReadSplitsOnExecutor() throws Exception {
+ FileStoreTable table = createFixedBucketTable();
+ commitOnDriver(table, row(1, 1, "a"), row(2, 2, "b"));
+
+ ReadBuilder driverReadBuilder = table.newReadBuilder();
+ List<Split> driverSplits = driverReadBuilder.newScan().plan().splits();
+
+ ReadBuilder readBuilder = shipToExecutor(driverReadBuilder);
+ List<Split> splits = new ArrayList<>();
+ for (Split split : driverSplits) {
+ splits.add(InstantiationUtil.clone(split));
+ }
+ List<InternalRow> rows = new ArrayList<>();
+ try (RecordReader<InternalRow> reader =
readBuilder.newRead().createReader(splits)) {
+ reader.forEachRemaining(rows::add);
+ }
+
+ assertThat(rows).hasSize(2);
+ assertNoExecutorConnections();
+ }
+
+ @Test
+ public void testScanPlanOnExecutor() throws Exception {
+ FileStoreTable table = createFixedBucketTable();
+ commitOnDriver(table, row(1, 1, "a"));
+
+ ReadBuilder readBuilder = shipToExecutor(table.newReadBuilder());
+ assertThat(readBuilder.newScan().plan().splits()).isNotEmpty();
+
+ assertNoExecutorConnections();
+ }
+
+ @Test
+ public void testDynamicBucketAssignOnExecutor() throws Exception {
+ FileStoreTable table =
+ createTable(
+ "dynamic_bucket",
+ CoreOptions.BUCKET.key(),
+ "-1",
+ CoreOptions.CHANGELOG_PRODUCER.key(),
+ "input");
+
+ FileStoreTable executorTable = shipToExecutor(table);
+ HashBucketAssigner assigner =
+ new HashBucketAssigner(
+ executorTable.snapshotManager(),
+ UUID.randomUUID().toString(),
+ executorTable.store().newIndexFileHandler(),
+ 1,
+ 1,
+ 0,
+
executorTable.coreOptions().dynamicBucketTargetRowNum(),
+ executorTable.coreOptions().dynamicBucketMaxBuckets());
+ assigner.assign(BinaryRow.singleColumn(1), 42);
+
+ assertNoExecutorConnections();
+ }
+
+ private FileStoreTable createFixedBucketTable() throws Exception {
+ // mirrors the reported table: fixed buckets, input changelog,
compaction in writers
+ return createTable(
+ "fixed_bucket",
+ CoreOptions.BUCKET.key(),
+ "100",
+ CoreOptions.CHANGELOG_PRODUCER.key(),
+ "input",
+ CoreOptions.WRITE_ONLY.key(),
+ "false",
+ CoreOptions.CONSUMER_EXPIRATION_TIME.key(),
+ "7d",
+ CoreOptions.SNAPSHOT_TIME_RETAINED.key(),
+ "7d");
+ }
+
+ private FileStoreTable createTable(String name, String... options) throws
Exception {
+ Schema.Builder schema =
+ Schema.newBuilder()
+ .column("pt", DataTypes.INT())
+ .column("id", DataTypes.INT())
+ .column("v", DataTypes.STRING())
+ .partitionKeys("pt")
+ .primaryKey("pt", "id");
+ for (int i = 0; i < options.length; i += 2) {
+ schema.option(options[i], options[i + 1]);
+ }
+ Identifier identifier = Identifier.create(DATABASE, name);
+ driverCatalog.createTable(identifier, schema.build(), false);
+ return (FileStoreTable) driverCatalog.getTable(identifier);
+ }
+
+ private void commitOnDriver(FileStoreTable table, InternalRow... rows)
throws Exception {
+ BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write = writeBuilder.newWrite();
+ BatchTableCommit commit = writeBuilder.newCommit()) {
+ for (InternalRow row : rows) {
+ write.write(row);
+ }
+ commit.commit(write.prepareCommit());
+ }
+ }
+
+ private static int bucketOf(FileStoreTable table, int pt, int id) throws
Exception {
+ try (BatchTableWrite write = table.newBatchWriteBuilder().newWrite()) {
+ return write.getBucket(row(pt, id, "x"));
+ }
+ }
+
+ /** Serializes like a Spark task closure and starts from an empty JVM-wide
pool cache. */
+ private static <T extends Serializable> T shipToExecutor(T object) throws
Exception {
+ T copy = InstantiationUtil.clone(object);
+ CachedJdbcClientPool.resetCache();
+ CountingDriver.reset();
+ return copy;
+ }
+
+ private static void assertNoExecutorConnections() {
+ assertThat(CountingDriver.OPENED.get()).as("JDBC connections opened on
executor").isZero();
+ assertThat(CachedJdbcClientPool.clientPools())
+ .as("JDBC client pools created on executor")
+ .isEmpty();
+ }
+
+ private static GenericRow row(int pt, int id, String v) {
+ return GenericRow.of(pt, id, BinaryString.fromString(v));
+ }
+
+ /** Delegates {@code jdbc:counting:<url>} to {@code jdbc:<url>} and counts
connections. */
+ private static class CountingDriver implements Driver {
+
+ private static final String PREFIX = "jdbc:counting:";
+ private static final AtomicInteger OPENED = new AtomicInteger();
+
+ static void reset() {
+ OPENED.set(0);
+ }
+
+ @Override
+ public Connection connect(String url, Properties info) throws
SQLException {
+ if (!acceptsURL(url)) {
+ return null;
+ }
+ Connection connection =
+ DriverManager.getConnection("jdbc:" +
url.substring(PREFIX.length()), info);
+ OPENED.incrementAndGet();
+ return connection;
+ }
+
+ @Override
+ public boolean acceptsURL(String url) {
+ return url != null && url.startsWith(PREFIX);
+ }
+
+ @Override
+ public DriverPropertyInfo[] getPropertyInfo(String url, Properties
info) {
+ return new DriverPropertyInfo[0];
+ }
+
+ @Override
+ public int getMajorVersion() {
+ return 1;
+ }
+
+ @Override
+ public int getMinorVersion() {
+ return 0;
+ }
+
+ @Override
+ public boolean jdbcCompliant() {
+ return false;
+ }
+
+ @Override
+ public Logger getParentLogger() throws SQLFeatureNotSupportedException
{
+ throw new SQLFeatureNotSupportedException();
+ }
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
index 2cb4756e9f..3fe3393428 100644
--- a/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/jdbc/JdbcCatalogTest.java
@@ -25,13 +25,18 @@ import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.CatalogContext;
import org.apache.paimon.catalog.CatalogTestBase;
import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.GenericRow;
import org.apache.paimon.fs.Path;
import org.apache.paimon.options.CatalogOptions;
import org.apache.paimon.options.Options;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.Table;
import org.apache.paimon.table.object.ObjectTable;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.view.View;
@@ -1506,4 +1511,27 @@ public class JdbcCatalogTest extends CatalogTestBase {
false))
.isInstanceOf(Catalog.DialectNotExistException.class);
}
+
+ @Test
+ public void testSnapshotLoaderDisabledWithoutVersionManagement() throws
Exception {
+ String databaseName = "snapshot_loader_db";
+ catalog.createDatabase(databaseName, false);
+ Identifier identifier = Identifier.create(databaseName, "table");
+ catalog.createTable(
+ identifier, Schema.newBuilder().column("col",
DataTypes.INT()).build(), false);
+
+ FileStoreTable table = (FileStoreTable) catalog.getTable(identifier);
+ assertThat(catalog.supportsVersionManagement()).isFalse();
+ assertThat(table.catalogEnvironment().snapshotLoader()).isNull();
+
+ BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write = writeBuilder.newWrite();
+ BatchTableCommit commit = writeBuilder.newCommit()) {
+ write.write(GenericRow.of(1));
+ commit.commit(write.prepareCommit());
+ }
+
+ assertThat(table.snapshotManager().latestSnapshotId()).isEqualTo(1L);
+
assertThat(table.snapshotManager().latestSnapshot().id()).isEqualTo(1L);
+ }
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
index 931a842c5e..0523d97f7c 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
@@ -152,6 +152,23 @@ class CatalogEnvironmentTest {
verifyNoInteractions(catalog);
}
+ @Test
+ void testSnapshotLoaderWithoutVersionManagementReturnsNull() {
+ CatalogEnvironment environment =
+ new CatalogEnvironment(
+ Identifier.create("db", "table"),
+ "table-uuid",
+ () -> {
+ throw new AssertionError("Catalog should not be
loaded");
+ },
+ null,
+ null,
+ null,
+ false,
+ false);
+ assertThat(environment.snapshotLoader()).isNull();
+ }
+
@Test
void testDependencyReadContextForRestCatalog() {
Identifier root = Identifier.create("db", "root$branch_dev");