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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +79 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +115 -0
  8. data/Rakefile +12 -0
  9. data/Steepfile +22 -0
  10. data/docker-compose.yml +13 -0
  11. data/lib/mysql_replicator/binlog_client.rb +201 -0
  12. data/lib/mysql_replicator/binlogs/column_parser.rb +425 -0
  13. data/lib/mysql_replicator/binlogs/constants.rb +74 -0
  14. data/lib/mysql_replicator/binlogs/event_parser.rb +134 -0
  15. data/lib/mysql_replicator/binlogs/format_description_event_parser.rb +24 -0
  16. data/lib/mysql_replicator/binlogs/json_parser.rb +335 -0
  17. data/lib/mysql_replicator/binlogs/query_event_parser.rb +69 -0
  18. data/lib/mysql_replicator/binlogs/rotate_event_parser.rb +37 -0
  19. data/lib/mysql_replicator/binlogs/rows_event_parser.rb +161 -0
  20. data/lib/mysql_replicator/binlogs/table_map_event_parser.rb +155 -0
  21. data/lib/mysql_replicator/binlogs/xid_event_parser.rb +25 -0
  22. data/lib/mysql_replicator/connection.rb +226 -0
  23. data/lib/mysql_replicator/connections/auth.rb +303 -0
  24. data/lib/mysql_replicator/connections/handshake.rb +132 -0
  25. data/lib/mysql_replicator/connections/query.rb +322 -0
  26. data/lib/mysql_replicator/error.rb +6 -0
  27. data/lib/mysql_replicator/logger.rb +43 -0
  28. data/lib/mysql_replicator/string_io_util.rb +199 -0
  29. data/lib/mysql_replicator/string_util.rb +106 -0
  30. data/lib/mysql_replicator/version.rb +6 -0
  31. data/lib/mysql_replicator.rb +51 -0
  32. data/sig/generated/mysql_replicator/binlog_client.rbs +52 -0
  33. data/sig/generated/mysql_replicator/binlogs/column_parser.rbs +134 -0
  34. data/sig/generated/mysql_replicator/binlogs/constants.rbs +69 -0
  35. data/sig/generated/mysql_replicator/binlogs/event_parser.rbs +35 -0
  36. data/sig/generated/mysql_replicator/binlogs/format_description_event_parser.rbs +13 -0
  37. data/sig/generated/mysql_replicator/binlogs/json_parser.rbs +101 -0
  38. data/sig/generated/mysql_replicator/binlogs/query_event_parser.rbs +14 -0
  39. data/sig/generated/mysql_replicator/binlogs/rotate_event_parser.rbs +14 -0
  40. data/sig/generated/mysql_replicator/binlogs/rows_event_parser.rbs +39 -0
  41. data/sig/generated/mysql_replicator/binlogs/table_map_event_parser.rbs +31 -0
  42. data/sig/generated/mysql_replicator/binlogs/xid_event_parser.rbs +13 -0
  43. data/sig/generated/mysql_replicator/connection.rbs +103 -0
  44. data/sig/generated/mysql_replicator/connections/auth.rbs +76 -0
  45. data/sig/generated/mysql_replicator/connections/handshake.rbs +21 -0
  46. data/sig/generated/mysql_replicator/connections/query.rbs +62 -0
  47. data/sig/generated/mysql_replicator/error.rbs +6 -0
  48. data/sig/generated/mysql_replicator/logger.rbs +26 -0
  49. data/sig/generated/mysql_replicator/string_io_util.rbs +75 -0
  50. data/sig/generated/mysql_replicator/string_util.rbs +45 -0
  51. data/sig/generated/mysql_replicator/types.rbs +19 -0
  52. data/sig/generated/mysql_replicator/version.rbs +5 -0
  53. data/sig/generated/mysql_replicator.rbs +16 -0
  54. 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