Skip to content

Commit 5a9943c

Browse files
committed
Backported time deserialization from master
1 parent 8dbccfd commit 5a9943c

File tree

2 files changed

+211
-70
lines changed

2 files changed

+211
-70
lines changed

src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AbstractRowsEventDataDeserializer.java

Lines changed: 144 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,38 @@
2525
import java.util.BitSet;
2626
import java.util.Calendar;
2727
import java.util.Map;
28+
import java.util.TimeZone;
2829

2930
/**
3031
* Whole class is basically a mix of <a href="https://code.google.com/p/open-replicator">open-replicator</a>'s
3132
* AbstractRowEventParser and MySQLUtils. Main purpose here is to ease rows deserialization.<p>
3233
*
3334
* Current {@link ColumnType} to java type mapping is following:
3435
* <pre>
35-
* Integer: {@link ColumnType#TINY}, {@link ColumnType#SHORT}, {@link ColumnType#LONG}, {@link ColumnType#INT24},
36-
* {@link ColumnType#YEAR}, {@link ColumnType#ENUM}, {@link ColumnType#SET},
37-
* Long: {@link ColumnType#LONGLONG},
38-
* Float: {@link ColumnType#FLOAT},
39-
* Double: {@link ColumnType#DOUBLE},
40-
* String: {@link ColumnType#VARCHAR}, {@link ColumnType#VAR_STRING}, {@link ColumnType#STRING},
41-
* java.util.BitSet: {@link ColumnType#BIT},
42-
* java.util.Date: {@link ColumnType#DATETIME}, {@link ColumnType#DATETIME_V2},
43-
* java.math.BigDecimal: {@link ColumnType#NEWDECIMAL},
44-
* java.sql.Timestamp: {@link ColumnType#TIMESTAMP}, {@link ColumnType#TIMESTAMP_V2},
45-
* java.sql.Date: {@link ColumnType#DATE},
46-
* java.sql.Time: {@link ColumnType#TIME}, {@link ColumnType#TIME_V2},
47-
* byte[]: {@link ColumnType#BLOB},
36+
* {@link ColumnType#TINY}: Integer
37+
* {@link ColumnType#SHORT}: Integer
38+
* {@link ColumnType#LONG}: Integer
39+
* {@link ColumnType#INT24}: Integer
40+
* {@link ColumnType#YEAR}: Integer
41+
* {@link ColumnType#ENUM}: Integer
42+
* {@link ColumnType#SET}: Long
43+
* {@link ColumnType#LONGLONG}: Long
44+
* {@link ColumnType#FLOAT}: Float
45+
* {@link ColumnType#DOUBLE}: Double
46+
* {@link ColumnType#BIT}: java.util.BitSet
47+
* {@link ColumnType#DATETIME}: java.util.Date
48+
* {@link ColumnType#DATETIME_V2}: java.util.Date
49+
* {@link ColumnType#NEWDECIMAL}: java.math.BigDecimal
50+
* {@link ColumnType#TIMESTAMP}: java.sql.Timestamp
51+
* {@link ColumnType#TIMESTAMP_V2}: java.sql.Timestamp
52+
* {@link ColumnType#DATE}: java.sql.Date
53+
* {@link ColumnType#TIME}: java.sql.Time
54+
* {@link ColumnType#TIME_V2}: java.sql.Time
55+
* {@link ColumnType#VARCHAR}: String
56+
* {@link ColumnType#VAR_STRING}: String
57+
* {@link ColumnType#STRING}: String
58+
* {@link ColumnType#BLOB}: byte[]
59+
* {@link ColumnType#GEOMETRY}: byte[]
4860
* </pre>
4961
*
5062
* At the moment {@link ColumnType#GEOMETRY} is unsupported.
@@ -208,101 +220,83 @@ protected Serializable deserializeDate(ByteArrayInputStream inputStream) throws
208220
value >>>= 5;
209221
int month = value % 16;
210222
int year = value >> 4;
211-
Calendar cal = Calendar.getInstance();
212-
cal.clear();
213-
cal.set(Calendar.YEAR, year);
214-
cal.set(Calendar.MONTH, month - 1);
215-
cal.set(Calendar.DATE, day);
216-
return new java.sql.Date(cal.getTimeInMillis());
223+
Long timestamp = asUnixTime(year, month, day, 0, 0, 0, 0);
224+
return timestamp != null ? new java.sql.Date(timestamp) : null;
217225
}
218226

219227
protected Serializable deserializeTime(ByteArrayInputStream inputStream) throws IOException {
220228
int value = inputStream.readInteger(3);
221229
int[] split = split(value, 100, 3);
222-
Calendar c = Calendar.getInstance();
223-
c.clear();
224-
c.set(Calendar.HOUR_OF_DAY, split[2]);
225-
c.set(Calendar.MINUTE, split[1]);
226-
c.set(Calendar.SECOND, split[0]);
227-
return new java.sql.Time(c.getTimeInMillis());
230+
Long timestamp = asUnixTime(1970, 1, 1, split[2], split[1], split[0], 0);
231+
return timestamp != null ? new java.sql.Time(timestamp) : null;
228232
}
229233

230234
protected Serializable deserializeTimeV2(int meta, ByteArrayInputStream inputStream) throws IOException {
231235
/*
232-
in big endian:
236+
(in big endian)
233237
234238
1 bit sign (1= non-negative, 0= negative)
235239
1 bit unused (reserved for future extensions)
236240
10 bits hour (0-838)
237241
6 bits minute (0-59)
238242
6 bits second (0-59)
239-
= (3 bytes in total)
240-
+
241-
fractional-seconds storage (size depends on meta)
243+
244+
(3 bytes in total)
245+
246+
+ fractional-seconds storage (size depends on meta)
242247
*/
243248
long time = bigEndianLong(inputStream.read(3), 0, 3);
244-
Calendar c = Calendar.getInstance();
245-
c.clear();
246-
c.set(Calendar.HOUR_OF_DAY, extractBits(time, 2, 10, 24));
247-
c.set(Calendar.MINUTE, extractBits(time, 12, 6, 24));
248-
c.set(Calendar.SECOND, extractBits(time, 18, 6, 24));
249-
c.set(Calendar.MILLISECOND, deserializeFractionalSeconds(meta, inputStream));
250-
return new java.sql.Time(c.getTimeInMillis());
249+
Long timestamp = asUnixTime(1970, 1, 1,
250+
bitSlice(time, 2, 10, 24),
251+
bitSlice(time, 12, 6, 24),
252+
bitSlice(time, 18, 6, 24),
253+
deserializeFractionalSeconds(meta, inputStream)
254+
);
255+
return timestamp != null ? new java.sql.Time(timestamp) : null;
251256
}
252257

