innodb_ruby 0.7.11 → 0.7.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  # An abstract InnoDB "free list" or FLST (renamed to just "list" here as it
2
3
  # frequently is used for structures that aren't free lists). This class must
3
4
  # be sub-classed to provide an appropriate #object_from_address method.
@@ -11,8 +12,10 @@ class Innodb::List
11
12
  # or "NULL" pointer (the page number is UINT32_MAX), or the address if
12
13
  # valid.
13
14
  def self.get_address(cursor)
14
- page = Innodb::Page.maybe_undefined(cursor.get_uint32)
15
- offset = cursor.get_uint16
15
+ page = cursor.name("page") {
16
+ Innodb::Page.maybe_undefined(cursor.get_uint32)
17
+ }
18
+ offset = cursor.name("offset") { cursor.get_uint16 }
16
19
  if page
17
20
  {
18
21
  :page => page,
@@ -30,8 +33,8 @@ class Innodb::List
30
33
  # linked list.
31
34
  def self.get_node(cursor)
32
35
  {
33
- :prev => get_address(cursor),
34
- :next => get_address(cursor),
36
+ :prev => cursor.name("prev") { get_address(cursor) },
37
+ :next => cursor.name("next") { get_address(cursor) },
35
38
  }
36
39
  end
37
40
 
@@ -46,9 +49,9 @@ class Innodb::List
46
49
  # address.
47
50
  def self.get_base_node(cursor)
48
51
  {
49
- :length => cursor.get_uint32,
50
- :first => get_address(cursor),
51
- :last => get_address(cursor),
52
+ :length => cursor.name("length") { cursor.get_uint32 },
53
+ :first => cursor.name("first") { get_address(cursor) },
54
+ :last => cursor.name("last") { get_address(cursor) },
52
55
  }
53
56
  end
54
57
 
@@ -86,6 +89,11 @@ class Innodb::List
86
89
  object_from_address(object.next_address)
87
90
  end
88
91
 
92
+ # Return the number of items in the list.
93
+ def length
94
+ @base[:length]
95
+ end
96
+
89
97
  # Return the first object in the list using the list base node "first"
90
98
  # address pointer.
91
99
  def first
@@ -99,8 +107,16 @@ class Innodb::List
99
107
  end
100
108
 
101
109
  # Return a list cursor for the list.
102
- def cursor(node=nil)
103
- Cursor.new(self, node)
110
+ def list_cursor(node=nil)
111
+ ListCursor.new(self, node)
112
+ end
113
+
114
+ # Return whether the given item is present in the list. This depends on the
115
+ # item and the items in the list implementing some sufficient == method.
116
+ # This is implemented rather inefficiently by constructing an array and
117
+ # leaning on Array#include? to do the real work.
118
+ def include?(item)
119
+ each.to_a.include? item
104
120
  end
105
121
 
106
122
  # Iterate through all nodes in the list.
@@ -109,7 +125,7 @@ class Innodb::List
109
125
  return enum_for(:each)
110
126
  end
111
127
 
112
- c = cursor
128
+ c = list_cursor
113
129
  while e = c.next
114
130
  yield e
115
131
  end
@@ -117,7 +133,7 @@ class Innodb::List
117
133
 
118
134
  # A list iteration cursor used primarily by the Innodb::List #cursor method
119
135
  # implicitly. Keeps its own state for iterating through lists efficiently.
120
- class Cursor
136
+ class ListCursor
121
137
  def initialize(list, node=nil)
122
138
  @list = list
123
139
  @cursor = node
@@ -174,4 +190,20 @@ class Innodb::List::Inode < Innodb::List
174
190
  page
175
191
  end
176
192
  end
177
- end
193
+ end
194
+
195
+ class Innodb::List::UndoPage < Innodb::List
196
+ def object_from_address(address)
197
+ if address && page = @space.page(address[:page])
198
+ page
199
+ end
200
+ end
201
+ end
202
+
203
+ class Innodb::List::History < Innodb::List
204
+ def object_from_address(address)
205
+ if address && page = @space.page(address[:page])
206
+ Innodb::UndoLog.new(page, address[:offset] - 34)
207
+ end
208
+ end
209
+ end
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  # An InnoDB transaction log file.
2
3
  class Innodb::Log
3
4
  HEADER_SIZE = 4 * Innodb::LogBlock::BLOCK_SIZE
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  require "innodb/cursor"
2
3
  require "pp"
3
4
 
@@ -179,4 +180,4 @@ class Innodb::LogBlock
179
180
  puts "record:"
180
181
  pp record
181
182
  end
182
- end
183
+ end
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  require "innodb/cursor"
2
3
 
3
4
  # A generic class for any type of page, which handles reading the common
@@ -26,15 +27,24 @@ class Innodb::Page
26
27
  # If there is a specialized class available for this page type, re-create
27
28
  # the page object using that specialized class.
28
29
  if specialized_class = SPECIALIZED_CLASSES[page.type]
29
- page = specialized_class.new(space, buffer)
30
+ page = specialized_class.handle(page, space, buffer)
30
31
  end
31
32
 
32
33
  page
33
34
  end
34
35
 
36
+ # Allow the specialized class to do something that isn't 'new' with this page.
37
+ def self.handle(page, space, buffer)
38
+ self.new(space, buffer)
39
+ end
40
+
35
41
  # Initialize a page by passing in a buffer containing the raw page contents.
36
42
  # The buffer size should match the space's page size.
37
43
  def initialize(space, buffer)
44
+ unless space && buffer
45
+ raise "Page can't be initialized from nil space or buffer (space: #{space}, buffer: #{buffer})"
46
+ end
47
+
38
48
  unless space.page_size == buffer.size
39
49
  raise "Buffer size #{buffer.size} is different than space page size"
40
50
  end
@@ -56,9 +66,20 @@ class Innodb::Page
56
66
  @buffer[offset...(offset + length)]
57
67
  end
58
68
 
59
- # Return an Innodb::Cursor object positioned at a specific offset.
69
+ # If no block is passed, return an Innodb::Cursor object positioned at a
70
+ # specific offset. If a block is passed, create a cursor at the provided
71
+ # offset and yield it to the provided block one time, and then return the
72
+ # return value of the block.
60
73
  def cursor(offset)
61
- Innodb::Cursor.new(self, offset)
74
+ new_cursor = Innodb::Cursor.new(self, offset)
75
+
76
+ if block_given?
77
+ # Call the block once and return its return value.
78
+ yield new_cursor
79
+ else
80
+ # Return the cursor itself.
81
+ new_cursor
82
+ end
62
83
  end
63
84
 
64
85
  # Return the byte offset of the start of the "fil" header, which is at the
@@ -83,6 +104,12 @@ class Innodb::Page
83
104
  4 + 4
84
105
  end
85
106
 
107
+ # Return the position of the "body" of the page, which starts after the FIL
108
+ # header.
109
+ def pos_page_body
110
+ pos_fil_header + size_fil_header
111
+ end
112
+
86
113
  # InnoDB Page Type constants from include/fil0fil.h.
87
114
  PAGE_TYPE = {
88
115
  0 => :ALLOCATED, # Freshly allocated page
@@ -108,23 +135,28 @@ class Innodb::Page
108
135
 
109
136
  # Return the "fil" header from the page, which is common for all page types.
110
137
  def fil_header
111
- c = cursor(pos_fil_header)
112
- @fil_header ||= {
113
- :checksum => c.get_uint32,
114
- :offset => c.get_uint32,
115
- :prev => Innodb::Page.maybe_undefined(c.get_uint32),
116
- :next => Innodb::Page.maybe_undefined(c.get_uint32),
117
- :lsn => c.get_uint64,
118
- :type => PAGE_TYPE[c.get_uint16],
119
- :flush_lsn => c.get_uint64,
120
- :space_id => c.get_uint32,
121
- }
138
+ @fil_header ||= cursor(pos_fil_header).name("fil") do |c|
139
+ {
140
+ :checksum => c.name("checksum") { c.get_uint32 },
141
+ :offset => c.name("offset") { c.get_uint32 },
142
+ :prev => c.name("prev") {
143
+ Innodb::Page.maybe_undefined(c.get_uint32)
144
+ },
145
+ :next => c.name("next") {
146
+ Innodb::Page.maybe_undefined(c.get_uint32)
147
+ },
148
+ :lsn => c.name("lsn") { c.get_uint64 },
149
+ :type => c.name("type") { PAGE_TYPE[c.get_uint16] },
150
+ :flush_lsn => c.name("flush_lsn") { c.get_uint64 },
151
+ :space_id => c.name("space_id") { c.get_uint32 },
152
+ }
153
+ end
122
154
  end
123
155
 
124
- # A helper function to return the page type from the "fil" header, for easier
156
+ # A helper function to return the checksum from the "fil" header, for easier
125
157
  # access.
126
- def type
127
- fil_header[:type]
158
+ def checksum
159
+ fil_header[:checksum]
128
160
  end
129
161
 
130
162
  # A helper function to return the page offset from the "fil" header, for
@@ -152,6 +184,50 @@ class Innodb::Page
152
184
  fil_header[:lsn]
153
185
  end
154
186
 
187
+ # A helper function to return the page type from the "fil" header, for easier
188
+ # access.
189
+ def type
190
+ fil_header[:type]
191
+ end
192
+
193
+ # Calculate the checksum of the page using InnoDB's algorithm. Two sections
194
+ # of the page are checksummed separately, and then added together to produce
195
+ # the final checksum.
196
+ def calculate_checksum
197
+ unless size == 16384
198
+ raise "Checksum calculation is only supported for 16 KiB pages"
199
+ end
200
+
201
+ # Calculate the checksum of the FIL header, except for the following:
202
+ # :checksum (offset 4, size 4)
203
+ # :flush_lsn (offset 26, size 8)
204
+ # :space_id (offset 34, size 4)
205
+ c_partial_header =
206
+ Innodb::Checksum.fold_enumerator(
207
+ cursor(pos_fil_header + 4).each_byte_as_uint8(
208
+ size_fil_header - 4 - 8 - 4
209
+ )
210
+ )
211
+
212
+ # Calculate the checksum of the page body, except for the FIL header and
213
+ # the FIL trailer.
214
+ c_page_body =
215
+ Innodb::Checksum.fold_enumerator(
216
+ cursor(pos_page_body).each_byte_as_uint8(
217
+ size - size_fil_trailer - size_fil_header
218
+ )
219
+ )
220
+
221
+ # Add the two checksums together, and mask the result back to 32 bits.
222
+ (c_partial_header + c_page_body) & Innodb::Checksum::MAX
223
+ end
224
+
225
+ # Is the page corrupt? Calculate the checksum of the page and compare to
226
+ # the stored checksum; return true or false.
227
+ def corrupt?
228
+ checksum != calculate_checksum
229
+ end
230
+
155
231
  # Implement a custom inspect method to avoid irb printing the contents of
156
232
  # the page buffer, since it's very large and mostly not interesting.
157
233
  def inspect
@@ -0,0 +1,60 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ class Innodb::Page::Blob < Innodb::Page
4
+ def pos_blob_header
5
+ pos_fil_header + size_fil_header
6
+ end
7
+
8
+ def size_blob_header
9
+ 4 + 4
10
+ end
11
+
12
+ def pos_blob_data
13
+ pos_blob_header + size_blob_header
14
+ end
15
+
16
+ def blob_header
17
+ cursor(pos_blob_header).name("blob_header") do |c|
18
+ {
19
+ :length => c.name("length") { c.get_uint32 },
20
+ :next => c.name("next") { Innodb::Page.maybe_undefined(c.get_uint32) },
21
+ }
22
+ end
23
+ end
24
+
25
+ def blob_data
26
+ cursor(pos_blob_data).name("blob_data") do |c|
27
+ c.get_bytes(blob_header[:length])
28
+ end
29
+ end
30
+
31
+ def dump_hex(string)
32
+ slice_size = 16
33
+ bytes = string.split("").map { |s| s.ord }
34
+ string.split("").each_slice(slice_size).each_with_index do |slice_bytes, slice_count|
35
+ puts "%08i %-23s %-23s |%-16s|" % [
36
+ (slice_count * slice_size),
37
+ slice_bytes[0..8].map { |n| "%02x" % n.ord }.join(" "),
38
+ slice_bytes[8..16].map { |n| "%02x" % n.ord }.join(" "),
39
+ slice_bytes.join(""),
40
+ ]
41
+ end
42
+ end
43
+
44
+ # Dump the contents of a page for debugging purposes.
45
+ def dump
46
+ super
47
+
48
+ puts "blob header:"
49
+ pp blob_header
50
+ puts
51
+
52
+ puts "blob data:"
53
+ dump_hex(blob_data)
54
+ puts
55
+
56
+ puts
57
+ end
58
+ end
59
+
60
+ Innodb::Page::SPECIALIZED_CLASSES[:BLOB] = Innodb::Page::Blob
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  require "innodb/list"
2
3
  require "innodb/xdes"
3
4
 
@@ -75,26 +76,34 @@ class Innodb::Page::FspHdrXdes < Innodb::Page
75
76
  # Read the FSP (filespace) header, which contains a few counters and flags,
76
77
  # as well as list base nodes for each list maintained in the filespace.
77
78
  def fsp_header
78
- c = cursor(pos_fsp_header)
79
- @fsp_header ||= {
80
- :space_id => c.get_uint32,
81
- :unused => c.get_uint32,
82
- :size => c.get_uint32,
83
- :free_limit => c.get_uint32,
84
- :flags => self.class.decode_flags(c.get_uint32),
85
- :frag_n_used => c.get_uint32,
86
- :free => Innodb::List::Xdes.new(@space,
87
- Innodb::List.get_base_node(c)),
88
- :free_frag => Innodb::List::Xdes.new(@space,
89
- Innodb::List.get_base_node(c)),
90
- :full_frag => Innodb::List::Xdes.new(@space,
91
- Innodb::List.get_base_node(c)),
92
- :first_unused_seg => c.get_uint64,
93
- :full_inodes => Innodb::List::Inode.new(@space,
94
- Innodb::List.get_base_node(c)),
95
- :free_inodes => Innodb::List::Inode.new(@space,
96
- Innodb::List.get_base_node(c)),
97
- }
79
+ @fsp_header ||= cursor(pos_fsp_header).name("fsp") do |c|
80
+ {
81
+ :space_id => c.name("space_id") { c.get_uint32 },
82
+ :unused => c.name("unused") { c.get_uint32 },
83
+ :size => c.name("size") { c.get_uint32 },
84
+ :free_limit => c.name("free_limit") { c.get_uint32 },
85
+ :flags => c.name("flags") {
86
+ self.class.decode_flags(c.get_uint32)
87
+ },
88
+ :frag_n_used => c.name("frag_n_used") { c.get_uint32 },
89
+ :free => c.name("list[free]") {
90
+ Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c))
91
+ },
92
+ :free_frag => c.name("list[free_frag]") {
93
+ Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c))
94
+ },
95
+ :full_frag => c.name("list[full_frag]") {
96
+ Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c))
97
+ },
98
+ :first_unused_seg => c.name("first_unused_seg") { c.get_uint64 },
99
+ :full_inodes => c.name("list[full_inodes]") {
100
+ Innodb::List::Inode.new(@space, Innodb::List.get_base_node(c))
101
+ },
102
+ :free_inodes => c.name("list[free_inodes]") {
103
+ Innodb::List::Inode.new(@space, Innodb::List.get_base_node(c))
104
+ },
105
+ }
106
+ end
98
107
  end
