innodb_ruby 0.9.14 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +5 -6
  3. data/bin/innodb_log +13 -18
  4. data/bin/innodb_space +654 -778
  5. data/lib/innodb/checksum.rb +26 -24
  6. data/lib/innodb/data_dictionary.rb +490 -550
  7. data/lib/innodb/data_type.rb +362 -325
  8. data/lib/innodb/field.rb +102 -89
  9. data/lib/innodb/fseg_entry.rb +22 -26
  10. data/lib/innodb/history.rb +21 -21
  11. data/lib/innodb/history_list.rb +72 -76
  12. data/lib/innodb/ibuf_bitmap.rb +36 -36
  13. data/lib/innodb/ibuf_index.rb +6 -2
  14. data/lib/innodb/index.rb +245 -276
  15. data/lib/innodb/inode.rb +166 -124
  16. data/lib/innodb/list.rb +196 -183
  17. data/lib/innodb/log.rb +139 -110
  18. data/lib/innodb/log_block.rb +100 -91
  19. data/lib/innodb/log_group.rb +53 -64
  20. data/lib/innodb/log_reader.rb +97 -96
  21. data/lib/innodb/log_record.rb +328 -279
  22. data/lib/innodb/lsn.rb +86 -81
  23. data/lib/innodb/page/blob.rb +82 -83
  24. data/lib/innodb/page/fsp_hdr_xdes.rb +174 -165
  25. data/lib/innodb/page/ibuf_bitmap.rb +34 -34
  26. data/lib/innodb/page/index.rb +965 -924
  27. data/lib/innodb/page/index_compressed.rb +34 -34
  28. data/lib/innodb/page/inode.rb +103 -112
  29. data/lib/innodb/page/sys.rb +13 -15
  30. data/lib/innodb/page/sys_data_dictionary_header.rb +81 -59
  31. data/lib/innodb/page/sys_ibuf_header.rb +45 -42
  32. data/lib/innodb/page/sys_rseg_header.rb +88 -82
  33. data/lib/innodb/page/trx_sys.rb +204 -182
  34. data/lib/innodb/page/undo_log.rb +106 -92
  35. data/lib/innodb/page.rb +417 -414
  36. data/lib/innodb/record.rb +121 -164
  37. data/lib/innodb/record_describer.rb +66 -68
  38. data/lib/innodb/space.rb +381 -413
  39. data/lib/innodb/stats.rb +33 -35
  40. data/lib/innodb/system.rb +149 -171
  41. data/lib/innodb/undo_log.rb +129 -107
  42. data/lib/innodb/undo_record.rb +255 -247
  43. data/lib/innodb/util/buffer_cursor.rb +81 -79
  44. data/lib/innodb/util/read_bits_at_offset.rb +2 -1
  45. data/lib/innodb/version.rb +2 -2
  46. data/lib/innodb/xdes.rb +144 -142
  47. data/lib/innodb.rb +4 -5
  48. metadata +100 -25
