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.
- data/bin/innodb_dump_log +25 -0
- data/bin/innodb_dump_space +33 -0
- data/lib/innodb.rb +8 -0
- data/lib/innodb/cursor.rb +148 -0
- data/lib/innodb/log.rb +43 -0
- data/lib/innodb/log_block.rb +182 -0
- data/lib/innodb/page.rb +313 -0
- data/lib/innodb/space.rb +29 -0
- data/lib/innodb/version.rb +3 -0
- metadata +74 -0
data/bin/innodb_dump_log
ADDED
|
@@ -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,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
|
data/lib/innodb/page.rb
ADDED
|
@@ -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
|
data/lib/innodb/space.rb
ADDED
|
@@ -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
|
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:
|