[ 
https://issues.apache.org/jira/browse/FLINK-40594?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18112627#comment-18112627
 ] 

tchivs commented on FLINK-40594:
--------------------------------

I would like to take this one.

My understanding of the root cause: the type system and the source path already 
carry sub-millisecond TIME, and only the pipeline runtime's storage narrows it. 
{{TimeType}} accepts {{0..9}} and {{DebeziumEventDeserializationSchema}} builds 
values through {{fromMicroOfDay}} / {{fromNanoOfDay}}, but {{TimeData}} holds 
{{int millisOfDay}}, so the fraction is dropped at construction and cannot be 
recovered downstream. The equality/ordering collapse follows from the same 
field, which is why I treat this as a correctness issue rather than a rendering 
one.

Design I would propose, keeping state compatibility intact:

* {{TimeData}} stores nanosecond-of-day and exposes {{toMicroOfDay()}} / 
{{toNanoOfDay()}}; {{toMillisOfDay()}} keeps its signature and truncating 
behaviour, so callers stay source and binary compatible.
* {{TimeDataSerializer}} becomes precision-aware, following the existing shape 
of {{TimestampDataSerializer}} and {{LocalZonedTimestampDataSerializer}} rather 
than inventing a new pattern:
** {{precision <= 3}} keeps the historical four-byte millisecond encoding and 
resolves as {{isCompatibleAsIs}};
** {{precision > 3}} uses an eight-byte nanosecond-of-day encoding and resolves 
as {{isCompatibleAfterMigration}}, through a versioned 
{{TypeSerializerSnapshot}} that still reads the legacy snapshot envelope and 
four-byte payload.
* The binary writers/readers and {{InternalSerializers}} thread the declared 
precision through.

POC status: I have this implemented on top of master ({{9f23c0356}}) and 
verified locally.

* {{flink-cdc-common}}: 46 tests pass, including a new {{TimeDataTest}}.
* {{flink-cdc-runtime}}: 992 tests pass, 0 failures. {{TimeDataSerializerTest}} 
becomes an abstract base with concrete {{TimeDataSerializer3Test}} / {{6Test}} 
/ {{9Test}}, plus a {{TimeDataSerializerCompatibilityTest}} that asserts 
{{isCompatibleAsIs}} for the millisecond encoding, 
{{isCompatibleAfterMigration}} for the widened one, and successful reading of 
the legacy envelope and four-byte payload.
* {{flink-cdc-composer}}: {{FlinkPipelineTransformITCase}} passes. Two 
expectations that pinned the truncation are corrected rather than relaxed: the 
{{TIME(6)}}/{{TIME(9)}} transform expectation now asserts the retained 
fraction, and the temporal-function check compares {{LOCALTIME}} at 
whole-second granularity, since {{LOCALTIME}} and {{CURRENT_TIME}} are 
{{TIME(0)}} and previously carried a fraction their declared type does not have.
* {{PostgresFullTypesITCase}} gains a direct microsecond-of-day assertion so 
the existing {{TIME(6)}} expectation can no longer pass vacuously.
* Spotless passes.

Happy to adjust the direction if the community prefers a different 
compatibility strategy. I will open the PR against master once this is assigned.

> 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.5.0, cdc-3.6.0
>            Reporter: tchivs
>            Priority: Major
>
> 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)

Reply via email to