253258
protected Serializable deserializeTimestamp(ByteArrayInputStream inputStream) throws IOException {
254-
long value = inputStream.readLong(4);
255-
return new java.sql.Timestamp(value * 1000L);
259+
return new java.sql.Timestamp(inputStream.readLong(4) * 1000);
256260
}
257261

258262
protected Serializable deserializeTimestampV2(int meta, ByteArrayInputStream inputStream) throws IOException {
259-
// big endian
260-
long timestamp = bigEndianLong(inputStream.read(4), 0, 4);
261-
Calendar c = Calendar.getInstance();
262-
c.setTimeInMillis(timestamp * 1000);
263-
c.set(Calendar.MILLISECOND, deserializeFractionalSeconds(meta, inputStream));
264-
return new java.sql.Timestamp(c.getTimeInMillis());
263+
return new java.sql.Timestamp(bigEndianLong(inputStream.read(4), 0, 4) * 1000 +
264+
deserializeFractionalSeconds(meta, inputStream));
265265
}
266266

267267
protected Serializable deserializeDatetime(ByteArrayInputStream inputStream) throws IOException {
268-
long value = inputStream.readLong(8);
269-
int[] split = split(value, 100, 6);
270-
Calendar c = Calendar.getInstance();
271-
c.set(Calendar.YEAR, split[5]);
272-
c.set(Calendar.MONTH, split[4] - 1);
273-
c.set(Calendar.DAY_OF_MONTH, split[3]);
274-
c.set(Calendar.HOUR_OF_DAY, split[2]);
275-
c.set(Calendar.MINUTE, split[1]);
276-
c.set(Calendar.SECOND, split[0]);
277-
c.set(Calendar.MILLISECOND, 0);
278-
return c.getTime();
268+
int[] split = split(inputStream.readLong(8), 100, 6);
269+
Long timestamp = asUnixTime(split[5], split[4], split[3], split[2], split[1], split[0], 0);
270+
return timestamp != null ? new java.util.Date(timestamp) : null;
279271
}
280272

