innodb_ruby 0.7.11 → 0.7.12

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,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