innodb_ruby 0.6.6 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,23 @@
1
+ # An InnoDB file segment entry, which appears in a few places, such as the
2
+ # FSEG header of INDEX pages, and in the TRX_SYS pages.
1
3
  class Innodb::FsegEntry
2
4
  SIZE = 4 + 4 + 2
3
5
 
4
- def self.get_entry(cursor)
6
+ # Return the FSEG entry address, which points to an entry on an INODE page.
7
+ def self.get_entry_address(cursor)
5
8
  {
6
9
  :space_id => cursor.get_uint32,
7
10
  :page_number => cursor.get_uint32,
8
11
  :offset => cursor.get_uint16,
9
12
  }
10
13
  end
14
+
15
+ # Return an INODE entry which represents this file segment.
16
+ def self.get_inode(space, cursor)
17
+ address = get_entry_address(cursor)
18
+ page = space.page(address[:page_number])
19
+ if page.type == :INODE
20
+ page.inode_at(address[:offset])
21
+ end
22
+ end
11
23
  end
@@ -102,12 +102,15 @@ class Innodb::Index
102
102
  # -1 = a is less than b
103
103
  # +1 = a is greater than b
104
104
  def compare_key(a, b)
105
- return -1 if a.size < b.size
106
- return +1 if a.size > b.size
105
+ return 0 if a.nil? && b.nil?
106
+ return -1 if a.nil? || (!b.nil? && a.size < b.size)
107
+ return +1 if b.nil? || (!a.nil? && a.size > b.size)
108
+
107
109
  a.each_index do |i|
108
110
  return -1 if a[i] < b[i]
109
111
  return +1 if a[i] > b[i]
110
112
  end
113
+
111
114
  return 0
112
115
  end
113
116
 
@@ -116,20 +119,87 @@ class Innodb::Index
116
119
  # than the key. (If an exact match is desired, compare_key must be used to
117
120
  # check if the returned record matches. This makes the function useful for
118
121
  # search in both leaf and non-leaf pages.)
119
- def linear_search_in_page(page, key)
120
- c = page.record_cursor(page.infimum[:next])
121
- this_rec = c.record
122
- while next_rec = c.record
123
- return this_rec if next_rec == page.supremum
122
+ def linear_search_from_cursor(cursor, key)
123
+ this_rec = cursor.record
124
+
125
+ # Iterate through all records until finding either a matching record or
126
+ # one whose key is greater than the desired key.
127
+ while this_rec && next_rec = cursor.record
128
+ # If we reach supremum, return the last non-system record we got.
129
+ return this_rec if next_rec[:header][:type] == :supremum
130
+
124
131
  if (compare_key(key, this_rec[:key]) >= 0) &&
125
132
  (compare_key(key, next_rec[:key]) < 0)
133
+ # The desired key is either an exact match for this_rec or is greater
134
+ # than it but less than next_rec. If this is a non-leaf page, that
135
+ # will mean that the record will fall on the leaf page this node
136
+ # pointer record points to, if it exists at all.
126
137
  return this_rec
127
138
  end
139
+
128
140
  this_rec = next_rec
129
141
  end
142
+
130
143
  this_rec
131
144
  end
132
145
 