281273
protected Serializable deserializeDatetimeV2(int meta, ByteArrayInputStream inputStream) throws IOException {
282274
/*
283-
in big endian:
275+
(in big endian)
284276
285277
1 bit sign (1= non-negative, 0= negative)
286278
17 bits year*13+month (year 0-9999, month 0-12)
287279
5 bits day (0-31)
288280
5 bits hour (0-23)
289281
6 bits minute (0-59)
290282
6 bits second (0-59)
291-
= (5 bytes in total)
292-
+
293-
fractional-seconds storage (size depends on meta)
283+
284+
(5 bytes in total)
285+
286+
+ fractional-seconds storage (size depends on meta)
294287
*/
295288
long datetime = bigEndianLong(inputStream.read(5), 0, 5);
296-
int yearMonth = extractBits(datetime, 1, 17, 40);
297-
Calendar c = Calendar.getInstance();
298-
c.set(Calendar.YEAR, yearMonth / 13);
299-
c.set(Calendar.MONTH, yearMonth % 13 - 1);
300-
c.set(Calendar.DAY_OF_MONTH, extractBits(datetime, 18, 5, 40));
301-
c.set(Calendar.HOUR_OF_DAY, extractBits(datetime, 23, 5, 40));
302-
c.set(Calendar.MINUTE, extractBits(datetime, 28, 6, 40));
303-
c.set(Calendar.SECOND, extractBits(datetime, 34, 6, 40));
304-
c.set(Calendar.MILLISECOND, deserializeFractionalSeconds(meta, inputStream));
305-
return c.getTime();
289+
int yearMonth = bitSlice(datetime, 1, 17, 40);
290+
Long timestamp = asUnixTime(
291+
yearMonth / 13,
292+
yearMonth % 13,
293+
bitSlice(datetime, 18, 5, 40),
294+
bitSlice(datetime, 23, 5, 40),
295+
bitSlice(datetime, 28, 6, 40),
296+
bitSlice(datetime, 34, 6, 40),
297+
deserializeFractionalSeconds(meta, inputStream)
298+
);
299+
return timestamp != null ? new java.util.Date(timestamp) : null;
306300
}
307301

