innodb_ruby 0.9.16 → 0.11.0

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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +5 -6
  3. data/bin/innodb_log +13 -18
  4. data/bin/innodb_space +377 -757
  5. data/lib/innodb.rb +4 -5
  6. data/lib/innodb/checksum.rb +26 -24
  7. data/lib/innodb/data_dictionary.rb +490 -550
  8. data/lib/innodb/data_type.rb +362 -326
  9. data/lib/innodb/field.rb +102 -89
  10. data/lib/innodb/fseg_entry.rb +22 -26
  11. data/lib/innodb/history.rb +21 -21
  12. data/lib/innodb/history_list.rb +72 -76
  13. data/lib/innodb/ibuf_bitmap.rb +36 -36
  14. data/lib/innodb/ibuf_index.rb +6 -2
  15. data/lib/innodb/index.rb +245 -276
  16. data/lib/innodb/inode.rb +154 -155
  17. data/lib/innodb/list.rb +191 -183
  18. data/lib/innodb/log.rb +139 -110
  19. data/lib/innodb/log_block.rb +100 -91
  20. data/lib/innodb/log_group.rb +53 -64
  21. data/lib/innodb/log_reader.rb +97 -96
  22. data/lib/innodb/log_record.rb +328 -279
  23. data/lib/innodb/lsn.rb +86 -81
  24. data/lib/innodb/page.rb +417 -414
  25. data/lib/innodb/page/blob.rb +82 -83
  26. data/lib/innodb/page/fsp_hdr_xdes.rb +174 -165
  27. data/lib/innodb/page/ibuf_bitmap.rb +34 -34
  28. data/lib/innodb/page/index.rb +964 -943
  29. data/lib/innodb/page/index_compressed.rb +34 -34
  30. data/lib/innodb/page/inode.rb +103 -112
  31. data/lib/innodb/page/sys.rb +13 -15
  32. data/lib/innodb/page/sys_data_dictionary_header.rb +81 -59
  33. data/lib/innodb/page/sys_ibuf_header.rb +45 -42
  34. data/lib/innodb/page/sys_rseg_header.rb +88 -82
  35. data/lib/innodb/page/trx_sys.rb +204 -182
  36. data/lib/innodb/page/undo_log.rb +106 -92
  37. data/lib/innodb/record.rb +121 -160
  38. data/lib/innodb/record_describer.rb +66 -68
  39. data/lib/innodb/space.rb +380 -418
  40. data/lib/innodb/stats.rb +33 -35
  41. data/lib/innodb/system.rb +149 -171
  42. data/lib/innodb/undo_log.rb +129 -107
  43. data/lib/innodb/undo_record.rb +255 -247
  44. data/lib/innodb/util/buffer_cursor.rb +81 -79
  45. data/lib/innodb/util/read_bits_at_offset.rb +2 -1
  46. data/lib/innodb/version.rb +2 -2
  47. data/lib/innodb/xdes.rb +144 -142
  48. metadata +80 -11
data/lib/innodb/lsn.rb CHANGED
@@ -1,103 +1,108 @@
1
- # -*- encoding : utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
3
  # A Log Sequence Number and its byte offset into the log group.
4
- class Innodb::LSN
5
- # The Log Sequence Number.
6
- attr_reader :lsn_no
7
-
8
- # Alias :lsn_no attribute.
9
- alias_method :no, :lsn_no
4
+ module Innodb
5
+ class LSN
6
+ # The Log Sequence Number.
7
+ attr_reader :lsn_no
8
+
9
+ # Alias :lsn_no attribute.
10
+ alias no lsn_no
11
+
12
+ # Initialize coordinates.
13
+ def initialize(lsn, offset)
14
+ @lsn_no = lsn
15
+ @lsn_offset = offset
16
+ end
10
17
 
11
- # Initialize coordinates.
12
- def initialize(lsn, offset)
13
- @lsn_no = lsn
14
- @lsn_offset = offset
15
- end
18
+ # Place LSN in a new position.
19
+ def reposition(new_lsn_no, group)
20
+ new_offset = offset_of(@lsn_no, @lsn_offset, new_lsn_no, group)
21
+ @lsn_no = new_lsn_no
22
+ @lsn_offset = new_offset
16
23
 
17
- # Place LSN in a new position.
18
- def reposition(new_lsn_no, group)
19
- new_offset = offset_of(@lsn_no, @lsn_offset, new_lsn_no, group)
20
- @lsn_no, @lsn_offset = [new_lsn_no, new_offset]
21
- end
24
+ [@lsn_no, @lsn_offset]
25
+ end
22
26
 
23
- # Advance by a given LSN amount.
24
- def advance(count_lsn_no, group)
25
- new_lsn_no = @lsn_no + count_lsn_no
26
- reposition(new_lsn_no, group)
27
- end
27
+ # Advance by a given LSN amount.
28
+ def advance(count_lsn_no, group)
29
+ new_lsn_no = @lsn_no + count_lsn_no
30
+ reposition(new_lsn_no, group)
31
+ end
28
32
 
29
- # Returns the location coordinates of this LSN.
30
- def location(group)
31
- location_of(@lsn_offset, group)
32
- end
33
+ # Returns the location coordinates of this LSN.
34
+ def location(group)
35
+ location_of(@lsn_offset, group)
36
+ end
33
37
 
34
- # Returns the LSN delta for the given amount of data.
35
- def delta(length)
36
- fragment = (@lsn_no % LOG_BLOCK_SIZE) - LOG_BLOCK_HEADER_SIZE
37
- raise "Invalid fragment #{fragment} for LSN #{@lsn_no}" unless
38
- fragment.between?(0, LOG_BLOCK_DATA_SIZE - 1)
39
- length + (fragment + length) / LOG_BLOCK_DATA_SIZE * LOG_BLOCK_FRAME_SIZE
40
- end
38
+ # Returns the LSN delta for the given amount of data.
39
+ def delta(length)
40
+ fragment = (@lsn_no % LOG_BLOCK_SIZE) - LOG_BLOCK_HEADER_SIZE
41
+ raise "Invalid fragment #{fragment} for LSN #{@lsn_no}" unless fragment.between?(0, LOG_BLOCK_DATA_SIZE - 1)
41
42
 
42
- # Whether LSN might point to log record data.
43
- def record?(group)
44
- data_offset?(@lsn_offset, group)
45
- end
43
+ length + (fragment + length) / LOG_BLOCK_DATA_SIZE * LOG_BLOCK_FRAME_SIZE
44
+ end
46
45
 
47
- private
46
+ # Whether LSN might point to log record data.
47
+ def record?(group)
48
+ data_offset?(@lsn_offset, group)
49
+ end
48
50
 
49
- # Short alias for the size of a log file header.
50
- LOG_HEADER_SIZE = Innodb::Log::LOG_HEADER_SIZE
51
+ private
51
52
 
52
- # Short aliases for the sizes of the subparts of a log block.
53
- LOG_BLOCK_SIZE = Innodb::LogBlock::BLOCK_SIZE
54
- LOG_BLOCK_HEADER_SIZE = Innodb::LogBlock::HEADER_SIZE
55
- LOG_BLOCK_TRAILER_SIZE = Innodb::LogBlock::TRAILER_SIZE
56
- LOG_BLOCK_DATA_SIZE = Innodb::LogBlock::DATA_SIZE
57
- LOG_BLOCK_FRAME_SIZE = LOG_BLOCK_HEADER_SIZE + LOG_BLOCK_TRAILER_SIZE
53
+ # Short alias for the size of a log file header.
54
+ LOG_HEADER_SIZE = Innodb::Log::LOG_HEADER_SIZE
58
55
 
