innodb_ruby 0.8.1 → 0.8.5

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.
@@ -0,0 +1,360 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require "stringio"
4
+ require "bigdecimal"
5
+ require "date"
6
+
7
+ class Innodb::DataType
8
+
9
+ # MySQL's Bit-Value Type (BIT).
10
+ class BitType
11
+ attr_reader :name, :width
12
+
13
+ def initialize(base_type, modifiers, properties)
14
+ nbits = modifiers.fetch(0, 1)
15
+ raise "Unsupported width for BIT type." unless nbits >= 0 and nbits <= 64
16
+ @width = (nbits + 7) / 8
17
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
18
+ end
19
+
20
+ def value(data)
21
+ "0b%b" % BinData::const_get("Uint%dbe" % (@width * 8)).read(data)
22
+ end
23
+ end
24
+
25
+ class IntegerType
26
+ attr_reader :name, :width
27
+
28
+ def initialize(base_type, modifiers, properties)
29
+ @width = base_type_width_map[base_type]
30
+ @unsigned = properties.include?(:UNSIGNED)
31
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
32
+ end
33
+
34
+ def base_type_width_map
35
+ {
36
+ :BOOL => 1,
37
+ :BOOLEAN => 1,
38
+ :TINYINT => 1,
39
+ :SMALLINT => 2,
40
+ :MEDIUMINT => 3,
41
+ :INT => 4,
42
+ :INT6 => 6,
43
+ :BIGINT => 8,
44
+ }
45
+ end
46
+
47
+ def value(data)
48
+ nbits = @width * 8
49
+ @unsigned ? get_uint(data, nbits) : get_int(data, nbits)
50
+ end
51
+
52
+ def get_uint(data, nbits)
53
+ BinData::const_get("Uint%dbe" % nbits).read(data)
54
+ end
55
+
56
+ def get_int(data, nbits)
57
+ BinData::const_get("Int%dbe" % nbits).read(data) ^ (-1 << (nbits - 1))
58
+ end
59
+ end
60
+
61
+ class FloatType
62
+ attr_reader :name, :width
63
+
64
+ def initialize(base_type, modifiers, properties)
65
+ @width = 4
66
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
67
+ end
68
+
69
+ # Read a little-endian single-precision floating-point number.
70
+ def value(data)
71
+ BinData::FloatLe.read(data)
72
+ end
73
+ end
74
+
75
+ class DoubleType
76
+ attr_reader :name, :width
77
+
78
+ def initialize(base_type, modifiers, properties)
79
+ @width = 8
80
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
81
+ end
82
+
83
+ # Read a little-endian double-precision floating-point number.
84
+ def value(data)
85
+ BinData::DoubleLe.read(data)
86
+ end
87
+ end
88
+
89
+ # MySQL's Fixed-Point Type (DECIMAL), stored in InnoDB as a binary string.
90
+ class DecimalType
91
+ attr_reader :name, :width
92
+
93
+ # The value is stored as a sequence of signed big-endian integers, each
94
+ # representing up to 9 digits of the integral and fractional parts. The
95
+ # first integer of the integral part and/or the last integer of the
96
+ # fractional part might be compressed (or packed) and are of variable
97
+ # length. The remaining integers (if any) are uncompressed and 32 bits
98
+ # wide.
99
+ MAX_DIGITS_PER_INTEGER = 9
100
+ BYTES_PER_DIGIT = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4]
101
+
102
+ def initialize(base_type, modifiers, properties)
103
+ precision, scale = sanity_check(modifiers)
104
+ integral = precision - scale
105
+ @uncomp_integral = integral / MAX_DIGITS_PER_INTEGER
106
+ @uncomp_fractional = scale / MAX_DIGITS_PER_INTEGER
107
+ @comp_integral = integral - (@uncomp_integral * MAX_DIGITS_PER_INTEGER)
108
+ @comp_fractional = scale - (@uncomp_fractional * MAX_DIGITS_PER_INTEGER)
109
+ @width = @uncomp_integral * 4 + BYTES_PER_DIGIT[@comp_integral] +
110
+ @comp_fractional * 4 + BYTES_PER_DIGIT[@comp_fractional]
111
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
112
+ end
113
+
114
+ def value(data)
115
+ # Strings representing the integral and fractional parts.
116
+ intg, frac = "", ""
117
+
118
+ stream = StringIO.new(data)
119
+ mask = sign_mask(stream)
120
+
121
+ intg << get_digits(stream, mask, @comp_integral)
122
+
123
+ (1 .. @uncomp_integral).each do
124
+ intg << get_digits(stream, mask, MAX_DIGITS_PER_INTEGER)
125
+ end
126
+
127
+ (1 .. @uncomp_fractional).each do
128
+ frac << get_digits(stream, mask, MAX_DIGITS_PER_INTEGER)
129
+ end
130
+
131
+ frac << get_digits(stream, mask, @comp_fractional)
132
+
133
+ # Convert to something resembling a string representation.
134
+ str = mask.to_s.chop + intg + '.' + frac
135
+
136
+ BigDecimal.new(str).to_s('F')
137
+ end
138
+
139
+ private
140
+
141
+ # Ensure width specification (if any) is compliant.
142
+ def sanity_check(modifiers)
143
+ raise "Invalid width specification" unless modifiers.size <= 2
144
+ precision = modifiers.fetch(0, 10)
145
+ raise "Unsupported precision for DECIMAL type" unless
146
+ precision >= 1 and precision <= 65
147
+ scale = modifiers.fetch(1, 0)
148
+ raise "Unsupported scale for DECIMAL type" unless
149
+ scale >= 0 and scale <= 30 and scale <= precision
150
+ [precision, scale]
151
+ end
152
+
153
+ # The sign is encoded in the high bit of the first byte/digit. The byte
154
+ # might be part of a larger integer, so apply the bit-flipper and push
155
+ # back the byte into the stream.
156
+ def sign_mask(stream)
157
+ byte = BinData::Uint8.read(stream)
158
+ sign = byte & 0x80
159
+ byte.assign(byte ^ 0x80)
160
+ stream.rewind
161
+ byte.write(stream)
162
+ stream.rewind
163
+ (sign == 0) ? -1 : 0
164
+ end
165
+
166
+ # Return a string representing an integer with a specific number of digits.
167
+ def get_digits(stream, mask, digits)
168
+ nbits = BYTES_PER_DIGIT[digits] * 8
169
+ return "" unless nbits > 0
170
+ value = (BinData::const_get("Int%dbe" % nbits).read(stream) ^ mask)
171
+ # Preserve leading zeros.
172
+ ("%0" + digits.to_s + "d") % value
173
+ end
174
+ end
175
+
176
+ # Fixed-length character type.
177
+ class CharacterType
178
+ attr_reader :name, :width
179
+
180
+ def initialize(base_type, modifiers, properties)
181
+ @width = modifiers.fetch(0, 1)
182
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
183
+ end
184
+ end
185
+
186
+ class VariableCharacterType
187
+ attr_reader :name, :width
188
+
189
+ def initialize(base_type, modifiers, properties)
190
+ @width = modifiers[0]
191
+ raise "Invalid width specification" unless modifiers.size == 1
192
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
193
+ end
194
+
195
+ def value(data)
196
+ # The SQL standard defines that VARCHAR fields should have end-spaces
197
+ # stripped off.
198
+ data.sub(/[ ]+$/, "")
199
+ end
200
+ end
201
+
202
+ # Fixed-length binary type.
203
+ class BinaryType
204
+ attr_reader :name, :width
205
+
206
+ def initialize(base_type, modifiers, properties)
207
+ @width = modifiers.fetch(0, 1)
208
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
209
+ end
210
+ end
211
+
212
+ class VariableBinaryType
213
+ attr_reader :name, :width
214
+
215
+ def initialize(base_type, modifiers, properties)
216
+ @width = modifiers[0]
217
+ raise "Invalid width specification" unless modifiers.size == 1
218
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
219
+ end
220
+ end
221
+
222
+ class BlobType
223
+ attr_reader :name
224
+
225
+ def initialize(base_type, modifiers, properties)
226
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
227
+ end
228
+ end
229
+
230
+ class YearType
231
+ attr_reader :name, :width
232
+
233
+ def initialize(base_type, modifiers, properties)
234
+ @width = 1
235
+ @display_width = modifiers.fetch(0, 4)
236
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
237
+ end
238
+
239
+ def value(data)
240
+ year = BinData::Uint8.read(data)
241
+ return (year % 100).to_s if @display_width != 4
242
+ return (year + 1900).to_s if year != 0
243
+ "0000"
244
+ end
245
+ end
246
+
247
+ class TimeType
248
+ attr_reader :name, :width
249
+
250
+ def initialize(base_type, modifiers, properties)
251
+ @width = 3
252
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
253
+ end
254
+
255
+ def value(data)
256
+ time = BinData::Int24be.read(data) ^ (-1 << 23)
257
+ sign = "-" if time < 0
258
+ time = time.abs
259
+ "%s%02d:%02d:%02d" % [sign, time / 10000, (time / 100) % 100, time % 100]
260
+ end
261
+ end
262
+
263
+ class DateType
264
+ attr_reader :name, :width
265
+
266
+ def initialize(base_type, modifiers, properties)
267
+ @width = 3
268
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
269
+ end
270
+
271
+ def value(data)
272
+ date = BinData::Int24be.read(data) ^ (-1 << 23)
273
+ day = date & 0x1f
274
+ month = (date >> 5) & 0xf
275
+ year = date >> 9
276
+ "%04d-%02d-%02d" % [year, month, day]
277
+ end
278
+ end
279
+
280
+ class DatetimeType
281
+ attr_reader :name, :width
282
+
283
+ def initialize(base_type, modifiers, properties)
284
+ @width = 8
285
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
286
+ end
287
+
288
+ def value(data)
289
+ datetime = BinData::Int64be.read(data) ^ (-1 << 63)
290
+ date = datetime / 1000000
291
+ year, month, day = [date / 10000, (date / 100) % 100, date % 100]
292
+ time = datetime - (date * 1000000)
293
+ hour, min, sec = [time / 10000, (time / 100) % 100, time % 100]
294
+ "%04d-%02d-%02d %02d:%02d:%02d" % [year, month, day, hour, min, sec]
295
+ end
296
+ end
297
+
298
+ class TimestampType
299
+ attr_reader :name, :width
300
+
301
+ def initialize(base_type, modifiers, properties)
302
+ @width = 4
303
+ @name = Innodb::DataType.make_name(base_type, modifiers, properties)
304
+ end
305
+
306
+ # Returns the UTC timestamp as a value in 'YYYY-MM-DD HH:MM:SS' format.
307
+ def value(data)
308
+ timestamp = BinData::Uint32be.read(data)
309
+ return "0000-00-00 00:00:00" if timestamp.zero?
310
+ DateTime.strptime(timestamp.to_s, '%s').strftime "%Y-%m-%d %H:%M:%S"
311
+ end
312
+ end
313
+
314
+ # Maps base type to data type class.
315
+ TYPES = {
316
+ :BIT => BitType,
317
+ :BOOL => IntegerType,
318
+ :BOOLEAN => IntegerType,
319
+ :TINYINT => IntegerType,
320
+ :SMALLINT => IntegerType,
321
+ :MEDIUMINT => IntegerType,
322
+ :INT => IntegerType,
323
+ :INT6 => IntegerType,
324
+ :BIGINT => IntegerType,
325
+ :FLOAT => FloatType,
326
+ :DOUBLE => DoubleType,
327
+ :DECIMAL => DecimalType,
328
+ :NUMERIC => DecimalType,
329
+ :CHAR => CharacterType,
330
+ :VARCHAR => VariableCharacterType,
331
+ :BINARY => BinaryType,
332
+ :VARBINARY => VariableBinaryType,
333
+ :TINYBLOB => BlobType,
334
+ :BLOB => BlobType,
335
+ :MEDIUMBLOB => BlobType,
336
+ :LONGBLOB => BlobType,
337
+ :TINYTEXT => BlobType,
338
+ :TEXT => BlobType,
339
+ :MEDIUMTEXT => BlobType,
340
+ :LONGTEXT => BlobType,
341
+ :YEAR => YearType,
342
+ :TIME => TimeType,
343
+ :DATE => DateType,
344
+ :DATETIME => DatetimeType,
345
+ :TIMESTAMP => TimestampType,
346
+ }
347
+
348
+ def self.make_name(base_type, modifiers, properties)
349
+ name = base_type.to_s
350
+ name << '(' + modifiers.join(',') + ')' if not modifiers.empty?
351
+ name << " "
352
+ name << properties.join(' ')
353
+ name.strip
354
+ end
355
+
356
+ def self.new(base_type, modifiers, properties)
357
+ raise "Data type '#{base_type}' is not supported" unless TYPES.key?(base_type)
358
+ TYPES[base_type].new(base_type, modifiers, properties)
359
+ end
360
+ end
data/lib/innodb/field.rb CHANGED
@@ -1,23 +1,32 @@
1
1
  # -*- encoding : utf-8 -*-