99
108
 
100
109
  # Iterate through all lists in the file space.
@@ -117,9 +126,10 @@ class Innodb::Page::FspHdrXdes < Innodb::Page
117
126
  return enum_for(:each_xdes)
118
127
  end
119
128
 
120
- c = cursor(pos_xdes_array)
121
- entries_in_xdes_array.times do
122
- yield Innodb::Xdes.new(self, c)
129
+ cursor(pos_xdes_array).name("xdes_array") do |c|
130
+ entries_in_xdes_array.times do |n|
131
+ yield Innodb::Xdes.new(self, c)
132
+ end
123
133
  end
124
134
  end
125
135
 
@@ -140,4 +150,4 @@ class Innodb::Page::FspHdrXdes < Innodb::Page
140
150
  end
141
151
 
142
152
  Innodb::Page::SPECIALIZED_CLASSES[:FSP_HDR] = Innodb::Page::FspHdrXdes
143
- Innodb::Page::SPECIALIZED_CLASSES[:XDES] = Innodb::Page::FspHdrXdes
153
+ Innodb::Page::SPECIALIZED_CLASSES[:XDES] = Innodb::Page::FspHdrXdes
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  require "innodb/fseg_entry"
2
3
 
3
4
  # A specialized class for handling INDEX pages, which contain a portion of
@@ -11,6 +12,60 @@ require "innodb/fseg_entry"
11
12
  class Innodb::Page::Index < Innodb::Page
12
13
  attr_accessor :record_describer
13
14
 
15
+ # The size (in bytes) of the "next" pointer in each record header.
16
+ RECORD_NEXT_SIZE = 2
17
+
18
+ # The size (in bytes) of the bit-packed fields in each record header for
19
+ # "redundant" record format.
20
+ RECORD_REDUNDANT_BITS_SIZE = 4
21
+
22
+ # Masks for 1-byte record end-offsets within "redundant" records.
23
+ RECORD_REDUNDANT_OFF1_OFFSET_MASK = 0x7f
24
+ RECORD_REDUNDANT_OFF1_NULL_MASK = 0x80
25
+
26
+ # Masks for 2-byte record end-offsets within "redundant" records.
27
+ RECORD_REDUNDANT_OFF2_OFFSET_MASK = 0x3fff
28
+ RECORD_REDUNDANT_OFF2_NULL_MASK = 0x8000
29
+ RECORD_REDUNDANT_OFF2_EXTERN_MASK = 0x4000
30
+
31
+ # The size (in bytes) of the bit-packed fields in each record header for
32
+ # "compact" record format.
33
+ RECORD_COMPACT_BITS_SIZE = 3
34
+
35
+ # Page direction values possible in the page_header's :direction field.
36
+ PAGE_DIRECTION = {
37
+ 1 => :left, # Inserts have been in descending order.
38
+ 2 => :right, # Inserts have been in ascending order.
39
+ 3 => :same_rec, # Unused by InnoDB.
40
+ 4 => :same_page, # Unused by InnoDB.
41
+ 5 => :no_direction, # Inserts have been in random order.
42
+ }
43
+
44
+ # Record types used in the :type field of the record header.
45
+ RECORD_TYPES = {
46
+ 0 => :conventional, # A normal user record in a leaf page.
47
+ 1 => :node_pointer, # A node pointer in a non-leaf page.
48
+ 2 => :infimum, # The system "infimum" record.
49
+ 3 => :supremum, # The system "supremum" record.
50
+ }
51
+
52
+ # This record is the minimum record at this level of the B-tree.
53
+ RECORD_INFO_MIN_REC_FLAG = 1
54
+
55
+ # This record has been marked as deleted.
56
+ RECORD_INFO_DELETED_FLAG = 2
57
+
58
+ # The size (in bytes) of the record pointers in each page directory slot.
59
+ PAGE_DIR_SLOT_SIZE = 2
60
+
61
+ # The minimum number of records "owned" by each record with an entry in
62
+ # the page directory.
63
+ PAGE_DIR_SLOT_MIN_N_OWNED = 4
64
+
65
+ # The maximum number of records "owned" by each record with an entry in
66
+ # the page directory.
67
+ PAGE_DIR_SLOT_MAX_N_OWNED = 8
68
+
14
69
  # Return the byte offset of the start of the "index" page header, which
