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.
@@ -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: the length of the fixed
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, :nullable, :fixed_len, :variable_len
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
- # Parse the data type description string of a field.
15
- def parse_data_type(data_type)
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
- # Maps data type to fixed storage length.
31
- def fixed_len_map
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
- case record[:format]
44
- when :compact
45
- header = record[:header]
46
- header[:null_bitmap][@position]
47
- end
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 get_variable_len(record)
52
- case record[:format]
53
- when :compact
54
- header = record[:header]
55
- header[:variable_length][@position]
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 @nullable and null?(record)
40
+ return :NULL if null?(record)
41
+ cursor.name(type.name) { type.reader.read(cursor, length(record)) }
42
+ end
62
43
 
63
- case @type
64
- when :TINYINT, :SMALLINT, :MEDIUMINT, :INT, :BIGINT
65
- symbol = @unsigned ? :get_uint_by_size : :get_i_sint_by_size
66
- cursor.send(symbol, @fixed_len)
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
@@ -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.get_uint32,
13
- :offset => cursor.get_uint16,
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
@@ -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 = @space.page(child_page_number)
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 = @space.page(record[:child_page_number])
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
- [:full, :not_full, :free].each do |list_name|
108
- yield list_name, fseg[list_name] if fseg[list_name].is_a?(Innodb::List)
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[:frag_array].each do |page_number|
119
- yield page_number, @space.page(page_number) if 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
- page = @space.page(page.next)
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 = @space.page(rec[:child_page_number])
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 = @space.page(rec[:child_page_number])
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
@@ -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