146
+ # Search or a record within a single page using the page directory to limit
147
+ # the number of record comparisons required. Once the last page directory
148
+ # entry closest to but not greater than the key is found, fall back to
149
+ # linear search using linear_search_from_cursor to find the closest record
150
+ # whose key is not greater than the desired key. (If an exact match is
151
+ # desired, the returned record must be checked in the same way as the above
152
+ # linear_search_from_cursor function.)
153
+ def binary_search_by_directory(page, dir, key)
154
+ return nil if dir.empty?
155
+
156
+ # Split the directory at the mid-point (using integer math, so the division
157
+ # is rounding down). Retrieve the record that sits at the mid-point.
158
+ mid = dir.size / 2
159
+ rec = page.record(dir[mid])
160
+
161
+ # The mid-point record was the infimum record, which is not comparable with
162
+ # compare_key, so we need to just linear scan from here. If the mid-point
163
+ # is the beginning of the page there can't be many records left to check
164
+ # anyway.
165
+ if rec[:header][:type] == :infimum
166
+ return linear_search_from_cursor(page.record_cursor(rec[:next]), key)
167
+ end
168
+
169
+ # Compare the desired key to the mid-point record's key.
170
+ case compare_key(key, rec[:key])
171
+ when 0
172
+ # An exact match for the key was found. Return the record.
173
+ rec
174
+ when +1
175
+ # The mid-point record's key is less than the desired key.
176
+ if dir.size == 1
177
+ # This is the last entry remaining from the directory, use linear
178
+ # search to find the record. We already know that there wasn't an
179
+ # exact match, so skip the current record and start cursoring from
180
+ # the next record.
181
+ linear_search_from_cursor(page.record_cursor(rec[:next]), key)
182
+ else
183
+ # There are more entries remaining from the directory, recurse again
184
+ # using binary search on the right half of the directory, which
185
+ # represents values greater than or equal to the mid-point record's
186
+ # key.
187
+ binary_search_by_directory(page, dir[mid...dir.size], key)
188
+ end
189
+ when -1
190
+ # The mid-point record's key is greater than the desired key.
191
+ if dir.size == 1
192
+ # If this is the last entry remaining from the directory, we didn't
193
+ # find anything workable.
194
+ nil
195
+ else
196
+ # Recurse on the left half of the directory, which represents values
197
+ # less than the mid-point record's key.
198
+ binary_search_by_directory(page, dir[0...mid], key)
199
+ end
200
+ end
201
+ end
202
+
133
203
  # Search for a record within the entire index, walking down the non-leaf
134
204
  # pages until a leaf page is found, and then verifying that the record
135
205
  # returned on the leaf page is an exact match for the key. If a matching
@@ -138,13 +208,41 @@ class Innodb::Index
138
208
  def linear_search(key)
139
209
  page = @root
140
210
 
141
- while rec = linear_search_in_page(page, key)
211
+ while rec =
212
+ linear_search_from_cursor(page.record_cursor(page.infimum[:next]), key)
142
213
  if page.level > 0
214
+ # If we haven't reached a leaf page yet, move down the tree and search
215
+ # again using linear search.
143
216
  page = @space.page(rec[:child_page_number])
144
217
  else
218
+ # We're on a leaf page, so return the page and record if there is a
219
+ # match. If there is no match, break the loop and cause nil to be
220
+ # returned.
145
221
  return page, rec if compare_key(key, rec[:key]) == 0
146
222
  break
147
223
  end
148
224
  end
149
225
  end
226
+
227
+ # Search for a record within the entire index like linear_search, but use
228
+ # the page directory to search while making as few record comparisons as
229
+ # possible. If a matching record is not found, nil is returned.
230
+ def binary_search(key)
231
+ page = @root
232
+
233
+ while rec = binary_search_by_directory(page, page.directory.dup, key)
234
+ if page.level > 0
235
+ # If we haven't reached a leaf page yet, move down the tree and search
236
+ # again using binary search.
237
+ page = @space.page(rec[:child_page_number])
238
+ else
239
+ # We're on a leaf page, so return the page and record if there is a
240
+ # match. If there is no match, break the loop and cause nil to be
241
+ # returned.
242
+ return page, rec if compare_key(key, rec[:key]) == 0
243
+ break
244
+ end
245
+ end
246
+ end
247
+
150
248
  end
