innodb_ruby 0.9.0 → 0.9.5

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.
@@ -0,0 +1,259 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # A single undo log record.
4
+ class Innodb::UndoRecord
5
+ attr_reader :undo_page
6
+ attr_reader :position
7
+
8
+ attr_accessor :undo_log
9
+ attr_accessor :index_page
10
+
11
+ def initialize(undo_page, position)
12
+ @undo_page = undo_page
13
+ @position = position
14
+
15
+ @undo_log = nil
16
+ @index_page = nil
17
+ end
18
+
19
+ def new_subordinate(undo_page, position)
20
+ new_undo_record = self.class.new(undo_page, position)
21
+ new_undo_record.undo_log = undo_log
22
+ new_undo_record.index_page = index_page
23
+
24
+ new_undo_record
25
+ end
26
+
27
+ # The header really starts 2 bytes before the undo record position, as the
28
+ # pointer to the previous record is written there.
29
+ def pos_header
30
+ @position - 2
31
+ end
32
+
33
+ # The size of the header.
34
+ def size_header
35
+ 2 + 2 + 1
36
+ end
37
+
38
+ def pos_record
39
+ pos_header + size_header
40
+ end
41
+
42
+ # Return a BufferCursor starting before the header.
43
+ def cursor(position)
44
+ new_cursor = @undo_page.cursor(position)
45
+ if @undo_log
46
+ new_cursor.push_name("undo_log[#{@undo_log.position}]")
47
+ end
48
+ new_cursor.push_name("undo_record[#{@position}]")
49
+ new_cursor
50
+ end
51
+
52
+ # Possible undo record types.
53
+ TYPE = {
54
+ 11 => :insert,
55
+ 12 => :update_existing,
56
+ 13 => :update_deleted,
57
+ 14 => :delete,
58
+ }
59
+
60
+ TYPE_MASK = 0x0f
61
+ COMPILATION_INFO_MASK = 0x70
62
+ COMPILATION_INFO_SHIFT = 4
63
+ COMPILATION_INFO_NO_ORDER_CHANGE_BV = 1
64
+ COMPILATION_INFO_NO_SIZE_CHANGE_BV = 2
65
+ EXTERN_FLAG = 0x80
66
+
67
+ def header
68
+ @header ||= cursor(pos_header).name("header") do |c|
69
+ header = {
70
+ :prev => c.name("prev") { c.get_uint16 },
71
+ :next => c.name("next") { c.get_uint16 },
72
+ }
73
+
74
+ info = c.name("info") { c.get_uint8 }
75
+ cmpl = (info & COMPILATION_INFO_MASK) >> COMPILATION_INFO_SHIFT
76
+ header[:type] = TYPE[info & TYPE_MASK]
77
+ header[:extern_flag] = (info & EXTERN_FLAG) != 0
78
+ header[:info] = {
79
+ :order_may_change => (cmpl & COMPILATION_INFO_NO_ORDER_CHANGE_BV) == 0,
80
+ :size_may_change => (cmpl & COMPILATION_INFO_NO_SIZE_CHANGE_BV) == 0,
81
+ }
82
+
83
+ header
84
+ end
85
+ end
86
+
87
+ def type
88
+ header[:type]
89
+ end
90
+
91
+ def has_previous_version?
92
+ [:update_existing, :update_deleted, :delete].include?(type)
93
+ end
94
+
95
+ def get(prev_or_next)
96
+ if header[prev_or_next] != 0
97
+ new_undo_record = new_subordinate(@undo_page, header[prev_or_next])
98
+ if new_undo_record.type
99
+ new_undo_record
100
+ end
101
+ end
102
+ end
103
+
104
+ def prev
105
+ get(:prev)
106
+ end
107
+
108
+ def next
109
+ get(:next)
110
+ end
111
+
112
+ def record_size
113
+ header[:next] - @position - size_header
114
+ end
115
+
116
+ def read_record
117
+ cursor(pos_record).name("record") do |c|
118
+ this_record = {
119
+ :page => undo_page.offset,
120
+ :offset => position,
121
+ :header => header,
122
+ :undo_no => c.name("undo_no") { c.get_imc_uint64 },
123
+ :table_id => c.name("table_id") { c.get_imc_uint64 },
124
+ }
125
+
126
+ if has_previous_version?
127
+ this_record[:info_bits] = c.name("info_bits") { c.get_uint8 }
128
+ this_record[:trx_id] = c.name("trx_id") { c.get_ic_uint64 }
129
+ this_record[:roll_ptr] = c.name("roll_ptr") {
130
+ Innodb::DataType::RollPointerType.parse_roll_pointer(c.get_ic_uint64)
131
+ }
132
+ end
133
+
134
+ if index_page
135
+ read_record_fields(this_record, c)
136
+ else
137
+ # Slurp up the remaining data as a string.
138
+ this_record[:data] = c.get_bytes(header[:next] - c.position - 2)
139
+ end
140
+
141
+ this_record
142
+ end
143
+ end
144
+
145
+ def read_record_fields(this_record, c)
146
+ this_record[:key] = []
147
+ index_page.record_format[:key].each do |field|
148
+ this_record[:key][field.position] = {
149
+ :name => field.name,
150
+ :type => field.data_type.name,
151
+ :value => c.name(field.name) {
152
+ field_length = c.name("field_length") { c.get_ic_uint32 }
153
+ field.value_by_length(c, field_length)
154
+ }
155
+ }
156
+ end
157
+
158
+ if has_previous_version?
159
+ field_count = c.name("field_count") { c.get_ic_uint32 }
160
+ this_record[:row] = Array.new(index_page.record_format[:row].size)
161
+ field_count.times do
162
+ field_number = c.name("field_number[#{field_count}]") { c.get_ic_uint32 }
163
+ field = nil
164
+ field_index = nil
165
+ index_page.record_format[:row].each_with_index do |candidate_field, index|
166
+ if candidate_field.position == field_number
167
+ field = candidate_field
168
+ field_index = index
169
+ end
170
+ end
171
+ raise "Unknown field #{field_number}" unless field
172
+ this_record[:row][field_index] = {
173
+ :name => field.name,
174
+ :type => field.data_type.name,
175
+ :value => c.name(field.name) {
176
+ field_length = c.name("field_length") { c.get_ic_uint32 }
177
+ field.value_by_length(c, field_length)
178
+ }
179
+ }
180
+ end
181
+ end
182
+ end
183
+
184
+ def undo_record
185
+ @undo_record ||= read_record
186
+ end
187
+
188
+ def undo_no
189
+ undo_record[:undo_no]
190
+ end
191
+
192
+ def table_id
193
+ undo_record[:table_id]
194
+ end
195
+
196
+ def trx_id
197
+ undo_record[:trx_id]
198
+ end
199
+
200
+ def roll_ptr
201
+ undo_record[:roll_ptr]
202
+ end
203
+
204
+ def key
205
+ undo_record[:key]
206
+ end
207
+
208
+ def key_string
209
+ key && key.map { |r| "%s=%s" % [r[:name], r[:value].inspect] }.join(", ")
210
+ end
211
+
212
+ def row
213
+ undo_record[:row]
214
+ end
215
+
216
+ def row_string
217
+ row && row.select { |r| !r.nil? }.map { |r| r && "%s=%s" % [r[:name], r[:value].inspect] }.join(", ")
218
+ end
219
+
220
+ def string
221
+ "(%s) → (%s)" % [key_string, row_string]
222
+ end
223
+
224
+ # Find the previous row version by following the roll_ptr from one undo
225
+ # record to the next (backwards through the record version history). Since
226
+ # we are operating without the benefit of knowing about active transactions
227
+ # and without protection from purge, check that everything looks sane before
228
+ # returning it.
229
+ def prev_by_history
230
+ unless has_previous_version?
231
+ # This undo record type has no previous version information.
232
+ return nil
233
+ end
234
+
235
+ undo_log = roll_ptr[:undo_log]
236
+ older_undo_page = @undo_page.space.page(undo_log[:page])
237
+
238
+ unless older_undo_page and older_undo_page.is_a?(Innodb::Page::UndoLog)
239
+ # The page was probably re-used for something else.
240
+ return nil
241
+ end
242
+
243
+ older_undo_record = new_subordinate(older_undo_page,
244
+ undo_log[:offset])
245
+
246
+ unless older_undo_record and table_id == older_undo_record.table_id
247
+ # The record space was probably re-used for something else.
248
+ return nil
249
+ end
250
+
251
+ unless older_undo_record.trx_id.nil? or trx_id >= older_undo_record.trx_id
252
+ # The trx_id should not be newer; but may be absent (for insert).
253
+ return nil
254
+ end
255
+
256
+ older_undo_record
257
+ end
258
+
259
+ end
@@ -2,8 +2,11 @@
2
2
 
