tchivs created FLINK-40594:
------------------------------
Summary: Pipeline runtime silently truncates TIME(p > 3) to
milliseconds
Key: FLINK-40594
URL: https://issues.apache.org/jira/browse/FLINK-40594
Project: Flink
Issue Type: Bug
Components: Flink CDC
Affects Versions: cdc-3.6.0, cdc-3.5.0
Reporter: tchivs
Flink CDC's pipeline runtime stores TIME as millisecond-of-day, while
{{TimeType}} accepts a precision of up to 9 and the Debezium deserializer
already produces microsecond and nanosecond values. Every {{TIME(p)}} with {{p
> 3}} is therefore silently truncated, and two values that differ only below
the millisecond become indistinguishable.
h2. How to reproduce
{code:java}
LocalTime t = LocalTime.parse("04:05:06.123456");
TimeData d = TimeData.fromLocalTime(t);
System.out.println(d.toLocalTime()); // 04:05:06.123 -- expected
04:05:06.123456
TimeData a = TimeData.fromLocalTime(LocalTime.parse("04:05:06.123456"));
TimeData b = TimeData.fromLocalTime(LocalTime.parse("04:05:06.123999"));
System.out.println(a.equals(b)); // true -- expected false
{code}
Executed against {{flink-cdc-common}} on master ({{9f23c0356}}). The same
storage is present in the {{release-3.5.0}} and {{release-3.6.0}} tags:
{{TimeData}} declares {{private final int millisOfDay}} and contains no
{{toMicroOfDay}}, so both released lines are affected. {{TimeData}} was
introduced with millisecond storage in FLINK-38079, so the behaviour dates from
that change; {{release-3.4}} does not contain the class.
h2. Where the precision is lost
* {{TimeType.MAX_PRECISION = 9}} and the constructor accepts {{0..9}}, so the
type system advertises a precision the data class cannot hold.
* {{TimeData}} keeps {{private final int millisOfDay}}. {{fromMicroOfDay}}
divides by 1000 and {{fromNanoOfDay}} divides by 1_000_000, discarding the
sub-millisecond part at construction. There is no {{toMicroOfDay()}} /
{{toNanoOfDay()}} accessor.
* Sources already deliver sub-millisecond values:
{{DebeziumEventDeserializationSchema}} calls {{TimeData.fromMicroOfDay}} for
{{MicroTime}} and {{TimeData.fromNanoOfDay}} for {{NanoTime}}. The precision is
parsed correctly and then dropped.
* {{TimeDataSerializer}} is a precision-less singleton with a fixed four-byte
encoding: {{getLength()}} returns 4, {{serialize}} writes
{{writeInt(record.toMillisOfDay())}} and {{deserialize}} reads an int.
{{copy(TimeData)}} round-trips through {{toMillisOfDay()}} and is lossy even in
memory. {{BinaryRecordData}}, {{BinaryArrayData}} and
{{GenericRecordDataSerializer}} read TIME as an int as well.
* {{equals}}, {{hashCode}} and {{compareTo}} compare {{millisOfDay}}. Two
{{TIME(6)}} values differing only in microseconds compare equal, so they
collapse during deduplication, ordering and key comparison. This is a
correctness problem, not only a rendering one.
h2. Impact
Any pipeline reading {{TIME(4..9)}} -- PostgreSQL {{time(6)}}, MySQL
{{TIME(6)}}, and anything routed through the Debezium {{MicroTime}} /
{{NanoTime}} semantic types -- loses sub-millisecond data, and TIME keys or
deduplication treat distinct values as identical.
h2. Two existing tests currently hide the defect
* {{TimeDataSerializerTest.getTestData()}} contains
{{TimeData.fromNanoOfDay(102400)}}, {{204800}} and {{409600}}. All three
collapse to zero at construction, so the round-trip assertion passes while the
intended distinct nanosecond values never existed.
* {{PostgresFullTypesITCase}} expects
{{TimeData.fromLocalTime(LocalTime.parse("18:00:22.123456"))}} for the
{{TIME(6)}} column. The expected and the actual value both truncate to
{{18:00:22.123}}, so the assertion passes vacuously.
h2. Suggested direction
Keep nanosecond-of-day inside {{TimeData}}, expose {{toMicroOfDay()}} /
{{toNanoOfDay()}}, and compare on the nanosecond value. Make
{{TimeDataSerializer}} precision-aware, as {{TimestampDataSerializer}} and
{{LocalZonedTimestampDataSerializer}} already are: keep the four-byte
millisecond encoding for {{precision <= 3}} so existing state stays compatible,
and use an eight-byte nanosecond encoding above it behind a versioned
{{TypeSerializerSnapshot}} that can still read the legacy snapshot envelope and
payload. {{toMillisOfDay()}} would keep its signature and truncating behaviour,
so existing callers stay source and binary compatible.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)