15
70
  # immediately follows the "fil" header.
16
71
  def pos_index_header
@@ -19,7 +74,7 @@ class Innodb::Page::Index < Innodb::Page
19
74
 
20
75
  # The size of the "index" header.
21
76
  def size_index_header
22
- 36
77
+ 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 8 + 2 + 8
23
78
  end
24
79
 
25
80
  # Return the byte offset of the start of the "fseg" header, which immediately
@@ -33,10 +88,27 @@ class Innodb::Page::Index < Innodb::Page
33
88
  2 * Innodb::FsegEntry::SIZE
34
89
  end
35
90
 
36
- # Return the byte offset of the start of records within the page (the
37
- # position immediately after the page header).
38
- def pos_records
39
- size_fil_header + size_index_header + size_fseg_header
91
+ # Return the size of the header for each record.
92
+ def size_record_header
93
+ case page_header[:format]
94
+ when :compact
95
+ RECORD_NEXT_SIZE + RECORD_COMPACT_BITS_SIZE
96
+ when :redundant
97
+ RECORD_NEXT_SIZE + RECORD_REDUNDANT_BITS_SIZE
98
+ end
99
+ end
100
+
101
+ # The size of the additional data structures in the header of the system
102
+ # records, which is just 1 byte in redundant format to store the offset
103
+ # of the end of the field. This is needed specifically here since we need
104
+ # to be able to calculate the fixed positions of these system records.
105
+ def size_mum_record_header_additional
106
+ case page_header[:format]
107
+ when :compact
108
+ 0 # No additional data is stored in compact format.
109
+ when :redundant
110
+ 1 # A 1-byte offset for 1 field is stored in redundant format.
111
+ end
40
112
  end
41
113
 
42
114
  # The size of the data from the supremum or infimum records.
@@ -49,7 +121,9 @@ class Innodb::Page::Index < Innodb::Page
49
121
  # page, and represents a record with a "lower value than any possible user
50
122
  # record". The infimum record immediately follows the page header.
51
123
  def pos_infimum
52
- pos_records + size_record_header + size_record_undefined
124
+ pos_records +
125
+ size_record_header +
126
+ size_mum_record_header_additional
53
127
  end
54
128
 
55
129
  # Return the byte offset of the start of the "origin" of the supremum record,
@@ -57,7 +131,18 @@ class Innodb::Page::Index < Innodb::Page
57
131
  # page, and represents a record with a "higher value than any possible user
58
132
  # record". The supremum record immediately follows the infimum record.
59
133
  def pos_supremum
60
- pos_infimum + size_record_header + size_record_undefined + size_mum_record
134
+ pos_infimum +
135
+ size_record_header +
136
+ size_mum_record_header_additional +
137
+ size_mum_record
138
+ end
139
+
140
+ # Return the byte offset of the start of records within the page (the
141
+ # position immediately after the page header).
142
+ def pos_records
143
+ size_fil_header +
144
+ size_index_header +
145
+ size_fseg_header
61
146
  end
62
147
 
63
148
  # Return the byte offset of the start of the user records in a page, which
@@ -96,7 +181,7 @@ class Innodb::Page::Index < Innodb::Page
96
181
 
97
182
  # Return the amount of free space in the page.
98
183
  def free_space
99
- page_header[:garbage] +
184
+ page_header[:garbage_size] +
100
185
  (size - size_fil_trailer - directory_space - page_header[:heap_top])
101
186
  end
102
187
 
@@ -116,33 +201,30 @@ class Innodb::Page::Index < Innodb::Page
116
201
  data(pos_user_records, page_header[:heap_top] - pos_user_records)
117
202
  end
118
203
 