@@ -1,95 +1,109 @@
1
- # -*- encoding : utf-8 -*-
2
-
3
- class Innodb::Page::UndoLog < Innodb::Page
4
- def pos_undo_page_header
5
- pos_page_body
6
- end
7
-
8
- def size_undo_page_header
9
- 2 + 2 + 2 + Innodb::List::NODE_SIZE
10
- end
11
-
12
- def pos_undo_segment_header
13
- pos_undo_page_header + size_undo_page_header
14
- end
15
-
16
- def size_undo_segment_header
17
- 2 + 2 + Innodb::FsegEntry::SIZE + Innodb::List::BASE_NODE_SIZE
18
- end
19
-
20
- def pos_undo_logs
21
- pos_undo_segment_header + size_undo_segment_header
22
- end
23
-
24
- UNDO_PAGE_TYPES = {
25
- 1 => :insert,
26
- 2 => :update,
27
- }
28
-
29
- UNDO_SEGMENT_STATES = {
30
- 1 => :active,
31
- 2 => :cached,
32
- 3 => :to_free,
33
- 4 => :to_purge,
34
- 5 => :prepared,
35
- }
36
-
37
- def undo_page_header
38
- @undo_page_header ||=
39
- cursor(pos_undo_page_header).name("undo_page_header") do |c|
40
- {
41
- :type => c.name("type") { UNDO_PAGE_TYPES[c.get_uint16] },
42
- :latest_log_record_offset => c.name("latest_log_record_offset") { c.get_uint16 },
43
- :free_offset => c.name("free_offset") { c.get_uint16 },
44
- :page_list_node => c.name("page_list") { Innodb::List.get_node(c) },
45
- }
1
+ # frozen_string_literal: true
2
+
3
+ module Innodb
4
+ class Page
5
+ class UndoLog < Page
6
+ specialization_for :UNDO_LOG
7
+
8
+ PageHeader = Struct.new(
9
+ :type,
10
+ :latest_log_record_offset,
11
+ :free_offset,
12
+ :page_list_node,
13
+ keyword_init: true
14
+ )
15
+
16
+ SegmentHeader = Struct.new(
17
+ :state,
18
+ :last_log_offset,
19
+ :fseg,
20
+ :page_list,
21
+ keyword_init: true
22
+ )
23
+
24
+ def pos_undo_page_header
25
+ pos_page_body
26
+ end
27
+
28
+ def size_undo_page_header
29
+ 2 + 2 + 2 + Innodb::List::NODE_SIZE
30
+ end
31
+
32
+ def pos_undo_segment_header
33
+ pos_undo_page_header + size_undo_page_header
34
+ end
35
+
36
+ def size_undo_segment_header
37
+ 2 + 2 + Innodb::FsegEntry::SIZE + Innodb::List::BASE_NODE_SIZE
38
+ end
39
+
40
+ def pos_undo_logs
41
+ pos_undo_segment_header + size_undo_segment_header
42
+ end
43
+
44
+ UNDO_PAGE_TYPES = {
45
+ 1 => :insert,
46
+ 2 => :update,
47
+ }.freeze
48
+
49
+ UNDO_SEGMENT_STATES = {
50
+ 1 => :active,
51
+ 2 => :cached,
52
+ 3 => :to_free,
53
+ 4 => :to_purge,
54
+ 5 => :prepared,
55
+ }.freeze
56
+
57
+ def undo_page_header
58
+ @undo_page_header ||= cursor(pos_undo_page_header).name("undo_page_header") do |c|
59
+ PageHeader.new(
60
+ type: c.name("type") { UNDO_PAGE_TYPES[c.read_uint16] },
61
+ latest_log_record_offset: c.name("latest_log_record_offset") { c.read_uint16 },
62
+ free_offset: c.name("free_offset") { c.read_uint16 },
63
+ page_list_node: c.name("page_list") { Innodb::List.get_node(c) }
64
+ )
65
+ end
66
+ end
67
+
68
+ def prev_address
69
+ undo_page_header[:page_list_node][:prev]
70
+ end
71
+
72
+ def next_address
73
+ undo_page_header[:page_list_node][:next]
74
+ end
75
+
76
+ def undo_segment_header
77
+ @undo_segment_header ||= cursor(pos_undo_segment_header).name("undo_segment_header") do |c|
78
+ SegmentHeader.new(
79
+ state: c.name("state") { UNDO_SEGMENT_STATES[c.read_uint16] },
80
+ last_log_offset: c.name("last_log_offset") { c.read_uint16 },
81
+ fseg: c.name("fseg") { Innodb::FsegEntry.get_inode(@space, c) },
82
+ page_list: c.name("page_list") { Innodb::List::UndoPage.new(@space, Innodb::List.get_base_node(c)) }
83
+ )
84
+ end
85
+ end
86
+
87
+ def undo_log(pos)
88
+ Innodb::UndoLog.new(self, pos)
89
+ end
90
+
91
+ # Dump the contents of a page for debugging purposes.
92
+ def dump
93
+ super
94
+
95
+ puts "undo page header:"
96
+ pp undo_page_header
97
+ puts
98
+
99
+ puts "undo segment header:"
100
+ pp undo_segment_header
101
+ puts
102
+
103
+ puts "last undo log:"
104
+ undo_log(undo_segment_header[:last_log_offset]).dump unless undo_segment_header[:last_log_offset].zero?
105
+ puts
106
+ end
46
107
  end
47
108
  end
48
-
49
- def prev_address
50
- undo_page_header[:page_list_node][:prev]
51
- end
52
-
53
- def next_address
54
- undo_page_header[:page_list_node][:next]
55
- end
56
-
57
- def undo_segment_header
58
- @undo_segment_header ||=
59
- cursor(pos_undo_segment_header).name("undo_segment_header") do |c|
60
- {
61
- :state => c.name("state") { UNDO_SEGMENT_STATES[c.get_uint16] },
62
- :last_log_offset => c.name("last_log_offset") { c.get_uint16 },
63
- :fseg => c.name("fseg") { Innodb::FsegEntry.get_inode(@space, c) },
64
- :page_list => c.name("page_list") {
65
- Innodb::List::UndoPage.new(@space, Innodb::List.get_base_node(c))
66
- },
67
- }
68
- end
69
- end
70
-
71
- def undo_log(pos)
72
- Innodb::UndoLog.new(self, pos)
73
- end
74
-
75
- # Dump the contents of a page for debugging purposes.
76
- def dump
77
- super
78
-
79
- puts "undo page header:"
80
- pp undo_page_header
81
- puts
82
-
83
- puts "undo segment header:"
84
- pp undo_segment_header
85
- puts
86
-
87
- puts "last undo log:"
88
- if undo_segment_header[:last_log_offset] != 0
89
- undo_log(undo_segment_header[:last_log_offset]).dump
90
- end
91
- puts
92
- end
93
109
  end
94
-
95
- Innodb::Page::SPECIALIZED_CLASSES[:UNDO_LOG] = Innodb::Page::UndoLog
data/lib/innodb/page.rb CHANGED
@@ -1,493 +1,496 @@
1
- # -*- encoding : utf-8 -*-
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
2
4
 
3
5
  # A generic class for any type of page, which handles reading the common
4
6
  # FIL header and trailer, and can handle (via #parse) dispatching to a more
5
7
  # specialized class depending on page type (which comes from the FIL header).
6
8
  # A page being handled by Innodb::Page indicates that its type is not currently
7
9
  # handled by any more specialized class.
