innodb_ruby 0.7.11 → 0.7.12
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/innodb_space +162 -25
- data/lib/innodb.rb +8 -1
- data/lib/innodb/checksum.rb +30 -0
- data/lib/innodb/cursor.rb +165 -37
- data/lib/innodb/field.rb +31 -57
- data/lib/innodb/field_type.rb +124 -0
- data/lib/innodb/fseg_entry.rb +9 -6
- data/lib/innodb/index.rb +23 -12
- data/lib/innodb/inode.rb +133 -0
- data/lib/innodb/list.rb +44 -12
- data/lib/innodb/log.rb +1 -0
- data/lib/innodb/log_block.rb +2 -1
- data/lib/innodb/page.rb +93 -17
- data/lib/innodb/page/blob.rb +60 -0
- data/lib/innodb/page/fsp_hdr_xdes.rb +34 -24
- data/lib/innodb/page/index.rb +364 -190
- data/lib/innodb/page/inode.rb +20 -51
- data/lib/innodb/page/sys.rb +22 -0
- data/lib/innodb/page/sys_data_dictionary_header.rb +92 -0
- data/lib/innodb/page/sys_rseg_header.rb +59 -0
- data/lib/innodb/page/trx_sys.rb +72 -29
- data/lib/innodb/page/undo_log.rb +95 -0
- data/lib/innodb/record_describer.rb +2 -1
- data/lib/innodb/space.rb +162 -17
- data/lib/innodb/undo_log.rb +73 -0
- data/lib/innodb/version.rb +2 -1
- data/lib/innodb/xdes.rb +44 -11
- metadata +19 -6
data/lib/innodb/field.rb
CHANGED
@@ -1,75 +1,49 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "innodb/field_type"
|
3
|
+
|
1
4
|
# A single field in an InnoDB record (within an INDEX page). This class
|
2
|
-
# provides essential information to parse records
|
3
|
-
# width portion of the field
|
5
|
+
# provides essential information to parse records, including the length
|
6
|
+
# of the fixed-width and variable-width portion of the field.
|
4
7
|
class Innodb::Field
|
5
|
-
attr_reader :position, :
|
6
|
-
|
7
|
-
def initialize(position, type, *properties)
|
8
|
-
@position = position
|
9
|
-
@type, @fixed_len, @variable_len = parse_data_type(type.to_s)
|
10
|
-
@nullable = (not properties.include?(:NOT_NULL))
|
11
|
-
@unsigned = properties.include?(:UNSIGNED)
|
12
|
-
end
|
8
|
+
attr_reader :position, :type
|
13
9
|
|
14
|
-
#
|
15
|
-
|
16
|
-
case data_type
|
17
|
-
when /^(tinyint|smallint|mediumint|int|bigint)$/i
|
18
|
-
type = data_type.upcase.to_sym
|
19
|
-
fixed_len = fixed_len_map[type]
|
20
|
-
[type, fixed_len, 0]
|
21
|
-
when /^varchar\((\d+)\)$/i
|
22
|
-
[:VARCHAR, 0, $1.to_i]
|
23
|
-
when /^char\((\d+)\)$/i
|
24
|
-
[:CHAR, $1.to_i, 0]
|
25
|
-
else
|
26
|
-
raise "Data type '#{data_type}' is not supported"
|
27
|
-
end
|
28
|
-
end
|
10
|
+
# Size of a reference to data stored externally to the page.
|
11
|
+
EXTERN_FIELD_SIZE = 20
|
29
12
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
:TINYINT => 1,
|
34
|
-
:SMALLINT => 2,
|
35
|
-
:MEDIUMINT => 3,
|
36
|
-
:INT => 4,
|
37
|
-
:BIGINT => 8,
|
38
|
-
}
|
13
|
+
def initialize(position, data_type, *properties)
|
14
|
+
@position = position
|
15
|
+
@type = Innodb::FieldType.new(data_type.to_s, properties)
|
39
16
|
end
|
40
17
|
|
41
18
|
# Return whether this field is NULL.
|
42
19
|
def null?(record)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
20
|
+
type.nullable? && record[:header][:field_nulls][position]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return whether a part of this field is stored externally (off-page).
|
24
|
+
def extern?(record)
|
25
|
+
record[:header][:field_externs][position]
|
48
26
|
end
|
49
27
|
|
50
|
-
# Return the length of this variable-length field.
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
28
|
+
# Return the actual length of this variable-length field.
|
29
|
+
def length(record)
|
30
|
+
if type.variable?
|
31
|
+
len = record[:header][:field_lengths][position]
|
32
|
+
else
|
33
|
+
len = type.length
|
56
34
|
end
|
35
|
+
extern?(record) ? len - EXTERN_FIELD_SIZE : len
|
57
36
|
end
|
58
37
|
|
59
38
|
# Read an InnoDB encoded data field.
|
60
39
|
def read(record, cursor)
|
61
|
-
return :NULL if
|
40
|
+
return :NULL if null?(record)
|
41
|
+
cursor.name(type.name) { type.reader.read(cursor, length(record)) }
|
42
|
+
end
|
62
43
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
when :VARCHAR
|
68
|
-
cursor.get_bytes(get_variable_len(record))
|
69
|
-
when :CHAR
|
70
|
-
# Fixed-width character fields will be space-padded up to their length,
|
71
|
-
# so SQL defines that trailing spaces should be removed.
|
72
|
-
cursor.get_bytes(fixed_len).sub(/[ ]+$/, "")
|
73
|
-
end
|
44
|
+
# Read an InnoDB external pointer field.
|
45
|
+
def read_extern(record, cursor)
|
46
|
+
return nil if not extern?(record)
|
47
|
+
cursor.name(type.name) { type.reader.read_extern(cursor) }
|
74
48
|
end
|
75
49
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class Innodb::FieldType
|
3
|
+
class GenericType
|
4
|
+
attr_reader :type
|
5
|
+
|
6
|
+
def initialize(type)
|
7
|
+
@type = type
|
8
|
+
end
|
9
|
+
|
10
|
+
def read(cursor, length)
|
11
|
+
cursor.get_bytes(length)
|
12
|
+
end
|
13
|
+
|
14
|
+
def read_extern(cursor)
|
15
|
+
cursor.name("extern") { get_extern_field(cursor) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return an external reference field. An extern field contains the page
|
19
|
+
# address and the length of the externally stored part of the record data.
|
20
|
+
def get_extern_field(cursor)
|
21
|
+
{
|
22
|
+
:space_id => cursor.name("space_id") { cursor.get_uint32 },
|
23
|
+
:page_number => cursor.name("page_number") { cursor.get_uint32 },
|
24
|
+
:offset => cursor.name("offset") { cursor.get_uint32 },
|
25
|
+
:length => cursor.name("length") { cursor.get_uint64 & 0x3fffffff }
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class IntegerType < GenericType
|
31
|
+
def read(cursor, length)
|
32
|
+
method = type.unsigned? ? :get_uint_by_size : :get_i_sint_by_size
|
33
|
+
cursor.send(method, type.length)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class VariableStringType < GenericType
|
38
|
+
def read(cursor, length)
|
39
|
+
# The SQL standard defines that VARCHAR fields should have end-spaces
|
40
|
+
# stripped off.
|
41
|
+
super.sub(/[ ]+$/, "")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Maps data type to fixed storage length.
|
46
|
+
TYPES = {
|
47
|
+
:TINYINT => { :class => IntegerType, :length => 1 },
|
48
|
+
:SMALLINT => { :class => IntegerType, :length => 2 },
|
49
|
+
:MEDIUMINT => { :class => IntegerType, :length => 3 },
|
50
|
+
:INT => { :class => IntegerType, :length => 4 },
|
51
|
+
:INT6 => { :class => IntegerType, :length => 6 },
|
52
|
+
:BIGINT => { :class => IntegerType, :length => 8 },
|
53
|
+
:CHAR => { :class => GenericType },
|
54
|
+
:VARCHAR => { :class => VariableStringType },
|
55
|
+
:BLOB => { :class => GenericType },
|
56
|
+
}
|
57
|
+
|
58
|
+
def self.parse_base_type_and_modifiers(type_string)
|
59
|
+
if matches = /^([a-zA-Z0-9]+)(\(([0-9, ]+)\))?$/.match(type_string)
|
60
|
+
base_type = matches[1].upcase.to_sym
|
61
|
+
if matches[3]
|
62
|
+
modifiers = matches[3].sub(/[ ]/, "").split(/,/).map { |s| s.to_i }
|
63
|
+
else
|
64
|
+
modifiers = []
|
65
|
+
end
|
66
|
+
[base_type, modifiers]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :base_type
|
71
|
+
attr_reader :length
|
72
|
+
attr_reader :reader
|
73
|
+
def initialize(type_string, properties)
|
74
|
+
@base_type, modifiers = self.class.parse_base_type_and_modifiers(type_string)
|
75
|
+
@length = nil
|
76
|
+
@nullable = !properties.include?(:NOT_NULL)
|
77
|
+
@unsigned = properties.include?(:UNSIGNED)
|
78
|
+
@variable = false
|
79
|
+
@blob = false
|
80
|
+
case
|
81
|
+
when TYPES[base_type][:class] == IntegerType
|
82
|
+
@length = TYPES[base_type][:length]
|
83
|
+
when base_type == :CHAR
|
84
|
+
@length = modifiers[0]
|
85
|
+
when base_type == :VARCHAR
|
86
|
+
@length = modifiers[0]
|
87
|
+
@variable = true
|
88
|
+
when base_type == :BLOB
|
89
|
+
@blob = true
|
90
|
+
@variable = true
|
91
|
+
else
|
92
|
+
raise "Data type '#{type_string}' is not supported"
|
93
|
+
end
|
94
|
+
@reader = TYPES[base_type][:class].new(self)
|
95
|
+
end
|
96
|
+
|
97
|
+
def unsigned?
|
98
|
+
@unsigned
|
99
|
+
end
|
100
|
+
|
101
|
+
def nullable?
|
102
|
+
@nullable
|
103
|
+
end
|
104
|
+
|
105
|
+
def variable?
|
106
|
+
@variable
|
107
|
+
end
|
108
|
+
|
109
|
+
def blob?
|
110
|
+
@blob
|
111
|
+
end
|
112
|
+
|
113
|
+
def name_suffix
|
114
|
+
if [:CHAR, :VARCHAR].include?(base_type)
|
115
|
+
"(#{length})"
|
116
|
+
else
|
117
|
+
""
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def name
|
122
|
+
"#{base_type}#{name_suffix}"
|
123
|
+
end
|
124
|
+
end
|
data/lib/innodb/fseg_entry.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
1
2
|
# An InnoDB file segment entry, which appears in a few places, such as the
|
2
3
|
# FSEG header of INDEX pages, and in the TRX_SYS pages.
|
3
4
|
class Innodb::FsegEntry
|
@@ -8,18 +9,20 @@ class Innodb::FsegEntry
|
|
8
9
|
# Return the FSEG entry address, which points to an entry on an INODE page.
|
9
10
|
def self.get_entry_address(cursor)
|
10
11
|
{
|
11
|
-
:space_id => cursor.get_uint32,
|
12
|
-
:page_number => cursor.
|
13
|
-
|
12
|
+
:space_id => cursor.name("space_id") { cursor.get_uint32 },
|
13
|
+
:page_number => cursor.name("page_number") {
|
14
|
+
Innodb::Page.maybe_undefined(cursor.get_uint32)
|
15
|
+
},
|
16
|
+
:offset => cursor.name("offset") { cursor.get_uint16 },
|
14
17
|
}
|
15
18
|
end
|
16
19
|
|
17
20
|
# Return an INODE entry which represents this file segment.
|
18
21
|
def self.get_inode(space, cursor)
|
19
|
-
address = get_entry_address(cursor)
|
22
|
+
address = cursor.name("address") { get_entry_address(cursor) }
|
20
23
|
page = space.page(address[:page_number])
|
21
24
|
if page.type == :INODE
|
22
|
-
page.inode_at(address[:offset])
|
25
|
+
page.inode_at(page.cursor(address[:offset]))
|
23
26
|
end
|
24
27
|
end
|
25
|
-
end
|
28
|
+
end
|
data/lib/innodb/index.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
1
2
|
# An InnoDB index B-tree, given an Innodb::Space and a root page number.
|
2
3
|
class Innodb::Index
|
3
4
|
attr_reader :root
|
4
5
|
attr_reader :stats
|
5
6
|
attr_accessor :debug
|
7
|
+
attr_accessor :record_describer
|
6
8
|
|
7
|
-
def initialize(space, root_page_number)
|
8
|
-
@space = space
|
9
|
-
@root = @space.page(root_page_number)
|
9
|
+
def initialize(space, root_page_number, record_describer=nil)
|
10
10
|
@debug = false
|
11
|
+
@space = space
|
12
|
+
@record_describer = record_describer || space.record_describer
|
13
|
+
|
14
|
+
@root = page(root_page_number)
|
11
15
|
|
12
16
|
unless @root
|
13
17
|
raise "Page #{root_page_number} couldn't be read"
|
@@ -26,6 +30,12 @@ class Innodb::Index
|
|
26
30
|
reset_stats
|
27
31
|
end
|
28
32
|
|
33
|
+
def page(page_number)
|
34
|
+
page = @space.page(page_number)
|
35
|
+
page.record_describer = @record_describer
|
36
|
+
page
|
37
|
+
end
|
38
|
+
|
29
39
|
def reset_stats
|
30
40
|
@stats = Hash.new(0)
|
31
41
|
end
|
@@ -53,7 +63,7 @@ class Innodb::Index
|
|
53
63
|
end
|
54
64
|
|
55
65
|
parent_page.each_child_page do |child_page_number, child_min_key|
|
56
|
-
child_page =
|
66
|
+
child_page = page(child_page_number)
|
57
67
|
child_page.record_describer = @space.record_describer
|
58
68
|
if child_page.type == :INDEX
|
59
69
|
if link_proc
|
@@ -76,7 +86,7 @@ class Innodb::Index
|
|
76
86
|
page = @root
|
77
87
|
record = @root.first_record
|
78
88
|
while record && page.level > level
|
79
|
-
page =
|
89
|
+
page = page(record[:child_page_number])
|
80
90
|
record = page.first_record
|
81
91
|
end
|
82
92
|
page if page.level == level
|
@@ -104,8 +114,8 @@ class Innodb::Index
|
|
104
114
|
return enum_for(:each_fseg_list, fseg)
|
105
115
|
end
|
106
116
|
|
107
|
-
|
108
|
-
yield list_name,
|
117
|
+
fseg.each_list do |list_name, list|
|
118
|
+
yield list_name, list
|
109
119
|
end
|
110
120
|
end
|
111
121
|
|
@@ -115,8 +125,8 @@ class Innodb::Index
|
|
115
125
|
return enum_for(:each_fseg_frag_page, fseg)
|
116
126
|
end
|
117
127
|
|
118
|
-
fseg
|
119
|
-
yield page_number,
|
128
|
+
fseg.frag_array_pages.each do |page_number|
|
129
|
+
yield page_number, page(page_number)
|
120
130
|
end
|
121
131
|
end
|
122
132
|
|
@@ -128,7 +138,8 @@ class Innodb::Index
|
|
128
138
|
|
129
139
|
while page && page.type == :INDEX
|
130
140
|
yield page
|
131
|
-
|
141
|
+
break unless page.next
|
142
|
+
page = page(page.next)
|
132
143
|
end
|
133
144
|
end
|
134
145
|
|
@@ -334,7 +345,7 @@ class Innodb::Index
|
|
334
345
|
if page.level > 0
|
335
346
|
# If we haven't reached a leaf page yet, move down the tree and search
|
336
347
|
# again using linear search.
|
337
|
-
page =
|
348
|
+
page = page(rec[:child_page_number])
|
338
349
|
else
|
339
350
|
# We're on a leaf page, so return the page and record if there is a
|
340
351
|
# match. If there is no match, break the loop and cause nil to be
|
@@ -367,7 +378,7 @@ class Innodb::Index
|
|
367
378
|
if page.level > 0
|
368
379
|
# If we haven't reached a leaf page yet, move down the tree and search
|
369
380
|
# again using binary search.
|
370
|
-
page =
|
381
|
+
page = page(rec[:child_page_number])
|
371
382
|
else
|
372
383
|
# We're on a leaf page, so return the page and record if there is a
|
373
384
|
# match. If there is no match, break the loop and cause nil to be
|
data/lib/innodb/inode.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class Innodb::Inode
|
3
|
+
# The number of "slots" (each representing one page) in the fragment array
|
4
|
+
# within each Inode entry.
|
5
|
+
FRAG_ARRAY_N_SLOTS = 32 # FSP_EXTENT_SIZE / 2
|
6
|
+
|
7
|
+
# The size (in bytes) of each slot in the fragment array.
|
8
|
+
FRAG_SLOT_SIZE = 4
|
9
|
+
|
10
|
+
# A magic number which helps determine if an Inode structure is in use
|
11
|
+
# and populated with valid data.
|
12
|
+
MAGIC_N_VALUE = 97937874
|
13
|
+
|
14
|
+
# The size (in bytes) of an Inode entry.
|
15
|
+
SIZE = (16 + (3 * Innodb::List::BASE_NODE_SIZE) +
|
16
|
+
(FRAG_ARRAY_N_SLOTS * FRAG_SLOT_SIZE))
|
17
|
+
|
18
|
+
# Read an array of page numbers (32-bit integers, which may be nil) from
|
19
|
+
# the provided cursor.
|
20
|
+
def self.page_number_array(size, cursor)
|
21
|
+
size.times.map do |n|
|
22
|
+
cursor.name("page[#{n}]") do |c|
|
23
|
+
Innodb::Page.maybe_undefined(c.get_uint32)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Construct a new Inode by reading an FSEG header from a cursor.
|
29
|
+
def self.new_from_cursor(space, cursor)
|
30
|
+
data = {
|
31
|
+
:fseg_id => cursor.name("fseg_id") {
|
32
|
+
cursor.get_uint64
|
33
|
+
},
|
34
|
+
:not_full_n_used => cursor.name("not_full_n_used") {
|
35
|
+
cursor.get_uint32
|
36
|
+
},
|
37
|
+
:free => cursor.name("list[free]") {
|
38
|
+
Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor))
|
39
|
+
},
|
40
|
+
:not_full => cursor.name("list[not_full]") {
|
41
|
+
Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor))
|
42
|
+
},
|
43
|
+
:full => cursor.name("list[full]") {
|
44
|
+
Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor))
|
45
|
+
},
|
46
|
+
:magic_n => cursor.name("magic_n") {
|
47
|
+
cursor.get_uint32
|
48
|
+
},
|
49
|
+
:frag_array => cursor.name("frag_array") {
|
50
|
+
page_number_array(FRAG_ARRAY_N_SLOTS, cursor)
|
51
|
+
},
|
52
|
+
}
|
53
|
+
|
54
|
+
Innodb::Inode.new(space, data)
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_accessor :space
|
58
|
+
|
59
|
+
def initialize(space, data)
|
60
|
+
@space = space
|
61
|
+
@data = data
|
62
|
+
end
|
63
|
+
|
64
|
+
def fseg_id; @data[:fseg_id]; end
|
65
|
+
def not_full_n_used; @data[:not_full_n_used]; end
|
66
|
+
def free; @data[:free]; end
|
67
|
+
def not_full; @data[:not_full]; end
|
68
|
+
def full; @data[:full]; end
|
69
|
+
def magic_n; @data[:magic_n]; end
|
70
|
+
def frag_array; @data[:frag_array]; end
|
71
|
+
|
72
|
+
# Helper method to determine if an Inode is in use. Inodes that are not in
|
73
|
+
# use have an fseg_id of 0.
|
74
|
+
def allocated?
|
75
|
+
fseg_id != 0
|
76
|
+
end
|
77
|
+
|
78
|
+
# Helper method to return an array of only non-nil fragment pages.
|
79
|
+
def frag_array_pages
|
80
|
+
frag_array.select { |n| ! n.nil? }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Helper method to count non-nil fragment pages.
|
84
|
+
def frag_array_n_used
|
85
|
+
frag_array.inject(0) { |n, i| n += 1 if i; n }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Calculate the total number of pages in use (not free) within this fseg.
|
89
|
+
def used_pages
|
90
|
+
frag_array_n_used + not_full_n_used
|
91
|
+
(full.length * @space.pages_per_extent)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Calculate the total number of pages within this fseg.
|
95
|
+
def total_pages
|
96
|
+
frag_array_n_used +
|
97
|
+
(free.length * @space.pages_per_extent) +
|
98
|
+
(not_full.length * @space.pages_per_extent) +
|
99
|
+
(full.length * @space.pages_per_extent)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Calculate the fill factor of this fseg, in percent.
|
103
|
+
def fill_factor
|
104
|
+
total_pages > 0 ? 100.0 * (used_pages.to_f / total_pages.to_f) : 0.0
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return an array of lists within an fseg.
|
108
|
+
def lists
|
109
|
+
[:free, :not_full, :full]
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return a list from the fseg, given its name as a symbol.
|
113
|
+
def list(name)
|
114
|
+
@data[name] if lists.include? name
|
115
|
+
end
|
116
|
+
|
117
|
+
# Iterate through all lists, yielding the list name and the list itself.
|
118
|
+
def each_list
|
119
|
+
lists.each do |name|
|
120
|
+
yield name, list(name)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Compare one Innodb::Inode to another.
|
125
|
+
def ==(other)
|
126
|
+
fseg_id == other.fseg_id
|
127
|
+
end
|
128
|
+
|
129
|
+
# Dump a summary of this object for debugging purposes.
|
130
|
+
def dump
|
131
|
+
pp @data
|
132
|
+
end
|
133
|
+
end
|