119
- # Page direction values possible in the page_header's :direction field.
120
- PAGE_DIRECTION = {
121
- 1 => :left, # Inserts have been in descending order.
122
- 2 => :right, # Inserts have been in ascending order.
123
- 3 => :same_rec, # Unused by InnoDB.
124
- 4 => :same_page, # Unused by InnoDB.
125
- 5 => :no_direction, # Inserts have been in random order.
126
- }
127
-
128
204
  # Return the "index" header.
129
205
  def page_header
130
- c = cursor(pos_index_header)
131
- @page_header ||= {
132
- :n_dir_slots => c.get_uint16,
133
- :heap_top => c.get_uint16,
134
- :n_heap => ((n_heap = c.get_uint16) & (2**15-1)),
135
- :free => c.get_uint16,
136
- :garbage => c.get_uint16,
137
- :last_insert => c.get_uint16,
138
- :direction => PAGE_DIRECTION[c.get_uint16],
139
- :n_direction => c.get_uint16,
140
- :n_recs => c.get_uint16,
141
- :max_trx_id => c.get_uint64,
142
- :level => c.get_uint16,
143
- :index_id => c.get_uint64,
144
- :format => (n_heap & 1<<15) == 0 ? :redundant : :compact,
145
- }
206
+ @page_header ||= cursor(pos_index_header).name("index") do |c|
207
+ index = {
208
+ :n_dir_slots => c.name("n_dir_slots") { c.get_uint16 },
209
+ :heap_top => c.name("heap_top") { c.get_uint16 },
210
+ :n_heap_format => c.name("n_heap_format") { c.get_uint16 },
211
+ :garbage_offset => c.name("garbage_offset") { c.get_uint16 },
212
+ :garbage_size => c.name("garbage_size") { c.get_uint16 },
213
+ :last_insert_offset => c.name("last_insert_offset") { c.get_uint16 },
214
+ :direction => c.name("direction") { PAGE_DIRECTION[c.get_uint16] },
215
+ :n_direction => c.name("n_direction") { c.get_uint16 },
216
+ :n_recs => c.name("n_recs") { c.get_uint16 },
217
+ :max_trx_id => c.name("max_trx_id") { c.get_uint64 },
218
+ :level => c.name("level") { c.get_uint16 },
219
+ :index_id => c.name("index_id") { c.get_uint64 },
220
+ }
221
+ index[:n_heap] = index[:n_heap_format] & (2**15-1)
222
+ index[:format] = (index[:n_heap_format] & 1<<15) == 0 ?
223
+ :redundant : :compact
224
+ index.delete :n_heap_format
225
+
226
+ index
227
+ end
146
228
  end
147
229
 
148
230
  # A helper function to return the page level from the "page" header, for
@@ -162,156 +244,197 @@ class Innodb::Page::Index < Innodb::Page
162
244
  self.prev.nil? && self.next.nil?
163
245
  end
164
246
 
165
- # Return the "fseg" header.
166
- def fseg_header
167
- c = cursor(pos_fseg_header)
168
- @fseg_header ||= {
169
- :leaf => Innodb::FsegEntry.get_inode(@space, c),
170
- :internal => Innodb::FsegEntry.get_inode(@space, c),
171
- }
172
- end
173
-
174
- # The size (in bytes) of the bit-packed fields in each record header.
175
- RECORD_BITS_SIZE = 3
176
-
177
- # The size (in bytes) of the "next" pointer in each record header.
178
- RECORD_NEXT_SIZE = 2
179
-
180
- # The size (in bytes) of the record pointers in each page directory slot.
181
- PAGE_DIR_SLOT_SIZE = 2
182
-
183
- # The minimum number of records "owned" by each record with an entry in
184
- # the page directory.
185
- PAGE_DIR_SLOT_MIN_N_OWNED = 4
186
-
187
- # The maximum number of records "owned" by each record with an entry in
188
- # the page directory.
189
- PAGE_DIR_SLOT_MAX_N_OWNED = 8
190
-
191
- # Return the size of the header for each record.
192
- def size_record_header
193
- case page_header[:format]
194
- when :compact
195
- RECORD_BITS_SIZE + RECORD_NEXT_SIZE
196
- when :redundant
197
- RECORD_BITS_SIZE + RECORD_NEXT_SIZE + 1
198
- end
247
+ # A helper function to return the offset to the first free record.
248
+ def garbage_offset
249
+ page_header && page_header[:garbage_offset]
199
250
  end
200
251
 
201
- # Return the size of a field in the record header for which no description
202
- # could be found (but must be skipped anyway).
203
- def size_record_undefined
204
- case page_header[:format]
205
- when :compact
206
- 0
207
- when :redundant
208
- 1
252
+ # Return the "fseg" header.
253
+ def fseg_header
254
+ @fseg_header ||= cursor(pos_fseg_header).name("fseg") do |c|
255
+ {
256
+ :leaf => c.name("fseg[leaf]") {
257
+ Innodb::FsegEntry.get_inode(@space, c)
258
+ },
259
+ :internal => c.name("fseg[internal]") {
260
+ Innodb::FsegEntry.get_inode(@space, c)
261
+ },
262
+ }
209
263
  end
210
264
  end
211
265
 
212
- # Record types used in the :type field of the record header.
213
- RECORD_TYPES = {
214
- 0 => :conventional, # A normal user record in a leaf page.
215
- 1 => :node_pointer, # A node pointer in a non-leaf page.
216
- 2 => :infimum, # The system "infimum" record.
217
- 3 => :supremum, # The system "supremum" record.
218
- }
219
-
220
- # This record is the minimum record at this level of the B-tree.
221
- RECORD_INFO_MIN_REC_FLAG = 1
222
-
223
- # This record has been marked as deleted.
224
- RECORD_INFO_DELETED_FLAG = 2
225
-
226
266
  # Return the header from a record.