@@ -0,0 +1,109 @@
1
+ class Innodb::List
2
+ FIL_ADDR_SIZE = 4 + 2
3
+ NODE_SIZE = 2 * FIL_ADDR_SIZE
4
+ BASE_NODE_SIZE = 4 + (2 * FIL_ADDR_SIZE)
5
+
6
+ def self.get_address(cursor)
7
+ page = Innodb::Page.maybe_undefined(cursor.get_uint32)
8
+ offset = cursor.get_uint16
9
+ if page
10
+ {
11
+ :page => page,
12
+ :offset => offset,
13
+ }
14
+ end
15
+ end
16
+
17
+ def self.get_node(cursor)
18
+ {
19
+ :prev => get_address(cursor),
20
+ :next => get_address(cursor),
21
+ }
22
+ end
23
+
24
+ def self.get_base_node(cursor)
25
+ {
26
+ :length => cursor.get_uint32,
27
+ :first => get_address(cursor),
28
+ :last => get_address(cursor),
29
+ }
30
+ end
31
+
32
+ def initialize(space, base)
33
+ @space = space
34
+ @base = base
35
+ end
36
+
37
+ attr_reader :space
38
+ attr_reader :base
39
+
40
+ def prev(object)
41
+ object_from_address(object.prev_address)
42
+ end
43
+
44
+ def next(object)
45
+ object_from_address(object.next_address)
46
+ end
47
+
48
+ def first
49
+ object_from_address(@base[:first])
50
+ end
51
+
52
+ def last
53
+ object_from_address(@base[:last])
54
+ end
55
+
56
+ def cursor(node=nil)
57
+ Cursor.new(self, node)
58
+ end
59
+
60
+ def each
61
+ c = cursor
62
+ while e = c.next
63
+ yield e
64
+ end
65
+ end
66
+
67
+ class Cursor
68
+ def initialize(list, node=nil)
69
+ @list = list
70
+ @cursor = node
71
+ end
72
+
73
+ def reset
74
+ @cursor = nil
75
+ end
76
+
77
+ def prev
78
+ if @cursor
79
+ @cursor = @list.prev(@cursor)
80
+ else
81
+ @cursor = @list.last
82
+ end
83
+ end
84
+
85
+ def next
86
+ if @cursor
87
+ @cursor = @list.next(@cursor)
88
+ else
89
+ @cursor = @list.first
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ class Innodb::List::Xdes < Innodb::List
96
+ def object_from_address(address)
97
+ if address && page = @space.page(address[:page])
98
+ Innodb::Xdes.new(page, page.cursor(address[:offset] - 8))
99
+ end
100
+ end
101
+ end
102
+
103
+ class Innodb::List::Inode < Innodb::List
104
+ def object_from_address(address)
105
+ if address && page = @space.page(address[:page])
106
+ page
107
+ end
108
+ end
109
+ end
@@ -1,5 +1,10 @@
1
1
  require "innodb/cursor"
2
2
 
3
+ # A generic class for any type of page, which handles reading the common
4
+ # FIL header and trailer, and can handle (via #parse) dispatching to a more
5
+ # specialized class depending on page type (which comes from the FIL header).
6
+ # A page being handled by Innodb::Page indicates that its type is not currently
7
+ # handled by any more specialized class.
3
8
  class Innodb::Page
4
9
  SPECIALIZED_CLASSES = {}
5
10
 
@@ -7,11 +12,11 @@ class Innodb::Page
7
12
  # and then attempt to hand off the page to a specialized class to be
8
13
  # re-parsed if possible. If there is no specialized class for this type
9
14
  # of page, return the generic object.
10
- def self.parse(buffer)
11
- page = Innodb::Page.new(buffer)
15
+ def self.parse(space, buffer)
16
+ page = Innodb::Page.new(space, buffer)
12
17
 
13
18
  if specialized_class = SPECIALIZED_CLASSES[page.type]
14
- page = specialized_class.new(buffer)
19
+ page = specialized_class.new(space, buffer)
15
20
  end
16
21
 
17
22
  page
@@ -19,7 +24,8 @@ class Innodb::Page
19
24
 
20
25
  # Initialize a page by passing in a 16kB buffer containing the raw page
21
26
  # contents. Currently only 16kB pages are supported.
22
- def initialize(buffer)
27
+ def initialize(space, buffer)
28
+ @space = space
23
29
  @buffer = buffer
24
30
  end
25
31
 
@@ -130,6 +136,22 @@ class Innodb::Page
130
136
  fil_header[:lsn]
131
137
  end
132
138
 
139
+ def inspect
140
+ if fil_header
141
+ "#<%s: size=%i, space_id=%i, offset=%i, type=%s, prev=%i, next=%i>" % [
142
+ self.class,
143
+ size,
144
+ fil_header[:space_id],
145
+ fil_header[:offset],
146
+ fil_header[:type],
147
+ fil_header[:prev],
148
+ fil_header[:next],
149
+ ]
150
+ else
151
+ "#<#{self.class}>"
152
+ end
153
+ end
154
+
133
155
  # Dump the contents of a page for debugging purposes.
134
156
  def dump
135
157
  puts "#{self}:"
