mysql_binlog 0.1.0 → 0.1.1
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.
- data/lib/mysql_binlog/binlog_event_field_parser.rb +34 -20
- data/lib/mysql_binlog/binlog_parser.rb +47 -12
- data/lib/mysql_binlog/mysql_binlog.rb +65 -26
- data/lib/mysql_binlog.rb +1 -0
- metadata +3 -3
@@ -1,17 +1,21 @@
|
|
1
1
|
module MysqlBinlog
|
2
|
+
# A mapping array for all values that may appear in the +status+ field of
|
3
|
+
# a query_event.
|
2
4
|
QUERY_EVENT_STATUS_TYPES = [
|
3
|
-
:flags2,
|
4
|
-
:sql_mode,
|
5
|
-
:catalog,
|
6
|
-
:auto_increment,
|
7
|
-
:charset,
|
8
|
-
:time_zone,
|
9
|
-
:catalog_nz,
|
10
|
-
:lc_time_names,
|
11
|
-
:charset_database,
|
12
|
-
:table_map_for_update,
|
5
|
+
:flags2, # 0
|
6
|
+
:sql_mode, # 1
|
7
|
+
:catalog, # 2
|
8
|
+
:auto_increment, # 3
|
9
|
+
:charset, # 4
|
10
|
+
:time_zone, # 5
|
11
|
+
:catalog_nz, # 6
|
12
|
+
:lc_time_names, # 7
|
13
|
+
:charset_database, # 8
|
14
|
+
:table_map_for_update, # 9
|
13
15
|
]
|
14
16
|
|
17
|
+
# A mapping hash for all values that may appear in the +flags2+ field of
|
18
|
+
# a query_event.
|
15
19
|
QUERY_EVENT_FLAGS2 = {
|
16
20
|
:auto_is_null => 1 << 14,
|
17
21
|
:not_autocommit => 1 << 19,
|
@@ -31,26 +35,27 @@ module MysqlBinlog
|
|
31
35
|
@table_map = {}
|
32
36
|
end
|
33
37
|
|
38
|
+
# Parse additional fields for a +Rotate+ event.
|
34
39
|
def rotate_event(header, fields)
|
35
40
|
name_length = reader.remaining(header)
|
36
|
-
fields[:name_length] = name_length
|
37
41
|
fields[:name] = reader.read(name_length)
|
38
42
|
end
|
39
43
|
|
44
|
+
# Parse a dynamic +status+ structure within a query_event, which consists
|
45
|
+
# of a status_length (uint16) followed by a number of status variables
|
46
|
+
# (determined by the +status_length+) each of which consist of:
|
47
|
+
# * A type code (uint8), one of QUERY_EVENT_STATUS_TYPES.
|
48
|
+
# * The content itself, determined by the type. Additional processing is
|
49
|
+
# required based on the type.
|
40
50
|
def _query_event_status(header, fields)
|
41
51
|
status = {}
|
42
|
-
|
52
|
+
status_length = parser.read_uint16
|
53
|
+
end_position = reader.position + status_length
|
43
54
|
while reader.position < end_position
|
44
55
|
status_type = QUERY_EVENT_STATUS_TYPES[parser.read_uint8]
|
45
56
|
status[status_type] = case status_type
|
46
57
|
when :flags2
|
47
|
-
|
48
|
-
QUERY_EVENT_FLAGS2.inject([]) do |result, (flag_name, flag_bit_value)|
|
49
|
-
if (flags2 & flag_bit_value) != 0
|
50
|
-
result << flag_name
|
51
|
-
end
|
52
|
-
result
|
53
|
-
end
|
58
|
+
parser.read_uint32_bitmap_by_name(QUERY_EVENT_FLAGS2)
|
54
59
|
when :sql_mode
|
55
60
|
parser.read_uint64
|
56
61
|
when :catalog
|
@@ -81,15 +86,16 @@ module MysqlBinlog
|
|
81
86
|
status
|
82
87
|
end
|
83
88
|
|
89
|
+
# Parse additional fields for a +Query+ event.
|
84
90
|
def query_event(header, fields)
|
85
91
|
fields[:status] = _query_event_status(header, fields)
|
86
|
-
fields.delete :status_length
|
87
92
|
fields[:db] = parser.read_nstringz(fields[:db_length])
|
88
93
|
fields.delete :db_length
|
89
94
|
query_length = reader.remaining(header)
|
90
95
|
fields[:query] = reader.read([query_length, binlog.max_query_length].min)
|
91
96
|
end
|
92
97
|
|
98
|
+
# Parse additional fields for an +Intvar+ event.
|
93
99
|
def intvar_event(header, fields)
|
94
100
|
case fields[:intvar_type]
|
95
101
|
when 1
|
@@ -101,6 +107,7 @@ module MysqlBinlog
|
|
101
107
|
end
|
102
108
|
end
|
103
109
|
|
110
|
+
# Parse additional fields for a +Table_map+ event.
|
104
111
|
def table_map_event(header, fields)
|
105
112
|
fields[:table_id] = parser.read_uint48
|
106
113
|
fields[:flags] = parser.read_uint16
|
@@ -122,6 +129,9 @@ module MysqlBinlog
|
|
122
129
|
fields[:map_entry] = map_entry
|
123
130
|
end
|
124
131
|
|
132
|
+
# Parse the row images present in a row-based replication row event. This
|
133
|
+
# is rather incomplete right now due missing support for many MySQL types,
|
134
|
+
# but can parse some basic events.
|
125
135
|
def _generic_rows_event_row_images(header, fields)
|
126
136
|
row_images = []
|
127
137
|
end_position = reader.position + reader.remaining(header)
|
@@ -142,6 +152,10 @@ module MysqlBinlog
|
|
142
152
|
row_images
|
143
153
|
end
|
144
154
|
|
155
|
+
# Parse additional fields for any of the row-based replication row events:
|
156
|
+
# * +Write_rows+ which is used for +INSERT+.
|
157
|
+
# * +Update_rows+ which is used for +UPDATE+.
|
158
|
+
# * +Delete_rows+ which is used for +DELETE+.
|
145
159
|
def generic_rows_event(header, fields)
|
146
160
|
table_id = parser.read_uint48
|
147
161
|
fields[:table] = @table_map[table_id]
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module MysqlBinlog
|
2
|
+
# All MySQL types mapping to their integer values.
|
2
3
|
MYSQL_TYPES_HASH = {
|
3
4
|
:decimal => 0,
|
4
5
|
:tiny => 1,
|
@@ -28,7 +29,8 @@ module MysqlBinlog
|
|
28
29
|
:string => 254,
|
29
30
|
:geometry => 255,
|
30
31
|
}
|
31
|
-
|
32
|
+
|
33
|
+
# All MySQL types in a simple lookup array to map an integer to its symbol.
|
32
34
|
MYSQL_TYPES = MYSQL_TYPES_HASH.inject(Array.new(256)) do |type_array, item|
|
33
35
|
type_array[item[1]] = item[0]
|
34
36
|
type_array
|
@@ -44,76 +46,107 @@ module MysqlBinlog
|
|
44
46
|
@reader = binlog_instance.reader
|
45
47
|
end
|
46
48
|
|
49
|
+
# Read an unsigned 8-bit (1-byte) integer.
|
47
50
|
def read_uint8
|
48
51
|
reader.read(1).unpack("C").first
|
49
52
|
end
|
50
53
|
|
54
|
+
# Read an unsigned 16-bit (2-byte) integer.
|
51
55
|
def read_uint16
|
52
56
|
reader.read(2).unpack("v").first
|
53
57
|
end
|
54
58
|
|
59
|
+
# Read an unsigned 24-bit (3-byte) integer.
|
55
60
|
def read_uint24
|
56
61
|
a, b, c = reader.read(3).unpack("CCC")
|
57
62
|
a + (b << 8) + (c << 16)
|
58
63
|
end
|
59
64
|
|
65
|
+
# Read an unsigned 32-bit (4-byte) integer.
|
60
66
|
def read_uint32
|
61
67
|
reader.read(4).unpack("V").first
|
62
68
|
end
|
63
69
|
|
70
|
+
# Read an unsigned 48-bit (6-byte) integer.
|
64
71
|
def read_uint48
|
65
72
|
a, b, c = reader.read(6).unpack("vvv")
|
66
73
|
a + (b << 16) + (c << 32)
|
67
74
|
end
|
68
75
|
|
76
|
+
# Read an unsigned 64-bit (8-byte) integer.
|
69
77
|
def read_uint64
|
70
78
|
reader.read(8).unpack("Q").first
|
71
79
|
end
|
72
80
|
|
81
|
+
# Read a single-precision (4-byte) floating point number.
|
73
82
|
def read_float
|
74
83
|
reader.read(4).unpack("g").first
|
75
84
|
end
|
76
85
|
|
86
|
+
# Read a double-precision (8-byte) floating point number.
|
77
87
|
def read_double
|
78
88
|
reader.read(8).unpack("G").first
|
79
89
|
end
|
80
90
|
|
91
|
+
# Read a variable-length encoded integer. This is very broken at the
|
92
|
+
# moment, and is just mapping to read_uint8, so it cannot handle numbers
|
93
|
+
# greater than 251. This works fine for most structural elements of binary
|
94
|
+
# logs, but will fall over with decoding actual RBR row images.
|
81
95
|
def read_varint
|
82
96
|
# Cheating for now.
|
83
97
|
read_uint8
|
84
98
|
end
|
85
99
|
|
86
|
-
|
87
|
-
|
100
|
+
# Read a non-terminated string, provided its length.
|
101
|
+
def read_nstring(length)
|
88
102
|
reader.read(length)
|
89
103
|
end
|
90
104
|
|
91
|
-
|
92
|
-
|
105
|
+
# Read a null-terminated string, provided its length (without the null).
|
106
|
+
def read_nstringz(length)
|
107
|
+
string = read_nstring(length)
|
93
108
|
reader.read(1) # null
|
94
109
|
string
|
95
110
|
end
|
96
111
|
|
97
|
-
|
98
|
-
|
99
|
-
|
112
|
+
# Read a (Pascal-style) length-prefixed string. The length is stored as a
|
113
|
+
# 8-bit (1-byte) unsigned integer followed by the string itself with no
|
114
|
+
# termination character.
|
115
|
+
def read_lpstring
|
116
|
+
length = read_uint8
|
117
|
+
read_nstring(length)
|
100
118
|
end
|
101
119
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
120
|
+
# Read an lpstring which is also terminated with a null byte.
|
121
|
+
def read_lpstringz
|
122
|
+
length = read_uint8
|
123
|
+
read_nstringz(length)
|
106
124
|
end
|
107
125
|
|
126
|
+
# Read an array of unsigned 8-bit (1-byte) integers.
|
108
127
|
def read_uint8_array(length)
|
109
128
|
reader.read(length).bytes.to_a
|
110
129
|
end
|
111
130
|
|
131
|
+
# Read an arbitrary-length bitmap, provided its length. Returns an array
|
132
|
+
# of true/false values.
|
112
133
|
def read_bit_array(length)
|
113
134
|
data = reader.read((length+7)/8)
|
114
135
|
data.unpack("b*").first.bytes.to_a.map { |i| (i-48) == 1 }.shift(length)
|
115
136
|
end
|
116
137
|
|
138
|
+
def read_uint32_bitmap_by_name(names)
|
139
|
+
value = read_uint32
|
140
|
+
names.inject([]) do |result, (name, bit_value)|
|
141
|
+
if (value & bit_value) != 0
|
142
|
+
result << name
|
143
|
+
end
|
144
|
+
result
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Read a series of fields, provided an array of field descriptions. This
|
149
|
+
# can be used to read many types of fixed-length structures.
|
117
150
|
def read_and_unpack(format_description)
|
118
151
|
@format_cache[format_description] ||= {}
|
119
152
|
this_format = @format_cache[format_description][:format] ||=
|
@@ -131,6 +164,8 @@ module MysqlBinlog
|
|
131
164
|
fields
|
132
165
|
end
|
133
166
|
|
167
|
+
# Read a single field, provided the MySQL column type as a symbol. Not all
|
168
|
+
# types are currently supported.
|
134
169
|
def read_mysql_type(column_type)
|
135
170
|
case column_type
|
136
171
|
#when :decimal
|
@@ -1,6 +1,7 @@
|
|
1
|
-
require 'mysql_binlog/binlog_parser'
|
2
|
-
|
3
1
|
module MysqlBinlog
|
2
|
+
# A simple method to print a string as in hex representation per byte,
|
3
|
+
# with no more than 24 bytes per line, and spaces between each byte.
|
4
|
+
# There is probably a better way to do this, but I don't know it.
|
4
5
|
def puts_hex(data)
|
5
6
|
hex = data.bytes.each_slice(24).inject("") do |string, slice|
|
6
7
|
string << slice.map { |b| "%02x" % b }.join(" ") + "\n"
|
@@ -9,19 +10,7 @@ module MysqlBinlog
|
|
9
10
|
puts hex
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
|
-
{ :name => :magic, :length => 4, :format => "V" },
|
14
|
-
]
|
15
|
-
|
16
|
-
EVENT_HEADER = [
|
17
|
-
{ :name => :timestamp, :length => 4, :format => "V" },
|
18
|
-
{ :name => :event_type, :length => 1, :format => "C" },
|
19
|
-
{ :name => :server_id, :length => 4, :format => "V" },
|
20
|
-
{ :name => :event_length, :length => 4, :format => "V" },
|
21
|
-
{ :name => :next_position, :length => 4, :format => "V" },
|
22
|
-
{ :name => :flags, :length => 2, :format => "v" },
|
23
|
-
]
|
24
|
-
|
13
|
+
# An array to quickly map an integer event type to its symbol.
|
25
14
|
EVENT_TYPES = [
|
26
15
|
:unknown_event, # 0
|
27
16
|
:start_event_v3, # 1
|
@@ -53,6 +42,19 @@ module MysqlBinlog
|
|
53
42
|
:heartbeat_log_event, # 27
|
54
43
|
]
|
55
44
|
|
45
|
+
# A common fixed-length header that is included with each event.
|
46
|
+
EVENT_HEADER = [
|
47
|
+
{ :name => :timestamp, :length => 4, :format => "V" },
|
48
|
+
{ :name => :event_type, :length => 1, :format => "C" },
|
49
|
+
{ :name => :server_id, :length => 4, :format => "V" },
|
50
|
+
{ :name => :event_length, :length => 4, :format => "V" },
|
51
|
+
{ :name => :next_position, :length => 4, :format => "V" },
|
52
|
+
{ :name => :flags, :length => 2, :format => "v" },
|
53
|
+
]
|
54
|
+
|
55
|
+
# Values for the 'flags' field that may appear in binlogs. There are
|
56
|
+
# several other values that never appear in a file but may be used
|
57
|
+
# in events in memory.
|
56
58
|
EVENT_FLAGS = {
|
57
59
|
:binlog_in_use => 0x01,
|
58
60
|
:thread_specific => 0x04,
|
@@ -61,6 +63,9 @@ module MysqlBinlog
|
|
61
63
|
:relay_log => 0x40,
|
62
64
|
}
|
63
65
|
|
66
|
+
# Format descriptions for fixed-length fields that may appear in the data
|
67
|
+
# for each event. Additional fields may be dynamic and are parsed by the
|
68
|
+
# methods in the BinlogEventFieldParser class.
|
64
69
|
EVENT_FORMATS = {
|
65
70
|
:format_description_event => [
|
66
71
|
{ :name => :binlog_version, :length => 2, :format => "v" },
|
@@ -76,7 +81,6 @@ module MysqlBinlog
|
|
76
81
|
{ :name => :elapsed_time, :length => 4, :format => "V" },
|
77
82
|
{ :name => :db_length, :length => 1, :format => "C" },
|
78
83
|
{ :name => :error_code, :length => 2, :format => "v" },
|
79
|
-
{ :name => :status_length, :length => 2, :format => "v" },
|
80
84
|
],
|
81
85
|
:intvar_event => [
|
82
86
|
{ :name => :intvar_type, :length => 1, :format => "C" },
|
@@ -115,49 +119,79 @@ module MysqlBinlog
|
|
115
119
|
@max_query_length = 1048576
|
116
120
|
end
|
117
121
|
|
122
|
+
# Rewind to the beginning of the log, if supported by the reader. The
|
123
|
+
# reader may throw an exception if rewinding is not supported (e.g. for
|
124
|
+
# a stream-based reader).
|
118
125
|
def rewind
|
119
126
|
reader.rewind
|
120
|
-
read_file_header
|
121
127
|
end
|
122
128
|
|
129
|
+
# Read fixed fields using format definitions in 'unpack' format provided
|
130
|
+
# in the EVENT_FORMATS hash.
|
131
|
+
def read_fixed_fields(event_type, header)
|
132
|
+
if EVENT_FORMATS.include? event_type
|
133
|
+
parser.read_and_unpack(EVENT_FORMATS[event_type])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Read dynamic fields or fields that require more processing before being
|
138
|
+
# saved in the event.
|
123
139
|
def read_additional_fields(event_type, header, fields)
|
124
140
|
if event_field_parser.methods.include? event_type.to_s
|
125
141
|
event_field_parser.send(event_type, header, fields)
|
126
142
|
end
|
127
143
|
end
|
128
144
|
|
145
|
+
# Skip the remainder of this event. This can be used to skip an entire
|
146
|
+
# event or merely the parts of the event this library does not understand.
|
129
147
|
def skip_event(header)
|
130
148
|
reader.skip(header)
|
131
149
|
end
|
132
150
|
|
151
|
+
# Read the common header for an event. Every event has a header.
|
133
152
|
def read_event_header
|
134
153
|
header = parser.read_and_unpack(EVENT_HEADER)
|
154
|
+
|
155
|
+
# Merge the read 'flags' bitmap with the EVENT_FLAGS hash to return
|
156
|
+
# the flags by name instead of returning the bitmap as an integer.
|
135
157
|
flags = EVENT_FLAGS.inject([]) do |result, (flag_name, flag_bit_value)|
|
136
158
|
if (header[:flags] & flag_bit_value) != 0
|
137
159
|
result << flag_name
|
138
160
|
end
|
139
161
|
result
|
140
162
|
end
|
163
|
+
|
164
|
+
# Overwrite the integer version of 'flags' with the array of names.
|
141
165
|
header[:flags] = flags
|
166
|
+
|
142
167
|
header
|
143
168
|
end
|
144
169
|
|
170
|
+
# Read the content of the event, which consists of an optional fixed field
|
171
|
+
# portion and an optional dynamic portion.
|
145
172
|
def read_event_content(header)
|
146
|
-
content = nil
|
147
|
-
|
148
173
|
event_type = EVENT_TYPES[header[:event_type]]
|
149
|
-
if EVENT_FORMATS.include? event_type
|
150
|
-
content = parser.read_and_unpack(EVENT_FORMATS[event_type])
|
151
|
-
else
|
152
|
-
content = {}
|
153
|
-
end
|
154
174
|
|
175
|
+
# Read the fixed portion of the event, if it is understood, or there is
|
176
|
+
# one. If not, initialize content with an empty hash instead.
|
177
|
+
content = read_fixed_fields(event_type, header) || {}
|
178
|
+
|
179
|
+
# Read additional fields from the dynamic portion of the event. Some of
|
180
|
+
# these may actually be fixed width but needed more processing in a
|
181
|
+
# function instead of the unpack formats possible in read_fixed_fields.
|
155
182
|
read_additional_fields(event_type, header, content)
|
156
183
|
|
184
|
+
# Anything left unread at this point is skipped based on the event length
|
185
|
+
# provided in the header. In this way, it is possible to skip over events
|
186
|
+
# that are not able to be parsed correctly by this library.
|
157
187
|
skip_event(header)
|
188
|
+
|
158
189
|
content
|
159
190
|
end
|
160
191
|
|
192
|
+
# Scan events until finding one that isn't rejected by the filter rules.
|
193
|
+
# If there are no filter rules, this will return the next event provided
|
194
|
+
# by the reader.
|
161
195
|
def read_event
|
162
196
|
while true
|
163
197
|
skip_this_event = false
|
@@ -187,12 +221,13 @@ module MysqlBinlog
|
|
187
221
|
end
|
188
222
|
end
|
189
223
|
|
224
|
+
# Never skip over rotate_event or format_description_event as they
|
225
|
+
# are critical to understanding the format of this event stream.
|
190
226
|
unless [:rotate_event, :format_description_event].include? event_type
|
191
227
|
next if skip_this_event
|
192
228
|
end
|
193
229
|
|
194
230
|
content = read_event_content(header)
|
195
|
-
content
|
196
231
|
|
197
232
|
case event_type
|
198
233
|
when :rotate_event
|
@@ -213,6 +248,9 @@ module MysqlBinlog
|
|
213
248
|
}
|
214
249
|
end
|
215
250
|
|
251
|
+
# Process a format description event, which describes the version of this
|
252
|
+
# file, and the format of events which will appear in this file. This also
|
253
|
+
# provides the version of the MySQL server which generated this file.
|
216
254
|
def process_fde(fde)
|
217
255
|
if (version = fde[:binlog_version]) != 4
|
218
256
|
raise UnsupportedVersionException.new("Binlog version #{version} is not supported")
|
@@ -224,7 +262,8 @@ module MysqlBinlog
|
|
224
262
|
:server_version => fde[:server_version],
|
225
263
|
}
|
226
264
|
end
|
227
|
-
|
265
|
+
|
266
|
+
# Iterate through all events.
|
228
267
|
def each_event
|
229
268
|
while event = read_event
|
230
269
|
yield event
|
data/lib/mysql_binlog.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysql_binlog
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jeremy Cole
|