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.
- data/bin/innodb_log +100 -34
- data/bin/innodb_space +288 -9
- data/lib/innodb.rb +10 -0
- data/lib/innodb/data_dictionary.rb +8 -8
- data/lib/innodb/data_type.rb +49 -0
- data/lib/innodb/field.rb +21 -11
- data/lib/innodb/history.rb +30 -0
- data/lib/innodb/history_list.rb +106 -0
- data/lib/innodb/index.rb +49 -57
- data/lib/innodb/inode.rb +11 -1
- data/lib/innodb/list.rb +45 -23
- data/lib/innodb/log.rb +22 -11
- data/lib/innodb/log_block.rb +52 -82
- data/lib/innodb/log_group.rb +59 -54
- data/lib/innodb/log_reader.rb +116 -0
- data/lib/innodb/log_record.rb +317 -0
- data/lib/innodb/lsn.rb +103 -0
- data/lib/innodb/page.rb +39 -5
- data/lib/innodb/page/blob.rb +26 -0
- data/lib/innodb/page/fsp_hdr_xdes.rb +38 -6
- data/lib/innodb/page/index.rb +176 -96
- data/lib/innodb/page/inode.rb +33 -1
- data/lib/innodb/page/sys_data_dictionary_header.rb +19 -0
- data/lib/innodb/page/sys_rseg_header.rb +41 -2
- data/lib/innodb/page/trx_sys.rb +69 -1
- data/lib/innodb/record.rb +37 -0
- data/lib/innodb/space.rb +28 -4
- data/lib/innodb/system.rb +4 -0
- data/lib/innodb/undo_log.rb +84 -24
- data/lib/innodb/undo_record.rb +259 -0
- data/lib/innodb/{cursor.rb → util/buffer_cursor.rb} +135 -29
- data/lib/innodb/util/read_bits_at_offset.rb +8 -0
- data/lib/innodb/version.rb +1 -1
- data/lib/innodb/xdes.rb +2 -0
- metadata +10 -3
@@ -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
|
6
|
-
|
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
|
-
@@
|
40
|
+
@@global_tracing = false
|
30
41
|
|
31
|
-
# Enable tracing for all
|
42
|
+
# Enable tracing for all BufferCursor objects globally.
|
32
43
|
def self.trace!(arg=true)
|
33
|
-
@@
|
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
|
73
|
-
@trace_proc.call(self, position, bytes, name) if
|
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
|
-
|
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 "
|
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
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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.
|