227
- def record_header(offset)
228
- c = cursor(offset).backward
229
- case page_header[:format]
230
- when :compact
231
- header = {}
232
- header[:next] = c.get_sint16
233
- bits1 = c.get_uint16
234
- header[:type] = RECORD_TYPES[bits1 & 0x07]
235
- header[:order] = (bits1 & 0xf8) >> 3
236
- bits2 = c.get_uint8
267
+ def record_header(cursor)
268
+ origin = cursor.position
269
+ header = {}
270
+ cursor.backward.name("header") do |c|
271
+ case page_header[:format]
272
+ when :compact
273
+ # The "next" pointer is a relative offset from the current record.
274
+ header[:next] = c.name("next") { origin + c.get_sint16 }
275
+
276
+ # Fields packed in a 16-bit integer (LSB first):
277
+ # 3 bits for type
278
+ # 13 bits for heap_number
279
+ bits1 = c.name("bits1") { c.get_uint16 }
280
+ header[:type] = RECORD_TYPES[bits1 & 0x07]
281
+ header[:heap_number] = (bits1 & 0xf8) >> 3
282
+ when :redundant
283
+ # The "next" pointer is an absolute offset within the page.
284
+ header[:next] = c.name("next") { c.get_uint16 }
285
+
286
+ # Fields packed in a 24-bit integer (LSB first):
287
+ # 1 bit for offset_size (0 = 2 bytes, 1 = 1 byte)
288
+ # 10 bits for n_fields
289
+ # 13 bits for heap_number
290
+ bits1 = c.name("bits1") { c.get_uint24 }
291
+ header[:offset_size] = (bits1 & 1) == 0 ? 2 : 1
292
+ header[:n_fields] = (bits1 & (((1 << 10) - 1) << 1)) >> 1
293
+ header[:heap_number] = (bits1 & (((1 << 13) - 1) << 11)) >> 11
294
+ end
295
+
296
+ # Fields packed in an 8-bit integer (LSB first):
297
+ # 4 bits for n_owned
298
+ # 4 bits for flags
299
+ bits2 = c.name("bits2") { c.get_uint8 }
237
300
  header[:n_owned] = bits2 & 0x0f
238
301
  info = (bits2 & 0xf0) >> 4
239
302
  header[:min_rec] = (info & RECORD_INFO_MIN_REC_FLAG) != 0
240
303
  header[:deleted] = (info & RECORD_INFO_DELETED_FLAG) != 0
241
- case header[:type]
242
- when :conventional, :node_pointer
243
- # The variable-length part of the record header contains a
244
- # bit vector indicating NULL fields and the length of each
245
- # non-NULL variable-length field.
246
- if record_format
247
- header[:null_bitmap] = nbmap = record_null_bitmap(c)
248
- header[:variable_length] = record_variable_length(c, nbmap)
249
- end
304
+
305
+ case page_header[:format]
306
+ when :compact
307
+ record_header_compact_additional(header, cursor)
308
+ when :redundant
309
+ record_header_redundant_additional(header, cursor)
310
+ end
311
+ end
312
+
313
+ header
314
+ end
315
+
316
+ # Read additional header information from a compact format record header.
317
+ def record_header_compact_additional(header, cursor)
318
+ case header[:type]
319
+ when :conventional, :node_pointer
320
+ # The variable-length part of the record header contains a
321
+ # bit vector indicating NULL fields and the length of each
322
+ # non-NULL variable-length field.
323
+ if record_format
324
+ header[:field_nulls] = cursor.name("field_nulls") {
325
+ record_header_compact_null_bitmap(cursor)
326
+ }
327
+ header[:field_lengths], header[:field_externs] =
328
+ cursor.name("field_lengths_and_externs") {
329
+ record_header_compact_variable_lengths_and_externs(cursor,
330
+ header[:field_nulls])
331
+ }
250
332
  end
251
- header
252
- when :redundant
253
- raise "Not implemented"
254
333
  end
255
334
  end
256
335
 
257
336
  # Return an array indicating which fields are null.
258
- def record_null_bitmap(cursor)
337
+ def record_header_compact_null_bitmap(cursor)
259
338
  fields = (record_format[:key] + record_format[:row])
260
339
 
261
340
  # The number of bits in the bitmap is the number of nullable fields.
262
- size = fields.count do |f| f.nullable end
341
+ size = fields.count { |f| f.type.nullable? }
263
342
 
264
343
  # There is no bitmap if there are no nullable fields.
265
344
  return nil unless size > 0
266
345
 
267
346
  # To simplify later checks, expand bitmap to one for each field.
268
- bitmap = Array.new(fields.size, false)
347
+ bitmap = Array.new(fields.last.position + 1, false)
269
348
 
270
349
  null_bit_array = cursor.get_bit_array(size).reverse!
271
350
 
272
351
  # For every nullable field, set whether the field is actually null.
273
352
  fields.each do |f|
274
- bitmap[f.position] = f.nullable ? (null_bit_array.shift == 1) : false
353
+ bitmap[f.position] = f.type.nullable? ? (null_bit_array.shift == 1) : false
275
354
  end
276
355
 
277
356
  return bitmap
278
357
  end
279
358
 
280
- # Return an array containing the length of each variable-length field.
281
- def record_variable_length(cursor, null_bitmap)
359
+ # Return an array containing an array of the length of each variable-length
360
+ # field and an array indicating which fields are stored externally.
361
+ def record_header_compact_variable_lengths_and_externs(cursor, null_bitmap)
282
362
  fields = (record_format[:key] + record_format[:row])
283
363
 
284
- len_array = Array.new(fields.size, 0)
364
+ len_array = Array.new(fields.last.position + 1, 0)
365
+ ext_array = Array.new(fields.last.position + 1, false)
285
366
 
286
367
  # For each non-NULL variable-length field, the record header contains
287
368
  # the length in one or two bytes.
288
369
  fields.each do |f|
289
- next if f.fixed_len > 0 or null_bitmap[f.position]
370
+ next if !f.type.variable? or (null_bitmap && null_bitmap[f.position])
290
371
 
291
372
  len = cursor.get_uint8
373
+ ext = false
292
374
 