8
- class Innodb::Page
9
- # A hash of page types to specialized classes to handle them. Normally
10
- # subclasses will register themselves in this list.
11
- SPECIALIZED_CLASSES = {}
12
-
13
- # Load a page as a generic page in order to make the "fil" header accessible,
14
- # and then attempt to hand off the page to a specialized class to be
15
- # re-parsed if possible. If there is no specialized class for this type
16
- # of page, return the generic object.
17
- #
18
- # This could be optimized to reach into the page buffer and efficiently
19
- # extract the page type in order to avoid throwing away a generic
20
- # Innodb::Page object when parsing every specialized page, but this is
21
- # a bit cleaner, and we're not particularly performance sensitive.
22
- def self.parse(space, buffer, page_number=nil)
23
- # Create a page object as a generic page.
24
- page = Innodb::Page.new(space, buffer, page_number)
25
-
26
- # If there is a specialized class available for this page type, re-create
27
- # the page object using that specialized class.
28
- if specialized_class = SPECIALIZED_CLASSES[page.type]
29
- page = specialized_class.handle(page, space, buffer, page_number)
30
- end
31
-
32
- page
33
- end
34
-
35
- # Allow the specialized class to do something that isn't 'new' with this page.
36
- def self.handle(page, space, buffer, page_number=nil)
37
- self.new(space, buffer, page_number)
38
- end
39
-
40
- # Initialize a page by passing in a buffer containing the raw page contents.
41
- # The buffer size should match the space's page size.
42
- def initialize(space, buffer, page_number=nil)
43
- unless space && buffer
44
- raise "Page can't be initialized from nil space or buffer (space: #{space}, buffer: #{buffer})"
10
+ module Innodb
11
+ class Page
12
+ extend Forwardable
13
+
14
+ Address = Struct.new(
15
+ :page,
16
+ :offset,
17
+ keyword_init: true
18
+ )
19
+
20
+ FilHeader = Struct.new(
21
+ :checksum,
22
+ :offset,
23
+ :prev,
24
+ :next,
25
+ :lsn,
26
+ :type,
27
+ :flush_lsn,
28
+ :space_id,
29
+ keyword_init: true
30
+ )
31
+
32
+ class FilHeader
33
+ def lsn_low32
34
+ lsn & 0xffffffff
35
+ end
45
36
  end
46
37
 
47
- unless space.page_size == buffer.size
48
- raise "Buffer size #{buffer.size} is different than space page size"
38
+ FilTrailer = Struct.new(
39
+ :checksum,
40
+ :lsn_low32,
41
+ keyword_init: true
42
+ )
43
+
44
+ Region = Struct.new(
45
+ :offset,
46
+ :length, # rubocop:disable Lint/StructNewOverride
47
+ :name,
48
+ :info,
49
+ keyword_init: true
50
+ )
51
+
52
+ # A hash of page types to specialized classes to handle them. Normally
53
+ # subclasses will register themselves in this list.
54
+ @specialized_classes = {}
55
+
56
+ class << self
57
+ attr_reader :specialized_classes
49
58
  end
50
59
 
51
- @space = space
52
- @buffer = buffer
53
- @page_number = page_number
54
- end
55
-
56
- attr_reader :space
57
-
58
- # Return the page size, to eventually be able to deal with non-16kB pages.
59
- def size
60
- @size ||= @buffer.size
61
- end
62
-
63
- # Return a simple string to uniquely identify this page within the space.
64
- # Be careful not to call anything which would instantiate a BufferCursor
65
- # so that we can use this method in cursor initialization.
66
- def name
67
- page_offset = BinData::Uint32be.read(@buffer.slice(4, 4))
68
- page_type = BinData::Uint16be.read(@buffer.slice(24, 2))
69
- "%i,%s" % [
70
- page_offset,
71
- PAGE_TYPE_BY_VALUE[page_type],
72
- ]
73
- end
74
-
75
- # If no block is passed, return an BufferCursor object positioned at a
76
- # specific offset. If a block is passed, create a cursor at the provided
77
- # offset and yield it to the provided block one time, and then return the
78
- # return value of the block.
79
- def cursor(buffer_offset)
80
- new_cursor = BufferCursor.new(@buffer, buffer_offset)
81
- new_cursor.push_name("space[#{space.name}]")
82
- new_cursor.push_name("page[#{name}]")
83
-
84
- if block_given?
85
- # Call the block once and return its return value.
86
- yield new_cursor
87
- else
88
- # Return the cursor itself.
89
- new_cursor
60
+ def self.register_specialization(page_type, specialized_class)
61
+ @specialized_classes[page_type] = specialized_class
90
62
  end
91
- end
92
63
 
93
- # Return the byte offset of the start of the "fil" header, which is at the
94
- # beginning of the page. Included here primarily for completeness.
95
- def pos_fil_header
96
- 0
97
- end
64
+ def self.specialization_for(page_type)
65
+ # This needs to intentionally use Innodb::Page because we need to register
66
+ # in the class instance variable in *that* class.
67
+ Innodb::Page.register_specialization(page_type, self)
68
+ end
98
69
 
99
- # Return the size of the "fil" header, in bytes.
100
- def size_fil_header
101
- 4 + 4 + 4 + 4 + 8 + 2 + 8 + 4
102
- end
70
+ def self.specialization_for?(page_type)
71
+ Innodb::Page.specialized_classes.include?(page_type)
72
+ end
103
73
 
104
- # The start of the checksummed portion of the file header.
105
- def pos_partial_page_header
106
- pos_fil_header + 4
107
- end
74
+ # Load a page as a generic page in order to make the "fil" header accessible,
75
+ # and then attempt to hand off the page to a specialized class to be
76
+ # re-parsed if possible. If there is no specialized class for this type
77
+ # of page, return the generic object.
78
+ #
79
+ # This could be optimized to reach into the page buffer and efficiently
80
+ # extract the page type in order to avoid throwing away a generic
81
+ # Innodb::Page object when parsing every specialized page, but this is
82
+ # a bit cleaner, and we're not particularly performance sensitive.
83
+ def self.parse(space, buffer, page_number = nil)
84
+ # Create a page object as a generic page.
85
+ page = Innodb::Page.new(space, buffer, page_number)
86
+
87
+ # If there is a specialized class available for this page type, re-create
88
+ # the page object using that specialized class.
89
+ if (specialized_class = specialized_classes[page.type])
90
+ page = specialized_class.handle(page, space, buffer, page_number)
91
+ end
108
92
 