2
- require "innodb/field_type"
2
+
3
+ require "innodb/data_type"
3
4
 
4
5
  # A single field in an InnoDB record (within an INDEX page). This class
5
6
  # provides essential information to parse records, including the length
6
7
  # of the fixed-width and variable-width portion of the field.
7
8
  class Innodb::Field
8
- attr_reader :position, :type
9
+ attr_reader :position, :name, :data_type, :nullable
9
10
 
10
11
  # Size of a reference to data stored externally to the page.
11
12
  EXTERN_FIELD_SIZE = 20
12
13
 
13
- def initialize(position, data_type, *properties)
14
+ def initialize(position, name, type_definition, *properties)
14
15
  @position = position
15
- @type = Innodb::FieldType.new(data_type.to_s, properties)
16
+ @name = name
17
+ @nullable = properties.delete(:NOT_NULL) ? false : true
18
+ base_type, modifiers = parse_type_definition(type_definition.to_s)
19
+ @data_type = Innodb::DataType.new(base_type, modifiers, properties)
20
+ end
21
+
22
+ # Return whether this field can be NULL.
23
+ def nullable?
24
+ @nullable
16
25
  end
17
26
 
18
27
  # Return whether this field is NULL.
19
28
  def null?(record)
20
- type.nullable? && record[:header][:field_nulls][position]
29
+ nullable? && record[:header][:field_nulls][position]
21
30
  end
