innodb_ruby 0.8.1 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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