hudi-agent commented on code in PR #19110: URL: https://github.com/apache/hudi/pull/19110#discussion_r3686865550
########## hudi-utilities/src/main/java/org/apache/hudi/utilities/transform/debezium/AbstractDebeziumTransformer.java: ########## @@ -0,0 +1,294 @@ +/* + * 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.hudi.utilities.transform.debezium; + +import org.apache.hudi.common.config.TypedProperties; +import org.apache.hudi.common.model.debezium.DebeziumConstants; +import org.apache.hudi.common.util.ConfigUtils; +import org.apache.hudi.common.util.Option; +import org.apache.hudi.utilities.config.DebeziumTransformerConfig; +import org.apache.hudi.utilities.transform.Transformer; + +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.functions; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.apache.hudi.config.HoodieErrorTableConfig.ERROR_TABLE_ENABLED; +import static org.apache.hudi.utilities.streamer.BaseErrorTableWriter.ERROR_TABLE_CURRUPT_RECORD_COL_NAME; + +/** + * Base {@link Transformer} that flattens a Debezium change-event envelope into a Hudi row. + * + * <p>A Debezium change event is a nested record of the form + * {@code {op, ts_ms, before:{...}, after:{...}, source:{...}}}. This transformer: + * <ul> + * <li>selects the {@code before} image for deletes and the {@code after} image otherwise, + * and explodes it to the row's top level;</li> + * <li>surfaces the common Debezium metadata columns (operation type, processing/origin + * timestamps, shard) along with any database-specific metadata columns supplied by the + * subclass;</li> + * <li>optionally nests the metadata columns under a single {@code _debezium_metadata} struct + * (see {@link DebeziumTransformerConfig#ENABLE_NESTED_FIELDS});</li> + * <li>optionally preserves the error-table corrupt-record column when the error table is + * enabled;</li> + * <li>applies an optional database-specific post-processing step (e.g. ordering/sequence + * columns, LSN defaulting);</li> + * <li>normalizes column nullability (see + * {@link DebeziumTransformerConfig#SCHEMA_AS_NULLABLE}).</li> + * </ul> + * + * <p>The flattened column names are defined in {@link DebeziumConstants}; the matching + * {@code DebeziumAvroPayload} implementations rely on these names for merge/ordering semantics. + * + * <p>The layout and nullability behavior are configured through {@link DebeziumTransformerConfig}. + * + * <p>Subclasses configure the database-specific behavior purely through the constructor; there is + * no abstract method to implement. + */ +public class AbstractDebeziumTransformer implements Transformer { + + private static final String DATA_FIELD = "__data"; + // Bare name of the optional {@code schema} field inside the Debezium {@code source} struct + // (INCOMING_SOURCE_SCHEMA_FIELD is the fully-qualified {@code source.schema} path). + private static final String SOURCE_SCHEMA_FIELD_NAME = "schema"; + + private static final List<Column> DEFAULT_ROOT_LEVEL_METADATA_COLUMNS = Arrays.asList( + new Column(DebeziumConstants.INCOMING_OP_FIELD).alias(DebeziumConstants.FLATTENED_OP_COL_NAME)); + + private static final List<Column> DEFAULT_NESTED_METADATA_COLUMNS = Arrays.asList( Review Comment: 🤖 nit: the field `postProcessingOption` encodes the type in the name — since the declared type is already `Option<...>`, `postProcessing` reads more cleanly and avoids the redundancy. <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.</i></sub> ########## hudi-utilities/src/main/java/org/apache/hudi/utilities/transform/debezium/MysqlDebeziumTransformer.java: ########## @@ -0,0 +1,82 @@ +/* + * 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.hudi.utilities.transform.debezium; + +import org.apache.hudi.common.model.debezium.DebeziumConstants; +import org.apache.hudi.common.util.Option; + +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.functions; + +import java.util.Arrays; +import java.util.List; + +/** + * {@link AbstractDebeziumTransformer} for MySQL Debezium change events. + * + * <p>Surfaces the MySQL binlog coordinates ({@code file}, {@code pos}, {@code row}) as the flattened + * {@code _event_bin_file}, {@code _event_pos} and {@code _event_row} columns, and derives the + * {@code _event_seq} ordering column as {@code "<binlog-file-suffix>.<pos>"} (e.g. {@code "000001.100"} + * for a binlog file {@code "mysql-bin.000001"} at position {@code 100}). {@code _event_seq} is the + * precombine/ordering field consumed by {@code MySqlDebeziumAvroPayload}. + * + * <p>Metadata is flattened to the root level by default; set + * {@code hoodie.streamer.transformer.debezium.nested.fields.enable=true} to group it under a + * {@code _debezium_metadata} struct instead. + */ +public class MysqlDebeziumTransformer extends AbstractDebeziumTransformer { + + private static final List<Column> MYSQL_METADATA = Arrays.asList( + new Column(DebeziumConstants.INCOMING_SOURCE_FILE_FIELD).alias(DebeziumConstants.FLATTENED_FILE_COL_NAME), + new Column(DebeziumConstants.INCOMING_SOURCE_POS_FIELD).alias(DebeziumConstants.FLATTENED_POS_COL_NAME), + new Column(DebeziumConstants.INCOMING_SOURCE_ROW_FIELD).alias(DebeziumConstants.FLATTENED_ROW_COL_NAME)); + + public MysqlDebeziumTransformer() { + super(MYSQL_METADATA, Option.of(MysqlDebeziumTransformer::applySeqNo)); + } + + /** + * Builds the {@code _event_seq} ordering column from the binlog file and position. The file column + * holds a name like {@code "mysql-bin.000001"}; only the numeric suffix after the last dot is used, + * yielding a sequence such as {@code "000001.100"}. Handles both the flat and nested metadata + * layouts (reading {@code file}/{@code pos} from the {@code _debezium_metadata} struct when nested). + * + * @param dataset flattened MySQL Debezium dataset. + * @return dataset with the {@code _event_seq} column added. + */ + private static Dataset<Row> applySeqNo(Dataset<Row> dataset) { + boolean isNested = Arrays.asList(dataset.columns()).contains(DEBEZIUM_METADATA_FIELD); + + Column fileCol = isNested + ? dataset.col(DEBEZIUM_METADATA_FIELD + "." + DebeziumConstants.FLATTENED_FILE_COL_NAME) + : dataset.col(DebeziumConstants.FLATTENED_FILE_COL_NAME); + + Column posCol = isNested + ? dataset.col(DEBEZIUM_METADATA_FIELD + "." + DebeziumConstants.FLATTENED_POS_COL_NAME) + : dataset.col(DebeziumConstants.FLATTENED_POS_COL_NAME); + + return dataset.withColumn(DebeziumConstants.ADDED_SEQ_COL_NAME, functions.concat( Review Comment: 🤖 Worth adding that the deferred failure is asymmetric, which strengthens the fail-fast case. In `MySqlDebeziumAvroPayload.shouldPickCurrentRecord`, a null `_event_seq` on the *incoming* record throws `HoodieDebeziumAvroPayloadException`, but a null seq on the *stored* record is swallowed by the bootstrap branch (`!currentSourceSeqOpt.isPresent()` → return false), so that record gets overwritten regardless of ordering rather than erroring. And a first-write of a malformed event goes through the insert path, so it can land silently and only trip later; `preCombine` would then hit `Long.parseLong("null")` → `NumberFormatException`. A guard in `applySeqNo` avoids all three of those less-obvious paths. ########## hudi-utilities/src/main/java/org/apache/hudi/utilities/transform/debezium/PostgresDebeziumTransformer.java: ########## @@ -0,0 +1,84 @@ +/* + * 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.hudi.utilities.transform.debezium; + +import org.apache.hudi.common.model.debezium.DebeziumConstants; +import org.apache.hudi.common.util.Option; + +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; + +import java.util.Arrays; +import java.util.List; + +import static org.apache.spark.sql.functions.expr; +import static org.apache.spark.sql.functions.when; + +/** + * {@link AbstractDebeziumTransformer} for Postgres Debezium change events. + * + * <p>Surfaces the Postgres-specific source metadata ({@code txId}, {@code lsn}, {@code xmin}) as the + * flattened {@code _event_tx_id}, {@code _event_lsn} and {@code _event_xmin} columns. The + * {@code _event_lsn} column is the ordering field used by {@code PostgresDebeziumAvroPayload}. + * + * <p>Post-processing defaults a null {@code _event_lsn} to {@code 0} for snapshot records, since the + * LSN is not populated for rows produced by Debezium incremental snapshots + * (see <a href="https://debezium.io/blog/2021/10/07/incremental-snapshots/">incremental snapshots</a>). + * + * <p>Metadata is flattened to the root level by default; set + * {@code hoodie.streamer.transformer.debezium.nested.fields.enable=true} to group it under a + * {@code _debezium_metadata} struct instead. When nested, the {@code _event_lsn} and operation-type + * columns stay at the root level so payload ordering keeps working. + */ +public class PostgresDebeziumTransformer extends AbstractDebeziumTransformer { + + // Nestable Postgres metadata (grouped under _debezium_metadata when nesting is enabled). + private static final List<Column> POSTGRES_METADATA = Arrays.asList( + new Column(DebeziumConstants.INCOMING_SOURCE_TXID_FIELD).alias(DebeziumConstants.FLATTENED_TX_ID_COL_NAME), + new Column(DebeziumConstants.INCOMING_SOURCE_XMIN_FIELD).alias(DebeziumConstants.FLATTENED_XMIN_COL_NAME)); + + // The LSN is the payload's ordering field, so it is kept at the root level in every layout. + private static final List<Column> POSTGRES_ORDERING_COLUMNS = Arrays.asList( + new Column(DebeziumConstants.INCOMING_SOURCE_LSN_FIELD).alias(DebeziumConstants.FLATTENED_LSN_COL_NAME)); + + public PostgresDebeziumTransformer() { + super(POSTGRES_METADATA, POSTGRES_ORDERING_COLUMNS, Option.of(PostgresDebeziumTransformer::useDefaultValuesForLsnIfNull)); + } + + /** + * Defaults a null {@code _event_lsn} to {@code 0} for snapshot records. The LSN is null when a + * table is added via a Debezium incremental snapshot; leaving it null would break LSN-based + * ordering in the payload. + * + * @param dataset flattened Postgres Debezium dataset. + * @return dataset where null {@code _event_lsn} values on snapshot rows are replaced with 0. + */ + private static Dataset<Row> useDefaultValuesForLsnIfNull(Dataset<Row> dataset) { + if (!Arrays.asList(dataset.columns()).contains(DebeziumConstants.FLATTENED_LSN_COL_NAME)) { + return dataset; + } + + return dataset.withColumn(DebeziumConstants.FLATTENED_LSN_COL_NAME, when( Review Comment: 🤖 nit: `useDefaultValuesForLsnIfNull` is a bit verbose and reads more like a description than a method name. Something like `applySnapshotLsnDefault` or `defaultNullSnapshotLsn` would be punchier and still self-documenting. <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.</i></sub> ########## hudi-utilities/src/main/java/org/apache/hudi/utilities/transform/debezium/AbstractDebeziumTransformer.java: ########## @@ -0,0 +1,294 @@ +/* + * 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.hudi.utilities.transform.debezium; + +import org.apache.hudi.common.config.TypedProperties; +import org.apache.hudi.common.model.debezium.DebeziumConstants; +import org.apache.hudi.common.util.ConfigUtils; +import org.apache.hudi.common.util.Option; +import org.apache.hudi.utilities.config.DebeziumTransformerConfig; +import org.apache.hudi.utilities.transform.Transformer; + +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.functions; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.apache.hudi.config.HoodieErrorTableConfig.ERROR_TABLE_ENABLED; +import static org.apache.hudi.utilities.streamer.BaseErrorTableWriter.ERROR_TABLE_CURRUPT_RECORD_COL_NAME; + +/** + * Base {@link Transformer} that flattens a Debezium change-event envelope into a Hudi row. + * + * <p>A Debezium change event is a nested record of the form + * {@code {op, ts_ms, before:{...}, after:{...}, source:{...}}}. This transformer: + * <ul> + * <li>selects the {@code before} image for deletes and the {@code after} image otherwise, + * and explodes it to the row's top level;</li> + * <li>surfaces the common Debezium metadata columns (operation type, processing/origin + * timestamps, shard) along with any database-specific metadata columns supplied by the + * subclass;</li> + * <li>optionally nests the metadata columns under a single {@code _debezium_metadata} struct + * (see {@link DebeziumTransformerConfig#ENABLE_NESTED_FIELDS});</li> + * <li>optionally preserves the error-table corrupt-record column when the error table is + * enabled;</li> + * <li>applies an optional database-specific post-processing step (e.g. ordering/sequence + * columns, LSN defaulting);</li> + * <li>normalizes column nullability (see + * {@link DebeziumTransformerConfig#SCHEMA_AS_NULLABLE}).</li> + * </ul> + * + * <p>The flattened column names are defined in {@link DebeziumConstants}; the matching + * {@code DebeziumAvroPayload} implementations rely on these names for merge/ordering semantics. Review Comment: 🤖 nit: the class is named `AbstractDebeziumTransformer` but has no abstract methods — the javadoc even calls this out explicitly. Could you rename it to `BaseDebeziumTransformer`? The `Abstract` prefix signals "extend me and implement the abstract contract," which is misleading when the contract is purely constructor-based. <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.</i></sub> -- 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]
