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.
@@ -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