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,132 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module MysqlReplicator
5
+ module Connections
6
+ class Handshake
7
+ # @rbs!
8
+ # type handshake = {
9
+ # protocol_version: Integer,
10
+ # server_version: String,
11
+ # connection_id: Integer,
12
+ # capability_flags: Integer | nil,
13
+ # charset: Integer | nil,
14
+ # status_flags: Integer | nil,
15
+ # auth_plugin_name: String,
16
+ # auth_plugin_data: String
17
+ # }
18
+
19
+ # @rbs connection: MysqlReplicator::Connection
20
+ # @rbs return: handshake
21
+ def self.execute(connection)
22
+ handshake_response_packet = connection.read_packet
23
+ handshake_info = parse_handshake_response_packet(handshake_response_packet)
24
+
25
+ debug_handshake_info(handshake_info)
26
+ handshake_info
27
+ end
28
+
29
+ # @rbs packet: MysqlReplicator::Types::packet
30
+ # @rbs return: handshake
31
+ def self.parse_handshake_response_packet(packet)
32
+ payload = packet[:payload]
33
+ offset = 0
34
+
35
+ # Protocol version (1 byte)
36
+ protocol_version = MysqlReplicator::StringUtil.read_uint8(payload[offset])
37
+ offset += 1
38
+
39
+ # Server version is null-terminated string
40
+ server_version_end = payload.index("\0", offset) || 0
41
+ server_version = MysqlReplicator::StringUtil.read_str(payload[offset...server_version_end])
42
+ offset = server_version_end + 1
43
+
44
+ # ConnectionID is 4bytes and little endian
45
+ connection_id = MysqlReplicator::StringUtil.read_uint32(payload[offset..(offset + 3)])
46
+ offset += 4
47
+
48
+ # Authentication plugin data (first 8 bytes)
49
+ auth_plugin_data_part1 = MysqlReplicator::StringUtil.read_str(payload[offset..(offset + 7)])
50
+ offset += 8
51
+
52
+ # Reserved (1 byte, always 0x00)
53
+ offset += 1
54
+
55
+ # Server capability flags (lower 2 bytes)
56
+ capability_flags_lower = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
57
+ offset += 2
58
+
59
+ # After MySQL 8.0, additional authentication plugin information is included
60
+ if offset < payload.length
61
+ # Character set (1 byte)
62
+ charset = MysqlReplicator::StringUtil.read_uint8(payload[offset])
63
+ offset += 1
64
+
65
+ # Status flags (2 bytes)
66
+ status_flags = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
67
+ offset += 2
68
+
69
+ # Server capability flags (upper 2 bytes)
70
+ capability_flags_upper = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
71
+ offset += 2
72
+
73
+ # Feature flags
74
+ capability_flags = capability_flags_lower | (capability_flags_upper << 16)
75
+
76
+ # Authentication plugin data length (1 byte)
77
+ auth_plugin_data_len = MysqlReplicator::StringUtil.read_uint8(payload[offset])
78
+ offset += 1
79
+
80
+ # Reserved (10 bytes)
81
+ offset += 10
82
+
83
+ # Authentication plugin data (part 2)
84
+ remaining_auth_data_len = [auth_plugin_data_len - 8, 13].max
85
+ auth_plugin_data_part2 = MysqlReplicator::StringUtil.read_str(payload[offset..(offset + remaining_auth_data_len - 1)])
86
+ offset += remaining_auth_data_len
87
+
88
+ # Authentication plugin name (null-terminated string)
89
+ plugin_name_end = payload.index("\0", offset)
90
+ auth_plugin_name = MysqlReplicator::StringUtil.read_str(payload[offset...plugin_name_end])
91
+ auth_plugin_data = auth_plugin_data_part1 + MysqlReplicator::StringUtil.read_str(auth_plugin_data_part2[0..11])
92
+ # Adjust 20 bytes
93
+ if auth_plugin_data.length > 20
94
+ auth_plugin_data = auth_plugin_data[0..19] || ''
95
+ elsif auth_plugin_data.length < 20
96
+ auth_plugin_data += "\x00" * (20 - auth_plugin_data.length)
97
+ end
98
+ else
99
+ auth_plugin_name = 'mysql_native_password'
100
+ auth_plugin_data = auth_plugin_data_part1
101
+ end
102
+
103
+ {
104
+ protocol_version: protocol_version,
105
+ server_version: server_version,
106
+ connection_id: connection_id,
107
+ capability_flags: capability_flags,
108
+ charset: charset,
109
+ status_flags: status_flags,
110
+ auth_plugin_name: auth_plugin_name,
111
+ auth_plugin_data: auth_plugin_data
112
+ }
113
+ end
114
+
115
+ # @rbs handshake_info: handshake
116
+ # @rbs return: void
117
+ def self.debug_handshake_info(handshake_info)
118
+ MysqlReplicator::Logger.debug \
119
+ "===== Start Handshake Info =====\n" \
120
+ "Protocol version: #{handshake_info[:protocol_version]}\n" \
121
+ "Server version: #{handshake_info[:server_version]}\n" \
122
+ "Connection ID: #{handshake_info[:connection_id]}\n" \
123
+ "Capability flags: 0x#{handshake_info[:capability_flags]&.to_s(16)&.upcase}\n" \
124
+ "Character set: #{handshake_info[:charset]}\n" \
125
+ "Status flags: #{handshake_info[:status_flags]}\n" \
126
+ "Authentication plugin name: #{handshake_info[:auth_plugin_name]}\n" \
127
+ "Authentication plugin data: #{handshake_info[:auth_plugin_data].unpack('C*').map { |b| format('%02X', b) }.join(' ')}\n" \
128
+ '===== End Handshake Info ====='
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module MysqlReplicator
5
+ module Connections
6
+ class Query
7
+ # @rbs!
8
+ # type columnData = {
9
+ # catalog: String,
10
+ # schema: String,
11
+ # table: String,
12
+ # org_table: String,
13
+ # name: String,
14
+ # org_name: String,
15
+ # charset: Integer,
16
+ # column_length: Integer,
17
+ # type: String
18
+ # }
19
+
20
+ # @rbs!
21
+ # type queryResultOk = {
22
+ # affected_rows: Integer | nil,
23
+ # insert_id: Integer | nil,
24
+ # status_flags: Integer | nil,
25
+ # warnings: Integer | nil,
26
+ # info_message: String | nil
27
+ # }
28
+
29
+ # @rbs!
30
+ # type queryResultError = {
31
+ # error_code: Integer,
32
+ # sql_state_marker: String,
33
+ # sql_state: String | nil,
34
+ # error_message: String | nil
35
+ # }
36
+
37
+ # @rbs!
38
+ # type queryResultSet = {
39
+ # columns: Array[columnData],
40
+ # rows: Array[Hash[Symbol, String | nil]],
41
+ # row_count: Integer
42
+ # }
43
+
44
+ # @rbs!
45
+ # type queryResult = queryResultOk | queryResultError | queryResultSet
46
+
47
+ # @rbs connection: MysqlReplicator::Connection
48
+ # @rbs sql: String
49
+ # @rbs return: queryResult
50
+ def self.execute(connection, sql)
51
+ query_payload = [0x03].pack('C') + sql.encode('utf-8')
52
+ connection.send_packet(query_payload)
53
+
54
+ response = connection.read_packet
55
+ case MysqlReplicator::StringUtil.read_uint8(response[:payload][0])
56
+ when 0x00 # OK
57
+ parse_ok(response[:payload])
58
+ when 0xFF # Error
59
+ parse_error(response[:payload])
60
+ else # Result set
61
+ parse_result_set(connection, response[:payload])
62
+ end
63
+ end
64
+
65
+ # @rbs payload: String
66
+ # @rbs return: queryResultOk
67
+ def self.parse_ok(payload)
68
+ offset = 1 # Skip 0x00
69
+
70
+ # affected_rows (length-encoded integer)
71
+ affected_rows = length_encoded_integer(payload, offset)[:value]
72
+ offset += length_encoded_integer_size(affected_rows)
73
+
74
+ # insert_id (length-encoded integer)
75
+ insert_id = length_encoded_integer(payload, offset)[:value]
76
+ offset += length_encoded_integer_size(insert_id)
77
+
78
+ # status_flags (2 bytes)
79
+ if payload.length > offset + 1
80
+ status_flags = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
81
+ offset += 2
82
+ else
83
+ status_flags = nil
84
+ end
85
+
86
+ # warnings (2 bytes)
87
+ if payload.length > offset + 3
88
+ warnings = MysqlReplicator::StringUtil.read_uint16(payload[(offset + 2)..(offset + 3)])
89
+ offset += 2
90
+ else
91
+ warnings = nil
92
+ end
93
+
94
+ # info_message (all the rest)
95
+ info_message = payload.length > offset + 4 ? payload[(offset + 4)..] : nil
96
+
97
+ {
98
+ affected_rows: affected_rows,
99
+ insert_id: insert_id,
100
+ status_flags: status_flags,
101
+ warnings: warnings,
102
+ info_message: info_message
103
+ }
104
+ end
105
+
106
+ # @rbs payload: String
107
+ # @rbs return: queryResultError
108
+ def self.parse_error(payload)
109
+ error_code = MysqlReplicator::StringUtil.read_uint16(payload[1..2])
110
+ sql_state_marker = (payload[3] || '').chr
111
+ sql_state = payload[4..8] || nil
112
+ error_message = payload[9..] || nil
113
+
114
+ {
115
+ error_code: error_code,
116
+ sql_state_marker: sql_state_marker,
117
+ sql_state: sql_state,
118
+ error_message: error_message
119
+ }
120
+ end
121
+
122
+ # @rbs connection: MysqlReplicator::Connection
123
+ # @rbs payload: String
124
+ # @rbs return: queryResultSet
125
+ def self.parse_result_set(connection, payload)
126
+ # Read columns definition
127
+ columns = []
128
+ column_count = length_encoded_integer(payload, 0)[:value].to_i
129
+ column_count.times do
130
+ column_packet = connection.read_packet
131
+ column_info = parse_column_definition(column_packet[:payload])
132
+ columns << column_info
133
+ end
134
+
135
+ # EOF packet(at finish)
136
+ connection.read_packet
137
+
138
+ rows = [] #: Array[Hash[Symbol, String | nil]]
139
+
140
+ loop do
141
+ row_packet = connection.read_packet
142
+
143
+ # Check EOF packet
144
+ if MysqlReplicator::StringUtil.read_uint8(row_packet[:payload][0]) == 0xFE
145
+ break
146
+ end
147
+
148
+ row_data = parse_row_data(row_packet[:payload], columns)
149
+ rows << row_data
150
+ end
151
+
152
+ { columns: columns, rows: rows, row_count: rows.length }
153
+ end
154
+
155
+ # @rbs payload: String
156
+ # @rbs return: columnData
157
+ def self.parse_column_definition(payload)
158
+ offset = 0
159
+
160
+ # catalog (length-encoded string)
161
+ catalog = length_encoded_string(payload, offset)
162
+ offset += catalog[:bytes_read]
163
+
164
+ # schema (length-encoded string)
165
+ schema = length_encoded_string(payload, offset)
166
+ offset += schema[:bytes_read]
167
+
168
+ # table (length-encoded string)
169
+ table = length_encoded_string(payload, offset)
170
+ offset += table[:bytes_read]
171
+
172
+ # org_table (length-encoded string)
173
+ org_table = length_encoded_string(payload, offset)
174
+ offset += org_table[:bytes_read]
175
+
176
+ # name (length-encoded string)
177
+ name = length_encoded_string(payload, offset)
178
+ offset += name[:bytes_read]
179
+
180
+ # org_name (length-encoded string)
181
+ org_name = length_encoded_string(payload, offset)
182
+ offset += org_name[:bytes_read]
183
+
184
+ # length of fixed-length fields (1 byte)
185
+ offset += 1
186
+
187
+ # character set (2 bytes)
188
+ charset = MysqlReplicator::StringUtil.read_uint16(payload[offset..(offset + 1)])
189
+ offset += 2
190
+
191
+ # column length (4 bytes)
192
+ column_length = MysqlReplicator::StringUtil.read_uint32(payload[offset..(offset + 3)])
193
+ offset += 4
194
+
195
+ # type (1 byte)
196
+ type = MysqlReplicator::StringUtil.read_uint8(payload[offset])
197
+
198
+ {
199
+ catalog: catalog[:value],
200
+ schema: schema[:value],
201
+ table: table[:value],
202
+ org_table: org_table[:value],
203
+ name: name[:value],
204
+ org_name: org_name[:value],
205
+ charset: charset,
206
+ column_length: column_length,
207
+ type: type_to_string(type)
208
+ }
209
+ end
210
+
211
+ # @rbs payload: String
212
+ # @rbs columns: Array[columnData]
213
+ # @rbs return: Hash[Symbol, String | nil]
214
+ def self.parse_row_data(payload, columns)
215
+ first_byte = MysqlReplicator::StringUtil.read_uint8(payload[0])
216
+
217
+ row = {} #: Hash[Symbol, String | nil]
218
+ offset = 0
219
+
220
+ columns.each do |column|
221
+ column_name_key = column[:name].downcase.to_sym
222
+
223
+ if offset >= payload.length
224
+ row[column_name_key] = nil
225
+ next
226
+ end
227
+
228
+ if first_byte == 0xFB
229
+ # NULL value
230
+ row[column_name_key] = nil
231
+ offset += 1
232
+ else
233
+ # row data (length-encoded string)
234
+ value = length_encoded_string(payload, offset)
235
+ row[column_name_key] = value[:value]
236
+ offset += value[:bytes_read]
237
+ end
238
+ end
239
+
240
+ row
241
+ end
242
+
243
+ # @rbs payload: String
244
+ # @rbs offset: Integer
245
+ # @rbs return: { value: Integer | nil, bytes_read: Integer }
246
+ def self.length_encoded_integer(payload, offset)
247
+ first_byte = MysqlReplicator::StringUtil.read_uint8(payload[offset])
248
+
249
+ case first_byte
250
+ when 0..250
251
+ { value: first_byte, bytes_read: 1 }
252
+ when 0xFC
253
+ value = MysqlReplicator::StringUtil.read_uint16(payload[(offset + 1)..(offset + 2)])
254
+ { value: value, bytes_read: 3 }
255
+ when 0xFD
256
+ value = MysqlReplicator::StringUtil.read_uint32(payload[(offset + 1)..(offset + 3)]) & 0xFFFFFF
257
+ { value: value, bytes_read: 4 }
258
+ when 0xFE
259
+ value = MysqlReplicator::StringUtil.read_uint64(payload[(offset + 1)..(offset + 8)])
260
+ { value: value, bytes_read: 9 }
261
+ else # Included 0xFB
262
+ { value: nil, bytes_read: 1 }
263
+ end
264
+ end
265
+
266
+ # @rbs value: Integer | nil
267
+ # @rbs return: Integer
268
+ def self.length_encoded_integer_size(value)
269
+ return 1 if value.nil?
270
+ return 1 if value <= 250
271
+ return 3 if value <= 0xFFFF
272
+ return 4 if value <= 0xFFFFFF
273
+
274
+ 9
275
+ end
276
+
277
+ # @rbs payload: String
278
+ # @rbs offset: Integer
279
+ # @rbs return { value: String, bytes_read: Integer }
280
+ def self.length_encoded_string(payload, offset)
281
+ length_info = length_encoded_integer(payload, offset)
282
+ return { value: '', bytes_read: length_info[:bytes_read] } if length_info[:value].nil?
283
+
284
+ string_start = offset + length_info[:bytes_read]
285
+ string_end = string_start + length_info[:value] - 1
286
+ value = payload[string_start..string_end] || ''
287
+
288
+ {
289
+ value: value,
290
+ bytes_read: length_info[:bytes_read] + length_info[:value]
291
+ }
292
+ end
293
+
294
+ # @rbs type: Integer | Float | String | nil
295
+ # @rbs return: String
296
+ def self.type_to_string(type)
297
+ case type
298
+ when 0x00 then 'DECIMAL'
299
+ when 0x01 then 'TINYINT'
300
+ when 0x02 then 'SMALLINT'
301
+ when 0x03 then 'INT'
302
+ when 0x04 then 'FLOAT'
303
+ when 0x05 then 'DOUBLE'
304
+ when 0x06 then 'NULL'
305
+ when 0x07 then 'TIMESTAMP'
306
+ when 0x08 then 'BIGINT'
307
+ when 0x09 then 'MEDIUMINT'
308
+ when 0x0A then 'DATE'
309
+ when 0x0B then 'TIME'
310
+ when 0x0C then 'DATETIME'
311
+ when 0x0D then 'YEAR'
312
+ when 0x0F then 'VARCHAR'
313
+ when 0xF6 then 'NEWDECIMAL'
314
+ when 0xFC then 'BLOB'
315
+ when 0xFD then 'VAR_STRING'
316
+ when 0xFE then 'STRING'
317
+ else "UNKNOWN(#{type})"
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module MysqlReplicator
5
+ class Error < StandardError; end
6
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require 'logger'
5
+
6
+ module MysqlReplicator
7
+ class Logger
8
+ # @rbs self.@logger: ::Logger
9
+ @logger = ::Logger.new($stdout)
10
+ @logger.level = ENV.fetch('MYSQL_REPLICATOR_LOG_LEVEL', ::Logger::INFO)
11
+
12
+ # @rbs logger: ::Logger
13
+ def self.logger=(logger) # rubocop:disable Style/TrivialAccessors
14
+ @logger = logger
15
+ end
16
+
17
+ class << self
18
+ # @rbs message: String
19
+ # @rbs return: void
20
+ def debug(message)
21
+ @logger.debug(message)
22
+ end
23
+
24
+ # @rbs message: String
25
+ # @rbs return: void
26
+ def info(message)
27
+ @logger.info(message)
28
+ end
29
+
30
+ # @rbs message: String
31
+ # @rbs return: void
32
+ def warn(message)
33
+ @logger.warn(message)
34
+ end
35
+
36
+ # @rbs message: String
37
+ # @return: void
38
+ def error(message)
39
+ @logger.error(message)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module MysqlReplicator
5
+ class StringIOUtil
6
+ # @rbs io: StringIO
7
+ # @rbs return: String
8
+ def self.read_str(io, length)
9
+ io.read(length) || ''
10
+ end
11
+
12
+ # @rbs io: StringIO
13
+ # @rbs return: Integer
14
+ def self.read_packed_integer(io)
15
+ first = read_uint8(io)
16
+
17
+ case first
18
+ when 0..250
19
+ first
20
+ when 252
21
+ read_uint16(io)
22
+ when 253
23
+ read_uint24(io)
24
+ when 254
25
+ read_uint64(io)
26
+ else
27
+ raise "Invalid packed integer: #{first}"
28
+ end
29
+ end
30
+
31
+ # @rbs io: StringIO
32
+ # @rbs return: Integer
33
+ def self.read_int8(io)
34
+ value = io.read(1)
35
+ if value.nil?
36
+ raise MysqlReplicator::Error, 'payload is nil'
37
+ end
38
+
39
+ value.unpack1('c').to_i
40
+ end
41
+
42
+ # @rbs io: StringIO
43
+ # @rbs return: Integer
44
+ def self.read_uint8(io)
45
+ value = io.read(1)
46
+ if value.nil?
47
+ raise MysqlReplicator::Error, 'payload is nil'
48
+ end
49
+
50
+ value.unpack1('C').to_i
51
+ end
52
+
53
+ # @rbs io: StringIO
54
+ # @rbs return: Integer
55
+ def self.read_int16(io)
56
+ value = io.read(2)
57
+ if value.nil?
58
+ raise MysqlReplicator::Error, 'payload is nil'
59
+ end
60
+
61
+ value.unpack1('s<').to_i
62
+ end
63
+
64
+ # @rbs io: StringIO
65
+ # @rbs return: Integer
66
+ def self.read_uint16(io)
67
+ value = io.read(2)
68
+ if value.nil?
69
+ raise MysqlReplicator::Error, 'payload is nil'
70
+ end
71
+
72
+ value.unpack1('v').to_i
73
+ end
74
+
75
+ # @rbs io: StringIO
76
+ # @rbs return: Integer
77
+ def self.read_int24(io)
78
+ payload = io.read(3)
79
+ if payload.nil?
80
+ raise MysqlReplicator::Error, 'payload is nil'
81
+ end
82
+
83
+ bytes = payload.unpack('C3').map(&:to_i)
84
+ value = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16)
85
+ value >= 0x800000 ? value - 0x1000000 : value
86
+ end
87
+
88
+ # @rbs io: StringIO
89
+ # @rbs return: Integer
90
+ def self.read_uint24(io)
91
+ payload = io.read(3)
92
+ if payload.nil?
93
+ raise MysqlReplicator::Error, 'payload is nil'
94
+ end
95
+
96
+ bytes = payload.unpack('C3').map(&:to_i)
97
+ bytes[0] | (bytes[1] << 8) | (bytes[2] << 16)
98
+ end
99
+
100
+ # @rbs io: StringIO
101
+ # @rbs return: Integer
102
+ def self.read_int32(io)
103
+ value = io.read(4)
104
+ if value.nil?
105
+ raise MysqlReplicator::Error, 'payload is nil'
106
+ end
107
+
108
+ value.unpack1('l<').to_i
109
+ end
110
+
111
+ # @rbs io: StringIO
112
+ # @rbs return: Integer
113
+ def self.read_uint32(io)
114
+ value = io.read(4)
115
+ if value.nil?
116
+ raise MysqlReplicator::Error, 'payload is nil'
117
+ end
118
+
119
+ value.unpack1('V').to_i
120
+ end
121
+
122
+ # @rbs io: StringIO
123
+ # @rbs return: Integer
124
+ def self.read_uint32_big_endian(io)
125
+ value = io.read(4)
126
+ if value.nil?
127
+ raise MysqlReplicator::Error, 'payload is nil'
128
+ end
129
+
130
+ value.unpack1('N').to_i
131
+ end
132
+
133
+ # @rbs io: StringIO
134
+ # @rbs return: Integer
135
+ def self.read_int48(io)
136
+ low = read_int32(io)
137
+ high = read_int16(io)
138
+ combined = low | (high << 32)
139
+ combined >= 0x800000000000 ? combined - 0x1000000000000 : combined
140
+ end
141
+
142
+ # @rbs io: StringIO
143
+ # @rbs return: Integer
144
+ def self.read_uint48(io)
145
+ low = read_uint32(io)
146
+ high = read_uint16(io)
147
+ low | (high << 32)
148
+ end
149
+
150
+ # @rbs io: StringIO
151
+ # @rbs return: Integer
152
+ def self.read_int64(io)
153
+ value = io.read(8)
154
+ if value.nil?
155
+ raise MysqlReplicator::Error, 'payload is nil'
156
+ end
157
+
158
+ value.unpack1('q<').to_i
159
+ end
160
+
161
+ # @rbs io: StringIO
162
+ # @rbs return: Integer
163
+ def self.read_uint64(io)
164
+ value = io.read(8)
165
+ if value.nil?
166
+ raise MysqlReplicator::Error, 'payload is nil'
167
+ end
168
+
169
+ value.unpack1('Q<').to_i
170
+ end
171
+
172
+ # @rbs io: StringIO
173
+ # @rbs return: Float
174
+ def self.read_float32(io)
175
+ value = io.read(4)
176
+ if value.nil?
177
+ raise MysqlReplicator::Error, 'payload is nil'
178
+ end
179
+
180
+ value.unpack1('e').to_f
181
+ end
182
+
183
+ # @rbs io: StringIO
184
+ # @rbs return: Float
185
+ def self.read_double64(io)
186
+ value = io.read(8)
187
+ if value.nil?
188
+ raise MysqlReplicator::Error, 'payload is nil'
189
+ end
190
+
191
+ value.unpack1('E').to_f
192
+ end
193
+
194
+ def self.read_array_from_int8(io, length)
195
+ value = io.read(length) || ''
196
+ value.unpack('C*').map(&:to_i)
197
+ end
198
+ end
199
+ end