This is an automated email from the ASF dual-hosted git repository.
lvyanquan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-cdc.git
The following commit(s) were added to refs/heads/master by this push:
new fe2aa4039 [FLINK-40739][maxcompute] Fix partition handling for sink
writes (#4562)
fe2aa4039 is described below
commit fe2aa4039e23eeac9adafea8c9a9a5dcc5346f2b
Author: Kunni <[email protected]>
AuthorDate: Tue Sep 29 09:47:31 2026 +0800
[FLINK-40739][maxcompute] Fix partition handling for sink writes (#4562)
---
.../pom.xml | 28 ++++
.../coordinator/SessionManageOperator.java | 17 ++-
.../maxcompute/utils/MaxComputeUtils.java | 47 +++---
.../maxcompute/utils/TypeConvertUtils.java | 15 +-
.../maxcompute/writer/BatchAppendWriter.java | 46 ++++--
.../maxcompute/writer/BatchUpsertWriter.java | 12 +-
.../connectors/maxcompute/EmulatorTestBase.java | 29 ++--
.../coordinator/SessionManageOperatorTest.java | 145 ++++++++++++++++++
.../utils/MaxComputeUtilsPartitionTest.java | 116 +++++++++++++++
.../utils/TypeConvertUtilsPartitionTest.java | 64 ++++++++
.../maxcompute/utils/TypeConvertUtilsTest.java | 27 ++++
.../maxcompute/writer/BatchAppendWriterTest.java | 163 +++++++++++++++++++++
.../maxcompute/writer/BatchUpsertWriterTest.java | 134 +++++++++++++++++
.../values/source/ValuesDataSourceHelper.java | 63 ++++++++
.../values/source/ValuesDataSourceOptions.java | 2 +
.../pipeline/tests/stage3/MaxComputeE2eITCase.java | 24 +++
16 files changed, 867 insertions(+), 65 deletions(-)
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/pom.xml
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/pom.xml
index a6398cad6..6c8e44d2f 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/pom.xml
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/pom.xml
@@ -26,6 +26,10 @@ limitations under the License.
<artifactId>flink-cdc-pipeline-connector-maxcompute</artifactId>
<name>flink-cdc-pipeline-connector-maxcompute</name>
+ <properties>
+ <mockito.version>3.12.4</mockito.version>
+ </properties>
+
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
@@ -81,6 +85,30 @@ limitations under the License.
<version>1.19.8</version>
<scope>test</scope>
</dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-inline</artifactId>
+ <version>${mockito.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>${mockito.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <version>${mockito.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/coordinator/SessionManageOperator.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/coordinator/SessionManageOperator.java
index 5c3485b69..89386300b 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/coordinator/SessionManageOperator.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/coordinator/SessionManageOperator.java
@@ -252,8 +252,7 @@ public class SessionManageOperator extends
AbstractStreamOperatorAdapter<Event>
}
}
- /** partition column is always after data column. */
- private String extractPartition(RecordData recordData, TableId tableId) {
+ String extractPartition(RecordData recordData, TableId tableId) {
Schema schema = schemaMaps.get(tableId);
int partitionKeyCount = schema.partitionKeys().size();
if (partitionKeyCount == 0) {
@@ -263,11 +262,17 @@ public class SessionManageOperator extends
AbstractStreamOperatorAdapter<Event>
List<RecordData.FieldGetter> fieldGetters =
fieldGetterMaps.get(tableId);
PartitionSpec partitionSpec = new PartitionSpec();
- for (int i = 0; i < partitionKeyCount; i++) {
- RecordData.FieldGetter fieldGetter =
- fieldGetters.get(columnCount - partitionKeyCount - 1 + i);
+ for (String partitionKey : schema.partitionKeys()) {
+ int partitionColumnIndex =
schema.getColumnNames().indexOf(partitionKey);
+ if (partitionColumnIndex < 0 || partitionColumnIndex >=
columnCount) {
+ throw new IllegalStateException(
+ String.format(
+ "Unable to find partition column \"%s\" in
schema %s",
+ partitionKey, schema));
+ }
+ RecordData.FieldGetter fieldGetter =
fieldGetters.get(partitionColumnIndex);
Object value = fieldGetter.getFieldOrNull(recordData);
- partitionSpec.set(schema.partitionKeys().get(i),
Objects.toString(value));
+ partitionSpec.set(partitionKey, Objects.toString(value));
}
return partitionSpec.toString(true, true);
}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/MaxComputeUtils.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/MaxComputeUtils.java
index a6832dbe5..bf5bd3a12 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/MaxComputeUtils.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/MaxComputeUtils.java
@@ -206,33 +206,32 @@ public class MaxComputeUtils {
MaxComputeOptions options, String schema, String table, String
partitionName)
throws OdpsException {
Odps odps = getOdps(options);
+ Table partitionedTable;
if (options.isSupportSchema()) {
- if (StringUtils.isNullOrEmpty(schema)) {
- LOG.info(
- "create partition {} in {}.default.{}",
- partitionName,
- options.getProject(),
- table);
- odps.tables()
- .get(options.getProject(), "default", table)
- .createPartition(new PartitionSpec(partitionName),
true);
- } else {
- LOG.info(
- "create partition {} in {}.{}.{}",
- partitionName,
- options.getProject(),
- schema,
- table);
- odps.tables()
- .get(options.getProject(), schema, table)
- .createPartition(new PartitionSpec(partitionName),
true);
- }
+ String resolvedSchema = StringUtils.isNullOrEmpty(schema) ?
"default" : schema;
+ partitionedTable = odps.tables().get(options.getProject(),
resolvedSchema, table);
+ LOG.info(
+ "create partition {} in {}.{}.{} if absent",
+ partitionName,
+ options.getProject(),
+ resolvedSchema,
+ table);
} else {
- LOG.info("create partition {} in {}.{}", partitionName,
options.getProject(), table);
- odps.tables()
- .get(options.getProject(), table)
- .createPartition(new PartitionSpec(partitionName), true);
+ partitionedTable = odps.tables().get(options.getProject(), table);
+ LOG.info(
+ "create partition {} in {}.{} if absent",
+ partitionName,
+ options.getProject(),
+ table);
+ }
+ PartitionSpec partitionSpec = new PartitionSpec(partitionName);
+ if (partitionedTable.hasPartition(partitionSpec)) {
+ LOG.info("partition {} already exists, skip creating",
partitionName);
+ return;
}
+ // ifNotExists guards against concurrent partition creation between the
+ // existence check and the creation attempt
+ partitionedTable.createPartition(partitionSpec, true);
}
public static String getSchema(MaxComputeOptions options, TableId tableId)
{
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtils.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtils.java
index a76bd8ef2..22b07a6e9 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtils.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtils.java
@@ -180,11 +180,12 @@ public class TypeConvertUtils {
public static void toMaxComputeRecord(Schema flinkSchema, RecordData from,
ArrayRecord to) {
Preconditions.checkNotNull(from, "flink record data");
Preconditions.checkNotNull(to, "maxcompute arrayRecord");
- int partitionKeyCount = flinkSchema.partitionKeys().size();
+ Set<String> partitionKeys = new HashSet<>(flinkSchema.partitionKeys());
+ int dataColumnCount = flinkSchema.getColumnCount() -
partitionKeys.size();
List<RecordData.FieldGetter> fieldGetters =
createFieldGetters(flinkSchema);
- if (to.getColumnCount() != (fieldGetters.size() - partitionKeyCount)) {
+ if (to.getColumnCount() != dataColumnCount) {
throw new IllegalArgumentException(
"record data count not match, odps {"
+ Arrays.stream(to.getColumns())
@@ -195,11 +196,15 @@ public class TypeConvertUtils {
+ "vs flink {"
+ flinkSchema
+ "} count "
- + (fieldGetters.size() - partitionKeyCount));
+ + dataColumnCount);
}
- for (int i = 0; i < (fieldGetters.size() - partitionKeyCount); i++) {
+ int dataColumnIndex = 0;
+ for (int i = 0; i < fieldGetters.size(); i++) {
+ if
(partitionKeys.contains(flinkSchema.getColumns().get(i).getName())) {
+ continue;
+ }
Object value = fieldGetters.get(i).getFieldOrNull(from);
- to.set(i, value);
+ to.set(dataColumnIndex++, value);
}
}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchAppendWriter.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchAppendWriter.java
index 1cf6be4c0..7212727d4 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchAppendWriter.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchAppendWriter.java
@@ -74,21 +74,39 @@ public class BatchAppendWriter implements MaxComputeWriter {
try {
if (StringUtils.isNullOrWhitespaceOnly(identifier.getSessionId()))
{
- this.uploadSession =
- tunnel.createUploadSession(
- identifier.getProject(),
- identifier.getSchema(),
- identifier.getTable(),
- new PartitionSpec(partitionSpec),
- false);
+ if (StringUtils.isNullOrWhitespaceOnly(partitionSpec)) {
+ this.uploadSession =
+ tunnel.createUploadSession(
+ identifier.getProject(),
+ identifier.getSchema(),
+ identifier.getTable(),
+ false);
+ } else {
+ this.uploadSession =
+ tunnel.createUploadSession(
+ identifier.getProject(),
+ identifier.getSchema(),
+ identifier.getTable(),
+ new PartitionSpec(partitionSpec),
+ false);
+ }
} else {
- this.uploadSession =
- tunnel.getUploadSession(
- identifier.getProject(),
- identifier.getSchema(),
- identifier.getTable(),
- new PartitionSpec(partitionSpec),
- sessionId);
+ if (StringUtils.isNullOrWhitespaceOnly(partitionSpec)) {
+ this.uploadSession =
+ tunnel.getUploadSession(
+ identifier.getProject(),
+ identifier.getSchema(),
+ identifier.getTable(),
+ sessionId);
+ } else {
+ this.uploadSession =
+ tunnel.getUploadSession(
+ identifier.getProject(),
+ identifier.getSchema(),
+ identifier.getTable(),
+ new PartitionSpec(partitionSpec),
+ sessionId);
+ }
}
this.recordWriter =
uploadSession.openBufferedWriter(
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchUpsertWriter.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchUpsertWriter.java
index 731e6139c..5fd949d04 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchUpsertWriter.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/main/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchUpsertWriter.java
@@ -18,6 +18,7 @@
package org.apache.flink.cdc.connectors.maxcompute.writer;
+import org.apache.flink.cdc.common.utils.StringUtils;
import org.apache.flink.cdc.connectors.maxcompute.common.SessionIdentifier;
import
org.apache.flink.cdc.connectors.maxcompute.common.UncheckedOdpsException;
import org.apache.flink.cdc.connectors.maxcompute.options.MaxComputeOptions;
@@ -69,14 +70,17 @@ public class BatchUpsertWriter implements MaxComputeWriter {
String partitionSpec = identifier.getPartitionName();
String sessionId = identifier.getSessionId();
try {
- this.upsertSession =
+ Builder builder =
((Builder)
tunnel.buildUpsertSession(
identifier.getProject(),
identifier.getTable()))
.setConfig(tunnel.getConfig())
- .setSchemaName(identifier.getSchema())
- .setPartitionSpec(partitionSpec)
- .setUpsertId(sessionId)
+ .setSchemaName(identifier.getSchema());
+ if (!StringUtils.isNullOrWhitespaceOnly(partitionSpec)) {
+ builder.setPartitionSpec(partitionSpec);
+ }
+ this.upsertSession =
+ builder.setUpsertId(sessionId)
.setConcurrentNum(writeOptions.getFlushConcurrent())
.build();
this.upsertStream =
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/EmulatorTestBase.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/EmulatorTestBase.java
index 7d930b829..cadff1e19 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/EmulatorTestBase.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/EmulatorTestBase.java
@@ -46,40 +46,45 @@ public class EmulatorTestBase {
public static final DockerImageName MAXCOMPUTE_IMAGE =
DockerImageName.parse("maxcompute/maxcompute-emulator:v0.0.7");
- public static final GenericContainer<?> MAXCOMPUTE_CONTAINER =
- new GenericContainer<>(MAXCOMPUTE_IMAGE)
- .withExposedPorts(8080)
- .waitingFor(
- Wait.forLogMessage(".*Started
MaxcomputeEmulatorApplication.*\\n", 1))
- .withLogConsumer(new Slf4jLogConsumer(LOG));
+ // a fresh container per test class: a stopped GenericContainer instance
cannot be restarted
+ public static GenericContainer<?> maxcomputeContainer;
@BeforeAll
static void createContainer() {
- Startables.deepStart(MAXCOMPUTE_CONTAINER).join();
+ maxcomputeContainer =
+ new GenericContainer<>(MAXCOMPUTE_IMAGE)
+ .withExposedPorts(8080)
+ .waitingFor(
+ Wait.forLogMessage(
+ ".*Started
MaxcomputeEmulatorApplication.*\\n", 1))
+ .withLogConsumer(new Slf4jLogConsumer(LOG));
+ Startables.deepStart(maxcomputeContainer).join();
}
@AfterAll
static void destroyContainer() {
- MAXCOMPUTE_CONTAINER.stop();
+ maxcomputeContainer.stop();
}
public final MaxComputeOptions testOptions =
- MaxComputeOptions.builder("ak", "sk", getEndpoint(),
"mocked_mc").build();
+ MaxComputeOptions.builder("ak", "sk", getEndpoint(), "mocked_mc")
+ .withTunnelEndpoint(getEndpoint())
+ .build();
public final Odps odpsInstance = MaxComputeUtils.getOdps(testOptions);
private String getEndpoint() {
String ip;
- if (MAXCOMPUTE_CONTAINER.getHost().equals("localhost")) {
+ if (maxcomputeContainer.getHost().equals("localhost")) {
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
ip = "127.0.0.1";
}
} else {
- ip = MAXCOMPUTE_CONTAINER.getHost();
+ ip = maxcomputeContainer.getHost();
}
- String endpoint = "http://" + ip + ":" +
MAXCOMPUTE_CONTAINER.getFirstMappedPort();
+ String endpoint = "http://" + ip + ":" +
maxcomputeContainer.getFirstMappedPort();
sendPOST(endpoint + "/init", endpoint);
return endpoint;
}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/coordinator/SessionManageOperatorTest.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/coordinator/SessionManageOperatorTest.java
new file mode 100644
index 000000000..95e9c24ce
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/coordinator/SessionManageOperatorTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.flink.cdc.connectors.maxcompute.coordinator;
+
+import org.apache.flink.cdc.common.data.RecordData;
+import org.apache.flink.cdc.common.data.binary.BinaryRecordData;
+import org.apache.flink.cdc.common.data.binary.BinaryStringData;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.common.types.RowType;
+import org.apache.flink.cdc.connectors.maxcompute.options.MaxComputeOptions;
+import org.apache.flink.cdc.connectors.maxcompute.utils.TypeConvertUtils;
+import org.apache.flink.cdc.runtime.typeutils.BinaryRecordDataGenerator;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+
+import com.aliyun.odps.PartitionSpec;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link SessionManageOperator}. */
+class SessionManageOperatorTest {
+
+ @AfterEach
+ void tearDown() {
+ SessionManageOperator.instance = null;
+ }
+
+ @Test
+ void testExtractPartitionByColumnName() throws Exception {
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("id", DataTypes.INT())
+ .physicalColumn("pt", DataTypes.STRING())
+ .physicalColumn("name", DataTypes.STRING())
+ .physicalColumn("dt", DataTypes.STRING())
+ .partitionKey("pt", "dt")
+ .build();
+ BinaryRecordDataGenerator dataGenerator =
+ new BinaryRecordDataGenerator((RowType)
schema.toRowDataType());
+ BinaryRecordData record =
+ dataGenerator.generate(
+ new Object[] {
+ 1,
+ BinaryStringData.fromString("hangzhou"),
+ BinaryStringData.fromString("Alice"),
+ BinaryStringData.fromString("20260728")
+ });
+
+ SessionManageOperator operator =
+ new SessionManageOperator(mock(MaxComputeOptions.class), new
OperatorID());
+ operator.open();
+ TableId tableId = TableId.tableId("test_table");
+ Map<TableId, Schema> schemaMaps = getField(operator, "schemaMaps");
+ Map<TableId, List<RecordData.FieldGetter>> fieldGetterMaps =
+ getField(operator, "fieldGetterMaps");
+ schemaMaps.put(tableId, schema);
+ fieldGetterMaps.put(tableId,
TypeConvertUtils.createFieldGetters(schema));
+
+ PartitionSpec expected = new PartitionSpec();
+ expected.set("pt", "hangzhou");
+ expected.set("dt", "20260728");
+
+ assertThat(operator.extractPartition(record, tableId))
+ .isEqualTo(expected.toString(true, true));
+ }
+
+ @Test
+ void testExtractPartitionFromUnpartitionedTable() throws Exception {
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("id", DataTypes.INT())
+ .physicalColumn("name", DataTypes.STRING())
+ .build();
+ BinaryRecordDataGenerator dataGenerator =
+ new BinaryRecordDataGenerator((RowType)
schema.toRowDataType());
+ BinaryRecordData record =
+ dataGenerator.generate(new Object[] {1,
BinaryStringData.fromString("Alice")});
+
+ SessionManageOperator operator =
+ new SessionManageOperator(mock(MaxComputeOptions.class), new
OperatorID());
+ operator.open();
+ TableId tableId = TableId.tableId("test_table");
+ Map<TableId, Schema> schemaMaps = getField(operator, "schemaMaps");
+ schemaMaps.put(tableId, schema);
+
+ assertThat(operator.extractPartition(record, tableId)).isNull();
+ }
+
+ @Test
+ void testExtractPartitionFailsForMissingPartitionColumn() throws Exception
{
+ Schema schema = mock(Schema.class);
+
when(schema.partitionKeys()).thenReturn(Collections.singletonList("missing_pt"));
+ when(schema.getColumnCount()).thenReturn(1);
+
when(schema.getColumnNames()).thenReturn(Collections.singletonList("id"));
+
+ SessionManageOperator operator =
+ new SessionManageOperator(mock(MaxComputeOptions.class), new
OperatorID());
+ operator.open();
+ TableId tableId = TableId.tableId("test_table");
+ Map<TableId, Schema> schemaMaps = getField(operator, "schemaMaps");
+ Map<TableId, List<RecordData.FieldGetter>> fieldGetterMaps =
+ getField(operator, "fieldGetterMaps");
+ schemaMaps.put(tableId, schema);
+ fieldGetterMaps.put(tableId, Collections.emptyList());
+
+ assertThatThrownBy(() ->
operator.extractPartition(mock(RecordData.class), tableId))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("missing_pt");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> T getField(Object target, String fieldName)
+ throws ReflectiveOperationException {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return (T) field.get(target);
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/MaxComputeUtilsPartitionTest.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/MaxComputeUtilsPartitionTest.java
new file mode 100644
index 000000000..7227f29ec
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/MaxComputeUtilsPartitionTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.flink.cdc.connectors.maxcompute.utils;
+
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.connectors.maxcompute.EmulatorTestBase;
+import org.apache.flink.cdc.connectors.maxcompute.common.SessionIdentifier;
+import org.apache.flink.cdc.connectors.maxcompute.options.CompressAlgorithm;
+import
org.apache.flink.cdc.connectors.maxcompute.options.MaxComputeWriteOptions;
+import org.apache.flink.cdc.connectors.maxcompute.writer.MaxComputeWriter;
+
+import com.aliyun.odps.Instance;
+import com.aliyun.odps.data.ArrayRecord;
+import com.aliyun.odps.data.Record;
+import com.aliyun.odps.task.SQLTask;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Emulator-based tests for partition handling. Note that the Emulator only
supports uppercase input
+ * (However, MaxCompute can correctly distinguish between uppercase and
lowercase).
+ */
+class MaxComputeUtilsPartitionTest extends EmulatorTestBase {
+
+ private static final String TEST_TABLE = "PARTITION_WRITE_TEST_TABLE";
+
+ @AfterEach
+ void deleteTable() throws Exception {
+ odpsInstance.tables().delete(TEST_TABLE, true);
+ }
+
+ /**
+ * Verifies the partitioned-table write path used by the pipeline job:
create the partitioned
+ * table, create the partition if absent, write records through the
session writer, and read
+ * them back. The partition key is deliberately placed as the first column
to cover partition
+ * columns that are not the last column.
+ */
+ @Test
+ void testCreatePartitionIfAbsentAndWritePartitionedTable() throws
Exception {
+ SchemaEvolutionUtils.createTable(
+ testOptions,
+ TableId.tableId(TEST_TABLE),
+ Schema.newBuilder()
+ .physicalColumn("PT", DataTypes.STRING())
+ .physicalColumn("COL1", DataTypes.STRING())
+ .physicalColumn("COL2", DataTypes.STRING())
+ .partitionKey("PT")
+ .primaryKey("COL1")
+ .build());
+
+ // must not throw, and must be idempotent
+ MaxComputeUtils.createPartitionIfAbsent(testOptions, null, TEST_TABLE,
"PT='2024-01'");
+ MaxComputeUtils.createPartitionIfAbsent(testOptions, null, TEST_TABLE,
"PT='2024-01'");
+
+ writeRow("2024-01", "1", "a");
+ writeRow("2024-01", "2", "b");
+ writeRow("2024-02", "3", "c");
+
+ Instance instance =
+ SQLTask.run(
+ odpsInstance,
+ "select PT, COL1, COL2 from " + TEST_TABLE + " order
by COL1;");
+ instance.waitForSuccess();
+ List<Record> result = SQLTask.getResult(instance);
+ assertThat(result).hasSize(3);
+ assertThat(result.get(0).get(0)).isEqualTo("2024-01");
+ assertThat(result.get(0).get(1)).isEqualTo("1");
+ assertThat(result.get(0).get(2)).isEqualTo("a");
+ assertThat(result.get(1).get(0)).isEqualTo("2024-01");
+ assertThat(result.get(1).get(1)).isEqualTo("2");
+ assertThat(result.get(1).get(2)).isEqualTo("b");
+ assertThat(result.get(2).get(0)).isEqualTo("2024-02");
+ assertThat(result.get(2).get(1)).isEqualTo("3");
+ assertThat(result.get(2).get(2)).isEqualTo("c");
+ }
+
+ private void writeRow(String pt, String col1, String col2) throws
Exception {
+ // session id is null on first creation, mirroring
SessionManageCoordinator#createWriter
+ SessionIdentifier identifier =
+ SessionIdentifier.of(testOptions.getProject(), null,
TEST_TABLE, "PT='" + pt + "'");
+ MaxComputeWriteOptions writeOptions =
+ MaxComputeWriteOptions.builder()
+ .withCompressAlgorithm(CompressAlgorithm.RAW)
+ .build();
+ MaxComputeWriter writer =
+ MaxComputeWriter.batchWriter(testOptions, writeOptions,
identifier);
+ ArrayRecord record = writer.newElement();
+ record.setString(0, col1);
+ record.setString(1, col2);
+ writer.write(record);
+ writer.flush();
+ writer.commit();
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtilsPartitionTest.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtilsPartitionTest.java
new file mode 100644
index 000000000..4cf56346c
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtilsPartitionTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.flink.cdc.connectors.maxcompute.utils;
+
+import org.apache.flink.cdc.common.data.binary.BinaryRecordData;
+import org.apache.flink.cdc.common.data.binary.BinaryStringData;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.common.types.RowType;
+import org.apache.flink.cdc.runtime.typeutils.BinaryRecordDataGenerator;
+
+import com.aliyun.odps.Column;
+import com.aliyun.odps.TableSchema;
+import com.aliyun.odps.data.ArrayRecord;
+import com.aliyun.odps.type.TypeInfoFactory;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Partition-related tests for {@link TypeConvertUtils}. */
+class TypeConvertUtilsPartitionTest {
+
+ @Test
+ void testRecordConversionRejectsMismatchedDataColumnCount() {
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("id", DataTypes.INT())
+ .physicalColumn("pt", DataTypes.STRING())
+ .partitionKey("pt")
+ .build();
+ BinaryRecordDataGenerator dataGenerator =
+ new BinaryRecordDataGenerator((RowType)
schema.toRowDataType());
+ BinaryRecordData record =
+ dataGenerator.generate(
+ new Object[] {7,
BinaryStringData.fromString("partition-value")});
+
+ TableSchema mismatchedSchema = new TableSchema();
+ mismatchedSchema.addColumn(new Column("id", TypeInfoFactory.INT));
+ mismatchedSchema.addColumn(new Column("extra",
TypeInfoFactory.STRING));
+ ArrayRecord arrayRecord = new ArrayRecord(mismatchedSchema);
+
+ assertThatThrownBy(() -> TypeConvertUtils.toMaxComputeRecord(schema,
record, arrayRecord))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("record data count not match")
+ .hasMessageContaining("count 2")
+ .hasMessageContaining("count 1");
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtilsTest.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtilsTest.java
index 26edede0d..189149851 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtilsTest.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/utils/TypeConvertUtilsTest.java
@@ -185,6 +185,33 @@ class TypeConvertUtilsTest {
assertThat(arrayRecord).hasToString(expect);
}
+ @Test
+ void testRecordConvertWithInterleavedPartitionColumn() {
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("id", DataTypes.INT())
+ .physicalColumn("pt", DataTypes.STRING())
+ .physicalColumn("name", DataTypes.STRING())
+ .partitionKey("pt")
+ .build();
+ BinaryRecordDataGenerator dataGenerator =
+ new BinaryRecordDataGenerator((RowType)
schema.toRowDataType());
+ BinaryRecordData record =
+ dataGenerator.generate(
+ new Object[] {
+ 7,
+ BinaryStringData.fromString("20260728"),
+ BinaryStringData.fromString("Alice")
+ });
+
+ ArrayRecord arrayRecord = new
ArrayRecord(TypeConvertUtils.toMaxCompute(schema));
+ TypeConvertUtils.toMaxComputeRecord(schema, record, arrayRecord);
+
+ assertThat(arrayRecord.getColumnCount()).isEqualTo(2);
+ assertThat(arrayRecord.get(0)).isEqualTo(7);
+ assertThat(arrayRecord.get(1)).isEqualTo("Alice");
+ }
+
@Test
void testColumnCommentConversion() {
// Test column with comment
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchAppendWriterTest.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchAppendWriterTest.java
new file mode 100644
index 000000000..8a754d7fc
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchAppendWriterTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.flink.cdc.connectors.maxcompute.writer;
+
+import org.apache.flink.cdc.connectors.maxcompute.common.SessionIdentifier;
+import org.apache.flink.cdc.connectors.maxcompute.options.MaxComputeOptions;
+import
org.apache.flink.cdc.connectors.maxcompute.options.MaxComputeWriteOptions;
+import org.apache.flink.cdc.connectors.maxcompute.utils.MaxComputeUtils;
+
+import com.aliyun.odps.PartitionSpec;
+import com.aliyun.odps.data.RecordWriter;
+import com.aliyun.odps.tunnel.TableTunnel;
+import com.aliyun.odps.tunnel.io.CompressOption;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link BatchAppendWriter}. */
+class BatchAppendWriterTest {
+
+ private static final String PROJECT = "project";
+ private static final String SCHEMA = "schema";
+ private static final String TABLE = "table";
+ private static final String PARTITION = "pt='partition-value'";
+ private static final String SESSION_ID = "session-id";
+
+ private MaxComputeOptions options;
+ private MaxComputeWriteOptions writeOptions;
+ private TableTunnel tunnel;
+ private TableTunnel.UploadSession uploadSession;
+ private MockedStatic<MaxComputeUtils> mockedMaxComputeUtils;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ options = mock(MaxComputeOptions.class);
+ writeOptions = MaxComputeWriteOptions.builder().build();
+ tunnel = mock(TableTunnel.class);
+ uploadSession = mock(TableTunnel.UploadSession.class);
+ RecordWriter recordWriter = mock(RecordWriter.class);
+ CompressOption compressOption = mock(CompressOption.class);
+
+ mockedMaxComputeUtils = Mockito.mockStatic(MaxComputeUtils.class);
+ mockedMaxComputeUtils
+ .when(() -> MaxComputeUtils.getTunnel(options, writeOptions))
+ .thenReturn(tunnel);
+ mockedMaxComputeUtils
+ .when(() ->
MaxComputeUtils.compressOptionOf(writeOptions.getCompressAlgorithm()))
+ .thenReturn(compressOption);
+
when(uploadSession.openBufferedWriter(compressOption)).thenReturn(recordWriter);
+ }
+
+ @AfterEach
+ void tearDown() {
+ mockedMaxComputeUtils.close();
+ }
+
+ @Test
+ void testCreateSessionWithoutPartitionSpec() throws Exception {
+ when(tunnel.createUploadSession(PROJECT, SCHEMA, TABLE,
false)).thenReturn(uploadSession);
+
+ new BatchAppendWriter(
+ options, writeOptions, SessionIdentifier.of(PROJECT, SCHEMA,
TABLE, null));
+
+ verify(tunnel).createUploadSession(PROJECT, SCHEMA, TABLE, false);
+ verify(tunnel, never())
+ .createUploadSession(
+ eq(PROJECT), eq(SCHEMA), eq(TABLE),
any(PartitionSpec.class), eq(false));
+ }
+
+ @Test
+ void testCreateSessionWithPartitionSpec() throws Exception {
+ when(tunnel.createUploadSession(
+ eq(PROJECT), eq(SCHEMA), eq(TABLE),
any(PartitionSpec.class), eq(false)))
+ .thenReturn(uploadSession);
+
+ new BatchAppendWriter(
+ options, writeOptions, SessionIdentifier.of(PROJECT, SCHEMA,
TABLE, PARTITION));
+
+ ArgumentCaptor<PartitionSpec> partitionCaptor =
+ ArgumentCaptor.forClass(PartitionSpec.class);
+ verify(tunnel)
+ .createUploadSession(
+ eq(PROJECT), eq(SCHEMA), eq(TABLE),
partitionCaptor.capture(), eq(false));
+ assertThat(partitionCaptor.getValue().toString(true, true))
+ .isEqualTo(new PartitionSpec(PARTITION).toString(true, true));
+ verify(tunnel, never()).createUploadSession(PROJECT, SCHEMA, TABLE,
false);
+ }
+
+ @Test
+ void testReloadSessionWithoutPartitionSpec() throws Exception {
+ when(tunnel.getUploadSession(PROJECT, SCHEMA, TABLE,
SESSION_ID)).thenReturn(uploadSession);
+
+ new BatchAppendWriter(
+ options,
+ writeOptions,
+ SessionIdentifier.of(PROJECT, SCHEMA, TABLE, " ",
SESSION_ID));
+
+ verify(tunnel).getUploadSession(PROJECT, SCHEMA, TABLE, SESSION_ID);
+ verify(tunnel, never())
+ .getUploadSession(
+ eq(PROJECT),
+ eq(SCHEMA),
+ eq(TABLE),
+ any(PartitionSpec.class),
+ eq(SESSION_ID));
+ }
+
+ @Test
+ void testReloadSessionWithPartitionSpec() throws Exception {
+ when(tunnel.getUploadSession(
+ eq(PROJECT),
+ eq(SCHEMA),
+ eq(TABLE),
+ any(PartitionSpec.class),
+ eq(SESSION_ID)))
+ .thenReturn(uploadSession);
+
+ new BatchAppendWriter(
+ options,
+ writeOptions,
+ SessionIdentifier.of(PROJECT, SCHEMA, TABLE, PARTITION,
SESSION_ID));
+
+ ArgumentCaptor<PartitionSpec> partitionCaptor =
+ ArgumentCaptor.forClass(PartitionSpec.class);
+ verify(tunnel)
+ .getUploadSession(
+ eq(PROJECT),
+ eq(SCHEMA),
+ eq(TABLE),
+ partitionCaptor.capture(),
+ eq(SESSION_ID));
+ assertThat(partitionCaptor.getValue().toString(true, true))
+ .isEqualTo(new PartitionSpec(PARTITION).toString(true, true));
+ verify(tunnel, never()).getUploadSession(PROJECT, SCHEMA, TABLE,
SESSION_ID);
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchUpsertWriterTest.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchUpsertWriterTest.java
new file mode 100644
index 000000000..e2a4acc6e
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-maxcompute/src/test/java/org/apache/flink/cdc/connectors/maxcompute/writer/BatchUpsertWriterTest.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.flink.cdc.connectors.maxcompute.writer;
+
+import org.apache.flink.cdc.connectors.maxcompute.common.SessionIdentifier;
+import org.apache.flink.cdc.connectors.maxcompute.options.MaxComputeOptions;
+import
org.apache.flink.cdc.connectors.maxcompute.options.MaxComputeWriteOptions;
+import org.apache.flink.cdc.connectors.maxcompute.utils.MaxComputeUtils;
+
+import com.aliyun.odps.PartitionSpec;
+import com.aliyun.odps.tunnel.Configuration;
+import com.aliyun.odps.tunnel.TableTunnel;
+import com.aliyun.odps.tunnel.impl.UpsertSessionImpl;
+import com.aliyun.odps.tunnel.impl.UpsertSessionImpl.Builder;
+import com.aliyun.odps.tunnel.io.CompressOption;
+import com.aliyun.odps.tunnel.streams.UpsertStream;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link BatchUpsertWriter}. */
+class BatchUpsertWriterTest {
+
+ private static final String PROJECT = "project";
+ private static final String SCHEMA = "schema";
+ private static final String TABLE = "table";
+ private static final String PARTITION = "pt='partition-value'";
+ private static final String SESSION_ID = "session-id";
+
+ private MaxComputeOptions options;
+ private MaxComputeWriteOptions writeOptions;
+ private TableTunnel tunnel;
+ private Builder sessionBuilder;
+ private MockedStatic<MaxComputeUtils> mockedMaxComputeUtils;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ options = mock(MaxComputeOptions.class);
+ writeOptions = MaxComputeWriteOptions.builder().build();
+ tunnel = mock(TableTunnel.class);
+ sessionBuilder = mock(Builder.class);
+ UpsertSessionImpl upsertSession = mock(UpsertSessionImpl.class);
+ UpsertStream.Builder streamBuilder = mock(UpsertStream.Builder.class);
+ UpsertStream upsertStream = mock(UpsertStream.class);
+ Configuration configuration = mock(Configuration.class);
+ CompressOption compressOption = mock(CompressOption.class);
+
+ mockedMaxComputeUtils = Mockito.mockStatic(MaxComputeUtils.class);
+ mockedMaxComputeUtils
+ .when(() -> MaxComputeUtils.getTunnel(options, writeOptions))
+ .thenReturn(tunnel);
+ mockedMaxComputeUtils
+ .when(() ->
MaxComputeUtils.compressOptionOf(writeOptions.getCompressAlgorithm()))
+ .thenReturn(compressOption);
+
+ when(tunnel.buildUpsertSession(PROJECT,
TABLE)).thenReturn(sessionBuilder);
+ when(tunnel.getConfig()).thenReturn(configuration);
+
when(sessionBuilder.setConfig(configuration)).thenReturn(sessionBuilder);
+ when(sessionBuilder.setSchemaName(SCHEMA)).thenReturn(sessionBuilder);
+
when(sessionBuilder.setPartitionSpec(anyString())).thenReturn(sessionBuilder);
+
when(sessionBuilder.setUpsertId(SESSION_ID)).thenReturn(sessionBuilder);
+
when(sessionBuilder.setConcurrentNum(writeOptions.getFlushConcurrent()))
+ .thenReturn(sessionBuilder);
+ when(sessionBuilder.build()).thenReturn(upsertSession);
+
+ when(upsertSession.buildUpsertStream()).thenReturn(streamBuilder);
+
when(streamBuilder.setListener(any(UpsertStream.Listener.class))).thenReturn(streamBuilder);
+ when(streamBuilder.setMaxBufferSize(writeOptions.getMaxBufferSize()))
+ .thenReturn(streamBuilder);
+ when(streamBuilder.setSlotBufferSize(writeOptions.getSlotBufferSize()))
+ .thenReturn(streamBuilder);
+
when(streamBuilder.setCompressOption(compressOption)).thenReturn(streamBuilder);
+ when(streamBuilder.build()).thenReturn(upsertStream);
+ }
+
+ @AfterEach
+ void tearDown() {
+ mockedMaxComputeUtils.close();
+ }
+
+ @Test
+ void testDoesNotSetNullOrBlankPartitionSpec() throws Exception {
+ new BatchUpsertWriter(
+ options,
+ writeOptions,
+ SessionIdentifier.of(PROJECT, SCHEMA, TABLE, null,
SESSION_ID));
+ new BatchUpsertWriter(
+ options,
+ writeOptions,
+ SessionIdentifier.of(PROJECT, SCHEMA, TABLE, " ",
SESSION_ID));
+
+ verify(sessionBuilder,
never()).setPartitionSpec(nullable(String.class));
+ verify(sessionBuilder,
never()).setPartitionSpec(any(PartitionSpec.class));
+ verify(sessionBuilder, times(2)).build();
+ }
+
+ @Test
+ void testSetsNonBlankPartitionSpec() throws Exception {
+ new BatchUpsertWriter(
+ options,
+ writeOptions,
+ SessionIdentifier.of(PROJECT, SCHEMA, TABLE, PARTITION,
SESSION_ID));
+
+ verify(sessionBuilder).setPartitionSpec(PARTITION);
+ verify(sessionBuilder,
never()).setPartitionSpec(any(PartitionSpec.class));
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceHelper.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceHelper.java
index 2d5ffd5d7..8b777ce9f 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceHelper.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceHelper.java
@@ -52,6 +52,7 @@ public class ValuesDataSourceHelper {
public enum EventSetId {
SINGLE_SPLIT_SINGLE_TABLE,
SINGLE_SPLIT_SINGLE_TABLE_WITH_DEFAULT_VALUE,
+ SINGLE_SPLIT_SINGLE_PARTITION_TABLE,
SINGLE_SPLIT_MULTI_TABLES,
MULTI_SPLITS_SINGLE_TABLE,
CUSTOM_SOURCE_EVENTS,
@@ -122,6 +123,11 @@ public class ValuesDataSourceHelper {
sourceEvents = singleSplitSingleTableWithDefaultValue();
break;
}
+ case SINGLE_SPLIT_SINGLE_PARTITION_TABLE:
+ {
+ sourceEvents = singleSplitSinglePartitionTable();
+ break;
+ }
case SINGLE_SPLIT_MULTI_TABLES:
{
sourceEvents = singleSplitMultiTables();
@@ -342,6 +348,63 @@ public class ValuesDataSourceHelper {
return eventOfSplits;
}
+ /**
+ * A single split with a single partitioned table. The partition column is
intentionally placed
+ * before data columns to verify partition handling when partition key is
not at the end of the
+ * schema.
+ */
+ public static List<List<Event>> singleSplitSinglePartitionTable() {
+ List<List<Event>> eventOfSplits = new ArrayList<>();
+ List<Event> split1 = new ArrayList<>();
+
+ // create table with partition column before data columns
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("pt", DataTypes.STRING())
+ .physicalColumn("col1", DataTypes.STRING())
+ .physicalColumn("col2", DataTypes.STRING())
+ .partitionKey("pt")
+ .primaryKey("col1")
+ .build();
+ split1.add(new CreateTableEvent(TABLE_1, schema));
+
+ BinaryRecordDataGenerator generator =
+ new BinaryRecordDataGenerator(
+ RowType.of(DataTypes.STRING(), DataTypes.STRING(),
DataTypes.STRING()));
+
+ // insert into different partitions
+ split1.add(
+ DataChangeEvent.insertEvent(
+ TABLE_1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("2024-01"),
+ BinaryStringData.fromString("1"),
+ BinaryStringData.fromString("a")
+ })));
+ split1.add(
+ DataChangeEvent.insertEvent(
+ TABLE_1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("2024-01"),
+ BinaryStringData.fromString("2"),
+ BinaryStringData.fromString("b")
+ })));
+ split1.add(
+ DataChangeEvent.insertEvent(
+ TABLE_1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("2024-02"),
+ BinaryStringData.fromString("3"),
+ BinaryStringData.fromString("c")
+ })));
+
+ eventOfSplits.add(split1);
+ return eventOfSplits;
+ }
+
private static AddColumnEvent addColumnWithDefaultValue(TableId tableId) {
AddColumnEvent.ColumnWithPosition columnWithPositionWithDefault =
new AddColumnEvent.ColumnWithPosition(
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceOptions.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceOptions.java
index 42009a48a..3df6c8d33 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceOptions.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-values/src/main/java/org/apache/flink/cdc/connectors/values/source/ValuesDataSourceOptions.java
@@ -42,6 +42,8 @@ public class ValuesDataSourceOptions {
"SINGLE_SPLIT_SINGLE_TABLE: Default and predetermined case. Creating schema
changes of single table and put them into one split."),
text(
"SINGLE_SPLIT_SINGLE_TABLE_WITH_DEFAULT_VALUE: A predetermined case. Creating
schema changes of single table (some columns have default value) and put them
into one split."),
+ text(
+
"SINGLE_SPLIT_SINGLE_PARTITION_TABLE: A predetermined case. Creating schema
changes of single partitioned table whose partition key is not the last column
and put them into one split."),
text(
"SINGLE_SPLIT_MULTI_TABLES: A predetermined case. Creating schema changes of
multiple tables and put them into one split."),
text(
diff --git
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage3/MaxComputeE2eITCase.java
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage3/MaxComputeE2eITCase.java
index 3d31ba015..901dded37 100644
---
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage3/MaxComputeE2eITCase.java
+++
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage3/MaxComputeE2eITCase.java
@@ -111,6 +111,30 @@ class MaxComputeE2eITCase extends PipelineTestEnvironment {
assertThat(result.get(1).get(1)).isEqualTo("NULL");
}
+ @Test
+ void testSingleSplitSinglePartitionTable() throws Exception {
+ startTest("SINGLE_SPLIT_SINGLE_PARTITION_TABLE");
+ Instance instance =
+ SQLTask.run(
+ MaxComputeUtils.getOdps(testOptions),
+ "select pt, col1, col2 from table1 order by col1;");
+ instance.waitForSuccess();
+ List<Record> result = SQLTask.getResult(instance);
+ LOG.info("{}", result);
+ assertThat(result).hasSize(3);
+ // verify partition column is correctly extracted and data columns are
+ // mapped correctly when partition key is not the last column
+ assertThat(result.get(0).get(0)).isEqualTo("2024-01");
+ assertThat(result.get(0).get(1)).isEqualTo("1");
+ assertThat(result.get(0).get(2)).isEqualTo("a");
+ assertThat(result.get(1).get(0)).isEqualTo("2024-01");
+ assertThat(result.get(1).get(1)).isEqualTo("2");
+ assertThat(result.get(1).get(2)).isEqualTo("b");
+ assertThat(result.get(2).get(0)).isEqualTo("2024-02");
+ assertThat(result.get(2).get(1)).isEqualTo("3");
+ assertThat(result.get(2).get(2)).isEqualTo("c");
+ }
+
private void startTest(String testSet) throws Exception {
sendPOST(getEndpoint() + "/init", getEndpoint());