innodb_ruby 0.9.13 → 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 +7 -0
  2. data/README.md +5 -6
  3. data/bin/innodb_log +14 -19
  4. data/bin/innodb_space +592 -745
  5. data/lib/innodb.rb +5 -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 -325
  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 -275
  16. data/lib/innodb/inode.rb +166 -124
  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 +446 -291
  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 +965 -924
  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 -164
  38. data/lib/innodb/record_describer.rb +66 -68
  39. data/lib/innodb/space.rb +386 -391
  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 +112 -21
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,341 +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)
23
- # Create a page object as a generic page.
24
- page = Innodb::Page.new(space, buffer)
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)
30
- end
31
-
32
- page
33
- end
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
36
+ end
34
37
 
35
- # Allow the specialized class to do something that isn't 'new' with this page.
36
- def self.handle(page, space, buffer)
37
- self.new(space, buffer)
38
- end
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
58
+ end
39
59
 
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)
43
- unless space && buffer
44
- raise "Page can't be initialized from nil space or buffer (space: #{space}, buffer: #{buffer})"
60
+ def self.register_specialization(page_type, specialized_class)
61
+ @specialized_classes[page_type] = specialized_class
45
62
  end
46
63
 
47
- unless space.page_size == buffer.size
48
- raise "Buffer size #{buffer.size} is different than space page size"
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)
49
68
  end
50
69
 
51
- @space = space
52
- @buffer = buffer
53
- end
70
+ def self.specialization_for?(page_type)
71
+ Innodb::Page.specialized_classes.include?(page_type)
72
+ end
54
73
 
55
- attr_reader :space
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
92
+
93
+ page
94
+ end
56
95
 
57
- # Return the page size, to eventually be able to deal with non-16kB pages.
58
- def size
59
- @size ||= @buffer.size
60
- 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
61
100
 
62
- # Return a simple string to uniquely identify this page within the space.
63
- # Be careful not to call anything which would instantiate a BufferCursor
64
- # so that we can use this method in cursor initialization.
65
- def name
66
- page_offset = BinData::Uint32be.read(@buffer.slice(4, 4))
67
- page_type = BinData::Uint16be.read(@buffer.slice(24, 2))
68
- "%i,%s" % [
69
- page_offset,
70
- PAGE_TYPE_BY_VALUE[page_type],
71
- ]
72
- 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
107
+
108
+ raise "Buffer size #{buffer.size} is different than space page size" unless space.page_size == buffer.size
73
109
 
74
- # If no block is passed, return an BufferCursor object positioned at a
75
- # specific offset. If a block is passed, create a cursor at the provided
76
- # offset and yield it to the provided block one time, and then return the
77
- # return value of the block.
78
- def cursor(buffer_offset)
79
- new_cursor = BufferCursor.new(@buffer, buffer_offset)
80
- new_cursor.push_name("space[#{space.name}]")
81
- new_cursor.push_name("page[#{name}]")
82
-
83
- if block_given?
84
- # Call the block once and return its return value.
85
- yield new_cursor
86
- else
87
- # Return the cursor itself.
88
- new_cursor
110
+ @space = space
111
+ @buffer = buffer
112
+ @page_number = page_number
89
113
  end
90
- end
91
114
 
92
- # Return the byte offset of the start of the "fil" header, which is at the
93
- # beginning of the page. Included here primarily for completeness.
94
- def pos_fil_header
95
- 0
96
- end
115
+ attr_reader :space
97
116
 
98
- # Return the size of the "fil" header, in bytes.
99
- def size_fil_header
100
- 4 + 4 + 4 + 4 + 8 + 2 + 8 + 4
101
- end
117
+ # Return the page size, to eventually be able to deal with non-16kB pages.
118
+ def size
119
+ @size ||= @buffer.size
120
+ end
102
121
 
103
- # Return the byte offset of the start of the "fil" trailer, which is at
104
- # the end of the page.
105
- def pos_fil_trailer
106
- size - size_fil_trailer
107
- end
122
+ def default_page_size?
123
+ size == Innodb::Space::DEFAULT_PAGE_SIZE
124
+ end
108
125
 
109
- # Return the size of the "fil" trailer, in bytes.
110
- def size_fil_trailer
111
- 4 + 4
112
- 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
113
137
 
114
- # Return the position of the "body" of the page, which starts after the FIL
115
- # header.
116
- def pos_page_body
117
- pos_fil_header + size_fil_header
118
- 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
119
155
 
