Skip to content

Honour the sign bit when deserializing TIME2 - #181

Open
PDGGK wants to merge 1 commit into
osheroff:masterfrom
PDGGK:fix-negative-time
Open

PDGGK wants to merge 1 commit into
osheroff:masterfrom
PDGGK:fix-negative-time

Conversation

@PDGGK

@PDGGK PDGGK commented Sep 5, 2026

Copy link
Copy Markdown

Fixes #154.

MySQL TIME is a signed duration in -838:59:59 .. 838:59:59. The leading bit of the packed TIME2 payload carries that sign — the comment in deserializeTimeV2 already documents it — but the code sliced hour, minute and second straight out of the raw value:

long time = bigEndianLong(inputStream.read(3), 0, 3);
Long timestamp = asUnixTime(1970, 1, 1,
    bitSlice(time, 2, 10, 24),
    bitSlice(time, 12, 6, 24),
    bitSlice(time, 18, 6, 24),
    fsp / 1000);

So every negative TIME came back as a large positive one. -00:00:01 decoded to 3686643000, which is the number in #154.

What the fix does

MySQL stores a negative TIME as the two's complement of the whole packed payload, fractional bytes included, so the sign has to come off before any field is sliced out and the fractional bytes have to be part of that same complement. This reads all 3 + (meta + 1) / 2 bytes at once, takes the complement when the leading bit is clear, slices, and negates the result.

That the fraction shares the complement is not a detail I could take on faith, so I checked it against a live server. mysql:8.0.46, row-format binlog, one table with TIME, TIME(3) and TIME(6) columns:

-16:08:04        t0 = 7e fd fc
-16:08:04.250    t3 = 7e fd fb | f6 3c
-16:08:04.250    t6 = 7e fd fb | fc 2f 70

The integer part is fb in the fractional rows and fc in the plain one — MySQL's intpart++ / frac -= 0x1<<n for a negative value with a fraction. Complementing only the first three bytes would get those rows wrong; complementing the whole payload gets them right.

Tests

Two cases in AbstractRowsEventDataDeserializerTest, using payloads taken from that binlog rather than hand-derived, covering both signs and all three fractional widths:

value payload before after
16:08:04 81 02 04 58084000 58084000
100:08:04 86 42 04 360484000 360484000
00:00:00 80 00 00 0 0
-00:00:01 7f ff ff 3686643000 -1000
-16:08:04 7e fd fc 3628560000 -58084000
-16:08:04.250 7e fd fb f6 3c 3628565303 -58084250
-16:08:04.250000 7e fd fb fc 2f 70 3628565303 -58084250

Reverting the change fails both new tests with expected [-1000] but found [3686643000] and expected [-58084250] but found [3628565303], and leaves the existing testFrom green. mvn test — 69 tests, all passing.

Positive values are untouched, including those above 24 hours, which TIME allows and which already decoded correctly.

Scope

Debezium is not affected: RowDeserializers in debezium-connector-binlog overrides all three row deserializers with its own TIME2 implementation that already handles the sign. That is most likely why this issue stalled — the reporter was running this library's own tests while the check in the thread was made through Debezium.

MySQL TIME is a signed duration. The leading bit of the packed TIME2
payload carries that sign, and MySQL stores a negative value as the two's
complement of the whole payload, fractional bytes included. The
deserializer sliced hour, minute and second straight out of the raw
value, so every negative TIME came back as a large positive one:
-00:00:01 decoded to 3686643000 rather than -1000.

Take the sign off the whole payload before slicing any field, and negate
the result. Positive values, including the ones above 24 hours that TIME
allows, are unchanged.

Fixes osheroff#154.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect handling of negative TIME values

1 participant