Savonitar commented on code in PR #4618:
URL: https://github.com/apache/flink-cdc/pull/4618#discussion_r4530833606


##########
flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-postgres/src/test/java/org/apache/flink/cdc/connectors/postgres/factory/PostgresDataSourceFactoryTest.java:
##########
@@ -363,6 +365,88 @@ public void testTableValidationWithOriginalBugScenario() {
                 .hasMessageContaining("Cannot find any table by the option 
'tables'");
     }
 
+    @Test
+    public void testChangelogModeDefaultsToAll() {
+        Map<String, String> options = new HashMap<>();
+        options.put(HOSTNAME.key(), POSTGRES_CONTAINER.getHost());
+        options.put(
+                PG_PORT.key(), 
String.valueOf(POSTGRES_CONTAINER.getMappedPort(POSTGRESQL_PORT)));
+        options.put(USERNAME.key(), TEST_USER);
+        options.put(PASSWORD.key(), TEST_PASSWORD);
+        options.put(TABLES.key(), POSTGRES_CONTAINER.getDatabaseName() + 
".inventory.prod\\.*");
+        options.put(SLOT_NAME.key(), slotName);
+        Factory.Context context = new 
MockContext(Configuration.fromMap(options));
+
+        PostgresDataSourceFactory factory = new PostgresDataSourceFactory();
+        PostgresDataSource dataSource = (PostgresDataSource) 
factory.createDataSource(context);
+        
assertThat(dataSource.getChangelogMode()).isEqualTo(DebeziumChangelogMode.ALL);
+    }
+
+    @Test
+    public void testChangelogModeUpsert() {
+        Map<String, String> options = new HashMap<>();
+        options.put(HOSTNAME.key(), POSTGRES_CONTAINER.getHost());
+        options.put(
+                PG_PORT.key(), 
String.valueOf(POSTGRES_CONTAINER.getMappedPort(POSTGRESQL_PORT)));
+        options.put(USERNAME.key(), TEST_USER);
+        options.put(PASSWORD.key(), TEST_PASSWORD);
+        options.put(TABLES.key(), POSTGRES_CONTAINER.getDatabaseName() + 
".inventory.prod\\.*");
+        options.put(SLOT_NAME.key(), slotName);
+        options.put(CHANGELOG_MODE.key(), "upsert");
+        Factory.Context context = new 
MockContext(Configuration.fromMap(options));
+
+        PostgresDataSourceFactory factory = new PostgresDataSourceFactory();

Review Comment:
   Could we exercise `changelog-mode: upsert` through the factory-created 
source provider, including a positive `REPLICA IDENTITY DEFAULT` update after 
the snapshot is observed? 



##########
flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-postgres/src/main/java/org/apache/flink/cdc/connectors/postgres/factory/PostgresDataSourceFactory.java:
##########
@@ -211,8 +216,42 @@ public DataSource createDataSource(Context context) {
         String metadataList = config.get(METADATA_LIST);
         List<PostgreSQLReadableMetadata> readableMetadataList = 
listReadableMetadata(metadataList);
 
-        // Create a custom PostgresDataSource that passes the 
includeDatabaseInTableId flag
-        return new PostgresDataSource(configFactory, readableMetadataList);
+        DebeziumChangelogMode changelogMode = config.get(CHANGELOG_MODE);
+        if (changelogMode == DebeziumChangelogMode.UPSERT) {
+            Set<String> capturedTableSet = new HashSet<>(capturedTables);
+            List<TableId> capturedTableIds =
+                    tableIds.stream()
+                            .filter(tableId -> 
capturedTableSet.contains(tableId.toString()))
+                            .collect(Collectors.toList());
+            validateUpsertModePrimaryKeys(configFactory.create(0), 
capturedTableIds);
+        }
+
+        return new PostgresDataSource(configFactory, readableMetadataList, 
changelogMode);
+    }
+
+    /**
+     * Validates that all captured tables have a primary key, which upsert 
changelog mode relies on
+     * to describe idempotent updates on a key. Mirrors the validation the SQL 
connector performs in
+     * PostgreSQLTableFactory.
+     */
+    private static void validateUpsertModePrimaryKeys(
+            PostgresSourceConfig sourceConfig, List<TableId> capturedTableIds) 
{
+        List<String> tablesWithoutPrimaryKey = new ArrayList<>();
+        try (PostgresConnection jdbc =
+                
PostgresSchemaUtils.getPostgresDialect(sourceConfig).openJdbcConnection()) {
+            for (TableId tableId : capturedTableIds) {
+                Schema schema = PostgresSchemaUtils.getTableSchema(tableId, 
sourceConfig, jdbc);

Review Comment:
   `getTableSchema` builds a new `PostgresSchema` and calls 
`schema.refresh(connection, false)` on every call.
   Did we consider other options, e.g. could we build the schema once and call 
`tableFor(...)` in the loop?



##########
flink-cdc-connect/flink-cdc-source-connectors/flink-connector-debezium/src/main/java/org/apache/flink/cdc/debezium/event/DebeziumEventDeserializationSchema.java:
##########
@@ -128,8 +128,10 @@ public List<DataChangeEvent> 
deserializeDataChangeRecord(SourceRecord record) th
                 return Collections.singletonList(
                         DataChangeEvent.updateEvent(tableId, before, after, 
meta));
             }
-            return Collections.singletonList(
-                    DataChangeEvent.updateEvent(tableId, null, after, meta));
+            // In upsert mode the before image may be unavailable (e.g. 
PostgreSQL with
+            // REPLICA IDENTITY DEFAULT), so emit a REPLACE event which 
carries the after
+            // image only instead of an UPDATE event with a null before image.
+            return 
Collections.singletonList(DataChangeEvent.replaceEvent(tableId, after, meta));

Review Comment:
   Should we handle or reject unavailable TOAST fields before emitting a full 
`REPLACE`, and cover this with a `DEFAULT`/`pgoutput` integration test? 
   For a primary-key table with an externally stored `TEXT` payload, snapshot 
the row and then update only an integer counter.
    [Debezium 1.9's TOAST 
contract](https://debezium.io/documentation/reference/1.9/connectors/postgresql.html#postgresql-toasted-values)
 says the unchanged payload is represented by an unavailable-value placeholder 
under `REPLICA IDENTITY DEFAULT`. 



##########
docs/content/docs/connectors/pipeline-connectors/postgres.md:
##########
@@ -324,6 +324,18 @@ pipeline:
         WARNING: Incremental/binlog phase: Rate limiting can cause the 
connector to fall behind the upstream change stream, risking binlog/WAL purging 
before the connector catches up (data loss for MySQL, replication slot issues 
for PostgreSQL).
       </td>
     </tr>
+    <tr>
+      <td>changelog-mode</td>
+      <td>optional</td>
+      <td style="word-wrap: break-word;">all</td>
+      <td>String</td>
+      <td>
+        The changelog mode used for encoding streaming changes.<br>
+        <code>all</code>: Emits UPDATE events carrying both the before and the 
after image. This is the default mode and requires the table's <code>REPLICA 
IDENTITY</code> to be <code>FULL</code>.<br>
+        <code>upsert</code>: Emits REPLACE events carrying only the after 
image, describing idempotent updates on a key. It can be used for tables with 
primary keys when <code>REPLICA IDENTITY FULL</code> is not an option.<br>

Review Comment:
   How are unchanged TOAST columns handled in upsert mode?
   



##########
flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-postgres/src/main/java/org/apache/flink/cdc/connectors/postgres/source/PostgresDataSourceOptions.java:
##########
@@ -317,4 +318,15 @@ public class PostgresDataSourceOptions {
                     .defaultValue(-1d)
                     .withDescription(
                             "The maximum number of records emitted per second, 
the default value: -1, means no rate limiting.");
+
+    @Experimental
+    public static final ConfigOption<DebeziumChangelogMode> CHANGELOG_MODE =
+            ConfigOptions.key("changelog-mode")
+                    .enumType(DebeziumChangelogMode.class)
+                    .defaultValue(DebeziumChangelogMode.ALL)
+                    .withDescription(
+                            "The changelog mode used for encoding streaming 
changes.\n"
+                                    + "\"all\": Encodes changes as UPDATE 
events carrying both the before and the after image. This is the default 
mode.\n"

Review Comment:
   nit: the docs say `all` requires `REPLICA IDENTITY FULL`, but this 
description doesn't. Should the two match?



##########
flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-postgres/src/test/java/org/apache/flink/cdc/connectors/postgres/source/PostgresEventDeserializerTest.java:
##########
@@ -50,4 +77,80 @@ void testNullBeforeThrowsNpeWithReplicaIdentityHint() throws 
Exception {
                 .hasMessageContaining("Before data is null")
                 .hasMessageContaining("REPLICA IDENTITY FULL");
     }
+
+    @Test
+    void testUpdateEmitsUpdateEventInAllMode() throws Exception {
+        PostgresEventDeserializer deserializer =
+                new PostgresEventDeserializer(DebeziumChangelogMode.ALL);
+
+        List<Event> events =
+                deserialize(deserializer, buildUpdateRecord(row(1, "spare"), 
row(1, "scooter")));
+
+        assertThat(events).hasSize(1);
+        DataChangeEvent event = (DataChangeEvent) events.get(0);
+        assertThat(event.op()).isEqualTo(OperationType.UPDATE);
+        assertThat(event.tableId()).isEqualTo(TableId.tableId("inventory", 
"products"));
+        assertThat(event.before()).isNotNull();
+        assertThat(event.before().getInt(0)).isEqualTo(1);
+        assertThat(event.before().getString(1).toString()).isEqualTo("spare");
+        assertThat(event.after().getInt(0)).isEqualTo(1);
+        assertThat(event.after().getString(1).toString()).isEqualTo("scooter");
+    }
+
+    @Test
+    void testUpdateEmitsReplaceEventInUpsertMode() throws Exception {

Review Comment:
   Could we also add an ITCase against a real table with `REPLICA IDENTITY 
DEFAULT` in upsert mode (INSERT, UPDATE, DELETE, and an UPDATE that changes the 
primary key)?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to