59
- # Returns the coordinates of the given offset.
60
- def location_of(offset, group)
61
- log_no, log_offset = offset.divmod(group.size)
62
- block_no, block_offset = (log_offset - LOG_HEADER_SIZE).divmod(LOG_BLOCK_SIZE)
63
- [log_no, block_no, block_offset]
64
- end
56
+ # Short aliases for the sizes of the subparts of a log block.
57
+ LOG_BLOCK_SIZE = Innodb::LogBlock::BLOCK_SIZE
58
+ LOG_BLOCK_HEADER_SIZE = Innodb::LogBlock::HEADER_SIZE
59
+ LOG_BLOCK_TRAILER_SIZE = Innodb::LogBlock::TRAILER_SIZE
60
+ LOG_BLOCK_DATA_SIZE = Innodb::LogBlock::DATA_SIZE
61
+ LOG_BLOCK_FRAME_SIZE = LOG_BLOCK_HEADER_SIZE + LOG_BLOCK_TRAILER_SIZE
65
62
 
66
- # Returns the offset of the given LSN within a log group.
67
- def offset_of(lsn, offset, new_lsn, group)
68
- log_size = group.log_size
69
- group_capacity = group.capacity
70
-
71
- # Calculate the offset in LSN.
72
- if new_lsn >= lsn
73
- lsn_offset = new_lsn - lsn
74
- else
75
- lsn_offset = lsn - new_lsn
76
- lsn_offset %= group_capacity
77
- lsn_offset = group_capacity - lsn_offset
63
+ # Returns the coordinates of the given offset.
64
+ def location_of(offset, group)
65
+ log_no, log_offset = offset.divmod(group.size)
66
+ block_no, block_offset = (log_offset - LOG_HEADER_SIZE).divmod(LOG_BLOCK_SIZE)
67
+ [log_no, block_no, block_offset]
78
68
  end
79
69
 
80
- # Transpose group size offset to a group capacity offset.
81
- group_offset = offset - (LOG_HEADER_SIZE * (1 + offset / log_size))
70
+ # Returns the offset of the given LSN within a log group.
71
+ def offset_of(lsn, offset, new_lsn, group)
72
+ log_size = group.log_size
73
+ group_capacity = group.capacity
82
74
 
83
- offset = (lsn_offset + group_offset) % group_capacity
75
+ # Calculate the offset in LSN.
76
+ if new_lsn >= lsn
77
+ lsn_offset = new_lsn - lsn
78
+ else
79
+ lsn_offset = lsn - new_lsn
80
+ lsn_offset %= group_capacity
81
+ lsn_offset = group_capacity - lsn_offset
82
+ end
84
83
 
85
- # Transpose group capacity offset to a group size offset.
86
- offset + LOG_HEADER_SIZE * (1 + offset / (log_size - LOG_HEADER_SIZE))
87
- end
84
+ # Transpose group size offset to a group capacity offset.
85
+ group_offset = offset - (LOG_HEADER_SIZE * (1 + offset / log_size))
88
86
 
89
- # Whether offset points to the data area of an existing log block.
90
- def data_offset?(offset, group)
91
- log_offset = offset % group.size
92
- log_no, block_no, block_offset = location_of(offset, group)
87
+ offset = (lsn_offset + group_offset) % group_capacity
93
88
 
94
- status ||= log_no > group.logs
95
- status ||= log_offset <= LOG_HEADER_SIZE
96
- status ||= block_no < 0
97
- status ||= block_no >= group.log(log_no).blocks
98
- status ||= block_offset < Innodb::LogBlock::DATA_OFFSET
99
- status ||= block_offset >= Innodb::LogBlock::TRAILER_OFFSET
89
+ # Transpose group capacity offset to a group size offset.
90
+ offset + LOG_HEADER_SIZE * (1 + offset / (log_size - LOG_HEADER_SIZE))
91
+ end
92
+
93
+ # Whether offset points to the data area of an existing log block.
94
+ def data_offset?(offset, group)
95
+ log_offset = offset % group.size
96
+ log_no, block_no, block_offset = location_of(offset, group)
100
97
 
101
- !status
98
+ status ||= log_no > group.logs
99
+ status ||= log_offset <= LOG_HEADER_SIZE
100
+ status ||= block_no.negative?
101
+ status ||= block_no >= group.log(log_no).blocks
102
+ status ||= block_offset < Innodb::LogBlock::DATA_OFFSET
103
+ status ||= block_offset >= Innodb::LogBlock::TRAILER_OFFSET
104
+
105
+ !status
106
+ end
102
107
  end
103
108
  end
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