innodb_ruby 0.7.11 → 0.7.12
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_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
|