mysql_binlog 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|