3
3
  require "bindata"
4
4
 
5
- # A cursor to walk through InnoDB data structures to read fields.
6
- class Innodb::Cursor
5
+ # A cursor to walk through data structures to read fields. The cursor can move
6
+ # forwards, backwards, is seekable, and supports peeking without moving the
7
+ # cursor. The BinData module is used for interpreting bytes as desired.
8
+ class BufferCursor
9
+ VERSION = "0.9.0"
7
10
 
8
11
  # An entry in a stack of cursors. The cursor position, direction, and
9
12
  # name array are each attributes of the current cursor stack and are
@@ -21,16 +24,24 @@ class Innodb::Cursor
21
24
  @name = name || []
22
25
  end
23
26
 
27
+ def inspect
28
+ "<%s direction=%s position=%s>" % [
29
+ self.class.name,
30
+ @direction.inspect,
31
+ @position,
32
+ ]
33
+ end
34
+
24
35
  def dup
25
36
  StackEntry.new(cursor, position, direction, name.dup)
26
37
  end
27
38
  end
28
39
 
29
- @@tracing = false
40
+ @@global_tracing = false
30
41
 
31
- # Enable tracing for all Innodb::Cursor objects.
42
+ # Enable tracing for all BufferCursor objects globally.
32
43
  def self.trace!(arg=true)
