This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 7b686d8387 [Fix][Connector-V2] Truncate the table path database
(#12877)
7b686d8387 is described below
commit 7b686d8387557ae4d90de35176d6dcafdc0e1fff
Author: hutiefang76 <[email protected]>
AuthorDate: Mon Oct 26 17:10:24 2026 +0000
[Fix][Connector-V2] Truncate the table path database (#12877)
---
docs/en/connectors/sink/Jdbc.md | 2 +
docs/zh/connectors/sink/Jdbc.md | 2 +
.../jdbc/catalog/AbstractJdbcCatalog.java | 15 ++-
.../jdbc/catalog/AbstractJdbcCatalogTest.java | 94 ++++++++++++++++++
.../connectors/seatunnel/jdbc/JdbcPostgresIT.java | 105 ++++++++++++++++++++-
5 files changed, 214 insertions(+), 4 deletions(-)
diff --git a/docs/en/connectors/sink/Jdbc.md b/docs/en/connectors/sink/Jdbc.md
index b2ba4e00d1..f5a70fb80f 100644
--- a/docs/en/connectors/sink/Jdbc.md
+++ b/docs/en/connectors/sink/Jdbc.md
@@ -411,6 +411,8 @@ Option introduction:
`CUSTOM_PROCESSING`:User defined processing
`ERROR_WHEN_DATA_EXISTS`:When there is data, an error is reported
+When the full target table path specifies a database
(`database.schema.table`), `DROP_DATA` executes `TRUNCATE TABLE` in that
database.
+
### custom_sql [String]
When data_save_mode selects CUSTOM_PROCESSING, you should fill in the
CUSTOM_SQL parameter. This parameter usually fills in a SQL that can be
executed. SQL will be executed before synchronization tasks.
diff --git a/docs/zh/connectors/sink/Jdbc.md b/docs/zh/connectors/sink/Jdbc.md
index 574f0d3138..8b85b2bf1a 100644
--- a/docs/zh/connectors/sink/Jdbc.md
+++ b/docs/zh/connectors/sink/Jdbc.md
@@ -407,6 +407,8 @@ Sink插件常用参数,请参考 [Sink常用选项](../common-options/sink-com
`CUSTOM_PROCESSING`:允许用户自定义数据处理方式<br/>
`ERROR_WHEN_DATA_EXISTS`:当有数据时抛出错误<br/>
+目标表的完整路径指定数据库(`database.schema.table`)时,`DROP_DATA` 会连接到该数据库执行 `TRUNCATE
TABLE`。
+
### table_options [Map]
Sink 在自动建表(SaveMode DDL)时附加的表级选项。仅在 `schema_save_mode` 触发建表时生效,例如
`CREATE_SCHEMA_WHEN_NOT_EXIST`、`RECREATE_SCHEMA`;**不影响**数据写入阶段的
INSERT/UPSERT,也**不会**对已存在表执行 `ALTER TABLE`。
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalog.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalog.java
index 9a40d2123f..882473551c 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalog.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalog.java
@@ -688,9 +688,22 @@ public abstract class AbstractJdbcCatalog implements
Catalog {
return CatalogUtils.getCatalogTable(defaultConnection, sqlQuery);
}
+ /**
+ * Truncates the table on the connection of the database that the table
path points to.
+ *
+ * <p>Most catalogs render the truncate DDL with schema and table only (no
database qualifier),
+ * so the connection URL is what decides which database is truncated. A
table path that carries
+ * an explicit database different from the catalog database must therefore
be truncated through
+ * that database's URL, like the other DDL siblings in this class. A table
path without a
+ * database keeps using the catalog default URL.
+ */
protected void truncateTableInternal(TablePath tablePath) throws
CatalogException {
+ String dbUrl =
+ StringUtils.isNotBlank(tablePath.getDatabaseName())
+ ? getUrlFromDatabaseName(tablePath.getDatabaseName())
+ : defaultUrl;
try {
- executeInternal(defaultUrl, getTruncateTableSql(tablePath));
+ executeInternal(dbUrl, getTruncateTableSql(tablePath));
} catch (Exception e) {
throw new CatalogException(
String.format(
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalogTest.java
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalogTest.java
index c1a1abf389..13d2a9f4d6 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalogTest.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/AbstractJdbcCatalogTest.java
@@ -17,6 +17,7 @@
package org.apache.seatunnel.connectors.seatunnel.jdbc.catalog;
+import org.apache.seatunnel.api.table.catalog.TablePath;
import org.apache.seatunnel.api.table.catalog.exception.CatalogException;
import org.apache.seatunnel.common.utils.JdbcUrlUtil;
@@ -24,14 +25,29 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
+import java.sql.PreparedStatement;
import java.sql.SQLException;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
class AbstractJdbcCatalogTest {
+ /** Database configured in the catalog URL. */
+ private static final String CATALOG_DATABASE = "db_a";
+
+ private static final String CATALOG_DATABASE_URL =
"jdbc:testdb://localhost:5432/db_a";
+
+ /** Database referenced by the table path, different from the catalog
database. */
+ private static final String TABLE_DATABASE = "db_b";
+
+ private static final String TABLE_DATABASE_URL =
"jdbc:testdb://localhost:5432/db_b";
+
@Test
void
shouldCloseRemainingConnectionsAndKeepSuppressedExceptionsWhenCloseFails()
throws SQLException {
@@ -51,6 +67,64 @@ class AbstractJdbcCatalogTest {
Assertions.assertEquals(1, exception.getSuppressed().length);
}
+ /**
+ * Control: a table inside the catalog database is truncated on the
catalog default connection.
+ */
+ @Test
+ void shouldTruncateTableOnCatalogDefaultConnection() throws SQLException {
+ TestJdbcCatalog catalog = new TestJdbcCatalog(CATALOG_DATABASE_URL);
+ Connection catalogConnection = registerMockConnection(catalog,
CATALOG_DATABASE_URL);
+ Connection tableDatabaseConnection = registerMockConnection(catalog,
TABLE_DATABASE_URL);
+
+ catalog.truncateTable(TablePath.of(CATALOG_DATABASE, "public", "t"),
false);
+
+ verify(catalogConnection).prepareStatement(contains("TRUNCATE"));
+ verify(tableDatabaseConnection, never()).prepareStatement(anyString());
+ }
+
+ /**
+ * The truncate DDL only qualifies schema and table (same as {@code
PostgresCatalog}), so the
+ * connection URL is the only thing that decides which database gets
truncated. Truncating
+ * {@code db_b.public.t} through a catalog whose own database is {@code
db_a} must therefore run
+ * on the {@code db_b} connection, not on the catalog default one.
+ */
+ @Test
+ void shouldRouteTruncateToTableDatabaseWhenItDiffersFromCatalogDefault()
throws SQLException {
+ TestJdbcCatalog catalog = new TestJdbcCatalog(CATALOG_DATABASE_URL);
+ Connection catalogConnection = registerMockConnection(catalog,
CATALOG_DATABASE_URL);
+ Connection tableDatabaseConnection = registerMockConnection(catalog,
TABLE_DATABASE_URL);
+
+ catalog.truncateTable(TablePath.of(TABLE_DATABASE, "public", "t"),
false);
+
+ verify(tableDatabaseConnection).prepareStatement(contains("TRUNCATE"));
+ verify(catalogConnection, never()).prepareStatement(anyString());
+ }
+
+ /**
+ * Control: a table path without database has no database to route to and
keeps using the
+ * catalog default connection.
+ */
+ @Test
+ void
shouldTruncateTableOnCatalogDefaultConnectionWhenTablePathHasNoDatabase()
+ throws SQLException {
+ TestJdbcCatalog catalog = new TestJdbcCatalog(CATALOG_DATABASE_URL);
+ Connection catalogConnection = registerMockConnection(catalog,
CATALOG_DATABASE_URL);
+
+ // truncateTableInternal() is invoked directly because tableExists()
cannot probe a
+ // database-less table path; the existence check is not part of the
routing contract here.
+ catalog.truncateTableInternal(new TablePath(null, "public", "t"));
+
+ verify(catalogConnection).prepareStatement(contains("TRUNCATE"));
+ }
+
+ private static Connection registerMockConnection(TestJdbcCatalog catalog,
String url)
+ throws SQLException {
+ Connection connection = mock(Connection.class);
+
when(connection.prepareStatement(anyString())).thenReturn(mock(PreparedStatement.class));
+ catalog.addConnection(url, connection);
+ return connection;
+ }
+
private static class TestJdbcCatalog extends AbstractJdbcCatalog {
private TestJdbcCatalog() {
@@ -64,8 +138,28 @@ class AbstractJdbcCatalogTest {
null);
}
+ private TestJdbcCatalog(String origin) {
+ super("test", "user", "password", JdbcUrlUtil.getUrlInfo(origin),
null, null);
+ }
+
private void addConnection(String url, Connection connection) {
connectionMap.put(url, connection);
}
+
+ /** The table is assumed to exist: existence checks are not part of
the reproduction. */
+ @Override
+ public boolean tableExists(TablePath tablePath) {
+ return true;
+ }
+
+ /** Mirrors {@code PostgresCatalog}: schema and table only, no
database qualifier. */
+ @Override
+ protected String getTruncateTableSql(TablePath tablePath) {
+ return "TRUNCATE TABLE \""
+ + tablePath.getSchemaName()
+ + "\".\""
+ + tablePath.getTableName()
+ + "\"";
+ }
}
}
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-3/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-3/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIT.java
index fcc3028878..a654f841e6 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-3/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-3/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcPostgresIT.java
@@ -269,6 +269,15 @@ public class JdbcPostgresIT extends TestSuiteBase
implements TestResource {
private static final String NUMERIC_SPECIAL_VALUE_SINK_QUERY =
"select gid, numeric_col from " + NUMERIC_SPECIAL_VALUE_SINK_TABLE
+ " order by gid";
+ /**
+ * Dedicated database created and dropped by the truncate routing test, so
truncating a table
+ * path that points to another database never touches the container's
default fixture tables.
+ */
+ private static final String TRUNCATE_ROUTE_DATABASE =
"pg_e2e_truncate_route_db";
+
+ /** Same table name in both the catalog database and {@link
#TRUNCATE_ROUTE_DATABASE}. */
+ private static final String TRUNCATE_ROUTE_TABLE =
"pg_e2e_truncate_route_table";
+
@TestContainerExtension
private final ContainerExtendedFactory extendedFactory =
container -> {
@@ -643,6 +652,88 @@ public class JdbcPostgresIT extends TestSuiteBase
implements TestResource {
catalog.close();
}
+ /**
+ * Truncating {@code database.schema.table} through a catalog whose own
database is the
+ * container default database must truncate the database of the table
path, and must leave the
+ * table with the same name in the catalog database untouched.
+ *
+ * <p>Only the dedicated database and table created here are truncated and
dropped.
+ */
+ @Test
+ public void testTruncateTableUsesTablePathDatabase() throws SQLException {
+ String schema = "public";
+ String catalogDatabaseName = POSTGRESQL_CONTAINER.getDatabaseName();
+ TablePath catalogDatabaseTablePath =
+ TablePath.of(catalogDatabaseName, schema,
TRUNCATE_ROUTE_TABLE);
+ TablePath routedDatabaseTablePath =
+ TablePath.of(TRUNCATE_ROUTE_DATABASE, schema,
TRUNCATE_ROUTE_TABLE);
+ String createTableSql =
+ "CREATE TABLE " + schema + "." + TRUNCATE_ROUTE_TABLE + " (gid
INT)";
+ String insertRowSql = "INSERT INTO " + schema + "." +
TRUNCATE_ROUTE_TABLE + " VALUES (1)";
+
+ PostgresCatalog postgresCatalog =
+ new PostgresCatalog(
+ DatabaseIdentifier.POSTGRESQL,
+ POSTGRESQL_CONTAINER.getUsername(),
+ POSTGRESQL_CONTAINER.getPassword(),
+
JdbcUrlUtil.getUrlInfo(POSTGRESQL_CONTAINER.getJdbcUrl()),
+ schema,
+ null);
+ postgresCatalog.open();
+ try {
+ postgresCatalog.createDatabase(routedDatabaseTablePath, true);
+ postgresCatalog.executeSql(catalogDatabaseTablePath,
createTableSql);
+ postgresCatalog.executeSql(routedDatabaseTablePath,
createTableSql);
+ postgresCatalog.executeSql(catalogDatabaseTablePath, insertRowSql);
+ postgresCatalog.executeSql(routedDatabaseTablePath, insertRowSql);
+ Assertions.assertEquals(1, countRows(catalogDatabaseName,
TRUNCATE_ROUTE_TABLE));
+ Assertions.assertEquals(1, countRows(TRUNCATE_ROUTE_DATABASE,
TRUNCATE_ROUTE_TABLE));
+
+ postgresCatalog.truncateTable(routedDatabaseTablePath, false);
+
+ Assertions.assertEquals(
+ 1,
+ countRows(catalogDatabaseName, TRUNCATE_ROUTE_TABLE),
+ "The table in the catalog database must not be truncated");
+ Assertions.assertEquals(
+ 0,
+ countRows(TRUNCATE_ROUTE_DATABASE, TRUNCATE_ROUTE_TABLE),
+ "The table in the database of the table path must be
truncated");
+ } finally {
+ try {
+ postgresCatalog.dropTable(routedDatabaseTablePath, true);
+ postgresCatalog.dropTable(catalogDatabaseTablePath, true);
+ } finally {
+ try {
+ postgresCatalog.dropDatabase(routedDatabaseTablePath,
true);
+ } finally {
+ postgresCatalog.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Counts the rows of a table in the given database on the PostgreSQL
container.
+ *
+ * @param databaseName database to connect to
+ * @param tableName table to count rows of, in the {@code public} schema
+ * @return the number of rows in the table
+ */
+ private int countRows(String databaseName, String tableName) throws
SQLException {
+ String baseJdbcUrl = POSTGRESQL_CONTAINER.getJdbcUrl();
+ String databaseUrl =
+ baseJdbcUrl.replace(
+ "/" + POSTGRESQL_CONTAINER.getDatabaseName(), "/" +
databaseName);
+ String countSql = "SELECT count(1) FROM public." + tableName;
+ try (Connection connection = getJdbcConnection(databaseUrl);
+ Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(countSql)) {
+ resultSet.next();
+ return resultSet.getInt(1);
+ }
+ }
+
/**
* Verifies PostgreSQL catalog discovery preserves visible column order
after a column is
* dropped.
@@ -799,10 +890,18 @@ public class JdbcPostgresIT extends TestSuiteBase
implements TestResource {
}
private Connection getJdbcConnection() throws SQLException {
+ return getJdbcConnection(POSTGRESQL_CONTAINER.getJdbcUrl());
+ }
+
+ /**
+ * Opens a connection to the given PostgreSQL URL on the container.
+ *
+ * @param jdbcUrl JDBC URL to connect to, including the database to use
+ * @return an open connection, owned by the caller
+ */
+ private Connection getJdbcConnection(String jdbcUrl) throws SQLException {
return DriverManager.getConnection(
- POSTGRESQL_CONTAINER.getJdbcUrl(),
- POSTGRESQL_CONTAINER.getUsername(),
- POSTGRESQL_CONTAINER.getPassword());
+ jdbcUrl, POSTGRESQL_CONTAINER.getUsername(),
POSTGRESQL_CONTAINER.getPassword());
}
private List<List<Object>> querySql(String sql) {