22
31
 
23
32
  # Return whether a part of this field is stored externally (off-page).
@@ -25,25 +34,71 @@ class Innodb::Field
25
34
  record[:header][:field_externs][position]
26
35
  end
27
36
 
37
+ def variable?
38
+ @data_type.is_a? Innodb::DataType::BlobType or
39
+ @data_type.is_a? Innodb::DataType::VariableBinaryType or
40
+ @data_type.is_a? Innodb::DataType::VariableCharacterType
41
+ end
42
+
43
+ def blob?
44
+ @data_type.is_a? Innodb::DataType::BlobType
45
+ end
46
+
28
47
  # Return the actual length of this variable-length field.
29
48
  def length(record)
30
- if type.variable?
49
+ if variable?
31
50
  len = record[:header][:field_lengths][position]
32
51
  else
33
- len = type.length
52
+ len = @data_type.width
34
53
  end
35
54
  extern?(record) ? len - EXTERN_FIELD_SIZE : len
36
55
  end
37
56
 
38
57
  # Read an InnoDB encoded data field.
39
58
  def read(record, cursor)
59
+ cursor.name(@data_type.name) { cursor.get_bytes(length(record)) }
60
+ end
61
+
62
+ # Read the data value (e.g. encoded in the data).
63
+ def value(record, cursor)
40
64
  return :NULL if null?(record)