33
- @@tracing = arg
44
+ @@global_tracing = arg
34
45
  end
35
46
 
36
47
  # Initialize a cursor within a buffer at the given position.
@@ -38,7 +49,22 @@ class Innodb::Cursor
38
49
  @buffer = buffer
39
50
  @stack = [ StackEntry.new(self, position) ]
40
51
 
52
+ trace false
41
53
  trace_with :print_trace
54
+ trace_to STDOUT
55
+ end
56
+
57
+ def inspect
58
+ "<%s size=%i current=%s>" % [
59
+ self.class.name,
60
+ @buffer.size,
61
+ current.inspect,
62
+ ]
63
+ end
64
+
65
+ def trace(arg=true)
66
+ @instance_tracing = arg
67
+ self
42
68
  end
43
69
 
44
70
  # Print a trace output for this cursor. The method is passed a cursor object,
@@ -46,7 +72,7 @@ class Innodb::Cursor
46
72
  def print_trace(cursor, position, bytes, name)
47
73
  slice_size = 16
48
74
  bytes.each_slice(slice_size).each_with_index do |slice_bytes, slice_count|
49
- puts "%06i %s %-32s %s" % [
75
+ @trace_io.puts "%06i %s %-32s %s" % [
50
76
  position + (slice_count * slice_size),
51
77
  direction == :backward ? "←" : "→",
52
78
  slice_bytes.map { |n| "%02x" % n }.join,
@@ -55,6 +81,11 @@ class Innodb::Cursor
55
81
  end
56
82
  end
57
83
 
84
+ def trace_to(file)
85
+ @trace_io = file
86
+ self
87
+ end
88
+
58
89
  # Set a Proc or method on self to trace with.
59
90
  def trace_with(arg=nil)
60
91
  if arg.nil?
@@ -66,11 +97,16 @@ class Innodb::Cursor
66
97
  else
67
98
  raise "Don't know how to trace with #{arg}"
68
99
  end
100
+ self
101
+ end
102
+
103
+ def tracing_enabled?
104
+ (@@global_tracing or @instance_tracing) && @trace_proc
69
105
  end
70
106
 
71
107
  # Generate a trace record from the current cursor.
72
- def trace(position, bytes, name)
73
- @trace_proc.call(self, position, bytes, name) if @@tracing && @trace_proc
108
+ def record_trace(position, bytes, name)
109
+ @trace_proc.call(self, position, bytes, name) if tracing_enabled?
74
110
  end
75
111
 
76
112
  # The current cursor object; the top of the stack.
@@ -78,6 +114,14 @@ class Innodb::Cursor
78
114
  @stack.last
79
115
  end
80
116
 
117
+ def push_name(name_arg)
118
+ current.name.push name_arg
119
+ end
120
+
121
+ def pop_name
122
+ current.name.pop
123
+ end
124
+
81
125
  # Set the field name.
82
126
  def name(name_arg=nil)
83
127
  if name_arg.nil?
@@ -170,7 +214,7 @@ class Innodb::Cursor
170
214
  data = @buffer.slice(current.position, length)
171
215
  end
172
216
 
173
- trace(cursor_start, data.bytes, current.name)
217
+ record_trace(cursor_start, data.bytes, current.name)
174
218
  data
175
219
  end
176
220
 
@@ -179,6 +223,12 @@ class Innodb::Cursor
179
223
  read_and_advance(length)
180
224
  end
181
225
 
226
+ # Return a null-terminated string.
227
+ def get_string(length)
228
+ BinData::Stringz.read(read_and_advance(length))
229
+ end
230
+
231
+ # Iterate through length bytes returning each as an unsigned 8-bit integer.
182
232
  def each_byte_as_uint8(length)
183
233
  unless block_given?
184
234
  return enum_for(:each_byte_as_uint8, length)
@@ -261,33 +311,89 @@ class Innodb::Cursor
261
311
  when 8
262
312
  get_uint64
263
313
  else
264
- raise "Not implemented"
314
+ raise "Integer size #{size} not implemented"
265
315
  end
266
316
  end
267
317
 
318
+ # Read an array of count unsigned integers given their size in bytes.
268
319
  def get_uint_array_by_size(size, count)
269
320
  (0...count).to_a.inject([]) { |a, n| a << get_uint_by_size(size); a }
270
321
  end
271
322
 
272
- # Read an InnoDB-compressed unsigned 32-bit integer.
273
- def get_ic_uint32
274
- flag = peek { name("ic_uint32") { get_uint8 } }
275
-
276
- case
277
- when flag < 0x80
278
- name("uint8") { get_uint8 }
279
- when flag < 0xc0
280
- name("uint16") { get_uint16 } & 0x7fff
281
- when flag < 0xe0
282
- name("uint24") { get_uint24 } & 0x3fffff
283
- when flag < 0xf0
284
- name("uint32") { get_uint32 } & 0x1fffffff
285
- when flag == 0xf0
286
- adjust(+1) # Skip the flag.
287
- name("uint32+1") { get_uint32 }
288
- else
289
- raise "Invalid flag #{flag.to_s} seen"
290
- end
323
+ # Read an InnoDB-compressed unsigned 32-bit integer (1-5 bytes).
324
+ #
325
+ # The first byte makes up part of the value stored as well as indicating
326
+ # the number of bytes stored, maximally an additional 4 bytes after the
327
+ # flag for integers >= 0xf0000000.
328
+ #
329
+ # Optionally accept a flag (first byte) if it has already been read (as is
330
+ # the case in get_imc_uint64).
331
+ def get_ic_uint32(flag=nil)
332
+ name("ic_uint32") {
333
+ if !flag
334
+ flag = peek { name("uint8_or_flag") { get_uint8 } }
335
+ end
336
+
337
+ case
338
+ when flag < 0x80
339
+ adjust(+1)
340
+ flag
341
+ when flag < 0xc0
342
+ name("uint16") { get_uint16 } & 0x7fff
343
+ when flag < 0xe0
344
+ name("uint24") { get_uint24 } & 0x3fffff
345
+ when flag < 0xf0
346
+ name("uint32") { get_uint32 } & 0x1fffffff
347
+ when flag == 0xf0
348
+ adjust(+1) # Skip the flag byte.
349
+ name("uint32+1") { get_uint32 }
350
+ else
351
+ raise "Invalid flag #{flag.to_s} seen"
352
+ end
353
+ }
354
+ end
355
+
356
+ # Read an InnoDB-compressed unsigned 64-bit integer (5-9 bytes).
357
+ #
358
+ # The high 32 bits are stored as an InnoDB-compressed unsigned 32-bit
359
+ # integer (1-5 bytes) while the low 32 bits are stored as a standard
360
+ # big-endian 32-bit integer (4 bytes). This makes a combined size of
361
+ # between 5 and 9 bytes.
362
+ def get_ic_uint64
363
+ name("ic_uint64") {
364
+ high = name("high") { get_ic_uint32 }
365
+ low = name("low") { name("uint32") { get_uint32 } }
366
+
367
+ (high << 32) | low
368
+ }
369
+ end
370
+
371
+ # Read an InnoDB-"much compressed" unsigned 64-bit integer (1-11 bytes).
372
+ #
373
+ # If the first byte is 0xff, this indicates that the high 32 bits are
374
+ # stored immediately afterwards as an InnoDB-compressed 32-bit unsigned
375
+ # integer. If it is any other value it represents the first byte (which
376
+ # is also a flag) of the low 32 bits of the value, also as an InnoDB-
377
+ # compressed 32-bit unsigned integer. This makes for a combined size
378
+ # of between 1 and 11 bytes.
379
+ def get_imc_uint64
380
+ name("imc_uint64") {
381
+ high = 0
382
+ flag = peek { name("uint8_or_flag") { get_uint8 } }
383
+
384
+ if flag == 0xff
385
+ # The high 32-bits are stored first as an ic_uint32.
386
+ adjust(+1) # Skip the flag byte.
387
+ high = name("high") { get_ic_uint32 }
388
+ flag = nil
389
+ end
390
+
391
+ # The low 32-bits are stored as an ic_uint32; pass the flag we already
392
+ # read, so we don't have to read it again.
393
+ low = name("low") { get_ic_uint32(flag) }
394
+
395
+ (high << 32) | low
396
+ }
291
397
  end
292
398
 
293
399
  # Read an array of 1-bit integers.