109
- # The size of the portion of the fil header that is included in the
110
- # checksum. Exclude the following:
111
- # :checksum (offset 4, size 4)
112
- # :flush_lsn (offset 26, size 8)
113
- # :space_id (offset 34, size 4)
114
- def size_partial_page_header
115
- size_fil_header - 4 - 8 - 4
116
- end
93
+ page
94
+ end
117
95
 
118
- # Return the byte offset of the start of the "fil" trailer, which is at
119
- # the end of the page.
120
- def pos_fil_trailer
121
- size - size_fil_trailer
122
- end
96
+ # Allow the specialized class to do something that isn't 'new' with this page.
97
+ def self.handle(_page, space, buffer, page_number = nil)
98
+ new(space, buffer, page_number)
99
+ end
123
100
 
124
- # Return the size of the "fil" trailer, in bytes.
125
- def size_fil_trailer
126
- 4 + 4
127
- end
101
+ # Initialize a page by passing in a buffer containing the raw page contents.
102
+ # The buffer size should match the space's page size.
103
+ def initialize(space, buffer, page_number = nil)
104
+ unless space && buffer
105
+ raise "Page can't be initialized from nil space or buffer (space: #{space}, buffer: #{buffer})"
106
+ end
128
107
 
129
- # Return the position of the "body" of the page, which starts after the FIL
130
- # header.
131
- def pos_page_body
132
- pos_fil_header + size_fil_header
133
- end
108
+ raise "Buffer size #{buffer.size} is different than space page size" unless space.page_size == buffer.size
134
109
 
135
- # Return the size of the page body, excluding the header and trailer.
136
- def size_page_body
137
- size - size_fil_trailer - size_fil_header
138
- end
110
+ @space = space
111
+ @buffer = buffer
112
+ @page_number = page_number
113
+ end
139
114
 
140
- # InnoDB Page Type constants from include/fil0fil.h.
141
- PAGE_TYPE = {
142
- :ALLOCATED => {
143
- :value => 0,
144
- :description => "Freshly allocated",
145
- :usage => "page type field has not been initialized",
146
- },
147
- :UNDO_LOG => {
148
- :value => 2,
149
- :description => "Undo log",
150
- :usage => "stores previous values of modified records",
151
- },
152
- :INODE => {
153
- :value => 3,
154
- :description => "File segment inode",
155
- :usage => "bookkeeping for file segments",
156
- },
157
- :IBUF_FREE_LIST => {
158
- :value => 4,
159
- :description => "Insert buffer free list",
160
- :usage => "bookkeeping for insert buffer free space management",
161
- },
162
- :IBUF_BITMAP => {
163
- :value => 5,
164
- :description => "Insert buffer bitmap",
165
- :usage => "bookkeeping for insert buffer writes to be merged",
166
- },
167
- :SYS => {
168
- :value => 6,
169
- :description => "System internal",
170
- :usage => "used for various purposes in the system tablespace",
171
- },
172
- :TRX_SYS => {
173
- :value => 7,
174
- :description => "Transaction system header",
175
- :usage => "bookkeeping for the transaction system in system tablespace",
176
- },
177
- :FSP_HDR => {
178
- :value => 8,
179
- :description => "File space header",
180
- :usage => "header page (page 0) for each tablespace file",
181
- },
182
- :XDES => {
183
- :value => 9,
184
- :description => "Extent descriptor",
185
- :usage => "header page for subsequent blocks of 16,384 pages",
186
- },
187
- :BLOB => {
188
- :value => 10,
189
- :description => "Uncompressed BLOB",
190
- :usage => "externally-stored uncompressed BLOB column data",
191
- },
192
- :ZBLOB => {
193
- :value => 11,
194
- :description => "First compressed BLOB",
195
- :usage => "externally-stored compressed BLOB column data, first page",
196
- },
197
- :ZBLOB2 => {
198
- :value => 12,
199
- :description => "Subsequent compressed BLOB",
200
- :usage => "externally-stored compressed BLOB column data, subsequent page",
201
- },
202
- :INDEX => {
203
- :value => 17855,
204
- :description => "B+Tree index",
205
- :usage => "table and index data stored in B+Tree structure",
206
- },
207
- }
208
-
209
- PAGE_TYPE_BY_VALUE = PAGE_TYPE.inject({}) { |h, (k, v)| h[v[:value]] = k; h }
210
-
211
- # A helper to convert "undefined" values stored in previous and next pointers
212
- # in the page header to nil.
213
- def self.maybe_undefined(value)
214
- value == 4294967295 ? nil : value
215
- end
115
+ attr_reader :space
216
116
 
217
- # Return the "fil" header from the page, which is common for all page types.
218
- def fil_header
219
- @fil_header ||= cursor(pos_fil_header).name("fil_header") do |c|
220
- {
221
- :checksum => c.name("checksum") { c.get_uint32 },
222
- :offset => c.name("offset") { c.get_uint32 },
223
- :prev => c.name("prev") {
224
- Innodb::Page.maybe_undefined(c.get_uint32)
225
- },
226
- :next => c.name("next") {
227
- Innodb::Page.maybe_undefined(c.get_uint32)
228
- },
229
- :lsn => c.name("lsn") { c.get_uint64 },
230
- :type => c.name("type") { PAGE_TYPE_BY_VALUE[c.get_uint16] },
231
- :flush_lsn => c.name("flush_lsn") { c.get_uint64 },
232
- :space_id => c.name("space_id") { c.get_uint32 },
233
- }
117
+ # Return the page size, to eventually be able to deal with non-16kB pages.
118
+ def size
119
+ @size ||= @buffer.size
234
120
  end