120
- # InnoDB Page Type constants from include/fil0fil.h.
121
- PAGE_TYPE = {
122
- :ALLOCATED => {
123
- :value => 0,
124
- :description => "Freshly allocated",
125
- :usage => "page type field has not been initialized",
126
- },
127
- :UNDO_LOG => {
128
- :value => 2,
129
- :description => "Undo log",
130
- :usage => "stores previous values of modified records",
131
- },
132
- :INODE => {
133
- :value => 3,
134
- :description => "File segment inode",
135
- :usage => "bookkeeping for file segments",
136
- },
137
- :IBUF_FREE_LIST => {
138
- :value => 4,
139
- :description => "Insert buffer free list",
140
- :usage => "bookkeeping for insert buffer free space management",
141
- },
142
- :IBUF_BITMAP => {
143
- :value => 5,
144
- :description => "Insert buffer bitmap",
145
- :usage => "bookkeeping for insert buffer writes to be merged",
146
- },
147
- :SYS => {
148
- :value => 6,
149
- :description => "System internal",
150
- :usage => "used for various purposes in the system tablespace",
151
- },
152
- :TRX_SYS => {
153
- :value => 7,
154
- :description => "Transaction system header",
155
- :usage => "bookkeeping for the transaction system in system tablespace",
156
- },
157
- :FSP_HDR => {
158
- :value => 8,
159
- :description => "File space header",
160
- :usage => "header page (page 0) for each tablespace file",
161
- },
162
- :XDES => {
163
- :value => 9,
164
- :description => "Extent descriptor",
165
- :usage => "header page for subsequent blocks of 16,384 pages",
166
- },
167
- :BLOB => {
168
- :value => 10,
169
- :description => "Uncompressed BLOB",
170
- :usage => "externally-stored uncompressed BLOB column data",
171
- },
172
- :ZBLOB => {
173
- :value => 11,
174
- :description => "First compressed BLOB",
175
- :usage => "externally-stored compressed BLOB column data, first page",
176
- },
177
- :ZBLOB2 => {
178
- :value => 12,
179
- :description => "Subsequent compressed BLOB",
180
- :usage => "externally-stored compressed BLOB column data, subsequent page",
181
- },
182
- :INDEX => {
183
- :value => 17855,
184
- :description => "B+Tree index",
185
- :usage => "table and index data stored in B+Tree structure",
186
- },
187
- }
188
-
189
- PAGE_TYPE_BY_VALUE = PAGE_TYPE.inject({}) { |h, (k, v)| h[v[:value]] = k; h }
190
-
191
- # A helper to convert "undefined" values stored in previous and next pointers
192
- # in the page header to nil.
193
- def self.maybe_undefined(value)
194
- value == 4294967295 ? nil : value
195
- 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
196
161
 
197
- # Return the "fil" header from the page, which is common for all page types.
198
- def fil_header
199
- @fil_header ||= cursor(pos_fil_header).name("fil") do |c|
200
- {
201
- :checksum => c.name("checksum") { c.get_uint32 },
202
- :offset => c.name("offset") { c.get_uint32 },
203
- :prev => c.name("prev") {
204
- Innodb::Page.maybe_undefined(c.get_uint32)
205
- },
206
- :next => c.name("next") {
207
- Innodb::Page.maybe_undefined(c.get_uint32)
208
- },
209
- :lsn => c.name("lsn") { c.get_uint64 },
210
- :type => c.name("type") { PAGE_TYPE_BY_VALUE[c.get_uint16] },
211
- :flush_lsn => c.name("flush_lsn") { c.get_uint64 },
212
- :space_id => c.name("space_id") { c.get_uint32 },
213
- }
162
+ # Return the size of the "fil" header, in bytes.
163
+ def size_fil_header
164
+ 4 + 4 + 4 + 4 + 8 + 2 + 8 + 4
214
165
  end
215
- end
216
166
 
217
- # A helper function to return the checksum from the "fil" header, for easier
218
- # access.
219
- def checksum
220
- fil_header[:checksum]
221
- 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
222
171
 
223
- # A helper function to return the page offset from the "fil" header, for
224
- # easier access.
225
- def offset
226
- fil_header[:offset]
227
- 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
228
180
 
229
- # A helper function to return the page number of the logical previous page
230
- # (from the doubly-linked list from page to page) from the "fil" header,
231
- # for easier access.
232
- def prev
233
- fil_header[:prev]
234
- 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
235
186
 
236
- # A helper function to return the page number of the logical next page
237
- # (from the doubly-linked list from page to page) from the "fil" header,
238
- # for easier access.
239
- def next
240
- fil_header[:next]
241
- end
187
+ # Return the size of the "fil" trailer, in bytes.
188
+ def size_fil_trailer
189
+ 4 + 4
190
+ end
242
191
 
243
- # A helper function to return the LSN, for easier access.
244
- def lsn
245
- fil_header[:lsn]
246
- 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
247
197
 
248
- # A helper function to return the page type from the "fil" header, for easier
249
- # access.
250
- def type
251
- fil_header[:type]
252
- end
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
201
+ end
253
202
 
254
- # Calculate the checksum of the page using InnoDB's algorithm. Two sections
255
- # of the page are checksummed separately, and then added together to produce
256
- # the final checksum.
257
- def calculate_checksum
258
- unless size == 16384
259
- raise "Checksum calculation is only supported for 16 KiB pages"
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
260
280
  end
261
281
 
