innodb_ruby 0.8.8 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +6 -1
- data/bin/innodb_log +8 -6
- data/bin/innodb_space +344 -212
- data/lib/innodb.rb +12 -1
- data/lib/innodb/cursor.rb +2 -2
- data/lib/innodb/data_dictionary.rb +591 -0
- data/lib/innodb/index.rb +149 -178
- data/lib/innodb/log.rb +96 -33
- data/lib/innodb/log_block.rb +34 -68
- data/lib/innodb/page.rb +1 -7
- data/lib/innodb/page/index.rb +303 -39
- data/lib/innodb/page/index_compressed.rb +4 -0
- data/lib/innodb/page/sys_data_dictionary_header.rb +1 -43
- data/lib/innodb/record.rb +34 -3
- data/lib/innodb/record_describer.rb +2 -0
- data/lib/innodb/space.rb +37 -22
- data/lib/innodb/stats.rb +46 -0
- data/lib/innodb/system.rb +122 -42
- data/lib/innodb/version.rb +1 -1
- metadata +3 -2
@@ -1,5 +1,9 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
|
3
|
+
# This is horribly incomplete and broken. InnoDB compression does not
|
4
|
+
# currently work in innodb_ruby. Patches are welcome!
|
5
|
+
# (Hint hint, nudge nudge, Facebook developers!)
|
6
|
+
|
3
7
|
class Innodb::Page::Index::Compressed < Innodb::Page::Index
|
4
8
|
# The number of directory slots in use.
|
5
9
|
def directory_slots
|
@@ -1,18 +1,6 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
|
3
|
-
require "innodb/data_dictionary"
|
4
|
-
|
5
3
|
class Innodb::Page::SysDataDictionaryHeader < Innodb::Page
|
6
|
-
RECORD_DESCRIBERS = {
|
7
|
-
:SYS_TABLES => {
|
8
|
-
:PRIMARY => Innodb::DataDictionary::SYS_TABLES_PRIMARY,
|
9
|
-
:ID => Innodb::DataDictionary::SYS_TABLES_ID
|
10
|
-
},
|
11
|
-
:SYS_COLUMNS => { :PRIMARY => Innodb::DataDictionary::SYS_COLUMNS_PRIMARY },
|
12
|
-
:SYS_INDEXES => { :PRIMARY => Innodb::DataDictionary::SYS_INDEXES_PRIMARY },
|
13
|
-
:SYS_FIELDS => { :PRIMARY => Innodb::DataDictionary::SYS_FIELDS_PRIMARY },
|
14
|
-
}
|
15
|
-
|
16
4
|
# The position of the data dictionary header within the page.
|
17
5
|
def pos_data_dictionary_header
|
18
6
|
pos_fil_header + size_fil_header
|
@@ -35,7 +23,7 @@ class Innodb::Page::SysDataDictionaryHeader < Innodb::Page
|
|
35
23
|
:indexes => c.name("indexes") {{
|
36
24
|
:SYS_TABLES => c.name("SYS_TABLES") {{
|
37
25
|
:PRIMARY => c.name("PRIMARY") { c.get_uint32 },
|
38
|
-
:ID => c.name("ID") { c.get_uint32 },
|
26
|
+
:ID => c.name("ID") { c.get_uint32 },
|
39
27
|
}},
|
40
28
|
:SYS_COLUMNS => c.name("SYS_COLUMNS") {{
|
41
29
|
:PRIMARY => c.name("PRIMARY") { c.get_uint32 },
|
@@ -53,36 +41,6 @@ class Innodb::Page::SysDataDictionaryHeader < Innodb::Page
|
|
53
41
|
end
|
54
42
|
end
|
55
43
|
|
56
|
-
def index(table_name, index_name)
|
57
|
-
unless table_entry = data_dictionary_header[:indexes][table_name]
|
58
|
-
raise "Unknown data dictionary table #{table_name}"
|
59
|
-
end
|
60
|
-
|
61
|
-
unless index_root_page = table_entry[index_name]
|
62
|
-
raise "Unknown data dictionary index #{table_name}.#{index_name}"
|
63
|
-
end
|
64
|
-
|
65
|
-
# If we have a record describer for this index, load it.
|
66
|
-
record_describer = RECORD_DESCRIBERS[table_name] &&
|
67
|
-
RECORD_DESCRIBERS[table_name][index_name]
|
68
|
-
|
69
|
-
@space.index(index_root_page, record_describer.new)
|
70
|
-
end
|
71
|
-
|
72
|
-
# Iterate through all indexes in the data dictionary, yielding the table
|
73
|
-
# name, index name, and the index itself as an Innodb::Index.
|
74
|
-
def each_index
|
75
|
-
unless block_given?
|
76
|
-
return enum_for(:each_index)
|
77
|
-
end
|
78
|
-
|
79
|
-
data_dictionary_header[:indexes].each do |table_name, indexes|
|
80
|
-
indexes.each do |index_name, root_page_number|
|
81
|
-
yield table_name, index_name, index(table_name, index_name)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
44
|
def dump
|
87
45
|
super
|
88
46
|
|
data/lib/innodb/record.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
|
3
3
|
class Innodb::Record
|
4
|
+
attr_reader :page
|
4
5
|
attr_accessor :record
|
5
6
|
|
6
|
-
def initialize(record)
|
7
|
+
def initialize(page, record)
|
8
|
+
@page = page
|
7
9
|
@record = record
|
8
10
|
end
|
9
11
|
|
@@ -24,7 +26,7 @@ class Innodb::Record
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def key_string
|
27
|
-
key && key.map { |r| "%s=%s" % [r[:name], r[:value]] }.join(", ")
|
29
|
+
key && key.map { |r| "%s=%s" % [r[:name], r[:value].inspect] }.join(", ")
|
28
30
|
end
|
29
31
|
|
30
32
|
def row
|
@@ -32,13 +34,21 @@ class Innodb::Record
|
|
32
34
|
end
|
33
35
|
|
34
36
|
def row_string
|
35
|
-
|
37
|
+
row && row.map { |r| "%s=%s" % [r[:name], r[:value].inspect] }.join(", ")
|
36
38
|
end
|
37
39
|
|
38
40
|
def child_page_number
|
39
41
|
record[:child_page_number]
|
40
42
|
end
|
41
43
|
|
44
|
+
def string
|
45
|
+
if child_page_number
|
46
|
+
"(%s) → #%s" % [key_string, child_page_number]
|
47
|
+
else
|
48
|
+
"(%s) → (%s)" % [key_string, row_string]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
42
52
|
def uncached_fields
|
43
53
|
fields_hash = {}
|
44
54
|
[:key, :row].each do |group|
|
@@ -54,4 +64,25 @@ class Innodb::Record
|
|
54
64
|
def fields
|
55
65
|
@fields ||= uncached_fields
|
56
66
|
end
|
67
|
+
|
68
|
+
# Compare two arrays of fields to determine if they are equal. This follows
|
69
|
+
# the same comparison rules as strcmp and others:
|
70
|
+
# 0 = a is equal to b
|
71
|
+
# -1 = a is less than b
|
72
|
+
# +1 = a is greater than b
|
73
|
+
def compare_key(other_key)
|
74
|
+
Innodb::Stats.increment :compare_key
|
75
|
+
|
76
|
+
return 0 if other_key.nil? && key.nil?
|
77
|
+
return -1 if other_key.nil? || (!key.nil? && other_key.size < key.size)
|
78
|
+
return +1 if key.nil? || (!other_key.nil? && other_key.size > key.size)
|
79
|
+
|
80
|
+
key.each_index do |i|
|
81
|
+
Innodb::Stats.increment :compare_key_field_comparison
|
82
|
+
return -1 if other_key[i] < key[i][:value]
|
83
|
+
return +1 if other_key[i] > key[i][:value]
|
84
|
+
end
|
85
|
+
|
86
|
+
return 0
|
87
|
+
end
|
57
88
|
end
|
@@ -88,6 +88,8 @@ class Innodb::RecordDescriber
|
|
88
88
|
|
89
89
|
def initialize
|
90
90
|
@description = self.class.static_description.dup
|
91
|
+
@description[:key] = @description[:key].dup
|
92
|
+
@description[:row] = @description[:row].dup
|
91
93
|
end
|
92
94
|
|
93
95
|
# Set the type of this record (:clustered or :secondary).
|
data/lib/innodb/space.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
# An InnoDB space file, which can be either a multi-table ibdataN file
|
4
4
|
# or a single-table "innodb_file_per_table" .ibd file.
|
5
|
-
|
6
5
|
class Innodb::Space
|
7
6
|
# InnoDB's default page size is 16KiB.
|
8
7
|
DEFAULT_PAGE_SIZE = 16384
|
@@ -35,9 +34,13 @@ class Innodb::Space
|
|
35
34
|
|
36
35
|
@pages = (@size / @page_size)
|
37
36
|
@compressed = fsp_flags[:compressed]
|
37
|
+
@innodb_system = nil
|
38
38
|
@record_describer = nil
|
39
39
|
end
|
40
40
|
|
41
|
+
# The Innodb::System to which this space belongs, if any.
|
42
|
+
attr_accessor :innodb_system
|
43
|
+
|
41
44
|
# An object which can be used to describe records found in pages within
|
42
45
|
# this space.
|
43
46
|
attr_accessor :record_describer
|
@@ -142,13 +145,7 @@ class Innodb::Space
|
|
142
145
|
|
143
146
|
# Get an Innodb::Page object for a specific page by page number.
|
144
147
|
def page(page_number)
|
145
|
-
|
146
|
-
|
147
|
-
if this_page.type == :INDEX
|
148
|
-
this_page.record_describer = @record_describer
|
149
|
-
end
|
150
|
-
|
151
|
-
this_page
|
148
|
+
Innodb::Page.parse(self, page_data(page_number))
|
152
149
|
end
|
153
150
|
|
154
151
|
# Determine whether this space looks like a system space. If the initial
|
@@ -206,7 +203,7 @@ class Innodb::Space
|
|
206
203
|
end
|
207
204
|
|
208
205
|
# Get the Innodb::Page::SysDataDictionaryHeader page for a system space.
|
209
|
-
def
|
206
|
+
def data_dictionary_page
|
210
207
|
page(page_sys_data_dictionary) if system_space?
|
211
208
|
end
|
212
209
|
|
@@ -219,32 +216,47 @@ class Innodb::Space
|
|
219
216
|
|
220
217
|
# Get an Innodb::Index object for a specific index by root page number.
|
221
218
|
def index(root_page_number, record_describer=nil)
|
222
|
-
Innodb::Index.new(self, root_page_number,
|
219
|
+
Innodb::Index.new(self, root_page_number,
|
220
|
+
record_describer || @record_describer)
|
223
221
|
end
|
224
222
|
|
225
|
-
# Iterate through
|
226
|
-
|
227
|
-
# root page. This should work fine for IBD files, but not for ibdata
|
228
|
-
# files.
|
229
|
-
def each_index
|
223
|
+
# Iterate through all root page numbers for indexes in the space.
|
224
|
+
def each_index_root_page_number
|
230
225
|
unless block_given?
|
231
|
-
return enum_for(:
|
226
|
+
return enum_for(:each_index_root_page_number)
|
232
227
|
end
|
233
228
|
|
234
|
-
if
|
235
|
-
|
236
|
-
|
229
|
+
if innodb_system
|
230
|
+
# Retrieve the index root page numbers from the data dictionary.
|
231
|
+
innodb_system.data_dictionary.each_index_by_space_id(space_id) do |record|
|
232
|
+
yield record["PAGE_NO"]
|
237
233
|
end
|
238
234
|
else
|
235
|
+
# Guess that the index root pages will be present starting at page 3,
|
236
|
+
# and walk forward until we find a non-root page. This should work fine
|
237
|
+
# for IBD files, if they haven't added indexes online.
|
239
238
|
(3...@pages).each do |page_number|
|
240
239
|
page = page(page_number)
|
241
240
|
if page.type == :INDEX && page.root?
|
242
|
-
yield
|
243
|
-
else
|
244
|
-
break
|
241
|
+
yield page_number
|
245
242
|
end
|
246
243
|
end
|
247
244
|
end
|
245
|
+
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
|
249
|
+
# Iterate through all indexes in the space.
|
250
|
+
def each_index
|
251
|
+
unless block_given?
|
252
|
+
return enum_for(:each_index)
|
253
|
+
end
|
254
|
+
|
255
|
+
each_index_root_page_number do |page_number|
|
256
|
+
yield index(page_number)
|
257
|
+
end
|
258
|
+
|
259
|
+
nil
|
248
260
|
end
|
249
261
|
|
250
262
|
# An array of Innodb::Inode list names.
|
@@ -336,6 +348,8 @@ class Innodb::Space
|
|
336
348
|
end
|
337
349
|
end
|
338
350
|
|
351
|
+
# Iterate through all pages, yielding the page number, page object,
|
352
|
+
# and page status.
|
339
353
|
def each_page_status(start_page=0)
|
340
354
|
unless block_given?
|
341
355
|
return enum_for(:each_page_with_status, start_page)
|
@@ -353,6 +367,7 @@ class Innodb::Space
|
|
353
367
|
end
|
354
368
|
end
|
355
369
|
|
370
|
+
# A helper to produce a printable page type.
|
356
371
|
def type_for_page(page, page_status)
|
357
372
|
page_status[:free] ? "FREE (#{page.type})" : page.type
|
358
373
|
end
|
data/lib/innodb/stats.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
# Collect stats globally within innodb_ruby for comparison purposes and for
|
4
|
+
# correctness checking.
|
5
|
+
class Innodb::Stats
|
6
|
+
@@data = Hash.new(0)
|
7
|
+
|
8
|
+
# Return the data hash directly.
|
9
|
+
def self.data
|
10
|
+
@@data
|
11
|
+
end
|
12
|
+
|
13
|
+
# Increment a statistic by name (typically a symbol), optionally by a value
|
14
|
+
# provided.
|
15
|
+
def self.increment(name, value=1)
|
16
|
+
@@data[name] += value
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get a statistic by name.
|
20
|
+
def self.get(name)
|
21
|
+
@@data[name]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reset all statistics.
|
25
|
+
def self.reset
|
26
|
+
@@data.clear
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Print a simple report of collected statistics, optionally to the IO object
|
31
|
+
# provided, or by default to STDOUT.
|
32
|
+
def self.print_report(io=STDOUT)
|
33
|
+
io.puts "%-50s%10s" % [
|
34
|
+
"Statistic",
|
35
|
+
"Count",
|
36
|
+
]
|
37
|
+
@@data.sort.each do |name, count|
|
38
|
+
io.puts "%-50s%10i" % [
|
39
|
+
name,
|
40
|
+
count
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
data/lib/innodb/system.rb
CHANGED
@@ -1,88 +1,168 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
|
3
|
+
# A class representing an entire InnoDB system, having a system tablespace
|
4
|
+
# and any number of attached single-table tablespaces.
|
3
5
|
class Innodb::System
|
6
|
+
# A hash of configuration options by configuration key.
|
4
7
|
attr_reader :config
|
5
|
-
|
8
|
+
|
9
|
+
# A hash of spaces by space ID.
|
10
|
+
attr_reader :spaces
|
11
|
+
|
12
|
+
# The Innodb::DataDictionary for this system.
|
13
|
+
attr_reader :data_dictionary
|
14
|
+
|
15
|
+
# The space ID of the system space, always 0.
|
16
|
+
SYSTEM_SPACE_ID = 0
|
6
17
|
|
7
18
|
def initialize(system_space_file)
|
19
|
+
@spaces = {}
|
8
20
|
@config = {
|
9
|
-
:datadir =>
|
21
|
+
:datadir => File.dirname(system_space_file),
|
10
22
|
}
|
11
|
-
|
12
|
-
|
23
|
+
|
24
|
+
add_space_file(system_space_file)
|
25
|
+
|
26
|
+
@data_dictionary = Innodb::DataDictionary.new(system_space)
|
13
27
|
end
|
14
28
|
|
29
|
+
# A helper to get the system space.
|
15
30
|
def system_space
|
16
|
-
|
31
|
+
spaces[SYSTEM_SPACE_ID]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Add an already-constructed Innodb::Space object.
|
35
|
+
def add_space(space)
|
36
|
+
unless space.is_a?(Innodb::Space)
|
37
|
+
raise "Object was not an Innodb::Space"
|
38
|
+
end
|
39
|
+
|
40
|
+
spaces[space.space_id.to_i] = space
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add a space by filename.
|
44
|
+
def add_space_file(space_file)
|
45
|
+
space = Innodb::Space.new(space_file)
|
46
|
+
space.innodb_system = self
|
47
|
+
add_space(space)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add a space by table name, constructing an appropriate filename
|
51
|
+
# from the provided table name.
|
52
|
+
def add_table(table_name)
|
53
|
+
add_space_file("%s/%s.ibd" % [config[:datadir], table_name])
|
17
54
|
end
|
18
55
|
|
19
|
-
|
56
|
+
# Return an Innodb::Space object for a given space ID, looking up
|
57
|
+
# and adding the single-table space if necessary.
|
58
|
+
def space(space_id)
|
59
|
+
return spaces[space_id] if spaces[space_id]
|
60
|
+
|
61
|
+
unless table_record = data_dictionary.table_by_space_id(space_id)
|
62
|
+
raise "Table with space ID #{space_id} not found"
|
63
|
+
end
|
64
|
+
|
65
|
+
add_table(table_record["NAME"])
|
66
|
+
|
67
|
+
spaces[space_id]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Return an Innodb::Space object by table name.
|
71
|
+
def space_by_table_name(table_name)
|
72
|
+
unless table_record = data_dictionary.table_by_name(table_name)
|
73
|
+
raise "Table #{table_name} not found"
|
74
|
+
end
|
75
|
+
|
76
|
+
if table_record["SPACE"] == 0
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
|
80
|
+
space(table_record["SPACE"])
|
81
|
+
end
|
82
|
+
|
83
|
+
# Iterate through all table names.
|
84
|
+
def each_table_name
|
20
85
|
unless block_given?
|
21
|
-
return enum_for(:
|
86
|
+
return enum_for(:each_table_name)
|
22
87
|
end
|
23
88
|
|
24
|
-
|
25
|
-
|
26
|
-
yield record
|
89
|
+
data_dictionary.each_table do |record|
|
90
|
+
yield record["NAME"]
|
27
91
|
end
|
92
|
+
|
28
93
|
nil
|
29
94
|
end
|
30
95
|
|
31
|
-
|
96
|
+
# Iterate through all column names by table name.
|
97
|
+
def each_column_name_by_table_name(table_name)
|
32
98
|
unless block_given?
|
33
|
-
return enum_for(:
|
99
|
+
return enum_for(:each_column_name_by_table_name, table_name)
|
34
100
|
end
|
35
101
|
|
36
|
-
|
37
|
-
yield record
|
102
|
+
data_dictionary.each_column_by_table_name(table_name) do |record|
|
103
|
+
yield record["NAME"]
|
38
104
|
end
|
105
|
+
|
106
|
+
nil
|
39
107
|
end
|
40
108
|
|
41
|
-
|
109
|
+
# Iterate through all index names by table name.
|
110
|
+
def each_index_name_by_table_name(table_name)
|
42
111
|
unless block_given?
|
43
|
-
return enum_for(:
|
112
|
+
return enum_for(:each_index_name_by_table_name, table_name)
|
44
113
|
end
|
45
114
|
|
46
|
-
|
47
|
-
yield record
|
115
|
+
data_dictionary.each_index_by_table_name(table_name) do |record|
|
116
|
+
yield record["NAME"]
|
48
117
|
end
|
49
|
-
end
|
50
118
|
|
51
|
-
|
52
|
-
send(method).select { |o| o[field] == value }.first
|
119
|
+
nil
|
53
120
|
end
|
54
121
|
|
55
|
-
|
56
|
-
|
57
|
-
|
122
|
+
# Iterate through all field names in a given index by table name
|
123
|
+
# and index name.
|
124
|
+
def each_index_field_name_by_index_name(table_name, index_name)
|
125
|
+
unless block_given?
|
126
|
+
return enum_for(:each_index_field_name_by_index_name,
|
127
|
+
table_name, index_name)
|
128
|
+
end
|
58
129
|
|
59
|
-
|
60
|
-
|
61
|
-
|
130
|
+
data_dictionary.each_field_by_index_name(table_name, index_name) do |record|
|
131
|
+
yield record["COL_NAME"]
|
132
|
+
end
|
62
133
|
|
63
|
-
|
64
|
-
object_by_field(:each_table, "NAME", value)
|
134
|
+
nil
|
65
135
|
end
|
66
136
|
|
67
|
-
|
68
|
-
|
137
|
+
# Return the table name given a table ID.
|
138
|
+
def table_name_by_id(table_id)
|
139
|
+
if table_record = data_dictionary.table_by_id(table_id)
|
140
|
+
table_record["NAME"]
|
141
|
+
end
|
69
142
|
end
|
70
143
|
|
71
|
-
|
72
|
-
|
73
|
-
|
144
|
+
# Return the index name given an index ID.
|
145
|
+
def index_name_by_id(index_id)
|
146
|
+
if index_record = data_dictionary.index_by_id(index_id)
|
147
|
+
index_record["NAME"]
|
74
148
|
end
|
75
|
-
|
76
|
-
object_by_two_fields(:each_index,
|
77
|
-
"TABLE_ID", table["ID"],
|
78
|
-
"NAME", index_name)
|
79
149
|
end
|
80
150
|
|
81
|
-
|
82
|
-
|
151
|
+
# Return an array of the table name and index name given an index ID.
|
152
|
+
def table_and_index_name_by_id(index_id)
|
153
|
+
if index_record = data_dictionary.index_by_id(index_id)
|
154
|
+
[table_name_by_id(index_record["TABLE_ID"]), index_record["NAME"]]
|
155
|
+
end
|
83
156
|
end
|
84
157
|
|
85
|
-
|
86
|
-
|
158
|
+
# Return an Innodb::Index object given a table name and index name.
|
159
|
+
def index_by_name(table_name, index_name)
|
160
|
+
index_record = data_dictionary.index_by_name(table_name, index_name)
|
161
|
+
|
162
|
+
index_space = space(index_record["SPACE"])
|
163
|
+
describer = data_dictionary.record_describer_by_index_name(table_name, index_name)
|
164
|
+
index = index_space.index(index_record["PAGE_NO"], describer)
|
165
|
+
|
166
|
+
index
|
87
167
|
end
|
88
168
|
end
|