innodb_ruby 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: