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.
@@ -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