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,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,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
|