mysql_replicator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +79 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +12 -0
- data/Steepfile +22 -0
- data/docker-compose.yml +13 -0
- data/lib/mysql_replicator/binlog_client.rb +201 -0
- data/lib/mysql_replicator/binlogs/column_parser.rb +425 -0
- data/lib/mysql_replicator/binlogs/constants.rb +74 -0
- data/lib/mysql_replicator/binlogs/event_parser.rb +134 -0
- data/lib/mysql_replicator/binlogs/format_description_event_parser.rb +24 -0
- data/lib/mysql_replicator/binlogs/json_parser.rb +335 -0
- data/lib/mysql_replicator/binlogs/query_event_parser.rb +69 -0
- data/lib/mysql_replicator/binlogs/rotate_event_parser.rb +37 -0
- data/lib/mysql_replicator/binlogs/rows_event_parser.rb +161 -0
- data/lib/mysql_replicator/binlogs/table_map_event_parser.rb +155 -0
- data/lib/mysql_replicator/binlogs/xid_event_parser.rb +25 -0
- data/lib/mysql_replicator/connection.rb +226 -0
- data/lib/mysql_replicator/connections/auth.rb +303 -0
- data/lib/mysql_replicator/connections/handshake.rb +132 -0
- data/lib/mysql_replicator/connections/query.rb +322 -0
- data/lib/mysql_replicator/error.rb +6 -0
- data/lib/mysql_replicator/logger.rb +43 -0
- data/lib/mysql_replicator/string_io_util.rb +199 -0
- data/lib/mysql_replicator/string_util.rb +106 -0
- data/lib/mysql_replicator/version.rb +6 -0
- data/lib/mysql_replicator.rb +51 -0
- data/sig/generated/mysql_replicator/binlog_client.rbs +52 -0
- data/sig/generated/mysql_replicator/binlogs/column_parser.rbs +134 -0
- data/sig/generated/mysql_replicator/binlogs/constants.rbs +69 -0
- data/sig/generated/mysql_replicator/binlogs/event_parser.rbs +35 -0
- data/sig/generated/mysql_replicator/binlogs/format_description_event_parser.rbs +13 -0
- data/sig/generated/mysql_replicator/binlogs/json_parser.rbs +101 -0
- data/sig/generated/mysql_replicator/binlogs/query_event_parser.rbs +14 -0
- data/sig/generated/mysql_replicator/binlogs/rotate_event_parser.rbs +14 -0
- data/sig/generated/mysql_replicator/binlogs/rows_event_parser.rbs +39 -0
- data/sig/generated/mysql_replicator/binlogs/table_map_event_parser.rbs +31 -0
- data/sig/generated/mysql_replicator/binlogs/xid_event_parser.rbs +13 -0
- data/sig/generated/mysql_replicator/connection.rbs +103 -0
- data/sig/generated/mysql_replicator/connections/auth.rbs +76 -0
- data/sig/generated/mysql_replicator/connections/handshake.rbs +21 -0
- data/sig/generated/mysql_replicator/connections/query.rbs +62 -0
- data/sig/generated/mysql_replicator/error.rbs +6 -0
- data/sig/generated/mysql_replicator/logger.rbs +26 -0
- data/sig/generated/mysql_replicator/string_io_util.rbs +75 -0
- data/sig/generated/mysql_replicator/string_util.rbs +45 -0
- data/sig/generated/mysql_replicator/types.rbs +19 -0
- data/sig/generated/mysql_replicator/version.rbs +5 -0
- data/sig/generated/mysql_replicator.rbs +16 -0
- metadata +124 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module MysqlReplicator
|
|
7
|
+
module Binlogs
|
|
8
|
+
class JsonParser
|
|
9
|
+
# @rbs!
|
|
10
|
+
# type jsonValue = String | Integer | Float | bool | nil
|
|
11
|
+
|
|
12
|
+
# @rbs!
|
|
13
|
+
# type json = Hash[Symbol, json] | Array[json] | jsonValue
|
|
14
|
+
|
|
15
|
+
JSONB_TYPE_SMALL_OBJECT = 0x00 #: Integer
|
|
16
|
+
JSONB_TYPE_LARGE_OBJECT = 0x01 #: Integer
|
|
17
|
+
JSONB_TYPE_SMALL_ARRAY = 0x02 #: Integer
|
|
18
|
+
JSONB_TYPE_LARGE_ARRAY = 0x03 #: Integer
|
|
19
|
+
JSONB_TYPE_LITERAL = 0x04 #: Integer
|
|
20
|
+
JSONB_TYPE_INT16 = 0x05 #: Integer
|
|
21
|
+
JSONB_TYPE_UINT16 = 0x06 #: Integer
|
|
22
|
+
JSONB_TYPE_INT32 = 0x07 #: Integer
|
|
23
|
+
JSONB_TYPE_UINT32 = 0x08 #: Integer
|
|
24
|
+
JSONB_TYPE_INT64 = 0x09 #: Integer
|
|
25
|
+
JSONB_TYPE_UINT64 = 0x0A #: Integer
|
|
26
|
+
JSONB_TYPE_DOUBLE = 0x0B #: Integer
|
|
27
|
+
JSONB_TYPE_STRING = 0x0C #: Integer
|
|
28
|
+
JSONB_TYPE_OPAQUE = 0x0F #: Integer
|
|
29
|
+
|
|
30
|
+
JSONB_NULL = 0x00 #: Integer
|
|
31
|
+
JSONB_TRUE = 0x01 #: Integer
|
|
32
|
+
JSONB_FALSE = 0x02 #: Integer
|
|
33
|
+
|
|
34
|
+
# @rbs payload: String | nil
|
|
35
|
+
# @rbs return: json
|
|
36
|
+
def self.parse(payload)
|
|
37
|
+
return nil if payload.nil? || payload.empty?
|
|
38
|
+
|
|
39
|
+
data = payload.dup.force_encoding(Encoding::BINARY)
|
|
40
|
+
|
|
41
|
+
type = MysqlReplicator::StringUtil.read_uint8(data[0])
|
|
42
|
+
parse_value(type, data, 1)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs type: Integer
|
|
46
|
+
# @rbs data: String
|
|
47
|
+
# @rbs pos: Integer
|
|
48
|
+
# @rbs return: json
|
|
49
|
+
def self.parse_value(type, data, pos)
|
|
50
|
+
case type
|
|
51
|
+
when JSONB_TYPE_SMALL_OBJECT
|
|
52
|
+
parse_object(data, pos, small: true)
|
|
53
|
+
when JSONB_TYPE_LARGE_OBJECT
|
|
54
|
+
parse_object(data, pos, small: false)
|
|
55
|
+
when JSONB_TYPE_SMALL_ARRAY
|
|
56
|
+
parse_array(data, pos, small: true)
|
|
57
|
+
when JSONB_TYPE_LARGE_ARRAY
|
|
58
|
+
parse_array(data, pos, small: false)
|
|
59
|
+
when JSONB_TYPE_LITERAL
|
|
60
|
+
parse_literal(MysqlReplicator::StringUtil.read_uint8(data[pos]))
|
|
61
|
+
when JSONB_TYPE_INT16
|
|
62
|
+
MysqlReplicator::StringUtil.read_int16(data[pos, 2])
|
|
63
|
+
when JSONB_TYPE_UINT16
|
|
64
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
65
|
+
when JSONB_TYPE_INT32
|
|
66
|
+
MysqlReplicator::StringUtil.read_int32(data[pos, 4])
|
|
67
|
+
when JSONB_TYPE_UINT32
|
|
68
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
69
|
+
when JSONB_TYPE_INT64
|
|
70
|
+
MysqlReplicator::StringUtil.read_int64(data[pos, 8])
|
|
71
|
+
when JSONB_TYPE_UINT64
|
|
72
|
+
MysqlReplicator::StringUtil.read_uint64(data[pos, 8])
|
|
73
|
+
when JSONB_TYPE_DOUBLE
|
|
74
|
+
MysqlReplicator::StringUtil.read_double64(data[pos, 8])
|
|
75
|
+
when JSONB_TYPE_STRING
|
|
76
|
+
parse_string(data, pos)
|
|
77
|
+
when JSONB_TYPE_OPAQUE
|
|
78
|
+
code = MysqlReplicator::StringUtil.read_uint8(data[pos])
|
|
79
|
+
raise "OPAQUE type is not supported (type code: 0x#{code.to_s(16)})"
|
|
80
|
+
else
|
|
81
|
+
raise "Unknown JSON type: 0x#{type.to_s(16)} at position #{pos}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @rbs type: Integer
|
|
86
|
+
# @rbs data: String
|
|
87
|
+
# @rbs offset: Integer
|
|
88
|
+
# @rbs base_offset: Integer
|
|
89
|
+
# @rbs return: json
|
|
90
|
+
def self.parse_value_at_offset(type, data, offset, base_offset)
|
|
91
|
+
abs_pos = base_offset + 1 + offset
|
|
92
|
+
|
|
93
|
+
case type
|
|
94
|
+
when JSONB_TYPE_SMALL_OBJECT
|
|
95
|
+
parse_object(data, abs_pos, small: true)
|
|
96
|
+
when JSONB_TYPE_LARGE_OBJECT
|
|
97
|
+
parse_object(data, abs_pos, small: false)
|
|
98
|
+
when JSONB_TYPE_SMALL_ARRAY
|
|
99
|
+
parse_array(data, abs_pos, small: true)
|
|
100
|
+
when JSONB_TYPE_LARGE_ARRAY
|
|
101
|
+
parse_array(data, abs_pos, small: false)
|
|
102
|
+
when JSONB_TYPE_STRING
|
|
103
|
+
parse_string(data, abs_pos)
|
|
104
|
+
when JSONB_TYPE_INT64
|
|
105
|
+
MysqlReplicator::StringUtil.read_int64(data[abs_pos, 8])
|
|
106
|
+
when JSONB_TYPE_UINT64
|
|
107
|
+
MysqlReplicator::StringUtil.read_uint64(data[abs_pos, 8])
|
|
108
|
+
when JSONB_TYPE_DOUBLE
|
|
109
|
+
MysqlReplicator::StringUtil.read_double64(data[abs_pos, 8])
|
|
110
|
+
when JSONB_TYPE_OPAQUE
|
|
111
|
+
code = MysqlReplicator::StringUtil.read_uint8(data[abs_pos])
|
|
112
|
+
raise "OPAQUE type is not supported (type code: 0x#{code.to_s(16)})"
|
|
113
|
+
else
|
|
114
|
+
raise "Unexpected type at offset: 0x#{type.to_s(16)}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# SMALL: under 65535 elements, and 2byte offsets
|
|
119
|
+
# LARGE: 65535 elements or more, and 4byte offsets
|
|
120
|
+
#
|
|
121
|
+
# @rbs data: String
|
|
122
|
+
# @rbs pos: Integer
|
|
123
|
+
# @rbs small: bool
|
|
124
|
+
# @rbs return: Hash[Symbol, json]
|
|
125
|
+
def self.parse_object(data, pos, small:)
|
|
126
|
+
offset_size = small ? 2 : 4
|
|
127
|
+
|
|
128
|
+
# base offset of this object
|
|
129
|
+
base_offset = pos - 1
|
|
130
|
+
|
|
131
|
+
# Read header
|
|
132
|
+
# element count is json key count
|
|
133
|
+
element_count = if small
|
|
134
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
135
|
+
else
|
|
136
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
137
|
+
end
|
|
138
|
+
pos += offset_size
|
|
139
|
+
_byte_size = if small
|
|
140
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
141
|
+
else
|
|
142
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
143
|
+
end
|
|
144
|
+
pos += offset_size
|
|
145
|
+
|
|
146
|
+
# Read key entries
|
|
147
|
+
key_entries = []
|
|
148
|
+
element_count.times do
|
|
149
|
+
key_offset = if small
|
|
150
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
151
|
+
else
|
|
152
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
153
|
+
end
|
|
154
|
+
pos += offset_size
|
|
155
|
+
key_length = MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
156
|
+
pos += 2
|
|
157
|
+
key_entries << { offset: key_offset, length: key_length }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Read value entries
|
|
161
|
+
value_entries = []
|
|
162
|
+
element_count.times do
|
|
163
|
+
value_type = MysqlReplicator::StringUtil.read_uint8(data[pos])
|
|
164
|
+
pos += 1
|
|
165
|
+
|
|
166
|
+
if inlined_type?(value_type, small)
|
|
167
|
+
# Small values (LITERAL, INT16, etc.) are embedded directly
|
|
168
|
+
inline_value = read_inlined_value(data, pos, value_type)
|
|
169
|
+
pos += offset_size
|
|
170
|
+
value_entries << { type: value_type, inline: true, value: inline_value }
|
|
171
|
+
else
|
|
172
|
+
# Large values are stored as offsets
|
|
173
|
+
value_offset = if small
|
|
174
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
175
|
+
else
|
|
176
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
177
|
+
end
|
|
178
|
+
pos += offset_size
|
|
179
|
+
value_entries << { type: value_type, inline: false, offset: value_offset }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Get real key string
|
|
184
|
+
keys = key_entries.map do |entry|
|
|
185
|
+
abs_pos = base_offset + 1 + entry[:offset]
|
|
186
|
+
str = MysqlReplicator::StringUtil.read_str(data[abs_pos, entry[:length]])
|
|
187
|
+
str.force_encoding(Encoding::UTF_8)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get real value
|
|
191
|
+
values = value_entries.map do |entry|
|
|
192
|
+
if entry[:inline]
|
|
193
|
+
entry[:value]
|
|
194
|
+
else
|
|
195
|
+
parse_value_at_offset(entry[:type], data, entry[:offset], base_offset)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Return key-value hash
|
|
200
|
+
keys.zip(values).to_h
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @rbs data: String
|
|
204
|
+
# @rbs pos: Integer
|
|
205
|
+
# @rbs small: bool
|
|
206
|
+
# @rbs return: Array[json]
|
|
207
|
+
def self.parse_array(data, pos, small:)
|
|
208
|
+
offset_size = small ? 2 : 4
|
|
209
|
+
|
|
210
|
+
# base offset of this object
|
|
211
|
+
base_offset = pos - 1
|
|
212
|
+
|
|
213
|
+
# Read header
|
|
214
|
+
# element count is json key count
|
|
215
|
+
element_count = if small
|
|
216
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
217
|
+
else
|
|
218
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
219
|
+
end
|
|
220
|
+
pos += offset_size
|
|
221
|
+
_byte_size = if small
|
|
222
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
223
|
+
else
|
|
224
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
225
|
+
end
|
|
226
|
+
pos += offset_size
|
|
227
|
+
|
|
228
|
+
# Read value entries
|
|
229
|
+
value_entries = []
|
|
230
|
+
element_count.times do
|
|
231
|
+
value_type = MysqlReplicator::StringUtil.read_uint8(data[pos])
|
|
232
|
+
pos += 1
|
|
233
|
+
|
|
234
|
+
if inlined_type?(value_type, small)
|
|
235
|
+
# Small values (LITERAL, INT16, etc.) are embedded directly
|
|
236
|
+
inline_value = read_inlined_value(data, pos, value_type)
|
|
237
|
+
pos += offset_size
|
|
238
|
+
value_entries << { type: value_type, inline: true, value: inline_value }
|
|
239
|
+
else
|
|
240
|
+
# Large values are stored as offsets
|
|
241
|
+
value_offset = if small
|
|
242
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
243
|
+
else
|
|
244
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
245
|
+
end
|
|
246
|
+
pos += offset_size
|
|
247
|
+
value_entries << { type: value_type, inline: false, offset: value_offset }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Return real value array
|
|
252
|
+
value_entries.map do |entry|
|
|
253
|
+
if entry[:inline]
|
|
254
|
+
entry[:value]
|
|
255
|
+
else
|
|
256
|
+
parse_value_at_offset(entry[:type], data, entry[:offset], base_offset)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# @rbs literal_type: Integer
|
|
262
|
+
# @rbs return: bool | nil
|
|
263
|
+
def self.parse_literal(literal_type)
|
|
264
|
+
case literal_type
|
|
265
|
+
when JSONB_NULL then nil
|
|
266
|
+
when JSONB_TRUE then true
|
|
267
|
+
when JSONB_FALSE then false
|
|
268
|
+
else
|
|
269
|
+
raise "Unknown literal type: #{literal_type}"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# @rbs data: String
|
|
274
|
+
# @rbs pos: Integer
|
|
275
|
+
# @rbs return: String
|
|
276
|
+
def self.parse_string(data, pos)
|
|
277
|
+
length, bytes_read = read_variable_length(data, pos)
|
|
278
|
+
value = MysqlReplicator::StringUtil.read_str(data[pos + bytes_read, length])
|
|
279
|
+
value.force_encoding(Encoding::UTF_8)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# @rbs type: Integer
|
|
283
|
+
# @rbs small: bool
|
|
284
|
+
# @rbs return: bool
|
|
285
|
+
def self.inlined_type?(type, small)
|
|
286
|
+
case type
|
|
287
|
+
when JSONB_TYPE_LITERAL, JSONB_TYPE_INT16, JSONB_TYPE_UINT16
|
|
288
|
+
true
|
|
289
|
+
when JSONB_TYPE_INT32, JSONB_TYPE_UINT32
|
|
290
|
+
!small
|
|
291
|
+
else
|
|
292
|
+
false
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# @rbs data: String
|
|
297
|
+
# @rbs pos: Integer
|
|
298
|
+
# @rbs return: Integer | bool | nil
|
|
299
|
+
def self.read_inlined_value(data, pos, type)
|
|
300
|
+
case type
|
|
301
|
+
when JSONB_TYPE_LITERAL
|
|
302
|
+
parse_literal(MysqlReplicator::StringUtil.read_uint8(data[pos]))
|
|
303
|
+
when JSONB_TYPE_INT16
|
|
304
|
+
MysqlReplicator::StringUtil.read_int16(data[pos, 2])
|
|
305
|
+
when JSONB_TYPE_UINT16
|
|
306
|
+
MysqlReplicator::StringUtil.read_uint16(data[pos, 2])
|
|
307
|
+
when JSONB_TYPE_INT32
|
|
308
|
+
MysqlReplicator::StringUtil.read_int32(data[pos, 4])
|
|
309
|
+
when JSONB_TYPE_UINT32
|
|
310
|
+
MysqlReplicator::StringUtil.read_uint32(data[pos, 4])
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# @rbs data: String
|
|
315
|
+
# @rbs pos: Integer
|
|
316
|
+
# @rbs return: [Integer, Integer]
|
|
317
|
+
def self.read_variable_length(data, pos)
|
|
318
|
+
length = 0
|
|
319
|
+
shift = 0
|
|
320
|
+
bytes_read = 0
|
|
321
|
+
|
|
322
|
+
loop do
|
|
323
|
+
byte = MysqlReplicator::StringUtil.read_uint8(data[pos + bytes_read])
|
|
324
|
+
bytes_read += 1
|
|
325
|
+
length |= (byte & 0x7F) << shift
|
|
326
|
+
break if (byte & 0x80).zero?
|
|
327
|
+
|
|
328
|
+
shift += 7
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
[length, bytes_read]
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module MysqlReplicator
|
|
5
|
+
module Binlogs
|
|
6
|
+
class QueryEventParser
|
|
7
|
+
# @rbs!
|
|
8
|
+
# type execution = {
|
|
9
|
+
# thread_id: Integer,
|
|
10
|
+
# exec_time: Integer,
|
|
11
|
+
# error_code: Integer,
|
|
12
|
+
# database: String | nil,
|
|
13
|
+
# sql: String | nil
|
|
14
|
+
# }
|
|
15
|
+
|
|
16
|
+
# @rbs payload: String
|
|
17
|
+
# @rbs checksum_enabled: bool
|
|
18
|
+
# @rbs return: execution
|
|
19
|
+
def self.parse(payload, checksum_enabled)
|
|
20
|
+
offset = 0
|
|
21
|
+
|
|
22
|
+
thread_id = MysqlReplicator::StringUtil.read_uint32(payload[offset, 4])
|
|
23
|
+
offset += 4
|
|
24
|
+
|
|
25
|
+
exec_time = MysqlReplicator::StringUtil.read_uint32(payload[offset, 4])
|
|
26
|
+
offset += 4
|
|
27
|
+
|
|
28
|
+
db_len = MysqlReplicator::StringUtil.read_uint8(payload[offset])
|
|
29
|
+
offset += 1
|
|
30
|
+
|
|
31
|
+
error_code = MysqlReplicator::StringUtil.read_uint16(payload[offset, 2])
|
|
32
|
+
offset += 2
|
|
33
|
+
|
|
34
|
+
# Skip status variables
|
|
35
|
+
status_vars_len = MysqlReplicator::StringUtil.read_uint16(payload[offset, 2])
|
|
36
|
+
offset += 2
|
|
37
|
+
if status_vars_len > 0
|
|
38
|
+
offset += status_vars_len
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Database name (null-terminated)
|
|
42
|
+
if db_len > 0
|
|
43
|
+
database = payload[offset, db_len]
|
|
44
|
+
offset += db_len
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Skip null terminator for database name
|
|
48
|
+
if offset < payload.length && payload[offset] == "\x00"
|
|
49
|
+
offset += 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The rest is the SQL query
|
|
53
|
+
if offset < payload.length
|
|
54
|
+
# Remove checksum if present (last 4 bytes)
|
|
55
|
+
sql_end = checksum_enabled ? -5 : -1
|
|
56
|
+
sql = payload[offset..sql_end]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
thread_id: thread_id,
|
|
61
|
+
exec_time: exec_time,
|
|
62
|
+
error_code: error_code,
|
|
63
|
+
database: database,
|
|
64
|
+
sql: sql
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module MysqlReplicator
|
|
5
|
+
module Binlogs
|
|
6
|
+
class RotateEventParser
|
|
7
|
+
# @rbs!
|
|
8
|
+
# type execution = {
|
|
9
|
+
# position: Integer,
|
|
10
|
+
# filename: String
|
|
11
|
+
# }
|
|
12
|
+
|
|
13
|
+
# @rbs payload: String
|
|
14
|
+
# @rbs checksum_enabled: bool
|
|
15
|
+
# @rbs return: execution
|
|
16
|
+
def self.parse(payload, checksum_enabled = false)
|
|
17
|
+
# Position (8 bytes, where the next binlog starts, Little Endian 64-bit)
|
|
18
|
+
position = MysqlReplicator::StringUtil.read_uint64(payload[0, 8])
|
|
19
|
+
# Filename (remaining bytes, new binlog filename)
|
|
20
|
+
filename = MysqlReplicator::StringUtil.read_str(payload[8..])
|
|
21
|
+
|
|
22
|
+
# Remove checksum if present (last 4 bytes)
|
|
23
|
+
if checksum_enabled && filename.length >= 4
|
|
24
|
+
filename = MysqlReplicator::StringUtil.read_str(filename[0...-4])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Find null terminator or use entire remaining data
|
|
28
|
+
null_pos = filename.index("\x00")
|
|
29
|
+
filename = MysqlReplicator::StringUtil.read_str(filename[0...null_pos]) if null_pos
|
|
30
|
+
# Clean up any non-printable characters
|
|
31
|
+
filename = filename.force_encoding(Encoding::UTF_8).scrub
|
|
32
|
+
|
|
33
|
+
{ position: position, filename: filename }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
module MysqlReplicator
|
|
7
|
+
module Binlogs
|
|
8
|
+
class RowsEventParser
|
|
9
|
+
# @rbs!
|
|
10
|
+
# type rowData = {
|
|
11
|
+
# ordinal_position: Integer,
|
|
12
|
+
# data_type: String,
|
|
13
|
+
# column_name: String,
|
|
14
|
+
# value: String | Integer | bool | nil,
|
|
15
|
+
# primary_key: bool
|
|
16
|
+
# }
|
|
17
|
+
|
|
18
|
+
# @rbs!
|
|
19
|
+
# type execution = {
|
|
20
|
+
# table_id: Integer,
|
|
21
|
+
# flags: Integer,
|
|
22
|
+
# extra_data_length: Integer,
|
|
23
|
+
# column_count: Integer,
|
|
24
|
+
# rows: Array[rowData]
|
|
25
|
+
# }
|
|
26
|
+
|
|
27
|
+
# @rbs event_type: :WRITE_ROWS | :UPDATE_ROWS | :DELETE_ROWS
|
|
28
|
+
# @rbs payload: String
|
|
29
|
+
# @rbs checksum_enabled bool
|
|
30
|
+
# @rbs table_map: Hash[Integer, MysqlReplicator::Binlogs::TableMapEventParser::execution]
|
|
31
|
+
# @rbs return: execution
|
|
32
|
+
def self.parse(event_type, payload, checksum_enabled, table_map)
|
|
33
|
+
io = StringIO.new(payload)
|
|
34
|
+
io.set_encoding(Encoding::BINARY)
|
|
35
|
+
|
|
36
|
+
# 4bytes checksum at the end if CRC32 checksum is enabled
|
|
37
|
+
payload_size = checksum_enabled ? payload.bytesize - 4 : payload.bytesize
|
|
38
|
+
|
|
39
|
+
# Table ID (6 bytes)
|
|
40
|
+
table_id = StringIOUtil.read_uint48(io)
|
|
41
|
+
# Flags (2 bytes)
|
|
42
|
+
flags = StringIOUtil.read_uint16(io)
|
|
43
|
+
# Extra data length (2 bytes)
|
|
44
|
+
extra_data_length = StringIOUtil.read_uint16(io)
|
|
45
|
+
# Skip extra data if present
|
|
46
|
+
io.read(extra_data_length - 2) if extra_data_length > 2
|
|
47
|
+
|
|
48
|
+
# Column count (variable length encoded)
|
|
49
|
+
column_count = read_packed_integer(io)
|
|
50
|
+
|
|
51
|
+
# Columns present bitmap
|
|
52
|
+
bitmap_size = (column_count + 7) / 8
|
|
53
|
+
# A bitmap indicating which columns are present in the row data
|
|
54
|
+
columns_present_bitmap = MysqlReplicator::StringIOUtil.read_array_from_int8(io, bitmap_size)
|
|
55
|
+
# if UPDATE_ROWS event, after this bitmap, there is another bitmap for "columns present in the after image"
|
|
56
|
+
columns_after_bitmap = event_type == :UPDATE_ROWS ? MysqlReplicator::StringIOUtil.read_array_from_int8(io, bitmap_size) : []
|
|
57
|
+
|
|
58
|
+
# Parse row data
|
|
59
|
+
table_def = table_map[table_id]
|
|
60
|
+
rows = []
|
|
61
|
+
while io.pos < payload_size
|
|
62
|
+
if event_type == :UPDATE_ROWS
|
|
63
|
+
before_row = parse_row(io, column_count, columns_present_bitmap, table_def)
|
|
64
|
+
after_row = parse_row(io, column_count, columns_after_bitmap, table_def)
|
|
65
|
+
rows << { before: before_row, after: after_row }
|
|
66
|
+
else
|
|
67
|
+
row = parse_row(io, column_count, columns_present_bitmap, table_def)
|
|
68
|
+
rows << row
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
table_id: table_id,
|
|
74
|
+
flags: flags,
|
|
75
|
+
extra_data_length: extra_data_length,
|
|
76
|
+
column_count: column_count,
|
|
77
|
+
rows: rows
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @rbs io: StringIO
|
|
82
|
+
# @rbs column_count: Integer
|
|
83
|
+
# @rbs columns_present_bitmap: Array[Integer]
|
|
84
|
+
# @rbs table_def: MysqlReplicator::Binlogs::TableMapEventParser::execution
|
|
85
|
+
# @rbs return: Array[rowData]
|
|
86
|
+
def self.parse_row(io, column_count, columns_present_bitmap, table_def)
|
|
87
|
+
# Null bitmap
|
|
88
|
+
# A bitmap indicating which columns are NULL
|
|
89
|
+
present_count = count_bits(columns_present_bitmap, column_count)
|
|
90
|
+
null_bitmap_size = (present_count + 7) / 8
|
|
91
|
+
null_bitmap = MysqlReplicator::StringIOUtil.read_array_from_int8(io, null_bitmap_size)
|
|
92
|
+
null_bit_index = 0
|
|
93
|
+
|
|
94
|
+
row = []
|
|
95
|
+
table_def[:columns].each_with_index do |column_def, column_index|
|
|
96
|
+
unless bit_set?(columns_present_bitmap, column_index)
|
|
97
|
+
next
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
column_name = column_def[:column_name]
|
|
101
|
+
|
|
102
|
+
# Check if the column is NULL
|
|
103
|
+
value = if bit_set?(null_bitmap, null_bit_index)
|
|
104
|
+
nil
|
|
105
|
+
else
|
|
106
|
+
MysqlReplicator::Binlogs::ColumnParser.parse(io, column_def)
|
|
107
|
+
end
|
|
108
|
+
null_bit_index += 1
|
|
109
|
+
|
|
110
|
+
row << {
|
|
111
|
+
ordinal_position: column_def[:ordinal_position].to_i,
|
|
112
|
+
data_type: column_def[:data_type],
|
|
113
|
+
column_name: column_name,
|
|
114
|
+
value: value,
|
|
115
|
+
primary_key: column_def[:primary_key]
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
row
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @rbs io: StringIO
|
|
123
|
+
# @rbs return: Integer
|
|
124
|
+
def self.read_packed_integer(io)
|
|
125
|
+
first = StringIOUtil.read_uint8(io)
|
|
126
|
+
case first
|
|
127
|
+
when 0..250
|
|
128
|
+
first
|
|
129
|
+
when 252
|
|
130
|
+
StringIOUtil.read_uint16(io)
|
|
131
|
+
when 253
|
|
132
|
+
StringIOUtil.read_uint24(io)
|
|
133
|
+
when 254
|
|
134
|
+
StringIOUtil.read_uint64(io)
|
|
135
|
+
else
|
|
136
|
+
raise "Invalid packed integer: #{first}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @rbs bitmap: Array[Integer]
|
|
141
|
+
# @rbs index: Integer
|
|
142
|
+
# @rbs return: bool
|
|
143
|
+
def self.bit_set?(bitmap, index)
|
|
144
|
+
byte_index = index / 8
|
|
145
|
+
bit_index = index % 8
|
|
146
|
+
(bitmap[byte_index] & (1 << bit_index)) != 0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @rbs bitmap: Array[Integer]
|
|
150
|
+
# @rbs max_bits: Integer
|
|
151
|
+
# @rbs return: Integer
|
|
152
|
+
def self.count_bits(bitmap, max_bits)
|
|
153
|
+
count = 0
|
|
154
|
+
max_bits.times do |i|
|
|
155
|
+
count += 1 if bit_set?(bitmap, i)
|
|
156
|
+
end
|
|
157
|
+
count
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|