@@ -1,24 +1,38 @@
1
- require "innodb/free_list"
1
+ require "innodb/list"
2
+ require "innodb/xdes"
2
3
 
4
+ # A specialized class for FSP_HDR (filespace header) and XDES (extent
5
+ # descriptor) page types. Each tablespace always has an FSP_HDR page as
6
+ # its first page (page 0), and has repeating XDES pages every 16,384 pages
7
+ # after that (page 16384, 32768, ...). The FSP_HDR and XDES page structure
8
+ # is completely identical, with the exception that the FSP header structure
9
+ # is zero-filled on XDES pages, but populated on FSP_HDR pages.
10
+ #
11
+ # The basic structure of FSP_HDR and XDES pages is: FIL header, FSP header,
12
+ # an array of 256 XDES entries, empty (unused) space, and FIL trailer.
3
13
  class Innodb::Page::FspHdrXdes < Innodb::Page
4
- XDES_BITS_PER_PAGE = 2
5
- XDES_BITMAP_SIZE = (64 * XDES_BITS_PER_PAGE) / 8
6
- XDES_SIZE = 8 + Innodb::FreeList::NODE_SIZE + 4 + XDES_BITMAP_SIZE
7
-
14
+ # This is actually defined as page size divided by extent size, which is
15
+ # 16384 / 64 = 256.
8
16
  XDES_N_ARRAY_ENTRIES = 256
9
17
 
18
+ # The FSP header immediately follows the FIL header.
10
19
  def pos_fsp_header
11
20
  pos_fil_header + size_fil_header
12
21
  end
13
22
 
23
+ # The FSP header contains six 32-bit integers, one 64-bit integer, and 5
24
+ # list base nodes.
14
25
  def size_fsp_header
15
- (32 + 5 * Innodb::FreeList::BASE_NODE_SIZE)
26
+ ((4 * 6) + (1 * 8) + (5 * Innodb::List::BASE_NODE_SIZE))
16
27
  end
17
28
 
29
+ # The XDES entry array immediately follows the FSP header.
18
30
  def pos_xdes_array
19
31
  pos_fsp_header + size_fsp_header
20
32
  end
21
33
 
34
+ # Read the FSP (filespace) header, which contains a few counters and flags,
35
+ # as well as list base nodes for each list maintained in the filespace.
22
36
  def fsp_header
23
37
  c = cursor(pos_fsp_header)
24
38
  @fsp_header ||= {
@@ -28,36 +42,28 @@ class Innodb::Page::FspHdrXdes < Innodb::Page
28
42
  :free_limit => c.get_uint32,
29
43
  :flags => c.get_uint32,
30
44
  :frag_n_used => c.get_uint32,
31
- :free => Innodb::FreeList::get_base_node(c),
32
- :free_frag => Innodb::FreeList::get_base_node(c),
33
- :full_frag => Innodb::FreeList::get_base_node(c),
45
+ :free => Innodb::List::Xdes.new(@space,
46
+ Innodb::List.get_base_node(c)),
47
+ :free_frag => Innodb::List::Xdes.new(@space,
48
+ Innodb::List.get_base_node(c)),
49
+ :full_frag => Innodb::List::Xdes.new(@space,
50
+ Innodb::List.get_base_node(c)),
34
51
  :first_unused_seg => c.get_uint64,
35
- :full_inodes => Innodb::FreeList::get_base_node(c),
36
- :free_inodes => Innodb::FreeList::get_base_node(c),
52
+ :full_inodes => Innodb::List::Inode.new(@space,
53
+ Innodb::List.get_base_node(c)),
54
+ :free_inodes => Innodb::List::Inode.new(@space,
55
+ Innodb::List.get_base_node(c)),
37
56
  }
38
57
  end
39
58
 
40
- XDES_STATES = {
41
- 1 => :free,
42
- 2 => :free_frag,
43
- 3 => :full_frag,
44
- 4 => :fseg,
45
- }
46
-
47
- def read_xdes(cursor)
48
- {
49
- :xdes_id => cursor.get_uint64,
50
- :position => cursor.position,
51
- :free_list => Innodb::FreeList::get_node(cursor),
52
- :state => XDES_STATES[cursor.get_uint32],
53
- :bitmap => cursor.get_hex(XDES_BITMAP_SIZE),
54
- }
55
- end
56
-
59
+ # Iterate through all XDES entries in order. This is useful for debugging,
60
+ # but each of these entries is actually a node in some other list. The state
61
+ # field in the XDES entry indicates which type of list it is present in,
62
+ # although not necessarily which list (e.g. :fseg).
57
63
  def each_xdes