235
- end
236
121
 
237
- # Return the "fil" trailer from the page, which is common for all page types.
238
- def fil_trailer
239
- @fil_trailer ||= cursor(pos_fil_trailer).name("fil_trailer") do |c|
240
- {
241
- :checksum => c.name("checksum") { c.get_uint32 },
242
- :lsn_low32 => c.name("lsn_low32") { c.get_uint32 },
243
- }
122
+ def default_page_size?
123
+ size == Innodb::Space::DEFAULT_PAGE_SIZE
244
124
  end
245
- end
246
-
247
- # A helper function to return the checksum from the "fil" header, for easier
248
- # access.
249
- def checksum
250
- fil_header[:checksum]
251
- end
252
125
 
253
- # A helper function to return the checksum from the "fil" trailer, for easier
254
- # access.
255
- def checksum_trailer
256
- fil_trailer[:checksum]
257
- end
126
+ # Return a simple string to uniquely identify this page within the space.
127
+ # Be careful not to call anything which would instantiate a BufferCursor
128
+ # so that we can use this method in cursor initialization.
129
+ def name
130
+ page_offset = BinData::Uint32be.read(@buffer.slice(4, 4))
131
+ page_type = BinData::Uint16be.read(@buffer.slice(24, 2))
132
+ "%i,%s" % [
133
+ page_offset,
134
+ PAGE_TYPE_BY_VALUE[page_type],
135
+ ]
136
+ end
258
137
 
259
- # A helper function to return the page offset from the "fil" header, for
260
- # easier access.
261
- def offset
262
- fil_header[:offset]
263
- end
138
+ # If no block is passed, return an BufferCursor object positioned at a
139
+ # specific offset. If a block is passed, create a cursor at the provided
140
+ # offset and yield it to the provided block one time, and then return the
141
+ # return value of the block.
142
+ def cursor(buffer_offset)
143
+ new_cursor = BufferCursor.new(@buffer, buffer_offset)
144
+ new_cursor.push_name("space[#{space.name}]")
145
+ new_cursor.push_name("page[#{name}]")
146
+
147
+ if block_given?
148
+ # Call the block once and return its return value.
149
+ yield new_cursor
150
+ else
151
+ # Return the cursor itself.
152
+ new_cursor
153
+ end
154
+ end
264
155
 
265
- # A helper function to return the page number of the logical previous page
266
- # (from the doubly-linked list from page to page) from the "fil" header,
267
- # for easier access.
268
- def prev
269
- fil_header[:prev]
270
- end
156
+ # Return the byte offset of the start of the "fil" header, which is at the
157
+ # beginning of the page. Included here primarily for completeness.
158
+ def pos_fil_header
159
+ 0
160
+ end
271
161
 
272
- # A helper function to return the page number of the logical next page
273
- # (from the doubly-linked list from page to page) from the "fil" header,
274
- # for easier access.
275
- def next
276
- fil_header[:next]
277
- end
162
+ # Return the size of the "fil" header, in bytes.
163
+ def size_fil_header
164
+ 4 + 4 + 4 + 4 + 8 + 2 + 8 + 4
165
+ end
278
166
 
279
- # A helper function to return the LSN from the page header, for easier access.
280
- def lsn
281
- fil_header[:lsn]
282
- end
167
+ # The start of the checksummed portion of the file header.
168
+ def pos_partial_page_header
169
+ pos_fil_header + 4
170
+ end
283
171
 
284
- # A helper function to return the low 32 bits of the LSN from the page header
285
- # for use in comparing to the low 32 bits stored in the trailer.
286
- def lsn_low32_header
287
- fil_header[:lsn] & 0xffffffff
288
- end
172
+ # The size of the portion of the fil header that is included in the
173
+ # checksum. Exclude the following:
174
+ # :checksum (offset 4, size 4)
175
+ # :flush_lsn (offset 26, size 8)
176
+ # :space_id (offset 34, size 4)
177
+ def size_partial_page_header
178
+ size_fil_header - 4 - 8 - 4
179
+ end
289
180
 
290
- # A helper function to return the low 32 bits of the LSN as stored in the page
291
- # trailer.
292
- def lsn_low32_trailer
293
- fil_trailer[:lsn_low32]
294
- end
181
+ # Return the byte offset of the start of the "fil" trailer, which is at
182
+ # the end of the page.
183
+ def pos_fil_trailer
184
+ size - size_fil_trailer
185
+ end
295
186
 
296
- # A helper function to return the page type from the "fil" header, for easier
297
- # access.
298
- def type
299
- fil_header[:type]
300
- end
187
+ # Return the size of the "fil" trailer, in bytes.
188
+ def size_fil_trailer
189
+ 4 + 4
190
+ end
301
191
 
302
- # A helper function to return the space ID from the "fil" header, for easier
303
- # access.
304
- def space_id
305
- fil_header[:space_id]
306
- end
192
+ # Return the position of the "body" of the page, which starts after the FIL
193
+ # header.
194
+ def pos_page_body
195
+ pos_fil_header + size_fil_header
196
+ end
307
197
 
