This is an automated email from the ASF dual-hosted git repository.

davidzollo 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 ebae5c10c3 [Fix][Connector-V2] Fix MongoDB CDC missing operation type 
handling (#11208)
ebae5c10c3 is described below

commit ebae5c10c3c2abacd8a8d872c6e5d590d353c322
Author: Jast <[email protected]>
AuthorDate: Tue Jul 21 22:44:35 2026 +0800

    [Fix][Connector-V2] Fix MongoDB CDC missing operation type handling (#11208)
---
 .../MongoDBConnectorDeserializationSchema.java     | 34 +---------
 .../source/fetch/MongodbFetchTaskContext.java      | 34 +---------
 .../cdc/mongodb/utils/MongodbRecordUtils.java      | 28 +++++++++
 .../MongoDBConnectorDeserializationSchemaTest.java | 73 ++++++++++++++++++++++
 4 files changed, 106 insertions(+), 63 deletions(-)

diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/sender/MongoDBConnectorDeserializationSchema.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/sender/MongoDBConnectorDeserializationSchema.java
index d762c36161..1ff9410dfd 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/sender/MongoDBConnectorDeserializationSchema.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/sender/MongoDBConnectorDeserializationSchema.java
@@ -75,8 +75,8 @@ import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.Mongo
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.ENCODE_VALUE_FIELD;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.FULL_DOCUMENT;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.NS_FIELD;
-import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.OPERATION_TYPE;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.extractBsonDocument;
+import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.getOperationType;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.isHeartbeatEvent;
 import static 
org.apache.seatunnel.shade.com.google.common.base.Preconditions.checkNotNull;
 
@@ -110,7 +110,7 @@ public class MongoDBConnectorDeserializationSchema
         Struct value = (Struct) record.value();
         Schema valueSchema = record.valueSchema();
 
-        OperationType op = operationTypeFor(record);
+        OperationType op = getOperationType(record);
         BsonDocument documentKey =
                 checkNotNull(
                         Objects.requireNonNull(
@@ -184,36 +184,6 @@ public class MongoDBConnectorDeserializationSchema
         return tables;
     }
 
-    private @Nonnull OperationType operationTypeFor(@Nonnull SourceRecord 
record) {
-        Struct value = (Struct) record.value();
-        Schema valueSchema = record.valueSchema();
-        if (valueSchema == null || valueSchema.field(OPERATION_TYPE) == null) {
-            throw new MongodbConnectorException(
-                    ILLEGAL_ARGUMENT,
-                    String.format(
-                            "MongoDB CDC record has no %s field. Topic: %s, 
partition: %s,"
-                                    + " offset: %s",
-                            OPERATION_TYPE,
-                            record.topic(),
-                            record.sourcePartition(),
-                            record.sourceOffset()));
-        }
-
-        String operationType = value.getString(OPERATION_TYPE);
-        if (operationType == null) {
-            throw new MongodbConnectorException(
-                    ILLEGAL_ARGUMENT,
-                    String.format(
-                            "MongoDB CDC record has null %s field. Topic: %s, 
partition: %s,"
-                                    + " offset: %s",
-                            OPERATION_TYPE,
-                            record.topic(),
-                            record.sourcePartition(),
-                            record.sourceOffset()));
-        }
-        return OperationType.fromString(operationType);
-    }
-
     // TODO:The dynamic schema will be completed based on this method later.
     private void emit(
             SourceRecord inRecord,
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/source/fetch/MongodbFetchTaskContext.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/source/fetch/MongodbFetchTaskContext.java
index 91fc9ec769..e018036389 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/source/fetch/MongodbFetchTaskContext.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/source/fetch/MongodbFetchTaskContext.java
@@ -71,6 +71,7 @@ import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.BsonUt
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.buildSourceRecord;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.extractBsonDocument;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.getDocumentKey;
+import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.getOperationType;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.getResumeToken;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbRecordUtils.isHeartbeatEvent;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbUtils.createMongoClient;
@@ -212,9 +213,8 @@ public class MongodbFetchTaskContext implements 
FetchTask.Context {
         Struct value = (Struct) changeRecord.value();
 
         if (value != null) {
-            String operationType = getOperationType(changeRecord, value);
-
-            switch (OperationType.fromString(operationType)) {
+            OperationType operationType = getOperationType(changeRecord);
+            switch (operationType) {
                 case INSERT:
                     outputBuffer.put(key, changeRecord);
                     break;
@@ -249,34 +249,6 @@ public class MongodbFetchTaskContext implements 
FetchTask.Context {
         }
     }
 
-    private String getOperationType(SourceRecord record, Struct value) {
-        if (record.valueSchema() == null || 
record.valueSchema().field(OPERATION_TYPE) == null) {
-            throw new MongodbConnectorException(
-                    ILLEGAL_ARGUMENT,
-                    String.format(
-                            "MongoDB CDC record has no %s field. Topic: %s, 
partition: %s,"
-                                    + " offset: %s",
-                            OPERATION_TYPE,
-                            record.topic(),
-                            record.sourcePartition(),
-                            record.sourceOffset()));
-        }
-
-        String operationType = value.getString(OPERATION_TYPE);
-        if (operationType == null) {
-            throw new MongodbConnectorException(
-                    ILLEGAL_ARGUMENT,
-                    String.format(
-                            "MongoDB CDC record has null %s field. Topic: %s, 
partition: %s,"
-                                    + " offset: %s",
-                            OPERATION_TYPE,
-                            record.topic(),
-                            record.sourcePartition(),
-                            record.sourceOffset()));
-        }
-        return operationType;
-    }
-
     @Override
     public List<SourceRecord> formatMessageTimestamp(
             @Nonnull Collection<SourceRecord> snapshotRecords) {
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/utils/MongodbRecordUtils.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/utils/MongodbRecordUtils.java
index 9b378df424..d6a2a406e4 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/utils/MongodbRecordUtils.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/mongodb/utils/MongodbRecordUtils.java
@@ -19,6 +19,8 @@ package 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils;
 
 import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
 
+import 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.exception.MongodbConnectorException;
+
 import org.apache.kafka.connect.data.Schema;
 import org.apache.kafka.connect.data.SchemaAndValue;
 import org.apache.kafka.connect.data.Struct;
@@ -29,6 +31,7 @@ import org.bson.BsonTimestamp;
 import org.bson.BsonValue;
 import org.bson.json.JsonWriterSettings;
 
+import com.mongodb.client.model.changestream.OperationType;
 import com.mongodb.kafka.connect.source.json.formatter.DefaultJson;
 import com.mongodb.kafka.connect.source.schema.AvroSchemaDefaults;
 import com.mongodb.kafka.connect.source.schema.BsonValueToSchemaAndValue;
@@ -43,6 +46,7 @@ import java.util.Map;
 
 import static com.mongodb.kafka.connect.source.schema.AvroSchema.fromJson;
 import static io.debezium.connector.AbstractSourceInfo.TABLE_NAME_KEY;
+import static 
org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated.ILLEGAL_ARGUMENT;
 import static 
org.apache.seatunnel.connectors.cdc.base.source.split.wartermark.WatermarkEvent.isWatermarkEvent;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.COLL_FIELD;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.COPY_KEY_FIELD;
@@ -51,6 +55,7 @@ import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.Mongo
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.HEARTBEAT_KEY_FIELD;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.ID_FIELD;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.NS_FIELD;
+import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.OPERATION_TYPE;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.OUTPUT_SCHEMA;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.config.MongodbSourceConstants.SOURCE_FIELD;
 import static 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.utils.MongodbUtils.buildConnectionNamespacePrefix;
@@ -71,6 +76,29 @@ public class MongodbRecordUtils {
         return !isWatermarkEvent(sourceRecord) && 
!isHeartbeatEvent(sourceRecord);
     }
 
+    public static @Nonnull OperationType getOperationType(@Nonnull 
SourceRecord sourceRecord) {
+        Struct value = (Struct) sourceRecord.value();
+        if (value == null) {
+            throw new MongodbConnectorException(
+                    ILLEGAL_ARGUMENT,
+                    "MongoDB CDC data change record has no value: " + 
sourceRecord);
+        }
+
+        String operationType = null;
+        if (value.schema().field(OPERATION_TYPE) != null) {
+            operationType = value.getString(OPERATION_TYPE);
+        }
+        if (StringUtils.isEmpty(operationType)) {
+            if (isSnapshotRecord(sourceRecord)) {
+                return OperationType.INSERT;
+            }
+            throw new MongodbConnectorException(
+                    ILLEGAL_ARGUMENT,
+                    "MongoDB CDC data change record has no operationType 
field: " + sourceRecord);
+        }
+        return OperationType.fromString(operationType);
+    }
+
     public static BsonDocument getResumeToken(SourceRecord sourceRecord) {
         return BsonDocument.parse(getOffsetValue(sourceRecord, ID_FIELD));
     }
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/test/java/mongodb/sender/MongoDBConnectorDeserializationSchemaTest.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/test/java/mongodb/sender/MongoDBConnectorDeserializationSchemaTest.java
index 8f5be4dcd6..12f8672373 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/test/java/mongodb/sender/MongoDBConnectorDeserializationSchemaTest.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-mongodb/src/test/java/mongodb/sender/MongoDBConnectorDeserializationSchemaTest.java
@@ -25,6 +25,7 @@ import org.apache.seatunnel.api.table.catalog.TableSchema;
 import org.apache.seatunnel.api.table.type.BasicType;
 import org.apache.seatunnel.api.table.type.DecimalType;
 import org.apache.seatunnel.api.table.type.LocalTimeType;
+import org.apache.seatunnel.api.table.type.RowKind;
 import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import 
org.apache.seatunnel.connectors.seatunnel.cdc.mongodb.exception.MongodbConnectorException;
@@ -174,6 +175,22 @@ public class MongoDBConnectorDeserializationSchemaTest {
         Assertions.assertEquals("inventory.products", tableId);
     }
 
+    @Test
+    public void deserializeSnapshotRecordWithoutOperationType() throws 
Exception {
+        MongoDBConnectorDeserializationSchema schema =
+                new 
MongoDBConnectorDeserializationSchema(Collections.singletonList(catalogTable));
+        SourceRecord sourceRecord = createSnapshotRecordWithoutOperationType();
+        List<SeaTunnelRow> rows = new ArrayList<>();
+
+        schema.deserialize(sourceRecord, new ListCollector(rows));
+
+        Assertions.assertEquals(1, rows.size());
+        SeaTunnelRow row = rows.get(0);
+        Assertions.assertEquals(RowKind.INSERT, row.getRowKind());
+        Assertions.assertEquals("database.table", row.getTableId());
+        Assertions.assertEquals("snapshot-value", row.getField(4));
+    }
+
     @Test
     public void skipHeartbeatRecordWithoutOperationType() throws Exception {
         MongoDBConnectorDeserializationSchema schema =
@@ -303,6 +320,62 @@ public class MongoDBConnectorDeserializationSchemaTest {
                 .orElseThrow(() -> new RuntimeException("not found field"));
     }
 
+    private SourceRecord createSnapshotRecordWithoutOperationType() {
+        Schema keySchema = SchemaBuilder.struct().field(ID_FIELD, 
Schema.STRING_SCHEMA).build();
+        Struct key = new Struct(keySchema).put(ID_FIELD, "12");
+        Schema sourceSchema =
+                SchemaBuilder.struct()
+                        .name("source")
+                        .field(TS_MS_FIELD, Schema.INT64_SCHEMA)
+                        .field("table", Schema.OPTIONAL_STRING_SCHEMA)
+                        .field(DB_FIELD, Schema.OPTIONAL_STRING_SCHEMA)
+                        .field(SNAPSHOT_FIELD, Schema.OPTIONAL_STRING_SCHEMA)
+                        .build();
+        Schema nsSchema =
+                SchemaBuilder.struct()
+                        .name("ns")
+                        .field(DB_FIELD, Schema.STRING_SCHEMA)
+                        .field(COLL_FIELD, Schema.OPTIONAL_STRING_SCHEMA)
+                        .build();
+        Schema valueSchema =
+                SchemaBuilder.struct()
+                        .name("ChangeStreamWithoutOperationType")
+                        .field(ID_FIELD, Schema.STRING_SCHEMA)
+                        .field(FULL_DOCUMENT, Schema.OPTIONAL_STRING_SCHEMA)
+                        .field(SOURCE_FIELD, sourceSchema)
+                        .field(TS_MS_FIELD, Schema.OPTIONAL_INT64_SCHEMA)
+                        .field(NS_FIELD, nsSchema)
+                        .field(DOCUMENT_KEY, Schema.OPTIONAL_STRING_SCHEMA)
+                        .build();
+
+        Struct source =
+                new Struct(sourceSchema)
+                        .put(TS_MS_FIELD, 0L)
+                        .put("table", "table")
+                        .put(DB_FIELD, "database")
+                        .put(SNAPSHOT_FIELD, SNAPSHOT_TRUE);
+        Struct ns = new Struct(nsSchema).put(DB_FIELD, 
"database").put(COLL_FIELD, "table");
+        BsonDocument documentKey = new BsonDocument(ID_FIELD, new 
BsonString("12"));
+        BsonDocument fullDocument = new BsonDocument("string", new 
BsonString("snapshot-value"));
+        Struct value =
+                new Struct(valueSchema)
+                        .put(ID_FIELD, documentKey.toJson())
+                        .put(FULL_DOCUMENT, fullDocument.toJson())
+                        .put(SOURCE_FIELD, source)
+                        .put(TS_MS_FIELD, System.currentTimeMillis())
+                        .put(NS_FIELD, ns)
+                        .put(DOCUMENT_KEY, documentKey.toJson());
+
+        return new SourceRecord(
+                MongodbRecordUtils.createPartitionMap("localhost:27017", 
"database", "table"),
+                createSourceOffsetMap(documentKey, true),
+                "database.table",
+                keySchema,
+                key,
+                valueSchema,
+                value);
+    }
+
     private SourceRecord createRecordWithoutOperationType(boolean heartbeat) {
         Schema valueSchema = SchemaBuilder.struct().field(TS_MS_FIELD, 
Schema.INT64_SCHEMA).build();
         Struct value = new Struct(valueSchema).put(TS_MS_FIELD, 
System.currentTimeMillis());

Reply via email to