58
64
  c = cursor(pos_xdes_array)
59
65
  XDES_N_ARRAY_ENTRIES.times do
60
- yield read_xdes(c)
66
+ yield Innodb::Xdes.new(self, c)
61
67
  end
62
68
  end
63
69
 
@@ -1,5 +1,13 @@
1
1
  require "innodb/fseg_entry"
2
2
 
3
+ # A specialized class for handling INDEX pages, which contain a portion of
4
+ # the data from exactly one B+tree. These are typically the most common type
5
+ # of page in any database.
6
+ #
7
+ # The basic structure of an INDEX page is: FIL header, INDEX header, FSEG
8
+ # header, fixed-width system records (infimum and supremum), user records
9
+ # (the actual data) which grow ascending by offset, free space, the page
10
+ # directory which grows descending by offset, and the FIL trailer.
3
11
  class Innodb::Page::Index < Innodb::Page
4
12
  attr_accessor :record_describer
5
13
 
@@ -158,16 +166,26 @@ class Innodb::Page::Index < Innodb::Page
158
166
  def fseg_header
159
167
  c = cursor(pos_fseg_header)
160
168
  @fseg_header ||= {
161
- :free_list => Innodb::FsegEntry.get_entry(c),
162
- :btree_segment => Innodb::FsegEntry.get_entry(c),
169
+ :leaf => Innodb::FsegEntry.get_inode(@space, c),
170
+ :internal => Innodb::FsegEntry.get_inode(@space, c),
163
171
  }
164
172
  end
165
173
 
174
+ # The size (in bytes) of the bit-packed fields in each record header.
166
175
  RECORD_BITS_SIZE = 3
176
+
177
+ # The size (in bytes) of the "next" pointer in each record header.
167
178
  RECORD_NEXT_SIZE = 2
168
179
 
180
+ # The size (in bytes) of the record pointers in each page directory slot.
169
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.
170
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.
171
189
  PAGE_DIR_SLOT_MAX_N_OWNED = 8
172
190
 
173
191
  # Return the size of the header for each record.
@@ -205,7 +223,7 @@ class Innodb::Page::Index < Innodb::Page
205
223
  # This record has been marked as deleted.
206
224
  RECORD_INFO_DELETED_FLAG = 2
207
225
 
208
- # Return the header from a record. (This is mostly unimplemented.)
226
+ # Return the header from a record.
209
227
  def record_header(offset)
210
228
  return nil unless type == :INDEX
211
229
 
@@ -222,12 +240,70 @@ class Innodb::Page::Index < Innodb::Page
222
240
  info = (bits2 & 0xf0) >> 4
223
241
  header[:min_rec] = (info & RECORD_INFO_MIN_REC_FLAG) != 0
224
242
  header[:deleted] = (info & RECORD_INFO_DELETED_FLAG) != 0
243
+ case header[:type]
244
+ when :conventional, :node_pointer:
245
+ # The variable-length part of the record header contains a
246
+ # bit vector indicating NULL fields and the length of each
247
+ # non-NULL variable-length field.
248
+ if record_format
249
+ header[:null_bitmap] = nbmap = record_null_bitmap(c)
250
+ header[:variable_length] = record_variable_length(c, nbmap)
251
+ end
252
+ end
225
253
  header
226
254
  when :redundant
227
255
  raise "Not implemented"
228
256
  end
229
257
  end
230
258
 
