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.
@@ -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
- end_position = reader.position + fields[:status_length]
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
- flags2 = parser.read_uint32
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
- def read_lpstring
87
- length = reader.read(1).unpack("C").first
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
- def read_lpstringz
92
- string = read_lpstring
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
- def read_nstring(length)
98
- string = reader.read(length)
99
- string
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
- def read_nstringz(length)
103
- string = reader.read(length)
104
- reader.read(1) # null
105
- string
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
- MAGIC = [
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
@@ -1,4 +1,5 @@
1
1
  require 'mysql_binlog/mysql_binlog'
2
+ require 'mysql_binlog/binlog_parser'
2
3
  require 'mysql_binlog/binlog_event_field_parser'
3
4
  require 'mysql_binlog/reader/binlog_file_reader'
4
5
  require 'mysql_binlog/reader/binlog_stream_reader'
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: 27
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
9
+ - 1
10
+ version: 0.1.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jeremy Cole