293
375
  # Two bytes are used only if the length exceeds 127 bytes and the
294
- # maximum length exceeds 255 bytes.
295
- if len > 127 and f.variable_len > 255
376
+ # maximum length exceeds 255 bytes (or the field is a BLOB type).
377
+ if len > 127 && (f.type.blob? || f.type.length > 255)
378
+ ext = (0x40 & len) != 0
296
379
  len = ((len & 0x3f) << 8) + cursor.get_uint8
297
380
  end
298
381
 
299
382
  len_array[f.position] = len
383
+ ext_array[f.position] = ext
300
384
  end
301
385
 
302
- return len_array
386
+ return len_array, ext_array
387
+ end
388
+
389
+ # Read additional header information from a redundant format record header.
390
+ def record_header_redundant_additional(header, cursor)
391
+ header[:field_lengths] = []
392
+ header[:field_nulls] = []
393
+ header[:field_externs] = []
394
+
395
+ field_offsets = record_header_redundant_field_end_offsets(header, cursor)
396
+
397
+ this_field_offset = 0
398
+ field_offsets.each do |n|
399
+ case header[:offset_size]
400
+ when 1
401
+ next_field_offset = (n & RECORD_REDUNDANT_OFF1_OFFSET_MASK)
402
+ header[:field_lengths] << (next_field_offset - this_field_offset)
403
+ header[:field_nulls] << ((n & RECORD_REDUNDANT_OFF1_NULL_MASK) != 0)
404
+ header[:field_externs] << false
405
+ when 2
406
+ next_field_offset = (n & RECORD_REDUNDANT_OFF2_OFFSET_MASK)
407
+ header[:field_lengths] << (next_field_offset - this_field_offset)
408
+ header[:field_nulls] << ((n & RECORD_REDUNDANT_OFF2_NULL_MASK) != 0)
409
+ header[:field_externs] << ((n & RECORD_REDUNDANT_OFF2_EXTERN_MASK) != 0)
410
+ end
411
+ this_field_offset = next_field_offset
412
+ end
413
+ end
414
+
415
+ # Read field end offsets from the provided cursor for each field as counted
416
+ # by n_fields.
417
+ def record_header_redundant_field_end_offsets(header, cursor)
418
+ (0...header[:n_fields]).to_a.inject([]) do |offsets, n|
419
+ cursor.name("field_end_offset[#{n}]") {
420
+ offsets << cursor.get_uint_by_size(header[:offset_size])
421
+ }
422
+ offsets
423
+ end
303
424
  end
304
425
 
305
426
  # Parse and return simple fixed-format system records, such as InnoDB's
306
427
  # internal infimum and supremum records.
307
428
  def system_record(offset)
308
- header = record_header(offset)
309
- {
310
- :offset => offset,
311
- :header => header,
312
- :next => offset + header[:next],
313
- :data => cursor(offset).get_bytes(size_mum_record),
314
- }
429
+ cursor(offset).name("record[#{offset}]") do |c|
430
+ header = c.peek { record_header(c) }
431
+ {
432
+ :offset => offset,
433
+ :header => header,
434
+ :next => header[:next],
435
+ :data => c.name("data") { c.get_bytes(size_mum_record) },
436
+ }
437
+ end
315
438
  end
316
439
 
317
440
  # Return the infimum record on a page.
@@ -328,18 +451,23 @@ class Innodb::Page::Index < Innodb::Page
328
451
  def make_record_description
329
452
  description = record_describer.cursor_sendable_description(self)
330
453
 
331
- fields = []
454
+ position = 0
455
+ fields = {:type => description[:type], :key => [], :row => []}
332
456
 
333
- (description[:key] + description[:row]).each_with_index do |d, p|
334
- fields << Innodb::Field.new(p, *d)
457
+ description[:key].each do |d|
458
+ fields[:key] << Innodb::Field.new(position, *d)
459
+ position += 1
335
460
  end
336
461
 
337
- n = description[:key].size
462
+ # Account for TRX_ID and ROLL_PTR.
463
+ position += 2
338
464
 
339
- description[:key] = fields.slice(0 .. n-1)
340
- description[:row] = fields.slice(n .. -1)
465
+ description[:row].each do |d|
466
+ fields[:row] << Innodb::Field.new(position, *d)
467
+ position += 1
468
+ end
341
469
 
342
- return description
470
+ fields
343
471
  end
344
472
 
345
473
  # Return (and cache) the record format provided by an external class.
@@ -355,62 +483,75 @@ class Innodb::Page::Index < Innodb::Page
355
483
  return infimum if offset == pos_infimum
356
484
  return supremum if offset == pos_supremum
357
485
 
358
- c = cursor(offset).forward
359
-
360
- # There is a header preceding the row itself, so back up and read it.
361
- header = record_header(offset)
362
-
363
- this_record = {
364
- :format => page_header[:format],
365
- :offset => offset,
366
- :header => header,
367
- :next => header[:next] == 0 ? nil : (offset + header[:next]),
368
- }
369
-
370
- if record_format
371
- this_record[:type] = record_format[:type]
486
+ cursor(offset).forward.name("record[#{offset}]") do |c|
487
+ # There is a header preceding the row itself, so back up and read it.
488
+ header = c.peek { record_header(c) }
489
+
490
+ this_record = {
491
+ :format => page_header[:format],
492
+ :offset => offset,
493
+ :header => header,
494
+ :next => header[:next] == 0 ? nil : (header[:next]),
495
+ }
496
+
497
+ if record_format
498
+ this_record[:type] = record_format[:type]
499
+
500
+ # Read the key fields present in all types of pages.
501
+ this_record[:key] = []
502
+ this_record[:key_ext] = []
503
+ c.name("key") do
504
+ record_format[:key].each do |f|
505
+ this_record[:key].push f.read(this_record, c)
506
+ this_record[:key_ext].push f.read_extern(this_record, c)
507
+ end
508
+ end
372
509
 