308302
protected Serializable deserializeYear(ByteArrayInputStream inputStream) throws IOException {
@@ -339,6 +333,15 @@ protected Serializable deserializeGeometry(int meta, ByteArrayInputStream inputS
339333
return inputStream.read(dataLength);
340334
}
341335

336+
// checkstyle, please ignore ParameterNumber for the next line
337+
protected Long asUnixTime(int year, int month, int day, int hour, int minute, int second, int millis) {
338+
// https://dev.mysql.com/doc/refman/5.0/en/datetime.html
339+
if (year == 0 || month == 0 || day == 0) {
340+
return null;
341+
}
342+
return UnixTime.from(year, month, day, hour, minute, second, millis);
343+
}
344+
342345
protected int deserializeFractionalSeconds(int meta, ByteArrayInputStream inputStream) throws IOException {
343346
int length = (meta + 1) / 2;
344347
if (length > 0) {
@@ -348,7 +351,7 @@ protected int deserializeFractionalSeconds(int meta, ByteArrayInputStream inputS
348351
return 0;
349352
}
350353

351-
private static int extractBits(long value, int bitOffset, int numberOfBits, int payloadSize) {
354+
private static int bitSlice(long value, int bitOffset, int numberOfBits, int payloadSize) {
352355
long result = value >> payloadSize - (bitOffset + numberOfBits);
353356
return (int) (result & ((1 << numberOfBits) - 1));
354357
}
@@ -424,4 +427,75 @@ private static long bigEndianLong(byte[] bytes, int offset, int length) {
424427
return result;
425428
}
426429

430+
/**
431+
* Class for working with Unix time.
432+
*/
433+
static class UnixTime {
434+
435+
private static final int[] YEAR_DAYS_BY_MONTH = new int[] {
436+
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365
437+
};
438+
private static final int[] LEAP_YEAR_DAYS_BY_MONTH = new int[] {
439+
0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366
440+
};
441+
442+
/**
443+
* Calendar::getTimeInMillis but magnitude faster for all dates starting from October 15, 1582
444+
* (Gregorian Calendar cutover).
445+
*
446+
* @param year year
447+
* @param month month [1..12]
448+
* @param day day [1..)
449+
* @param hour hour [0..23]
450+
* @param minute [0..59]
451+
* @param second [0..59]
452+
* @param millis [0..999]
453+
*
454+
* @return Unix time (number of seconds that have elapsed since 00:00:00 (UTC), Thursday,
455+
* 1 January 1970, not counting leap seconds)
456+
*/
457+
// checkstyle, please ignore ParameterNumber for the next line
458+
public static long from(int year, int month, int day, int hour, int minute, int second, int millis) {
459+
if (year < 1582 || (year == 1582 && (month < 10 || (month == 10 && day < 15)))) {
460+
return fallbackToGC(year, month, day, hour, minute, second, millis);
461+
}
462+
long timestamp = 0;
463+
int numberOfLeapYears = leapYears(1970, year);
464+
timestamp += 366L * 24 * 60 * 60 * numberOfLeapYears;
465+
timestamp += 365L * 24 * 60 * 60 * (year - 1970 - numberOfLeapYears);
466+
long daysUpToMonth = isLeapYear(year) ? LEAP_YEAR_DAYS_BY_MONTH[month - 1] : YEAR_DAYS_BY_MONTH[month - 1];
467+
timestamp += ((daysUpToMonth + day - 1) * 24 * 60 * 60) +
468+
(hour * 60 * 60) + (minute * 60) + (second);
469+
timestamp = timestamp * 1000 + millis;
470+
return timestamp;
471+
}
472+
473+
// checkstyle, please ignore ParameterNumber for the next line
474+
private static long fallbackToGC(int year, int month, int dayOfMonth, int hourOfDay,
475+
int minute, int second, int millis) {
476+
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
477+
c.set(Calendar.YEAR, year);
478+
c.set(Calendar.MONTH, month - 1);
479+
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
480+
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
481+
c.set(Calendar.MINUTE, minute);
482+
c.set(Calendar.SECOND, second);
483+
c.set(Calendar.MILLISECOND, millis);
484+
return c.getTimeInMillis();
485+
}
486+
487+
private static int leapYears(int from, int end) {
488+
return leapYearsBefore(end) - leapYearsBefore(from + 1);
489+
}
490+
491+
private static int leapYearsBefore(int year) {
492+
year--; return (year / 4) - (year / 100) + (year / 400);
493+
}
494+
495+
private static boolean isLeapYear(int year) {
496+
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
497+
}
498+
499+
}
500+
427501
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2013 Stanley Shyiko
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.shyiko.mysql.binlog.event.deserialization;
17+
18+
import com.github.shyiko.mysql.binlog.event.deserialization.AbstractRowsEventDataDeserializer.UnixTime;
19+
import org.testng.annotations.Test;
20+
21+
import java.util.Calendar;
22+
import java.util.TimeZone;
23+
24+
import static org.testng.Assert.assertEquals;
25+
26+
/**
27+
* @author <a href="mailto:stanley.shyiko@gmail.com">Stanley Shyiko</a>
28+
*/
29+
public class AbstractRowsEventDataDeserializerTest {
30+
31+
@Test
32+
public void testFrom() throws Exception {
33+
assetTimeEquals(UnixTime.from(2260, 8, 19, 0, 0, 0, 0),
34+
timestamp(2260, 8, 19, 0, 0, 0, 0));
35+
assetTimeEquals(UnixTime.from(2016, 8, 19, 0, 0, 0, 0),
36+
timestamp(2016, 8, 19, 0, 0, 0, 0));
37+
assetTimeEquals(UnixTime.from(1970, 1, 1, 0, 0, 0, 0), 0);
38+
assetTimeEquals(UnixTime.from(1969, 1, 1, 0, 0, 0, 0),
39+
-365L * 24 * 60 * 60 * 1000);
40+
assetTimeEquals(UnixTime.from(1582, 10, 15, 0, 0, 0, 0),
41+
timestamp(1582, 10, 15, 0, 0, 0, 0));
42+
assetTimeEquals(UnixTime.from(1582, 10, 14, 0, 0, 0, 0),
43+
timestamp(1582, 10, 14, 0, 0, 0, 0));
44+
assetTimeEquals(UnixTime.from(1, 1, 1, 0, 0, 0, 0),
45+
timestamp(1, 1, 1, 0, 0, 0, 0));
46+
}
47+
48+
private void assetTimeEquals(long actual, long expected) {
49+
assertEquals(actual, expected, actual + " != " + expected +
50+
", discrepancy: " + (actual - expected));
51+
}
52+
53+
// checkstyle, please ignore ParameterNumber for the next line
54+
private long timestamp(int year, int month, int dayOfMonth, int hourOfDay,
55+
int minute, int second, int millis) {
56+
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
57+
c.set(Calendar.YEAR, year);
58+
c.set(Calendar.MONTH, month - 1);
59+
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
60+
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
61+
c.set(Calendar.MINUTE, minute);
62+
c.set(Calendar.SECOND, second);
63+
c.set(Calendar.MILLISECOND, millis);
64+
return c.getTimeInMillis();
65+
}
66+
67+
}

0 commit comments

Comments
 (0)