dev-donghwan commented on code in PR #4618:
URL: https://github.com/apache/flink-cdc/pull/4618#discussion_r4553969937
##########
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:
Thanks a lot for this, it helped me harden the change considerably. I added
fail-fast handling and probed the TOAST-able types with REPLICA IDENTITY
DEFAULT + pgoutput (snapshot a large value, then update another column or the
primary key). While adding the primary-key-change case you asked for, I also
found that a PK change is emitted as DELETE + INSERT, so the check now covers
INSERT events in upsert mode as well.
| Case | Before this change | Now |
|---|---|---|
| text, jsonb, hstore (json mode), bytea (bytes mode), non-key UPDATE |
REPLACE with `__debezium_unavailable_value` | job fails with a REPLICA IDENTITY
FULL hint |
| text, PK-changing UPDATE | INSERT with `__debezium_unavailable_value` |
job fails with a REPLICA IDENTITY FULL hint |
| text[], int[], bytea (base64 / hex mode) | REPLACE / INSERT with **NULL**
| still **NULL** (documented limitation) |
All rows are covered by ITCases in `PostgresPipelineITCase`. The null cases
come from Debezium 1.9.8, which has no placeholder for these types, so they
can't be told apart from real nulls in the deserializer. I've asked the
maintainers in the conversation tab how they'd like to handle them.
##########
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:
Added `testUpsertModeWithReplicaIdentityDefault` in
`PostgresPipelineITCase`. It creates the source through
`PostgresDataSourceFactory` and, after the snapshot is observed, checks INSERT
/ UPDATE (REPLACE) / PK-changing UPDATE / DELETE on a REPLICA IDENTITY DEFAULT
table, which also covers your ITCase comment.
##########
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:
Documented the current behavior: unchanged TOASTed string, JSON and binary
(bytes mode) values fail the source, while arrays and binary values in
base64/hex mode are emitted as null because Debezium 1.9.8 has no placeholder
for them. See the thread on `DebeziumEventDeserializationSchema` for details.
##########
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:
Good point. Added `PostgresSchemaUtils#getTableSchemas`, which refreshes the
schema once and resolves each table via `tableFor`; the validation now uses it,
and `getTableSchema` shares the same helper. Also added
`testChangelogModeUpsertReportsOnlyTablesWithoutPrimaryKey` to cover the
multi-table path (a table with and a table without a primary key).
##########
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:
Aligned the option description with the docs.
--
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]