codope opened a new issue, #20299:
URL: https://github.com/apache/hudi/issues/20299
### Bug Description
**What happened:**
In Spark CDC queries (`hudi_table_changes(..., 'cdc', ...)`, or the
DataFrame reader with
`hoodie.datasource.query.incremental.format=cdc`), DECIMAL values in the
`before` and `after`
JSON images are wrong. The symptom depends on where the image comes from:
- **Merge-on-Read** (images inferred from base and log files): every DECIMAL
is written as
`{"zero":false}` (or `{"zero":true}`). The value is lost.
- **Copy-on-Write** (images from the supplemental CDC log): every DECIMAL is
written as the raw
bytes of its unscaled value, for example `[0, 0, 3, -25]` for 9.99. You
can only recover the
value if you already know the scale.
DATE and TIMESTAMP values come out as days since epoch and epoch
microseconds on both table
types. They are readable, but don't match the column types.
Downstream consumers of CDC (SCD tables, audit trails, sinks that parse the
images) can't use
any decimal column. Money columns are usually decimals.
**What you expected:**
The images carry the column values in a form JSON readers understand, for
example
`"amt": 9.99` (or `"9.99"`), and the same representation on both table
types. Dates and
timestamps would ideally be ISO strings, or at least documented.
**Steps to reproduce:**
```scala
// spark-shell with hudi-spark3.5-bundle_2.12:1.2.1 and the usual Hudi
session extensions
import org.apache.spark.sql.functions._
import org.apache.spark.sql.SaveMode
def run(tableType: String, path: String) = {
val df = spark.range(0, 3).select(col("id"),
lit(java.math.BigDecimal.valueOf(1.25)).cast("decimal(7,2)").as("amt"),
lit("2026-10-13").cast("date").as("d"),
lit("2026-10-13 10:00:00").cast("timestamp").as("ts"),
lit(1).as("qty"))
val o = Map(
"hoodie.table.name" -> s"cdc_types_$tableType",
"hoodie.datasource.write.recordkey.field" -> "id",
"hoodie.datasource.write.table.type" -> tableType,
"hoodie.table.cdc.enabled" -> "true",
"hoodie.table.cdc.supplemental.logging.mode" -> "DATA_BEFORE_AFTER")
df.write.format("hudi").options(o).mode(SaveMode.Overwrite).save(path)
df.where("id = 1")
.withColumn("amt",
lit(java.math.BigDecimal.valueOf(9.99)).cast("decimal(7,2)"))
.withColumn("qty", lit(2))
.write.format("hudi").options(o + ("hoodie.datasource.write.operation"
-> "upsert"))
.mode("append").save(path)
spark.sql(s"select before, after from hudi_table_changes('$path', 'cdc',
'earliest') where op = 'u'")
.show(false)
}
run("COPY_ON_WRITE", "/tmp/cdc_types_cow")
run("MERGE_ON_READ", "/tmp/cdc_types_mor")
```
Actual output:
```
COPY_ON_WRITE before={"id": 1, "amt": [0, 0, 0, 125], "d": 20739, "ts":
1791865800000000, "qty": 1}
after ={"id": 1, "amt": [0, 0, 3, -25], "d": 20739, "ts":
1791865800000000, "qty": 2}
MERGE_ON_READ
before={"id":1,"amt":{"zero":false},"d":20739,"ts":1791865800000000,"qty":1}
after
={"id":1,"amt":{"zero":false},"d":20739,"ts":1791865800000000,"qty":2}
```
`from_json(after, <table schema>)` returns null for `amt` in both cases.
### Root cause
- **MOR:** `InternalRowToJsonStringConverter.convertField`
(`hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/InternalRowToJsonStringConverter.scala`,
default branch at line 94 on `release-1.2.1`, line 130 on master) has no
`DecimalType` case.
Spark's internal `org.apache.spark.sql.types.Decimal` falls through to
Jackson, which
serializes it as a bean; its only bean-style getter is `isZero()`.
`DateType` and
`TimestampType` fall through as their internal `Int` and `Long`.
- **COW:** `HoodieCDCUtils.recordToJson`
(`hudi-common/.../table/cdc/HoodieCDCUtils.java`,
line 143 on `release-1.2.1`, line 160 on master) is
`GenericData.get().toString(record)`. That
prints Avro decimal logical types as their underlying bytes, and date and
timestamp logical
types as numbers.
Master added VARIANT handling to `convertField` (#19583) but nothing for
DECIMAL, DATE, or
TIMESTAMP.
### Suggested fix
Convert logical types to readable values in both paths:
- `convertField` adds cases for `DecimalType` (`Decimal.toJavaBigDecimal`),
`DateType`, and
`TimestampType`/`TimestampNTZType`.
- `recordToJson` uses a converter that applies Avro logical-type conversions
instead of
`GenericData.toString`.
Add a test with decimal, date, and timestamp columns on both table types,
asserting that the two
produce the same image.
### Environment
**Hudi version:** 1.2.1 (also reproduced by reading master's code path)
**Query engine:** Spark 3.5.6, Scala 2.12, Java 11
### Logs and Stack Trace
_No response_
--
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]