373
- # Read the key fields present in all types of pages.
374
- this_record[:key] = []
375
- record_format[:key].each do |f|
376
- this_record[:key].push f.read(this_record, c)
377
- end
510
+ # If this is a leaf page of the clustered index, read InnoDB's internal
511
+ # fields, a transaction ID and roll pointer.
512
+ if level == 0 && record_format[:type] == :clustered
513
+ this_record[:transaction_id] = c.name("transaction_id") { c.get_hex(6) }
514
+ c.name("roll_pointer") do
515
+ rseg_id_insert_flag = c.name("rseg_id_insert_flag") { c.get_uint8 }
516
+ this_record[:roll_pointer] = {
517
+ :is_insert => (rseg_id_insert_flag & 0x80) == 0x80,
518
+ :rseg_id => rseg_id_insert_flag & 0x7f,
519
+ :undo_log => c.name("undo_log") {
520
+ {
521
+ :page => c.name("page") { c.get_uint32 },
522
+ :offset => c.name("offset") { c.get_uint16 },
523
+ }
524
+ }
525
+ }
526
+ end
527
+ end
378
528
 
379
- # If this is a leaf page of the clustered index, read InnoDB's internal
380
- # fields, a transaction ID and roll pointer.
381
- if level == 0 && record_format[:type] == :clustered
382
- this_record[:transaction_id] = c.get_hex(6)
383
- first_byte = c.get_uint8
384
- this_record[:roll_pointer] = {
385
- :is_insert => (first_byte & 0x80) == 0x80,
386
- :rseg_id => first_byte & 0x7f,
387
- :undo_log => {
388
- :page => c.get_uint32,
389
- :offset => c.get_uint16,
390
- }
391
- }
392
- end
529
+ # If this is a leaf page of the clustered index, or any page of a
530
+ # secondary index, read the non-key fields.
531
+ if (level == 0 && record_format[:type] == :clustered) ||
532
+ (record_format[:type] == :secondary)
533
+ # Read the non-key fields.
534
+ this_record[:row] = []
535
+ this_record[:row_ext] = []
536
+ c.name("row") do
537
+ record_format[:row].each do |f|
538
+ this_record[:row].push f.read(this_record, c)
539
+ this_record[:row_ext].push f.read_extern(this_record, c)
540
+ end
541
+ end
542
+ end
393
543
 
394
- # If this is a leaf page of the clustered index, or any page of a
395
- # secondary index, read the non-key fields.
396
- if (level == 0 && record_format[:type] == :clustered) ||
397
- (record_format[:type] == :secondary)
398
- # Read the non-key fields.
399
- this_record[:row] = []
400
- record_format[:row].each do |f|
401
- this_record[:row].push f.read(this_record, c)
544
+ # If this is a node (non-leaf) page, it will have a child page number
545
+ # (or "node pointer") stored as the last field.
546
+ if level > 0
547
+ # Read the node pointer in a node (non-leaf) page.
548
+ this_record[:child_page_number] =
549
+ c.name("child_page_number") { c.get_uint32 }
402
550
  end
403
551
  end
404
552
 
405
- # If this is a node (non-leaf) page, it will have a child page number
406
- # (or "node pointer") stored as the last field.
407
- if level > 0
408
- # Read the node pointer in a node (non-leaf) page.
409
- this_record[:child_page_number] = c.get_uint32
410
- end
553
+ this_record
411
554
  end
412
-
413
- this_record
414
555
  end
415
556
 
416
557
  # A class for cursoring through records starting from an arbitrary point.
@@ -428,7 +569,13 @@ class Innodb::Page::Index < Innodb::Page
428
569
  record = @page.record(@offset)
429
570
 
430
571
  if record == @page.supremum
572
+ # We've reached the end of the linked list at supremum.
573
+ @offset = nil
574
+ elsif record[:next] == @offset
575
+ # The record links to itself; go ahead and return it (once), but set
576
+ # the next offset to nil to end the loop.
431
577
  @offset = nil
578
+ record
432
579
  else
433
580
  @offset = record[:next]
434
581
  record
@@ -462,6 +609,24 @@ class Innodb::Page::Index < Innodb::Page
462
609
  nil
463
610
  end
464
611
 
612
+ def each_garbage_record
613
+ unless block_given?
614
+ return enum_for(:each_garbage_record)
615
+ end
616
+
617
+ if garbage_offset == 0
618
+ return nil
619
+ end
620
+
621
+ c = record_cursor(garbage_offset)
622
+
623
+ while rec = c.record
624
+ yield rec
625
+ end
626
+
627
+ nil
628
+ end
629
+
465
630
  # Iterate through all child pages of a node (non-leaf) page, which are
466
631
  # stored as records with the child page number as the last field in the
467
632
  # record.
@@ -484,9 +649,10 @@ class Innodb::Page::Index < Innodb::Page
484
649
  return @directory if @directory
485
650
 
486
651
  @directory = []
487
- c = cursor(pos_directory).backward
488
- directory_slots.times do
489
- @directory.push c.get_uint16
652
+ cursor(pos_directory).backward.name("page_directory") do |c|
653
+ directory_slots.times do |n|
654
+ @directory.push c.name("slot[#{n}]") { c.get_uint16 }
655
+ end
490
656
  end
491
657
 
492
658
  @directory
@@ -517,18 +683,26 @@ class Innodb::Page::Index < Innodb::Page
517
683
  ]
518
684
  puts
519
685
 
686
+ puts "page directory:"
687
+ pp directory
688
+ puts
689
+
520
690
  puts "system records:"
521
691
  pp infimum
522
692
  pp supremum
523
693
  puts
524
694
 
525
- puts "page directory:"
526
- pp directory
695
+ puts "garbage records:"
696
+ each_garbage_record do |rec|
697
+ pp rec
698
+ puts
699
+ end
527
700
  puts
528
701
 
529
702
  puts "records:"
530
703
  each_record do |rec|
531
704
  pp rec
705
+ puts
532
706
  end
533
707
  puts
534
708
  end