308
- # Iterate each byte of the FIL header.
309
- def each_page_header_byte_as_uint8
310
- unless block_given?
311
- return enum_for(:each_page_header_byte_as_uint8)
198
+ # Return the size of the page body, excluding the header and trailer.
199
+ def size_page_body
200
+ size - size_fil_trailer - size_fil_header
312
201
  end
313
202
 
314
- cursor(pos_partial_page_header).
315
- each_byte_as_uint8(size_partial_page_header) do |byte|
316
- yield byte
203
+ # InnoDB Page Type constants from include/fil0fil.h.
204
+ PAGE_TYPE = {
205
+ ALLOCATED: {
206
+ value: 0,
207
+ description: "Freshly allocated",
208
+ usage: "page type field has not been initialized",
209
+ },
210
+ UNDO_LOG: {
211
+ value: 2,
212
+ description: "Undo log",
213
+ usage: "stores previous values of modified records",
214
+ },
215
+ INODE: {
216
+ value: 3,
217
+ description: "File segment inode",
218
+ usage: "bookkeeping for file segments",
219
+ },
220
+ IBUF_FREE_LIST: {
221
+ value: 4,
222
+ description: "Insert buffer free list",
223
+ usage: "bookkeeping for insert buffer free space management",
224
+ },
225
+ IBUF_BITMAP: {
226
+ value: 5,
227
+ description: "Insert buffer bitmap",
228
+ usage: "bookkeeping for insert buffer writes to be merged",
229
+ },
230
+ SYS: {
231
+ value: 6,
232
+ description: "System internal",
233
+ usage: "used for various purposes in the system tablespace",
234
+ },
235
+ TRX_SYS: {
236
+ value: 7,
237
+ description: "Transaction system header",
238
+ usage: "bookkeeping for the transaction system in system tablespace",
239
+ },
240
+ FSP_HDR: {
241
+ value: 8,
242
+ description: "File space header",
243
+ usage: "header page (page 0) for each tablespace file",
244
+ },
245
+ XDES: {
246
+ value: 9,
247
+ description: "Extent descriptor",
248
+ usage: "header page for subsequent blocks of 16,384 pages",
249
+ },
250
+ BLOB: {
251
+ value: 10,
252
+ description: "Uncompressed BLOB",
253
+ usage: "externally-stored uncompressed BLOB column data",
254
+ },
255
+ ZBLOB: {
256
+ value: 11,
257
+ description: "First compressed BLOB",
258
+ usage: "externally-stored compressed BLOB column data, first page",
259
+ },
260
+ ZBLOB2: {
261
+ value: 12,
262
+ description: "Subsequent compressed BLOB",
263
+ usage: "externally-stored compressed BLOB column data, subsequent page",
264
+ },
265
+ INDEX: {
266
+ value: 17_855,
267
+ description: "B+Tree index",
268
+ usage: "table and index data stored in B+Tree structure",
269
+ },
270
+ }.freeze
271
+
272
+ PAGE_TYPE_BY_VALUE = PAGE_TYPE.each_with_object({}) { |(k, v), h| h[v[:value]] = k }
273
+
274
+ # A page number representing "undefined" values, (4294967295).
275
+ UNDEFINED_PAGE_NUMBER = 2**32 - 1
276
+
277
+ # A helper to check if a page number is the undefined page number.
278
+ def self.undefined?(page_number)
279
+ page_number == UNDEFINED_PAGE_NUMBER
317
280
  end
318
- end
319
281
 
320
- # Iterate each byte of the page body, except for the FIL header and
321
- # the FIL trailer.
322
- def each_page_body_byte_as_uint8
323
- unless block_given?
324
- return enum_for(:each_page_body_byte_as_uint8)
282
+ # A helper to convert "undefined" values stored in previous and next pointers
283
+ # in the page header to nil.
284
+ def self.maybe_undefined(page_number)
285
+ page_number unless undefined?(page_number)
325
286
  end
326
287
 
327
- cursor(pos_page_body).
328
- each_byte_as_uint8(size_page_body) do |byte|
329
- yield byte
288
+ # Return the "fil" header from the page, which is common for all page types.
289
+ def fil_header
290
+ @fil_header ||= cursor(pos_fil_header).name("fil_header") do |c|
291
+ FilHeader.new(
292
+ checksum: c.name("checksum") { c.read_uint32 },
293
+ offset: c.name("offset") { c.read_uint32 },
294
+ prev: c.name("prev") { Innodb::Page.maybe_undefined(c.read_uint32) },
295
+ next: c.name("next") { Innodb::Page.maybe_undefined(c.read_uint32) },
296
+ lsn: c.name("lsn") { c.read_uint64 },
297
+ type: c.name("type") { PAGE_TYPE_BY_VALUE[c.read_uint16] },
298
+ flush_lsn: c.name("flush_lsn") { c.read_uint64 },
299
+ space_id: c.name("space_id") { c.read_uint32 }
300
+ )
301
+ end
330
302
  end
331
- end
332
303
 
333
- # Calculate the checksum of the page using InnoDB's algorithm.
334
- def checksum_innodb
335
- unless size == 16384
336
- raise "Checksum calculation is only supported for 16 KiB pages"
304
+ # Return the "fil" trailer from the page, which is common for all page types.
305
+ def fil_trailer
306
+ @fil_trailer ||= cursor(pos_fil_trailer).name("fil_trailer") do |c|
307
+ FilTrailer.new(
308
+ checksum: c.name("checksum") { c.read_uint32 },
309
+ lsn_low32: c.name("lsn_low32") { c.read_uint32 }
310
+ )
311
+ end
337
312
  end
338
313
 
