[FLINK-39036][formats] Honor microsecond Avro timestamp logical types - #28071
[FLINK-39036][formats] Honor microsecond Avro timestamp logical types#28071daguimu wants to merge 1 commit into
Conversation
| } | ||
|
|
||
| private static boolean isMicrosLogicalType(Schema schema) { | ||
| final org.apache.avro.LogicalType logicalType = schema.getLogicalType(); |
There was a problem hiding this comment.
nit: this should be an import
There was a problem hiding this comment.
Good catch on cleaning up the FQN. Unfortunately org.apache.flink.table.types.logical.LogicalType is already imported on line 28 and used at ~8 sites in this file (e.g. createConverter(LogicalType type) on line 76, LogicalType[] fieldTypes on line 282, etc.), so an unqualified import org.apache.avro.LogicalType would collide. Using the FQN at this single helper-method call site looked less invasive than FQN-ing every Flink LogicalType reference.
Happy to flip it the other way (import the Avro one, FQN the Flink ones) if you'd prefer — let me know which you'd like to see.
| public Object convert(Schema schema, Object object) { | ||
| return ((TimestampData) object).toInstant().toEpochMilli(); | ||
| final TimestampData timestampData = (TimestampData) object; | ||
| if (isMicrosLogicalType(schema)) { |
There was a problem hiding this comment.
I agree the the idea here, but am concerned that introducing this behaviour might result in a regression for existing applications. I wonder if it would be safer to introduce the new behaviour under a config flag until there is a version change.
There was a problem hiding this comment.
Thanks for the careful read.
My read is that this is a correctness fix rather than a behavior change worth preserving. The read path in AvroToRowDataConverters already honors timestamp-micros / local-timestamp-micros and returns microseconds, while the write path here ignores the same logical type and always emits milliseconds. A pipeline that round-trips through Flink with a timestamp-micros schema silently scales timestamps by 1000× — that's a data-corruption bug, not a stable contract.
The fix is also gated by what the schema declares: users whose schemas are timestamp-millis (the previous behavior for both branches) see no change at all. Only users who already declared timestamp-micros and were silently getting wrong values are affected, and for them the new output is what their schema asked for.
That said, the scope question deserves a committer's call. Could one of the flink-avro maintainers weigh in on whether a config flag is warranted on master? If so, happy to add one as a follow-up.
There was a problem hiding this comment.
Apologies — I need to correct my earlier comment. I claimed AvroToRowDataConverters already honors timestamp-micros, but on closer reading it does not: convertToTimestamp treats every incoming Long as milliseconds regardless of the schema's logical type, and there's no Conversions.addLogicalTypeConversion registered anywhere in flink-avro.
That means today both read and write ignore the logical type symmetrically, which is why a Flink-internal round-trip with a timestamp-micros schema happens to round-trip correctly (two bugs cancel out). Landing only the write-side fix in this PR would break that internal round-trip — exactly the regression you flagged. Your concern was well-founded; I had it wrong.
I'll expand the scope of this PR to also fix the read path so the bug is addressed symmetrically. That removes the regression risk and makes a config flag unnecessary. Will push a follow-up commit and update the PR title accordingly.
7c3ee37 to
5cf66e9
Compare
Both directions of the flink-avro RowData<->GenericRecord conversion ignored the Avro `timestamp-micros` / `local-timestamp-micros` logical types and treated the long-encoded epoch value as milliseconds. Because the bug was symmetric, a Flink-internal round-trip happened to round-trip correctly while any cross-system round-trip drifted by a factor of 1000. Write side (RowDataToAvroConverters): * Inspect the target schema in each TIMESTAMP / TIMESTAMP_WITH_LOCAL_TIME_ZONE branch (legacy and non-legacy mapping). When the schema declares `timestamp-micros` or `local-timestamp-micros`, emit the value in microseconds; otherwise keep the existing millisecond output. Read side (AvroToRowDataConverters): * `convertToTimestamp` now takes the Flink type's precision and treats the incoming long as microseconds when precision > 3, matching the precision mapping that AvroSchemaConverter establishes between the Avro logical type and the Flink TIMESTAMP / TIMESTAMP_WITH_LOCAL_TIME_ZONE precision. Negative epoch values are split with `Math.floorDiv` / `Math.floorMod` so the sub-millisecond component stays in the [0, 999_999] range that `TimestampData.fromEpochMillis(long, int)` requires. Tests: * RowDataToAvroConvertersTest covers each writer branch (with-zone, without-zone, legacy) under both micros and millis schemas. * AvroToRowDataConvertersTest covers the reader for `timestamp-micros`, `local-timestamp-micros`, the millis regression case, and pre-1970 negative micros values that exercise floor semantics. * Two end-to-end round-trip tests use both converters together to assert that a TimestampData written under a `timestamp-micros` / `timestamp-millis` schema decodes back to the same TimestampData. Closes #FLINK-39036
5cf66e9 to
aae33cc
Compare
|
@davidradl summary of the new revision (force-pushed to Scope change. Following up on the discussion above — the writer-only fix would have flipped a symmetric bug into an asymmetric one and broken Flink-internal round-trips with a
Tests. A new PR title and description are updated to reflect the bidirectional scope. The |
|
This PR is being marked as stale since it has not had any activity in the last 90 days. If you are having difficulty finding a reviewer, please reach out to the If this PR is no longer valid or desired, please feel free to close it. |
What is the purpose of the change
Fix FLINK-39036: both directions of the flink-avro RowData ⇄ GenericRecord conversion ignored the Avro
timestamp-micros/local-timestamp-microslogical types and treated the long-encoded epoch value as milliseconds. Because the bug was symmetric, a Flink-internal round-trip happened to round-trip correctly while any cross-system round-trip drifted by a factor of 1000. The first revision of this PR addressed only the writer, which would have flipped that asymmetry the wrong way (writes correct, reads still wrong → internal round-trip broken). The current revision fixes both sides so reads and writes agree on the logical type.Brief change log
flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/RowDataToAvroConverters.javaTIMESTAMP_WITHOUT_TIME_ZONEandTIMESTAMP_WITH_LOCAL_TIME_ZONEconverter (legacy and non-legacy mapping) check whether the target schema declarestimestamp-micros/local-timestamp-micros. If yes, emit the value in microseconds; otherwise keep the existing millisecond output.isMicrosLogicalType(Schema)andtoEpochMicros(Instant).flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroToRowDataConverters.javaconvertToTimestampnow takes the Flink type's precision and treats the incomingLongas microseconds whenprecision > 3, matching the precision mapping thatAvroSchemaConverteralready establishes between the Avro logical type and the FlinkTIMESTAMP/TIMESTAMP_WITH_LOCAL_TIME_ZONEprecision.Math.floorDiv/Math.floorModso the sub-millisecond component stays in the[0, 999_999]range thatTimestampData.fromEpochMillis(long, int)requires.flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/RowDataToAvroConvertersTest.java(new)flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroToRowDataConvertersTest.java(new)timestamp-micros,local-timestamp-micros, the millis regression case, and a pre-1970 negative micros case that exercises floor semantics.TimestampDataround-trips through atimestamp-microsschema and atimestamp-millisschema.Verifying this change
This change is covered by the new tests:
RowDataToAvroConvertersTest#testTimestampWithLocalTimeZoneRespectsMicrosLogicalType—LocalZonedTimestampType(6)writer emits1_704_164_645_123_456Lunder the micros schema and1_704_164_645_123Lunder the millis schema.RowDataToAvroConvertersTest#testTimestampWithoutTimeZoneRespectsLocalMicrosLogicalType—TimestampType(6)writer emits the same numeric values for thelocal-timestamp-micros/-millisschemas.RowDataToAvroConvertersTest#testLegacyTimestampMappingRespectsMicrosLogicalType—legacyTimestampMapping=truepath still honourstimestamp-micros.AvroToRowDataConvertersTest#testTimestampMicrosLogicalTypeReadAsMicrosand#testLocalTimestampMicrosLogicalTypeReadAsMicros— reader returns the correct sub-millisecondTimestampDatafor the two micros logical types.AvroToRowDataConvertersTest#testTimestampMillisPrecisionPreservesExistingBehavior— guards the existing millis behaviour forprecision <= 3.AvroToRowDataConvertersTest#testNegativeMicrosTimestampHandlesFloorSemantics— pre-1970 micros value (-1500L) decodes to(-2 ms, 500_000 ns).AvroToRowDataConvertersTest#testRoundTripPreservesMicrosPrecisionand#testRoundTripPreservesMillisPrecision— write-then-read round-trips of aTimestampDataare no-ops under bothtimestamp-microsandtimestamp-millisschemas.Does this pull request potentially affect one of the following parts:
@Public(Evolving): noisMicrosboolean on the reader side)Documentation