262
- # Calculate the checksum of the FIL header, except for the following:
263
- # :checksum (offset 4, size 4)
264
- # :flush_lsn (offset 26, size 8)
265
- # :space_id (offset 34, size 4)
266
- c_partial_header =
267
- Innodb::Checksum.fold_enumerator(
268
- cursor(pos_fil_header + 4).each_byte_as_uint8(
269
- size_fil_header - 4 - 8 - 4
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)
286
+ end
287
+
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 }
270
300
  )
271
- )
301
+ end
302
+ end
272
303
 
273
- # Calculate the checksum of the page body, except for the FIL header and
274
- # the FIL trailer.
275
- c_page_body =
276
- Innodb::Checksum.fold_enumerator(
277
- cursor(pos_page_body).each_byte_as_uint8(
278
- size - size_fil_trailer - size_fil_header
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 }
279
310
  )
280
- )
311
+ end
312
+ end
281
313
 
282
- # Add the two checksums together, and mask the result back to 32 bits.
283
- (c_partial_header + c_page_body) & Innodb::Checksum::MAX
284
- end
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
285
321
 
286
- # Is the page corrupt? Calculate the checksum of the page and compare to
287
- # the stored checksum; return true or false.
288
- def corrupt?
289
- checksum != calculate_checksum
290
- end
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?
291
325
 
292
- def each_region
293
- unless block_given?
294
- return enum_for(:each_region)
326
+ cursor(pos_partial_page_header).each_byte_as_uint8(size_partial_page_header, &block)
295
327
  end
296
328
 
297
- yield({
298
- :offset => pos_fil_header,
299
- :length => size_fil_header,
300
- :name => :fil_header,
301
- :info => "FIL Header",
302
- })
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?
303
333
 
304
- yield({
305
- :offset => pos_fil_trailer,
306
- :length => size_fil_trailer,
307
- :name => :fil_trailer,
308
- :info => "FIL Trailer",
309
- })
334
+ cursor(pos_page_body).each_byte_as_uint8(size_page_body, &block)
335
+ end
310
336
 
311
- nil
312
- end
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?
313
340
 
314
- # Implement a custom inspect method to avoid irb printing the contents of
315
- # the page buffer, since it's very large and mostly not interesting.
316
- def inspect
317
- if fil_header
318
- "#<%s: size=%i, space_id=%i, offset=%i, type=%s, prev=%s, next=%s>" % [
319
- self.class,
320
- size,
321
- fil_header[:space_id],
322
- fil_header[:offset],
323
- fil_header[:type],
324
- fil_header[:prev] || "nil",
325
- fil_header[:next] || "nil",
326
- ]
327
- else
328
- "#<#{self.class}>"
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
350
+ end
351
+ end
352
+
353
+ def checksum_innodb?
354
+ checksum == checksum_innodb
355
+ end
356
+
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
376
+ end
377
+ end
378
+
379
+ def checksum_crc32?
380
+ checksum == checksum_crc32
381
+ end
382
+
383
+ # Is the page checksum correct?
384
+ def checksum_valid?
385
+ checksum_crc32? || checksum_innodb?
386
+ end
387
+
388
+ # Is the page checksum incorrect?
389
+ def checksum_invalid?
390
+ !checksum_valid?
391
+ end
392
+
393
+ def checksum_type
394
+ return :crc32 if checksum_crc32?
395
+ return :innodb if checksum_innodb?
396
+
397
+ nil
398
+ end
399
+
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
329
404
  end
330
- end
331
405
 
332
- # Dump the contents of a page for debugging purposes.
333
- def dump
334
- puts "#{self}:"
335
- puts
406
+ # Is the page in the doublewrite buffer?
407
+ def in_doublewrite_buffer?
408
+ space&.system_space? && space&.doublewrite_page?(offset)
409
+ end
410
+
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
416
+
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
422
+
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
336
427
 
337
- puts "fil header:"
338
- pp fil_header
339
- puts
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
433
+
434
+ # Is this an extent descriptor page (either FSP_HDR or XDES)?
435
+ def extent_descriptor?
436
+ type == :FSP_HDR || type == :XDES
437
+ end
438
+
439
+ def each_region
440
+ return enum_for(:each_region) unless block_given?
441
+
442
+ yield Region.new(
443
+ offset: pos_fil_header,
444
+ length: size_fil_header,
445
+ name: :fil_header,
446
+ info: "FIL Header"
447
+ )
448
+
449
+ yield Region.new(
450
+ offset: pos_fil_trailer,
451
+ length: size_fil_trailer,
452
+ name: :fil_trailer,
453
+ info: "FIL Trailer"
454
+ )
455
+
456
+ nil
457
+ end
458
+
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
481
+
482
+ # Dump the contents of a page for debugging purposes.
483
+ def dump
484
+ puts "#{self}:"
485
+ puts
486
+
487
+ puts "fil header:"
488
+ pp fil_header
489
+ puts
490
+
491
+ puts "fil trailer:"
492
+ pp fil_trailer
493
+ puts
494
+ end
340
495
  end
341
496
  end