41
- cursor.name(type.name) { type.reader.read(cursor, length(record)) }
65
+ data = read(record, cursor)
66
+ @data_type.respond_to?(:value) ? @data_type.value(data) : data
42
67
  end
43
68
 
44
69
  # Read an InnoDB external pointer field.
45
- def read_extern(record, cursor)
70
+ def extern(record, cursor)
46
71
  return nil if not extern?(record)
47
- cursor.name(type.name) { type.reader.read_extern(cursor) }
72
+ cursor.name(@name) { read_extern(cursor) }
73
+ end
74
+
75
+ private
76
+
77
+ # Return an external reference field. An extern field contains the page
78
+ # address and the length of the externally stored part of the record data.
79
+ def get_extern_reference(cursor)
80
+ {
81
+ :space_id => cursor.name("space_id") { cursor.get_uint32 },
82
+ :page_number => cursor.name("page_number") { cursor.get_uint32 },
83
+ :offset => cursor.name("offset") { cursor.get_uint32 },
84
+ :length => cursor.name("length") { cursor.get_uint64 & 0x3fffffff }
85
+ }
86
+ end
87
+
88
+ def read_extern(cursor)
89
+ cursor.name("extern") { get_extern_reference(cursor) }
90
+ end
91
+
92
+ # Parse a data type definition and extract the base type and any modifiers.
93
+ def parse_type_definition(type_string)
94
+ if matches = /^([a-zA-Z0-9]+)(\(([0-9, ]+)\))?$/.match(type_string)
95
+ base_type = matches[1].upcase.to_sym
96
+ if matches[3]
97
+ modifiers = matches[3].sub(/[ ]/, "").split(/,/).map { |s| s.to_i }
98
+ else
99
+ modifiers = []
100
+ end
101
+ [base_type, modifiers]
102
+ end
48
103
  end