339
- @checksum_innodb ||= begin
340
- # Calculate the InnoDB checksum of the page header.
341
- c_partial_header = Innodb::Checksum.fold_enumerator(each_page_header_byte_as_uint8)
314
+ def_delegator :fil_header, :checksum
315
+ def_delegator :fil_header, :offset
316
+ def_delegator :fil_header, :prev
317
+ def_delegator :fil_header, :next
318
+ def_delegator :fil_header, :lsn
319
+ def_delegator :fil_header, :type
320
+ def_delegator :fil_header, :space_id
342
321
 
343
- # Calculate the InnoDB checksum of the page body.
344
- c_page_body = Innodb::Checksum.fold_enumerator(each_page_body_byte_as_uint8)
322
+ # Iterate each byte of the FIL header.
323
+ def each_page_header_byte_as_uint8(&block)
324
+ return enum_for(:each_page_header_byte_as_uint8) unless block_given?
345
325
 
346
- # Add the two checksums together, and mask the result back to 32 bits.
347
- (c_partial_header + c_page_body) & Innodb::Checksum::MAX
326
+ cursor(pos_partial_page_header).each_byte_as_uint8(size_partial_page_header, &block)
348
327
  end
349
- end
350
328
 
351
- def checksum_innodb?
352
- checksum == checksum_innodb
353
- end
329
+ # Iterate each byte of the page body, except for the FIL header and
330
+ # the FIL trailer.
331
+ def each_page_body_byte_as_uint8(&block)
332
+ return enum_for(:each_page_body_byte_as_uint8) unless block_given?
354
333
 
355
- # Calculate the checksum of the page using the CRC32c algorithm.
356
- def checksum_crc32
357
- unless size == 16384
358
- raise "Checksum calculation is only supported for 16 KiB pages"
334
+ cursor(pos_page_body).each_byte_as_uint8(size_page_body, &block)
359
335
  end
360
336
 
361
- @checksum_crc32 ||= begin
362
- # Calculate the CRC32c of the page header.
363
- crc_partial_header = Digest::CRC32c.new
364
- each_page_header_byte_as_uint8 do |byte|
365
- crc_partial_header << byte.chr
337
+ # Calculate the checksum of the page using InnoDB's algorithm.
338
+ def checksum_innodb
339
+ raise "Checksum calculation is only supported for 16 KiB pages" unless default_page_size?
340
+
341
+ @checksum_innodb ||= begin
342
+ # Calculate the InnoDB checksum of the page header.
343
+ c_partial_header = Innodb::Checksum.fold_enumerator(each_page_header_byte_as_uint8)
344
+
345
+ # Calculate the InnoDB checksum of the page body.
346
+ c_page_body = Innodb::Checksum.fold_enumerator(each_page_body_byte_as_uint8)
347
+
348
+ # Add the two checksums together, and mask the result back to 32 bits.
349
+ (c_partial_header + c_page_body) & Innodb::Checksum::MAX
366
350
  end
351
+ end
352
+
353
+ def checksum_innodb?
354
+ checksum == checksum_innodb
355
+ end
367
356
 
368
- # Calculate the CRC32c of the page body.
369
- crc_page_body = Digest::CRC32c.new
370
- each_page_body_byte_as_uint8 do |byte|
371
- crc_page_body << byte.chr
357
+ # Calculate the checksum of the page using the CRC32c algorithm.
358
+ def checksum_crc32
359
+ raise "Checksum calculation is only supported for 16 KiB pages" unless default_page_size?
360
+
361
+ @checksum_crc32 ||= begin
362
+ # Calculate the CRC32c of the page header.
363
+ crc_partial_header = Digest::CRC32c.new
364
+ each_page_header_byte_as_uint8 do |byte|
365
+ crc_partial_header << byte.chr
366
+ end
367
+
368
+ # Calculate the CRC32c of the page body.
369
+ crc_page_body = Digest::CRC32c.new
370
+ each_page_body_byte_as_uint8 do |byte|
371
+ crc_page_body << byte.chr
372
+ end
373
+
374
+ # Bitwise XOR the two checksums together.
375
+ crc_partial_header.checksum ^ crc_page_body.checksum
372
376
  end
377
+ end
373
378
 
374
- # Bitwise XOR the two checksums together.
375
- crc_partial_header.checksum ^ crc_page_body.checksum
379
+ def checksum_crc32?
380
+ checksum == checksum_crc32
376
381
  end
377
- end
378
382
 
379
- def checksum_crc32?
380
- checksum == checksum_crc32
381
- end
383
+ # Is the page checksum correct?
384
+ def checksum_valid?
385
+ checksum_crc32? || checksum_innodb?
386
+ end
382
387
 
383
- # Is the page checksum correct?
384
- def checksum_valid?
385
- checksum_crc32? || checksum_innodb?
386
- end
388
+ # Is the page checksum incorrect?
389
+ def checksum_invalid?
390
+ !checksum_valid?
391
+ end
387
392
 
388
- # Is the page checksum incorrect?
389
- def checksum_invalid?
390
- !checksum_valid?
391
- end
393
+ def checksum_type
394
+ return :crc32 if checksum_crc32?
395
+ return :innodb if checksum_innodb?
392
396
 
393
- def checksum_type
394
- case
395
- when checksum_crc32?
396
- :crc32
397
- when checksum_innodb?
398
- :innodb
397
+ nil
399
398
  end
400
- end
401
399
 
