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 e649b2a319 [Fix][Connector-V2] Preserve numeric JDBC partition bounds
(#12584)
e649b2a319 is described below
commit e649b2a319c9234ecf8a47bae219f7e65e2c26c2
Author: hutiefang76 <[email protected]>
AuthorDate: Wed Oct 28 04:06:28 2026 +0000
[Fix][Connector-V2] Preserve numeric JDBC partition bounds (#12584)
Co-authored-by: zhiweiniu <[email protected]>
---
docs/en/connectors/source/Jdbc.md | 2 +
docs/zh/connectors/source/Jdbc.md | 2 +
.../JdbcNumericBetweenParametersProvider.java | 54 +++---
.../JdbcNumericBetweenParametersProviderTest.java | 184 +++++++++++++++++++++
.../jdbc/source/FixedChunkSplitterTest.java | 168 +++++++++++++++++++
.../seatunnel/jdbc/JdbcMysqlSplitIT.java | 82 +++++++++
6 files changed, 463 insertions(+), 29 deletions(-)
diff --git a/docs/en/connectors/source/Jdbc.md
b/docs/en/connectors/source/Jdbc.md
index 4ee50242d8..f4c20f3150 100644
--- a/docs/en/connectors/source/Jdbc.md
+++ b/docs/en/connectors/source/Jdbc.md
@@ -321,6 +321,8 @@ For a full-table snapshot, configure `table_path` and
normally leave split-key d
Only the top-level combination of `query` and `partition_column` selects the
legacy fixed splitter; `partition_num` then controls the number of splits. The
partition column must be present in the query result. Optional lower and upper
bounds avoid extra `MIN`/`MAX` queries, but incorrect bounds can omit source
rows, so use them only when the full data range is known.
+For fixed splitting of integer keys, including BIGINT and zero-scale DECIMAL,
the partitions cover the numeric range without gaps or overlaps. An uneven
division by `partition_num` preserves the remaining values, and each interval
stays within the lower and upper bounds.
+
Entries inside `table_list` continue to use dynamic splitting even when they
include `query` or partition settings. Do not expect `split.size` to affect the
top-level fixed partition mode.
### Query and primary-key caution
diff --git a/docs/zh/connectors/source/Jdbc.md
b/docs/zh/connectors/source/Jdbc.md
index 8f6fa0c37b..7d06080bb5 100644
--- a/docs/zh/connectors/source/Jdbc.md
+++ b/docs/zh/connectors/source/Jdbc.md
@@ -317,6 +317,8 @@ numeric_special_value_as_null = true
只有顶层同时配置 `query` 和 `partition_column` 时才会使用旧版 fixed splitter,再通过
`partition_num` 控制分片数。分区列必须包含在 query 结果中。配置上下界可以避免额外执行 `MIN`/`MAX`
查询,但错误的边界会漏读源数据,因此只有确认完整数据范围时才应设置。
+对 BIGINT、零标度 DECIMAL 等整数键使用固定分片时,各分片完整覆盖数值范围,不产生间隙或重叠。范围不能被 `partition_num`
整除时,剩余值仍会保留,各区间均位于上下界之内。
+
`table_list` 中的条目即使配置了 `query` 或分区参数,仍然使用动态分片。`split.size` 不影响顶层 fixed
partition 模式。
### Query 与主键注意事项
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/split/JdbcNumericBetweenParametersProvider.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/split/JdbcNumericBetweenParametersProvider.java
index 4bf8834f6d..c514450032 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/split/JdbcNumericBetweenParametersProvider.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/split/JdbcNumericBetweenParametersProvider.java
@@ -44,8 +44,9 @@ public class JdbcNumericBetweenParametersProvider implements
JdbcParameterValues
private final BigDecimal minVal;
private final BigDecimal maxVal;
- private long batchSize;
+ private BigDecimal batchSize;
private int batchNum;
+ private BigDecimal largeBatchCount;
/**
* NumericBetweenParametersProviderJdbc constructor.
@@ -78,16 +79,15 @@ public class JdbcNumericBetweenParametersProvider
implements JdbcParameterValues
checkArgument(batchSize > 0, "Batch size must be positive");
BigDecimal maxElemCount =
(maxVal.subtract(minVal)).add(BigDecimal.valueOf(1));
- if (BigDecimal.valueOf(batchSize).compareTo(maxElemCount) > 0) {
- batchSize = maxElemCount.longValue();
+ BigDecimal requestedBatchSize = BigDecimal.valueOf(batchSize);
+ if (requestedBatchSize.compareTo(maxElemCount) > 0) {
+ requestedBatchSize = maxElemCount;
}
- this.batchSize = batchSize;
- this.batchNum =
- new Double(
- Math.ceil(
-
(maxElemCount.divide(BigDecimal.valueOf(batchSize)))
- .doubleValue()))
- .intValue();
+ int numberOfBatches =
+ maxElemCount.divide(requestedBatchSize, 0,
RoundingMode.CEILING).intValueExact();
+ this.batchSize = requestedBatchSize;
+ this.batchNum = numberOfBatches;
+ this.largeBatchCount = BigDecimal.valueOf(this.batchNum);
return this;
}
@@ -99,36 +99,32 @@ public class JdbcNumericBetweenParametersProvider
implements JdbcParameterValues
batchNum = maxElemCount.intValue();
}
this.batchNum = batchNum;
- // For the presence of a decimal we take the integer up
- this.batchSize =
- (maxElemCount.divide(BigDecimal.valueOf(batchNum), 2,
RoundingMode.HALF_UP))
- .setScale(0, RoundingMode.CEILING)
- .longValue();
+ this.batchSize = maxElemCount.divide(BigDecimal.valueOf(batchNum), 0,
RoundingMode.CEILING);
+ // Distribute the remainder without rounding away keys or exceeding
the upper bound.
+ this.largeBatchCount =
+ maxElemCount.subtract(
+ this.batchSize
+ .subtract(BigDecimal.valueOf(1))
+ .multiply(BigDecimal.valueOf(batchNum)));
return this;
}
@Override
public Serializable[][] getParameterValues() {
checkState(
- batchSize > 0,
+ batchSize != null && batchSize.compareTo(BigDecimal.ZERO) > 0
&& batchNum > 0,
"Batch size and batch number must be positive. Have you called
`ofBatchSize` or `ofBatchNum`?");
- BigDecimal maxElemCount =
(maxVal.subtract(minVal)).add(BigDecimal.valueOf(1));
- BigDecimal bigBatchNum =
- maxElemCount
- .subtract(BigDecimal.valueOf(batchSize - 1))
- .multiply(BigDecimal.valueOf(batchNum));
-
Serializable[][] parameters = new Serializable[batchNum][2];
BigDecimal start = minVal;
for (int i = 0; i < batchNum; i++) {
- BigDecimal end =
- start.add(BigDecimal.valueOf(batchSize))
- .subtract(BigDecimal.valueOf(1))
- .subtract(
-
BigDecimal.valueOf(i).compareTo(bigBatchNum) >= 0
- ? BigDecimal.ONE
- : BigDecimal.ZERO);
+ BigDecimal end =
start.add(batchSize).subtract(BigDecimal.valueOf(1));
+ if (BigDecimal.valueOf(i).compareTo(largeBatchCount) >= 0) {
+ end = end.subtract(BigDecimal.valueOf(1));
+ }
+ if (end.compareTo(maxVal) > 0) {
+ end = maxVal;
+ }
parameters[i] = new BigDecimal[] {start, end};
start = end.add(BigDecimal.valueOf(1));
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/split/JdbcNumericBetweenParametersProviderTest.java
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/split/JdbcNumericBetweenParametersProviderTest.java
new file mode 100644
index 0000000000..40161f94ee
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/split/JdbcNumericBetweenParametersProviderTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.jdbc.internal.split;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JdbcNumericBetweenParametersProviderTest {
+
+ private static final BigDecimal ONE = BigDecimal.ONE;
+ private static final BigDecimal DECIMAL_38_MAX =
+ new BigDecimal("99999999999999999999999999999999999999");
+
+ @Test
+ void splitsRangeByFetchSize() {
+ Serializable[][] actual =
+ new JdbcNumericBetweenParametersProvider(300, ONE,
BigDecimal.valueOf(1000))
+ .getParameterValues();
+ BigDecimal[][] expected = {
+ {ONE, BigDecimal.valueOf(300)},
+ {BigDecimal.valueOf(301), BigDecimal.valueOf(600)},
+ {BigDecimal.valueOf(601), BigDecimal.valueOf(900)},
+ {BigDecimal.valueOf(901), BigDecimal.valueOf(1000)}
+ };
+ assertPairs(expected, actual);
+ }
+
+ @Test
+ void clampsBatchSizeToSingletonRange() {
+ Serializable[][] actual =
+ new JdbcNumericBetweenParametersProvider(
+ BigDecimal.valueOf(5), BigDecimal.valueOf(5))
+ .ofBatchSize(3)
+ .getParameterValues();
+ BigDecimal[][] expected = {{BigDecimal.valueOf(5),
BigDecimal.valueOf(5)}};
+ assertPairs(expected, actual);
+ }
+
+ @Test
+ void clampsBatchNumToElementCount() {
+ Serializable[][] actual =
+ new JdbcNumericBetweenParametersProvider(
+ BigDecimal.valueOf(-2), BigDecimal.valueOf(2))
+ .ofBatchNum(10)
+ .getParameterValues();
+ BigDecimal[][] expected = {
+ {BigDecimal.valueOf(-2), BigDecimal.valueOf(-2)},
+ {BigDecimal.valueOf(-1), BigDecimal.valueOf(-1)},
+ {BigDecimal.ZERO, BigDecimal.ZERO},
+ {ONE, ONE},
+ {BigDecimal.valueOf(2), BigDecimal.valueOf(2)}
+ };
+ assertPairs(expected, actual);
+ }
+
+ @Test
+ void keepsExactBoundsForDecimal38Range() {
+ Serializable[][] actual =
+ new JdbcNumericBetweenParametersProvider(ONE, DECIMAL_38_MAX)
+ .ofBatchNum(3)
+ .getParameterValues();
+ assertEquals(3, actual.length);
+ assertEquals(ONE, actual[0][0]);
+ assertEquals(DECIMAL_38_MAX, actual[2][1]);
+ for (int i = 1; i < actual.length; i++) {
+ assertEquals(((BigDecimal) actual[i - 1][1]).add(ONE),
actual[i][0]);
+ assertTrue(((BigDecimal) actual[i][0]).compareTo((BigDecimal)
actual[i][1]) <= 0);
+ }
+ }
+
+ @Test
+ void rejectsNonPositiveBatchSize() {
+ assertMessage(
+ IllegalArgumentException.class,
+ "Batch size must be positive",
+ () -> new JdbcNumericBetweenParametersProvider(ONE,
BigDecimal.TEN).ofBatchSize(0));
+ }
+
+ @Test
+ void rejectsNonPositiveBatchCount() {
+ assertMessage(
+ IllegalArgumentException.class,
+ "Batch number must be positive",
+ () -> new JdbcNumericBetweenParametersProvider(ONE,
BigDecimal.TEN).ofBatchNum(0));
+ }
+
+ @Test
+ void rejectsReversedBounds() {
+ assertMessage(
+ IllegalArgumentException.class,
+ "minVal must not be larger than maxVal",
+ () -> new JdbcNumericBetweenParametersProvider(BigDecimal.TEN,
ONE));
+ }
+
+ @Test
+ void rejectsUnconfiguredState() {
+ assertMessage(
+ IllegalStateException.class,
+ "Batch size and batch number must be positive. Have you called
`ofBatchSize` or `ofBatchNum`?",
+ () ->
+ new JdbcNumericBetweenParametersProvider(ONE,
BigDecimal.TEN)
+ .getParameterValues());
+ }
+
+ @Test
+ void preservesFractionalBoundsWhenClampingToOneBatch() {
+ BigDecimal min = new BigDecimal("0.1");
+ BigDecimal max = new BigDecimal("0.2");
+ assertPairs(
+ new BigDecimal[][] {{min, max}},
+ new JdbcNumericBetweenParametersProvider(min, max)
+ .ofBatchNum(2)
+ .getParameterValues());
+ }
+
+ @Test
+ void replacesPreviousPartitionMode() {
+ JdbcNumericBetweenParametersProvider provider =
+ new JdbcNumericBetweenParametersProvider(ONE, BigDecimal.TEN);
+ provider.ofBatchNum(3).ofBatchSize(4);
+ assertPairs(
+ new BigDecimal[][] {
+ {ONE, BigDecimal.valueOf(4)},
+ {BigDecimal.valueOf(5), BigDecimal.valueOf(8)},
+ {BigDecimal.valueOf(9), BigDecimal.TEN}
+ },
+ provider.getParameterValues());
+ provider.ofBatchNum(3);
+ assertPairs(
+ new BigDecimal[][] {
+ {ONE, BigDecimal.valueOf(4)},
+ {BigDecimal.valueOf(5), BigDecimal.valueOf(7)},
+ {BigDecimal.valueOf(8), BigDecimal.TEN}
+ },
+ provider.getParameterValues());
+ }
+
+ @Test
+ void rejectsTooManyBatchesWithoutChangingConfiguration() {
+ BigDecimal min = BigDecimal.valueOf(Long.MIN_VALUE);
+ BigDecimal max = BigDecimal.valueOf(Long.MAX_VALUE);
+ JdbcNumericBetweenParametersProvider provider =
+ new JdbcNumericBetweenParametersProvider(min,
max).ofBatchNum(2);
+ assertMessage(ArithmeticException.class, "Overflow", () ->
provider.ofBatchSize(1));
+ assertPairs(
+ new BigDecimal[][] {{min, BigDecimal.valueOf(-1)},
{BigDecimal.ZERO, max}},
+ provider.getParameterValues());
+ }
+
+ private static void assertPairs(BigDecimal[][] expected, Serializable[][]
actual) {
+ assertEquals(expected.length, actual.length);
+ for (int i = 0; i < expected.length; i++) {
+ assertArrayEquals(expected[i], actual[i], "pair " + i);
+ }
+ }
+
+ private static void assertMessage(
+ Class<? extends Throwable> type, String message, Executable
executable) {
+ assertEquals(message, assertThrows(type, executable).getMessage());
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/FixedChunkSplitterTest.java
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/FixedChunkSplitterTest.java
index cdd4bb8e99..c1b230c2c9 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/FixedChunkSplitterTest.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/FixedChunkSplitterTest.java
@@ -26,6 +26,7 @@ import
org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcConnectionConfi
import org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcSourceConfig;
import
org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorException;
+import org.duckdb.DuckDBDriver;
import org.junit.jupiter.api.Test;
import lombok.extern.slf4j.Slf4j;
@@ -34,12 +35,27 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
+import java.sql.Connection;
import java.sql.PreparedStatement;
+import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -201,4 +217,156 @@ public class FixedChunkSplitterTest {
handler);
}
}
+
+ private static final String DUCKDB_URL = "jdbc:duckdb:";
+
+ @Test
+ public void testNumericSplitsReadEveryRowExactlyOnce() throws Exception {
+ try (Connection connection = new DuckDBDriver().connect(DUCKDB_URL,
new Properties());
+ DuckDbFixedChunkSplitter splitter =
+ new DuckDbFixedChunkSplitter(duckDbConfig(),
connection)) {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE t (id BIGINT)");
+ statement.execute("INSERT INTO t SELECT * FROM range(1,
2002)");
+ }
+
+ JdbcSourceTable table =
+ JdbcSourceTable.builder()
+ .tablePath(TablePath.of("main", "t"))
+ .query("SELECT id FROM t")
+ .partitionColumn("id")
+ .partitionStart("1")
+ .partitionEnd("2001")
+ .partitionNumber(1000)
+ .build();
+
+ Collection<JdbcSourceSplit> splits = splitter.createSplits(table,
splitKeyType());
+ List<Long> ids = readAllIds(splitter, splits);
+
+ assertEquals(2001, ids.size(), "every row must be read exactly
once");
+ List<Long> distinct = new ArrayList<>(new LinkedHashSet<>(ids));
+ assertEquals(2001, distinct.size(), "no row may be read twice");
+ Collections.sort(distinct);
+ assertEquals(
+ LongStream.rangeClosed(1,
2001).boxed().collect(Collectors.toList()), distinct);
+ }
+ }
+
+ @Test
+ public void testSignedBigIntBoundariesAreSplitWithoutNarrowing() throws
Exception {
+ try (Connection connection = new DuckDBDriver().connect(DUCKDB_URL,
new Properties());
+ DuckDbFixedChunkSplitter splitter =
+ new DuckDbFixedChunkSplitter(duckDbConfig(),
connection)) {
+ long[] boundaries = {Long.MIN_VALUE, -1L, 0L, Long.MAX_VALUE};
+ try (Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE t (id BIGINT)");
+ }
+ try (PreparedStatement insert =
+ connection.prepareStatement("INSERT INTO t (id) VALUES
(?)")) {
+ for (long value : boundaries) {
+ insert.setLong(1, value);
+ insert.addBatch();
+ }
+ insert.executeBatch();
+ }
+
+ JdbcSourceTable table =
+ JdbcSourceTable.builder()
+ .tablePath(TablePath.of("main", "t"))
+ .query("SELECT id FROM t")
+ .partitionColumn("id")
+ .partitionStart(String.valueOf(Long.MIN_VALUE))
+ .partitionEnd(String.valueOf(Long.MAX_VALUE))
+ .partitionNumber(2)
+ .build();
+
+ Collection<JdbcSourceSplit> splits = splitter.createSplits(table,
splitKeyType());
+ List<Long> ids = readAllIds(splitter, splits);
+
+ assertEquals(4, ids.size(), "each signed BIGINT boundary must be
read exactly once");
+ List<Long> sorted = new ArrayList<>(ids);
+ Collections.sort(sorted);
+ assertEquals(Arrays.asList(Long.MIN_VALUE, -1L, 0L,
Long.MAX_VALUE), sorted);
+ }
+ }
+
+ @Test
+ public void testConfiguredBoundsCoverRangeAndExcludeOutsiders() throws
Exception {
+ try (Connection connection = new DuckDBDriver().connect(DUCKDB_URL,
new Properties());
+ DuckDbFixedChunkSplitter splitter =
+ new DuckDbFixedChunkSplitter(duckDbConfig(),
connection)) {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE t (id BIGINT)");
+ statement.execute("INSERT INTO t SELECT * FROM range(0, 12)");
+ }
+
+ JdbcSourceTable table =
+ JdbcSourceTable.builder()
+ .tablePath(TablePath.of("main", "t"))
+ .query("SELECT id FROM t")
+ .partitionColumn("id")
+ .partitionStart("1")
+ .partitionEnd("10")
+ .partitionNumber(3)
+ .build();
+
+ Collection<JdbcSourceSplit> splits = splitter.createSplits(table,
splitKeyType());
+ List<Long> ids = readAllIds(splitter, splits);
+ assertEquals(10, ids.size());
+ Set<Long> distinct = new HashSet<>(ids);
+
+ assertEquals(
+ LongStream.rangeClosed(1,
10).boxed().collect(Collectors.toSet()),
+ distinct,
+ "the configured bounds must cover 1..10");
+ assertFalse(
+ distinct.contains(0L), "values below the configured start
must be excluded");
+ assertFalse(distinct.contains(11L), "values above the configured
end must be excluded");
+ }
+ }
+
+ private static List<Long> readAllIds(ChunkSplitter splitter,
Collection<JdbcSourceSplit> splits)
+ throws SQLException {
+ List<Long> ids = new ArrayList<>();
+ for (JdbcSourceSplit split : splits) {
+ try (PreparedStatement statement =
+ splitter.generateSplitStatement(split,
TableSchema.builder().build());
+ ResultSet resultSet = statement.executeQuery()) {
+ while (resultSet.next()) {
+ ids.add(resultSet.getLong(1));
+ }
+ }
+ }
+ return ids;
+ }
+
+ private static JdbcSourceConfig duckDbConfig() {
+ return JdbcSourceConfig.builder()
+ .jdbcConnectionConfig(
+ JdbcConnectionConfig.builder()
+ .url(DUCKDB_URL)
+ .driverName("org.duckdb.DuckDBDriver")
+ .build())
+ .build();
+ }
+
+ private static SeaTunnelRowType splitKeyType() {
+ return new SeaTunnelRowType(
+ new String[] {"id"}, new SeaTunnelDataType<?>[]
{BasicType.LONG_TYPE});
+ }
+
+ private static final class DuckDbFixedChunkSplitter extends
FixedChunkSplitter {
+
+ private final Connection connection;
+
+ private DuckDbFixedChunkSplitter(JdbcSourceConfig config, Connection
connection) {
+ super(config);
+ this.connection = connection;
+ }
+
+ @Override
+ protected PreparedStatement createPreparedStatement(String sql) throws
SQLException {
+ return connection.prepareStatement(sql);
+ }
+ }
}
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlSplitIT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlSplitIT.java
index e32dc8b593..fe850d5f9c 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlSplitIT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-7/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlSplitIT.java
@@ -56,7 +56,9 @@ import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
+import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDate;
@@ -65,8 +67,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -791,4 +795,82 @@ public class JdbcMysqlSplitIT extends TestSuiteBase
implements TestResource {
}
}
}
+
+ @Test
+ public void testFixedNumericSplitsReadAllRows() throws Exception {
+ try (Connection connection = getJdbcConnection();
+ Statement statement = connection.createStatement()) {
+ statement.execute(
+ "CREATE TABLE "
+ + MYSQL_DATABASE
+ + ".fixed_numeric_uneven (id BIGINT PRIMARY KEY)");
+ try {
+ try (PreparedStatement insert =
+ connection.prepareStatement(
+ "INSERT INTO "
+ + MYSQL_DATABASE
+ + ".fixed_numeric_uneven (id) VALUES
(?)")) {
+ for (int id = 0; id <= 2002; id++) {
+ insert.setLong(1, id);
+ insert.addBatch();
+ }
+ insert.executeBatch();
+ }
+
+ Map<String, Object> configMap = new HashMap<>();
+ configMap.put("url", mysqlUrlInfo.getUrlWithDatabase().get());
+ configMap.put("driver", "com.mysql.cj.jdbc.Driver");
+ configMap.put("username", MYSQL_USERNAME);
+ configMap.put("password", MYSQL_PASSWORD);
+ configMap.put(
+ "query", "SELECT id FROM " + MYSQL_DATABASE +
".fixed_numeric_uneven");
+ configMap.put("partition_column", "id");
+
+ TablePath tablePath = TablePath.of(MYSQL_DATABASE,
"fixed_numeric_uneven");
+ try (MySqlCatalog mySqlCatalog =
+ new MySqlCatalog(
+ "mysql",
+ MYSQL_USERNAME,
+ MYSQL_PASSWORD,
+ mysqlUrlInfo,
+ null);
+ FixedChunkSplitter splitter =
getFixedChunkSplitter(configMap)) {
+ mySqlCatalog.open();
+ CatalogTable table = mySqlCatalog.getTable(tablePath);
+ JdbcSourceTable jdbcSourceTable =
+ JdbcSourceTable.builder()
+ .tablePath(tablePath)
+ .catalogTable(table)
+ .query(
+ "SELECT id FROM "
+ + MYSQL_DATABASE
+ + ".fixed_numeric_uneven")
+ .partitionColumn("id")
+ .partitionNumber(1000)
+ .partitionStart("1")
+ .partitionEnd("2001")
+ .build();
+
+ Set<Long> uniqueIds = new HashSet<>();
+ for (JdbcSourceSplit split :
splitter.generateSplits(jdbcSourceTable)) {
+ try (PreparedStatement splitStatement =
+ splitter.generateSplitStatement(
+ split, table.getTableSchema());
+ ResultSet resultSet =
splitStatement.executeQuery()) {
+ while (resultSet.next()) {
+ long id = resultSet.getLong(1);
+ Assertions.assertTrue(
+ id >= 1 && id <= 2001, "id out of
range: " + id);
+ Assertions.assertTrue(uniqueIds.add(id),
"duplicated id: " + id);
+ }
+ }
+ }
+ Assertions.assertEquals(2001, uniqueIds.size());
+ }
+ } finally {
+ statement.execute(
+ "DROP TABLE IF EXISTS " + MYSQL_DATABASE +
".fixed_numeric_uneven");
+ }
+ }
+ }
}