49
104
  end
@@ -1,6 +1,8 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  # An InnoDB file segment entry, which appears in a few places, such as the
3
4
  # FSEG header of INDEX pages, and in the TRX_SYS pages.
5
+
4
6
  class Innodb::FsegEntry
5
7
  # The size (in bytes) of an FSEG entry, which contains a two 32-bit integers
6
8
  # and a 16-bit integer.
data/lib/innodb/index.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  # An InnoDB index B-tree, given an Innodb::Space and a root page number.
3
4
  class Innodb::Index
4
5
  attr_reader :root
@@ -97,6 +98,10 @@ class Innodb::Index
97
98
  @root.fseg_header[name]
98
99
  end
99
100
 
101
+ def field_names
102
+ record_describer.field_names
103
+ end
104
+
100
105
  # Iterate through all file segments in the index.
101
106
  def each_fseg
102
107
  unless block_given?
data/lib/innodb/inode.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  class Innodb::Inode
3
4
  # The number of "slots" (each representing one page) in the fragment array
4
5
  # within each Inode entry.
data/lib/innodb/list.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  # An abstract InnoDB "free list" or FLST (renamed to just "list" here as it
3
4
  # frequently is used for structures that aren't free lists). This class must
4
5
  # be sub-classed to provide an appropriate #object_from_address method.
6
+
5
7
  class Innodb::List
6
8
  # An "address", which consists of a page number and byte offset within the
7
9
  # page. This points to the list "node" pointers (prev and next) of the
data/lib/innodb/log.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  # An InnoDB transaction log file.
4
+
3
5
  class Innodb::Log
4
6
  HEADER_SIZE = 4 * Innodb::LogBlock::BLOCK_SIZE
5
7
  HEADER_START = 0
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  require "innodb/cursor"
3
4
  require "pp"
4
5
 
@@ -0,0 +1,79 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ class Innodb::LogGroup
4
+ def initialize
5
+ @files = []
6
+ end
7
+
8
+ def add_log(file)
9
+ if file = Innodb::Log.new(file)
10
+ @files.push file
11
+ else
12
+ raise "Couldn't open #{file}"
13
+ end
14
+ end
15
+
16
+ def each_block
17
+ @files.each do |file|
18
+ file.each_block do |block_number, block|
19
+ yield block_number, block
20
+ end
21
+ end
22
+ end
23
+
24
+ def current_tail_position
25
+ max = 0
26
+ max_file = nil
27
+ max_block = nil
28
+
29
+ @files.each_with_index do |file, file_number|
30
+ file.each_block do |block_number, block|
31
+ if block.header[:block] > max
32
+ max = block.header[:block]
33
+ max_file = file_number
34
+ max_block = block_number
35
+ end
36
+ end
37
+ end
38
+
39
+ { :file => max_file, :block => max_block }
40
+ end
41
+
42
+ def successor_position(position)
43
+ if position[:block] == @files[position[:file]].blocks
44
+ if position[:file] == @files.size
45
+ { :file => 0, :block => 0 }
46
+ else
47
+ { :file => position[:file] + 1, :block => 0 }
48
+ end
49
+ else
50
+ { :file => position[:file], :block => position[:block] + 1 }
51
+ end
52
+ end
53
+
54
+ def block(file_number, block_number)
55
+ @files[file_number].block(block_number)
56
+ end
57
+
58
+ def block_if_newer(old_block, new_block)
59
+ return new_block if old_block.nil?
60
+ #puts "old: #{old_block.header[:block]} new: #{new_block.header[:block]}"
61
+ if new_block.header[:block] >= old_block.header[:block]
62
+ new_block
63
+ end
64
+ end
65
+
66
+ def tail_blocks
67
+ position = current_tail_position
68
+ current_block = nil
69
+ while true
70
+ until block_if_newer(current_block, new_block = block(position[:file], position[:block]))
71
+ #puts "Waiting at the tail: #{position[:file]} #{position[:block]}"
72
+ sleep 0.1
73
+ end
74
+ yield new_block
75
+ position = successor_position(position)
76
+ current_block = new_block
77
+ end
78
+ end
79
+ end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  require "innodb/list"
3
4
  require "innodb/xdes"
4
5