402
- # Is the LSN stored in the header different from the one stored in the
403
- # trailer?
404
- def torn?
405
- lsn_low32_header != lsn_low32_trailer
406
- end
400
+ # Is the LSN stored in the header different from the one stored in the
401
+ # trailer?
402
+ def torn?
403
+ fil_header.lsn_low32 != fil_trailer.lsn_low32
404
+ end
407
405
 
408
- # Is the page in the doublewrite buffer?
409
- def in_doublewrite_buffer?
410
- space && space.system_space? && space.doublewrite_page?(offset)
411
- end
406
+ # Is the page in the doublewrite buffer?
407
+ def in_doublewrite_buffer?
408
+ space&.system_space? && space&.doublewrite_page?(offset)
409
+ end
412
410
 
413
- # Is the space ID stored in the header different from that of the space
414
- # provided when initializing this page?
415
- def misplaced_space?
416
- space && (space_id != space.space_id)
417
- end
411
+ # Is the space ID stored in the header different from that of the space
412
+ # provided when initializing this page?
413
+ def misplaced_space?
414
+ space && (space_id != space.space_id)
415
+ end
418
416
 
419
- # Is the page number stored in the header different from the page number
420
- # which was supposed to be read?
421
- def misplaced_offset?
422
- offset != @page_number
423
- end
417
+ # Is the page number stored in the header different from the page number
418
+ # which was supposed to be read?
419
+ def misplaced_offset?
420
+ offset != @page_number
421
+ end
424
422
 
425
- # Is the page misplaced in the wrong file or by offset in the file?
426
- def misplaced?
427
- !in_doublewrite_buffer? && (misplaced_space? || misplaced_offset?)
428
- end
423
+ # Is the page misplaced in the wrong file or by offset in the file?
424
+ def misplaced?
425
+ !in_doublewrite_buffer? && (misplaced_space? || misplaced_offset?)
426
+ end
429
427
 
430
- # Is the page corrupt, either due to data corruption, tearing, or in the
431
- # wrong place?
432
- def corrupt?
433
- checksum_invalid? || torn? || misplaced?
434
- end
428
+ # Is the page corrupt, either due to data corruption, tearing, or in the
429
+ # wrong place?
430
+ def corrupt?
431
+ checksum_invalid? || torn? || misplaced?
432
+ end
435
433
 
436
- def each_region
437
- unless block_given?
438
- return enum_for(:each_region)
434
+ # Is this an extent descriptor page (either FSP_HDR or XDES)?
435
+ def extent_descriptor?
436
+ type == :FSP_HDR || type == :XDES
439
437
  end
440
438
 
441
- yield({
442
- :offset => pos_fil_header,
443
- :length => size_fil_header,
444
- :name => :fil_header,
445
- :info => "FIL Header",
446
- })
439
+ def each_region
440
+ return enum_for(:each_region) unless block_given?
447
441
 
448
- yield({
449
- :offset => pos_fil_trailer,
450
- :length => size_fil_trailer,
451
- :name => :fil_trailer,
452
- :info => "FIL Trailer",
453
- })
442
+ yield Region.new(
443
+ offset: pos_fil_header,
444
+ length: size_fil_header,
445
+ name: :fil_header,
446
+ info: "FIL Header"
447
+ )
454
448
 
455
- nil
456
- end
449
+ yield Region.new(
450
+ offset: pos_fil_trailer,
451
+ length: size_fil_trailer,
452
+ name: :fil_trailer,
453
+ info: "FIL Trailer"
454
+ )
457
455
 
458
- # Implement a custom inspect method to avoid irb printing the contents of
459
- # the page buffer, since it's very large and mostly not interesting.
460
- def inspect
461
- if fil_header
462
- "#<%s: size=%i, space_id=%i, offset=%i, type=%s, prev=%s, next=%s, checksum_valid?=%s (%s), torn?=%s, misplaced?=%s>" % [
463
- self.class,
464
- size,
465
- fil_header[:space_id],
466
- fil_header[:offset],
467
- fil_header[:type],
468
- fil_header[:prev] || "nil",
469
- fil_header[:next] || "nil",
470
- checksum_valid?,
471
- checksum_type ? checksum_type : "unknown",
472
- torn?,
473
- misplaced?,
474
- ]
475
- else
476
- "#<#{self.class}>"
456
+ nil
477
457
  end
478
- end
479
458
 
480
- # Dump the contents of a page for debugging purposes.
481
- def dump
482
- puts "#{self}:"
483
- puts
459
+ def inspect_header_fields
460
+ return nil unless fil_header
461
+
462
+ %i[
463
+ size
464
+ space_id
465
+ offset
466
+ type
467
+ prev
468
+ next
469
+ checksum_valid?
470
+ checksum_type
471
+ torn?
472
+ misplaced?
473
+ ].map { |m| "#{m}=#{send(m).inspect}" }.join(", ")
474
+ end
475
+
476
+ # Implement a custom inspect method to avoid irb printing the contents of
477
+ # the page buffer, since it's very large and mostly not interesting.
478
+ def inspect
479
+ "#<#{self.class} #{inspect_header_fields || '(page header unavailable)'}>"
480
+ end
484
481
 
485
- puts "fil header:"
486
- pp fil_header
487
- puts
482
+ # Dump the contents of a page for debugging purposes.
483
+ def dump
484
+ puts "#{self}:"
485
+ puts
488
486
 
489
- puts "fil trailer:"
490
- pp fil_trailer
491
- puts
487
+ puts "fil header:"
488
+ pp fil_header
489
+ puts
490
+
491
+ puts "fil trailer:"
492
+ pp fil_trailer
493
+ puts
494
+ end
492
495
  end
493
496
  end