259
+ # Return an array indicating which fields are null.
260
+ def record_null_bitmap(cursor)
261
+ fields = (record_format[:key] + record_format[:row])
262
+
263
+ # The number of bits in the bitmap is the number of nullable fields.
264
+ size = fields.count do |f| f.nullable end
265
+
266
+ # There is no bitmap if there are no nullable fields.
267
+ return nil unless size > 0
268
+
269
+ # To simplify later checks, expand bitmap to one for each field.
270
+ bitmap = Array.new(fields.size, false)
271
+
272
+ null_bit_array = cursor.get_bit_array(size).reverse!
273
+
274
+ # For every nullable field, set whether the field is actually null.
275
+ fields.each do |f|
276
+ bitmap[f.position] = f.nullable ? (null_bit_array.shift == 1) : false
277
+ end
278
+
279
+ return bitmap
280
+ end
281
+
282
+ # Return an array containing the length of each variable-length field.
283
+ def record_variable_length(cursor, null_bitmap)
284
+ fields = (record_format[:key] + record_format[:row])
285
+
286
+ len_array = Array.new(fields.size, 0)
287
+
288
+ # For each non-NULL variable-length field, the record header contains
289
+ # the length in one or two bytes.
290
+ fields.each do |f|
291
+ next if f.fixed_len > 0 or null_bitmap[f.position]
292
+
293
+ len = cursor.get_uint8
294
+
295
+ # Two bytes are used only if the length exceeds 127 bytes and the
296
+ # maximum length exceeds 255 bytes.
297
+ if len > 127 and f.variable_len > 255
298
+ len = ((len & 0x3f) << 8) + cursor.get_uint8
299
+ end
300
+
301
+ len_array[f.position] = len
302
+ end
303
+
304
+ return len_array
305
+ end
306
+
231
307
  # Parse and return simple fixed-format system records, such as InnoDB's
232
308
  # internal infimum and supremum records.
233
309
  def system_record(offset)
@@ -235,6 +311,7 @@ class Innodb::Page::Index < Innodb::Page
235
311
 
236
312
  header = record_header(offset)
237
313
  {
314
+ :offset => offset,
238
315
  :header => header,
239
316
  :next => offset + header[:next],
240
317
  :data => cursor(offset).get_bytes(size_mum_record),
@@ -251,10 +328,28 @@ class Innodb::Page::Index < Innodb::Page
251
328
  @supremum ||= system_record(pos_supremum)
252
329
  end
253
330
 
331
+ # Return a set of field objects that describe the record.
332
+ def make_record_description
333
+ description = record_describer.cursor_sendable_description(self)
334
+
335
+ fields = []
336
+
337
+ (description[:key] + description[:row]).each_with_index do |d, p|
338
+ fields << Innodb::Field.new(p, *d)
339
+ end
340
+
341
+ n = description[:key].size
342
+
343
+ description[:key] = fields.slice(0 .. n-1)
344
+ description[:row] = fields.slice(n .. -1)
345
+
346
+ return description
347
+ end
348
+
254
349
  # Return (and cache) the record format provided by an external class.
255
350
  def record_format
256
351
  if record_describer
257
- @record_format ||= record_describer.cursor_sendable_description(self)
352
+ @record_format ||= make_record_description()
258
353
  end
259
354
  end
260
355
 
@@ -271,6 +366,7 @@ class Innodb::Page::Index < Innodb::Page
271
366
  header = record_header(offset)
272
367
 
273
368
  this_record = {
369
+ :format => page_header[:format],
274
370
  :offset => offset,
275
371
  :header => header,
276
372
  :next => header[:next] == 0 ? nil : (offset + header[:next]),
@@ -282,7 +378,7 @@ class Innodb::Page::Index < Innodb::Page
282
378
  # Read the key fields present in all types of pages.
283
379
  this_record[:key] = []
284
380
  record_format[:key].each do |f|
285
- this_record[:key].push c.send(*f)
381
+ this_record[:key].push f.read(this_record, c)
286
382
  end
287
383
 
288
384
  # If this is a leaf page of the clustered index, read InnoDB's internal
@@ -299,7 +395,7 @@ class Innodb::Page::Index < Innodb::Page
299
395
  # Read the non-key fields.
300
396
  this_record[:row] = []
301
397
  record_format[:row].each do |f|
302
- this_record[:row].push c.send(*f)
398
+ this_record[:row].push f.read(this_record, c)
303
399
  end
304
400
  end
305
401
 
@@ -427,4 +523,4 @@ class Innodb::Page::Index < Innodb::Page
427
523
  end
428
524
  end
429
525
 
430
- Innodb::Page::SPECIALIZED_CLASSES[:INDEX] = Innodb::Page::Index
526
+ Innodb::Page::SPECIALIZED_CLASSES[:INDEX] = Innodb::Page::Index