innodb_ruby 0.4

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.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "innodb"
4
+
5
+ filename, block_number = ARGV.shift(2)
6
+
7
+ log = Innodb::Log.new(filename)
8
+
9
+ puts "%-10s%-20s%-10s%-10s" % [
10
+ "block",
11
+ "type",
12
+ "space",
13
+ "page",
14
+ ]
15
+ log.each_block do |block_number, block|
16
+ if block.record
17
+ puts "%-10i%-20s%-10i%-10i" % [
18
+ block_number,
19
+ block.record[:type],
20
+ block.record[:space],
21
+ block.record[:page_number],
22
+ ]
23
+ #block.dump
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pp"
4
+ require "innodb"
5
+
6
+ file, page_number = ARGV.shift(2)
7
+
8
+ space = Innodb::Space.new(file)
9
+ #space.page(page_number).dump
10
+ #exit
11
+
12
+ page_types = Hash.new(0)
13
+ page_levels = Hash.new(0)
14
+ records_per_level = {}
15
+ puts "%-8s%-8s%-8s%-8s" % ["page", "index", "level", "data"]
16
+ space.each_page do |page_number, page|
17
+ page_types[page.type] += 1
18
+ if page.type == :INDEX
19
+ puts "%-8i%-8i%-8i%-8i" % [
20
+ page_number,
21
+ page.ph[:index_id],
22
+ page.ph[:level],
23
+ page.record_space,
24
+ ]
25
+ #page_levels[page.ph[:index_id]] ||= Hash.new(0)
26
+ #page_levels[page.ph[:index_id]][page.ph[:level]] += 1
27
+ #records_per_level[page.ph[:level]] ||= Hash.new(0)
28
+ #records_per_level[page.ph[:level]][page.ph[:n_recs]] += 1
29
+ end
30
+ if page.type == :ALLOCATED
31
+ puts "%-8i%-8i%-8i%-8i" % [ page_number, 0, 0, 0 ]
32
+ end
33
+ end
data/lib/innodb.rb ADDED
@@ -0,0 +1,8 @@
1
+ # A set of classes for parsing and working with InnoDB data files.
2
+ module Innodb; end
3
+
4
+ require "innodb/version"
5
+ require "innodb/page"
6
+ require "innodb/space"
7
+ require "innodb/log_block"
8
+ require "innodb/log"
@@ -0,0 +1,148 @@
1
+ # A cursor to walk through InnoDB data structures to read fields.
2
+ class Innodb::Cursor
3
+ def initialize(buffer, offset)
4
+ @buffer = buffer
5
+ @cursor = [ offset ]
6
+ @direction = :forward
7
+ end
8
+
9
+ # Set the direction of the cursor to "forward".
10
+ def forward
11
+ @direction = :forward
12
+ self
13
+ end
14
+
15
+ # Set the direction of the cursor to "backward".
16
+ def backward
17
+ @direction = :backward
18
+ self
19
+ end
20
+
21
+ # Return the position of the current cursor.
22
+ def position
23
+ @cursor[0]
24
+ end
25
+
26
+ # Move the current cursor to a new absolute position.
27
+ def seek(offset)
28
+ @cursor[0] = offset if offset
29
+ self
30
+ end
31
+
32
+ # Adjust the current cursor to a new relative position.
33
+ def adjust(relative_offset)
34
+ @cursor[0] += relative_offset
35
+ self
36
+ end
37
+
38
+ # Save the current cursor position and start a new (nested, stacked) cursor.
39
+ def push(offset=nil)
40
+ @cursor.unshift(offset.nil? ? @cursor[0] : offset)
41
+ self
42
+ end
43
+
44
+ # Restore the last cursor position.
45
+ def pop
46
+ raise "No cursors to pop" unless @cursor.size > 1
47
+ @cursor.shift
48
+ self
49
+ end
50
+
51
+ # Execute a block and restore the cursor to the previous position after
52
+ # the block returns. Return the block's return value after restoring the
53
+ # cursor.
54
+ def peek
55
+ raise "No block given" unless block_given?
56
+ push
57
+ result = yield
58
+ pop
59
+ result
60
+ end
61
+
62
+ # Read a number of bytes forwards or backwards from the current cursor
63
+ # position and adjust the cursor position by that amount, optionally
64
+ # unpacking the data using the provided type.
65
+ def read_and_advance(length, type=nil)
66
+ data = nil
67
+ #print "data(#{@cursor[0]}..."
68
+ case @direction
69
+ when :forward
70
+ data = @buffer.data(@cursor[0], length)
71
+ adjust(length)
72
+ when :backward
73
+ adjust(-length)
74
+ data = @buffer.data(@cursor[0], length)
75
+ end
76
+ #puts "#{@cursor[0]}) = #{data.bytes.map { |n| "%02x" % n }.join}"
77
+ type ? data.unpack(type).first : data
78
+ end
79
+
80
+ # Return raw bytes.
81
+ def get_bytes(length)
82
+ read_and_advance(length)
83
+ end
84
+
85
+ # Return a big-endian unsigned 8-bit integer.
86
+ def get_uint8(offset=nil)
87
+ seek(offset)
88
+ read_and_advance(1, "C")
89
+ end
90
+
91
+ # Return a big-endian unsigned 16-bit integer.
92
+ def get_uint16(offset=nil)
93
+ seek(offset)
94
+ read_and_advance(2, "n")
95
+ end
96
+
97
+ # Return a big-endian signed 16-bit integer.
98
+ def get_sint16(offset=nil)
99
+ seek(offset)
100
+ uint = read_and_advance(2, "n")
101
+ (uint & 32768) == 0 ? uint : -(uint ^ 65535) - 1
102
+ end
103
+
104
+ # Return a big-endian unsigned 24-bit integer.
105
+ def get_uint24(offset=nil)
106
+ seek(offset)
107
+ # Ruby 1.8 doesn't support big-endian 24-bit unpack; unpack as one
108
+ # 8-bit and one 16-bit big-endian instead.
109
+ high, low = read_and_advance(3).unpack("nC")
110
+ (high << 8) | low
111
+ end
112
+
113
+ # Return a big-endian unsigned 32-bit integer.
114
+ def get_uint32(offset=nil)
115
+ seek(offset)
116
+ read_and_advance(4, "N")
117
+ end
118
+
119
+ # Return a big-endian unsigned 64-bit integer.
120
+ def get_uint64(offset=nil)
121
+ seek(offset)
122
+ # Ruby 1.8 doesn't support big-endian quad-word unpack; unpack as two
123
+ # 32-bit big-endian instead.
124
+ high, low = read_and_advance(8).unpack("NN")
125
+ (high << 32) | low
126
+ end
127
+
128
+ # Return an InnoDB-compressed unsigned 32-bit integer.
129
+ def get_ic_uint32
130
+ flag = peek { get_uint8 }
131
+
132
+ case
133
+ when flag < 0x80
134
+ get_uint8
135
+ when flag < 0xc0
136
+ get_uint16 & 0x7fff
137
+ when flag < 0xe0
138
+ get_uint24 & 0x3fffff
139
+ when flag < 0xf0
140
+ get_uint32 & 0x1fffffff
141
+ when flag == 0xf0
142
+ adjust(+1) # Skip the flag.
143
+ get_uint32
144
+ else
145
+ raise "Invalid flag #{flag.to_s(16)} seen"
146
+ end
147
+ end
148
+ end
data/lib/innodb/log.rb ADDED
@@ -0,0 +1,43 @@
1
+ # An InnoDB transaction log file.
2
+ class Innodb::Log
3
+ HEADER_SIZE = 4 * Innodb::LogBlock::BLOCK_SIZE
4
+ HEADER_START = 0
5
+ DATA_START = HEADER_START + HEADER_SIZE
6
+
7
+ #define LOG_GROUP_ID 0 /* log group number */
8
+ #define LOG_FILE_START_LSN 4 /* lsn of the start of data in this
9
+ #define LOG_FILE_NO 12 /* 4-byte archived log file number;
10
+ #define LOG_FILE_WAS_CREATED_BY_HOT_BACKUP 16
11
+ #define LOG_FILE_ARCH_COMPLETED OS_FILE_LOG_BLOCK_SIZE
12
+ #define LOG_FILE_END_LSN (OS_FILE_LOG_BLOCK_SIZE + 4)
13
+ #define LOG_CHECKPOINT_1 OS_FILE_LOG_BLOCK_SIZE
14
+ #define LOG_CHECKPOINT_2 (3 * OS_FILE_LOG_BLOCK_SIZE)
15
+ #define LOG_FILE_HDR_SIZE (4 * OS_FILE_LOG_BLOCK_SIZE)
16
+
17
+ # Open a log file.
18
+ def initialize(file)
19
+ @file = File.open(file)
20
+ @size = @file.stat.size
21
+ @blocks = ((@size - DATA_START) / Innodb::LogBlock::BLOCK_SIZE)
22
+ end
23
+
24
+ # Return a log block with a given block number as an InnoDB::LogBlock object.
25
+ # Blocks are numbered after the log file header, starting from 0.
26
+ def block(block_number)
27
+ offset = DATA_START + (block_number.to_i * Innodb::LogBlock::BLOCK_SIZE)
28
+ return nil unless offset < @size
29
+ return nil unless (offset + Innodb::LogBlock::BLOCK_SIZE) <= @size
30
+ @file.seek(offset)
31
+ block_data = @file.read(Innodb::LogBlock::BLOCK_SIZE)
32
+ Innodb::LogBlock.new(block_data)
33
+ end
34
+
35
+ # Iterate through all log blocks, returning the block number and an
36
+ # InnoDB::LogBlock object for each block.
37
+ def each_block
38
+ (0...@blocks).each do |block_number|
39
+ current_block = block(block_number)
40
+ yield block_number, current_block if current_block
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,182 @@
1
+ require "innodb/cursor"
2
+ require "pp"
3
+
4
+ # An InnoDB transaction log block.
5
+ class Innodb::LogBlock
6
+ # Log blocks are fixed-length at 512 bytes in InnoDB.
7
+ BLOCK_SIZE = 512
8
+
9
+ HEADER_SIZE = 12
10
+ HEADER_START = 0
11
+
12
+ TRAILER_SIZE = 4
13
+ TRAILER_START = BLOCK_SIZE - TRAILER_SIZE
14
+
15
+ RECORD_START = HEADER_START + HEADER_SIZE
16
+
17
+ # Header:
18
+ #define LOG_BLOCK_HDR_NO 0 /* block number which must be > 0 and
19
+ #define LOG_BLOCK_HDR_DATA_LEN 4 /* number of bytes of log written to
20
+ #define LOG_BLOCK_FIRST_REC_GROUP 6 /* offset of the first start of an
21
+ #define LOG_BLOCK_CHECKPOINT_NO 8 /* 4 lower bytes of the value of
22
+
23
+ # Trailer:
24
+ #define LOG_BLOCK_CHECKSUM 0 /* 4 byte checksum of the log block
25
+
26
+ #/* Offsets for a checkpoint field */
27
+ #define LOG_CHECKPOINT_NO 0
28
+ #define LOG_CHECKPOINT_LSN 8
29
+ #define LOG_CHECKPOINT_OFFSET 16
30
+ #define LOG_CHECKPOINT_LOG_BUF_SIZE 20
31
+ #define LOG_CHECKPOINT_ARCHIVED_LSN 24
32
+ #define LOG_CHECKPOINT_GROUP_ARRAY 32
33
+
34
+ # Initialize a log block by passing in a 512-byte buffer containing the raw
35
+ # log block contents.
36
+ def initialize(buffer)
37
+ unless buffer.size == BLOCK_SIZE
38
+ raise "Log block buffer provided was not #{BLOCK_SIZE} bytes"
39
+ end
40
+
41
+ @buffer = buffer
42
+ end
43
+
44
+ # A helper function to return bytes from the log block buffer based on offset
45
+ # and length, both in bytes.
46
+ def data(offset, length)
47
+ @buffer[offset...(offset + length)]
48
+ end
49
+
50
+ # Return an Innodb::Cursor object positioned at a specific offset.
51
+ def cursor(offset)
52
+ Innodb::Cursor.new(self, offset)
53
+ end
54
+
55
+ # Return the log block header.
56
+ def header
57
+ @header ||= begin
58
+ c = cursor(HEADER_START)
59
+ {
60
+ :block => c.get_uint32,
61
+ :data_length => c.get_uint16,
62
+ :first_rec_group => c.get_uint16,
63
+ :checkpoint_no => c.get_uint32,
64
+ }
65
+ end
66
+ end
67
+
68
+ # Return the log block trailer.
69
+ def trailer
70
+ @trailer ||= begin
71
+ c = cursor(TRAILER_START)
72
+ {
73
+ :checksum => c.get_uint32,
74
+ }
75
+ end
76
+ end
77
+
78
+ # The constants used by InnoDB for identifying different log record types.
79
+ RECORD_TYPES = {
80
+ 1 => :MLOG_1BYTE,
81
+ 2 => :MLOG_2BYTE,
82
+ 4 => :MLOG_4BYTE,
83
+ 8 => :MLOG_8BYTE,
84
+ 9 => :REC_INSERT,
85
+ 10 => :REC_CLUST_DELETE_MARK,
86
+ 11 => :REC_SEC_DELETE_MARK,
87
+ 13 => :REC_UPDATE_IN_PLACE,
88
+ 14 => :REC_DELETE,
89
+ 15 => :LIST_END_DELETE,
90
+ 16 => :LIST_START_DELETE,
91
+ 17 => :LIST_END_COPY_CREATED,
92
+ 18 => :PAGE_REORGANIZE,
93
+ 19 => :PAGE_CREATE,
94
+ 20 => :UNDO_INSERT,
95
+ 21 => :UNDO_ERASE_END,
96
+ 22 => :UNDO_INIT,
97
+ 23 => :UNDO_HDR_DISCARD,
98
+ 24 => :UNDO_HDR_REUSE,
99
+ 25 => :UNDO_HDR_CREATE,
100
+ 26 => :REC_MIN_MARK,
101
+ 27 => :IBUF_BITMAP_INIT,
102
+ 28 => :LSN,
103
+ 29 => :INIT_FILE_PAGE,
104
+ 30 => :WRITE_STRING,
105
+ 31 => :MULTI_REC_END,
106
+ 32 => :DUMMY_RECORD,
107
+ 33 => :FILE_CREATE,
108
+ 34 => :FILE_RENAME,
109
+ 35 => :FILE_DELETE,
110
+ 36 => :COMP_REC_MIN_MARK,
111
+ 37 => :COMP_PAGE_CREATE,
112
+ 38 => :COMP_REC_INSERT,
113
+ 39 => :COMP_REC_CLUST_DELETE_MARK,
114
+ 40 => :COMP_REC_SEC_DELETE_MARK,
115
+ 41 => :COMP_REC_UPDATE_IN_PLACE,
116
+ 42 => :COMP_REC_DELETE,
117
+ 43 => :COMP_LIST_END_DELETE,
118
+ 44 => :COMP_LIST_START_DELETE,
119
+ 45 => :COMP_LIST_END_COPY_CREATE,
120
+ 46 => :COMP_PAGE_REORGANIZE,
121
+ 47 => :FILE_CREATE2,
122
+ 48 => :ZIP_WRITE_NODE_PTR,
123
+ 49 => :ZIP_WRITE_BLOB_PTR,
124
+ 50 => :ZIP_WRITE_HEADER,
125
+ 51 => :ZIP_PAGE_COMPRESS,
126
+ }
127
+
128
+ # Return the log record contents. (This is mostly unimplemented.)
129
+ def record_content(record_type, offset)
130
+ c = cursor(offset)
131
+ case record_type
132
+ when :MLOG_1BYTE
133
+ c.get_uint8
134
+ when :MLOG_2BYTE
135
+ c.get_uint16
136
+ when :MLOG_4BYTE
137
+ c.get_uint32
138
+ when :MLOG_8BYTE
139
+ c.get_uint64
140
+ when :UNDO_INSERT
141
+ when :COMP_REC_INSERT
142
+ end
143
+ end
144
+
145
+ SINGLE_RECORD_MASK = 0x80
146
+ RECORD_TYPE_MASK = 0x7f
147
+
148
+ # Return the log record. (This is mostly unimplemented.)
149
+ def record
150
+ @record ||= begin
151
+ if header[:first_rec_group] != 0
152
+ c = cursor(header[:first_rec_group])
153
+ type_and_flag = c.get_uint8
154
+ type = type_and_flag & RECORD_TYPE_MASK
155
+ type = RECORD_TYPES[type] || type
156
+ single_record = (type_and_flag & SINGLE_RECORD_MASK) == SINGLE_RECORD_MASK
157
+ {
158
+ :type => type,
159
+ :single_record => single_record,
160
+ :content => record_content(type, c.position),
161
+ :space => c.get_ic_uint32,
162
+ :page_number => c.get_ic_uint32,
163
+ }
164
+ end
165
+ end
166
+ end
167
+
168
+ # Dump the contents of a log block for debugging purposes.
169
+ def dump
170
+ puts
171
+ puts "header:"
172
+ pp header
173
+
174
+ puts
175
+ puts "trailer:"
176
+ pp trailer
177
+
178
+ puts
179
+ puts "record:"
180
+ pp record
181
+ end
182
+ end
@@ -0,0 +1,313 @@
1
+ require "innodb/cursor"
2
+
3
+ class Innodb::Page
4
+ # Currently only 16kB InnoDB pages are supported.
5
+ PAGE_SIZE = 16384
6
+
7
+ # InnoDB Page Type constants from include/fil0fil.h.
8
+ PAGE_TYPE = {
9
+ 0 => :ALLOCATED, # Freshly allocated page
10
+ 2 => :UNDO_LOG, # Undo log page
11
+ 3 => :INODE, # Index node
12
+ 4 => :IBUF_FREE_LIST, # Insert buffer free list
13
+ 5 => :IBUF_BITMAP, # Insert buffer bitmap
14
+ 6 => :SYS, # System page
15
+ 7 => :TRX_SYS, # Transaction system data
16
+ 8 => :FSP_HDR, # File space header
17
+ 9 => :XDES, # Extent descriptor page
18
+ 10 => :BLOB, # Uncompressed BLOB page
19
+ 11 => :ZBLOB, # First compressed BLOB page
20
+ 12 => :ZBLOB2, # Subsequent compressed BLOB page
21
+ 17855 => :INDEX, # B-tree node
22
+ }
23
+
24
+ # Initialize a page by passing in a 16kB buffer containing the raw page
25
+ # contents. Currently only 16kB pages are supported.
26
+ def initialize(buffer)
27
+ unless buffer.size == PAGE_SIZE
28
+ raise "Page buffer provided was not #{PAGE_SIZE} bytes"
29
+ end
30
+
31
+ @buffer = buffer
32
+ end
33
+
34
+ # A helper function to return bytes from the page buffer based on offset
35
+ # and length, both in bytes.
36
+ def data(offset, length)
37
+ @buffer[offset...(offset + length)]
38
+ end
39
+
40
+ # Return an Innodb::Cursor object positioned at a specific offset.
41
+ def cursor(offset)
42
+ Innodb::Cursor.new(self, offset)
43
+ end
44
+
45
+ FIL_HEADER_SIZE = 38
46
+ FIL_HEADER_START = 0
47
+
48
+ # A helper to convert "undefined" values stored in previous and next pointers
49
+ # in the page header to nil.
50
+ def maybe_undefined(value)
51
+ value == 4294967295 ? nil : value
52
+ end
53
+
54
+ # Return the "fil" header from the page, which is common for all page types.
55
+ def fil_header
56
+ c = cursor(FIL_HEADER_START)
57
+ @fil_header ||= {
58
+ :checksum => c.get_uint32,
59
+ :offset => c.get_uint32,
60
+ :prev => maybe_undefined(c.get_uint32),
61
+ :next => maybe_undefined(c.get_uint32),
62
+ :lsn => c.get_uint64,
63
+ :type => PAGE_TYPE[c.get_uint16],
64
+ :flush_lsn => c.get_uint64,
65
+ :space_id => c.get_uint32,
66
+ }
67
+ end
68
+ alias :fh :fil_header
69
+
70
+ # A helper function to return the page type from the "fil" header, for easier
71
+ # access.
72
+ def type
73
+ fil_header[:type]
74
+ end
75
+
76
+ # A helper function to return the page number of the logical previous page
77
+ # (from the doubly-linked list from page to page) from the "fil" header,
78
+ # for easier access.
79
+ def prev
80
+ fil_header[:prev]
81
+ end
82
+
83
+ # A helper function to return the page number of the logical next page
84
+ # (from the doubly-linked list from page to page) from the "fil" header,
85
+ # for easier access.
86
+ def next
87
+ fil_header[:next]
88
+ end
89
+
90
+ PAGE_HEADER_SIZE = 36
91
+ PAGE_HEADER_START = FIL_HEADER_START + FIL_HEADER_SIZE
92
+
93
+ PAGE_TRAILER_SIZE = 16
94
+ PAGE_TRAILER_START = PAGE_SIZE - PAGE_TRAILER_SIZE
95
+
96
+ FSEG_HEADER_SIZE = 10
97
+ FSEG_HEADER_START = PAGE_HEADER_START + PAGE_HEADER_SIZE
98
+ FSEG_HEADER_COUNT = 2
99
+
100
+ MUM_RECORD_SIZE = 8
101
+
102
+ RECORD_BITS_SIZE = 3
103
+ RECORD_NEXT_SIZE = 2
104
+
105
+ # Page direction values possible in the page_header[:direction] field.
106
+ PAGE_DIRECTION = {
107
+ 1 => :left,
108
+ 2 => :right,
109
+ 3 => :same_rec,
110
+ 4 => :same_page,
111
+ 5 => :no_direction,
112
+ }
113
+
114
+ # Return the size of the header for each record.
115
+ def size_record_header
116
+ case page_header[:format]
117
+ when :compact
118
+ RECORD_BITS_SIZE + RECORD_NEXT_SIZE
119
+ when :redundant
120
+ RECORD_BITS_SIZE + RECORD_NEXT_SIZE + 1
121
+ end
122
+ end
123
+
124
+ # Return the size of a field in the record header for which no description
125
+ # could be found (but must be skipped anyway).
126
+ def size_record_undefined
127
+ case page_header[:format]
128
+ when :compact
129
+ 0
130
+ when :redundant
131
+ 1
132
+ end
133
+ end
134
+
135
+ # Return the "page" header; currently only "INDEX" pages are supported.
136
+ def page_header
137
+ return nil unless type == :INDEX
138
+
139
+ c = cursor(PAGE_HEADER_START)
140
+ @page_header ||= {
141
+ :n_dir_slots => c.get_uint16,
142
+ :heap_top => c.get_uint16,
143
+ :n_heap => ((n_heap = c.get_uint16) & (2**15-1)),
144
+ :free => c.get_uint16,
145
+ :garbage => c.get_uint16,
146
+ :last_insert => c.get_uint16,
147
+ :direction => PAGE_DIRECTION[c.get_uint16],
148
+ :n_direction => c.get_uint16,
149
+ :n_recs => c.get_uint16,
150
+ :max_trx_id => c.get_uint64,
151
+ :level => c.get_uint16,
152
+ :index_id => c.get_uint64,
153
+ :format => (n_heap & 1<<15) == 0 ? :redundant : :compact,
154
+ }
155
+ end
156
+ alias :ph :page_header
157
+
158
+ # Parse and return simple fixed-format system records, such as InnoDB's
159
+ # internal infimum and supremum records.
160
+ def system_record(offset)
161
+ return nil unless type == :INDEX
162
+
163
+ c = cursor(offset)
164
+ c.adjust(-2)
165
+ {
166
+ :next => offset + c.get_sint16,
167
+ :data => c.get_bytes(8),
168
+ }
169
+ end
170
+
171
+ # Return the byte offset of the start of the "origin" of the infimum record,
172
+ # which is always the first record in the singly-linked record chain on any
173
+ # page, and represents a record with a "lower value than any possible user
174
+ # record". The infimum record immediately follows the page header.
175
+ def pos_infimum
176
+ pos_records + size_record_header + size_record_undefined
177
+ end
178
+
179
+ # Return the infimum record on a page.
180
+ def infimum
181
+ @infimum ||= system_record(pos_infimum)
182
+ end
183
+
184
+ # Return the byte offset of the start of the "origin" of the supremum record,
185
+ # which is always the last record in the singly-linked record chain on any
186
+ # page, and represents a record with a "higher value than any possible user
187
+ # record". The supremum record immediately follows the infimum record.
188
+ def pos_supremum
189
+ pos_infimum + size_record_header + size_record_undefined + MUM_RECORD_SIZE
190
+ end
191
+
192
+ # Return the supremum record on a page.
193
+ def supremum
194
+ @supremum ||= system_record(pos_supremum)
195
+ end
196
+
197
+ # Return the byte offset of the start of records within the page (the
198
+ # position immediately after the page header).
199
+ def pos_records
200
+ FIL_HEADER_SIZE +
201
+ PAGE_HEADER_SIZE +
202
+ (FSEG_HEADER_COUNT * FSEG_HEADER_SIZE)
203
+ end
204
+
205
+ # Return the byte offset of the start of the user records in a page, which
206
+ # immediately follows the supremum record.
207
+ def pos_user_records
208
+ pos_supremum + size_record_header + size_record_undefined + MUM_RECORD_SIZE
209
+ end
210
+
211
+ # Return the amount of free space in the page.
212
+ def free_space
213
+ unused_space = (PAGE_TRAILER_START - page_header[:heap_top])
214
+ unused_space + page_header[:garbage]
215
+ end
216
+
217
+ # Return the amount of used space in the page.
218
+ def used_space
219
+ PAGE_SIZE - free_space
220
+ end
221
+
222
+ # Return the amount of space occupied by records in the page.
223
+ def record_space
224
+ used_space - pos_user_records
225
+ end
226
+
227
+ # Return the actual bytes of the portion of the page which is used to
228
+ # store user records (eliminate the headers and trailer from the page).
229
+ def record_bytes
230
+ data(pos_user_records, page_header[:heap_top] - pos_user_records)
231
+ end
232
+
233
+ # Return the header from a record. (This is mostly unimplemented.)
234
+ def record_header(offset)
235
+ return nil unless type == :INDEX
236
+
237
+ c = cursor(offset).backward
238
+ case page_header[:format]
239
+ when :compact
240
+ header = {}
241
+ header[:next] = c.get_sint16
242
+ bits1 = c.get_uint16
243
+ header[:type] = bits1 & 0x07
244
+ header[:order] = (bits1 & 0xf8) >> 3
245
+ bits2 = c.get_uint8
246
+ header[:n_owned] = bits2 & 0x0f
247
+ header[:deleted] = (bits2 & 0xf0) >> 4
248
+ header
249
+ when :redundant
250
+ raise "Not implemented"
251
+ end
252
+ end
253
+
254
+ # Return a record. (This is mostly unimplemented.)
255
+ def record(offset)
256
+ return nil unless offset
257
+ return nil unless type == :INDEX
258
+ return nil if offset == pos_infimum
259
+ return nil if offset == pos_supremum
260
+
261
+ c = cursor(offset).forward
262
+ # There is a header preceding the row itself, so back up and read it.
263
+ header = record_header(offset)
264
+ {
265
+ :header => header,
266
+ :next => header[:next] == 0 ? nil : (offset + header[:next]),
267
+ # These system records may not be present depending on schema.
268
+ :rec1 => c.get_bytes(6),
269
+ :rec2 => c.get_bytes(6),
270
+ :rec3 => c.get_bytes(7),
271
+ # Read a few bytes just so it can be visually verified.
272
+ :data => c.get_bytes(8),
273
+ }
274
+ end
275
+
276
+ # Iterate through all records. (This is mostly unimplemented.)
277
+ def each_record
278
+ rec = infimum
279
+ while rec = record(rec[:next])
280
+ yield rec
281
+ end
282
+ nil
283
+ end
284
+
285
+ # Dump the contents of a page for debugging purposes.
286
+ def dump
287
+ puts
288
+ puts "fil header:"
289
+ pp fil_header
290
+
291
+ puts
292
+ puts "page header:"
293
+ pp page_header
294
+
295
+ puts
296
+ puts "free space: #{free_space}"
297
+ puts "used space: #{used_space}"
298
+ puts "record space: #{record_space}"
299
+
300
+ if type == :INDEX
301
+ puts
302
+ puts "system records:"
303
+ pp infimum
304
+ pp supremum
305
+
306
+ puts
307
+ puts "records:"
308
+ each_record do |rec|
309
+ pp rec
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,29 @@
1
+ # An InnoDB tablespace file, which can be either a multi-table ibdataN file
2
+ # or a single-table "innodb_file_per_table" .ibd file.
3
+ class Innodb::Space
4
+ # Open a tablespace file.
5
+ def initialize(file)
6
+ @file = File.open(file)
7
+ @size = @file.stat.size
8
+ @pages = (@size / Innodb::Page::PAGE_SIZE)
9
+ end
10
+
11
+ # Get an Innodb::Page object for a specific page by page number.
12
+ def page(page_number)
13
+ offset = page_number.to_i * Innodb::Page::PAGE_SIZE
14
+ return nil unless offset < @size
15
+ return nil unless (offset + Innodb::Page::PAGE_SIZE) <= @size
16
+ @file.seek(offset)
17
+ page_data = @file.read(Innodb::Page::PAGE_SIZE)
18
+ Innodb::Page.new(page_data)
19
+ end
20
+
21
+ # Iterate through all pages in a tablespace, returning the page number
22
+ # and an Innodb::Page object for each one.
23
+ def each_page
24
+ (0...@pages).each do |page_number|
25
+ current_page = page(page_number)
26
+ yield page_number, current_page if current_page
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Innodb
2
+ VERSION = "0.4"
3
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: innodb_ruby
3
+ version: !ruby/object:Gem::Version
4
+ hash: 3
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 4
9
+ version: "0.4"
10
+ platform: ruby
11
+ authors:
12
+ - Jeremy Cole
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-11-20 00:00:00 Z
18
+ dependencies: []
19
+
20
+ description: Library for parsing InnoDB data files in Ruby
21
+ email: jeremy@jcole.us
22
+ executables:
23
+ - innodb_dump_log
24
+ - innodb_dump_space
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/innodb.rb
31
+ - lib/innodb/cursor.rb
32
+ - lib/innodb/log.rb
33
+ - lib/innodb/log_block.rb
34
+ - lib/innodb/page.rb
35
+ - lib/innodb/space.rb
36
+ - lib/innodb/version.rb
37
+ - bin/innodb_dump_log
38
+ - bin/innodb_dump_space
39
+ homepage: http://jcole.us/
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ hash: 3
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.10
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: InnoDB data file parser
72
+ test_files: []
73
+
74
+ has_rdoc: