Skip to content

Commit d215ee7

Browse files
authored
Zero-pad fixed-length binary fields (#401)
MySQL is zero-padding fixed-length binary fields [1], but is not storing trailing zeros to the binlog. Consequently, when reading values of fixed-length binary fields, the value must be padded with zeros up to the specified length. The `test_fixed_length_binary` test case exemplifies the issue. `varbinary` columns are not padded and not affected. This commit extends the `information_schema` query in `__get_table_information` to fetch information about the length limitation of the field and stores this information in the `fixed_binary_length` attribute of `Column` instances. Upon decoding of binary fields (which share the same binlog field type value of `254` as strings), the value is zero padded at the end, when it does not meet the specified length as retrieved from the information schema. Fixes #400. [1] https://dev.mysql.com/doc/refman/5.7/en/binary-varbinary.html
1 parent 59c5111 commit d215ee7

File tree

5 files changed

+37
-1
lines changed

5 files changed

+37
-1
lines changed

pymysqlreplication/binlogstream.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,8 @@ def __get_table_information(self, schema, table):
616616
cur.execute("""
617617
SELECT
618618
COLUMN_NAME, COLLATION_NAME, CHARACTER_SET_NAME,
619-
COLUMN_COMMENT, COLUMN_TYPE, COLUMN_KEY, ORDINAL_POSITION
619+
COLUMN_COMMENT, COLUMN_TYPE, COLUMN_KEY, ORDINAL_POSITION,
620+
DATA_TYPE, CHARACTER_OCTET_LENGTH
620621
FROM
621622
information_schema.columns
622623
WHERE

pymysqlreplication/column.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def __parse_column_definition(self, column_type, column_schema, packet):
2626
self.type_is_bool = False
2727
self.is_primary = column_schema["COLUMN_KEY"] == "PRI"
2828

29+
# Check for fixed-length binary type. When that's the case then we need
30+
# to zero-pad the values to full length at read time.
31+
self.fixed_binary_length = None
32+
if column_schema["DATA_TYPE"] == "binary":
33+
self.fixed_binary_length = column_schema["CHARACTER_OCTET_LENGTH"]
34+
2935
if self.type == FIELD_TYPE.VARCHAR:
3036
self.max_length = struct.unpack('<H', packet.read(2))[0]
3137
elif self.type == FIELD_TYPE.DOUBLE:

pymysqlreplication/row_event.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def _read_column_data(self, cols_bitmap):
109109
name = self.table_map[self.table_id].columns[i].name
110110
unsigned = self.table_map[self.table_id].columns[i].unsigned
111111
zerofill = self.table_map[self.table_id].columns[i].zerofill
112+
fixed_binary_length = self.table_map[self.table_id].columns[i].fixed_binary_length
112113

113114
if BitGet(cols_bitmap, i) == 0:
114115
values[name] = None
@@ -154,6 +155,14 @@ def _read_column_data(self, cols_bitmap):
154155
values[name] = self.__read_string(2, column)
155156
else:
156157
values[name] = self.__read_string(1, column)
158+
159+
if fixed_binary_length and len(values[name]) < fixed_binary_length:
160+
# Fixed-length binary fields are stored in the binlog
161+
# without trailing zeros and must be padded with zeros up
162+
# to the specified length at read time.
163+
nr_pad = fixed_binary_length - len(values[name])
164+
values[name] += b'\x00' * nr_pad
165+
157166
elif column.type == FIELD_TYPE.NEWDECIMAL:
158167
values[name] = self.__read_new_decimal(column)
159168
elif column.type == FIELD_TYPE.BLOB:
@@ -640,6 +649,8 @@ def __init__(self, from_packet, event_size, table_map, ctl_connection, **kwargs)
640649
'COLUMN_NAME': '__dropped_col_{i}__'.format(i=i),
641650
'COLLATION_NAME': None,
642651
'CHARACTER_SET_NAME': None,
652+
'CHARACTER_OCTET_LENGTH': None,
653+
'DATA_TYPE': 'BLOB',
643654
'COLUMN_COMMENT': None,
644655
'COLUMN_TYPE': 'BLOB', # we don't know what it is, so let's not do anything with it.
645656
'COLUMN_KEY': '',

pymysqlreplication/tests/test_data_objects.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def test_column_is_primary(self):
2222
{"COLUMN_NAME": "test",
2323
"COLLATION_NAME": "utf8_general_ci",
2424
"CHARACTER_SET_NAME": "UTF8",
25+
"CHARACTER_OCTET_LENGTH": None,
26+
"DATA_TYPE": "tinyint",
2527
"COLUMN_COMMENT": "",
2628
"COLUMN_TYPE": "tinyint(2)",
2729
"COLUMN_KEY": "PRI"},
@@ -33,6 +35,8 @@ def test_column_not_primary(self):
3335
{"COLUMN_NAME": "test",
3436
"COLLATION_NAME": "utf8_general_ci",
3537
"CHARACTER_SET_NAME": "UTF8",
38+
"CHARACTER_OCTET_LENGTH": None,
39+
"DATA_TYPE": "tinyint",
3640
"COLUMN_COMMENT": "",
3741
"COLUMN_TYPE": "tinyint(2)",
3842
"COLUMN_KEY": ""},
@@ -44,6 +48,8 @@ def test_column_serializable(self):
4448
{"COLUMN_NAME": "test",
4549
"COLLATION_NAME": "utf8_general_ci",
4650
"CHARACTER_SET_NAME": "UTF8",
51+
"CHARACTER_OCTET_LENGTH": None,
52+
"DATA_TYPE": "tinyint",
4753
"COLUMN_COMMENT": "",
4854
"COLUMN_TYPE": "tinyint(2)",
4955
"COLUMN_KEY": "PRI"},

pymysqlreplication/tests/test_data_type.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ def create_and_get_tablemap_event(self, bit):
101101

102102
return event
103103

104+
def test_varbinary(self):
105+
create_query = "CREATE TABLE test(b VARBINARY(4))"
106+
insert_query = "INSERT INTO test VALUES(UNHEX('ff010000'))"
107+
event = self.create_and_insert_value(create_query, insert_query)
108+
self.assertEqual(event.rows[0]["values"]["b"], b'\xff\x01\x00\x00')
109+
110+
def test_fixed_length_binary(self):
111+
create_query = "CREATE TABLE test(b BINARY(4))"
112+
insert_query = "INSERT INTO test VALUES(UNHEX('ff010000'))"
113+
event = self.create_and_insert_value(create_query, insert_query)
114+
self.assertEqual(event.rows[0]["values"]["b"], b'\xff\x01\x00\x00')
115+
104116
def test_decimal(self):
105117
create_query = "CREATE TABLE test (test DECIMAL(2,1))"
106118
insert_query = "INSERT INTO test VALUES(4.2)"

0 commit comments

Comments
 (0)