diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ca998f87 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + time: "13:00" + open-pull-requests-limit: 10 + ignore: + - dependency-name: rubocop + versions: + - 1.10.0 + - 1.12.0 + - 1.12.1 + - 1.9.0 + - 1.9.1 diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml new file mode 100644 index 00000000..73185e63 --- /dev/null +++ b/.github/workflows/rspec.yml @@ -0,0 +1,23 @@ +name: rspec + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + rspec: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ["3.0", "3.1", "3.2", "3.3"] + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run rspec tests + run: bundle exec rspec spec diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 00000000..752d2862 --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,23 @@ +name: rubocop + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + rubocop: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ["3.0", "3.1", "3.2", "3.3"] + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run rubocop style check + run: bundle exec rubocop diff --git a/.gitignore b/.gitignore index c529daea..7d50bc35 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ *.page_info ._* tags +vendor +.bundle +.idea +.byebug_history diff --git a/.rubocop.yml b/.rubocop.yml index 32a1e4b6..f0fa685f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,27 @@ +# TODO: Enable rubocop-rspec only along with all the fixes it demands. +# require: rubocop-rspec + AllCops: TargetRubyVersion: 2.6 + NewCops: enable +Gemspec/DevelopmentDependencies: + Enabled: false +Gemspec/RequireMFA: + Enabled: false Lint/AmbiguousOperator: Enabled: false +Lint/FormatParameterMismatch: + Enabled: false Layout/LineLength: Max: 120 Metrics/BlockLength: Max: 400 +Naming/VariableNumber: + Enabled: false +Style/AccessorGrouping: + EnforcedStyle: separated +Style/StringLiterals: + EnforcedStyle: double_quotes Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: consistent_comma Style/TrailingCommaInHashLiteral: @@ -24,9 +40,11 @@ Metrics/AbcSize: Enabled: false Metrics/CyclomaticComplexity: Enabled: false +Metrics/ParameterLists: + Max: 10 Metrics/PerceivedComplexity: Enabled: false Style/SymbolArray: MinSize: 1 Style/EmptyCaseCondition: - Enabled: false + Enabled: false \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fe1960f1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: ruby -rvm: - - 2.6 - - 2.7 -script: bundle exec rspec spec diff --git a/Gemfile b/Gemfile index 7f4f5e95..be173b20 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 369623b8..d7a7040d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,59 +1,85 @@ PATH remote: . specs: - innodb_ruby (0.10.1) - bindata (~> 1.4, >= 1.4.5) + innodb_ruby (0.14.0) + bigdecimal (~> 3.1.8) + bindata (>= 1.4.5, < 3.0) + csv (~> 3.3) digest-crc (~> 0.4, >= 0.4.1) + getoptlong (~> 0.2.1) histogram (~> 0.2) GEM remote: https://rubygems.org/ specs: - ast (2.4.0) - bindata (1.8.3) - diff-lcs (1.3) - digest-crc (0.5.1) + ast (2.4.2) + bigdecimal (3.1.8) + bindata (2.5.0) + csv (3.3.0) + diff-lcs (1.5.1) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + getoptlong (0.2.1) gnuplot (2.6.2) histogram (0.2.4.1) - parallel (1.19.1) - parser (2.7.1.2) - ast (~> 2.4.0) - rainbow (3.0.0) - rexml (3.2.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) + json (2.7.5) + language_server-protocol (3.17.0.3) + parallel (1.26.3) + parser (3.3.5.0) + ast (~> 2.4.1) + racc + racc (1.8.1) + rainbow (3.1.1) + rake (13.2.1) + regexp_parser (2.9.2) + rspec (3.11.0) + rspec-core (~> 3.11.0) + rspec-expectations (~> 3.11.0) + rspec-mocks (~> 3.11.0) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-support (3.9.3) - rubocop (0.84.0) + rspec-support (~> 3.11.0) + rspec-support (3.11.1) + rubocop (1.67.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 2.7.0.1) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - rexml - rubocop-ast (>= 0.0.3) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.0.3) - parser (>= 2.7.0.1) - ruby-progressbar (1.10.1) - unicode-display_width (1.7.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.33.0) + parser (>= 3.3.1.0) + rubocop-capybara (2.21.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.26.1) + rubocop (~> 1.61) + rubocop-rspec (2.31.0) + rubocop (~> 1.40) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + rubocop-rspec_rails (~> 2.28) + rubocop-rspec_rails (2.29.1) + rubocop (~> 1.61) + ruby-progressbar (1.13.0) + unicode-display_width (2.6.0) PLATFORMS ruby DEPENDENCIES - gnuplot + gnuplot (~> 2.6.0) innodb_ruby! - rspec - rubocop + rspec (~> 3.11.0) + rubocop (~> 1.18) + rubocop-rspec (~> 2.4) BUNDLED WITH - 2.1.4 + 2.5.22 diff --git a/README.md b/README.md index b58ce476..ee008abc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # A parser for InnoDB file formats, in Ruby +[![rspec test status](https://github.com/jeremycole/innodb_ruby/actions/workflows/rspec.yml/badge.svg)](https://github.com/jeremycole/innodb_ruby/actions/workflows/rspec.yml) +[![rubocop style check status](https://github.com/jeremycole/innodb_ruby/actions/workflows/rubocop.yml/badge.svg)](https://github.com/jeremycole/innodb_ruby/actions/workflows/rubocop.yml) + The purpose for this library and tools is to expose some otherwise hidden internals of InnoDB. This code is not intended for critical production usage. It is definitely buggy, and it may be dangerous. Neither its internal APIs or its output are considered stable and are subject to change at any time. It is intended as for a few purposes: @@ -15,5 +18,3 @@ Various parts of this library and the tools included may have wildly differing m * The [innodb_ruby wiki](https://github.com/jeremycole/innodb_ruby/wiki) contains some additional references and documentation to help you get started. * See the [RubyGems page for innodb_ruby](http://rubygems.org/gems/innodb_ruby) — Gem packaged releases are published regularly to RubyGems.org, which also provides online documentation. - -[![Build Status](https://travis-ci.org/jeremycole/innodb_ruby.svg?branch=master)](https://travis-ci.org/jeremycole/innodb_ruby) diff --git a/bin/innodb_log b/bin/innodb_log index d3a2a518..9954d0a6 100755 --- a/bin/innodb_log +++ b/bin/innodb_log @@ -1,17 +1,16 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'getoptlong' -require 'ostruct' -require 'set' -require 'innodb' +require "getoptlong" +require "set" +require "innodb" def log_summary(log_group) - puts '%-20s%-15s%-10s%-12s%-10s' % %w[lsn block length first_rec checkpoint] + puts "%-20s%-15s%-10s%-12s%-10s" % %w[lsn block length first_rec checkpoint] lsn = log_group.start_lsn.first log_group.each_block do |_block_index, block| header = block.header - puts '%-20i%-15i%-10i%-12i%-10i' % [ + puts "%-20i%-15i%-10i%-12i%-10i" % [ lsn, header[:block_number], header[:data_length], @@ -24,13 +23,13 @@ def log_summary(log_group) end def log_reader_record_summary(reader, follow) - puts '%-10s%-10s%-20s%-10s%-10s%-10s' % %w[time lsn type size space page] + puts "%-10s%-10s%-20s%-10s%-10s%-10s" % %w[time lsn type size space page] reader.each_record(follow) do |rec| preamble = rec.preamble.dup - preamble.default = '' - puts '%-10s%-10i%-20s%-10i%-10s%-10s' % [ - (Time.now.strftime '%H:%M:%S'), + preamble.default = "" + puts "%-10s%-10i%-20s%-10i%-10s%-10s" % [ + (Time.now.strftime "%H:%M:%S"), rec.lsn.first, preamble[:type].to_s, rec.size, @@ -60,7 +59,7 @@ def usage(exit_code, message = nil) print "Error: #{message}\n" unless message.nil? # rubocop:disable Layout/HeredocIndentation - print <<'END_OF_USAGE' + print <] -f @@ -96,17 +95,24 @@ END_OF_USAGE exit exit_code end -@options = OpenStruct.new +InnodbLogOptions = Struct.new( + :log_files, + :dump, + :lsn, + keyword_init: true +) + +@options = InnodbLogOptions.new @options.log_files = [] @options.dump = false @options.lsn = nil # rubocop:disable Layout/SpaceInsideArrayLiteralBrackets getopt_options = [ - [ '--help', '-?', GetoptLong::NO_ARGUMENT ], - [ '--log-file', '-f', GetoptLong::REQUIRED_ARGUMENT ], - [ '--dump-blocks', '-d', GetoptLong::NO_ARGUMENT ], - [ '--lsn', '-l', GetoptLong::REQUIRED_ARGUMENT ], + [ "--help", "-?", GetoptLong::NO_ARGUMENT ], + [ "--log-file", "-f", GetoptLong::REQUIRED_ARGUMENT ], + [ "--dump-blocks", "-d", GetoptLong::NO_ARGUMENT ], + [ "--lsn", "-l", GetoptLong::REQUIRED_ARGUMENT ], ] # rubocop:enable Layout/SpaceInsideArrayLiteralBrackets @@ -114,13 +120,13 @@ getopt = GetoptLong.new(*getopt_options) getopt.each do |opt, arg| case opt - when '--help' + when "--help" usage 0 - when '--log-file' + when "--log-file" @options.log_files << arg - when '--dump-blocks' + when "--dump-blocks" @options.dump = true - when '--lsn' + when "--lsn" @options.lsn = arg.to_i end end @@ -130,15 +136,15 @@ mode = ARGV.shift # rubocop:disable Style/IfUnlessModifier unless mode - usage 1, 'At least one mode must be provided' + usage 1, "At least one mode must be provided" end if @options.log_files.empty? - usage 1, 'At least one log file (-f) must be specified' + usage 1, "At least one log file (-f) must be specified" end if /^(log-)?record-/.match(mode) && !@options.lsn - usage 1, 'LSN must be specified using -l/--lsn' + usage 1, "LSN must be specified using -l/--lsn" end # rubocop:enable Style/IfUnlessModifier @@ -146,13 +152,13 @@ end log_group = Innodb::LogGroup.new(@options.log_files.sort) case mode -when 'log-summary' +when "log-summary" log_summary(log_group) -when 'log-follow-tail-summary' +when "log-follow-tail-summary" log_follow_tail_summary(log_group) -when 'log-record-summary' +when "log-record-summary" log_record_summary(log_group, @options.lsn) -when 'record-dump' +when "record-dump" record_dump(log_group, @options.lsn) else usage 1, "Unknown mode: #{mode}" diff --git a/bin/innodb_ruby_generate_mysql_collations b/bin/innodb_ruby_generate_mysql_collations new file mode 100755 index 00000000..5bb91dda --- /dev/null +++ b/bin/innodb_ruby_generate_mysql_collations @@ -0,0 +1,96 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# To update lib/innodb/mysql_collations.rb, run this with a path to the MySQL source directory +# containing CHARSET_INFO collation definitions, e.g.: + +# bundle exec bin/innodb_ruby_generate_mysql_collations ~/git/mysql-server > lib/innodb/mysql_collations.rb + +MysqlCharsetInfo = Struct.new( + :number, + :primary_number, + :binary_number, + :state, + :csname, + :m_coll_name, + :comment, + :tailoring, + :coll_param, + :ctype, + :to_lower, + :to_upper, + :sort_order, + :uca, + :tab_to_uni, + :tab_from_uni, + :caseinfo, + :state_maps, + :ident_map, + :strxfrm_multiply, + :caseup_multiply, + :casedn_multiply, + :mbminlen, + :mbmaxlen, + :mbmaxlenlen, + :min_sort_char, + :max_sort_char, + :pad_char, + :escape_with_backslash_is_dangerous, + :levels_for_compare, + :cset, + :coll, + :pad_attribute +) + +charset_infos = [] + +raise "First argument must be the path to a modern MySQL source tree" unless (ARGV.size == 1) && Dir.exist?(ARGV[0]) + +Dir.glob(File.join(ARGV[0], "strings/ctype-**.cc")).each do |filename| + content = File.read(filename) + warn "Parsing #{filename}..." + + # Global individual constants e.g. CHARSET_INFO my_charset_utf8mb4_general_ci = { ... } + charset_info_strings = content.scan(/^CHARSET_INFO \w+ = ({.*?})/m).flatten + + # Global array of constants e.g. CHARSET_INFO compiled_charsets[] = { { ... }, { ... } }; + content.match(/CHARSET_INFO \w+\[\] = {\s*(?:{.*?}\s*,\s*)+/m) + &.match(0) + &.gsub(/CHARSET_INFO \w+\[\] = {/, "") + &.scan(/{.*?}/m) + &.each do |s| + charset_info_strings.push(s) + end + + charset_info_strings = charset_info_strings.map do |x| + x.gsub(%r{/\*.*?\*/}, "").gsub(%r{//.*?$}, "").gsub(/\s+/, " ").gsub(/["']/, "") + end + + charset_infos += charset_info_strings.map do |charset_info_string| + matches = charset_info_string.match(/{(?.*?)}/) + + MysqlCharsetInfo.new(*matches[:definition].split(",").map(&:strip).map { |x| x =~ /^[0-9]+$/ ? x.to_i : x }) + end +end + +if charset_infos.empty? + warn "No MySQL collations found... bad path provided?" + exit 1 +end + +warn "Found #{charset_infos.size} collations, generating output." + +puts "# frozen_string_literal: true" +puts +puts "# Generated at #{Time.now.utc} using innodb_ruby_generate_mysql_collations. Do not edit!" +puts + +puts "# rubocop:disable all" +charset_infos.sort_by(&:number).each do |charset_info| + puts format("Innodb::MysqlCollation.add(id: %d, name: %s, character_set_name: %s, mbminlen: %i, mbmaxlen: %i)", + charset_info.number, + charset_info.m_coll_name.inspect, + charset_info.csname.inspect, + charset_info.mbminlen, + charset_info.mbmaxlen) +end diff --git a/bin/innodb_space b/bin/innodb_space index 83f1444f..edcd107e 100755 --- a/bin/innodb_space +++ b/bin/innodb_space @@ -1,10 +1,21 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'getoptlong' -require 'ostruct' -require 'histogram/array' -require 'innodb' +require "getoptlong" +require "histogram/array" +require "innodb" + +class String + def squish! + gsub!(/[[:space:]]+/, " ") + strip! + self + end + + def squish + dup.squish! + end +end # Convert a floating point RGB array into an ANSI color number approximating it. def rgb_to_ansi(rgb) @@ -59,8 +70,8 @@ def ansi_color(color, text) end # Zero and 1/8 through 8/8 illustrations. -BLOCK_CHARS_V = '░▁▂▃▄▅▆▇█'.split('').freeze -BLOCK_CHARS_H = '░▏▎▍▌▋▊▉█'.split('').freeze +BLOCK_CHARS_V = "░▁▂▃▄▅▆▇█".chars.freeze +BLOCK_CHARS_H = "░▏▎▍▌▋▊▉█".chars.freeze # A reasonably large prime number to multiply identifiers by in order to # space out the colors used for similar identifiers. @@ -69,7 +80,7 @@ COLOR_SPACING_PRIME = 999_983 # Return a string with a possibly colored filled block for use in printing # to an ANSI-capable Unicode-enabled terminal. def filled_block(fraction, identifier = nil, block_chars = BLOCK_CHARS_V) - if fraction == 0.0 + if fraction.zero? block = block_chars[0] else parts = (fraction.to_f * (block_chars.size.to_f - 1)).floor @@ -88,12 +99,12 @@ def center(text, width) return text if text.size >= width spaces = (width - text.size) / 2 - (' ' * spaces) + text + (' ' * spaces) + (" " * spaces) + text + (" " * spaces) end # Print metadata about each list in an array of InnoDB::List objects. def print_lists(lists) - puts '%-20s%-12s%-12s%-12s%-12s%-12s' % %w[ + puts "%-20s%-12s%-12s%-12s%-12s%-12s" % %w[ name length f_page @@ -103,13 +114,13 @@ def print_lists(lists) ] lists.each do |name, list| - puts '%-20s%-12i%-12i%-12i%-12i%-12i' % [ + puts "%-20s%-12i%-12i%-12i%-12i%-12i" % [ name, list.base[:length], - list.base[:first] && list.base[:first][:page] || 0, - list.base[:first] && list.base[:first][:offset] || 0, - list.base[:last] && list.base[:last][:page] || 0, - list.base[:last] && list.base[:last][:offset] || 0, + (list.base[:first] && list.base[:first][:page]) || 0, + (list.base[:first] && list.base[:first][:offset]) || 0, + (list.base[:last] && list.base[:last][:page]) || 0, + (list.base[:last] && list.base[:last][:offset]) || 0, ] end end @@ -117,25 +128,25 @@ end # Print a page usage bitmap for each extent descriptor in an array of # Innodb::XdesEntry objects. def print_xdes_list(space, list) - puts '%-12s%-64s' % %w[ + puts "%-12s%-64s" % %w[ start_page page_used_bitmap ] list.each do |entry| - display = entry.each_page_status.inject('') do |bitmap, (page_number, page_status)| + display = entry.each_page_status.inject("") do |bitmap, (page_number, page_status)| if page_number < space.pages - bitmap += page_status[:free] ? '.' : '#' + bitmap += page_status[:free] ? "." : "#" end bitmap end - puts '%-12i%-64s' % [entry.xdes[:start_page], display] + puts "%-12i%-64s" % [entry.xdes[:start_page], display] end end # Print a summary of page usage for all pages in an index. def print_index_page_summary(pages) - puts '%-12s%-8s%-8s%-8s%-8s%-8s' % %w[ + puts "%-12s%-8s%-8s%-8s%-8s%-8s" % %w[ page index level @@ -145,9 +156,9 @@ def print_index_page_summary(pages) ] pages.each do |page_number, page| - case page.type - when :INDEX - puts '%-12i%-8i%-8i%-8i%-8i%-8i' % [ + case + when page.is_a?(Innodb::Page::Index) + puts "%-12i%-8i%-8i%-8i%-8i%-8i" % [ page_number, page.page_header[:index_id], page.level, @@ -155,138 +166,84 @@ def print_index_page_summary(pages) page.free_space, page.records, ] - when :ALLOCATED - puts '%-12i%-8i%-8i%-8i%-8i%-8i' % [page_number, 0, 0, 0, page.size, 0] + when page.type == :ALLOCATED + puts "%-12i%-8i%-8i%-8i%-8i%-8i" % [page_number, 0, 0, 0, page.size, 0] end end end # Print a summary of all spaces in the InnoDB system. def system_spaces(innodb_system) - puts '%-32s%-12s%-12s' % %w[ + puts "%-64s%-12s" % %w[ name pages - indexes ] - print_space_information = lambda do |name, space| - puts '%-32s%-12i%-12i' % [ - name, + innodb_system&.each_space do |space| + puts "%-64s%-12i" % [ + space.name.gsub("#{innodb_system.data_directory}/", ""), space.pages, - space.each_index.to_a.size, ] end - print_space_information.call('(system)', innodb_system.system_space) - - innodb_system.each_table_name do |table_name| - space = innodb_system.space_by_table_name(table_name) - next unless space - - print_space_information.call(table_name, space) - end - innodb_system.each_orphan do |table_name| - puts '%-43s (orphan/tmp)' % table_name + puts "%-64s%-12s" % [table_name, "(orphan/tmp)"] end end -# Print the contents of the SYS_TABLES data dictionary table. +# Print the contents of the table list from the data dictionary. def data_dictionary_tables(innodb_system) - puts '%-32s%-12s%-12s%-12s%-12s%-12s%-15s%-12s' % %w[ + puts "%-64s%-12s%-12s" % %w[ name - id - n_cols - type - mix_id - mix_len - cluster_name - space + columns + indexes ] - innodb_system.data_dictionary.each_table do |record| - puts '%-32s%-12i%-12i%-12i%-12i%-12i%-15s%-12i' % [ - record['NAME'], - record['ID'], - record['N_COLS'], - record['TYPE'], - record['MIX_ID'], - record['MIX_LEN'], - record['CLUSTER_NAME'], - record['SPACE'], + innodb_system.data_dictionary.tables.each do |table| + puts "%-64s%-12i%-12i" % [ + table.name, + table.columns.count, + table.indexes.count, ] end end -# Print the contents of the SYS_COLUMNS data dictionary table. +# Print the contents of the column list from the data dictionary. def data_dictionary_columns(innodb_system) - puts '%-12s%-6s%-32s%-12s%-12s%-6s%-6s' % %w[ - table_id - pos + puts "%-64s%-32s%-32s" % %w[ + table name - mtype - prtype - len - prec + description ] - innodb_system.data_dictionary.each_column do |record| - puts '%-12i%-6i%-32s%-12i%-12i%-6i%-6i' % [ - record['TABLE_ID'], - record['POS'], - record['NAME'], - record['MTYPE'], - record['PRTYPE'], - record['LEN'], - record['PREC'], + innodb_system.data_dictionary.columns.each do |column| + puts "%-64s%-32s%-32s" % [ + column.table.name, + column.name, + column.description, ] end end -# Print the contents of the SYS_INDEXES data dictionary table. +# Print the contents of the index list from the data dictionary. def data_dictionary_indexes(innodb_system) - puts '%-12s%-12s%-32s%-10s%-6s%-12s%-12s' % %w[ - table_id - id + puts "%-64s%-32s%-32s" % %w[ + table name - n_fields - type - space - page_no + columns ] - innodb_system.data_dictionary.each_index do |record| - puts '%-12i%-12i%-32s%-10i%-6i%-12i%-12i' % [ - record['TABLE_ID'], - record['ID'], - record['NAME'], - record['N_FIELDS'], - record['TYPE'], - record['SPACE'], - record['PAGE_NO'], - ] - end -end - -# Print the contents of the SYS_FIELDS data dictionary table. -def data_dictionary_fields(innodb_system) - puts '%-12s%-12s%-32s' % %w[ - index_id - pos - col_name - ] - - innodb_system.data_dictionary.each_field do |record| - puts '%-12i%-12i%-32s' % [ - record['INDEX_ID'], - record['POS'], - record['COL_NAME'], + innodb_system.data_dictionary.indexes.each do |index| + puts "%-64s%-32s%-32s" % [ + index.table.name, + index.name, + index.column_references.each.map(&:name), ] end end def space_summary(space, start_page) - puts '%-12s%-20s%-12s%-12s%-20s' % %w[ + puts "%-12s%-20s%-12s%-12s%-20s" % %w[ page type prev @@ -295,7 +252,7 @@ def space_summary(space, start_page) ] space.each_page(start_page) do |page_number, page| - puts '%-12i%-20s%-12i%-12i%-20i' % [ + puts "%-12i%-20s%-12i%-12i%-20i" % [ page_number, page.type, page.prev || 0, @@ -314,7 +271,7 @@ def space_index_fseg_pages_summary(space, fseg_id) end def space_page_type_regions(space, start_page) - puts '%-12s%-12s%-12s%-20s' % %w[ + puts "%-12s%-12s%-12s%-20s" % %w[ start end count @@ -322,7 +279,7 @@ def space_page_type_regions(space, start_page) ] space.each_page_type_region(start_page) do |region| - puts '%-12i%-12i%-12i%-20s' % [ + puts "%-12i%-12i%-12i%-20s" % [ region[:start], region[:end], region[:count], @@ -342,7 +299,7 @@ def space_page_type_summary(space, start_page) page_type[page.type] += 1 end - puts '%-20s%-12s%-12s%-20s' % %w[ + puts "%-20s%-12s%-12s%-20s" % %w[ type count percent @@ -351,7 +308,7 @@ def space_page_type_summary(space, start_page) # Sort the page type Hash by count, descending. page_type.sort { |a, b| b[1] <=> a[1] }.each do |type, type_count| - puts '%-20s%-12i%-12.2f%-20s' % [ + puts "%-20s%-12i%-12.2f%-20s" % [ type, type_count, 100.0 * (type_count.to_f / page_count), @@ -367,17 +324,17 @@ end def space_list_iterate(space, list_name) fsp = space.page(0).fsp_header - raise "List '#{list_name}' doesn't exist" unless fsp[list_name]&.is_a?(Innodb::List) + raise "List '#{list_name}' doesn't exist" unless fsp[list_name].is_a?(Innodb::List) case fsp[list_name] when Innodb::List::Xdes print_xdes_list(space, fsp[list_name]) when Innodb::List::Inode - puts '%-12s' % %w[ + puts "%-12s" % %w[ page ] fsp[list_name].each do |page| - puts '%-12i' % [ + puts "%-12i" % [ page.offset, ] end @@ -385,7 +342,7 @@ def space_list_iterate(space, list_name) end def space_indexes(innodb_system, space) - puts '%-12s%-32s%-12s%-12s%-12s%-12s%-12s%-12s' % %w[ + puts "%-12s%-32s%-12s%-12s%-12s%-12s%-12s%-12s" % %w[ id name root @@ -398,56 +355,60 @@ def space_indexes(innodb_system, space) space.each_index do |index| index.each_fseg do |fseg_name, fseg| - puts '%-12i%-32s%-12i%-12s%-12i%-12i%-12i%-12s' % [ + puts "%-12i%-32s%-12i%-12s%-12i%-12i%-12i%-12s" % [ index.id, - innodb_system ? innodb_system.index_name_by_id(index.id) : '', + innodb_system.data_dictionary.indexes.find(innodb_index_id: index.id)&.name, index.root.offset, fseg_name, fseg.fseg_id, fseg.used_pages, fseg.total_pages, - '%.2f%%' % fseg.fill_factor, + "%.2f%%" % fseg.fill_factor, ] end end end -def space_index_pages_free_plot(space, image, start_page) - raise 'Could not load gnuplot. Is it installed?' unless require 'gnuplot' +def space_index_pages_free_plot(space, start_page) + raise "Could not load gnuplot. Is it installed?" unless require "gnuplot" index_data = { 0 => { x: [], y: [] } } space.each_page(start_page) do |page_number, page| - case page.type - when :INDEX + case + when page.is_a?(Innodb::Page::Index) data = (index_data[page.page_header[:index_id]] ||= { x: [], y: [] }) data[:x] << page_number data[:y] << page.free_space - when :ALLOCATED + when page.type == :ALLOCATED index_data[0][:x] << page_number index_data[0][:y] << page.size end end - image_file = image + '_free.png' + image_name = space.name + image_name = image_name.sub(%r{^#{space.innodb_system.data_directory}/}, "") if space.innodb_system + image_name = image_name.sub(".ibd", "").gsub(/[^a-zA-Z0-9_]/, "_").sub(/\A_+/, "") + image_file = "#{image_name}_free.png" + # Aim for one horizontal pixel per extent, but min 1k and max 10k width. - image_width = [10_000, [1_000, space.pages / space.pages_per_extent].max].min + image_width = (space.pages / space.pages_per_extent).clamp(1_000, 10_000) Gnuplot.open do |gp| Gnuplot::Plot.new(gp) do |plot| plot.terminal "png size #{image_width}, 800" plot.output image_file - plot.title image - plot.key 'reverse left top box horizontal Left textcolor variable' - plot.ylabel 'free space per page' - plot.xlabel 'page number' - plot.yrange '[-100:18000]' - plot.xtics 'border' + plot.title image_name.gsub("_", " ") + plot.key "reverse left top box horizontal Left textcolor variable" + plot.ylabel "free space per page" + plot.xlabel "page number" + plot.yrange "[-100:18000]" + plot.xtics "border" index_data.sort.each do |id, data| plot.data << Gnuplot::DataSet.new([data[:x], data[:y]]) do |ds| - ds.with = 'dots' - ds.title = id.zero? ? 'Unallocated' : "Index #{id}" + ds.with = "dots" + ds.title = id.zero? ? "Unallocated" : "Index #{id}" end end @@ -462,9 +423,9 @@ end # rubocop:disable Metrics/BlockNesting def space_extents_illustrate_page_status(space, entry, count_by_identifier, identifiers) - entry.each_page_status.each_with_object(''.dup) do |(page_number, page_status), bitmap| + entry.each_page_status.with_object("".dup) do |(page_number, page_status), bitmap| if page_number >= space.pages - bitmap << ' ' + bitmap << " " next end @@ -478,10 +439,10 @@ def space_extents_illustrate_page_status(space, entry, count_by_identifier, iden if page.respond_to?(:index_id) identifier = page.index_id unless identifiers[identifier] - identifiers[identifier] = page.ibuf_index? ? 'Insert Buffer Index' : "Index #{page.index_id}" + identifiers[identifier] = page.ibuf_index? ? "Insert Buffer Index" : "Index #{page.index_id}" if space.innodb_system - table, index = space.innodb_system.table_and_index_name_by_id(page.index_id) - identifiers[identifier] += ' (%s.%s)' % [table, index] if table && index + dd_index = space.innodb_system.data_dictionary.indexes.find(innodb_index_id: page.index_id) + identifiers[identifier] += " (%s.%s)" % [dd_index.table.name, dd_index.name] if dd_index end end end @@ -489,10 +450,10 @@ def space_extents_illustrate_page_status(space, entry, count_by_identifier, iden bitmap << filled_block(used_fraction, identifier) - if used_fraction != 0.0 - count_by_identifier[identifier] += 1 - else + if used_fraction.zero? count_by_identifier[:free] += 1 + else + count_by_identifier[identifier] += 1 end end end @@ -504,8 +465,8 @@ end def space_extents_illustrate(space) width = space.pages_per_extent puts - puts "%12s %-#{width}s " % ['', center(space.name, width)] - puts "%12s ╭%-#{width}s╮" % ['Start Page', '─' * width] + puts "%12s %-#{width}s " % ["", center(space.name, width)] + puts "%12s ╭%-#{width}s╮" % ["Start Page", "─" * width] identifiers = {} count_by_identifier = Hash.new(0) @@ -518,32 +479,32 @@ def space_extents_illustrate(space) end total_pages = count_by_identifier.values.reduce(:+) - puts "%12s ╰%-#{width}s╯" % ['', '─' * width] + puts "%12s ╰%-#{width}s╯" % ["", "─" * width] puts - puts 'Legend (%s = 1 page):' % [filled_block(1.0, nil)] - puts ' %-62s %8s %8s' % [ - 'Page Type', - 'Pages', - 'Ratio', + puts "Legend (%s = 1 page):" % [filled_block(1.0, nil)] + puts " %-62s %8s %8s" % [ + "Page Type", + "Pages", + "Ratio", ] - puts ' %s %-60s %8i %7.2f%%' % [ + puts " %s %-60s %8i %7.2f%%" % [ filled_block(1.0, nil), - 'System', + "System", count_by_identifier[nil], 100.0 * (count_by_identifier[nil].to_f / total_pages), ] identifiers.sort.each do |identifier, description| - puts ' %s %-60s %8i %7.2f%%' % [ + puts " %s %-60s %8i %7.2f%%" % [ filled_block(1.0, identifier), description, count_by_identifier[identifier], 100.0 * (count_by_identifier[identifier].to_f / total_pages), ] end - puts ' %s %-60s %8i %7.2f%%' % [ + puts " %s %-60s %8i %7.2f%%" % [ filled_block(0.0, nil), - 'Free space', + "Free space", count_by_identifier[:free], 100.0 * (count_by_identifier[:free].to_f / total_pages), ] @@ -563,26 +524,26 @@ def space_lsn_age_illustrate(space) next if page.lsn.zero? page_lsn[page_number] = page.lsn - lsn_min = page.lsn < lsn_min ? page.lsn : lsn_min - lsn_max = page.lsn > lsn_max ? page.lsn : lsn_max + lsn_min = [page.lsn, lsn_min].min + lsn_max = [page.lsn, lsn_max].max end lsn_delta = lsn_max - lsn_min puts - puts "%12s %-#{width}s " % ['', center(space.name, width)] - puts "%12s ╭%-#{width}s╮" % ['Start Page', '─' * width] + puts "%12s %-#{width}s " % ["", center(space.name, width)] + puts "%12s ╭%-#{width}s╮" % ["Start Page", "─" * width] start_page = 0 page_lsn.each_slice(width) do |slice| puts "%12i │%-#{width}s│" % [ start_page, - slice.inject('') do |line, lsn| + slice.inject("") do |line, lsn| if lsn age_ratio = (lsn - lsn_min).to_f / lsn_delta color = colors[(age_ratio * colors.size.to_f).floor] line += ansi_color(color, filled_block(1.0, nil)) else - line += ' ' + line += " " end line end, @@ -590,28 +551,28 @@ def space_lsn_age_illustrate(space) start_page += width end - puts "%12s ╰%-#{width}s╯" % ['', '─' * width] + puts "%12s ╰%-#{width}s╯" % ["", "─" * width] - _, lsn_freq = page_lsn.reject(&:nil?).histogram(colors.size, min: lsn_min, max: lsn_max) + _, lsn_freq = page_lsn.compact.histogram(colors.size, min: lsn_min, max: lsn_max) lsn_freq_delta = lsn_freq.max - lsn_freq.min - lsn_age_histogram = ''.dup + lsn_age_histogram = "".dup lsn_freq.each do |freq| freq_norm = freq / lsn_freq_delta - lsn_age_histogram << (freq_norm > 0.0 ? filled_block(freq_norm) : ' ') + lsn_age_histogram << (freq_norm > 0.0 ? filled_block(freq_norm) : " ") end puts - puts 'LSN Age Histogram (%s = ~%d pages):' % [ + puts "LSN Age Histogram (%s = ~%d pages):" % [ filled_block(1.0, nil), (space.pages.to_f / colors.size).round, ] - puts ' %12s %s %-12s' % [ - 'Min LSN', + puts " %12s %s %-12s" % [ + "Min LSN", lsn_age_histogram, - 'Max LSN', + "Max LSN", ] - puts ' %12i %s %-12i' % [ + puts " %12i %s %-12i" % [ lsn_min, colors.map { |c| ansi_color(c, filled_block(1.0, nil)) }.join, lsn_max, @@ -619,7 +580,7 @@ def space_lsn_age_illustrate(space) end def print_inode_summary(inode) - puts 'INODE fseg_id=%d, pages=%d, frag=%d, full=%d, not_full=%d, free=%d' % [ + puts "INODE fseg_id=%d, pages=%d, frag=%d, full=%d, not_full=%d, free=%d" % [ inode.fseg_id, inode.total_pages, inode.frag_array_n_used, @@ -631,19 +592,19 @@ end def print_inode_detail(inode) # rubocop:disable Layout/LineLength - puts 'INODE fseg_id=%d, pages=%d, frag=%d pages (%s), full=%d extents (%s), not_full=%d extents (%s) (%d/%d pages used), free=%d extents (%s)' % [ + puts "INODE fseg_id=%d, pages=%d, frag=%d pages (%s), full=%d extents (%s), not_full=%d extents (%s) (%d/%d pages used), free=%d extents (%s)" % [ inode.fseg_id, inode.total_pages, inode.frag_array_n_used, - inode.frag_array_pages.join(', '), + inode.frag_array_pages.join(", "), inode.full.length, - inode.full.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(', '), + inode.full.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(", "), inode.not_full.length, - inode.not_full.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(', '), + inode.not_full.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(", "), inode.not_full_n_used, inode.not_full.length * inode.space.pages_per_extent, inode.free.length, - inode.free.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(', '), + inode.free.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(", "), ] # rubocop:enable Layout/LineLength end @@ -666,24 +627,35 @@ def space_inodes_detail(space) end end +def space_sdi_construct_json(space) + space.sdi.each_object.map do |object| + { type: object.dd_object_type, id: object.id, object: object.data } + end +end + +def space_sdi_json_dump(space) + raise "Space does not have SDI data; is it older than MySQL 8.0?" unless space.sdi.valid? + + puts JSON.pretty_generate(space_sdi_construct_json(space)) +end + def page_account(innodb_system, space, page_number) puts "Accounting for page #{page_number}:" if page_number > space.pages - puts ' Page does not exist.' + puts " Page does not exist." return end page = space.page(page_number) page_type = Innodb::Page::PAGE_TYPE[page.type] - puts ' Page type is %s (%s, %s).' % [ + puts " Page type is %s (%s)." % [ page.type, page_type[:description], - page_type[:usage], ] xdes = space.xdes_for_page(page_number) - puts ' Extent descriptor for pages %d-%d is at page %d, offset %d.' % [ + puts " Extent descriptor for pages %d-%d is at page %d, offset %d." % [ xdes.start_page, xdes.end_page, xdes.this[:page], @@ -693,12 +665,12 @@ def page_account(innodb_system, space, page_number) if xdes.allocated_to_fseg? puts " Extent is fully allocated to fseg #{xdes.fseg_id}." else - puts ' Extent is not fully allocated to an fseg; may be a fragment extent.' + puts " Extent is not fully allocated to an fseg; may be a fragment extent." end xdes_status = xdes.page_status(page_number) - puts ' Page is marked as %s in extent descriptor.' % [ - xdes_status[:free] ? 'free' : 'used', + puts " Page is marked as %s in extent descriptor." % [ + xdes_status[:free] ? "free" : "used", ] space.each_xdes_list do |name, list| @@ -716,7 +688,7 @@ def page_account(innodb_system, space, page_number) if inode.frag_array.include?(page_number) # rubocop:disable Style/Next page_inode = inode - puts ' Page is in fragment array of fseg %d.' % [ + puts " Page is in fragment array of fseg %d." % [ inode.fseg_id, ] end @@ -729,15 +701,15 @@ def page_account(innodb_system, space, page_number) puts " Fseg is in #{fseg_name} fseg of index #{index.id}." puts " Index root is page #{index.root.offset}." if innodb_system - table_name, index_name = innodb_system.table_and_index_name_by_id(index.id) - puts " Index is #{table_name}.#{index_name}." if table_name && index_name + dd_index = innodb_system.data_dictionary.indexes.find(innodb_index_id: index.id) + puts " Index is #{dd_index.table.name}.#{dd_index.name}." if dd_index end end end if space.system_space? # rubocop:disable Style/GuardClause - puts ' Fseg is trx_sys.' if page_inode == space.trx_sys.fseg - puts ' Fseg is doublewrite buffer.' if page_inode == space.trx_sys.doublewrite[:fseg] + puts " Fseg is trx_sys." if page_inode == space.trx_sys.fseg + puts " Fseg is doublewrite buffer." if page_inode == space.trx_sys.doublewrite[:fseg] innodb_system.data_dictionary&.each_data_dictionary_index do |table_name, index_name, index| index.each_fseg do |_fseg_name, fseg| @@ -756,9 +728,9 @@ end def page_validate_index(page) page_is_valid = true - print 'Parsing all records in page... ' + print "Parsing all records in page... " records = page.each_record.to_a - puts 'done.' + puts "done." directory_offsets = page.each_directory_offset.to_a record_offsets = records.map(&:offset) @@ -767,9 +739,9 @@ def page_validate_index(page) unless invalid_directory_entries.empty? page_is_valid = false - puts 'Invalid page directory entries (offsets not to valid records):' + puts "Invalid page directory entries (offsets not to valid records):" invalid_directory_entries.each do |offset| - puts ' slot %d, offset %d' % [ + puts " slot %d, offset %d" % [ page.offset_is_directory_slot?(offset), offset, ] @@ -798,9 +770,9 @@ def page_validate_index(page) prev = rec end unless misordered_directory_entries.empty? - puts 'Misordered page directory entries (key < prev key):' + puts "Misordered page directory entries (key < prev key):" misordered_directory_entries.each do |entry| - puts ' slot %d, offset %d, key %s, prev key %s' % [ + puts " slot %d, offset %d, key %s, prev key %s" % [ entry[:slot], entry[:offset], entry[:key], @@ -827,9 +799,9 @@ def page_validate_index(page) prev = rec end unless misordered_records.empty? - puts 'Misordered records in record list (key < prev key):' + puts "Misordered records in record list (key < prev key):" misordered_records.each do |entry| - puts ' offset %d, key %s, prev key %s' % [ + puts " offset %d, key %s, prev key %s" % [ entry[:offset], entry[:key], entry[:prev_key], @@ -842,31 +814,31 @@ end def page_validate(_innodb_system, space, page_number) page_is_valid = true - puts 'Validating page %d...' % [page_number] + puts "Validating page %d..." % [page_number] - print 'Parsing page... ' + print "Parsing page... " page = space.page(page_number) - puts 'done.' + puts "done." if page.corrupt? page_is_valid = false - puts 'Page appears to be corrupt:' - puts ' Stored checksums:' - puts ' header %10d (0x%08x), type %s' % [ + puts "Page appears to be corrupt:" + puts " Stored checksums:" + puts " header %10d (0x%08x), type %s" % [ page.checksum, page.checksum, - page.checksum_type || 'unknown', + page.checksum_type || "unknown", ] - puts ' trailer %10d (0x%08x)' % [ + puts " trailer %10d (0x%08x)" % [ page.fil_trailer.checksum, page.fil_trailer.checksum, ] - puts ' Calculated checksums:' - puts ' crc32 %10d (0x%08x)' % [ + puts " Calculated checksums:" + puts " crc32 %10d (0x%08x)" % [ page.checksum_crc32, page.checksum_crc32, ] - puts ' innodb %10d (0x%08x)' % [ + puts " innodb %10d (0x%08x)" % [ page.checksum_innodb, page.checksum_innodb, ] @@ -874,18 +846,18 @@ def page_validate(_innodb_system, space, page_number) if page.torn? page_is_valid = false - puts 'Page appears to be torn:' - puts ' Full LSN:' - puts ' header %d (0x%016x)' % [ + puts "Page appears to be torn:" + puts " Full LSN:" + puts " header %d (0x%016x)" % [ page.lsn, page.lsn, ] - puts ' Low 32 bits of LSN:' - puts ' header %10d (0x%08x)' % [ + puts " Low 32 bits of LSN:" + puts " header %10d (0x%08x)" % [ page.fil_header.lsn_low32, page.fil_header.lsn_low32, ] - puts ' trailer %10d (0x%08x)' % [ + puts " trailer %10d (0x%08x)" % [ page.fil_trailer.lsn_low32, page.fil_trailer.lsn_low32, ] @@ -893,33 +865,33 @@ def page_validate(_innodb_system, space, page_number) if page.misplaced? page_is_valid = false - puts 'Page appears to be misplaced:' + puts "Page appears to be misplaced:" if page.misplaced_offset? - puts ' Requested page %d but offset stored in page is %d.' % [ + puts " Requested page %d but offset stored in page is %d." % [ page_number, page.offset, ] end if page.misplaced_space? - puts ' Space ID %d does not match page stored space ID %d.' % [ + puts " Space ID %d does not match page stored space ID %d." % [ page.space.space_id, page.space_id, ] end end - page_is_valid = false if page.type == :INDEX && !page_validate_index(page) + page_is_valid = false if page.is_a?(Innodb::Page::Index) && !page_validate_index(page) - puts 'Page %d appears to be %s!' % [ + puts "Page %d appears to be %s!" % [ page_number, - page_is_valid ? 'valid' : 'corrupted', + page_is_valid ? "valid" : "corrupted", ] end def page_directory_summary(_space, page) - usage(1, 'Page must be an index page') if page.type != :INDEX + usage(1, "Page must be an index page") unless page.is_a?(Innodb::Page::Index) - puts '%-8s%-8s%-14s%-8s%s' % %w[ + puts "%-8s%-8s%-14s%-8s%s" % %w[ slot offset type @@ -929,9 +901,9 @@ def page_directory_summary(_space, page) page.directory.each_with_index do |offset, slot| record = page.record(offset) - key = %i[conventional node_pointer].include?(record.header[:type]) ? '(%s)' % record.key_string : '' + key = %i[conventional node_pointer].include?(record.header[:type]) ? "(%s)" % record.key_string : "" - puts '%-8i%-8i%-14s%-8i%s' % [ + puts "%-8i%-8i%-14s%-8i%s" % [ slot, offset, record.header[:type], @@ -943,18 +915,17 @@ end def page_records(_space, page) page.each_record do |record| - puts 'Record %i: %s' % [ + puts "Record %i: %s" % [ record.offset, record.string, ] - puts if record.header[:type] == :conventional end end def page_illustrate(page) width = 64 - unknown_page_content = page.type == :INDEX && page.record_describer.nil? - blocks = Array.new(page.size, unknown_page_content ? '▞' : ' ') + unknown_page_content = page.is_a?(Innodb::Page::Index) && page.record_describer.nil? + blocks = Array.new(page.size, unknown_page_content ? "▞" : " ") identifiers = {} identifier_sort = 0 count_by_identifier = Hash.new(0) @@ -969,7 +940,7 @@ def page_illustrate(page) unless identifiers[identifier] # Prefix an integer <0123> on each name so that the legend can be # sorted by the appearance of each region in the page. - identifiers[identifier] = '<%04i>%s' % [ + identifiers[identifier] = "<%04i>%s" % [ identifier_sort, region.info, ] @@ -982,58 +953,58 @@ def page_illustrate(page) end puts - puts "%12s %-#{width}s " % ['', center("Page #{page.offset} (#{page.type})", width)] - puts "%12s ╭%-#{width}s╮" % ['Offset', '─' * width] + puts "%12s %-#{width}s " % ["", center("Page #{page.offset} (#{page.type})", width)] + puts "%12s ╭%-#{width}s╮" % ["Offset", "─" * width] offset = 0 skipped_lines = 0 blocks.each_slice(width) do |slice| - if slice.any? { |s| s != ' ' } + if slice.any? { |s| s != " " } if skipped_lines.positive? - puts "%12s │%-#{width}s│" % ['...', ''] + puts "%12s │%-#{width}s│" % ["...", ""] skipped_lines = 0 end - puts '%12i │%-s│' % [offset, slice.join] + puts "%12i │%-s│" % [offset, slice.join] else skipped_lines += 1 end offset += width end - puts "%12s ╰%-#{width}s╯" % ['', '─' * width] + puts "%12s ╰%-#{width}s╯" % ["", "─" * width] puts - puts 'Legend (%s = 1 byte):' % [filled_block(1.0, nil)] - puts ' %-32s %8s %8s' % [ - 'Region Type', - 'Bytes', - 'Ratio', + puts "Legend (%s = 1 byte):" % [filled_block(1.0, nil)] + puts " %-32s %8s %8s" % [ + "Region Type", + "Bytes", + "Ratio", ] identifiers.sort { |a, b| a[1] <=> b[1] }.each do |identifier, description| - puts ' %s %-30s %8i %7.2f%%' % [ + puts " %s %-30s %8i %7.2f%%" % [ filled_block(1.0, identifier), - description.gsub(/^<\d+>/, ''), + description.gsub(/^<\d+>/, ""), count_by_identifier[identifier], 100.0 * (count_by_identifier[identifier].to_f / page.size), ] end - puts ' %s %-30s %8i %7.2f%%' % [ + puts " %s %-30s %8i %7.2f%%" % [ filled_block(0.0, nil), - 'Garbage', + "Garbage", count_by_identifier[nil], 100.0 * (count_by_identifier[nil].to_f / page.size), ] free_space = page.size - count_by_identifier.inject(0) { |sum, (_k, v)| sum + v } - puts ' %s %-30s %8i %7.2f%%' % [ - unknown_page_content ? '▞' : ' ', - unknown_page_content ? 'Unknown (no data dictionary)' : 'Free', + puts " %s %-30s %8i %7.2f%%" % [ + unknown_page_content ? "▞" : " ", + unknown_page_content ? "Unknown (no data dictionary)" : "Free", free_space, 100.0 * (free_space.to_f / page.size), ] if unknown_page_content puts - puts 'Note:' - puts ' Records could not be parsed because no data dictionary or record describer' - puts ' was available. Use -s instead of -f, or provide a record describer class.' + puts "Note:" + puts " Records could not be parsed because no data dictionary or record describer" + puts " was available. Use -s instead of -f, or provide a record describer class." end puts @@ -1044,23 +1015,25 @@ def record_dump(page, record_offset) raise "Record at offset #{record_offset} not found" unless record record.dump +rescue IOError + raise "Record could not be read at offset #{record_offset}; is it a valid record offset?" end def record_history(page, record_offset) - raise 'Record is not located on a leaf page; no history available' unless page.leaf? + raise "Record is not located on a leaf page; no history available" unless page.leaf? record = page.record(record_offset) raise "Record at offset #{record_offset} not found" unless record - puts '%-14s%-20s%s' % [ - 'Transaction', - 'Type', - 'Undo record', + puts "%-14s%-20s%s" % [ + "Transaction", + "Type", + "Undo record", ] record.each_undo_record do |undo| - puts '%-14s%-20s%s' % [ - undo.trx_id || '(n/a)', + puts "%-14s%-20s%s" % [ + undo.trx_id || "(n/a)", undo.header[:type], undo.string, ] @@ -1092,8 +1065,8 @@ end def index_recurse(index) index.recurse( lambda do |page, depth| - puts '%s%s NODE #%i: %i records, %i bytes' % [ - ' ' * depth, + puts "%s%s NODE #%i: %i records, %i bytes" % [ + " " * depth, index.node_type(page).to_s.upcase, page.offset, page.records, @@ -1101,8 +1074,8 @@ def index_recurse(index) ] if page.level.zero? page.each_record do |record| - puts '%sRECORD: (%s) → (%s)' % [ - ' ' * (depth + 1), + puts "%sRECORD: (%s) → (%s)" % [ + " " * (depth + 1), record.key_string, record.row_string, ] @@ -1110,9 +1083,9 @@ def index_recurse(index) end end, lambda do |_parent_page, child_page, child_min_key, depth| - puts '%sNODE POINTER RECORD ≥ (%s) → #%i' % [ - ' ' * depth, - child_min_key.map { |r| '%s=%s' % [r[:name], r[:value].inspect] }.join(', '), + puts "%sNODE POINTER RECORD ≥ (%s) → #%i" % [ + " " * depth, + child_min_key.map { |r| "%s=%s" % [r[:name], r[:value].inspect] }.join(", "), child_page.offset, ] end @@ -1120,7 +1093,7 @@ def index_recurse(index) end def index_record_offsets(index) - puts '%-20s%-20s' % %w[ + puts "%-20s%-20s" % %w[ page_offset record_offset ] @@ -1128,7 +1101,7 @@ def index_record_offsets(index) lambda do |page, _depth| if page.level.zero? page.each_record do |record| - puts '%-20i%-20i' % [ + puts "%-20i%-20i" % [ page.offset, record.offset, ] @@ -1140,41 +1113,41 @@ def index_record_offsets(index) end def index_digraph(index) - puts 'digraph btree {' - puts ' rankdir = LR;' - puts ' ranksep = 2.0;' + puts "digraph btree {" + puts " rankdir = LR;" + puts " ranksep = 2.0;" index.recurse( lambda do |page, depth| - label = 'Page %i|(%i records)' % [ + label = "Page %i|(%i records)" % [ page.offset, page.records, ] page.each_child_page do |child_page_number, child_key| - label += '|(%s)' % [ + label += "|(%s)" % [ child_page_number, - child_key.join(', '), + child_key.join(", "), ] end - puts ' %spage_%i [ shape = \'record\'; label = \'%s\'; ];' % [ - ' ' * depth, + puts " %spage_%i [ shape = 'record'; label = '%s'; ];" % [ + " " * depth, page.offset, label, ] end, lambda do |parent_page, child_page, _child_key, depth| - puts ' %spage_%i:dir_%i → page_%i:page:nw;' % [ - ' ' * depth, + puts " %spage_%i:dir_%i → page_%i:page:nw;" % [ + " " * depth, parent_page.offset, child_page.offset, child_page.offset, ] end ) - puts '}' + puts "}" end def index_level_summary(index, level) - puts '%-8s%-8s%-8s%-8s%-8s%-8s%-8s' % %w[ + puts "%-8s%-8s%-8s%-8s%-8s%-8s%-8s" % %w[ page index level @@ -1185,7 +1158,7 @@ def index_level_summary(index, level) ] index.each_page_at_level(level) do |page| - puts '%-8i%-8i%-8i%-8i%-8i%-8i%s' % [ + puts "%-8i%-8i%-8i%-8i%-8i%-8i%s" % [ page.offset, page.page_header[:index_id], page.level, @@ -1200,7 +1173,7 @@ end def undo_history_summary(innodb_system) history_list = innodb_system.history.each_history_list.reject { |h| h.list.empty? } - puts '%-8s%-8s%-14s%-20s%s' % %w[ + puts "%-8s%-8s%-14s%-20s%s" % %w[ Page Offset Transaction @@ -1210,8 +1183,8 @@ def undo_history_summary(innodb_system) history_list.each do |history| history.each_undo_record do |undo| - table_name = innodb_system.table_name_by_id(undo.table_id) - puts '%-8s%-8s%-14s%-20s%s' % [ + table_name = innodb_system.data_dictionary.tables(innodb_table_id: undo.table_id) + puts "%-8s%-8s%-14s%-20s%s" % [ undo.page, undo.offset, undo.trx_id, @@ -1236,20 +1209,21 @@ def usage(exit_code, message = nil) end # rubocop:disable Layout/HeredocIndentation - print <<'END_OF_USAGE' + print < Invocation examples: - innodb_space -s ibdata1 [-T tname [-I iname]] [options] - Use ibdata1 as the system tablespace and load the tname table (and the - iname index for modes that require it) from data located in the system - tablespace data dictionary. This will automatically generate a record - describer for any indexes. + innodb_space -s ibdata1 [-T table-name [-I index-name [-R record-offset]]] [options] + Use ibdata1 as the system tablespace and load the table-name table (and + the index-name index for modes that require it) from data located in the + system tablespace data dictionary. This will automatically generate a + record describer for any indexes using the data dictionary. - innodb_space -f tname.ibd [-r ./desc.rb -d DescClass] [options] - Use the tname.ibd table (and the DescClass describer where required). + innodb_space -f file-name.ibd [-r ./describer.rb -d DescriberClass] [options] + Use the file-name.ibd tablespace file (and the DescriberClass describer + where required) to read the tablespace structures or indexes. The following options are supported: @@ -1268,11 +1242,22 @@ The following options are supported: files named 'ibdata?' which will then be sorted alphabetically and used to load the system tablespace. - --table-name, -T - Use the table name . + If using the --system-space-file option, the following options may also + be used: + + --table-name, -T + Use the table name . - --index-name, -I - Use the index name . + --index-name, -I + Use the index name . + + --system-space-tables, -x + Allow opening tables from the system space to support system spaces with + tables created without innodb-file-per-table enabled. + + --data-directory, -D + Open per-table tablespace files from rather than from the + directory where the system-space-file is located. --space-file, -f Load the tablespace file . @@ -1280,6 +1265,9 @@ The following options are supported: --page, -p Operate on the page . + --record, -R + Operate on the record located at within the index page. + --level, -l Operate on the level . @@ -1302,16 +1290,13 @@ The following modes are supported: Print a summary of all spaces in the system. data-dictionary-tables - Print all records in the SYS_TABLES data dictionary table. + Print the contents of the table list from the data dictionary. data-dictionary-columns - Print all records in the SYS_COLUMNS data dictionary table. + Print the contents of the column list from the data dictionary. data-dictionary-indexes - Print all records in the SYS_INDEXES data dictionary table. - - data-dictionary-fields - Print all records in the SYS_FIELDS data dictionary table. + Print the contents of the index list from the data dictionary. space-summary Summarize all pages within a tablespace. A starting page number can be @@ -1375,6 +1360,10 @@ The following modes are supported: space-inodes-detail Iterate through all inodes, printing a detailed report of each FSEG. + space-sdi-json-dump + Dump the contents of any SDI (serialized dictionary information) from a + space, in JSON format, similar to ibd2sdi. + index-recurse Recurse an index, starting at the root (which must be provided in the first --page/-p argument), printing the node pages, node pointers (links), leaf @@ -1395,20 +1384,20 @@ The following modes are supported: index-fseg-internal-lists index-fseg-leaf-lists - Print a summary of all lists in an index file segment. Index root page must - be provided with --page/-p. + Print a summary of all lists in an index file segment. Index must be specified + by name with --index-name/-I or by root page number with --page/-p. index-fseg-internal-list-iterate index-fseg-leaf-list-iterate Iterate the file segment list (whose name is provided in the first --list/-L - argument) for internal or leaf pages for a given index (whose root page - is provided in the first --page/-p argument). The lists used for each + argument) for internal or leaf pages for a given index (specified by name with + --index-name/-I or by root page number with --page/-p). The lists used for each index are 'full', 'not_full', and 'free'. index-fseg-internal-frag-pages index-fseg-leaf-frag-pages - Print a summary of all fragment pages in an index file segment. Index root - page must be provided with --page/-p. + Print a summary of all fragment pages in an index file segment. Index must be + specified by name with --index-name/-I or by root page number with --page/-p. page-dump Dump the contents of a page, using the Ruby pp ('pretty-print') module. @@ -1450,12 +1439,34 @@ END_OF_USAGE exit exit_code end -Signal.trap('INT') { exit } -Signal.trap('PIPE') { exit } - -@options = OpenStruct.new +%w[INT PIPE].each do |name| + Signal.trap(name) { exit } if Signal.list.include?(name) +end + +InnodbSpaceOptions = Struct.new( + :trace, + :system_space_file, + :system_space_tables, + :data_directory, + :space_file, + :table_name, + :index_name, + :page, + :record, + :level, + :list, + :fseg_id, + :describer, + :illustration_line_width, + :illustration_block_size, + keyword_init: true +) + +@options = InnodbSpaceOptions.new @options.trace = 0 @options.system_space_file = nil +@options.system_space_tables = false +@options.data_directory = nil @options.space_file = nil @options.table_name = nil @options.index_name = nil @@ -1470,21 +1481,23 @@ Signal.trap('PIPE') { exit } # rubocop:disable Layout/SpaceInsideArrayLiteralBrackets getopt_options = [ - [ '--help', '-?', GetoptLong::NO_ARGUMENT ], - [ '--trace', '-t', GetoptLong::NO_ARGUMENT ], - [ '--system-space-file', '-s', GetoptLong::REQUIRED_ARGUMENT ], - [ '--space-file', '-f', GetoptLong::REQUIRED_ARGUMENT ], - [ '--table-name', '-T', GetoptLong::REQUIRED_ARGUMENT ], - [ '--index-name', '-I', GetoptLong::REQUIRED_ARGUMENT ], - [ '--page', '-p', GetoptLong::REQUIRED_ARGUMENT ], - [ '--record', '-R', GetoptLong::REQUIRED_ARGUMENT ], - [ '--level', '-l', GetoptLong::REQUIRED_ARGUMENT ], - [ '--list', '-L', GetoptLong::REQUIRED_ARGUMENT ], - [ '--fseg-id', '-F', GetoptLong::REQUIRED_ARGUMENT ], - [ '--require', '-r', GetoptLong::REQUIRED_ARGUMENT ], - [ '--describer', '-d', GetoptLong::REQUIRED_ARGUMENT ], - [ '--illustration-line-width', GetoptLong::REQUIRED_ARGUMENT ], - [ '--illustration-block-size', GetoptLong::REQUIRED_ARGUMENT ], + [ "--help", "-?", GetoptLong::NO_ARGUMENT ], + [ "--trace", "-t", GetoptLong::NO_ARGUMENT ], + [ "--system-space-file", "-s", GetoptLong::REQUIRED_ARGUMENT ], + [ "--system-space-tables", "-x", GetoptLong::NO_ARGUMENT ], + [ "--data-directory", "-D", GetoptLong::REQUIRED_ARGUMENT ], + [ "--space-file", "-f", GetoptLong::REQUIRED_ARGUMENT ], + [ "--table-name", "-T", GetoptLong::REQUIRED_ARGUMENT ], + [ "--index-name", "-I", GetoptLong::REQUIRED_ARGUMENT ], + [ "--page", "-p", GetoptLong::REQUIRED_ARGUMENT ], + [ "--record", "-R", GetoptLong::REQUIRED_ARGUMENT ], + [ "--level", "-l", GetoptLong::REQUIRED_ARGUMENT ], + [ "--list", "-L", GetoptLong::REQUIRED_ARGUMENT ], + [ "--fseg-id", "-F", GetoptLong::REQUIRED_ARGUMENT ], + [ "--require", "-r", GetoptLong::REQUIRED_ARGUMENT ], + [ "--describer", "-d", GetoptLong::REQUIRED_ARGUMENT ], + [ "--illustration-line-width", GetoptLong::REQUIRED_ARGUMENT ], + [ "--illustration-block-size", GetoptLong::REQUIRED_ARGUMENT ], ] # rubocop:enable Layout/SpaceInsideArrayLiteralBrackets @@ -1492,35 +1505,39 @@ getopt = GetoptLong.new(*getopt_options) getopt.each do |opt, arg| case opt - when '--help' + when "--help" usage 0 - when '--trace' + when "--trace" @options.trace += 1 - when '--system-space-file' - @options.system_space_file = arg.split(',') - when '--space-file' - @options.space_file = arg.split(',') - when '--table-name' + when "--system-space-file" + @options.system_space_file = arg.split(",") + when "--system-space-tables" + @options.system_space_tables = true + when "--data-directory" + @options.data_directory = arg + when "--space-file" + @options.space_file = arg.split(",") + when "--table-name" @options.table_name = arg - when '--index-name' + when "--index-name" @options.index_name = arg - when '--page' + when "--page" @options.page = arg.to_i - when '--record' + when "--record" @options.record = arg.to_i - when '--level' + when "--level" @options.level = arg.to_i - when '--list' + when "--list" @options.list = arg.to_sym - when '--fseg-id' + when "--fseg-id" @options.fseg_id = arg.to_i - when '--require' + when "--require" require File.expand_path(arg) - when '--describer' + when "--describer" @options.describer = arg - when '--illustration-line-width' + when "--illustration-line-width" @options.illustration_line_width = arg.to_i - when '--illustration-block-size' + when "--illustration-block-size" @options.illustration_block_size = arg.to_i end end @@ -1528,11 +1545,24 @@ end # rubocop:disable Style/IfUnlessModifier unless @options.system_space_file || @options.space_file - usage 1, 'System space file (-s) or space file (-f) must be specified' + usage(1, "Either the --system-space-file (-s) or --space-file (-f) must be specified") end if @options.system_space_file && @options.space_file - usage 1, 'Only one of system space or space file may be specified' + usage(1, "Only one of --system-space-file (-s) or --space-file (-f) may be specified") +end + +system_space_options = + @options.table_name || @options.index_name || @options.system_space_tables || @options.data_directory + +if !@options.system_space_file && system_space_options + usage( + 1, + %{ + The --table-name (-T), --index-name (-I), --system-space-tables (-x), and --data-directory (-D) + options can only be used with the --system-space-file (-s) option + }.squish + ) end BufferCursor.trace! if @options.trace > 1 @@ -1544,12 +1574,12 @@ index = nil page = nil if @options.system_space_file - innodb_system = Innodb::System.new(@options.system_space_file) + innodb_system = Innodb::System.new(@options.system_space_file, data_directory: @options.data_directory) end if innodb_system && @options.table_name - table_tablespace = innodb_system.space_by_table_name(@options.table_name) - space = table_tablespace || innodb_system.system_space + space = innodb_system.space_by_table_name(@options.table_name) + raise "Couldn't load space for table #{@options.table_name}" unless space elsif @options.space_file space = Innodb::Space.new(@options.space_file) else @@ -1567,7 +1597,7 @@ if innodb_system && @options.table_name && @options.index_name page = @options.page ? space.page(@options.page) : index.root elsif @options.page page = space.page(@options.page) - index = space.index(@options.page) if page&.type == :INDEX && page&.root? + index = space.index(@options.page) if page.is_a?(Innodb::Page::Index) && page&.root? end # The non-option argument on the command line is the mode (usually the last, @@ -1575,43 +1605,61 @@ end mode = ARGV.shift unless mode - usage(1, 'At least one mode must be provided') + usage(1, "At least one mode must be provided") end if /^(system-|data-dictionary-)/.match(mode) && !innodb_system - usage(1, 'System tablespace must be specified using -s/--system-space-file') + usage(1, "System tablespace must be specified using --system-space-file (-s)") end if /^space-/.match(mode) && !space usage( 1, - 'Tablespace must be specified using either -f/--space-file ' \ - 'or a combination of -s/--system-space-file and -T/--table' + %{ + Tablespace must be specified using either --space-file (-f) + or a combination of --system-space-file (-s) and --table (-T) + }.squish ) end if /^index-/.match(mode) && !index usage( 1, - 'Index must be specified using a combination of either -f/--space-file and -p/--page ' \ - 'or -s/--system-space-file, -T/--table-name, and -I/--index-name' + %{ + Index must be specified using a combination of either --space-file (-f) and --page (-p) + or --system-space-file (-s), --table-name (-T), and --index-name (-I) + }.squish ) end if /^page-/.match(mode) && !page - usage(1, 'Page number must be specified using -p/--page') + usage(1, "Page number must be specified using --page (-p)") end if /^record-/.match(mode) && !@options.record - usage(1, 'Record offset must be specified using -R/--record') + usage(1, "Record offset must be specified using --record (-R)") +end + +if /^record-/.match(mode) && !page + usage( + 1, + %{ + An index page must be available when using --record (-R); specify either + --page (-p) or --table-name (-T) and --index-name (-I) for the index root page. + } + ) +end + +if /^record-/.match(mode) && !page.is_a?(Innodb::Page::Index) + usage(1, "Mode #{mode} may be used only with index pages") end if /-list-iterate$/.match(mode) && !@options.list - usage(1, 'List name must be specified using -L/--list') + usage(1, "List name must be specified using --list (-L)") end if /-level-/.match(mode) && !@options.level - usage(1, 'Level must be specified using -l/--level') + usage(1, "Level must be specified using --level (-l)") end if %w[ @@ -1620,13 +1668,13 @@ if %w[ index-digraph index-level-summary ].include?(mode) && !index.record_describer - usage(1, 'Record describer must be specified using -d/--describer') + usage(1, "Record describer must be specified using --describer (-d)") end if %w[ space-index-fseg-pages-summary ].include?(mode) && !@options.fseg_id - usage(1, 'File segment id must be specified using -F/--fseg-id') + usage(1, "File segment id must be specified using --fseg-id (-F)") end # rubocop:enable Style/IfUnlessModifier @@ -1634,86 +1682,85 @@ end BufferCursor.trace! if @options.trace.positive? case mode -when 'system-spaces' +when "system-spaces" system_spaces(innodb_system) -when 'data-dictionary-tables' +when "data-dictionary-tables" data_dictionary_tables(innodb_system) -when 'data-dictionary-columns' +when "data-dictionary-columns" data_dictionary_columns(innodb_system) -when 'data-dictionary-indexes' +when "data-dictionary-indexes" data_dictionary_indexes(innodb_system) -when 'data-dictionary-fields' - data_dictionary_fields(innodb_system) -when 'space-summary' +when "space-summary" space_summary(space, @options.page || 0) -when 'space-index-pages-summary' +when "space-index-pages-summary" space_index_pages_summary(space, @options.page || 0) -when 'space-index-fseg-pages-summary' +when "space-index-fseg-pages-summary" space_index_fseg_pages_summary(space, @options.fseg_id) -when 'space-index-pages-free-plot' - file_name = space.name.sub('.ibd', '').sub(/[^a-zA-Z0-9_]/, '_') - space_index_pages_free_plot(space, file_name, @options.page || 0) -when 'space-page-type-regions' +when "space-index-pages-free-plot" + space_index_pages_free_plot(space, @options.page || 0) +when "space-page-type-regions" space_page_type_regions(space, @options.page || 0) -when 'space-page-type-summary' +when "space-page-type-summary" space_page_type_summary(space, @options.page || 0) -when 'space-lists' +when "space-lists" space_lists(space) -when 'space-list-iterate' +when "space-list-iterate" space_list_iterate(space, @options.list) -when 'space-indexes' +when "space-indexes" space_indexes(innodb_system, space) -when 'space-extents' +when "space-extents" space_extents(space) -when 'space-extents-illustrate' +when "space-extents-illustrate" space_extents_illustrate(space) -when 'space-lsn-age-illustrate' +when "space-lsn-age-illustrate" space_lsn_age_illustrate(space) -when 'space-inodes-fseg-id' +when "space-inodes-fseg-id" space_inodes_fseg_id(space) -when 'space-inodes-summary' +when "space-inodes-summary" space_inodes_summary(space) -when 'space-inodes-detail' +when "space-inodes-detail" space_inodes_detail(space) -when 'index-recurse' +when "space-sdi-json-dump" + space_sdi_json_dump(space) +when "index-recurse" index_recurse(index) -when 'index-record-offsets' +when "index-record-offsets" index_record_offsets(index) -when 'index-digraph' +when "index-digraph" index_digraph(index) -when 'index-level-summary' +when "index-level-summary" index_level_summary(index, @options.level) -when 'index-fseg-leaf-lists' +when "index-fseg-leaf-lists" index_fseg_lists(index, :leaf) -when 'index-fseg-internal-lists' +when "index-fseg-internal-lists" index_fseg_lists(index, :internal) -when 'index-fseg-leaf-list-iterate' +when "index-fseg-leaf-list-iterate" index_fseg_list_iterate(index, :leaf, @options.list) -when 'index-fseg-internal-list-iterate' +when "index-fseg-internal-list-iterate" index_fseg_list_iterate(index, :internal, @options.list) -when 'index-fseg-leaf-frag-pages' +when "index-fseg-leaf-frag-pages" index_fseg_frag_pages(index, :leaf) -when 'index-fseg-internal-frag-pages' +when "index-fseg-internal-frag-pages" index_fseg_frag_pages(index, :internal) -when 'page-dump' +when "page-dump" page.dump -when 'page-account' +when "page-account" page_account(innodb_system, space, @options.page) -when 'page-validate' +when "page-validate" page_validate(innodb_system, space, @options.page) -when 'page-directory-summary' +when "page-directory-summary" page_directory_summary(space, page) -when 'page-records' +when "page-records" page_records(space, page) -when 'page-illustrate' +when "page-illustrate" page_illustrate(page) -when 'record-dump' +when "record-dump" record_dump(page, @options.record) -when 'record-history' +when "record-history" record_history(page, @options.record) -when 'undo-history-summary' +when "undo-history-summary" undo_history_summary(innodb_system) -when 'undo-record-dump' +when "undo-record-dump" undo_record_dump(innodb_system, page, @options.record) else usage 1, "Unknown mode: #{mode}" diff --git a/examples/describer/employees_db.rb b/examples/describer/employees_db.rb index b071b520..ba199f5f 100644 --- a/examples/describer/employees_db.rb +++ b/examples/describer/employees_db.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'innodb/record_describer' +require "innodb/record_describer" # CREATE TABLE employees ( # emp_no INT NOT NULL, @@ -14,12 +14,12 @@ class Employees_employees_PRIMARY < Innodb::RecordDescriber type :clustered - key 'emp_no', :INT, :NOT_NULL - row 'birth_date', :MEDIUMINT, :NOT_NULL - row 'first_name', 'VARCHAR(14)', :NOT_NULL - row 'last_name', 'VARCHAR(16)', :NOT_NULL - row 'gender', :TINYINT, :UNSIGNED, :NOT_NULL - row 'hire_date', :MEDIUMINT, :NOT_NULL + key "emp_no", :INT, :NOT_NULL + row "birth_date", :MEDIUMINT, :NOT_NULL + row "first_name", "VARCHAR(14)", :NOT_NULL + row "last_name", "VARCHAR(16)", :NOT_NULL + row "gender", :TINYINT, :UNSIGNED, :NOT_NULL + row "hire_date", :MEDIUMINT, :NOT_NULL end # CREATE TABLE departments ( @@ -31,14 +31,14 @@ class Employees_employees_PRIMARY < Innodb::RecordDescriber class Employees_departments_PRIMARY < Innodb::RecordDescriber type :clustered - key 'dept_no', 'CHAR(4)', :NOT_NULL - row 'dept_name', 'VARCHAR(40)', :NOT_NULL + key "dept_no", "CHAR(4)", :NOT_NULL + row "dept_name", "VARCHAR(40)", :NOT_NULL end class Employees_departments_dept_name < Innodb::RecordDescriber type :secondary - key 'dept_name', 'VARCHAR(40)', :NOT_NULL - row 'dept_no', 'CHAR(4)', :NOT_NULL + key "dept_name", "VARCHAR(40)", :NOT_NULL + row "dept_no", "CHAR(4)", :NOT_NULL end # CREATE TABLE dept_manager ( @@ -55,22 +55,22 @@ class Employees_departments_dept_name < Innodb::RecordDescriber class Employees_dept_manager_PRIMARY < Innodb::RecordDescriber type :clustered - key 'emp_no', :INT, :NOT_NULL - key 'dept_no', 'CHAR(4)', :NOT_NULL - row 'from_date', :MEDIUMINT, :NOT_NULL - row 'to_date', :MEDIUMINT, :NOT_NULL + key "emp_no", :INT, :NOT_NULL + key "dept_no", "CHAR(4)", :NOT_NULL + row "from_date", :MEDIUMINT, :NOT_NULL + row "to_date", :MEDIUMINT, :NOT_NULL end class Employees_dept_manager_emp_no < Innodb::RecordDescriber type :secondary - key 'emp_no', :INT, :NOT_NULL - row 'dept_no', 'CHAR(4)', :NOT_NULL + key "emp_no", :INT, :NOT_NULL + row "dept_no", "CHAR(4)", :NOT_NULL end class Employees_dept_manager_dept_no < Innodb::RecordDescriber type :secondary - key 'dept_no', 'CHAR(4)', :NOT_NULL - row 'emp_no', :INT, :NOT_NULL + key "dept_no", "CHAR(4)", :NOT_NULL + row "emp_no", :INT, :NOT_NULL end # CREATE TABLE dept_emp ( @@ -87,22 +87,22 @@ class Employees_dept_manager_dept_no < Innodb::RecordDescriber class Employees_dept_emp_PRIMARY < Innodb::RecordDescriber type :clustered - key 'emp_no', :INT, :NOT_NULL - key 'dept_no', 'CHAR(4)', :NOT_NULL - row 'from_date', :MEDIUMINT, :NOT_NULL - row 'to_date', :MEDIUMINT, :NOT_NULL + key "emp_no", :INT, :NOT_NULL + key "dept_no", "CHAR(4)", :NOT_NULL + row "from_date", :MEDIUMINT, :NOT_NULL + row "to_date", :MEDIUMINT, :NOT_NULL end class Employees_dept_emp_emp_no < Innodb::RecordDescriber type :secondary - key 'emp_no', :INT, :NOT_NULL - row 'dept_no', 'CHAR(4)', :NOT_NULL + key "emp_no", :INT, :NOT_NULL + row "dept_no", "CHAR(4)", :NOT_NULL end class Employees_dept_emp_dept_no < Innodb::RecordDescriber type :secondary - key 'dept_no', 'CHAR(4)', :NOT_NULL - row 'emp_no', :INT, :NOT_NULL + key "dept_no", "CHAR(4)", :NOT_NULL + row "emp_no", :INT, :NOT_NULL end # CREATE TABLE titles ( @@ -117,17 +117,17 @@ class Employees_dept_emp_dept_no < Innodb::RecordDescriber class Employees_titles_PRIMARY < Innodb::RecordDescriber type :clustered - key 'emp_no', :INT, :NOT_NULL - key 'title', 'VARCHAR(50)', :NOT_NULL - key 'from_date', :MEDIUMINT, :NOT_NULL - row 'to_date', :MEDIUMINT, :NOT_NULL + key "emp_no", :INT, :NOT_NULL + key "title", "VARCHAR(50)", :NOT_NULL + key "from_date", :MEDIUMINT, :NOT_NULL + row "to_date", :MEDIUMINT, :NOT_NULL end class Employees_titles_emp_no < Innodb::RecordDescriber type :secondary - key 'emp_no', :INT, :NOT_NULL - row 'title', 'VARCHAR(50)', :NOT_NULL - row 'from_date', :MEDIUMINT, :NOT_NULL + key "emp_no", :INT, :NOT_NULL + row "title", "VARCHAR(50)", :NOT_NULL + row "from_date", :MEDIUMINT, :NOT_NULL end # CREATE TABLE salaries ( @@ -142,14 +142,14 @@ class Employees_titles_emp_no < Innodb::RecordDescriber class Employees_salaries_PRIMARY < Innodb::RecordDescriber type :clustered - key 'emp_no', :INT, :NOT_NULL - key 'from_date', :MEDIUMINT, :NOT_NULL - row 'salary', :INT, :NOT_NULL - row 'to_date', :MEDIUMINT, :NOT_NULL + key "emp_no", :INT, :NOT_NULL + key "from_date", :MEDIUMINT, :NOT_NULL + row "salary", :INT, :NOT_NULL + row "to_date", :MEDIUMINT, :NOT_NULL end class Employees_salaries_emp_no < Innodb::RecordDescriber type :secondary - key 'emp_no', :INT, :NOT_NULL - row 'from_date', :MEDIUMINT, :NOT_NULL + key "emp_no", :INT, :NOT_NULL + row "from_date", :MEDIUMINT, :NOT_NULL end diff --git a/examples/describer/hello_world.rb b/examples/describer/hello_world.rb index a989c9bc..1b678679 100644 --- a/examples/describer/hello_world.rb +++ b/examples/describer/hello_world.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'innodb/record_describer' +require "innodb/record_describer" # CREATE TABLE hello_world ( # id INT NOT NULL, @@ -15,13 +15,13 @@ class HelloWorld_PRIMARY < Innodb::RecordDescriber type :clustered - key 'id', :INT, :NOT_NULL - row 'message', 'VARCHAR(100)', :NOT_NULL - row 'author', 'VARCHAR(100)', :NOT_NULL + key "id", :INT, :NOT_NULL + row "message", "VARCHAR(100)", :NOT_NULL + row "author", "VARCHAR(100)", :NOT_NULL end class HelloWorld_message < Innodb::RecordDescriber type :secondary - key 'message', 'VARCHAR(100)', :NOT_NULL - row 'id', :INT, :NOT_NULL + key "message", "VARCHAR(100)", :NOT_NULL + row "id", :INT, :NOT_NULL end diff --git a/examples/describer/simple_describer.rb b/examples/describer/simple_describer.rb index 3eab1176..b623b688 100644 --- a/examples/describer/simple_describer.rb +++ b/examples/describer/simple_describer.rb @@ -2,6 +2,6 @@ class SimpleDescriber < Innodb::RecordDescriber type :clustered - key 'i', :INT, :NOT_NULL - row 's', 'VARCHAR(100)', :NOT_NULL + key "i", :INT, :NOT_NULL + row "s", "VARCHAR(100)", :NOT_NULL end diff --git a/innodb_ruby.gemspec b/innodb_ruby.gemspec index 20682789..3a0bef6d 100644 --- a/innodb_ruby.gemspec +++ b/innodb_ruby.gemspec @@ -1,32 +1,36 @@ # frozen_string_literal: true -lib = File.expand_path('lib', __dir__) +lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) -require 'innodb/version' +require "innodb/version" Gem::Specification.new do |s| - s.name = 'innodb_ruby' + s.name = "innodb_ruby" s.version = Innodb::VERSION - s.date = Time.now.strftime('%Y-%m-%d') - s.summary = 'InnoDB data file parser' - s.license = 'BSD-3-Clause' - s.description = 'Library for parsing InnoDB data files in Ruby' + s.summary = "InnoDB data file parser" + s.license = "BSD-3-Clause" + s.description = "Library for parsing InnoDB data files in Ruby" s.authors = [ - 'Jeremy Cole', - 'Davi Arnaut', + "Jeremy Cole", + "Davi Arnaut", ] - s.email = 'jeremy@jcole.us' - s.homepage = 'https://github.com/jeremycole/innodb_ruby' - s.files = Dir.glob('{bin,lib}/**/*') + %w[LICENSE AUTHORS.md README.md] + s.email = "jeremy@jcole.us" + s.homepage = "https://github.com/jeremycole/innodb_ruby" + s.files = Dir.glob("{bin,lib}/**/*") + %w[LICENSE AUTHORS.md README.md] s.executables = %w[innodb_log innodb_space] - s.required_ruby_version = '>= 2.6' + s.required_ruby_version = ">= 2.6" - s.add_runtime_dependency('bindata', '~> 1.4', '>= 1.4.5') - s.add_runtime_dependency('digest-crc', '~> 0.4', '>= 0.4.1') - s.add_runtime_dependency('histogram', '~> 0.2') + s.add_dependency("bigdecimal", "~> 3.1.8") + s.add_dependency("bindata", ">= 1.4.5", "< 3.0") + s.add_dependency("csv", "~> 3.3") + s.add_dependency("digest-crc", "~> 0.4", ">= 0.4.1") + s.add_dependency("getoptlong", "~> 0.2.1") + s.add_dependency("histogram", "~> 0.2") - s.add_development_dependency('gnuplot') - s.add_development_dependency('rspec') - s.add_development_dependency('rubocop') + s.add_development_dependency("gnuplot", "~> 2.6.0") + s.add_development_dependency("rspec", "~> 3.11.0") + s.add_development_dependency("rubocop", "~> 1.18") + s.add_development_dependency("rubocop-rspec", "~> 2.4") + s.metadata["rubygems_mfa_required"] = "false" end diff --git a/lib/innodb.rb b/lib/innodb.rb index c5b85d6c..62ea5248 100644 --- a/lib/innodb.rb +++ b/lib/innodb.rb @@ -14,41 +14,71 @@ def self.debug=(value) end end -require 'pp' -require 'digest/crc32c' -require 'innodb/util/buffer_cursor' -require 'innodb/util/read_bits_at_offset' +require "digest/crc32c" +require "innodb/util/buffer_cursor" +require "innodb/util/read_bits_at_offset" +require "innodb/util/hex_format" -require 'innodb/version' -require 'innodb/stats' -require 'innodb/checksum' -require 'innodb/record_describer' -require 'innodb/data_dictionary' -require 'innodb/page' -require 'innodb/page/blob' -require 'innodb/page/fsp_hdr_xdes' -require 'innodb/page/ibuf_bitmap' -require 'innodb/page/inode' -require 'innodb/page/index' -require 'innodb/page/trx_sys' -require 'innodb/page/sys' -require 'innodb/page/undo_log' -require 'innodb/record' -require 'innodb/field' -require 'innodb/space' -require 'innodb/system' -require 'innodb/history' -require 'innodb/history_list' -require 'innodb/ibuf_bitmap' -require 'innodb/ibuf_index' -require 'innodb/inode' -require 'innodb/index' -require 'innodb/log_record' -require 'innodb/log_block' -require 'innodb/log' -require 'innodb/lsn' -require 'innodb/log_group' -require 'innodb/log_reader' -require 'innodb/undo_log' -require 'innodb/undo_record' -require 'innodb/xdes' +require "innodb/version" +require "innodb/stats" +require "innodb/checksum" +require "innodb/mysql_collation" +require "innodb/mysql_collations" +require "innodb/mysql_type" +require "innodb/record_describer" +require "innodb/sys_data_dictionary" +require "innodb/sdi" +require "innodb/sdi/sdi_object" +require "innodb/sdi/table" +require "innodb/sdi/table_column" +require "innodb/sdi/table_index" +require "innodb/sdi/table_index_element" +require "innodb/sdi/tablespace" +require "innodb/sdi_data_dictionary" +require "innodb/data_dictionary" +require "innodb/page" +require "innodb/page/blob" +require "innodb/page/fsp_hdr_xdes" +require "innodb/page/ibuf_bitmap" +require "innodb/page/inode" +require "innodb/page/index" +require "innodb/page/trx_sys" +require "innodb/page/sdi" +require "innodb/page/sdi_blob" +require "innodb/page/sys" +require "innodb/page/undo_log" +require "innodb/data_type" +require "innodb/data_type/bit" +require "innodb/data_type/blob" +require "innodb/data_type/character" +require "innodb/data_type/date" +require "innodb/data_type/datetime" +require "innodb/data_type/decimal" +require "innodb/data_type/enum" +require "innodb/data_type/floating_point" +require "innodb/data_type/innodb_roll_pointer" +require "innodb/data_type/innodb_transaction_id" +require "innodb/data_type/integer" +require "innodb/data_type/set" +require "innodb/data_type/time" +require "innodb/data_type/timestamp" +require "innodb/data_type/year" +require "innodb/record" +require "innodb/field" +require "innodb/space" +require "innodb/system" +require "innodb/history" +require "innodb/history_list" +require "innodb/ibuf_bitmap" +require "innodb/ibuf_index" +require "innodb/inode" +require "innodb/index" +require "innodb/log_record" +require "innodb/log_block" +require "innodb/log" +require "innodb/lsn" +require "innodb/log_group" +require "innodb/log_reader" +require "innodb/undo_log" +require "innodb/undo_record" +require "innodb/xdes" diff --git a/lib/innodb/data_dictionary.rb b/lib/innodb/data_dictionary.rb index ff796abb..b401f0ae 100644 --- a/lib/innodb/data_dictionary.rb +++ b/lib/innodb/data_dictionary.rb @@ -1,637 +1,47 @@ # frozen_string_literal: true -# A class representing InnoDB's data dictionary, which contains metadata about -# tables, columns, and indexes. +require "innodb/data_dictionary/tablespaces" +require "innodb/data_dictionary/tables" + module Innodb class DataDictionary - MysqlType = Struct.new( - :value, - :type, - keyword_init: true - ) - - # rubocop:disable Layout/ExtraSpacing - - # A record describer for SYS_TABLES clustered records. - class SysTablesPrimary < Innodb::RecordDescriber - type :clustered - key 'NAME', 'VARCHAR(100)', :NOT_NULL - row 'ID', :BIGINT, :UNSIGNED, :NOT_NULL - row 'N_COLS', :INT, :UNSIGNED, :NOT_NULL - row 'TYPE', :INT, :UNSIGNED, :NOT_NULL - row 'MIX_ID', :BIGINT, :UNSIGNED, :NOT_NULL - row 'MIX_LEN', :INT, :UNSIGNED, :NOT_NULL - row 'CLUSTER_NAME', 'VARCHAR(100)', :NOT_NULL - row 'SPACE', :INT, :UNSIGNED, :NOT_NULL - end - - # A record describer for SYS_TABLES secondary key on ID. - class SysTablesId < Innodb::RecordDescriber - type :secondary - key 'ID', :BIGINT, :UNSIGNED, :NOT_NULL - row 'NAME', 'VARCHAR(100)', :NOT_NULL - end - - # A record describer for SYS_COLUMNS clustered records. - class SysColumnsPrimary < Innodb::RecordDescriber - type :clustered - key 'TABLE_ID', :BIGINT, :UNSIGNED, :NOT_NULL - key 'POS', :INT, :UNSIGNED, :NOT_NULL - row 'NAME', 'VARCHAR(100)', :NOT_NULL - row 'MTYPE', :INT, :UNSIGNED, :NOT_NULL - row 'PRTYPE', :INT, :UNSIGNED, :NOT_NULL - row 'LEN', :INT, :UNSIGNED, :NOT_NULL - row 'PREC', :INT, :UNSIGNED, :NOT_NULL - end - - # A record describer for SYS_INDEXES clustered records. - class SysIndexesPrimary < Innodb::RecordDescriber - type :clustered - key 'TABLE_ID', :BIGINT, :UNSIGNED, :NOT_NULL - key 'ID', :BIGINT, :UNSIGNED, :NOT_NULL - row 'NAME', 'VARCHAR(100)', :NOT_NULL - row 'N_FIELDS', :INT, :UNSIGNED, :NOT_NULL - row 'TYPE', :INT, :UNSIGNED, :NOT_NULL - row 'SPACE', :INT, :UNSIGNED, :NOT_NULL - row 'PAGE_NO', :INT, :UNSIGNED, :NOT_NULL - end - - # A record describer for SYS_FIELDS clustered records. - class SysFieldsPrimary < Innodb::RecordDescriber - type :clustered - key 'INDEX_ID', :BIGINT, :UNSIGNED, :NOT_NULL - key 'POS', :INT, :UNSIGNED, :NOT_NULL - row 'COL_NAME', 'VARCHAR(100)', :NOT_NULL - end - - # rubocop:enable Layout/ExtraSpacing - - # A hash of hashes of table name and index name to describer - # class. - DATA_DICTIONARY_RECORD_DESCRIBERS = { - SYS_TABLES: { - PRIMARY: SysTablesPrimary, - ID: SysTablesId, - }.freeze, - SYS_COLUMNS: { PRIMARY: SysColumnsPrimary }.freeze, - SYS_INDEXES: { PRIMARY: SysIndexesPrimary }.freeze, - SYS_FIELDS: { PRIMARY: SysFieldsPrimary }.freeze, - }.freeze - - # A hash of MySQL's internal type system to the stored - # values for those types, and the 'external' SQL type. - # rubocop:disable Layout/HashAlignment - # rubocop:disable Layout/CommentIndentation - MYSQL_TYPE = { - # DECIMAL: MysqlType.new(value: 0, type: :DECIMAL), - TINY: MysqlType.new(value: 1, type: :TINYINT), - SHORT: MysqlType.new(value: 2, type: :SMALLINT), - LONG: MysqlType.new(value: 3, type: :INT), - FLOAT: MysqlType.new(value: 4, type: :FLOAT), - DOUBLE: MysqlType.new(value: 5, type: :DOUBLE), - # NULL: MysqlType.new(value: 6, type: nil), - TIMESTAMP: MysqlType.new(value: 7, type: :TIMESTAMP), - LONGLONG: MysqlType.new(value: 8, type: :BIGINT), - INT24: MysqlType.new(value: 9, type: :MEDIUMINT), - # DATE: MysqlType.new(value: 10, type: :DATE), - TIME: MysqlType.new(value: 11, type: :TIME), - DATETIME: MysqlType.new(value: 12, type: :DATETIME), - YEAR: MysqlType.new(value: 13, type: :YEAR), - NEWDATE: MysqlType.new(value: 14, type: :DATE), - VARCHAR: MysqlType.new(value: 15, type: :VARCHAR), - BIT: MysqlType.new(value: 16, type: :BIT), - NEWDECIMAL: MysqlType.new(value: 246, type: :CHAR), - # ENUM: MysqlType.new(value: 247, type: :ENUM), - # SET: MysqlType.new(value: 248, type: :SET), - TINY_BLOB: MysqlType.new(value: 249, type: :TINYBLOB), - MEDIUM_BLOB: MysqlType.new(value: 250, type: :MEDIUMBLOB), - LONG_BLOB: MysqlType.new(value: 251, type: :LONGBLOB), - BLOB: MysqlType.new(value: 252, type: :BLOB), - # VAR_STRING: MysqlType.new(value: 253, type: :VARCHAR), - STRING: MysqlType.new(value: 254, type: :CHAR), - GEOMETRY: MysqlType.new(value: 255, type: :GEOMETRY), - }.freeze - # rubocop:enable Layout/CommentIndentation - # rubocop:enable Layout/HashAlignment - - # A hash of MYSQL_TYPE keys by value :value key. - MYSQL_TYPE_BY_VALUE = MYSQL_TYPE.transform_values(&:value).invert.freeze - - # A hash of InnoDB's internal type system to the values - # stored for each type. - COLUMN_MTYPE = { - VARCHAR: 1, - CHAR: 2, - FIXBINARY: 3, - BINARY: 4, - BLOB: 5, - INT: 6, - SYS_CHILD: 7, - SYS: 8, - FLOAT: 9, - DOUBLE: 10, - DECIMAL: 11, - VARMYSQL: 12, - MYSQL: 13, - }.freeze - - # A hash of COLUMN_MTYPE keys by value. - COLUMN_MTYPE_BY_VALUE = COLUMN_MTYPE.invert.freeze - - # A hash of InnoDB 'precise type' bitwise flags. - COLUMN_PRTYPE_FLAG = { - NOT_NULL: 256, - UNSIGNED: 512, - BINARY: 1024, - LONG_TRUE_VARCHAR: 4096, - }.freeze - - # A hash of COLUMN_PRTYPE keys by value. - COLUMN_PRTYPE_FLAG_BY_VALUE = COLUMN_PRTYPE_FLAG.invert.freeze - - # The bitmask to extract the MySQL internal type - # from the InnoDB 'precise type'. - COLUMN_PRTYPE_MYSQL_TYPE_MASK = 0xFF - - # A hash of InnoDB's index type flags. - INDEX_TYPE_FLAG = { - CLUSTERED: 1, - UNIQUE: 2, - UNIVERSAL: 4, - IBUF: 8, - CORRUPT: 16, - FTS: 32, - }.freeze - - # A hash of INDEX_TYPE_FLAG keys by value. - INDEX_TYPE_FLAG_BY_VALUE = INDEX_TYPE_FLAG.invert.freeze - - # Return the 'external' SQL type string (such as 'VARCHAR' or - # 'INT') given the stored mtype and prtype from the InnoDB - # data dictionary. Note that not all types are extractable - # into fully defined SQL types due to the lossy nature of - # the MySQL-to-InnoDB interface regarding types. - def self.mtype_prtype_to_type_string(mtype, prtype, len, prec) - mysql_type = prtype & COLUMN_PRTYPE_MYSQL_TYPE_MASK - internal_type = MYSQL_TYPE_BY_VALUE[mysql_type] - external_type = MYSQL_TYPE[internal_type].type - - case external_type - when :VARCHAR - # One-argument: length. - '%s(%i)' % [external_type, len] - when :FLOAT, :DOUBLE - # Two-argument: length and precision. - '%s(%i,%i)' % [external_type, len, prec] - when :CHAR - if COLUMN_MTYPE_BY_VALUE[mtype] == :MYSQL - # When the mtype is :MYSQL, the column is actually - # stored as VARCHAR despite being a CHAR. This is - # done for CHAR columns having multi-byte character - # sets in order to limit size. Note that such data - # are still space-padded to at least len. - 'VARCHAR(%i)' % [len] - else - 'CHAR(%i)' % [len] + attr_reader :tablespaces + attr_reader :tables + attr_reader :indexes + attr_reader :columns + + def initialize + @tablespaces = Tablespaces.new + @tables = Tables.new + @indexes = Indexes.new + @columns = Columns.new + end + + def inspect + format("#<%s: %i tablespaces, %i tables, %i indexes, %i columns>", + self.class.name, + tablespaces.count, + tables.count, + indexes.count, + columns.count) + end + + def refresh + tables.each do |table| + table.indexes.each do |index| + indexes.add(index) end - when :DECIMAL - # The DECIMAL type is designated as DECIMAL(M,D) - # however the M and D definitions are not stored - # in the InnoDB data dictionary. We need to define - # the column as something which will extract the - # raw bytes in order to read the column, but we - # can't figure out the right decimal type. The - # len stored here is actually the on-disk storage - # size. - 'CHAR(%i)' % [len] - else - external_type - end - end - - # Return a full data type given an mtype and prtype, such - # as ['VARCHAR(10)', :NOT_NULL] or [:INT, :UNSIGNED]. - def self.mtype_prtype_to_data_type(mtype, prtype, len, prec) - type = mtype_prtype_to_type_string(mtype, prtype, len, prec) - raise "Unsupported type (mtype #{mtype}, prtype #{prtype})" unless type - data_type = [type] - data_type << :NOT_NULL if prtype & COLUMN_PRTYPE_FLAG[:NOT_NULL] != 0 - data_type << :UNSIGNED if prtype & COLUMN_PRTYPE_FLAG[:UNSIGNED] != 0 - - data_type - end - - attr_reader :system_space - - def initialize(system_space) - @system_space = system_space - end - - # A helper method to reach inside the system space and retrieve - # the data dictionary index locations from the data dictionary - # header. - def data_dictionary_indexes - system_space.data_dictionary_page.data_dictionary_header[:indexes] - end - - def data_dictionary_index_ids - return @data_dictionary_index_ids if @data_dictionary_index_ids - - # TODO: This could probably be done a lot more Ruby-like. - @data_dictionary_index_ids = {} - data_dictionary_indexes.each do |table, indexes| - indexes.each do |index, root_page_number| - root_page = system_space.page(root_page_number) - next unless root_page - - @data_dictionary_index_ids[root_page.index_id] = { - table: table, - index: index, - } + table.columns.each do |column| + columns.add(column) end end - @data_dictionary_index_ids - end - - def data_dictionary_table?(table_name) - DATA_DICTIONARY_RECORD_DESCRIBERS.include?(table_name.to_sym) - end - - def data_dictionary_index?(table_name, index_name) - return unless data_dictionary_table?(table_name) - - DATA_DICTIONARY_RECORD_DESCRIBERS[table_name.to_sym].include?(index_name.to_sym) - end - - def data_dictionary_index_describer(table_name, index_name) - return unless data_dictionary_index?(table_name, index_name) - - DATA_DICTIONARY_RECORD_DESCRIBERS[table_name.to_sym][index_name.to_sym].new - end - - # Return an Innodb::Index object initialized to the - # internal data dictionary index with an appropriate - # record describer so that records can be recursed. - def data_dictionary_index(table_name, index_name) - table_entry = data_dictionary_indexes[table_name] - raise "Unknown data dictionary table #{table_name}" unless table_entry - - index_root_page = table_entry[index_name] - raise "Unknown data dictionary index #{table_name}.#{index_name}" unless index_root_page - - # If we have a record describer for this index, load it. - record_describer = data_dictionary_index_describer(table_name, index_name) - - system_space.index(index_root_page, record_describer) - end - - # Iterate through all data dictionary indexes, yielding the - # table name, index name, and root page number. - def each_data_dictionary_index_root_page_number - return enum_for(:each_data_dictionary_index_root_page_number) unless block_given? - - data_dictionary_indexes.each do |table_name, indexes| - indexes.each do |index_name, root_page_number| - yield table_name, index_name, root_page_number - end - end - - nil - end - - # Iterate through all data dictionary indexes, yielding the table - # name, index name, and the index itself as an Innodb::Index. - def each_data_dictionary_index - return enum_for(:each_data_dictionary_index) unless block_given? - - data_dictionary_indexes.each do |table_name, indexes| - indexes.each do |index_name, _root_page_number| - yield table_name, index_name, - data_dictionary_index(table_name, index_name) - end - end - - nil - end - - # Iterate through records from a data dictionary index yielding each record - # as a Innodb::Record object. - def each_record_from_data_dictionary_index(table, index) - return enum_for(:each_index, table, index) unless block_given? - - data_dictionary_index(table, index).each_record do |record| - yield record - end - nil end - # Iterate through the records in the SYS_TABLES data dictionary table. - def each_table - return enum_for(:each_table) unless block_given? - - each_record_from_data_dictionary_index(:SYS_TABLES, :PRIMARY) do |record| - yield record.fields - end - - nil - end - - # Iterate through the records in the SYS_COLUMNS data dictionary table. - def each_column - return enum_for(:each_column) unless block_given? - - each_record_from_data_dictionary_index(:SYS_COLUMNS, :PRIMARY) do |record| - yield record.fields - end - - nil - end - - # Iterate through the records in the SYS_INDEXES dictionary table. - def each_index - return enum_for(:each_index) unless block_given? - - each_record_from_data_dictionary_index(:SYS_INDEXES, :PRIMARY) do |record| - yield record.fields - end - - nil - end - - # Iterate through the records in the SYS_FIELDS data dictionary table. - def each_field - return enum_for(:each_field) unless block_given? - - each_record_from_data_dictionary_index(:SYS_FIELDS, :PRIMARY) do |record| - yield record.fields - end - - nil - end - - # A helper to iterate the method provided and return the first record - # where the record's field matches the provided value. - def object_by_field(method, field, value) - send(method).select { |o| o[field] == value }.first - end - - # A helper to iterate the method provided and return the first record - # where the record's fields f1 and f2 match the provided values v1 and v2. - def object_by_two_fields(method, field1, value1, field2, value2) - send(method).select { |o| o[field1] == value1 && o[field2] == value2 }.first - end - - # Lookup a table by table ID. - def table_by_id(table_id) - object_by_field(:each_table, 'ID', table_id) - end - - # Lookup a table by table name. - def table_by_name(table_name) - object_by_field(:each_table, 'NAME', table_name) - end - - # Lookup a table by space ID. - def table_by_space_id(space_id) - object_by_field(:each_table, 'SPACE', space_id) - end - - # Lookup a column by table name and column name. - def column_by_name(table_name, column_name) - table = table_by_name(table_name) - return unless table - - object_by_two_fields(:each_column, 'TABLE_ID', table['ID'], 'NAME', column_name) - end - - # Lookup an index by index ID. - def index_by_id(index_id) - object_by_field(:each_index, 'ID', index_id) - end - - # Lookup an index by table name and index name. - def index_by_name(table_name, index_name) - table = table_by_name(table_name) - return unless table - - object_by_two_fields(:each_index, 'TABLE_ID', table['ID'], 'NAME', index_name) - end - - # Iterate through indexes by space ID. - def each_index_by_space_id(space_id) - return enum_for(:each_index_by_space_id, space_id) unless block_given? - - each_index do |record| - yield record if record['SPACE'] == space_id - end - - nil - end - - # Iterate through all indexes in a table by table ID. - def each_index_by_table_id(table_id) - return enum_for(:each_index_by_table_id, table_id) unless block_given? - - each_index do |record| - yield record if record['TABLE_ID'] == table_id - end - - nil - end - - # Iterate through all indexes in a table by table name. - def each_index_by_table_name(table_name) - return enum_for(:each_index_by_table_name, table_name) unless block_given? - - table = table_by_name(table_name) - raise "Table #{table_name} not found" unless table - - each_index_by_table_id(table['ID']) do |record| - yield record - end - - nil - end - - # Iterate through all fields in an index by index ID. - def each_field_by_index_id(index_id) - return enum_for(:each_field_by_index_id, index_id) unless block_given? - - each_field do |record| - yield record if record['INDEX_ID'] == index_id - end - - nil - end - - # Iterate through all fields in an index by index name. - def each_field_by_index_name(table_name, index_name) - return enum_for(:each_field_by_name, table_name, index_name) unless block_given? - - index = index_by_name(table_name, index_name) - raise "Index #{index_name} for table #{table_name} not found" unless index - - each_field_by_index_id(index['ID']) do |record| - yield record - end - - nil - end - - # Iterate through all columns in a table by table ID. - def each_column_by_table_id(table_id) - return enum_for(:each_column_by_table_id, table_id) unless block_given? - - each_column do |record| - yield record if record['TABLE_ID'] == table_id - end - - nil - end - - # Iterate through all columns in a table by table name. - def each_column_by_table_name(table_name) - return enum_for(:each_column_by_table_name, table_name) unless block_given? - raise "Table #{table_name} not found" unless (table = table_by_name(table_name)) - - each_column_by_table_id(table['ID']) do |record| - yield record - end - - nil - end - - # Iterate through all columns in an index by table name and index name. - def each_column_in_index_by_name(table_name, index_name) - return enum_for(:each_column_in_index_by_name, table_name, index_name) unless block_given? - - each_field_by_index_name(table_name, index_name) do |record| - yield column_by_name(table_name, record['COL_NAME']) - end - - nil - end - - # Iterate through all columns not in an index by table name and index name. - # This is useful when building index descriptions for secondary indexes. - def each_column_not_in_index_by_name(table_name, index_name) - return enum_for(:each_column_not_in_index_by_name, table_name, index_name) unless block_given? - - columns_in_index = {} - each_column_in_index_by_name(table_name, index_name) do |record| - columns_in_index[record['NAME']] = 1 - end - - each_column_by_table_name(table_name) do |record| - yield record unless columns_in_index.include?(record['NAME']) - end - - nil - end - - # Return the name of the clustered index (usually 'PRIMARY', but not always) - # for a given table name. - def clustered_index_name_by_table_name(table_name) - table_record = table_by_name(table_name) - raise "Table #{table_name} not found" unless table_record - - index_record = object_by_two_fields(:each_index, 'TABLE_ID', table_record['ID'], 'TYPE', 3) - index_record['NAME'] if index_record - end - - # Produce a Innodb::RecordDescriber-compatible column description - # given a type (:key, :row) and data dictionary SYS_COLUMNS record. - def _make_column_description(type, record) - { - type: type, - name: record['NAME'], - description: self.class.mtype_prtype_to_data_type( - record['MTYPE'], - record['PRTYPE'], - record['LEN'], - record['PREC'] - ), - } - end - - # Iterate through Innodb::RecordDescriber-compatible column descriptions - # for a given index by table name and index name. - def each_column_description_by_index_name(table_name, index_name) - return enum_for(:each_column_description_by_index_name, table_name, index_name) unless block_given? - - unless (index = index_by_name(table_name, index_name)) - raise "Index #{index_name} for table #{table_name} not found" - end - - columns_in_index = {} - each_column_in_index_by_name(table_name, index_name) do |record| - columns_in_index[record['NAME']] = 1 - yield _make_column_description(:key, record) - end - - if index['TYPE'] & INDEX_TYPE_FLAG[:CLUSTERED] != 0 - each_column_by_table_name(table_name) do |record| - yield _make_column_description(:row, record) unless columns_in_index.include?(record['NAME']) - end - else - clustered_index_name = clustered_index_name_by_table_name(table_name) - - each_column_in_index_by_name(table_name, clustered_index_name) do |record| - yield _make_column_description(:row, record) - end - end - - nil - end - - # Return an Innodb::RecordDescriber object describing records for a given - # index by table name and index name. - def record_describer_by_index_name(table_name, index_name) - return data_dictionary_index_describer(table_name, index_name) if data_dictionary_index?(table_name, index_name) - - unless (index = index_by_name(table_name, index_name)) - raise "Index #{index_name} for table #{table_name} not found" - end - - describer = Innodb::RecordDescriber.new - - if index['TYPE'] & INDEX_TYPE_FLAG[:CLUSTERED] != 0 - describer.type :clustered - else - describer.type :secondary - end - - each_column_description_by_index_name(table_name, index_name) do |column| - case column[:type] - when :key - describer.key column[:name], *column[:description] - when :row - describer.row column[:name], *column[:description] - end - end - - describer - end - - # Return an Innodb::RecordDescriber object describing the records - # in a given index by index ID. - def record_describer_by_index_id(index_id) - if (dd_index = data_dictionary_index_ids[index_id]) - return data_dictionary_index_describer(dd_index[:table], dd_index[:index]) - end - - unless (index = index_by_id(index_id)) - raise "Index #{index_id} not found" - end - - unless (table = table_by_id(index['TABLE_ID'])) - raise "Table #{INDEX['TABLE_ID']} not found" - end - - record_describer_by_index_name(table['NAME'], index['NAME']) + def populated? + tablespaces.any? || tables.any? end end end diff --git a/lib/innodb/data_dictionary/column.rb b/lib/innodb/data_dictionary/column.rb new file mode 100644 index 00000000..8439811f --- /dev/null +++ b/lib/innodb/data_dictionary/column.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Innodb + class DataDictionary + class Column + attr_reader :name + attr_reader :description + attr_reader :table + + def initialize(name:, description:, table:) + @name = name + @description = description + @table = table + end + end + end +end diff --git a/lib/innodb/data_dictionary/columns.rb b/lib/innodb/data_dictionary/columns.rb new file mode 100644 index 00000000..b9b154e2 --- /dev/null +++ b/lib/innodb/data_dictionary/columns.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "innodb/data_dictionary/object_store" +require "innodb/data_dictionary/column" + +module Innodb + class DataDictionary + class Columns < ObjectStore + def initialize + super(allowed_type: Column) + end + end + end +end diff --git a/lib/innodb/data_dictionary/index.rb b/lib/innodb/data_dictionary/index.rb new file mode 100644 index 00000000..6200728a --- /dev/null +++ b/lib/innodb/data_dictionary/index.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "innodb/data_dictionary/index_column_references" + +module Innodb + class DataDictionary + class Index + attr_reader :name + attr_reader :type + attr_reader :tablespace + attr_reader :root_page_number + attr_reader :table + attr_reader :innodb_index_id + attr_reader :column_references + + def initialize(name:, type:, table:, tablespace:, root_page_number:, innodb_index_id: nil) + @name = name + @type = type + @table = table + @tablespace = tablespace + @root_page_number = root_page_number + @innodb_index_id = innodb_index_id + @column_references = IndexColumnReferences.new + end + + def record_describer + describer = Innodb::RecordDescriber.new + + describer.type(type) + + column_references.each do |column_reference| + case column_reference.usage + when :key + describer.key(column_reference.column.name, *column_reference.column.description) + when :row + describer.row(column_reference.column.name, *column_reference.column.description) + end + end + + describer + end + end + end +end diff --git a/lib/innodb/data_dictionary/index_column_reference.rb b/lib/innodb/data_dictionary/index_column_reference.rb new file mode 100644 index 00000000..c5930424 --- /dev/null +++ b/lib/innodb/data_dictionary/index_column_reference.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "forwardable" + +module Innodb + class DataDictionary + class IndexColumnReference + extend Forwardable + + attr_reader :column + attr_reader :usage + attr_reader :index + + def_delegators :column, :name, :description, :table + + def initialize(column:, usage:, index:) + @column = column + @usage = usage + @index = index + end + end + end +end diff --git a/lib/innodb/data_dictionary/index_column_references.rb b/lib/innodb/data_dictionary/index_column_references.rb new file mode 100644 index 00000000..28393b53 --- /dev/null +++ b/lib/innodb/data_dictionary/index_column_references.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "innodb/data_dictionary/object_store" +require "innodb/data_dictionary/index_column_reference" + +module Innodb + class DataDictionary + class IndexColumnReferences < ObjectStore + def initialize + super(allowed_type: IndexColumnReference) + end + end + end +end diff --git a/lib/innodb/data_dictionary/indexes.rb b/lib/innodb/data_dictionary/indexes.rb new file mode 100644 index 00000000..f71b023b --- /dev/null +++ b/lib/innodb/data_dictionary/indexes.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "innodb/data_dictionary/object_store" +require "innodb/data_dictionary/index" + +module Innodb + class DataDictionary + class Indexes < ObjectStore + def initialize + super(allowed_type: Index) + end + end + end +end diff --git a/lib/innodb/data_dictionary/object_store.rb b/lib/innodb/data_dictionary/object_store.rb new file mode 100644 index 00000000..7d381dad --- /dev/null +++ b/lib/innodb/data_dictionary/object_store.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "forwardable" + +module Innodb + class DataDictionary + class ObjectStore + extend Forwardable + def_delegators :@objects, :[], :first, :each, :empty?, :any?, :count + + class ObjectTypeError < RuntimeError; end + + attr_reader :allowed_type + attr_reader :objects + + def initialize(allowed_type: Object) + @allowed_type = allowed_type + @objects = [] + end + + def add(new_object) + raise ObjectTypeError unless new_object.is_a?(@allowed_type) + + @objects.push(new_object) + + new_object + end + + def make(**attributes) + add(@allowed_type.new(**attributes)) + end + + def by(**attributes) + @objects.select { |o| attributes.all? { |k, v| o.send(k) == v } } + end + + def find(**attributes) + by(**attributes).first + end + end + end +end diff --git a/lib/innodb/data_dictionary/table.rb b/lib/innodb/data_dictionary/table.rb new file mode 100644 index 00000000..7cc8cc2e --- /dev/null +++ b/lib/innodb/data_dictionary/table.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "innodb/data_dictionary/columns" +require "innodb/data_dictionary/indexes" + +module Innodb + class DataDictionary + class Table + attr_reader :name + attr_reader :tablespace + attr_reader :innodb_table_id + attr_reader :columns + attr_reader :indexes + + def initialize(name:, tablespace: nil, innodb_table_id: nil) + @name = name + @tablespace = tablespace + @innodb_table_id = innodb_table_id + @columns = Columns.new + @indexes = Indexes.new + end + + def clustered_index + indexes.find(type: :clustered) + end + end + end +end diff --git a/lib/innodb/data_dictionary/tables.rb b/lib/innodb/data_dictionary/tables.rb new file mode 100644 index 00000000..4d314303 --- /dev/null +++ b/lib/innodb/data_dictionary/tables.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "innodb/data_dictionary/object_store" +require "innodb/data_dictionary/table" + +module Innodb + class DataDictionary + class Tables < ObjectStore + def initialize + super(allowed_type: Table) + end + end + end +end diff --git a/lib/innodb/data_dictionary/tablespace.rb b/lib/innodb/data_dictionary/tablespace.rb new file mode 100644 index 00000000..137b3596 --- /dev/null +++ b/lib/innodb/data_dictionary/tablespace.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Innodb + class DataDictionary + class Tablespace + attr_reader :name + attr_reader :innodb_space_id + + def initialize(name:, innodb_space_id:) + @name = name + @innodb_space_id = innodb_space_id + end + end + end +end diff --git a/lib/innodb/data_dictionary/tablespaces.rb b/lib/innodb/data_dictionary/tablespaces.rb new file mode 100644 index 00000000..64df7a73 --- /dev/null +++ b/lib/innodb/data_dictionary/tablespaces.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "innodb/data_dictionary/object_store" +require "innodb/data_dictionary/tablespace" + +module Innodb + class DataDictionary + class Tablespaces < ObjectStore + def initialize + super(allowed_type: Tablespace) + end + end + end +end diff --git a/lib/innodb/data_type.rb b/lib/innodb/data_type.rb index 81b6a43f..1964bcfb 100644 --- a/lib/innodb/data_type.rb +++ b/lib/innodb/data_type.rb @@ -1,436 +1,121 @@ # frozen_string_literal: true -require 'stringio' -require 'bigdecimal' -require 'date' +require "csv" module Innodb class DataType - # MySQL's Bit-Value Type (BIT). - class BitType - attr_reader :name, :width + class InvalidSpecificationError < StandardError; end - def initialize(base_type, modifiers, properties) - nbits = modifiers.fetch(0, 1) - raise 'Unsupported width for BIT type.' unless nbits >= 0 && nbits <= 64 + # A hash of page types to specialized classes to handle them. Normally + # subclasses will register themselves in this list. + @specialized_classes = {} - @width = (nbits + 7) / 8 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def value(data) - '0b%b' % BinData.const_get('Uint%dbe' % (@width * 8)).read(data) - end + class << self + attr_reader :specialized_classes end - class IntegerType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = base_type_width_map[base_type] - @unsigned = properties.include?(:UNSIGNED) - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def base_type_width_map - { - BOOL: 1, - BOOLEAN: 1, - TINYINT: 1, - SMALLINT: 2, - MEDIUMINT: 3, - INT: 4, - INT6: 6, - BIGINT: 8, - } - end - - def value(data) - nbits = @width * 8 - @unsigned ? get_uint(data, nbits) : get_int(data, nbits) - end - - def get_uint(data, nbits) - BinData.const_get('Uint%dbe' % nbits).read(data) - end - - def get_int(data, nbits) - BinData.const_get('Int%dbe' % nbits).read(data) ^ (-1 << (nbits - 1)) - end + def self.register_specialization(data_type, specialized_class) + @specialized_classes[data_type] = specialized_class end - class FloatType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 4 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - # Read a little-endian single-precision floating-point number. - def value(data) - BinData::FloatLe.read(data) - end + def self.specialization_for(data_type) + # This needs to intentionally use Innodb::Page because we need to register + # in the class instance variable in *that* class. + Innodb::DataType.register_specialization(data_type, self) end - class DoubleType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 8 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - # Read a little-endian double-precision floating-point number. - def value(data) - BinData::DoubleLe.read(data) - end + def self.specialization_for?(data_type) + Innodb::DataType.specialized_classes.include?(data_type) end - # MySQL's Fixed-Point Type (DECIMAL), stored in InnoDB as a binary string. - class DecimalType - attr_reader :name, :width - - # The value is stored as a sequence of signed big-endian integers, each - # representing up to 9 digits of the integral and fractional parts. The - # first integer of the integral part and/or the last integer of the - # fractional part might be compressed (or packed) and are of variable - # length. The remaining integers (if any) are uncompressed and 32 bits - # wide. - MAX_DIGITS_PER_INTEGER = 9 - BYTES_PER_DIGIT = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4].freeze - - def initialize(base_type, modifiers, properties) - precision, scale = sanity_check(modifiers) - integral = precision - scale - @uncomp_integral = integral / MAX_DIGITS_PER_INTEGER - @uncomp_fractional = scale / MAX_DIGITS_PER_INTEGER - @comp_integral = integral - (@uncomp_integral * MAX_DIGITS_PER_INTEGER) - @comp_fractional = scale - (@uncomp_fractional * MAX_DIGITS_PER_INTEGER) - @width = @uncomp_integral * 4 + BYTES_PER_DIGIT[@comp_integral] + - @comp_fractional * 4 + BYTES_PER_DIGIT[@comp_fractional] - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def value(data) - # Strings representing the integral and fractional parts. - intg = ''.dup - frac = ''.dup - - stream = StringIO.new(data) - mask = sign_mask(stream) - - intg << get_digits(stream, mask, @comp_integral) - - (1..@uncomp_integral).each do - intg << get_digits(stream, mask, MAX_DIGITS_PER_INTEGER) - end - - (1..@uncomp_fractional).each do - frac << get_digits(stream, mask, MAX_DIGITS_PER_INTEGER) - end - - frac << get_digits(stream, mask, @comp_fractional) - frac = '0' if frac.empty? - - # Convert to something resembling a string representation. - str = mask.to_s.chop + intg + '.' + frac - - BigDecimal(str).to_s('F') - end - - private - - # Ensure width specification (if any) is compliant. - def sanity_check(modifiers) - raise 'Invalid width specification' unless modifiers.size <= 2 - - precision = modifiers.fetch(0, 10) - raise 'Unsupported precision for DECIMAL type' unless precision >= 1 && precision <= 65 - - scale = modifiers.fetch(1, 0) - raise 'Unsupported scale for DECIMAL type' unless scale >= 0 && scale <= 30 && scale <= precision - - [precision, scale] - end - - # The sign is encoded in the high bit of the first byte/digit. The byte - # might be part of a larger integer, so apply the bit-flipper and push - # back the byte into the stream. - def sign_mask(stream) - byte = BinData::Uint8.read(stream) - sign = byte & 0x80 - byte.assign(byte ^ 0x80) - stream.rewind - byte.write(stream) - stream.rewind - sign.zero? ? -1 : 0 - end - - # Return a string representing an integer with a specific number of digits. - def get_digits(stream, mask, digits) - nbits = BYTES_PER_DIGIT[digits] * 8 - return '' unless nbits.positive? - - value = (BinData.const_get('Int%dbe' % nbits).read(stream) ^ mask) - # Preserve leading zeros. - ('%0' + digits.to_s + 'd') % value - end + def self.ceil_to(value, multiple) + ((value + (multiple - 1)) / multiple) * multiple end - # Fixed-length character type. - class CharacterType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = modifiers.fetch(0, 1) - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def value(data) - # The SQL standard defines that CHAR fields should have end-spaces - # stripped off. - data.sub(/[ ]+$/, '') + module HasNumericModifiers + def coerce_modifiers(modifiers) + modifiers = modifiers&.split(",") if modifiers.is_a?(String) + modifiers&.map(&:to_i) end end - class VariableCharacterType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = modifiers[0] - raise 'Invalid width specification' unless modifiers.size == 1 - - @name = Innodb::DataType.make_name(base_type, modifiers, properties) + module HasStringListModifiers + def coerce_modifiers(modifiers) + CSV.parse_line(modifiers, quote_char: "'")&.map(&:to_s) end - def value(data) - # The SQL standard defines that VARCHAR fields should have end-spaces - # stripped off. - data.sub(/[ ]+$/, '') + def formatted_modifiers + CSV.generate_line(modifiers, quote_char: "'", force_quotes: true, row_sep: "") end end - # Fixed-length binary type. - class BinaryType - attr_reader :name, :width + attr_reader :type_name + attr_reader :modifiers + attr_reader :properties - def initialize(base_type, modifiers, properties) - @width = modifiers.fetch(0, 1) - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end + def initialize(type_name, modifiers = nil, properties = nil) + @type_name = type_name + @modifiers = Array(coerce_modifiers(modifiers)) + @properties = Array(properties) end - class VariableBinaryType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = modifiers[0] - raise 'Invalid width specification' unless modifiers.size == 1 - - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end + def variable? + false end - class BlobType - attr_reader :name - - def initialize(base_type, modifiers, properties) - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end + def blob? + false end - class YearType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 1 - @display_width = modifiers.fetch(0, 4) - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def value(data) - year = BinData::Uint8.read(data) - return (year % 100).to_s if @display_width != 4 - return (year + 1900).to_s if year != 0 - - '0000' - end + def value(data) + data end - class TimeType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 3 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def value(data) - time = BinData::Int24be.read(data) ^ (-1 << 23) - sign = '-' if time.negative? - time = time.abs - '%s%02d:%02d:%02d' % [sign, time / 10_000, (time / 100) % 100, time % 100] - end + def coerce_modifiers(modifiers) + modifiers end - class DateType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 3 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def value(data) - date = BinData::Int24be.read(data) ^ (-1 << 23) - day = date & 0x1f - month = (date >> 5) & 0xf - year = date >> 9 - '%04d-%02d-%02d' % [year, month, day] - end + def formatted_modifiers + modifiers.join(",") end - class DatetimeType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 8 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def value(data) - datetime = BinData::Int64be.read(data) ^ (-1 << 63) - date = datetime / 1_000_000 - year = date / 10_000 - month = (date / 100) % 100 - day = date % 100 - time = datetime - (date * 1_000_000) - hour = time / 10_000 - min = (time / 100) % 100 - sec = time % 100 - '%04d-%02d-%02d %02d:%02d:%02d' % [year, month, day, hour, min, sec] - end + def format_type_name + [ + [ + type_name.to_s, + modifiers&.any? ? "(#{formatted_modifiers})" : nil, + ].compact.join, + *properties&.map { |p| p.to_s.sub("_", " ") }, + ].compact.join(" ") end - class TimestampType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 4 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - # Returns the UTC timestamp as a value in 'YYYY-MM-DD HH:MM:SS' format. - def value(data) - timestamp = BinData::Uint32be.read(data) - return '0000-00-00 00:00:00' if timestamp.zero? - - DateTime.strptime(timestamp.to_s, '%s').strftime '%Y-%m-%d %H:%M:%S' - end + def name + @name ||= format_type_name end - # - # Data types for InnoDB system columns. - # - - # Transaction ID. - class TransactionIdType - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 6 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end - - def read(cursor) - cursor.name('transaction_id') { cursor.read_uint48 } - end + def length + raise NotImplementedError end - # Rollback data pointer. - class RollPointerType - extend ReadBitsAtOffset + # Parse a data type definition and extract the base type and any modifiers. + def self.parse_type_name_and_modifiers(type_string) + matches = /^(?[a-zA-Z0-9_]+)(?:\((?.+)\))?(?\s+unsigned)?$/.match(type_string) + raise "Unparseable type #{type_string}" unless matches - Pointer = Struct.new( - :is_insert, - :rseg_id, - :undo_log, - keyword_init: true - ) - - attr_reader :name, :width - - def initialize(base_type, modifiers, properties) - @width = 7 - @name = Innodb::DataType.make_name(base_type, modifiers, properties) - end + type_name = matches[:type_name].upcase.to_sym + return [type_name, []] unless matches[:modifiers] - def self.parse_roll_pointer(roll_ptr) - Pointer.new( - is_insert: read_bits_at_offset(roll_ptr, 1, 55) == 1, - rseg_id: read_bits_at_offset(roll_ptr, 7, 48), - undo_log: Innodb::Page::Address.new( - page: read_bits_at_offset(roll_ptr, 32, 16), - offset: read_bits_at_offset(roll_ptr, 16, 0) - ) - ) - end - - def value(data) - roll_ptr = BinData::Uint56be.read(data) - self.class.parse_roll_pointer(roll_ptr) - end + # Use the CSV parser since it can understand quotes properly. + [type_name, matches[:modifiers]] end - # Maps base type to data type class. - TYPES = { - BIT: BitType, - BOOL: IntegerType, - BOOLEAN: IntegerType, - TINYINT: IntegerType, - SMALLINT: IntegerType, - MEDIUMINT: IntegerType, - INT: IntegerType, - INT6: IntegerType, - BIGINT: IntegerType, - FLOAT: FloatType, - DOUBLE: DoubleType, - DECIMAL: DecimalType, - NUMERIC: DecimalType, - CHAR: CharacterType, - VARCHAR: VariableCharacterType, - BINARY: BinaryType, - VARBINARY: VariableBinaryType, - TINYBLOB: BlobType, - BLOB: BlobType, - MEDIUMBLOB: BlobType, - LONGBLOB: BlobType, - TINYTEXT: BlobType, - TEXT: BlobType, - MEDIUMTEXT: BlobType, - LONGTEXT: BlobType, - YEAR: YearType, - TIME: TimeType, - DATE: DateType, - DATETIME: DatetimeType, - TIMESTAMP: TimestampType, - TRX_ID: TransactionIdType, - ROLL_PTR: RollPointerType, - }.freeze - - def self.make_name(base_type, modifiers, properties) - name = base_type.to_s.dup - name << '(' + modifiers.join(',') + ')' unless modifiers.empty? - name << ' ' - name << properties.join(' ') - name.strip - end + def self.parse(type_string, properties = nil) + type_name, modifiers = parse_type_name_and_modifiers(type_string.to_s) - def self.new(base_type, modifiers, properties) - raise "Data type '#{base_type}' is not supported" unless TYPES.key?(base_type) + type_class = Innodb::DataType.specialized_classes[type_name] + raise "Unrecognized type #{type_name}" unless type_class - TYPES[base_type].new(base_type, modifiers, properties) + type_class.new(type_name, modifiers, properties) end end end diff --git a/lib/innodb/data_type/bit.rb b/lib/innodb/data_type/bit.rb new file mode 100644 index 00000000..5a47a591 --- /dev/null +++ b/lib/innodb/data_type/bit.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Innodb + class DataType + # MySQL's Bit-Value Type (BIT). + class Bit < DataType + specialization_for :BIT + + include HasNumericModifiers + + DEFAULT_SIZE = 1 + SUPPORTED_SIZE_RANGE = (1..64).freeze + + def initialize(type_name, modifiers, properties) + super + + @size = @modifiers.fetch(0, DEFAULT_SIZE) + raise "Unsupported width for #{@type_name} type" unless SUPPORTED_SIZE_RANGE.include?(@size) + end + + def value(data) + "0b%b" % BinData.const_get("Uint%dbe" % Innodb::DataType.ceil_to(@size, 8)).read(data) + end + + def length + @length = Innodb::DataType.ceil_to(@size, 8) / 8 + end + end + end +end diff --git a/lib/innodb/data_type/blob.rb b/lib/innodb/data_type/blob.rb new file mode 100644 index 00000000..e4bfc94c --- /dev/null +++ b/lib/innodb/data_type/blob.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Blob < DataType + specialization_for :TINYBLOB + specialization_for :BLOB + specialization_for :MEDIUMBLOB + specialization_for :LONGBLOB + specialization_for :TINYTEXT + specialization_for :TEXT + specialization_for :MEDIUMTEXT + specialization_for :LONGTEXT + specialization_for :JSON + specialization_for :GEOMETRY + + include HasNumericModifiers + + def variable? + true + end + + def blob? + true + end + end + end +end diff --git a/lib/innodb/data_type/character.rb b/lib/innodb/data_type/character.rb new file mode 100644 index 00000000..df1521d9 --- /dev/null +++ b/lib/innodb/data_type/character.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Innodb + class DataType + # Fixed-length character type. + class Character < DataType + specialization_for :CHAR + specialization_for :VARCHAR + specialization_for :BINARY + specialization_for :VARBINARY + + include HasNumericModifiers + + VALID_LENGTH_RANGE = (0..65_535).freeze # 1..255 characters, up to 4 bytes each + DEFAULT_LENGTH = 1 + + def initialize(type_name, modifiers, properties) + super + + @variable = false + @binary = false + + if %i[VARCHAR VARBINARY].include?(@type_name) + @variable = true + if @modifiers.empty? + raise InvalidSpecificationError, "Missing length specification for variable-length type #{@type_name}" + elsif @modifiers.size > 1 + raise InvalidSpecificationError, "Invalid length specification for variable-length type #{@type_name}" + end + end + + @binary = true if %i[BINARY VARBINARY].include?(@type_name) + + @length = @modifiers.fetch(0, DEFAULT_LENGTH) + return if VALID_LENGTH_RANGE.include?(@length) + + raise InvalidSpecificationError, "Length #{@length} out of range for #{@type_name}" + end + + def variable? + @variable + end + + def value(data) + # The SQL standard defines that CHAR fields should have end-spaces + # stripped off. + @binary ? data : data.sub(/ +$/, "") + end + + attr_reader :length + end + end +end diff --git a/lib/innodb/data_type/date.rb b/lib/innodb/data_type/date.rb new file mode 100644 index 00000000..2e92a873 --- /dev/null +++ b/lib/innodb/data_type/date.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Date < DataType + specialization_for :DATE + + include HasNumericModifiers + + def value(data) + date = BinData::Int24be.read(data) ^ (-1 << 23) + day = date & 0x1f + month = (date >> 5) & 0xf + year = date >> 9 + "%04d-%02d-%02d" % [year, month, day] + end + + def length + 3 + end + end + end +end diff --git a/lib/innodb/data_type/datetime.rb b/lib/innodb/data_type/datetime.rb new file mode 100644 index 00000000..7b6b2a66 --- /dev/null +++ b/lib/innodb/data_type/datetime.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Datetime < DataType + specialization_for :DATETIME + + include HasNumericModifiers + + def value(data) + datetime = BinData::Int64be.read(data) ^ (-1 << 63) + date = datetime / 1_000_000 + year = date / 10_000 + month = (date / 100) % 100 + day = date % 100 + time = datetime - (date * 1_000_000) + hour = time / 10_000 + min = (time / 100) % 100 + sec = time % 100 + "%04d-%02d-%02d %02d:%02d:%02d" % [year, month, day, hour, min, sec] + end + + def length + 8 + end + end + end +end diff --git a/lib/innodb/data_type/decimal.rb b/lib/innodb/data_type/decimal.rb new file mode 100644 index 00000000..e6ff6699 --- /dev/null +++ b/lib/innodb/data_type/decimal.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require "bigdecimal" +require "stringio" + +module Innodb + class DataType + # MySQL's Fixed-Point Type (DECIMAL), stored in InnoDB as a binary string. + class Decimal < DataType + specialization_for :DECIMAL + specialization_for :NUMERIC + + include HasNumericModifiers + + # The value is stored as a sequence of signed big-endian integers, each + # representing up to 9 digits of the integral and fractional parts. The + # first integer of the integral part and/or the last integer of the + # fractional part might be compressed (or packed) and are of variable + # length. The remaining integers (if any) are uncompressed and 32 bits + # wide. + MAX_DIGITS_PER_INTEGER = 9 + BYTES_PER_DIGIT = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4].freeze + + DEFAULT_PRECISION = 10 + VALID_PRECISION_RANGE = (1..65).freeze + + DEFAULT_SCALE = 0 + VALID_SCALE_RANGE = (0..30).freeze + + def self.length_attributes(precision, scale) + integral = precision - scale + + integral_count_full_parts = integral / MAX_DIGITS_PER_INTEGER + integral_first_part_length = integral - (integral_count_full_parts * MAX_DIGITS_PER_INTEGER) + + fractional_count_full_parts = scale / MAX_DIGITS_PER_INTEGER + fractional_first_part_length = scale - (fractional_count_full_parts * MAX_DIGITS_PER_INTEGER) + + integral_length = (integral_count_full_parts * 4) + BYTES_PER_DIGIT[integral_first_part_length] + fractional_length = (fractional_count_full_parts * 4) + BYTES_PER_DIGIT[fractional_first_part_length] + + { + length: integral_length + fractional_length, + integral: { + length: integral_length, + first_part_length: integral_first_part_length, + count_full_parts: integral_count_full_parts, + }, + fractional: { + length: fractional_length, + first_part_length: fractional_first_part_length, + count_full_parts: fractional_count_full_parts, + }, + } + end + + def initialize(type_name, modifiers, properties) + super + + raise "Invalid #{@type_name} specification: #{@modifiers}" unless @modifiers.size <= 2 + + @precision = @modifiers.fetch(0, DEFAULT_PRECISION) + @scale = @modifiers.fetch(1, DEFAULT_SCALE) + + unless VALID_PRECISION_RANGE.include?(@precision) + raise "Unsupported precision #{@precision} for #{@type_name} type" + end + + unless VALID_SCALE_RANGE.include?(@scale) && @scale <= @precision + raise "Unsupported scale #{@scale} for #{@type_name} type" + end + + @length_attributes = self.class.length_attributes(@precision, @scale) + end + + def length + @length_attributes[:length] + end + + def value(data) + # Strings representing the integral and fractional parts. + intg = "".dup + frac = "".dup + + stream = StringIO.new(data) + mask = sign_mask(stream) + + intg << get_digits(stream, mask, @length_attributes[:integral][:first_part_length]) + + @length_attributes[:integral][:count_full_parts].times do + intg << get_digits(stream, mask, MAX_DIGITS_PER_INTEGER) + end + + @length_attributes[:fractional][:count_full_parts].times do + frac << get_digits(stream, mask, MAX_DIGITS_PER_INTEGER) + end + + frac << get_digits(stream, mask, @length_attributes[:fractional][:first_part_length]) + frac = "0" if frac.empty? + + # Convert to something resembling a string representation. + str = "#{mask.to_s.chop}#{intg}.#{frac}" + + BigDecimal(str).to_s("F") + end + + private + + # The sign is encoded in the high bit of the first byte/digit. The byte + # might be part of a larger integer, so apply the bit-flipper and push + # back the byte into the stream. + def sign_mask(stream) + byte = BinData::Uint8.read(stream) + sign = byte & 0x80 + byte.assign(byte ^ 0x80) + stream.rewind + byte.write(stream) + stream.rewind + sign.zero? ? -1 : 0 + end + + # Return a string representing an integer with a specific number of digits. + def get_digits(stream, mask, digits) + nbits = BYTES_PER_DIGIT[digits] * 8 + return "" unless nbits.positive? + + value = (BinData.const_get("Int%dbe" % nbits).read(stream) ^ mask) + # Preserve leading zeros. + "%0#{digits}d" % value + end + end + end +end diff --git a/lib/innodb/data_type/enum.rb b/lib/innodb/data_type/enum.rb new file mode 100644 index 00000000..1ba497e1 --- /dev/null +++ b/lib/innodb/data_type/enum.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Enum < DataType + specialization_for :ENUM + + include HasStringListModifiers + + attr_reader :values + + def initialize(type_name, modifiers, properties) + super + + @values = { 0 => "" } + @values.merge!(@modifiers.each_with_index.to_h { |s, i| [i + 1, s] }) + end + + def bit_length + @bit_length ||= Innodb::DataType.ceil_to(Math.log2(@values.length).ceil, 8) + end + + def value(data) + index = BinData.const_get("Int%dbe" % bit_length).read(data) + values[index] + end + + def length + bit_length / 8 + end + end + end +end diff --git a/lib/innodb/data_type/floating_point.rb b/lib/innodb/data_type/floating_point.rb new file mode 100644 index 00000000..9e7cedd9 --- /dev/null +++ b/lib/innodb/data_type/floating_point.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class FloatingPoint < DataType + specialization_for :FLOAT + specialization_for :DOUBLE + + include HasNumericModifiers + + def value(data) + case type_name + when :FLOAT + BinData::FloatLe.read(data) + when :DOUBLE + BinData::DoubleLe.read(data) + end + end + + def length + case type_name + when :FLOAT + 4 + when :DOUBLE + 8 + end + end + end + end +end diff --git a/lib/innodb/data_type/innodb_roll_pointer.rb b/lib/innodb/data_type/innodb_roll_pointer.rb new file mode 100644 index 00000000..9208d075 --- /dev/null +++ b/lib/innodb/data_type/innodb_roll_pointer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Innodb + class DataType + # Rollback data pointer. + class InnodbRollPointer < DataType + specialization_for :ROLL_PTR + + extend ReadBitsAtOffset + + Pointer = Struct.new( + :is_insert, + :rseg_id, + :undo_log, + keyword_init: true + ) + + def self.parse_roll_pointer(roll_ptr) + Pointer.new( + is_insert: read_bits_at_offset(roll_ptr, 1, 55) == 1, + rseg_id: read_bits_at_offset(roll_ptr, 7, 48), + undo_log: Innodb::Page::Address.new( + page: read_bits_at_offset(roll_ptr, 32, 16), + offset: read_bits_at_offset(roll_ptr, 16, 0) + ) + ) + end + + def value(data) + roll_ptr = BinData::Uint56be.read(data) + self.class.parse_roll_pointer(roll_ptr) + end + + def length + 7 + end + end + end +end diff --git a/lib/innodb/data_type/innodb_transaction_id.rb b/lib/innodb/data_type/innodb_transaction_id.rb new file mode 100644 index 00000000..3559e783 --- /dev/null +++ b/lib/innodb/data_type/innodb_transaction_id.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Innodb + class DataType + # Transaction ID. + class InnodbTransactionId < DataType + specialization_for :TRX_ID + + def value(data) + BinData::Uint48be.read(data).to_i + end + + def length + 6 + end + end + end +end diff --git a/lib/innodb/data_type/integer.rb b/lib/innodb/data_type/integer.rb new file mode 100644 index 00000000..45c4d533 --- /dev/null +++ b/lib/innodb/data_type/integer.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Integer < DataType + specialization_for :BOOL + specialization_for :BOOLEAN + specialization_for :TINYINT + specialization_for :SMALLINT + specialization_for :MEDIUMINT + specialization_for :INT + specialization_for :INT6 + specialization_for :BIGINT + + include HasNumericModifiers + + TYPE_BIT_LENGTH_MAP = { + BOOL: 8, + BOOLEAN: 8, + TINYINT: 8, + SMALLINT: 16, + MEDIUMINT: 24, + INT: 32, + INT6: 48, + BIGINT: 64, + }.freeze + + def initialize(type_name, modifiers, properties) + super + + @unsigned = properties&.include?(:UNSIGNED) + end + + def bit_length + @bit_length ||= TYPE_BIT_LENGTH_MAP[type_name] + end + + def unsigned? + @unsigned + end + + def value(data) + if unsigned? + BinData.const_get("Uint%dbe" % bit_length).read(data) + else + BinData.const_get("Int%dbe" % bit_length).read(data) ^ (-1 << (bit_length - 1)) + end + end + + def length + bit_length / 8 + end + end + end +end diff --git a/lib/innodb/data_type/set.rb b/lib/innodb/data_type/set.rb new file mode 100644 index 00000000..addeb3f0 --- /dev/null +++ b/lib/innodb/data_type/set.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Set < DataType + specialization_for :SET + + include HasStringListModifiers + + attr_reader :values + + def initialize(type_name, modifiers, properties) + super + + @values = @modifiers.each_with_index.to_h { |s, i| [2**i, s] } + end + + def bit_length + @bit_length ||= Innodb::DataType.ceil_to(@values.length, 8) + end + + def value(data) + bitmap = BinData.const_get("Int%dbe" % bit_length).read(data) + (0...bit_length).map { |i| bitmap & (2**i) }.reject(&:zero?).map { |i| values[i] } + end + + def length + bit_length / 8 + end + end + end +end diff --git a/lib/innodb/data_type/time.rb b/lib/innodb/data_type/time.rb new file mode 100644 index 00000000..339a26e6 --- /dev/null +++ b/lib/innodb/data_type/time.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Time < DataType + specialization_for :TIME + + include HasNumericModifiers + + def value(data) + time = BinData::Int24be.read(data) ^ (-1 << 23) + sign = "-" if time.negative? + time = time.abs + "%s%02d:%02d:%02d" % [sign, time / 10_000, (time / 100) % 100, time % 100] + end + + def length + 3 + end + end + end +end diff --git a/lib/innodb/data_type/timestamp.rb b/lib/innodb/data_type/timestamp.rb new file mode 100644 index 00000000..cbcd8458 --- /dev/null +++ b/lib/innodb/data_type/timestamp.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "date" + +module Innodb + class DataType + class Timestamp < DataType + specialization_for :TIMESTAMP + + include HasNumericModifiers + + # Returns the UTC timestamp as a value in 'YYYY-MM-DD HH:MM:SS' format. + def value(data) + timestamp = BinData::Uint32be.read(data) + return "0000-00-00 00:00:00" if timestamp.zero? + + DateTime.strptime(timestamp.to_s, "%s").strftime "%Y-%m-%d %H:%M:%S" + end + + def length + 4 + end + end + end +end diff --git a/lib/innodb/data_type/year.rb b/lib/innodb/data_type/year.rb new file mode 100644 index 00000000..c7ad95d6 --- /dev/null +++ b/lib/innodb/data_type/year.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Innodb + class DataType + class Year < DataType + specialization_for :YEAR + + include HasNumericModifiers + + DEFAULT_DISPLAY_WIDTH = 4 + VALID_DISPLAY_WIDTHS = [2, 4].freeze + + def initialize(type_name, modifiers, properties) + super + + @display_width = modifiers.fetch(0, DEFAULT_DISPLAY_WIDTH) + return if VALID_DISPLAY_WIDTHS.include?(@display_width) + + raise InvalidSpecificationError, "Unsupported display width #{@display_width} for type #{type_name}" + end + + def value(data) + year = BinData::Uint8.read(data) + return (year % 100).to_s if @display_width != 4 + return (year + 1900).to_s if year != 0 + + "0000" + end + + def length + 1 + end + end + end +end diff --git a/lib/innodb/field.rb b/lib/innodb/field.rb index 8603f0c5..6771786c 100644 --- a/lib/innodb/field.rb +++ b/lib/innodb/field.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'innodb/data_type' +require "innodb/data_type" # A single field in an InnoDB record (within an INDEX page). This class # provides essential information to parse records, including the length @@ -11,7 +11,7 @@ class Field :space_id, :page_number, :offset, - :length, + :length, # rubocop:disable Lint/StructNewOverride keyword_init: true ) @@ -27,8 +27,7 @@ def initialize(position, name, type_definition, *properties) @position = position @name = name @nullable = !properties.delete(:NOT_NULL) - base_type, modifiers = parse_type_definition(type_definition.to_s) - @data_type = Innodb::DataType.new(base_type, modifiers, properties) + @data_type = Innodb::DataType.parse(type_definition, properties) end # Return whether this field can be NULL. @@ -47,24 +46,26 @@ def extern?(record) end def variable? - [ - Innodb::DataType::BlobType, - Innodb::DataType::VariableBinaryType, - Innodb::DataType::VariableCharacterType, - ].any? { |c| @data_type.is_a?(c) } + @data_type.variable? + end + + def fixed? + !variable? end def blob? - @data_type.is_a?(Innodb::DataType::BlobType) + @data_type.blob? end # Return the actual length of this variable-length field. def length(record) if record.header.lengths.include?(@name) len = record.header.lengths[@name] - raise 'Fixed-length mismatch' unless variable? || len == @data_type.width + if fixed? && len != @data_type.length + raise "Fixed-length mismatch; #{len} vs #{@data_type.length} for #{@data_type.name}" + end else - len = @data_type.width + len = @data_type.length end extern?(record) ? len - EXTERN_FIELD_SIZE : len end @@ -75,13 +76,7 @@ def read(cursor, field_length) end def value_by_length(cursor, field_length) - if @data_type.respond_to?(:read) - cursor.name(@data_type.name) { @data_type.read(cursor) } - elsif @data_type.respond_to?(:value) - @data_type.value(read(cursor, field_length)) - else - read(cursor, field_length) - end + @data_type.value(read(cursor, field_length)) end # Read the data value (e.g. encoded in the data). @@ -103,25 +98,14 @@ def extern(cursor, record) # Return an external reference field. An extern field contains the page # address and the length of the externally stored part of the record data. def read_extern(cursor) - cursor.name('extern') do |c| + cursor.name("extern") do |c| ExternReference.new( - space_id: c.name('space_id') { c.read_uint32 }, - page_number: c.name('page_number') { c.read_uint32 }, - offset: c.name('offset') { c.read_uint32 }, - length: c.name('length') { c.read_uint64 & 0x3fffffff } + space_id: c.name("space_id") { c.read_uint32 }, + page_number: c.name("page_number") { c.read_uint32 }, + offset: c.name("offset") { c.read_uint32 }, + length: c.name("length") { c.read_uint64 & 0x3fffffff } ) end end - - # Parse a data type definition and extract the base type and any modifiers. - def parse_type_definition(type_string) - matches = /^([a-zA-Z0-9_]+)(\(([0-9, ]+)\))?$/.match(type_string) - return unless matches - - base_type = matches[1].upcase.to_sym - return [base_type, []] unless matches[3] - - [base_type, matches[3].sub(/[ ]/, '').split(/,/).map(&:to_i)] - end end end diff --git a/lib/innodb/fseg_entry.rb b/lib/innodb/fseg_entry.rb index 0da76e64..3285fc48 100644 --- a/lib/innodb/fseg_entry.rb +++ b/lib/innodb/fseg_entry.rb @@ -12,15 +12,15 @@ class FsegEntry # Return the FSEG entry address, which points to an entry on an INODE page. def self.get_entry_address(cursor) { - space_id: cursor.name('space_id') { cursor.read_uint32 }, - page_number: cursor.name('page_number') { Innodb::Page.maybe_undefined(cursor.read_uint32) }, - offset: cursor.name('offset') { cursor.read_uint16 }, + space_id: cursor.name("space_id") { cursor.read_uint32 }, + page_number: cursor.name("page_number") { Innodb::Page.maybe_undefined(cursor.read_uint32) }, + offset: cursor.name("offset") { cursor.read_uint16 }, } end # Return an INODE entry which represents this file segment. def self.get_inode(space, cursor) - address = cursor.name('address') { get_entry_address(cursor) } + address = cursor.name("address") { get_entry_address(cursor) } return nil if address[:offset].zero? page = space.page(address[:page_number]) diff --git a/lib/innodb/history_list.rb b/lib/innodb/history_list.rb index cf00800b..49295a2c 100644 --- a/lib/innodb/history_list.rb +++ b/lib/innodb/history_list.rb @@ -29,7 +29,7 @@ def initialize(history, undo_record, direction = :forward) @undo_record_cursor = @undo_log.undo_record_cursor(:max, direction) end else - raise 'Not implemented' + raise "Not implemented" end # rubocop:enable Style/IfUnlessModifier end @@ -93,12 +93,10 @@ def undo_record_cursor(undo_record = :min, direction = :forward) UndoRecordCursor.new(self, undo_record, direction) end - def each_undo_record + def each_undo_record(&block) return enum_for(:each_undo_record) unless block_given? - undo_record_cursor.each_undo_record do |rec| - yield rec - end + undo_record_cursor.each_undo_record(&block) end end end diff --git a/lib/innodb/ibuf_bitmap.rb b/lib/innodb/ibuf_bitmap.rb index 38e68ac6..3282de88 100644 --- a/lib/innodb/ibuf_bitmap.rb +++ b/lib/innodb/ibuf_bitmap.rb @@ -25,7 +25,7 @@ def size_bitmap end def read_bitmap(cursor) - cursor.name('ibuf_bitmap') { |c| c.read_bytes(size_bitmap) } + cursor.name("ibuf_bitmap") { |c| c.read_bytes(size_bitmap) } end def each_page_status diff --git a/lib/innodb/index.rb b/lib/innodb/index.rb index cdd22992..ffc5674f 100644 --- a/lib/innodb/index.rb +++ b/lib/innodb/index.rb @@ -21,7 +21,9 @@ def initialize(space, root_page_number, record_describer = nil) raise "Page #{root_page_number} couldn't be read" unless @root # The root page should be an index page. - raise "Page #{root_page_number} is a #{@root.type} page, not an INDEX page" unless @root.type == :INDEX + unless @root.is_a?(Innodb::Page::Index) + raise "Page #{root_page_number} is a #{@root.type} page, not an INDEX page" + end # The root page should be the only page at its level. raise "Page #{root_page_number} does not appear to be an index root" if @root.prev || @root.next @@ -53,12 +55,12 @@ def node_type(page) # Internal method used by recurse. def _recurse(parent_page, page_proc, link_proc, depth = 0) - page_proc.call(parent_page, depth) if page_proc && parent_page.type == :INDEX + page_proc.call(parent_page, depth) if page_proc && parent_page.is_a?(Innodb::Page::Index) parent_page.each_child_page do |child_page_number, child_min_key| child_page = page(child_page_number) child_page.record_describer = record_describer - next unless child_page.type == :INDEX + next unless child_page.is_a?(Innodb::Page::Index) link_proc&.call(parent_page, child_page, child_min_key, depth + 1) _recurse(child_page, page_proc, link_proc, depth + 1) @@ -124,12 +126,10 @@ def each_fseg end # Iterate through all lists in a given fseg. - def each_fseg_list(fseg) + def each_fseg_list(fseg, &block) return enum_for(:each_fseg_list, fseg) unless block_given? - fseg.each_list do |list_name, list| - yield list_name, list - end + fseg.each_list(&block) end # Iterate through all frag pages in a given fseg. @@ -145,7 +145,7 @@ def each_fseg_frag_page(fseg) def each_page_from(page) return enum_for(:each_page_from, page) unless block_given? - while page && page.type == :INDEX + while page.is_a?(Innodb::Page::Index) yield page break unless page.next @@ -155,20 +155,18 @@ def each_page_from(page) # Iterate through all pages at the given level by finding the first page # and following the next pointers in each page. - def each_page_at_level(level) + def each_page_at_level(level, &block) return enum_for(:each_page_at_level, level) unless block_given? - each_page_from(min_page_at_level(level)) { |page| yield page } + each_page_from(min_page_at_level(level), &block) end # Iterate through all records on all leaf pages in ascending order. - def each_record + def each_record(&block) return enum_for(:each_record) unless block_given? each_page_at_level(0) do |page| - page.each_record do |record| - yield record - end + page.each_record(&block) end end @@ -183,10 +181,10 @@ def linear_search(key) page = @root if Innodb.debug? - puts 'linear_search: root=%i, level=%i, key=(%s)' % [ + puts "linear_search: root=%i, level=%i, key=(%s)" % [ page.offset, page.level, - key.join(', '), + key.join(", "), ] end @@ -215,10 +213,10 @@ def binary_search(key) page = @root if Innodb.debug? - puts 'binary_search: root=%i, level=%i, key=(%s)' % [ + puts "binary_search: root=%i, level=%i, key=(%s)" % [ page.offset, page.level, - key.join(', '), + key.join(", "), ] end @@ -291,8 +289,8 @@ def each_record # Move the cursor to a new starting position in a given page. def move_cursor(page, record) - raise 'Failed to load page' unless (@page = @index.page(page)) - raise 'Failed to position cursor' unless (@page_cursor = @page.record_cursor(record, @direction)) + raise "Failed to load page" unless (@page = @index.page(page)) + raise "Failed to position cursor" unless (@page_cursor = @page.record_cursor(record, @direction)) end # Move to the next record in the forward direction and return it. diff --git a/lib/innodb/inode.rb b/lib/innodb/inode.rb index c263135f..fb9567e0 100644 --- a/lib/innodb/inode.rb +++ b/lib/innodb/inode.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" module Innodb class Inode @@ -55,13 +55,13 @@ def self.new_from_cursor(space, cursor) space, Header.new( offset: cursor.position, - fseg_id: cursor.name('fseg_id') { cursor.read_uint64 }, - not_full_n_used: cursor.name('not_full_n_used') { cursor.read_uint32 }, - free: cursor.name('list[free]') { Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor)) }, - not_full: cursor.name('list[not_full]') { Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor)) }, - full: cursor.name('list[full]') { Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor)) }, - magic_n: cursor.name('magic_n') { cursor.read_uint32 }, - frag_array: cursor.name('frag_array') { page_number_array(FRAG_ARRAY_N_SLOTS, cursor) } + fseg_id: cursor.name("fseg_id") { cursor.read_uint64 }, + not_full_n_used: cursor.name("not_full_n_used") { cursor.read_uint32 }, + free: cursor.name("list[free]") { Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor)) }, + not_full: cursor.name("list[not_full]") { Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor)) }, + full: cursor.name("list[full]") { Innodb::List::Xdes.new(space, Innodb::List.get_base_node(cursor)) }, + magic_n: cursor.name("magic_n") { cursor.read_uint32 }, + frag_array: cursor.name("frag_array") { page_number_array(FRAG_ARRAY_N_SLOTS, cursor) } ) ) end @@ -84,7 +84,7 @@ def initialize(space, header) def_delegator :header, :frag_array def inspect - '<%s space=%s, fseg=%i>' % [ + "<%s space=%s, fseg=%i>" % [ self.class.name, space.inspect, fseg_id, @@ -99,7 +99,7 @@ def allocated? # Helper method to return an array of only non-nil fragment pages. def frag_array_pages - frag_array.reject(&:nil?) + frag_array.compact end # Helper method to count non-nil fragment pages. @@ -147,18 +147,14 @@ def each_list # Iterate through the fragment array followed by all lists, yielding the # page number. This allows a convenient way to identify all pages that are # part of this inode. - def each_page_number + def each_page_number(&block) return enum_for(:each_page_number) unless block_given? - frag_array_pages.each do |page_number| - yield page_number - end + frag_array_pages.each(&block) each_list do |_fseg_name, fseg_list| fseg_list.each do |xdes| - xdes.each_page_status do |page_number| - yield page_number - end + xdes.each_page_status(&block) end end diff --git a/lib/innodb/list.rb b/lib/innodb/list.rb index 942def40..8ef3b4f7 100644 --- a/lib/innodb/list.rb +++ b/lib/innodb/list.rb @@ -6,8 +6,18 @@ module Innodb class List - BaseNode = Struct.new(:length, :first, :last, keyword_init: true) - Node = Struct.new(:prev, :next, keyword_init: true) + BaseNode = Struct.new( + :length, # rubocop:disable Lint/StructNewOverride + :first, # rubocop:disable Lint/StructNewOverride + :last, + keyword_init: true + ) + + Node = Struct.new( + :prev, + :next, + keyword_init: true + ) # An "address", which consists of a page number and byte offset within the # page. This points to the list "node" pointers (prev and next) of the @@ -18,8 +28,8 @@ class List # or "NULL" pointer (the page number is UINT32_MAX), or the address if # valid. def self.get_address(cursor) - page = cursor.name('page') { Innodb::Page.maybe_undefined(cursor.read_uint32) } - offset = cursor.name('offset') { cursor.read_uint16 } + page = cursor.name("page") { Innodb::Page.maybe_undefined(cursor.read_uint32) } + offset = cursor.name("offset") { cursor.read_uint16 } Innodb::Page::Address.new(page: page, offset: offset) if page end @@ -33,8 +43,8 @@ def self.get_address(cursor) # linked list. def self.get_node(cursor) Node.new( - prev: cursor.name('prev') { get_address(cursor) }, - next: cursor.name('next') { get_address(cursor) } + prev: cursor.name("prev") { get_address(cursor) }, + next: cursor.name("next") { get_address(cursor) } ) end @@ -49,9 +59,9 @@ def self.get_node(cursor) # address. def self.get_base_node(cursor) BaseNode.new( - length: cursor.name('length') { cursor.read_uint32 }, - first: cursor.name('first') { get_address(cursor) }, - last: cursor.name('last') { get_address(cursor) } + length: cursor.name("length") { cursor.read_uint32 }, + first: cursor.name("first") { get_address(cursor) }, + last: cursor.name("last") { get_address(cursor) } ) end @@ -90,6 +100,11 @@ def length @base.length end + # Is the list currently empty? + def empty? + length.zero? + end + # Return the first object in the list using the list base node "first" # address pointer. def first @@ -116,12 +131,10 @@ def include?(item) end # Iterate through all nodes in the list. - def each + def each(&block) return enum_for(:each) unless block_given? - list_cursor.each_node do |node| - yield node - end + list_cursor.each_node(&block) end # A list iteration cursor used primarily by the Innodb::List #cursor method diff --git a/lib/innodb/log.rb b/lib/innodb/log.rb index 225d66e8..cbf50052 100644 --- a/lib/innodb/log.rb +++ b/lib/innodb/log.rb @@ -73,7 +73,7 @@ def initialize(filename) # Get the raw byte buffer for a specific block by block offset. def block_data(offset) - raise 'Invalid block offset' unless (offset % Innodb::LogBlock::BLOCK_SIZE).zero? + raise "Invalid block offset" unless (offset % Innodb::LogBlock::BLOCK_SIZE).zero? @file.sysseek(offset) @file.sysread(Innodb::LogBlock::BLOCK_SIZE) @@ -87,12 +87,12 @@ def block_cursor(offset) # Return the log header. def header offset = LOG_HEADER_BLOCK_MAP[:LOG_FILE_HEADER] * Innodb::LogBlock::BLOCK_SIZE - @header ||= block_cursor(offset).name('header') do |c| + @header ||= block_cursor(offset).name("header") do |c| Header.new( - group_id: c.name('group_id') { c.read_uint32 }, - start_lsn: c.name('start_lsn') { c.read_uint64 }, - file_no: c.name('file_no') { c.read_uint32 }, - created_by: c.name('created_by') { c.read_string(32) } + group_id: c.name("group_id") { c.read_uint32 }, + start_lsn: c.name("start_lsn") { c.read_uint64 }, + file_no: c.name("file_no") { c.read_uint32 }, + created_by: c.name("created_by") { c.read_string(32) } ) end end @@ -103,24 +103,24 @@ def read_checkpoint(cursor) # use or even read by InnoDB. However, for the sake of completeness, # they are included. Checkpoint.new( - number: cursor.name('number') { cursor.read_uint64 }, - lsn: cursor.name('lsn') { cursor.read_uint64 }, - lsn_offset: cursor.name('lsn_offset') { cursor.read_uint32 }, - buffer_size: cursor.name('buffer_size') { cursor.read_uint32 }, - archived_lsn: cursor.name('archived_lsn') { cursor.read_uint64 }, + number: cursor.name("number") { cursor.read_uint64 }, + lsn: cursor.name("lsn") { cursor.read_uint64 }, + lsn_offset: cursor.name("lsn_offset") { cursor.read_uint32 }, + buffer_size: cursor.name("buffer_size") { cursor.read_uint32 }, + archived_lsn: cursor.name("archived_lsn") { cursor.read_uint64 }, group_array: (0..(LOG_CHECKPOINT_GROUPS - 1)).map do |n| cursor.name("group_array[#{n}]") do CheckpointGroup.new( - archived_file_no: cursor.name('archived_file_no') { cursor.read_uint32 }, - archived_offset: cursor.name('archived_offset') { cursor.read_uint32 } + archived_file_no: cursor.name("archived_file_no") { cursor.read_uint32 }, + archived_offset: cursor.name("archived_offset") { cursor.read_uint32 } ) end end, - checksum_1: cursor.name('checksum_1') { cursor.read_uint32 }, - checksum_2: cursor.name('checksum_2') { cursor.read_uint32 }, - fsp_free_limit: cursor.name('fsp_free_limit') { cursor.read_uint32 }, - fsp_magic: cursor.name('fsp_magic') { cursor.read_uint32 } + checksum_1: cursor.name("checksum_1") { cursor.read_uint32 }, + checksum_2: cursor.name("checksum_2") { cursor.read_uint32 }, + fsp_free_limit: cursor.name("fsp_free_limit") { cursor.read_uint32 }, + fsp_magic: cursor.name("fsp_magic") { cursor.read_uint32 } ) end @@ -129,8 +129,8 @@ def checkpoint offset1 = LOG_HEADER_BLOCK_MAP[:LOG_CHECKPOINT_1] * Innodb::LogBlock::BLOCK_SIZE offset2 = LOG_HEADER_BLOCK_MAP[:LOG_CHECKPOINT_2] * Innodb::LogBlock::BLOCK_SIZE @checkpoint ||= CheckpointSet.new( - checkpoint_1: block_cursor(offset1).name('checkpoint_1') { |c| read_checkpoint(c) }, - checkpoint_2: block_cursor(offset2).name('checkpoint_2') { |c| read_checkpoint(c) } + checkpoint_1: block_cursor(offset1).name("checkpoint_1") { |c| read_checkpoint(c) }, + checkpoint_2: block_cursor(offset2).name("checkpoint_2") { |c| read_checkpoint(c) } ) end diff --git a/lib/innodb/log_block.rb b/lib/innodb/log_block.rb index 7ee1ca1d..38c58684 100644 --- a/lib/innodb/log_block.rb +++ b/lib/innodb/log_block.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" # An InnoDB transaction log block. module Innodb @@ -60,13 +60,13 @@ def cursor(offset) # Return the log block header. def header - @header ||= cursor(HEADER_OFFSET).name('header') do |c| + @header ||= cursor(HEADER_OFFSET).name("header") do |c| Header.new( - flush: c.name('flush') { c.peek { (c.read_uint32 & HEADER_FLUSH_BIT_MASK).positive? } }, - block_number: c.name('block_number') { c.read_uint32 & ~HEADER_FLUSH_BIT_MASK }, - data_length: c.name('data_length') { c.read_uint16 }, - first_rec_group: c.name('first_rec_group') { c.read_uint16 }, - checkpoint_no: c.name('checkpoint_no') { c.read_uint32 } + flush: c.name("flush") { c.peek { (c.read_uint32 & HEADER_FLUSH_BIT_MASK).positive? } }, + block_number: c.name("block_number") { c.read_uint32 & ~HEADER_FLUSH_BIT_MASK }, + data_length: c.name("data_length") { c.read_uint16 }, + first_rec_group: c.name("first_rec_group") { c.read_uint16 }, + checkpoint_no: c.name("checkpoint_no") { c.read_uint32 } ) end end @@ -83,15 +83,15 @@ def data(offset = DATA_OFFSET) length = data_length length -= TRAILER_SIZE if length == BLOCK_SIZE - raise 'Invalid block data offset' if offset < DATA_OFFSET || offset > length + raise "Invalid block data offset" if offset < DATA_OFFSET || offset > length @buffer.slice(offset, length - offset) end # Return the log block trailer. def trailer - @trailer ||= cursor(TRAILER_OFFSET).name('trailer') do |c| - Trailer.new(checksum: c.name('checksum') { c.read_uint32 }) + @trailer ||= cursor(TRAILER_OFFSET).name("trailer") do |c| + Trailer.new(checksum: c.name("checksum") { c.read_uint32 }) end end @@ -118,11 +118,11 @@ def corrupt? # Dump the contents of a log block for debugging purposes. def dump puts - puts 'header:' + puts "header:" pp header puts - puts 'trailer:' + puts "trailer:" pp trailer end end diff --git a/lib/innodb/log_group.rb b/lib/innodb/log_group.rb index 341482c5..3b46ced1 100644 --- a/lib/innodb/log_group.rb +++ b/lib/innodb/log_group.rb @@ -6,26 +6,22 @@ class LogGroup # Initialize group given a set of sorted log files. def initialize(log_files) @logs = log_files.map { |fn| Innodb::Log.new(fn) } - raise 'Log file sizes do not match' unless @logs.map(&:size).uniq.size == 1 + raise "Log file sizes do not match" unless @logs.map(&:size).uniq.size == 1 end # Iterate through all logs. - def each_log + def each_log(&block) return enum_for(:each_log) unless block_given? - @logs.each do |log| - yield log - end + @logs.each(&block) end # Iterate through all blocks. - def each_block + def each_block(&block) return enum_for(:each_block) unless block_given? each_log do |log| - log.each_block do |block_index, block| - yield block_index, block - end + log.each_block(&block) end end diff --git a/lib/innodb/log_reader.rb b/lib/innodb/log_reader.rb index 784e51f3..ce746537 100644 --- a/lib/innodb/log_reader.rb +++ b/lib/innodb/log_reader.rb @@ -110,8 +110,8 @@ def preload(size) break if data_length < Innodb::LogBlock::BLOCK_SIZE end - raise ChecksumError, 'Block is corrupted' if corrupt - raise EOFError, 'End of log reached' if buffer.size < size + raise ChecksumError, "Block is corrupted" if corrupt + raise EOFError, "End of log reached" if buffer.size < size end end end diff --git a/lib/innodb/log_record.rb b/lib/innodb/log_record.rb index a5da516b..6e3ee320 100644 --- a/lib/innodb/log_record.rb +++ b/lib/innodb/log_record.rb @@ -14,7 +14,7 @@ class LogRecord IndexFieldInfo = Struct.new( :mtype, :prtype, - :length, + :length, # rubocop:disable Lint/StructNewOverride keyword_init: true ) @@ -109,7 +109,7 @@ def dump # Return a preamble of the first record in this block. def read_preamble(cursor) - type_and_flag = cursor.name('type') { cursor.read_uint8 } + type_and_flag = cursor.name("type") { cursor.read_uint8 } type = type_and_flag & RECORD_TYPE_MASK type = RECORD_TYPES[type] || type # Whether this is a single record for a single page. @@ -121,8 +121,8 @@ def read_preamble(cursor) Preamble.new( type: type, single_record: single_record, - space: cursor.name('space') { cursor.read_ic_uint32 }, - page_number: cursor.name('page_number') { cursor.read_ic_uint32 } + space: cursor.name("space") { cursor.read_ic_uint32 }, + page_number: cursor.name("page_number") { cursor.read_ic_uint32 } ) end end @@ -130,13 +130,13 @@ def read_preamble(cursor) # Read the index part of a log record for a compact record insert. # Ref. mlog_parse_index def read_index(cursor) - n_cols = cursor.name('n_cols') { cursor.read_uint16 } - n_uniq = cursor.name('n_uniq') { cursor.read_uint16 } + n_cols = cursor.name("n_cols") { cursor.read_uint16 } + n_uniq = cursor.name("n_uniq") { cursor.read_uint16 } cols = n_cols.times.collect do - info = cursor.name('field_info') { cursor.read_uint16 } + info = cursor.name("field_info") { cursor.read_uint16 } IndexFieldInfo.new( mtype: ((info + 1) & 0x7fff) <= 1 ? :BINARY : :FIXBINARY, - prtype: (info & 0x8000) != 0 ? :NOT_NULL : nil, + prtype: (info & 0x8000).zero? ? nil : :NOT_NULL, length: info & 0x7fff ) end @@ -150,8 +150,8 @@ def read_index(cursor) # Read the insert record into page part of a insert log. # Ref. page_cur_parse_insert_rec def read_insert_record(cursor) - page_offset = cursor.name('page_offset') { cursor.read_uint16 } - end_seg_len = cursor.name('end_seg_len') { cursor.read_ic_uint32 } + page_offset = cursor.name("page_offset") { cursor.read_uint16 } + end_seg_len = cursor.name("end_seg_len") { cursor.read_ic_uint32 } if (end_seg_len & INFO_AND_STATUS_MASK) != 0 info_and_status_bits = cursor.read_uint8 @@ -165,7 +165,7 @@ def read_insert_record(cursor) info_and_status_bits: info_and_status_bits, origin_offset: origin_offset, mismatch_index: mismatch_index, - record: cursor.name('record') { cursor.read_bytes(end_seg_len >> 1) }, + record: cursor.name("record") { cursor.read_bytes(end_seg_len >> 1) }, } end @@ -173,9 +173,9 @@ def read_insert_record(cursor) # Ref. btr_cur_parse_update_in_place def read_update_in_place_record(cursor) { - flags: cursor.name('flags') { cursor.read_uint8 }, + flags: cursor.name("flags") { cursor.read_uint8 }, sys_fields: read_sys_fields(cursor), - rec_offset: cursor.name('rec_offset') { cursor.read_uint16 }, + rec_offset: cursor.name("rec_offset") { cursor.read_uint16 }, update_index: read_update_index(cursor), } end @@ -185,13 +185,13 @@ def read_update_in_place_record(cursor) # Read the update vector for an update log record. # Ref. row_upd_index_parse def read_update_index(cursor) - info_bits = cursor.name('info_bits') { cursor.read_uint8 } - n_fields = cursor.name('n_fields') { cursor.read_ic_uint32 } + info_bits = cursor.name("info_bits") { cursor.read_uint8 } + n_fields = cursor.name("n_fields") { cursor.read_ic_uint32 } fields = n_fields.times.collect do { - field_no: cursor.name('field_no') { cursor.read_ic_uint32 }, - len: len = cursor.name('len') { cursor.read_ic_uint32 }, - data: cursor.name('data') { len != LENGTH_NULL ? cursor.read_bytes(len) : :NULL }, + field_no: cursor.name("field_no") { cursor.read_ic_uint32 }, + len: len = cursor.name("len") { cursor.read_ic_uint32 }, + data: cursor.name("data") { len == LENGTH_NULL ? :NULL : cursor.read_bytes(len) }, } end { @@ -205,9 +205,9 @@ def read_update_index(cursor) # Ref. row_upd_parse_sys_vals def read_sys_fields(cursor) { - trx_id_pos: cursor.name('trx_id_pos') { cursor.read_ic_uint32 }, - roll_ptr: cursor.name('roll_ptr') { cursor.read_bytes(7) }, - trx_id: cursor.name('trx_id') { cursor.read_ic_uint64 }, + trx_id_pos: cursor.name("trx_id_pos") { cursor.read_ic_uint32 }, + roll_ptr: cursor.name("roll_ptr") { cursor.read_bytes(7) }, + trx_id: cursor.name("trx_id") { cursor.read_ic_uint64 }, } end @@ -216,140 +216,142 @@ def read_sys_fields(cursor) # Ref. btr_cur_parse_del_mark_set_clust_rec def read_clust_delete_mark(cursor) { - flags: cursor.name('flags') { cursor.read_uint8 }, - value: cursor.name('value') { cursor.read_uint8 }, - sys_fields: cursor.name('sys_fields') { read_sys_fields(cursor) }, - offset: cursor.name('offset') { cursor.read_uint16 }, + flags: cursor.name("flags") { cursor.read_uint8 }, + value: cursor.name("value") { cursor.read_uint8 }, + sys_fields: cursor.name("sys_fields") { read_sys_fields(cursor) }, + offset: cursor.name("offset") { cursor.read_uint16 }, } end + # The bodies of the branches here are sometimes duplicates, but logically distinct. + # rubocop:disable Lint/DuplicateBranch def read_payload(type, cursor) case type when :MLOG_1BYTE, :MLOG_2BYTE, :MLOG_4BYTE { - page_offset: cursor.name('page_offset') { cursor.read_uint16 }, - value: cursor.name('value') { cursor.read_ic_uint32 }, + page_offset: cursor.name("page_offset") { cursor.read_uint16 }, + value: cursor.name("value") { cursor.read_ic_uint32 }, } when :MLOG_8BYTE { - offset: cursor.name('offset') { cursor.read_uint16 }, - value: cursor.name('value') { cursor.read_ic_uint64 }, + offset: cursor.name("offset") { cursor.read_uint16 }, + value: cursor.name("value") { cursor.read_ic_uint64 }, } when :UNDO_HDR_CREATE, :UNDO_HDR_REUSE { - trx_id: cursor.name('trx_id') { cursor.read_ic_uint64 }, + trx_id: cursor.name("trx_id") { cursor.read_ic_uint64 }, } when :UNDO_INSERT { - length: length = cursor.name('length') { cursor.read_uint16 }, - value: cursor.name('value') { cursor.read_bytes(length) }, + length: length = cursor.name("length") { cursor.read_uint16 }, + value: cursor.name("value") { cursor.read_bytes(length) }, } when :REC_INSERT { - record: cursor.name('record') { read_insert_record(cursor) }, + record: cursor.name("record") { read_insert_record(cursor) }, } when :COMP_REC_INSERT { - index: cursor.name('index') { read_index(cursor) }, - record: cursor.name('record') { read_insert_record(cursor) }, + index: cursor.name("index") { read_index(cursor) }, + record: cursor.name("record") { read_insert_record(cursor) }, } when :COMP_REC_UPDATE_IN_PLACE { - index: cursor.name('index') { read_index(cursor) }, - record: cursor.name('record') { read_update_in_place_record(cursor) }, + index: cursor.name("index") { read_index(cursor) }, + record: cursor.name("record") { read_update_in_place_record(cursor) }, } when :REC_UPDATE_IN_PLACE { - record: cursor.name('record') { read_update_in_place_record(cursor) }, + record: cursor.name("record") { read_update_in_place_record(cursor) }, } when :WRITE_STRING { - offset: cursor.name('offset') { cursor.read_uint16 }, - length: length = cursor.name('length') { cursor.read_uint16 }, - value: cursor.name('value') { cursor.read_bytes(length) }, + offset: cursor.name("offset") { cursor.read_uint16 }, + length: length = cursor.name("length") { cursor.read_uint16 }, + value: cursor.name("value") { cursor.read_bytes(length) }, } when :UNDO_INIT { - type: cursor.name('type') { UNDO_TYPES[cursor.read_ic_uint32] }, + type: cursor.name("type") { UNDO_TYPES[cursor.read_ic_uint32] }, } when :FILE_CREATE, :FILE_DELETE { - name_len: name_len = cursor.name('name_len') { cursor.read_uint16 }, - name: cursor.name('name') { cursor.read_bytes(name_len) }, + name_len: name_len = cursor.name("name_len") { cursor.read_uint16 }, + name: cursor.name("name") { cursor.read_bytes(name_len) }, } when :FILE_CREATE2 { - flags: cursor.name('flags') { cursor.read_uint32 }, - name_len: name_len = cursor.name('name_len') { cursor.read_uint16 }, - name: cursor.name('name') { cursor.read_bytes(name_len) }, + flags: cursor.name("flags") { cursor.read_uint32 }, + name_len: name_len = cursor.name("name_len") { cursor.read_uint16 }, + name: cursor.name("name") { cursor.read_bytes(name_len) }, } when :FILE_RENAME { old: { - name_len: name_len = cursor.name('name_len') { cursor.read_uint16 }, - name: cursor.name('name') { cursor.read_bytes(name_len) }, + name_len: name_len = cursor.name("name_len") { cursor.read_uint16 }, + name: cursor.name("name") { cursor.read_bytes(name_len) }, }, new: { - name_len: name_len = cursor.name('name_len') { cursor.read_uint16 }, - name: cursor.name('name') { cursor.read_bytes(name_len) }, + name_len: name_len = cursor.name("name_len") { cursor.read_uint16 }, + name: cursor.name("name") { cursor.read_bytes(name_len) }, }, } when :COMP_REC_CLUST_DELETE_MARK { - index: cursor.name('index') { read_index(cursor) }, - record: cursor.name('record') { read_clust_delete_mark(cursor) }, + index: cursor.name("index") { read_index(cursor) }, + record: cursor.name("record") { read_clust_delete_mark(cursor) }, } when :REC_CLUST_DELETE_MARK { - record: cursor.name('record') { read_clust_delete_mark(cursor) }, + record: cursor.name("record") { read_clust_delete_mark(cursor) }, } when :COMP_REC_SEC_DELETE_MARK { - index: cursor.name('index') { read_index(cursor) }, - value: cursor.name('value') { cursor.read_uint8 }, - offset: cursor.name('offset') { cursor.read_uint16 }, + index: cursor.name("index") { read_index(cursor) }, + value: cursor.name("value") { cursor.read_uint8 }, + offset: cursor.name("offset") { cursor.read_uint16 }, } when :REC_SEC_DELETE_MARK { - value: cursor.name('value') { cursor.read_uint8 }, - offset: cursor.name('offset') { cursor.read_uint16 }, + value: cursor.name("value") { cursor.read_uint8 }, + offset: cursor.name("offset") { cursor.read_uint16 }, } when :REC_DELETE { - offset: cursor.name('offset') { cursor.read_uint16 }, + offset: cursor.name("offset") { cursor.read_uint16 }, } when :COMP_REC_DELETE { - index: cursor.name('index') { read_index(cursor) }, - offset: cursor.name('offset') { cursor.read_uint16 }, + index: cursor.name("index") { read_index(cursor) }, + offset: cursor.name("offset") { cursor.read_uint16 }, } when :REC_MIN_MARK, :COMP_REC_MIN_MARK { - offset: cursor.name('offset') { cursor.read_uint16 }, + offset: cursor.name("offset") { cursor.read_uint16 }, } when :LIST_START_DELETE, :LIST_END_DELETE { - offset: cursor.name('offset') { cursor.read_uint16 }, + offset: cursor.name("offset") { cursor.read_uint16 }, } when :COMP_LIST_START_DELETE, :COMP_LIST_END_DELETE { - index: cursor.name('index') { read_index(cursor) }, - offset: cursor.name('offset') { cursor.read_uint16 }, + index: cursor.name("index") { read_index(cursor) }, + offset: cursor.name("offset") { cursor.read_uint16 }, } when :LIST_END_COPY_CREATED { - length: length = cursor.name('length') { cursor.read_uint32 }, - data: cursor.name('data') { cursor.read_bytes(length) }, + length: length = cursor.name("length") { cursor.read_uint32 }, + data: cursor.name("data") { cursor.read_bytes(length) }, } when :COMP_LIST_END_COPY_CREATE { - index: cursor.name('index') { read_index(cursor) }, - length: length = cursor.name('length') { cursor.read_uint32 }, - data: cursor.name('data') { cursor.read_bytes(length) }, + index: cursor.name("index") { read_index(cursor) }, + length: length = cursor.name("length") { cursor.read_uint32 }, + data: cursor.name("data") { cursor.read_bytes(length) }, } when :COMP_PAGE_REORGANIZE { - index: cursor.name('index') { read_index(cursor) }, + index: cursor.name("index") { read_index(cursor) }, } when :DUMMY_RECORD, :MULTI_REC_END, :INIT_FILE_PAGE, :IBUF_BITMAP_INIT, :PAGE_CREATE, :COMP_PAGE_CREATE, @@ -359,5 +361,6 @@ def read_payload(type, cursor) raise "Unsupported log record type: #{type}" end end + # rubocop:enable Lint/DuplicateBranch end end diff --git a/lib/innodb/lsn.rb b/lib/innodb/lsn.rb index e6537b0a..5d95fcd2 100644 --- a/lib/innodb/lsn.rb +++ b/lib/innodb/lsn.rb @@ -40,7 +40,7 @@ def delta(length) fragment = (@lsn_no % LOG_BLOCK_SIZE) - LOG_BLOCK_HEADER_SIZE raise "Invalid fragment #{fragment} for LSN #{@lsn_no}" unless fragment.between?(0, LOG_BLOCK_DATA_SIZE - 1) - length + (fragment + length) / LOG_BLOCK_DATA_SIZE * LOG_BLOCK_FRAME_SIZE + length + ((fragment + length) / LOG_BLOCK_DATA_SIZE * LOG_BLOCK_FRAME_SIZE) end # Whether LSN might point to log record data. @@ -82,12 +82,12 @@ def offset_of(lsn, offset, new_lsn, group) end # Transpose group size offset to a group capacity offset. - group_offset = offset - (LOG_HEADER_SIZE * (1 + offset / log_size)) + group_offset = offset - (LOG_HEADER_SIZE * (1 + (offset / log_size))) offset = (lsn_offset + group_offset) % group_capacity # Transpose group capacity offset to a group size offset. - offset + LOG_HEADER_SIZE * (1 + offset / (log_size - LOG_HEADER_SIZE)) + offset + (LOG_HEADER_SIZE * (1 + (offset / (log_size - LOG_HEADER_SIZE)))) end # Whether offset points to the data area of an existing log block. diff --git a/lib/innodb/mysql_collation.rb b/lib/innodb/mysql_collation.rb new file mode 100644 index 00000000..f9d639da --- /dev/null +++ b/lib/innodb/mysql_collation.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Innodb + class MysqlCollation + class DuplicateIdError < StandardError; end + class DuplicateNameError < StandardError; end + + @collations = [] + @collations_by_id = {} + @collations_by_name = {} + + class << self + attr_reader :collations + end + + def self.add(kwargs) + raise DuplicateIdError if @collations_by_id.key?(kwargs[:id]) + raise DuplicateNameError if @collations_by_name.key?(kwargs[:name]) + + collation = new(**kwargs) + @collations.push(collation) + @collations_by_id[collation.id] = collation + @collations_by_name[collation.name] = collation + @all_fixed_ids = nil + collation + end + + def self.by_id(id) + @collations_by_id[id] + end + + def self.by_name(name) + @collations_by_name[name] + end + + def self.all_fixed_ids + @all_fixed_ids ||= Innodb::MysqlCollation.collations.select(&:fixed?).map(&:id).sort + end + + attr_reader :id + attr_reader :name + attr_reader :character_set_name + attr_reader :mbminlen + attr_reader :mbmaxlen + + def initialize(id:, name:, character_set_name:, mbminlen:, mbmaxlen:) + @id = id + @name = name + @character_set_name = character_set_name + @mbminlen = mbminlen + @mbmaxlen = mbmaxlen + end + + def fixed? + mbminlen == mbmaxlen + end + + def variable? + !fixed? + end + end +end diff --git a/lib/innodb/mysql_collations.rb b/lib/innodb/mysql_collations.rb new file mode 100644 index 00000000..19b08ecb --- /dev/null +++ b/lib/innodb/mysql_collations.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +# Generated at 2024-11-26 00:42:28 UTC using innodb_ruby_generate_mysql_collations. Do not edit! + +# rubocop:disable all +Innodb::MysqlCollation.add(id: 1, name: "big5_chinese_ci", character_set_name: "big5", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 2, name: "latin2_czech_cs", character_set_name: "latin2", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 3, name: "dec8_swedish_ci", character_set_name: "dec8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 4, name: "cp850_general_ci", character_set_name: "cp850", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 5, name: "latin1_german1_ci", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 6, name: "hp8_english_ci", character_set_name: "hp8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 7, name: "koi8r_general_ci", character_set_name: "koi8r", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 8, name: "latin1_swedish_ci", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 9, name: "latin2_general_ci", character_set_name: "latin2", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 10, name: "swe7_swedish_ci", character_set_name: "swe7", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 11, name: "ascii_general_ci", character_set_name: "ascii", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 12, name: "ujis_japanese_ci", character_set_name: "ujis", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 13, name: "sjis_japanese_ci", character_set_name: "sjis", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 14, name: "cp1251_bulgarian_ci", character_set_name: "cp1251", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 15, name: "latin1_danish_ci", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 16, name: "hebrew_general_ci", character_set_name: "hebrew", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 17, name: "filename", character_set_name: "filename", mbminlen: 1, mbmaxlen: 5) +Innodb::MysqlCollation.add(id: 18, name: "tis620_thai_ci", character_set_name: "tis620", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 19, name: "euckr_korean_ci", character_set_name: "euckr", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 20, name: "latin7_estonian_cs", character_set_name: "latin7", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 21, name: "latin2_hungarian_ci", character_set_name: "latin2", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 22, name: "koi8u_general_ci", character_set_name: "koi8u", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 23, name: "cp1251_ukrainian_ci", character_set_name: "cp1251", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 24, name: "gb2312_chinese_ci", character_set_name: "gb2312", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 25, name: "greek_general_ci", character_set_name: "greek", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 26, name: "cp1250_general_ci", character_set_name: "cp1250", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 27, name: "latin2_croatian_ci", character_set_name: "latin2", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 28, name: "gbk_chinese_ci", character_set_name: "gbk", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 29, name: "cp1257_lithuanian_ci", character_set_name: "cp1257", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 30, name: "latin5_turkish_ci", character_set_name: "latin5", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 31, name: "latin1_german2_ci", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 32, name: "armscii8_general_ci", character_set_name: "armscii8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 33, name: "utf8mb3_general_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 34, name: "cp1250_czech_cs", character_set_name: "cp1250", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 35, name: "ucs2_general_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 36, name: "cp866_general_ci", character_set_name: "cp866", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 37, name: "keybcs2_general_ci", character_set_name: "keybcs2", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 38, name: "macce_general_ci", character_set_name: "macce", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 39, name: "macroman_general_ci", character_set_name: "macroman", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 40, name: "cp852_general_ci", character_set_name: "cp852", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 41, name: "latin7_general_ci", character_set_name: "latin7", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 42, name: "latin7_general_cs", character_set_name: "latin7", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 43, name: "macce_bin", character_set_name: "macce", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 44, name: "cp1250_croatian_ci", character_set_name: "cp1250", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 45, name: "utf8mb4_general_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 46, name: "utf8mb4_bin", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 47, name: "latin1_bin", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 48, name: "latin1_general_ci", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 49, name: "latin1_general_cs", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 50, name: "cp1251_bin", character_set_name: "cp1251", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 51, name: "cp1251_general_ci", character_set_name: "cp1251", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 52, name: "cp1251_general_cs", character_set_name: "cp1251", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 53, name: "macroman_bin", character_set_name: "macroman", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 54, name: "utf16_general_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 55, name: "utf16_bin", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 56, name: "utf16le_general_ci", character_set_name: "utf16le", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 57, name: "cp1256_general_ci", character_set_name: "cp1256", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 58, name: "cp1257_bin", character_set_name: "cp1257", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 59, name: "cp1257_general_ci", character_set_name: "cp1257", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 60, name: "utf32_general_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 61, name: "utf32_bin", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 62, name: "utf16le_bin", character_set_name: "utf16le", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 63, name: "binary", character_set_name: "binary", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 64, name: "armscii8_bin", character_set_name: "armscii8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 65, name: "ascii_bin", character_set_name: "ascii", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 66, name: "cp1250_bin", character_set_name: "cp1250", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 67, name: "cp1256_bin", character_set_name: "cp1256", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 68, name: "cp866_bin", character_set_name: "cp866", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 69, name: "dec8_bin", character_set_name: "dec8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 70, name: "greek_bin", character_set_name: "greek", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 71, name: "hebrew_bin", character_set_name: "hebrew", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 72, name: "hp8_bin", character_set_name: "hp8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 73, name: "keybcs2_bin", character_set_name: "keybcs2", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 74, name: "koi8r_bin", character_set_name: "koi8r", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 75, name: "koi8u_bin", character_set_name: "koi8u", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 76, name: "utf8mb3_tolower_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 77, name: "latin2_bin", character_set_name: "latin2", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 78, name: "latin5_bin", character_set_name: "latin5", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 79, name: "latin7_bin", character_set_name: "latin7", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 80, name: "cp850_bin", character_set_name: "cp850", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 81, name: "cp852_bin", character_set_name: "cp852", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 82, name: "swe7_bin", character_set_name: "swe7", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 83, name: "utf8mb3_bin", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 84, name: "big5_bin", character_set_name: "big5", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 85, name: "euckr_bin", character_set_name: "euckr", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 86, name: "gb2312_bin", character_set_name: "gb2312", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 87, name: "gbk_bin", character_set_name: "gbk", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 88, name: "sjis_bin", character_set_name: "sjis", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 89, name: "tis620_bin", character_set_name: "tis620", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 90, name: "ucs2_bin", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 91, name: "ujis_bin", character_set_name: "ujis", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 92, name: "geostd8_general_ci", character_set_name: "geostd8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 93, name: "geostd8_bin", character_set_name: "geostd8", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 94, name: "latin1_spanish_ci", character_set_name: "latin1", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 95, name: "cp932_japanese_ci", character_set_name: "cp932", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 96, name: "cp932_bin", character_set_name: "cp932", mbminlen: 1, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 97, name: "eucjpms_japanese_ci", character_set_name: "eucjpms", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 98, name: "eucjpms_bin", character_set_name: "eucjpms", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 99, name: "cp1250_polish_ci", character_set_name: "cp1250", mbminlen: 1, mbmaxlen: 1) +Innodb::MysqlCollation.add(id: 101, name: "utf16_unicode_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 102, name: "utf16_icelandic_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 103, name: "utf16_latvian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 104, name: "utf16_romanian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 105, name: "utf16_slovenian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 106, name: "utf16_polish_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 107, name: "utf16_estonian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 108, name: "utf16_spanish_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 109, name: "utf16_swedish_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 110, name: "utf16_turkish_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 111, name: "utf16_czech_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 112, name: "utf16_danish_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 113, name: "utf16_lithuanian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 114, name: "utf16_slovak_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 115, name: "utf16_spanish2_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 116, name: "utf16_roman_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 117, name: "utf16_persian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 118, name: "utf16_esperanto_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 119, name: "utf16_hungarian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 120, name: "utf16_sinhala_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 121, name: "utf16_german2_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 122, name: "utf16_croatian_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 123, name: "utf16_unicode_520_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 124, name: "utf16_vietnamese_ci", character_set_name: "utf16", mbminlen: 2, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 128, name: "ucs2_unicode_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 129, name: "ucs2_icelandic_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 130, name: "ucs2_latvian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 131, name: "ucs2_romanian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 132, name: "ucs2_slovenian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 133, name: "ucs2_polish_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 134, name: "ucs2_estonian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 135, name: "ucs2_spanish_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 136, name: "ucs2_swedish_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 137, name: "ucs2_turkish_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 138, name: "ucs2_czech_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 139, name: "ucs2_danish_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 140, name: "ucs2_lithuanian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 141, name: "ucs2_slovak_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 142, name: "ucs2_spanish2_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 143, name: "ucs2_roman_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 144, name: "ucs2_persian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 145, name: "ucs2_esperanto_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 146, name: "ucs2_hungarian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 147, name: "ucs2_sinhala_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 148, name: "ucs2_german2_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 149, name: "ucs2_croatian_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 150, name: "ucs2_unicode_520_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 151, name: "ucs2_vietnamese_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 159, name: "ucs2_general_mysql500_ci", character_set_name: "ucs2", mbminlen: 2, mbmaxlen: 2) +Innodb::MysqlCollation.add(id: 160, name: "utf32_unicode_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 161, name: "utf32_icelandic_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 162, name: "utf32_latvian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 163, name: "utf32_romanian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 164, name: "utf32_slovenian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 165, name: "utf32_polish_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 166, name: "utf32_estonian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 167, name: "utf32_spanish_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 168, name: "utf32_swedish_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 169, name: "utf32_turkish_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 170, name: "utf32_czech_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 171, name: "utf32_danish_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 172, name: "utf32_lithuanian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 173, name: "utf32_slovak_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 174, name: "utf32_spanish2_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 175, name: "utf32_roman_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 176, name: "utf32_persian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 177, name: "utf32_esperanto_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 178, name: "utf32_hungarian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 179, name: "utf32_sinhala_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 180, name: "utf32_german2_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 181, name: "utf32_croatian_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 182, name: "utf32_unicode_520_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 183, name: "utf32_vietnamese_ci", character_set_name: "utf32", mbminlen: 4, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 192, name: "utf8mb3_unicode_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 193, name: "utf8mb3_icelandic_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 194, name: "utf8mb3_latvian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 195, name: "utf8mb3_romanian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 196, name: "utf8mb3_slovenian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 197, name: "utf8mb3_polish_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 198, name: "utf8mb3_estonian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 199, name: "utf8mb3_spanish_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 200, name: "utf8mb3_swedish_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 201, name: "utf8mb3_turkish_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 202, name: "utf8mb3_czech_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 203, name: "utf8mb3_danish_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 204, name: "utf8mb3_lithuanian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 205, name: "utf8mb3_slovak_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 206, name: "utf8mb3_spanish2_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 207, name: "utf8mb3_roman_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 208, name: "utf8mb3_persian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 209, name: "utf8mb3_esperanto_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 210, name: "utf8mb3_hungarian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 211, name: "utf8mb3_sinhala_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 212, name: "utf8mb3_german2_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 213, name: "utf8mb3_croatian_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 214, name: "utf8mb3_unicode_520_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 215, name: "utf8mb3_vietnamese_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 223, name: "utf8mb3_general_mysql500_ci", character_set_name: "utf8mb3", mbminlen: 1, mbmaxlen: 3) +Innodb::MysqlCollation.add(id: 224, name: "utf8mb4_unicode_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 225, name: "utf8mb4_icelandic_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 226, name: "utf8mb4_latvian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 227, name: "utf8mb4_romanian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 228, name: "utf8mb4_slovenian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 229, name: "utf8mb4_polish_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 230, name: "utf8mb4_estonian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 231, name: "utf8mb4_spanish_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 232, name: "utf8mb4_swedish_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 233, name: "utf8mb4_turkish_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 234, name: "utf8mb4_czech_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 235, name: "utf8mb4_danish_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 236, name: "utf8mb4_lithuanian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 237, name: "utf8mb4_slovak_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 238, name: "utf8mb4_spanish2_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 239, name: "utf8mb4_roman_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 240, name: "utf8mb4_persian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 241, name: "utf8mb4_esperanto_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 242, name: "utf8mb4_hungarian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 243, name: "utf8mb4_sinhala_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 244, name: "utf8mb4_german2_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 245, name: "utf8mb4_croatian_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 246, name: "utf8mb4_unicode_520_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 247, name: "utf8mb4_vietnamese_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 248, name: "gb18030_chinese_ci", character_set_name: "gb18030", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 249, name: "gb18030_bin", character_set_name: "gb18030", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 250, name: "gb18030_unicode_520_ci", character_set_name: "gb18030", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 255, name: "utf8mb4_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 256, name: "utf8mb4_de_pb_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 257, name: "utf8mb4_is_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 258, name: "utf8mb4_lv_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 259, name: "utf8mb4_ro_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 260, name: "utf8mb4_sl_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 261, name: "utf8mb4_pl_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 262, name: "utf8mb4_et_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 263, name: "utf8mb4_es_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 264, name: "utf8mb4_sv_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 265, name: "utf8mb4_tr_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 266, name: "utf8mb4_cs_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 267, name: "utf8mb4_da_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 268, name: "utf8mb4_lt_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 269, name: "utf8mb4_sk_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 270, name: "utf8mb4_es_trad_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 271, name: "utf8mb4_la_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 273, name: "utf8mb4_eo_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 274, name: "utf8mb4_hu_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 275, name: "utf8mb4_hr_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 277, name: "utf8mb4_vi_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 278, name: "utf8mb4_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 279, name: "utf8mb4_de_pb_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 280, name: "utf8mb4_is_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 281, name: "utf8mb4_lv_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 282, name: "utf8mb4_ro_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 283, name: "utf8mb4_sl_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 284, name: "utf8mb4_pl_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 285, name: "utf8mb4_et_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 286, name: "utf8mb4_es_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 287, name: "utf8mb4_sv_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 288, name: "utf8mb4_tr_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 289, name: "utf8mb4_cs_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 290, name: "utf8mb4_da_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 291, name: "utf8mb4_lt_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 292, name: "utf8mb4_sk_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 293, name: "utf8mb4_es_trad_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 294, name: "utf8mb4_la_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 296, name: "utf8mb4_eo_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 297, name: "utf8mb4_hu_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 298, name: "utf8mb4_hr_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 300, name: "utf8mb4_vi_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 303, name: "utf8mb4_ja_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 304, name: "utf8mb4_ja_0900_as_cs_ks", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 305, name: "utf8mb4_0900_as_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 306, name: "utf8mb4_ru_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 307, name: "utf8mb4_ru_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 308, name: "utf8mb4_zh_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 309, name: "utf8mb4_0900_bin", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 310, name: "utf8mb4_nb_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 311, name: "utf8mb4_nb_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 312, name: "utf8mb4_nn_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 313, name: "utf8mb4_nn_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 314, name: "utf8mb4_sr_latn_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 315, name: "utf8mb4_sr_latn_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 316, name: "utf8mb4_bs_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 317, name: "utf8mb4_bs_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 318, name: "utf8mb4_bg_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 319, name: "utf8mb4_bg_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 320, name: "utf8mb4_gl_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 321, name: "utf8mb4_gl_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 322, name: "utf8mb4_mn_cyrl_0900_ai_ci", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) +Innodb::MysqlCollation.add(id: 323, name: "utf8mb4_mn_cyrl_0900_as_cs", character_set_name: "utf8mb4", mbminlen: 1, mbmaxlen: 4) diff --git a/lib/innodb/mysql_type.rb b/lib/innodb/mysql_type.rb new file mode 100644 index 00000000..46298142 --- /dev/null +++ b/lib/innodb/mysql_type.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Innodb + class MysqlType + attr_reader :mysql_field_type_value + attr_reader :sdi_column_type_value + attr_reader :type + attr_reader :handle_as + + def initialize(type:, mysql_field_type_value:, sdi_column_type_value:, handle_as: nil) + @mysql_field_type_value = mysql_field_type_value + @sdi_column_type_value = sdi_column_type_value + @type = type + @handle_as = handle_as || type + end + + # A hash of MySQL's internal type system to the stored + # values for those types, and the 'external' SQL type. + TYPES = [ + new(type: :DECIMAL, mysql_field_type_value: 0, sdi_column_type_value: 1), + new(type: :TINYINT, mysql_field_type_value: 1, sdi_column_type_value: 2), + new(type: :SMALLINT, mysql_field_type_value: 2, sdi_column_type_value: 3), + new(type: :INT, mysql_field_type_value: 3, sdi_column_type_value: 4), + new(type: :FLOAT, mysql_field_type_value: 4, sdi_column_type_value: 5), + new(type: :DOUBLE, mysql_field_type_value: 5, sdi_column_type_value: 6), + new(type: :TYPE_NULL, mysql_field_type_value: 6, sdi_column_type_value: 7), + new(type: :TIMESTAMP, mysql_field_type_value: 7, sdi_column_type_value: 8), + new(type: :BIGINT, mysql_field_type_value: 8, sdi_column_type_value: 9), + new(type: :MEDIUMINT, mysql_field_type_value: 9, sdi_column_type_value: 10), + new(type: :DATE, mysql_field_type_value: 10, sdi_column_type_value: 11), + new(type: :TIME, mysql_field_type_value: 11, sdi_column_type_value: 12), + new(type: :DATETIME, mysql_field_type_value: 12, sdi_column_type_value: 13), + new(type: :YEAR, mysql_field_type_value: 13, sdi_column_type_value: 14), + new(type: :DATE, mysql_field_type_value: 14, sdi_column_type_value: 15), + new(type: :VARCHAR, mysql_field_type_value: 15, sdi_column_type_value: 16), + new(type: :BIT, mysql_field_type_value: 16, sdi_column_type_value: 17), + new(type: :TIMESTAMP2, mysql_field_type_value: 17, sdi_column_type_value: 18), + new(type: :DATETIME2, mysql_field_type_value: 18, sdi_column_type_value: 19), + new(type: :TIME2, mysql_field_type_value: 19, sdi_column_type_value: 20), + new(type: :NEWDECIMAL, mysql_field_type_value: 246, sdi_column_type_value: 21, handle_as: :CHAR), + new(type: :ENUM, mysql_field_type_value: 247, sdi_column_type_value: 22), + new(type: :SET, mysql_field_type_value: 248, sdi_column_type_value: 23), + new(type: :TINYBLOB, mysql_field_type_value: 249, sdi_column_type_value: 24), + new(type: :MEDIUMBLOB, mysql_field_type_value: 250, sdi_column_type_value: 25), + new(type: :LONGBLOB, mysql_field_type_value: 251, sdi_column_type_value: 26), + new(type: :BLOB, mysql_field_type_value: 252, sdi_column_type_value: 27), + new(type: :VARCHAR, mysql_field_type_value: 253, sdi_column_type_value: 28), + new(type: :CHAR, mysql_field_type_value: 254, sdi_column_type_value: 29), + new(type: :GEOMETRY, mysql_field_type_value: 255, sdi_column_type_value: 30), + new(type: :JSON, mysql_field_type_value: 245, sdi_column_type_value: 31), + ].freeze + + # A hash of types by mysql_field_type_value. + TYPES_BY_MYSQL_FIELD_TYPE_VALUE = Innodb::MysqlType::TYPES.to_h { |t| [t.mysql_field_type_value, t] }.freeze + + # A hash of types by sdi_column_type_value. + TYPES_BY_SDI_COLUMN_TYPE_VALUE = Innodb::MysqlType::TYPES.to_h { |t| [t.sdi_column_type_value, t] }.freeze + + def self.by_mysql_field_type(value) + TYPES_BY_MYSQL_FIELD_TYPE_VALUE[value] + end + + def self.by_sdi_column_type(value) + TYPES_BY_SDI_COLUMN_TYPE_VALUE[value] + end + end +end diff --git a/lib/innodb/page.rb b/lib/innodb/page.rb index 731affde..b4e7fca6 100644 --- a/lib/innodb/page.rb +++ b/lib/innodb/page.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" # A generic class for any type of page, which handles reading the common # FIL header and trailer, and can handle (via #parse) dispatching to a more @@ -43,7 +43,7 @@ def lsn_low32 Region = Struct.new( :offset, - :length, + :length, # rubocop:disable Lint/StructNewOverride :name, :info, keyword_init: true @@ -101,11 +101,8 @@ def self.handle(_page, space, buffer, page_number = nil) # Initialize a page by passing in a buffer containing the raw page contents. # The buffer size should match the space's page size. def initialize(space, buffer, page_number = nil) - unless space && buffer - raise "Page can't be initialized from nil space or buffer (space: #{space}, buffer: #{buffer})" - end - - raise "Buffer size #{buffer.size} is different than space page size" unless space.page_size == buffer.size + raise "Page can't be initialized from nil space or buffer (space: #{space}, buffer: #{buffer})" unless buffer + raise "Buffer size #{buffer.size} is different than space page size" if space && space.page_size != buffer.size @space = space @buffer = buffer @@ -129,7 +126,7 @@ def default_page_size? def name page_offset = BinData::Uint32be.read(@buffer.slice(4, 4)) page_type = BinData::Uint16be.read(@buffer.slice(24, 2)) - '%i,%s' % [ + "%i,%s" % [ page_offset, PAGE_TYPE_BY_VALUE[page_type], ] @@ -141,7 +138,7 @@ def name # return value of the block. def cursor(buffer_offset) new_cursor = BufferCursor.new(@buffer, buffer_offset) - new_cursor.push_name("space[#{space.name}]") + new_cursor.push_name("space[#{space&.name || 'unknown'}]") new_cursor.push_name("page[#{name}]") if block_given? @@ -204,75 +201,138 @@ def size_page_body PAGE_TYPE = { ALLOCATED: { value: 0, - description: 'Freshly allocated', - usage: 'page type field has not been initialized', + description: "Freshly allocated", }, UNDO_LOG: { value: 2, - description: 'Undo log', - usage: 'stores previous values of modified records', + description: "Undo log", }, INODE: { value: 3, - description: 'File segment inode', - usage: 'bookkeeping for file segments', + description: "File segment inode", }, IBUF_FREE_LIST: { value: 4, - description: 'Insert buffer free list', - usage: 'bookkeeping for insert buffer free space management', + description: "Insert buffer free list", }, IBUF_BITMAP: { value: 5, - description: 'Insert buffer bitmap', - usage: 'bookkeeping for insert buffer writes to be merged', + description: "Insert buffer bitmap", }, SYS: { value: 6, - description: 'System internal', - usage: 'used for various purposes in the system tablespace', + description: "System internal", }, TRX_SYS: { value: 7, - description: 'Transaction system header', - usage: 'bookkeeping for the transaction system in system tablespace', + description: "Transaction system header", }, FSP_HDR: { value: 8, - description: 'File space header', - usage: 'header page (page 0) for each tablespace file', + description: "File space header", }, XDES: { value: 9, - description: 'Extent descriptor', - usage: 'header page for subsequent blocks of 16,384 pages', + description: "Extent descriptor", }, BLOB: { value: 10, - description: 'Uncompressed BLOB', - usage: 'externally-stored uncompressed BLOB column data', + description: "Uncompressed BLOB", }, ZBLOB: { value: 11, - description: 'First compressed BLOB', - usage: 'externally-stored compressed BLOB column data, first page', + description: "First compressed BLOB", }, ZBLOB2: { value: 12, - description: 'Subsequent compressed BLOB', - usage: 'externally-stored compressed BLOB column data, subsequent page', + description: "Subsequent compressed BLOB", + }, + UNKNOWN: { + value: 13, + description: "Unknown", + }, + COMPRESSED: { + value: 14, + description: "Compressed", + }, + ENCRYPTED: { + value: 15, + description: "Encrypted", + }, + COMPRESSED_AND_ENCRYPTED: { + value: 16, + description: "Compressed and Encrypted", + }, + ENCRYPTED_RTREE: { + value: 17, + description: "Encrypted R-tree", + }, + SDI_BLOB: { + value: 18, + description: "Uncompressed SDI BLOB", + }, + SDI_ZBLOB: { + value: 19, + description: "Compressed SDI BLOB", + }, + LEGACY_DBLWR: { + value: 20, + description: "Legacy doublewrite buffer", + }, + RSEG_ARRAY: { + value: 21, + description: "Rollback Segment Array", + }, + LOB_INDEX: { + value: 22, + description: "Index of uncompressed LOB", + }, + LOB_DATA: { + value: 23, + description: "Data of uncompressed LOB", + }, + LOB_FIRST: { + value: 24, + description: "First page of an uncompressed LOB", + }, + ZLOB_FIRST: { + value: 25, + description: "First page of a compressed LOB", + }, + ZLOB_DATA: { + value: 26, + description: "Data of compressed LOB", + }, + ZLOB_INDEX: { + value: 27, + description: "Index of compressed LOB", + }, + ZLOB_FRAG: { + value: 28, + description: "Fragment of compressed LOB", + }, + ZLOB_FRAG_ENTRY: { + value: 29, + description: "Index of fragment for compressed LOB", + }, + SDI: { + value: 17_853, + description: "Serialized Dictionary Information", + }, + RTREE: { + value: 17_854, + description: "R-tree index", }, INDEX: { value: 17_855, - description: 'B+Tree index', - usage: 'table and index data stored in B+Tree structure', + description: "B+Tree index", }, }.freeze PAGE_TYPE_BY_VALUE = PAGE_TYPE.each_with_object({}) { |(k, v), h| h[v[:value]] = k } # A page number representing "undefined" values, (4294967295). - UNDEFINED_PAGE_NUMBER = 2**32 - 1 + UNDEFINED_PAGE_NUMBER = (2**32) - 1 # A helper to check if a page number is the undefined page number. def self.undefined?(page_number) @@ -285,28 +345,32 @@ def self.maybe_undefined(page_number) page_number unless undefined?(page_number) end + def self.page_type_by_value(value) + PAGE_TYPE_BY_VALUE[value] || value + end + # Return the "fil" header from the page, which is common for all page types. def fil_header - @fil_header ||= cursor(pos_fil_header).name('fil_header') do |c| + @fil_header ||= cursor(pos_fil_header).name("fil_header") do |c| FilHeader.new( - checksum: c.name('checksum') { c.read_uint32 }, - offset: c.name('offset') { c.read_uint32 }, - prev: c.name('prev') { Innodb::Page.maybe_undefined(c.read_uint32) }, - next: c.name('next') { Innodb::Page.maybe_undefined(c.read_uint32) }, - lsn: c.name('lsn') { c.read_uint64 }, - type: c.name('type') { PAGE_TYPE_BY_VALUE[c.read_uint16] }, - flush_lsn: c.name('flush_lsn') { c.read_uint64 }, - space_id: c.name('space_id') { c.read_uint32 } + checksum: c.name("checksum") { c.read_uint32 }, + offset: c.name("offset") { c.read_uint32 }, + prev: c.name("prev") { Innodb::Page.maybe_undefined(c.read_uint32) }, + next: c.name("next") { Innodb::Page.maybe_undefined(c.read_uint32) }, + lsn: c.name("lsn") { c.read_uint64 }, + type: c.name("type") { Innodb::Page.page_type_by_value(c.read_uint16) }, + flush_lsn: c.name("flush_lsn") { c.read_uint64 }, + space_id: c.name("space_id") { c.read_uint32 } ) end end # Return the "fil" trailer from the page, which is common for all page types. def fil_trailer - @fil_trailer ||= cursor(pos_fil_trailer).name('fil_trailer') do |c| + @fil_trailer ||= cursor(pos_fil_trailer).name("fil_trailer") do |c| FilTrailer.new( - checksum: c.name('checksum') { c.read_uint32 }, - lsn_low32: c.name('lsn_low32') { c.read_uint32 } + checksum: c.name("checksum") { c.read_uint32 }, + lsn_low32: c.name("lsn_low32") { c.read_uint32 } ) end end @@ -320,27 +384,23 @@ def fil_trailer def_delegator :fil_header, :space_id # Iterate each byte of the FIL header. - def each_page_header_byte_as_uint8 + def each_page_header_byte_as_uint8(&block) return enum_for(:each_page_header_byte_as_uint8) unless block_given? - cursor(pos_partial_page_header).each_byte_as_uint8(size_partial_page_header) do |byte| - yield byte - end + cursor(pos_partial_page_header).each_byte_as_uint8(size_partial_page_header, &block) end # Iterate each byte of the page body, except for the FIL header and # the FIL trailer. - def each_page_body_byte_as_uint8 + def each_page_body_byte_as_uint8(&block) return enum_for(:each_page_body_byte_as_uint8) unless block_given? - cursor(pos_page_body).each_byte_as_uint8(size_page_body) do |byte| - yield byte - end + cursor(pos_page_body).each_byte_as_uint8(size_page_body, &block) end # Calculate the checksum of the page using InnoDB's algorithm. def checksum_innodb - raise 'Checksum calculation is only supported for 16 KiB pages' unless default_page_size? + raise "Checksum calculation is only supported for 16 KiB pages" unless default_page_size? @checksum_innodb ||= begin # Calculate the InnoDB checksum of the page header. @@ -360,7 +420,7 @@ def checksum_innodb? # Calculate the checksum of the page using the CRC32c algorithm. def checksum_crc32 - raise 'Checksum calculation is only supported for 16 KiB pages' unless default_page_size? + raise "Checksum calculation is only supported for 16 KiB pages" unless default_page_size? @checksum_crc32 ||= begin # Calculate the CRC32c of the page header. @@ -409,7 +469,7 @@ def torn? # Is the page in the doublewrite buffer? def in_doublewrite_buffer? - space&.system_space? && space&.doublewrite_page?(offset) + space&.system_space? && space.doublewrite_page?(offset) end # Is the space ID stored in the header different from that of the space @@ -447,14 +507,14 @@ def each_region offset: pos_fil_header, length: size_fil_header, name: :fil_header, - info: 'FIL Header' + info: "FIL Header" ) yield Region.new( offset: pos_fil_trailer, length: size_fil_trailer, name: :fil_trailer, - info: 'FIL Trailer' + info: "FIL Trailer" ) nil @@ -474,7 +534,7 @@ def inspect_header_fields checksum_type torn? misplaced? - ].map { |m| "#{m}=#{send(m).inspect}" }.join(', ') + ].map { |m| "#{m}=#{send(m).inspect}" }.join(", ") end # Implement a custom inspect method to avoid irb printing the contents of @@ -488,11 +548,11 @@ def dump puts "#{self}:" puts - puts 'fil header:' + puts "fil header:" pp fil_header puts - puts 'fil trailer:' + puts "fil trailer:" pp fil_trailer puts end diff --git a/lib/innodb/page/blob.rb b/lib/innodb/page/blob.rb index 8a285eb8..3dfc4a17 100644 --- a/lib/innodb/page/blob.rb +++ b/lib/innodb/page/blob.rb @@ -18,51 +18,43 @@ def pos_blob_data end def blob_header - cursor(pos_blob_header).name('blob_header') do |c| + cursor(pos_blob_header).name("blob_header") do |c| { - length: c.name('length') { c.read_uint32 }, - next: c.name('next') { Innodb::Page.maybe_undefined(c.read_uint32) }, + length: c.name("length") { c.read_uint32 }, + next: c.name("next") { Innodb::Page.maybe_undefined(c.read_uint32) }, } end end def blob_data - cursor(pos_blob_data).name('blob_data') do |c| + cursor(pos_blob_data).name("blob_data") do |c| c.read_bytes(blob_header[:length]) end end - def dump_hex(string) - slice_size = 16 - string.split('').each_slice(slice_size).each_with_index do |slice_bytes, slice_count| - puts '%08i %-23s %-23s |%-16s|' % [ - (slice_count * slice_size), - slice_bytes[0..8].map { |n| '%02x' % n.ord }.join(' '), - slice_bytes[8..16].map { |n| '%02x' % n.ord }.join(' '), - slice_bytes.join(''), - ] - end + def next_blob_page + return unless blob_header[:next] + + space.page(blob_header[:next]) end - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_blob_header, length: size_blob_header, name: :blob_header, - info: 'Blob Header' + info: "Blob Header" ) yield Region.new( offset: pos_blob_data, length: blob_header[:length], name: :blob_data, - info: 'Blob Data' + info: "Blob Data" ) nil @@ -72,12 +64,12 @@ def each_region def dump super - puts 'blob header:' + puts "blob header:" pp blob_header puts - puts 'blob data:' - dump_hex(blob_data) + puts "blob data:" + HexFormat.puts(blob_data) puts puts diff --git a/lib/innodb/page/fsp_hdr_xdes.rb b/lib/innodb/page/fsp_hdr_xdes.rb index 3fb156a8..16f24a51 100644 --- a/lib/innodb/page/fsp_hdr_xdes.rb +++ b/lib/innodb/page/fsp_hdr_xdes.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'innodb/list' -require 'innodb/xdes' +require "innodb/list" +require "innodb/xdes" # A specialized class for FSP_HDR (filespace header) and XDES (extent # descriptor) page types. Each tablespace always has an FSP_HDR page as @@ -34,7 +34,7 @@ class FspHdrXdes < Page Header = Struct.new( :space_id, :unused, - :size, + :size, # rubocop:disable Lint/StructNewOverride :free_limit, :flags, :frag_n_used, @@ -47,6 +47,22 @@ class FspHdrXdes < Page keyword_init: true ) + EncryptionHeader = Struct.new( + :magic, + :master_key_id, + :key, + :iv, + :server_uuid, + :checksum, + keyword_init: true + ) + + SdiHeader = Struct.new( + :version, + :root_page_number, + keyword_init: true + ) + # A value added to the adjusted exponent stored in the page size field of # the flags in the FSP header. FLAGS_PAGE_SIZE_SHIFT = 9 @@ -118,23 +134,39 @@ def size_xdes_array entries_in_xdes_array * size_xdes_entry end + def pos_encryption_header + pos_xdes_array + size_xdes_array + end + + def size_encryption_header + 3 + 4 + (32 * 2) + 36 + 4 + 4 + end + + def pos_sdi_header + pos_encryption_header + size_encryption_header + end + + def size_sdi_header + 8 + end + # Read the FSP (filespace) header, which contains a few counters and flags, # as well as list base nodes for each list maintained in the filespace. def fsp_header - @fsp_header ||= cursor(pos_fsp_header).name('fsp') do |c| + @fsp_header ||= cursor(pos_fsp_header).name("fsp") do |c| Header.new( - space_id: c.name('space_id') { c.read_uint32 }, - unused: c.name('unused') { c.read_uint32 }, - size: c.name('size') { c.read_uint32 }, - free_limit: c.name('free_limit') { c.read_uint32 }, - flags: c.name('flags') { self.class.decode_flags(c.read_uint32) }, - frag_n_used: c.name('frag_n_used') { c.read_uint32 }, - free: c.name('list[free]') { Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c)) }, - free_frag: c.name('list[free_frag]') { Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c)) }, - full_frag: c.name('list[full_frag]') { Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c)) }, - first_unused_seg: c.name('first_unused_seg') { c.read_uint64 }, - full_inodes: c.name('list[full_inodes]') { Innodb::List::Inode.new(@space, Innodb::List.get_base_node(c)) }, - free_inodes: c.name('list[free_inodes]') { Innodb::List::Inode.new(@space, Innodb::List.get_base_node(c)) } + space_id: c.name("space_id") { c.read_uint32 }, + unused: c.name("unused") { c.read_uint32 }, + size: c.name("size") { c.read_uint32 }, + free_limit: c.name("free_limit") { c.read_uint32 }, + flags: c.name("flags") { self.class.decode_flags(c.read_uint32) }, + frag_n_used: c.name("frag_n_used") { c.read_uint32 }, + free: c.name("list[free]") { Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c)) }, + free_frag: c.name("list[free_frag]") { Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c)) }, + full_frag: c.name("list[full_frag]") { Innodb::List::Xdes.new(@space, Innodb::List.get_base_node(c)) }, + first_unused_seg: c.name("first_unused_seg") { c.read_uint64 }, + full_inodes: c.name("list[full_inodes]") { Innodb::List::Inode.new(@space, Innodb::List.get_base_node(c)) }, + free_inodes: c.name("list[free_inodes]") { Innodb::List::Inode.new(@space, Innodb::List.get_base_node(c)) } ) end end @@ -155,37 +187,71 @@ def each_list def each_xdes return enum_for(:each_xdes) unless block_given? - cursor(pos_xdes_array).name('xdes_array') do |c| + cursor(pos_xdes_array).name("xdes_array") do |c| entries_in_xdes_array.times do yield Innodb::Xdes.new(self, c) end end end - def each_region - return enum_for(:each_region) unless block_given? + def encryption_header + @encryption_header ||= cursor(pos_encryption_header).name("encryption_header") do |c| + EncryptionHeader.new( + magic: c.name("magic") { c.read_bytes(3) }, + master_key_id: c.name("master_key_id") { c.read_uint32 }, + key: c.name("key") { c.read_bytes(32) }, + iv: c.name("iv") { c.read_bytes(32) }, + server_uuid: c.name("server_uuid") { c.read_string(36) }, + checksum: c.name("checksum") { c.read_uint32 } + ) + end + end - super do |region| - yield region + def sdi_header + @sdi_header ||= cursor(pos_sdi_header).name("sdi_header") do |c| + SdiHeader.new( + version: c.name("version") { c.read_uint32 }, + root_page_number: c.name("root_page_number") { c.read_uint32 } + ) end + end + + def each_region(&block) + return enum_for(:each_region) unless block_given? + + super yield Region.new( offset: pos_fsp_header, length: size_fsp_header, name: :fsp_header, - info: 'FSP Header' + info: "FSP Header" ) each_xdes do |xdes| - state = xdes.state || 'unused' + state = xdes.state || "unused" yield Region.new( offset: xdes.offset, length: size_xdes_entry, - name: "xdes_#{state}".to_sym, + name: :"xdes_#{state}", info: "Extent Descriptor (#{state})" ) end + yield Region.new( + offset: pos_encryption_header, + length: size_encryption_header, + name: :encryption_header, + info: "Encryption Header" + ) + + yield Region.new( + offset: pos_sdi_header, + length: size_sdi_header, + name: :sdi_header, + info: "SDI Header" + ) + nil end @@ -193,15 +259,23 @@ def each_region def dump super - puts 'fsp header:' + puts "fsp header:" pp fsp_header puts - puts 'xdes entries:' + puts "xdes entries:" each_xdes do |xdes| pp xdes end puts + + puts "encryption header:" + pp encryption_header + puts + + puts "serialized dictionary information header:" + pp sdi_header + puts end end end diff --git a/lib/innodb/page/ibuf_bitmap.rb b/lib/innodb/page/ibuf_bitmap.rb index 306dea42..c5ef25c4 100644 --- a/lib/innodb/page/ibuf_bitmap.rb +++ b/lib/innodb/page/ibuf_bitmap.rb @@ -19,18 +19,16 @@ def ibuf_bitmap Innodb::IbufBitmap.new(self, cursor(pos_ibuf_bitmap)) end - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_ibuf_bitmap, length: size_ibuf_bitmap, name: :ibuf_bitmap, - info: 'Insert Buffer Bitmap' + info: "Insert Buffer Bitmap" ) nil @@ -39,9 +37,9 @@ def each_region def dump super - puts 'ibuf bitmap:' + puts "ibuf bitmap:" ibuf_bitmap.each_page_status do |page_number, page_status| - puts ' Page %i: %s' % [page_number, page_status.inspect] + puts " Page %i: %s" % [page_number, page_status.inspect] end end end diff --git a/lib/innodb/page/index.rb b/lib/innodb/page/index.rb index 081e132f..11e55a64 100644 --- a/lib/innodb/page/index.rb +++ b/lib/innodb/page/index.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" -require 'innodb/fseg_entry' +require "innodb/fseg_entry" # A specialized class for handling INDEX pages, which contain a portion of # the data from exactly one B+tree. These are typically the most common type @@ -20,7 +20,7 @@ class Index < Page specialization_for :INDEX RecordHeader = Struct.new( - :length, + :length, # rubocop:disable Lint/StructNewOverride :next, :type, :heap_number, @@ -55,7 +55,7 @@ def deleted? :header, :next, :data, - :length, + :length, # rubocop:disable Lint/StructNewOverride keyword_init: true ) @@ -71,7 +71,7 @@ def deleted? :child_page_number, :transaction_id, :roll_pointer, - :length, + :length, # rubocop:disable Lint/StructNewOverride keyword_init: true ) @@ -130,7 +130,7 @@ def deleted? # Maximum number of fields. RECORD_MAX_N_SYSTEM_FIELDS = 3 RECORD_MAX_N_FIELDS = 1024 - 1 - RECORD_MAX_N_USER_FIELDS = RECORD_MAX_N_FIELDS - RECORD_MAX_N_SYSTEM_FIELDS * 2 + RECORD_MAX_N_USER_FIELDS = RECORD_MAX_N_FIELDS - (RECORD_MAX_N_SYSTEM_FIELDS * 2) # Page direction values possible in the page_header's :direction field. PAGE_DIRECTION = { @@ -298,24 +298,24 @@ def space_per_record # Return the "index" header. def page_header - @page_header ||= cursor(pos_index_header).name('index') do |c| + @page_header ||= cursor(pos_index_header).name("index") do |c| index = PageHeader.new( - n_dir_slots: c.name('n_dir_slots') { c.read_uint16 }, - heap_top: c.name('heap_top') { c.read_uint16 }, - n_heap_format: c.name('n_heap_format') { c.read_uint16 }, - garbage_offset: c.name('garbage_offset') { c.read_uint16 }, - garbage_size: c.name('garbage_size') { c.read_uint16 }, - last_insert_offset: c.name('last_insert_offset') { c.read_uint16 }, - direction: c.name('direction') { PAGE_DIRECTION[c.read_uint16] }, - n_direction: c.name('n_direction') { c.read_uint16 }, - n_recs: c.name('n_recs') { c.read_uint16 }, - max_trx_id: c.name('max_trx_id') { c.read_uint64 }, - level: c.name('level') { c.read_uint16 }, - index_id: c.name('index_id') { c.read_uint64 } + n_dir_slots: c.name("n_dir_slots") { c.read_uint16 }, + heap_top: c.name("heap_top") { c.read_uint16 }, + n_heap_format: c.name("n_heap_format") { c.read_uint16 }, + garbage_offset: c.name("garbage_offset") { c.read_uint16 }, + garbage_size: c.name("garbage_size") { c.read_uint16 }, + last_insert_offset: c.name("last_insert_offset") { c.read_uint16 }, + direction: c.name("direction") { PAGE_DIRECTION[c.read_uint16] }, + n_direction: c.name("n_direction") { c.read_uint16 }, + n_recs: c.name("n_recs") { c.read_uint16 }, + max_trx_id: c.name("max_trx_id") { c.read_uint64 }, + level: c.name("level") { c.read_uint16 }, + index_id: c.name("index_id") { c.read_uint64 } ) - index.n_heap = index.n_heap_format & (2**15 - 1) - index.format = (index.n_heap_format & 1 << 15).zero? ? :redundant : :compact + index.n_heap = index.n_heap_format & ((2**15) - 1) + index.format = (index.n_heap_format & (1 << 15)).zero? ? :redundant : :compact index end @@ -344,10 +344,10 @@ def ibuf_index? # Return the "fseg" header. def fseg_header - @fseg_header ||= cursor(pos_fseg_header).name('fseg') do |c| + @fseg_header ||= cursor(pos_fseg_header).name("fseg") do |c| FsegHeader.new( - leaf: c.name('fseg[leaf]') { Innodb::FsegEntry.get_inode(@space, c) }, - internal: c.name('fseg[internal]') { Innodb::FsegEntry.get_inode(@space, c) } + leaf: c.name("fseg[leaf]") { Innodb::FsegEntry.get_inode(@space, c) }, + internal: c.name("fseg[internal]") { Innodb::FsegEntry.get_inode(@space, c) } ) end end @@ -356,27 +356,27 @@ def fseg_header def record_header(cursor) origin = cursor.position header = RecordHeader.new - cursor.backward.name('header') do |c| + cursor.backward.name("header") do |c| case page_header.format when :compact # The "next" pointer is a relative offset from the current record. - header.next = c.name('next') { origin + c.read_sint16 } + header.next = c.name("next") { origin + c.read_sint16 } # Fields packed in a 16-bit integer (LSB first): # 3 bits for type # 13 bits for heap_number - bits1 = c.name('bits1') { c.read_uint16 } + bits1 = c.name("bits1") { c.read_uint16 } header.type = RECORD_TYPES[bits1 & 0x07] header.heap_number = (bits1 & 0xfff8) >> 3 when :redundant # The "next" pointer is an absolute offset within the page. - header.next = c.name('next') { c.read_uint16 } + header.next = c.name("next") { c.read_uint16 } # Fields packed in a 24-bit integer (LSB first): # 1 bit for offset_size (0 = 2 bytes, 1 = 1 byte) # 10 bits for n_fields # 13 bits for heap_number - bits1 = c.name('bits1') { c.read_uint24 } + bits1 = c.name("bits1") { c.read_uint24 } header.offset_size = (bits1 & 1).zero? ? 2 : 1 header.n_fields = (bits1 & (((1 << 10) - 1) << 1)) >> 1 header.heap_number = (bits1 & (((1 << 13) - 1) << 11)) >> 11 @@ -385,7 +385,7 @@ def record_header(cursor) # Fields packed in an 8-bit integer (LSB first): # 4 bits for n_owned # 4 bits for flags - bits2 = c.name('bits2') { c.read_uint8 } + bits2 = c.name("bits2") { c.read_uint8 } header.n_owned = bits2 & 0x0f header.info_flags = (bits2 & 0xf0) >> 4 @@ -410,8 +410,8 @@ def record_header_compact_additional(header, cursor) # bit vector indicating NULL fields and the length of each # non-NULL variable-length field. if record_format - header.nulls = cursor.name('nulls') { record_header_compact_null_bitmap(cursor) } - header.lengths, header.externs = cursor.name('lengths_and_externs') do + header.nulls = cursor.name("nulls") { record_header_compact_null_bitmap(cursor) } + header.lengths, header.externs = cursor.name("lengths_and_externs") do record_header_compact_variable_lengths_and_externs(cursor, header.nulls) end end @@ -453,7 +453,7 @@ def record_header_compact_variable_lengths_and_externs(cursor, nulls) # Two bytes are used only if the length exceeds 127 bytes and the # maximum length exceeds 255 bytes (or the field is a BLOB type). - if len > 127 && (f.blob? || f.data_type.width > 255) + if len > 127 && (f.blob? || f.data_type.length > 255) ext = (0x40 & len) != 0 len = ((len & 0x3f) << 8) + cursor.read_uint8 end @@ -528,7 +528,7 @@ def system_record(offset) offset: offset, header: header, next: header.next, - data: c.name('data') { c.read_bytes(size_mum_record) }, + data: c.name("data") { c.read_bytes(size_mum_record) }, length: c.position - offset ) ) @@ -546,8 +546,12 @@ def supremum end def make_record_describer - if space&.innodb_system && index_id - @record_describer = space.innodb_system.data_dictionary.record_describer_by_index_id(index_id) + if space.innodb_system.data_dictionary && index_id && !ibuf_index? + @record_describer = space.innodb_system + .data_dictionary + .indexes + .find(innodb_index_id: index_id) + .record_describer elsif space @record_describer = space.record_describer end @@ -570,7 +574,7 @@ def make_record_description # If this is a leaf page of the clustered index, read InnoDB's internal # fields, a transaction ID and roll pointer. if leaf? && fields[:type] == :clustered - [['DB_TRX_ID', :TRX_ID], ['DB_ROLL_PTR', :ROLL_PTR]].each do |name, type| + [["DB_TRX_ID", :TRX_ID], ["DB_ROLL_PTR", :ROLL_PTR]].each do |name, type| fields[:sys] << Innodb::Field.new(position.next, name, type, :NOT_NULL) end end @@ -638,16 +642,16 @@ def record(offset) # If this is a node (non-leaf) page, it will have a child page number # (or "node pointer") stored as the last field. - this_record.child_page_number = c.name('child_page_number') { c.read_uint32 } unless leaf? + this_record.child_page_number = c.name("child_page_number") { c.read_uint32 } unless leaf? this_record.length = c.position - offset # Add system field accessors for convenience. this_record.sys.each do |f| case f[:name] - when 'DB_TRX_ID' + when "DB_TRX_ID" this_record.transaction_id = f[:value] - when 'DB_ROLL_PTR' + when "DB_ROLL_PTR" this_record.roll_pointer = f[:value] end end @@ -659,7 +663,7 @@ def record(offset) # Return an array of row offsets for all entries in the page directory. def directory - @directory ||= cursor(pos_directory).backward.name('page_directory') do |c| + @directory ||= cursor(pos_directory).backward.name("page_directory") do |c| directory_slots.times.map { |n| c.name("slot[#{n}]") { c.read_uint16 } } end end @@ -684,7 +688,7 @@ def directory_slot_for_record(this_record) return slot if slot search_cursor = record_cursor(this_record.next) - raise 'Could not position cursor' unless search_cursor + raise "Could not position cursor" unless search_cursor while (rec = search_cursor.record) slot = record_directory_slot(rec) @@ -756,10 +760,10 @@ def prev_record Innodb::Stats.increment :page_record_cursor_prev_record slot = @page.directory_slot_for_record(@record) - raise 'Could not find slot for record' unless slot + raise "Could not find slot for record" unless slot search_cursor = @page.record_cursor(@page.directory[slot - 1]) - raise 'Could not position search cursor' unless search_cursor + raise "Could not position search cursor" unless search_cursor while (rec = search_cursor.record) && rec.offset != @record.offset next unless rec.next == @record.offset @@ -818,7 +822,7 @@ def max_record # order to do find the last record, we must create a cursor and walk # backwards one step. max_cursor = record_cursor(supremum.offset, :backward) - raise 'Could not position cursor' unless max_cursor + raise "Could not position cursor" unless max_cursor # Note the deliberate use of prev_record rather than record; we want # to skip over supremum itself. @@ -837,7 +841,7 @@ def linear_search_from_cursor(search_cursor, key) this_rec = search_cursor.record if Innodb.debug? - puts 'linear_search_from_cursor: page=%i, level=%i, start=(%s)' % [ + puts "linear_search_from_cursor: page=%i, level=%i, start=(%s)" % [ offset, level, this_rec && this_rec.key_string, @@ -850,7 +854,7 @@ def linear_search_from_cursor(search_cursor, key) Innodb::Stats.increment :linear_search_from_cursor_record_scans if Innodb.debug? - puts 'linear_search_from_cursor: page=%i, level=%i, current=(%s)' % [ + puts "linear_search_from_cursor: page=%i, level=%i, current=(%s)" % [ offset, level, this_rec.key_string, @@ -893,7 +897,7 @@ def binary_search_by_directory(dir, key) return unless rec if Innodb.debug? - puts 'binary_search_by_directory: page=%i, level=%i, dir.size=%i, dir[%i]=(%s)' % [ + puts "binary_search_by_directory: page=%i, level=%i, dir.size=%i, dir[%i]=(%s)" % [ offset, level, dir.size, @@ -995,39 +999,37 @@ def each_child_page nil end - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_index_header, length: size_index_header, name: :index_header, - info: 'Index Header' + info: "Index Header" ) yield Region.new( offset: pos_fseg_header, length: size_fseg_header, name: :fseg_header, - info: 'File Segment Header' + info: "File Segment Header" ) yield Region.new( offset: pos_infimum - 5, length: size_mum_record + 5, name: :infimum, - info: 'Infimum' + info: "Infimum" ) yield Region.new( offset: pos_supremum - 5, length: size_mum_record + 5, name: :supremum, - info: 'Supremum' + info: "Supremum" ) directory_slots.times do |n| @@ -1035,7 +1037,7 @@ def each_region offset: pos_directory - (n * 2), length: 2, name: :directory, - info: 'Page Directory' + info: "Page Directory" ) end @@ -1044,7 +1046,7 @@ def each_region offset: record.offset - record.header.length, length: record.length + record.header.length, name: :garbage, - info: 'Garbage' + info: "Garbage" ) end @@ -1053,14 +1055,14 @@ def each_region offset: record.offset - record.header.length, length: record.header.length, name: :record_header, - info: 'Record Header' + info: "Record Header" ) yield Region.new( offset: record.offset, length: record.length || 1, name: :record_data, - info: 'Record Data' + info: "Record Data" ) end @@ -1071,44 +1073,50 @@ def each_region def dump super - puts 'page header:' + puts "page header:" pp page_header puts - puts 'fseg header:' + puts "fseg header:" pp fseg_header puts - puts 'sizes:' - puts ' %-15s%5i' % ['header', header_space] - puts ' %-15s%5i' % ['trailer', trailer_space] - puts ' %-15s%5i' % ['directory', directory_space] - puts ' %-15s%5i' % ['free', free_space] - puts ' %-15s%5i' % ['used', used_space] - puts ' %-15s%5i' % ['record', record_space] - puts ' %-15s%5.2f' % ['per record', space_per_record] + puts "sizes:" + puts " %-15s%5i" % ["header", header_space] + puts " %-15s%5i" % ["trailer", trailer_space] + puts " %-15s%5i" % ["directory", directory_space] + puts " %-15s%5i" % ["free", free_space] + puts " %-15s%5i" % ["used", used_space] + puts " %-15s%5i" % ["record", record_space] + puts " %-15s%5.2f" % ["per record", space_per_record] puts - puts 'page directory:' + puts "page directory:" pp directory puts - puts 'system records:' + puts "system records:" pp infimum.record pp supremum.record puts - puts 'garbage records:' - each_garbage_record do |rec| - pp rec.record + if ibuf_index? + puts "(records not dumped due to this being an insert buffer index)" + elsif !record_describer + puts "(records not dumped due to missing record describer or data dictionary)" + else + puts "garbage records:" + each_garbage_record do |rec| + pp rec.record + puts + end puts - end - puts - puts 'records:' - each_record do |rec| - pp rec.record - puts + puts "records:" + each_record do |rec| + pp rec.record + puts + end end puts end diff --git a/lib/innodb/page/index_compressed.rb b/lib/innodb/page/index_compressed.rb index abc0e366..f0f44fcb 100644 --- a/lib/innodb/page/index_compressed.rb +++ b/lib/innodb/page/index_compressed.rb @@ -34,7 +34,7 @@ def uncompressed_columns_size def free_space free_space_start = size - size_fil_trailer - directory_space - (uncompressed_columns_size * (page_header.n_heap - 2)) - puts 'Free space start == %04x' % [offset * size + free_space_start] + puts "Free space start == %04x" % [(offset * size) + free_space_start] c = cursor(free_space_start).backward zero_bytes = 0 zero_bytes += 1 while c.read_uint8.zero? diff --git a/lib/innodb/page/inode.rb b/lib/innodb/page/inode.rb index 482dddc0..e98172bb 100644 --- a/lib/innodb/page/inode.rb +++ b/lib/innodb/page/inode.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'innodb/list' +require "innodb/list" # A specialized class for handling INODE pages, which contain index FSEG (file # segment) information. This allows all extents and individual pages assigned @@ -38,7 +38,7 @@ def size_inode_array # Return the list entry. def list_entry - cursor(pos_list_entry).name('list') { |c| Innodb::List.get_node(c) } + cursor(pos_list_entry).name("list") { |c| Innodb::List.get_node(c) } end # Return the "previous" address pointer from the list entry. This is used @@ -80,18 +80,16 @@ def each_allocated_inode end end - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_list_entry, length: size_list_entry, name: :list_entry, - info: 'Inode List Entry' + info: "Inode List Entry" ) each_inode do |inode| @@ -100,14 +98,14 @@ def each_region offset: inode.offset, length: Innodb::Inode::SIZE, name: :inode_used, - info: 'Inode (used)' + info: "Inode (used)" ) else yield Region.new( offset: inode.offset, length: Innodb::Inode::SIZE, name: :inode_free, - info: 'Inode (free)' + info: "Inode (free)" ) end end @@ -119,11 +117,11 @@ def each_region def dump super - puts 'list entry:' + puts "list entry:" pp list_entry puts - puts 'inodes:' + puts "inodes:" each_inode(&:dump) puts end diff --git a/lib/innodb/page/sdi.rb b/lib/innodb/page/sdi.rb new file mode 100644 index 00000000..28af1dc7 --- /dev/null +++ b/lib/innodb/page/sdi.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Innodb + class Page + # SDI (Serialized Dictionary Information) pages are actually INDEX pages and store data dictionary + # information in an InnoDB index structure in the typical way. However they use a fixed definition + # for the (unnamed except for in-memory as "SDI_") index, since there would, logically, + # be nowhere else to store the definition of this index. + class Sdi < Index + specialization_for :SDI + + # Every SDI index has the same structure, equivalent to the following SQL: + # + # CREATE TABLE `SDI_` ( + # `type` INT UNSIGNED NOT NULL, + # `id` BIGINT UNSIGNED NOT NULL, + # `uncompressed_len` INT UNSIGNED NOT NULL, + # `compressed_len` INT UNSIGNED NOT NULL, + # `data` LONGBLOB NOT NULL, + # PRIMARY KEY (`type`, `id`) + # ) + # + class RecordDescriber < Innodb::RecordDescriber + type :clustered + key "type", :INT, :UNSIGNED, :NOT_NULL + key "id", :BIGINT, :UNSIGNED, :NOT_NULL + row "uncompressed_len", :INT, :UNSIGNED, :NOT_NULL + row "compressed_len", :INT, :UNSIGNED, :NOT_NULL + row "data", :BLOB, :NOT_NULL + end + + def make_record_describer + RecordDescriber.new + end + end + end +end diff --git a/lib/innodb/page/sdi_blob.rb b/lib/innodb/page/sdi_blob.rb new file mode 100644 index 00000000..35d0252e --- /dev/null +++ b/lib/innodb/page/sdi_blob.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Innodb + class Page + # SDI (Serialized Dictionary Information) BLOB pages are actually BLOB pages with a different page + # type number but otherwise the same structure. + class SdiBlob < Blob + specialization_for :SDI_BLOB + end + end +end diff --git a/lib/innodb/page/sys.rb b/lib/innodb/page/sys.rb index 72d050c2..486b2ef7 100644 --- a/lib/innodb/page/sys.rb +++ b/lib/innodb/page/sys.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'innodb/page/sys_rseg_header' -require 'innodb/page/sys_data_dictionary_header' -require 'innodb/page/sys_ibuf_header' +require "innodb/page/sys_rseg_header" +require "innodb/page/sys_data_dictionary_header" +require "innodb/page/sys_ibuf_header" # Another layer of indirection for pages of type SYS, as they have multiple # uses within InnoDB. We'll override the self.handle method and check the diff --git a/lib/innodb/page/sys_data_dictionary_header.rb b/lib/innodb/page/sys_data_dictionary_header.rb index 9acfbcaa..1fe0601d 100644 --- a/lib/innodb/page/sys_data_dictionary_header.rb +++ b/lib/innodb/page/sys_data_dictionary_header.rb @@ -27,56 +27,54 @@ def size_data_dictionary_header # Parse the data dictionary header from the page. def data_dictionary_header - cursor(pos_data_dictionary_header).name('data_dictionary_header') do |c| + cursor(pos_data_dictionary_header).name("data_dictionary_header") do |c| Header.new( - max_row_id: c.name('max_row_id') { c.read_uint64 }, - max_table_id: c.name('max_table_id') { c.read_uint64 }, - max_index_id: c.name('max_index_id') { c.read_uint64 }, - max_space_id: c.name('max_space_id') { c.read_uint32 }, - unused_mix_id_low: c.name('unused_mix_id_low') { c.read_uint32 }, - indexes: c.name('indexes') do + max_row_id: c.name("max_row_id") { c.read_uint64 }, + max_table_id: c.name("max_table_id") { c.read_uint64 }, + max_index_id: c.name("max_index_id") { c.read_uint64 }, + max_space_id: c.name("max_space_id") { c.read_uint32 }, + unused_mix_id_low: c.name("unused_mix_id_low") { c.read_uint32 }, + indexes: c.name("indexes") do { - SYS_TABLES: c.name('SYS_TABLES') do + "SYS_TABLES" => c.name("SYS_TABLES") do { - PRIMARY: c.name('PRIMARY') { c.read_uint32 }, - ID: c.name('ID') { c.read_uint32 }, + "PRIMARY" => c.name("PRIMARY") { c.read_uint32 }, + "ID" => c.name("ID") { c.read_uint32 }, } end, - SYS_COLUMNS: c.name('SYS_COLUMNS') do + "SYS_COLUMNS" => c.name("SYS_COLUMNS") do { - PRIMARY: c.name('PRIMARY') { c.read_uint32 }, + "PRIMARY" => c.name("PRIMARY") { c.read_uint32 }, } end, - SYS_INDEXES: c.name('SYS_INDEXES') do + "SYS_INDEXES" => c.name("SYS_INDEXES") do { - PRIMARY: c.name('PRIMARY') { c.read_uint32 }, + "PRIMARY" => c.name("PRIMARY") { c.read_uint32 }, } end, - SYS_FIELDS: c.name('SYS_FIELDS') do + "SYS_FIELDS" => c.name("SYS_FIELDS") do { - PRIMARY: c.name('PRIMARY') { c.read_uint32 }, + "PRIMARY" => c.name("PRIMARY") { c.read_uint32 }, } end, } end, - unused_space: c.name('unused_space') { c.read_bytes(4) }, - fseg: c.name('fseg') { Innodb::FsegEntry.get_inode(@space, c) } + unused_space: c.name("unused_space") { c.read_bytes(4) }, + fseg: c.name("fseg") { Innodb::FsegEntry.get_inode(@space, c) } ) end end - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_data_dictionary_header, length: size_data_dictionary_header, name: :data_dictionary_header, - info: 'Data Dictionary Header' + info: "Data Dictionary Header" ) nil @@ -86,7 +84,7 @@ def dump super puts - puts 'data_dictionary header:' + puts "data_dictionary header:" pp data_dictionary_header end end diff --git a/lib/innodb/page/sys_ibuf_header.rb b/lib/innodb/page/sys_ibuf_header.rb index 18a0806a..791f9c0c 100644 --- a/lib/innodb/page/sys_ibuf_header.rb +++ b/lib/innodb/page/sys_ibuf_header.rb @@ -17,32 +17,30 @@ def size_ibuf_header end def ibuf_header - cursor(pos_ibuf_header).name('ibuf_header') do |c| + cursor(pos_ibuf_header).name("ibuf_header") do |c| Header.new( - fseg: c.name('fseg') { Innodb::FsegEntry.get_inode(space, c) } + fseg: c.name("fseg") { Innodb::FsegEntry.get_inode(space, c) } ) end end - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_ibuf_header, length: size_ibuf_header, name: :ibuf_header, - info: 'Insert Buffer Header' + info: "Insert Buffer Header" ) end def dump super - puts 'ibuf header:' + puts "ibuf header:" pp ibuf_header end end diff --git a/lib/innodb/page/sys_rseg_header.rb b/lib/innodb/page/sys_rseg_header.rb index a1c02caa..0321319b 100644 --- a/lib/innodb/page/sys_rseg_header.rb +++ b/lib/innodb/page/sys_rseg_header.rb @@ -34,14 +34,14 @@ def size_undo_segment_slot # Parse the rollback segment header from the page. def rseg_header - cursor(pos_rseg_header).name('rseg_header') do |c| + cursor(pos_rseg_header).name("rseg_header") do |c| Header.new( - max_size: c.name('max_size') { c.read_uint32 }, - history_size: c.name('history_size') { c.read_uint32 }, - history_list: c.name('history_list') do + max_size: c.name("max_size") { c.read_uint32 }, + history_size: c.name("history_size") { c.read_uint32 }, + history_list: c.name("history_list") do Innodb::List::History.new(@space, Innodb::List.get_base_node(c)) end, - fseg: c.name('fseg') { Innodb::FsegEntry.get_inode(@space, c) } + fseg: c.name("fseg") { Innodb::FsegEntry.get_inode(@space, c) } ) end end @@ -53,7 +53,7 @@ def history_list def each_undo_segment return enum_for(:each_undo_segment) unless block_given? - cursor(pos_undo_segment_array).name('undo_segment_array') do |c| + cursor(pos_undo_segment_array).name("undo_segment_array") do |c| (0...UNDO_SEGMENT_SLOTS).each do |slot| page_number = c.name("slot[#{slot}]") do Innodb::Page.maybe_undefined(c.read_uint32) @@ -63,18 +63,16 @@ def each_undo_segment end end - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_rseg_header, length: size_rseg_header, name: :rseg_header, - info: 'Rollback Segment Header' + info: "Rollback Segment Header" ) (0...UNDO_SEGMENT_SLOTS).each do |slot| @@ -82,7 +80,7 @@ def each_region offset: pos_undo_segment_array + (slot * size_undo_segment_slot), length: size_undo_segment_slot, name: :undo_segment_slot, - info: 'Undo Segment Slot' + info: "Undo Segment Slot" ) end @@ -93,11 +91,11 @@ def dump super puts - puts 'rollback segment header:' + puts "rollback segment header:" pp rseg_header puts - puts 'undo segment array:' + puts "undo segment array:" each_undo_segment do |slot, page_number| puts " #{slot}: #{page_number}" end diff --git a/lib/innodb/page/trx_sys.rb b/lib/innodb/page/trx_sys.rb index 9d653a3d..1864ba04 100644 --- a/lib/innodb/page/trx_sys.rb +++ b/lib/innodb/page/trx_sys.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" # A specialized class for TRX_SYS pages, which contain various information # about the transaction system within InnoDB. Only one TRX_SYS page exists in @@ -113,8 +113,8 @@ def rsegs_array(cursor) cursor.name("slot[#{n}]") do |c| slot = RsegSlot.new( offset: c.position, - space_id: c.name('space_id') { Innodb::Page.maybe_undefined(c.read_uint32) }, - page_number: c.name('page_number') { Innodb::Page.maybe_undefined(c.read_uint32) } + space_id: c.name("space_id") { Innodb::Page.maybe_undefined(c.read_uint32) }, + page_number: c.name("page_number") { Innodb::Page.maybe_undefined(c.read_uint32) } ) a << slot if slot.space_id && slot.page_number end @@ -124,20 +124,20 @@ def rsegs_array(cursor) # Read a MySQL binary log information structure from a given position. def mysql_log_info(cursor, offset) cursor.peek(offset) do |c| - magic_n = c.name('magic_n') { c.read_uint32 } == MYSQL_LOG_MAGIC_N + magic_n = c.name("magic_n") { c.read_uint32 } == MYSQL_LOG_MAGIC_N break unless magic_n MysqlLogInfo.new( magic_n: magic_n, - offset: c.name('offset') { c.read_uint64 }, - name: c.name('name') { c.read_bytes(100) } + offset: c.name("offset") { c.read_uint64 }, + name: c.name("name") { c.read_bytes(100) } ) end end # Read a single doublewrite buffer information structure from a given cursor. def doublewrite_page_info(cursor) - magic_n = cursor.name('magic_n') { cursor.read_uint32 } + magic_n = cursor.name("magic_n") { cursor.read_uint32 } DoublewritePageInfo.new( magic_n: magic_n, @@ -148,11 +148,11 @@ def doublewrite_page_info(cursor) # Read the overall doublewrite buffer structures def doublewrite_info(cursor) cursor.peek(pos_doublewrite_info) do |c_doublewrite| - c_doublewrite.name('doublewrite') do |c| + c_doublewrite.name("doublewrite") do |c| DoublewriteInfo.new( - fseg: c.name('fseg') { Innodb::FsegEntry.get_inode(@space, c) }, + fseg: c.name("fseg") { Innodb::FsegEntry.get_inode(@space, c) }, page_info: [0, 1].map { |n| c.name("group[#{n}]") { doublewrite_page_info(c) } }, - space_id_stored: (c.name('space_id_stored') { c.read_uint32 } == DOUBLEWRITE_SPACE_ID_STORED_MAGIC_N) + space_id_stored: (c.name("space_id_stored") { c.read_uint32 } == DOUBLEWRITE_SPACE_ID_STORED_MAGIC_N) ) end end @@ -160,13 +160,13 @@ def doublewrite_info(cursor) # Read the TRX_SYS headers and other information. def trx_sys - @trx_sys ||= cursor(pos_trx_sys_header).name('trx_sys') do |c| + @trx_sys ||= cursor(pos_trx_sys_header).name("trx_sys") do |c| Header.new( - trx_id: c.name('trx_id') { c.read_uint64 }, - fseg: c.name('fseg') { Innodb::FsegEntry.get_inode(@space, c) }, - rsegs: c.name('rsegs') { rsegs_array(c) }, - binary_log: c.name('binary_log') { mysql_log_info(c, pos_mysql_binary_log_info) }, - master_log: c.name('master_log') { mysql_log_info(c, pos_mysql_master_log_info) }, + trx_id: c.name("trx_id") { c.read_uint64 }, + fseg: c.name("fseg") { Innodb::FsegEntry.get_inode(@space, c) }, + rsegs: c.name("rsegs") { rsegs_array(c) }, + binary_log: c.name("binary_log") { mysql_log_info(c, pos_mysql_binary_log_info) }, + master_log: c.name("master_log") { mysql_log_info(c, pos_mysql_master_log_info) }, doublewrite: doublewrite_info(c) ) end @@ -179,18 +179,16 @@ def trx_sys def_delegator :trx_sys, :master_log def_delegator :trx_sys, :doublewrite - def each_region + def each_region(&block) return enum_for(:each_region) unless block_given? - super do |region| - yield region - end + super yield Region.new( offset: pos_trx_sys_header, length: size_trx_sys_header, name: :trx_sys_header, - info: 'Transaction System Header' + info: "Transaction System Header" ) rsegs.each do |rseg| @@ -198,7 +196,7 @@ def each_region offset: rseg[:offset], length: 4 + 4, name: :rseg, - info: 'Rollback Segment' + info: "Rollback Segment" ) end @@ -206,21 +204,21 @@ def each_region offset: pos_mysql_binary_log_info, length: size_mysql_log_info, name: :mysql_binary_log_info, - info: 'Binary Log Info' + info: "Binary Log Info" ) yield Region.new( offset: pos_mysql_master_log_info, length: size_mysql_log_info, name: :mysql_master_log_info, - info: 'Master Log Info' + info: "Master Log Info" ) yield Region.new( offset: pos_doublewrite_info, length: size_doublewrite_info, name: :doublewrite_info, - info: 'Double Write Buffer Info' + info: "Double Write Buffer Info" ) nil @@ -230,7 +228,7 @@ def each_region def dump super - puts 'trx_sys:' + puts "trx_sys:" pp trx_sys puts end diff --git a/lib/innodb/page/undo_log.rb b/lib/innodb/page/undo_log.rb index a6b45348..f35b8d22 100644 --- a/lib/innodb/page/undo_log.rb +++ b/lib/innodb/page/undo_log.rb @@ -55,12 +55,12 @@ def pos_undo_logs }.freeze def undo_page_header - @undo_page_header ||= cursor(pos_undo_page_header).name('undo_page_header') do |c| + @undo_page_header ||= cursor(pos_undo_page_header).name("undo_page_header") do |c| PageHeader.new( - type: c.name('type') { UNDO_PAGE_TYPES[c.read_uint16] }, - latest_log_record_offset: c.name('latest_log_record_offset') { c.read_uint16 }, - free_offset: c.name('free_offset') { c.read_uint16 }, - page_list_node: c.name('page_list') { Innodb::List.get_node(c) } + type: c.name("type") { UNDO_PAGE_TYPES[c.read_uint16] }, + latest_log_record_offset: c.name("latest_log_record_offset") { c.read_uint16 }, + free_offset: c.name("free_offset") { c.read_uint16 }, + page_list_node: c.name("page_list") { Innodb::List.get_node(c) } ) end end @@ -74,12 +74,12 @@ def next_address end def undo_segment_header - @undo_segment_header ||= cursor(pos_undo_segment_header).name('undo_segment_header') do |c| + @undo_segment_header ||= cursor(pos_undo_segment_header).name("undo_segment_header") do |c| SegmentHeader.new( - state: c.name('state') { UNDO_SEGMENT_STATES[c.read_uint16] }, - last_log_offset: c.name('last_log_offset') { c.read_uint16 }, - fseg: c.name('fseg') { Innodb::FsegEntry.get_inode(@space, c) }, - page_list: c.name('page_list') { Innodb::List::UndoPage.new(@space, Innodb::List.get_base_node(c)) } + state: c.name("state") { UNDO_SEGMENT_STATES[c.read_uint16] }, + last_log_offset: c.name("last_log_offset") { c.read_uint16 }, + fseg: c.name("fseg") { Innodb::FsegEntry.get_inode(@space, c) }, + page_list: c.name("page_list") { Innodb::List::UndoPage.new(@space, Innodb::List.get_base_node(c)) } ) end end @@ -92,15 +92,15 @@ def undo_log(pos) def dump super - puts 'undo page header:' + puts "undo page header:" pp undo_page_header puts - puts 'undo segment header:' + puts "undo segment header:" pp undo_segment_header puts - puts 'last undo log:' + puts "last undo log:" undo_log(undo_segment_header[:last_log_offset]).dump unless undo_segment_header[:last_log_offset].zero? puts end diff --git a/lib/innodb/record.rb b/lib/innodb/record.rb index 51424dc1..820c50f9 100644 --- a/lib/innodb/record.rb +++ b/lib/innodb/record.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" module Innodb class Record @@ -32,11 +32,21 @@ def initialize(page, record) def_delegator :header, :min_rec? def key_string - key&.map { |r| '%s=%s' % [r.name, r.value.inspect] }&.join(', ') + key&.map { |r| "%s=%s" % [r.name, r.value.inspect] }&.join(", ") end def row_string - row&.map { |r| '%s=%s' % [r.name, r.value.inspect] }&.join(', ') + row&.map { |r| "%s=%s" % [r.name, r.value.inspect] }&.join(", ") + end + + def full_value_with_externs_for_field(field) + blob_value = field.value + extern_page = field.extern && page.space.page(field.extern.page_number) + while extern_page + blob_value += extern_page.blob_data + extern_page = extern_page.next_blob_page + end + blob_value end def undo @@ -65,9 +75,9 @@ def each_undo_record def string if child_page_number - '(%s) → #%s' % [key_string, child_page_number] + "(%s) → #%s" % [key_string, child_page_number] else - '(%s) → (%s)' % [key_string, row_string] + "(%s) → (%s)" % [key_string, row_string] end end @@ -107,33 +117,33 @@ def compare_key(other_key) end def dump - puts 'Record at offset %i' % offset + puts "Record at offset %i" % offset puts - puts 'Header:' - puts ' %-20s: %i' % ['Next record offset', header.next] - puts ' %-20s: %i' % ['Heap number', header.heap_number] - puts ' %-20s: %s' % ['Type', header.type] - puts ' %-20s: %s' % ['Deleted', header.deleted?] - puts ' %-20s: %s' % ['Length', header.length] + puts "Header:" + puts " %-20s: %i" % ["Next record offset", header.next] + puts " %-20s: %i" % ["Heap number", header.heap_number] + puts " %-20s: %s" % ["Type", header.type] + puts " %-20s: %s" % ["Deleted", header.deleted?] + puts " %-20s: %s" % ["Length", header.length] puts if page.leaf? - puts 'System fields:' - puts ' Transaction ID: %s' % transaction_id - puts ' Roll Pointer:' - puts ' Undo Log: page %i, offset %i' % [ + puts "System fields:" + puts " Transaction ID: %s" % transaction_id + puts " Roll Pointer:" + puts " Undo Log: page %i, offset %i" % [ roll_pointer.undo_log.page, roll_pointer.undo_log.offset, ] - puts ' Rollback Segment ID: %i' % roll_pointer.rseg_id - puts ' Insert: %s' % roll_pointer.is_insert + puts " Rollback Segment ID: %i" % roll_pointer.rseg_id + puts " Insert: %s" % roll_pointer.is_insert puts end - puts 'Key fields:' + puts "Key fields:" key.each do |field| - puts ' %s: %s' % [ + puts " %s: %s" % [ field.name, field.value.inspect, ] @@ -141,15 +151,15 @@ def dump puts if page.leaf? - puts 'Non-key fields:' + puts "Non-key fields:" row.each do |field| - puts ' %s: %s' % [ + puts " %s: %s" % [ field.name, field.value.inspect, ] end else - puts 'Child page number: %i' % child_page_number + puts "Child page number: %i" % child_page_number end puts end diff --git a/lib/innodb/record_describer.rb b/lib/innodb/record_describer.rb index 4b7a5665..acbfa4d2 100644 --- a/lib/innodb/record_describer.rb +++ b/lib/innodb/record_describer.rb @@ -116,23 +116,5 @@ def field_names end names end - - def generate_class(name = "Describer_#{object_id}") - str = "class #{name}\n".dup - str << " type %s\n" % [ - description[:type].inspect, - ] - %i[key row].each do |group| - description[group].each do |item| - str << " %s %s, %s\n" % [ - group, - item[:name].inspect, - item[:type].map(&:inspect).join(', '), - ] - end - end - str << "end\n" - str - end end end diff --git a/lib/innodb/sdi.rb b/lib/innodb/sdi.rb new file mode 100644 index 00000000..25f7a9a0 --- /dev/null +++ b/lib/innodb/sdi.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Innodb + class Sdi + # A hash of page types to specialized classes to handle them. Normally + # subclasses will register themselves in this list. + @specialized_classes = {} + + class << self + attr_reader :specialized_classes + + def register_specialization(id, specialized_class) + @specialized_classes[id] = specialized_class + end + + def parse_semicolon_value_list(data) + data&.split(";").to_h { |x| x.split("=") } + end + + def parse_se_private_data(data) + parse_semicolon_value_list(data) + end + + def parse_options(data) + parse_semicolon_value_list(data) + end + end + + attr_reader :space + + def initialize(space) + @space = space + end + + def sdi_header + @sdi_header ||= space.page(0).sdi_header + end + + def version + sdi_header[:version] + end + + def root_page_number + sdi_header[:root_page_number] + end + + def valid? + root_page_number != 0 + end + + def index + return unless valid? + + space.index(root_page_number) + end + + def each_object + return unless valid? + return enum_for(:each_object) unless block_given? + + index.each_record do |record| + yield SdiObject.from_record(record) + end + + nil + end + + def tables + each_object.select { |o| o.is_a?(Table) } + end + + def tablespaces + each_object.select { |o| o.is_a?(Tablespace) } + end + end +end diff --git a/lib/innodb/sdi/sdi_object.rb b/lib/innodb/sdi/sdi_object.rb new file mode 100644 index 00000000..da70681b --- /dev/null +++ b/lib/innodb/sdi/sdi_object.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "zlib" +require "json" + +module Innodb + class Sdi + class SdiObject + class << self + def specialization_for(id) + Innodb::Sdi.register_specialization(id, self) + end + + def from_record(record) + type = record.key[0].value + id = record.key[1].value + # Ignore uncompressed_len = record.row[0].value + # Ignore compressed_len = record.row[1].value + data = record.full_value_with_externs_for_field(record.row[2]) + + type_handler = Innodb::Sdi.specialized_classes[type] + return unless type_handler + + parsed_data = JSON.parse(Zlib::Inflate.inflate(data)) + type_handler.new(type, id, parsed_data) + end + end + + attr_reader :type + attr_reader :id + attr_reader :data + + def initialize(type, id, data) + @type = type + @id = id + @data = data + end + + def mysqld_version_id + data["mysqld_version_id"] + end + + def dd_version + data["dd_version"] + end + + def sdi_version + data["sdi_version"] + end + + def dd_object_type + data["dd_object_type"] + end + + def dd_object + data["dd_object"] + end + + def name + dd_object["name"] + end + + def options + Innodb::Sdi.parse_options(data["options"]) + end + + def se_private_data + Innodb::Sdi.parse_se_private_data(dd_object["se_private_data"]) + end + end + end +end diff --git a/lib/innodb/sdi/table.rb b/lib/innodb/sdi/table.rb new file mode 100644 index 00000000..e9c068d2 --- /dev/null +++ b/lib/innodb/sdi/table.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Innodb + class Sdi + class Table < SdiObject + specialization_for 1 + + def name + format("%s/%s", dd_object["schema_ref"], dd_object["name"]) + end + + def columns + dd_object["columns"].map { |column| TableColumn.new(self, column) } + end + + def indexes + dd_object["indexes"].map { |index| TableIndex.new(self, index) } + end + + def space_id + indexes.first.space_id + end + + def find_index_by_name(name) + indexes.find { |index| index.name == name } + end + + def clustered_index + indexes.select { |i| %i[PRIMARY UNIQUE].include?(i.type) }.first + end + end + end +end diff --git a/lib/innodb/sdi/table_column.rb b/lib/innodb/sdi/table_column.rb new file mode 100644 index 00000000..c20595ce --- /dev/null +++ b/lib/innodb/sdi/table_column.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Innodb + class Sdi + class TableColumn + HIDDEN_STATUSES = { + VISIBLE: 1, + HIDDEN_SE: 2, + HIDDEN_SQL: 3, + HIDDEN_USER: 4, + }.freeze + + HIDDEN_STATUSES_BY_VALUE = HIDDEN_STATUSES.invert + + attr_reader :table + attr_reader :data + + def initialize(table, data) + @table = table + @data = data + end + + def name + data["name"] + end + + def type + data["type"] + end + + def nullable? + data["is_nullable"] + end + + def zerofill? + data["is_zerofill"] + end + + def unsigned? + data["is_unsigned"] + end + + def auto_increment? + data["is_auto_increment"] + end + + def virtual? + data["is_virtual"] + end + + def explicit_collation? + data["is_explicit_collation"] + end + + def hidden_status + HIDDEN_STATUSES_BY_VALUE[data["hidden"]] + end + + def visible? + hidden_status != :VISIBLE + end + + def hidden? + !visible? + end + + def system? + %w[DB_TRX_ID DB_ROLL_PTR].include?(name) + end + + def description + [ + data["column_type_utf8"].sub(/ unsigned$/, ""), + unsigned? ? :UNSIGNED : nil, + nullable? ? nil : :NOT_NULL, + ].compact + end + + def se_private_data + Innodb::Sdi.parse_se_private_data(data["se_private_data"]) + end + + def table_id + se_private_data["table_id"].to_i + end + end + end +end diff --git a/lib/innodb/sdi/table_index.rb b/lib/innodb/sdi/table_index.rb new file mode 100644 index 00000000..7cfbc8c2 --- /dev/null +++ b/lib/innodb/sdi/table_index.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Innodb + class Sdi + class TableIndex + TYPES = { + 1 => :PRIMARY, + 2 => :UNIQUE, + 3 => :MULTIPLE, + 4 => :FULLTEXT, + 5 => :SPATIAL, + }.freeze + + ALGORITHMS = { + 1 => :SE_SPECIFIC, + 2 => :BTREE, + 3 => :RTREE, + 4 => :HASH, + 5 => :FULLTEXT, + }.freeze + + attr_reader :table + attr_reader :data + + def initialize(table, data) + @table = table + @data = data + end + + def name + data["name"] + end + + def hidden? + data["hidden"] + end + + def visible? + data["is_visible"] + end + + def generated? + data["is_generated"] + end + + def elements + data["elements"] + .map { |element| TableIndexElement.new(self, element) } + .sort_by(&:ordinal_position) + end + + def se_private_data + Innodb::Sdi.parse_se_private_data(data["se_private_data"]) + end + + def innodb_index_id + se_private_data["id"].to_i + end + + def root_page_number + se_private_data["root"].to_i + end + + def innodb_space_id + se_private_data["space_id"].to_i + end + + def innodb_table_id + se_private_data["table_id"].to_i + end + + def trx_id + se_private_data["trx_id"].to_i + end + + def options + Innodb::Sdi.parse_options(data["options"]) + end + + def type + TYPES[data["type"]] + end + + def primary? + type == :PRIMARY + end + + def clustered? + table.clustered_index.innodb_index_id == innodb_index_id + end + end + end +end diff --git a/lib/innodb/sdi/table_index_element.rb b/lib/innodb/sdi/table_index_element.rb new file mode 100644 index 00000000..9bf9b085 --- /dev/null +++ b/lib/innodb/sdi/table_index_element.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Innodb + class Sdi + class TableIndexElement + attr_reader :index + attr_reader :data + + def initialize(index, data) + @index = index + @data = data + end + + def ordinal_position + data["ordinal_position"] + end + + def length + data["length"] + end + + def order + data["order"] + end + + def hidden? + data["hidden"] + end + + def visible? + !hidden? + end + + def key? + visible? + end + + def row? + hidden? + end + + def type + return :sys if %w[DB_TRX_ID DB_ROLL_PTR].include?(column.name) + return :key if key? + + :row + end + + def column_opx + data["column_opx"] + end + + def column + index.table.columns[column_opx] + end + end + end +end diff --git a/lib/innodb/sdi/tablespace.rb b/lib/innodb/sdi/tablespace.rb new file mode 100644 index 00000000..cc8d054c --- /dev/null +++ b/lib/innodb/sdi/tablespace.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Innodb + class Sdi + class Tablespace < SdiObject + specialization_for 2 + + def space_id + se_private_data["id"].to_i + end + end + end +end diff --git a/lib/innodb/sdi_data_dictionary.rb b/lib/innodb/sdi_data_dictionary.rb new file mode 100644 index 00000000..2cb2aaa8 --- /dev/null +++ b/lib/innodb/sdi_data_dictionary.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# A class representing MySQL's SDI-based data dictionary (used in MySQL +# versions starting in MySQL 8.0), which contains metadata about tables, +# columns, and indexes distributed in BLOBs of JSON stored in each InnoDB +# tablespace file. +module Innodb + class SdiDataDictionary + extend Forwardable + + attr_reader :innodb_system + + def_delegator :innodb_system, :data_dictionary + + def initialize(innodb_system) + @innodb_system = innodb_system + end + + def populate_data_dictionary_using_space_sdi(space) + sdi_tablespace = space.sdi.tablespaces.first + return unless sdi_tablespace + + innodb_space_id = sdi_tablespace.se_private_data["id"].to_i + new_tablespace = data_dictionary.tablespaces.make(name: sdi_tablespace.name, innodb_space_id: innodb_space_id) + + space.sdi.tables.each do |table| + new_table = data_dictionary.tables.make(name: table.name, tablespace: new_tablespace) + + table.columns.each do |column| + next if %w[DB_TRX_ID DB_ROLL_PTR].include?(column.name) + + new_table.columns.make(name: column.name, description: column.description, table: new_table) + end + + table.indexes.each do |index| + new_index = new_table.indexes.make( + name: index.name, + type: index.clustered? ? :clustered : :secondary, + table: new_table, + tablespace: new_tablespace, + root_page_number: index.root_page_number, + innodb_index_id: index.innodb_index_id + ) + + db_trx_id = Innodb::DataDictionary::Column.new( + name: "DB_TRX_ID", + description: %i[DB_TRX_ID], + table: new_table + ) + db_roll_ptr = Innodb::DataDictionary::Column.new( + name: "DB_ROLL_PTR", + description: %i[DB_ROLL_PTR], + table: new_table + ) + + index.elements.each do |element| + case element.column.name + when "DB_TRX_ID" + new_index.column_references.make(column: db_trx_id, usage: :sys, index: new_index) + when "DB_ROLL_PTR" + new_index.column_references.make(column: db_roll_ptr, usage: :sys, index: new_index) + else + new_index.column_references.make(column: new_table.columns.find(name: element.column.name), + usage: element.type, index: new_index) + end + end + end + end + + nil + end + + def populate_data_dictionary + data_dictionary.tablespaces.make(name: "innodb_system", innodb_space_id: 0) + + innodb_system.each_space do |space| + populate_data_dictionary_using_space_sdi(space) + end + end + end +end diff --git a/lib/innodb/space.rb b/lib/innodb/space.rb index fa91ab7f..d6ac35fb 100644 --- a/lib/innodb/space.rb +++ b/lib/innodb/space.rb @@ -48,16 +48,13 @@ def initialize(filename, offset) end def name - prefix = '' - prefix = File.basename(File.dirname(file.path)) + '/' if File.extname(file.path) == '.ibd' - - prefix + File.basename(file.path) + file.path end end # Open a space file, optionally providing the page size to use. Pages # that aren't 16 KiB may not be supported well. - def initialize(filenames) + def initialize(filenames, innodb_system: nil) filenames = [filenames] unless filenames.is_a?(Array) @data_files = [] @@ -73,7 +70,7 @@ def initialize(filenames) @compressed = fsp_flags.compressed @pages = (@size / @page_size) - @innodb_system = nil + @innodb_system = innodb_system @record_describer = nil end @@ -100,11 +97,11 @@ def initialize(filenames) # to do anything which could instantiate a BufferCursor so that we can use # this method in cursor initialization. def name - @name ||= @data_files.map(&:name).join(',') + @name ||= @data_files.map(&:name).join(",") end def inspect - '<%s file=%s, page_size=%i, pages=%i>' % [ + "<%s file=%s, page_size=%i, pages=%i>" % [ self.class.name, name.inspect, page_size, @@ -121,7 +118,7 @@ def raw_fsp_header_flags page_offset = BinData::Uint32be.read(read_at_offset(4, 4)).to_i page_type = BinData::Uint16be.read(read_at_offset(24, 2)).to_i unless page_offset.zero? && Innodb::Page::PAGE_TYPE_BY_VALUE[page_type] == :FSP_HDR - raise 'Something is very wrong; Page 0 does not seem to be type FSP_HDR; got page type %i but expected %i' % [ + raise "Something is very wrong; Page 0 does not seem to be type FSP_HDR; got page type %i but expected %i" % [ page_type, Innodb::Page::PAGE_TYPE[:FSP_HDR][:value], ] @@ -132,7 +129,7 @@ def raw_fsp_header_flags fil_space = BinData::Uint32be.read(read_at_offset(34, 4)).to_i fsp_space = BinData::Uint32be.read(read_at_offset(38, 4)).to_i unless fil_space == fsp_space - raise 'Something is very wrong; FIL and FSP header Space IDs do not match: FIL is %i but FSP is %i' % [ + raise "Something is very wrong; FIL and FSP header Space IDs do not match: FIL is %i but FSP is %i" % [ fil_space, fsp_space, ] @@ -276,6 +273,12 @@ def space_id fsp[:space_id] end + def checked_page_class!(page, expected_class) + return page if page.instance_of?(expected_class) + + raise "Page #{page.offset} is not the correct type, found: #{page.class}, expected: #{expected_class}" + end + # Return the page number for the space's TRX_SYS page. def page_trx_sys 5 @@ -283,13 +286,15 @@ def page_trx_sys # Get the Innodb::Page::TrxSys page for a system space. def trx_sys - page(page_trx_sys) if system_space? + raise "Transaction System is only available in system spaces" unless system_space? + + checked_page_class!(page(page_trx_sys), Innodb::Page::TrxSys) end def rseg_page?(page_number) return false unless trx_sys - !trx_sys.rsegs.select { |rseg| rseg.space_id.zero? && rseg.page_number == page_number }.empty? + trx_sys.rsegs.any? { |rseg| rseg.space_id.zero? && rseg.page_number == page_number } end # Return the page number for the space's SYS data dictionary header. @@ -299,7 +304,9 @@ def page_sys_data_dictionary # Get the Innodb::Page::SysDataDictionaryHeader page for a system space. def data_dictionary_page - page(page_sys_data_dictionary) if system_space? + raise "Data Dictionary is only available in system spaces" unless system_space? + + checked_page_class!(page(page_sys_data_dictionary), Innodb::Page::SysDataDictionaryHeader) end # Get an Innodb::List object for a specific list by list name. @@ -307,6 +314,14 @@ def list(name) fsp[name] if XDES_LISTS.include?(name) || INODE_LISTS.include?(name) end + def sdi + @sdi ||= Innodb::Sdi.new(self) + end + + def sdi? + sdi.valid? + end + # Get an Innodb::Index object for a specific index by root page number. def index(root_page_number, record_describer = nil) Innodb::Index.new(self, root_page_number, record_describer || @record_describer) @@ -316,10 +331,14 @@ def index(root_page_number, record_describer = nil) def each_index_root_page_number return enum_for(:each_index_root_page_number) unless block_given? - if innodb_system + if innodb_system&.data_dictionary&.populated? # Retrieve the index root page numbers from the data dictionary. - innodb_system.data_dictionary.each_index_by_space_id(space_id) do |record| - yield record['PAGE_NO'] + # TODO: An efficient way to handle this? + tablespace = innodb_system.data_dictionary.tablespaces.find(innodb_space_id: space_id) + innodb_system.data_dictionary.tables.each do |table| + table.indexes.by(tablespace: tablespace).each do |index| + yield index.root_page_number + end end else # Guess that the index root pages will be present starting at page 3, @@ -327,7 +346,7 @@ def each_index_root_page_number # for IBD files, if they haven't added indexes online. (3...@pages).each do |page_number| page = page(page_number) - yield page_number if page.type == :INDEX && page.root? + yield page_number if page.is_a?(Innodb::Page::Index) && page.root? end end @@ -355,14 +374,12 @@ def each_inode_list end # Iterate through Innodb::Inode objects in the space. - def each_inode + def each_inode(&block) return enum_for(:each_inode) unless block_given? - each_inode_list.each do |_name, list| + each_inode_list do |_name, list| list.each do |page| - page.each_allocated_inode do |inode| - yield inode - end + page.each_allocated_inode(&block) end end end @@ -374,14 +391,12 @@ def inode(fseg_id) end # Iterate through the page numbers in the doublewrite buffer. - def each_doublewrite_page_number + def each_doublewrite_page_number(&block) return nil unless system_space? return enum_for(:each_doublewrite_page_number) unless block_given? trx_sys.doublewrite[:page_info][0][:page_number].each do |start_page| - (start_page...(start_page + pages_per_extent)).each do |page_number| - yield page_number - end + (start_page...(start_page + pages_per_extent)).each(&block) end end @@ -414,12 +429,10 @@ def each_xdes_list end # An array of all FSP/XDES page numbers for the space. - def each_xdes_page_number + def each_xdes_page_number(&block) return enum_for(:each_xdes_page_number) unless block_given? - 0.step(pages - 1, pages_per_bookkeeping_page).each do |n| - yield n - end + 0.step(pages - 1, pages_per_bookkeeping_page).each(&block) end # Iterate through all extent descriptor pages, returning an Innodb::Page object @@ -450,7 +463,7 @@ def each_xdes # Iterate through all pages, yielding the page number, page object, # and page status. def each_page_status(start_page = 0) - return enum_for(:each_page_with_status, start_page) unless block_given? + return enum_for(:each_page_status, start_page) unless block_given? each_xdes do |xdes| xdes.each_page_status do |page_number, page_status| diff --git a/lib/innodb/stats.rb b/lib/innodb/stats.rb index 09857c0e..78e78f35 100644 --- a/lib/innodb/stats.rb +++ b/lib/innodb/stats.rb @@ -29,10 +29,10 @@ def self.reset # Print a simple report of collected statistics, optionally to the IO object # provided, or by default to STDOUT. - def self.print_report(io = STDOUT) - io.puts '%-50s%10s' % %w[Statistic Count] + def self.print_report(io = $stdout) + io.puts "%-50s%10s" % %w[Statistic Count] @data.sort.each do |name, count| - io.puts '%-50s%10i' % [ + io.puts "%-50s%10i" % [ name, count, ] diff --git a/lib/innodb/sys_data_dictionary.rb b/lib/innodb/sys_data_dictionary.rb new file mode 100644 index 00000000..c61d4a1e --- /dev/null +++ b/lib/innodb/sys_data_dictionary.rb @@ -0,0 +1,310 @@ +# frozen_string_literal: true + +require "forwardable" + +# A class representing InnoDB's SYS_* data dictionary (used in MySQL +# versions prior to MySQL 8.0), which contains metadata about tables, +# columns, and indexes in internal InnoDB tables named SYS_*. +module Innodb + class SysDataDictionary + # rubocop:disable Layout/ExtraSpacing + SYSTEM_TABLES = [ + { + name: "SYS_TABLES", + columns: [ + { name: "NAME", description: ["VARCHAR(100)", :NOT_NULL] }, + { name: "ID", description: %i[BIGINT UNSIGNED NOT_NULL] }, + { name: "N_COLS", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "TYPE", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "MIX_ID", description: %i[BIGINT UNSIGNED NOT_NULL] }, + { name: "MIX_LEN", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "CLUSTER_NAME", description: ["VARCHAR(100)", :NOT_NULL] }, + { name: "SPACE", description: %i[INT UNSIGNED NOT_NULL] }, + ], + indexes: [ + { name: "PRIMARY", type: :clustered, column_names: ["NAME"] }, + { name: "ID", type: :secondary, column_names: ["ID"] }, + ], + }, + { + name: "SYS_COLUMNS", + columns: [ + { name: "TABLE_ID", description: %i[BIGINT UNSIGNED NOT_NULL] }, + { name: "POS", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "NAME", description: ["VARCHAR(100)", :NOT_NULL] }, + { name: "MTYPE", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "PRTYPE", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "LEN", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "PREC", description: %i[INT UNSIGNED NOT_NULL] }, + ], + indexes: [ + { name: "PRIMARY", type: :clustered, column_names: %w[TABLE_ID POS] }, + ], + }, + { + name: "SYS_INDEXES", + columns: [ + { name: "TABLE_ID", description: %i[BIGINT UNSIGNED NOT_NULL] }, + { name: "ID", description: %i[BIGINT UNSIGNED NOT_NULL] }, + { name: "NAME", description: ["VARCHAR(100)", :NOT_NULL] }, + { name: "N_FIELDS", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "TYPE", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "SPACE", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "PAGE_NO", description: %i[INT UNSIGNED NOT_NULL] }, + ], + indexes: [ + { name: "PRIMARY", type: :clustered, column_names: %w[TABLE_ID ID] }, + ], + }, + { + name: "SYS_FIELDS", + columns: [ + { name: "INDEX_ID", description: %i[BIGINT UNSIGNED NOT_NULL] }, + { name: "POS", description: %i[INT UNSIGNED NOT_NULL] }, + { name: "COL_NAME", description: ["VARCHAR(100)", :NOT_NULL] }, + ], + indexes: [ + { name: "PRIMARY", type: :clustered, column_names: %w[INDEX_ID POS] }, + ], + }, + ].freeze + # rubocop:enable Layout/ExtraSpacing + + # A hash of InnoDB's internal type system to the values + # stored for each type. + COLUMN_MTYPE = { + VARCHAR: 1, + CHAR: 2, + FIXBINARY: 3, + BINARY: 4, + BLOB: 5, + INT: 6, + SYS_CHILD: 7, + SYS: 8, + FLOAT: 9, + DOUBLE: 10, + DECIMAL: 11, + VARMYSQL: 12, + MYSQL: 13, + }.freeze + + # A hash of COLUMN_MTYPE keys by value. + COLUMN_MTYPE_BY_VALUE = COLUMN_MTYPE.invert.freeze + + # A hash of InnoDB 'precise type' bitwise flags. + COLUMN_PRTYPE_FLAG = { + NOT_NULL: 256, + UNSIGNED: 512, + BINARY: 1024, + LONG_TRUE_VARCHAR: 4096, + }.freeze + + # A hash of COLUMN_PRTYPE keys by value. + COLUMN_PRTYPE_FLAG_BY_VALUE = COLUMN_PRTYPE_FLAG.invert.freeze + + # The bitmask to extract the MySQL internal type + # from the InnoDB 'precise type'. + COLUMN_PRTYPE_MYSQL_TYPE_MASK = 0xFF + + # A hash of InnoDB's index type flags. + INDEX_TYPE_FLAG = { + CLUSTERED: 1, + UNIQUE: 2, + UNIVERSAL: 4, + IBUF: 8, + CORRUPT: 16, + FTS: 32, + }.freeze + + # A hash of INDEX_TYPE_FLAG keys by value. + INDEX_TYPE_FLAG_BY_VALUE = INDEX_TYPE_FLAG.invert.freeze + + # Return the 'external' SQL type string (such as 'VARCHAR' or + # 'INT') given the stored mtype and prtype from the InnoDB + # data dictionary. Note that not all types are extractable + # into fully defined SQL types due to the lossy nature of + # the MySQL-to-InnoDB interface regarding types. + def self.mtype_prtype_to_type_string(mtype, prtype, len, prec) + mysql_type = prtype & COLUMN_PRTYPE_MYSQL_TYPE_MASK + internal_type = Innodb::MysqlType.by_mysql_field_type(mysql_type) + external_type = internal_type.handle_as + + case external_type + when :VARCHAR + # One-argument: length. + "%s(%i)" % [external_type, len] + when :FLOAT, :DOUBLE + # Two-argument: length and precision. + "%s(%i,%i)" % [external_type, len, prec] + when :CHAR + if COLUMN_MTYPE_BY_VALUE[mtype] == :MYSQL + # When the mtype is :MYSQL, the column is actually + # stored as VARCHAR despite being a CHAR. This is + # done for CHAR columns having multi-byte character + # sets in order to limit size. Note that such data + # are still space-padded to at least len. + "VARCHAR(%i)" % [len] + else + "CHAR(%i)" % [len] + end + when :DECIMAL + # The DECIMAL type is designated as DECIMAL(M,D) + # however the M and D definitions are not stored + # in the InnoDB data dictionary. We need to define + # the column as something which will extract the + # raw bytes in order to read the column, but we + # can't figure out the right decimal type. The + # len stored here is actually the on-disk storage + # size. + "CHAR(%i)" % [len] + else + external_type + end + end + + # Return a full data type given an mtype and prtype, such + # as ['VARCHAR(10)', :NOT_NULL] or [:INT, :UNSIGNED]. + def self.mtype_prtype_to_data_type(mtype, prtype, len, prec) + type = mtype_prtype_to_type_string(mtype, prtype, len, prec) + raise "Unsupported type (mtype #{mtype}, prtype #{prtype})" unless type + + data_type = [type] + data_type << :NOT_NULL if prtype & COLUMN_PRTYPE_FLAG[:NOT_NULL] != 0 + data_type << :UNSIGNED if prtype & COLUMN_PRTYPE_FLAG[:UNSIGNED] != 0 + + data_type + end + + extend Forwardable + + attr_reader :innodb_system + + def_delegator :innodb_system, :data_dictionary + + def initialize(innodb_system) + @innodb_system = innodb_system + end + + private + + # A helper method to reach inside the system space and retrieve + # the data dictionary index locations from the data dictionary + # header. + def _data_dictionary_indexes + innodb_system.system_space.data_dictionary_page.data_dictionary_header[:indexes] + end + + def _populate_index_with_system_and_non_key_columns(new_table, new_index) + if new_index.type == :clustered + db_trx_id = Innodb::DataDictionary::Column.new(name: "DB_TRX_ID", description: %i[DB_TRX_ID], table: new_table) + new_index.column_references.make(column: db_trx_id, usage: :sys, index: new_index) + db_roll_ptr = Innodb::DataDictionary::Column.new(name: "DB_ROLL_PTR", description: %i[DB_ROLL_PTR], + table: new_table) + new_index.column_references.make(column: db_roll_ptr, usage: :sys, index: new_index) + + new_table.columns.each do |column| + unless new_index.column_references.find(name: column.name) + new_index.column_references.make(column: column, usage: :row, + index: new_index) + end + end + else + clustered_index = new_table.indexes.find(type: :clustered) + clustered_index.column_references.each do |column| + new_index.column_references.make(column: column, usage: :row, index: new_index) unless column.usage == :sys + end + end + end + + public + + def populate_data_dictionary_with_system_table_definitions + system_tablespace = data_dictionary.tablespaces.make(name: "innodb_system", innodb_space_id: 0) + + SYSTEM_TABLES.each do |table| + new_table = data_dictionary.tables.make(name: table[:name], tablespace: system_tablespace) + + table[:columns].each do |column| + new_table.columns.make(name: column[:name], description: column[:description], table: new_table) + end + + table[:indexes].each do |index| + new_index = new_table.indexes.make( + name: index[:name], + type: index[:type], + table: new_table, + tablespace: system_tablespace, + root_page_number: _data_dictionary_indexes[table[:name]][index[:name]] + ) + index[:column_names].each do |column_name| + new_index.column_references.make( + column: new_table.columns.find(name: column_name), + usage: :key, + index: new_index + ) + end + _populate_index_with_system_and_non_key_columns(new_table, new_index) + end + end + + nil + end + + def populate_data_dictionary_from_system_tables + # Read the entire contents of all tables for efficiency sake, since we'll need to do many sub-iterations + # below and don't want to re-parse the records every time. + sys_tables = innodb_system.index_by_name("SYS_TABLES", "PRIMARY").each_record.map(&:fields) + sys_columns = innodb_system.index_by_name("SYS_COLUMNS", "PRIMARY").each_record.map(&:fields) + sys_indexes = innodb_system.index_by_name("SYS_INDEXES", "PRIMARY").each_record.map(&:fields) + sys_fields = innodb_system.index_by_name("SYS_FIELDS", "PRIMARY").each_record.map(&:fields) + + sys_tables.each do |table_record| + tablespace = data_dictionary.tablespaces.find(innodb_space_id: table_record["SPACE"]) + tablespace ||= data_dictionary.tablespaces.make(name: table_record["NAME"], + innodb_space_id: table_record["SPACE"]) + + new_table = data_dictionary.tables.make(name: table_record["NAME"], tablespace: tablespace, + innodb_table_id: table_record["ID"]) + + sys_columns.select { |r| r["TABLE_ID"] == table_record["ID"] }.each do |column_record| + description = self.class.mtype_prtype_to_data_type( + column_record["MTYPE"], + column_record["PRTYPE"], + column_record["LEN"], + column_record["PREC"] + ) + new_table.columns.make(name: column_record["NAME"], description: description, table: new_table) + end + + sys_indexes.select { |r| r["TABLE_ID"] == table_record["ID"] }.each do |index_record| + raise "Different tablespace between table and index" unless table_record["SPACE"] == index_record["SPACE"] + + type = index_record["TYPE"] & INDEX_TYPE_FLAG[:CLUSTERED] ? :clustered : :secondary + new_index = new_table.indexes.make( + name: index_record["NAME"], + type: type, + table: new_table, + tablespace: tablespace, + root_page_number: index_record["PAGE_NO"], + innodb_index_id: index_record["ID"] + ) + sys_fields.select { |r| r["INDEX_ID"] == index_record["ID"] }.each do |field_record| + new_index.column_references.make(column: new_table.columns.find(name: field_record["COL_NAME"]), + usage: :key, index: new_index) + end + + _populate_index_with_system_and_non_key_columns(new_table, new_index) + end + end + + nil + end + + def populate_data_dictionary + populate_data_dictionary_with_system_table_definitions + populate_data_dictionary_from_system_tables + + nil + end + end +end diff --git a/lib/innodb/system.rb b/lib/innodb/system.rb index 034ec368..6039da85 100644 --- a/lib/innodb/system.rb +++ b/lib/innodb/system.rb @@ -19,13 +19,18 @@ class System # The space ID of the system space, always 0. SYSTEM_SPACE_ID = 0 - def initialize(arg) + # The space ID of the mysql.ibd space, always 4294967294 (2**32-2). + MYSQL_SPACE_ID = 4_294_967_294 + + def initialize(arg, data_directory: nil) + @data_dictionary = Innodb::DataDictionary.new + if arg.is_a?(Array) && arg.size > 1 data_filenames = arg else arg = arg.first if arg.is_a?(Array) if File.directory?(arg) - data_filenames = Dir.glob(arg + '/ibdata?').sort + data_filenames = Dir.glob("#{arg}/ibdata?").sort raise "Couldn't find any ibdata files in #{arg}" if data_filenames.empty? else data_filenames = [arg] @@ -35,12 +40,29 @@ def initialize(arg) @spaces = {} @orphans = [] @config = { - datadir: File.dirname(data_filenames.first), + data_directory: data_directory || File.dirname(data_filenames.first), } add_space_file(data_filenames) - @data_dictionary = Innodb::DataDictionary.new(system_space) + add_mysql_space_file + add_all_ibd_files + + @internal_data_dictionary = if system_space.page(0).prev > 80_000 # ugh + Innodb::SdiDataDictionary.new(self) + else + Innodb::SysDataDictionary.new(self) + end + @internal_data_dictionary.populate_data_dictionary + data_dictionary.refresh + + data_dictionary.tables.each do |table| + add_table(table.name) unless spaces[table.tablespace.innodb_space_id] + end + end + + def data_directory + config[:data_directory] end # A helper to get the system space. @@ -48,18 +70,21 @@ def system_space spaces[SYSTEM_SPACE_ID] end + def mysql_space + spaces[MYSQL_SPACE_ID] + end + # Add an already-constructed Innodb::Space object. def add_space(space) - raise 'Object was not an Innodb::Space' unless space.is_a?(Innodb::Space) + raise "Object was not an Innodb::Space" unless space.is_a?(Innodb::Space) - spaces[space.space_id.to_i] = space + spaces[space.space_id] = space end # Add a space by filename. def add_space_file(space_filenames) - space = Innodb::Space.new(space_filenames) - space.innodb_system = self - add_space(space) + space = Innodb::Space.new(space_filenames, innodb_system: self) + add_space(space) unless spaces[space.space_id] end # Add an orphaned space. @@ -70,7 +95,8 @@ def add_space_orphan(space_file) # Add a space by table name, constructing an appropriate filename # from the provided table name. def add_table(table_name) - space_file = '%s/%s.ibd' % [config[:datadir], table_name] + space_file = File.join(config[:data_directory], format("%s.ibd", table_name)) + if File.exist?(space_file) add_space_file(space_file) else @@ -83,126 +109,75 @@ def add_table(table_name) def space(space_id) return spaces[space_id] if spaces[space_id] - unless (table_record = data_dictionary.table_by_space_id(space_id)) + unless (table = data_dictionary.tables.find(innodb_space_id: space_id)) raise "Table with space ID #{space_id} not found" end - add_table(table_record['NAME']) + add_table(table.name) spaces[space_id] end - # Return an Innodb::Space object by table name. def space_by_table_name(table_name) - unless (table_record = data_dictionary.table_by_name(table_name)) - raise "Table #{table_name} not found" - end - - return if table_record['SPACE'].zero? + space_id = data_dictionary.tables.find(name: table_name)&.tablespace&.innodb_space_id - space(table_record['SPACE']) + spaces[space_id] if space_id end - # Iterate through all table names. - def each_table_name - return enum_for(:each_table_name) unless block_given? - - data_dictionary.each_table do |record| - yield record['NAME'] - end - - nil + def add_mysql_space_file + mysql_ibd = File.join(data_directory, "mysql.ibd") + add_space_file(mysql_ibd) if File.exist?(mysql_ibd) end - # Iterate throught all orphaned spaces. - def each_orphan - return enum_for(:each_orphan) unless block_given? + # Iterate through all table names. + def each_ibd_file_name(&block) + return enum_for(:each_ibd_file_name) unless block_given? - orphans.each do |space_name| - yield space_name - end + Dir.glob(File.join(data_directory, "**/*.ibd")) + .map { |f| f.sub(File.join(data_directory, "/"), "") }.each(&block) nil end - # Iterate through all column names by table name. - def each_column_name_by_table_name(table_name) - return enum_for(:each_column_name_by_table_name, table_name) unless block_given? - - data_dictionary.each_column_by_table_name(table_name) do |record| - yield record['NAME'] + def add_all_ibd_files + each_ibd_file_name do |file_name| + add_space_file(File.join(data_directory, file_name)) end nil end - # Iterate through all index names by table name. - def each_index_name_by_table_name(table_name) - return enum_for(:each_index_name_by_table_name, table_name) unless block_given? + def each_space(&block) + return enum_for(:each_space) unless block_given? - data_dictionary.each_index_by_table_name(table_name) do |record| - yield record['NAME'] - end + spaces.each_value(&block) nil end - # Iterate through all field names in a given index by table name - # and index name. - def each_index_field_name_by_index_name(table_name, index_name) - return enum_for(:each_index_field_name_by_index_name, table_name, index_name) unless block_given? + # Iterate throught all orphaned spaces. + def each_orphan(&block) + return enum_for(:each_orphan) unless block_given? - data_dictionary.each_field_by_index_name(table_name, index_name) do |record| - yield record['COL_NAME'] - end + orphans.each(&block) nil end - # Return the table name given a table ID. - def table_name_by_id(table_id) - data_dictionary.table_by_id(table_id).fetch('NAME', nil) - end - - # Return the index name given an index ID. - def index_name_by_id(index_id) - data_dictionary.index_by_id(index_id).fetch('NAME', nil) - end - - # Return the clustered index name given a table name. - def clustered_index_by_table_name(table_name) - data_dictionary.clustered_index_name_by_table_name(table_name) - end - - # Return an array of the table name and index name given an index ID. - def table_and_index_name_by_id(index_id) - if (dd_index = data_dictionary.data_dictionary_index_ids[index_id]) - # This is a data dictionary index, which won't be found in the data - # dictionary itself. - [dd_index[:table], dd_index[:index]] - elsif (index_record = data_dictionary.index_by_id(index_id)) - # This is a system or user index. - [table_name_by_id(index_record['TABLE_ID']), index_record['NAME']] - end - end - # Return an Innodb::Index object given a table name and index name. def index_by_name(table_name, index_name) - index_record = data_dictionary.index_by_name(table_name, index_name) - - index_space = space(index_record['SPACE']) - describer = data_dictionary.record_describer_by_index_name(table_name, index_name) - index = index_space.index(index_record['PAGE_NO'], describer) + table = data_dictionary.tables.find(name: table_name) + index = table.indexes.find(name: index_name) - index + space(index.tablespace.innodb_space_id).index(index.root_page_number, index.record_describer) end # Return the clustered index given a table ID. def clustered_index_by_table_id(table_id) - table_name = table_name_by_id(table_id) - return unless table_name + table = data_dictionary.tables.find(innodb_table_id: table_id) + return unless table - index_by_name(table_name, clustered_index_by_table_name(table_name)) + index_by_name(table.name, table.clustered_index.name) end def history diff --git a/lib/innodb/undo_log.rb b/lib/innodb/undo_log.rb index 2e71ba95..f6a95351 100644 --- a/lib/innodb/undo_log.rb +++ b/lib/innodb/undo_log.rb @@ -42,27 +42,27 @@ def size_header end def header - @header ||= page.cursor(@position).name('header') do |c| + @header ||= page.cursor(@position).name("header") do |c| header = Header.new( - trx_id: c.name('trx_id') { c.read_uint64 }, - trx_no: c.name('trx_no') { c.read_uint64 }, - delete_mark_flag: c.name('delete_mark_flag') { (c.read_uint16 != 0) }, - log_start_offset: c.name('log_start_offset') { c.read_uint16 }, - xid_flag: c.name('xid_flag') { (c.read_uint8 != 0) }, - ddl_flag: c.name('ddl_flag') { (c.read_uint8 != 0) }, - ddl_table_id: c.name('ddl_table_id') { c.read_uint64 }, - next_log_offset: c.name('next_log_offset') { c.read_uint16 }, - prev_log_offset: c.name('prev_log_offset') { c.read_uint16 }, - history_list_node: c.name('history_list_node') { Innodb::List.get_node(c) } + trx_id: c.name("trx_id") { c.read_uint64 }, + trx_no: c.name("trx_no") { c.read_uint64 }, + delete_mark_flag: c.name("delete_mark_flag") { (c.read_uint16 != 0) }, + log_start_offset: c.name("log_start_offset") { c.read_uint16 }, + xid_flag: c.name("xid_flag") { (c.read_uint8 != 0) }, + ddl_flag: c.name("ddl_flag") { (c.read_uint8 != 0) }, + ddl_table_id: c.name("ddl_table_id") { c.read_uint64 }, + next_log_offset: c.name("next_log_offset") { c.read_uint16 }, + prev_log_offset: c.name("prev_log_offset") { c.read_uint16 }, + history_list_node: c.name("history_list_node") { Innodb::List.get_node(c) } ) if header.xid_flag - header.xid = c.name('xid') do + header.xid = c.name("xid") do HeaderXid.new( - format: c.name('format') { c.read_uint32 }, - trid_len: c.name('trid_len') { c.read_uint32 }, - bqual_len: c.name('bqual_len') { c.read_uint32 }, - data: c.name('data') { c.read_bytes(128) } + format: c.name("format") { c.read_uint32 }, + trid_len: c.name("trid_len") { c.read_uint32 }, + bqual_len: c.name("bqual_len") { c.read_uint32 }, + data: c.name("data") { c.read_bytes(128) } ) end end @@ -100,7 +100,7 @@ def initialize(undo_log, offset, direction = :forward) when :min @undo_record = @undo_log.min_undo_record when :max - raise 'Not implemented' + raise "Not implemented" else @undo_record = @undo_log.undo_record(offset) end @@ -148,7 +148,7 @@ def first_undo_record_cursor end def dump - puts 'header:' + puts "header:" pp header puts end diff --git a/lib/innodb/undo_record.rb b/lib/innodb/undo_record.rb index eceb1fc3..29f389d9 100644 --- a/lib/innodb/undo_record.rb +++ b/lib/innodb/undo_record.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" # A single undo log record. module Innodb @@ -111,13 +111,13 @@ def cursor(position) EXTERN_FLAG = 0x80 def header - @header ||= cursor(pos_header).name('header') do |c| + @header ||= cursor(pos_header).name("header") do |c| header = Header.new( - prev: c.name('prev') { c.read_uint16 }, - next: c.name('next') { c.read_uint16 } + prev: c.name("prev") { c.read_uint16 }, + next: c.name("next") { c.read_uint16 } ) - info = c.name('info') { c.read_uint8 } + info = c.name("info") { c.read_uint8 } cmpl = (info & COMPILATION_INFO_MASK) >> COMPILATION_INFO_SHIFT header.type = TYPE[info & TYPE_MASK] header.extern_flag = (info & EXTERN_FLAG) != 0 @@ -156,19 +156,19 @@ def record_size end def read_record - cursor(pos_record).name('record') do |c| + cursor(pos_record).name("record") do |c| this_record = Record.new( page: undo_page.offset, offset: position, header: header, - undo_no: c.name('undo_no') { c.read_imc_uint64 }, - table_id: c.name('table_id') { c.read_imc_uint64 } + undo_no: c.name("undo_no") { c.read_imc_uint64 }, + table_id: c.name("table_id") { c.read_imc_uint64 } ) if previous_version? - this_record.info_bits = c.name('info_bits') { c.read_uint8 } - this_record.trx_id = c.name('trx_id') { c.read_ic_uint64 } - this_record.roll_ptr = c.name('roll_ptr') do + this_record.info_bits = c.name("info_bits") { c.read_uint8 } + this_record.trx_id = c.name("trx_id") { c.read_ic_uint64 } + this_record.roll_ptr = c.name("roll_ptr") do Innodb::DataType::RollPointerType.parse_roll_pointer(c.read_ic_uint64) end end @@ -187,7 +187,7 @@ def read_record def read_record_fields(this_record, cursor) this_record.key = [] index_page.record_format[:key].each do |field| - length = cursor.name('field_length') { cursor.read_ic_uint32 } + length = cursor.name("field_length") { cursor.read_ic_uint32 } value = cursor.name(field.name) { field.value_by_length(cursor, length) } this_record.key[field.position] = Field.new(name: field.name, type: field.data_type.name, value: value) @@ -195,7 +195,7 @@ def read_record_fields(this_record, cursor) return unless previous_version? - field_count = cursor.name('field_count') { cursor.read_ic_uint32 } + field_count = cursor.name("field_count") { cursor.read_ic_uint32 } this_record.row = Array.new(index_page.record_format[:row].size) field_count.times do field_number = cursor.name("field_number[#{field_count}]") { cursor.read_ic_uint32 } @@ -210,7 +210,7 @@ def read_record_fields(this_record, cursor) raise "Unknown field #{field_number}" unless field - length = cursor.name('field_length') { cursor.read_ic_uint32 } + length = cursor.name("field_length") { cursor.read_ic_uint32 } value = cursor.name(field.name) { field.value_by_length(cursor, length) } this_record.row[field_index] = Field.new(name: field.name, type: field.data_type.name, value: value) @@ -230,7 +230,7 @@ def undo_record def_delegator :undo_record, :offset def key_string - key&.map { |r| '%s=%s' % [r[:name], r[:value].inspect] }&.join(', ') + key&.map { |r| "%s=%s" % [r[:name], r[:value].inspect] }&.join(", ") end def row @@ -238,11 +238,11 @@ def row end def row_string - row&.reject(&:nil?)&.map { |r| r && '%s=%s' % [r[:name], r[:value].inspect] }&.join(', ') + row&.compact&.map { |r| r && format("%s=%s", r[:name], r[:value].inspect) }&.join(", ") end def string - '(%s) → (%s)' % [key_string, row_string] + "(%s) → (%s)" % [key_string, row_string] end # Find the previous row version by following the roll_ptr from one undo @@ -258,7 +258,7 @@ def prev_by_history older_undo_page = @undo_page.space.page(undo_log[:page]) # The page was probably re-used for something else. - return unless older_undo_page&.is_a?(Innodb::Page::UndoLog) + return unless older_undo_page.is_a?(Innodb::Page::UndoLog) older_undo_record = new_subordinate(older_undo_page, undo_log[:offset]) @@ -272,39 +272,39 @@ def prev_by_history end def dump - puts 'Undo record at offset %i' % offset + puts "Undo record at offset %i" % offset puts - puts 'Header:' - puts ' %-25s: %i' % ['Previous record offset', header[:prev]] - puts ' %-25s: %i' % ['Next record offset', header[:next]] - puts ' %-25s: %s' % ['Type', header[:type]] + puts "Header:" + puts " %-25s: %i" % ["Previous record offset", header[:prev]] + puts " %-25s: %i" % ["Next record offset", header[:next]] + puts " %-25s: %s" % ["Type", header[:type]] puts - puts 'System fields:' - puts ' Transaction ID: %s' % trx_id - puts ' Roll Pointer:' - puts ' Undo Log: page %i, offset %i' % [ + puts "System fields:" + puts " Transaction ID: %s" % trx_id + puts " Roll Pointer:" + puts " Undo Log: page %i, offset %i" % [ roll_ptr[:undo_log][:page], roll_ptr[:undo_log][:offset], ] - puts ' Rollback Segment ID: %i' % roll_ptr[:rseg_id] + puts " Rollback Segment ID: %i" % roll_ptr[:rseg_id] puts - puts 'Key fields:' + puts "Key fields:" key.each do |field| - puts ' %s: %s' % [ + puts " %s: %s" % [ field[:name], field[:value].inspect, ] end puts - puts 'Non-key fields:' + puts "Non-key fields:" row.each do |field| next unless field - puts ' %s: %s' % [ + puts " %s: %s" % [ field[:name], field[:value].inspect, ] diff --git a/lib/innodb/util/buffer_cursor.rb b/lib/innodb/util/buffer_cursor.rb index ba90329f..29ef30a4 100644 --- a/lib/innodb/util/buffer_cursor.rb +++ b/lib/innodb/util/buffer_cursor.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'bindata' +require "bindata" # A cursor to walk through data structures to read fields. The cursor can move # forwards, backwards, is seekable, and supports peeking without moving the # cursor. The BinData module is used for interpreting bytes as desired. class BufferCursor - VERSION = '0.9.0' + VERSION = "0.9.0" # An entry in a stack of cursors. The cursor position, direction, and # name array are each attributes of the current cursor stack and are @@ -25,7 +25,7 @@ def initialize(cursor, position = 0, direction = :forward, name = nil) end def inspect - '<%s direction=%s position=%s>' % [ + "<%s direction=%s position=%s>" % [ self.class.name, @direction.inspect, @position, @@ -40,7 +40,7 @@ def dup @global_tracing = false # Enable tracing for all BufferCursor objects globally. - def self.trace!(arg = true) + def self.trace!(arg = true) # rubocop:disable Style/OptionalBooleanParameter @global_tracing = arg end @@ -55,18 +55,18 @@ def initialize(buffer, position) trace false trace_with :print_trace - trace_to STDOUT + trace_to $stdout end def inspect - '<%s size=%i current=%s>' % [ + "<%s size=%i current=%s>" % [ self.class.name, @buffer.size, current.inspect, ] end - def trace(arg = true) + def trace(arg = true) # rubocop:disable Style/OptionalBooleanParameter @instance_tracing = arg self @@ -76,12 +76,12 @@ def trace(arg = true) # position, raw byte buffer, and array of names. def print_trace(_cursor, position, bytes, name) slice_size = 16 - bytes.each_slice(slice_size).each_with_index do |slice_bytes, slice_count| - @trace_io.puts '%06i %s %-32s %s' % [ + bytes.each_slice(slice_size).with_index do |slice_bytes, slice_count| + @trace_io.puts "%06i %s %-32s %s" % [ position + (slice_count * slice_size), - direction == :backward ? '←' : '→', - slice_bytes.map { |n| '%02x' % n }.join, - slice_count.zero? ? name.join('.') : '↵', + direction == :backward ? "←" : "→", + slice_bytes.map { |n| "%02x" % n }.join, + slice_count.zero? ? name.join(".") : "↵", ] end end @@ -96,9 +96,9 @@ def trace_to(file) def trace_with(arg = nil) if arg.nil? @trace_proc = nil - elsif arg.class == Proc + elsif arg.instance_of?(Proc) @trace_proc = arg - elsif arg.class == Symbol + elsif arg.instance_of?(Symbol) @trace_proc = ->(cursor, position, bytes, name) { send(arg, cursor, position, bytes, name) } else raise "Don't know how to trace with #{arg}" @@ -133,7 +133,7 @@ def pop_name def name(name_arg = nil) return current.name if name_arg.nil? - raise 'No block given' unless block_given? + raise "No block given" unless block_given? current.name.push name_arg ret = yield(self) @@ -189,7 +189,7 @@ def push(position = nil) # Restore the last cursor position. def pop - raise 'No cursors to pop' unless @stack.size > 1 + raise "No cursors to pop" unless @stack.size > 1 @stack.pop @@ -200,7 +200,7 @@ def pop # the block returns. Return the block's return value after restoring the # cursor. Optionally seek to provided position before executing block. def peek(position = nil) - raise 'No block given' unless block_given? + raise "No block given" unless block_given? push(position) result = yield(self) @@ -237,19 +237,17 @@ def read_string(length) end # Iterate through length bytes returning each as an unsigned 8-bit integer. - def each_byte_as_uint8(length) + def each_byte_as_uint8(length, &block) return enum_for(:each_byte_as_uint8, length) unless block_given? - read_and_advance(length).bytes.each do |byte| - yield byte - end + read_and_advance(length).bytes.each(&block) nil end # Return raw bytes as hex. def read_hex(length) - read_and_advance(length).bytes.map { |c| '%02x' % c }.join + read_and_advance(length).bytes.map { |c| "%02x" % c }.join end # Read an unsigned 8-bit integer. @@ -335,22 +333,22 @@ def read_uint_array_by_size(size, count) # Optionally accept a flag (first byte) if it has already been read (as is # the case in read_imc_uint64). def read_ic_uint32(flag = nil) - name('ic_uint32') do - flag ||= peek { name('uint8_or_flag') { read_uint8 } } + name("ic_uint32") do + flag ||= peek { name("uint8_or_flag") { read_uint8 } } case when flag < 0x80 adjust(+1) flag when flag < 0xc0 - name('uint16') { read_uint16 } & 0x7fff + name("uint16") { read_uint16 } & 0x7fff when flag < 0xe0 - name('uint24') { read_uint24 } & 0x3fffff + name("uint24") { read_uint24 } & 0x3fffff when flag < 0xf0 - name('uint32') { read_uint32 } & 0x1fffffff + name("uint32") { read_uint32 } & 0x1fffffff when flag == 0xf0 adjust(+1) # Skip the flag byte. - name('uint32+1') { read_uint32 } + name("uint32+1") { read_uint32 } else raise "Invalid flag #{flag} seen" end @@ -364,9 +362,9 @@ def read_ic_uint32(flag = nil) # big-endian 32-bit integer (4 bytes). This makes a combined size of # between 5 and 9 bytes. def read_ic_uint64 - name('ic_uint64') do - high = name('high') { read_ic_uint32 } - low = name('low') { name('uint32') { read_uint32 } } + name("ic_uint64") do + high = name("high") { read_ic_uint32 } + low = name("low") { name("uint32") { read_uint32 } } (high << 32) | low end @@ -381,20 +379,20 @@ def read_ic_uint64 # compressed 32-bit unsigned integer. This makes for a combined size # of between 1 and 11 bytes. def read_imc_uint64 - name('imc_uint64') do + name("imc_uint64") do high = 0 - flag = peek { name('uint8_or_flag') { read_uint8 } } + flag = peek { name("uint8_or_flag") { read_uint8 } } if flag == 0xff # The high 32-bits are stored first as an ic_uint32. adjust(+1) # Skip the flag byte. - high = name('high') { read_ic_uint32 } + high = name("high") { read_ic_uint32 } flag = nil end # The low 32-bits are stored as an ic_uint32; pass the flag we already # read, so we don't have to read it again. - low = name('low') { read_ic_uint32(flag) } + low = name("low") { read_ic_uint32(flag) } (high << 32) | low end diff --git a/lib/innodb/util/hex_format.rb b/lib/innodb/util/hex_format.rb new file mode 100644 index 00000000..db162958 --- /dev/null +++ b/lib/innodb/util/hex_format.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module HexFormat + LINE_SIZE = 16 + GROUP_SIZE = 8 + GROUP_FORMAT_LENGTH = ((LINE_SIZE.to_f / GROUP_SIZE).ceil * (GROUP_SIZE * 3)) + + def self.format_group(data) + data.map { |n| "%02x" % n.ord }.join(" ") + end + + def self.format_groups(data, size) + data.each_slice(size).map { |g| format_group(g) }.join(" ") + end + + def self.format_printable(data) + data.join.gsub(/[^[:print:]]/, ".") + end + + def self.format_hex(data) + data.chars.each_slice(LINE_SIZE).with_index do |bytes, i| + yield format("%08i %-#{GROUP_FORMAT_LENGTH}s |%-#{LINE_SIZE}s|", + (i * LINE_SIZE), format_groups(bytes, GROUP_SIZE), format_printable(bytes)) + end + + nil + end + + def self.puts(data, io: $stdout) + format_hex(data) { |line| io.puts(line) } + end +end diff --git a/lib/innodb/version.rb b/lib/innodb/version.rb index ef69c041..492ee8f6 100644 --- a/lib/innodb/version.rb +++ b/lib/innodb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Innodb - VERSION = '0.10.1' + VERSION = "0.14.0" end diff --git a/lib/innodb/xdes.rb b/lib/innodb/xdes.rb index ffedc677..07896131 100644 --- a/lib/innodb/xdes.rb +++ b/lib/innodb/xdes.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'forwardable' +require "forwardable" # An InnoDB "extent descriptor entry" or "+XDES+". These structures are used # in the +XDES+ entry array contained in +FSP_HDR+ and +XDES+ pages. @@ -85,11 +85,11 @@ def read_xdes_entry(page, cursor) offset: c.position, start_page: start_page, end_page: start_page + page.space.pages_per_extent - 1, - fseg_id: c.name('fseg_id') { c.read_uint64 }, + fseg_id: c.name("fseg_id") { c.read_uint64 }, this: Innodb::Page::Address.new(page: page.offset, offset: c.position), - list: c.name('list') { Innodb::List.get_node(c) }, - state: c.name('state') { STATES[c.read_uint32] }, - bitmap: c.name('bitmap') { c.read_bytes(size_bitmap) } + list: c.name("list") { Innodb::List.get_node(c) }, + state: c.name("state") { STATES[c.read_uint32] }, + bitmap: c.name("bitmap") { c.read_bytes(size_bitmap) } ) end end @@ -118,7 +118,7 @@ def page_status(page_number) def each_page_status return enum_for(:each_page_status) unless block_given? - bitmap.each_byte.each_with_index do |byte, byte_index| + bitmap.each_byte.with_index do |byte, byte_index| (0..3).each do |page_offset| page_number = start_page + (byte_index * 4) + page_offset page_bits = ((byte >> (page_offset * BITS_PER_PAGE)) & BITMAP_BV_ALL) diff --git a/spec/data/ib_logfile0 b/spec/data/orphan/ib_logfile0 similarity index 100% rename from spec/data/ib_logfile0 rename to spec/data/orphan/ib_logfile0 diff --git a/spec/data/ib_logfile1 b/spec/data/orphan/ib_logfile1 similarity index 100% rename from spec/data/ib_logfile1 rename to spec/data/orphan/ib_logfile1 diff --git a/spec/data/ibdata1 b/spec/data/orphan/ibdata1 similarity index 100% rename from spec/data/ibdata1 rename to spec/data/orphan/ibdata1 diff --git a/spec/data/sakila/5.7/ib_logfile0 b/spec/data/sakila/5.7/ib_logfile0 new file mode 100644 index 00000000..248b2f64 Binary files /dev/null and b/spec/data/sakila/5.7/ib_logfile0 differ diff --git a/spec/data/sakila/5.7/ib_logfile1 b/spec/data/sakila/5.7/ib_logfile1 new file mode 100644 index 00000000..274bba07 Binary files /dev/null and b/spec/data/sakila/5.7/ib_logfile1 differ diff --git a/spec/data/sakila/5.7/ibdata1 b/spec/data/sakila/5.7/ibdata1 new file mode 100644 index 00000000..de1c3a88 Binary files /dev/null and b/spec/data/sakila/5.7/ibdata1 differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_1.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_1.ibd new file mode 100644 index 00000000..ba78956f Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_1.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_2.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_2.ibd new file mode 100644 index 00000000..f90ca050 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_2.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_3.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_3.ibd new file mode 100644 index 00000000..a9cedc2c Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_3.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_4.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_4.ibd new file mode 100644 index 00000000..b73a8a1b Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_4.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_5.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_5.ibd new file mode 100644 index 00000000..a5903bbe Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_5.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_6.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_6.ibd new file mode 100644 index 00000000..87586f40 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_0000000000000045_INDEX_6.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_BEING_DELETED.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_BEING_DELETED.ibd new file mode 100644 index 00000000..dda81f94 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_BEING_DELETED.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_BEING_DELETED_CACHE.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_BEING_DELETED_CACHE.ibd new file mode 100644 index 00000000..de6aed81 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_BEING_DELETED_CACHE.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_CONFIG.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_CONFIG.ibd new file mode 100644 index 00000000..582c794f Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_CONFIG.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_DELETED.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_DELETED.ibd new file mode 100644 index 00000000..f1cd7994 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_DELETED.ibd differ diff --git a/spec/data/sakila/5.7/sakila/FTS_000000000000002d_DELETED_CACHE.ibd b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_DELETED_CACHE.ibd new file mode 100644 index 00000000..e1ffc2a9 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/FTS_000000000000002d_DELETED_CACHE.ibd differ diff --git a/spec/data/sakila/5.7/sakila/actor.ibd b/spec/data/sakila/5.7/sakila/actor.ibd new file mode 100644 index 00000000..408ddc7e Binary files /dev/null and b/spec/data/sakila/5.7/sakila/actor.ibd differ diff --git a/spec/data/sakila/5.7/sakila/address.ibd b/spec/data/sakila/5.7/sakila/address.ibd new file mode 100644 index 00000000..1555789e Binary files /dev/null and b/spec/data/sakila/5.7/sakila/address.ibd differ diff --git a/spec/data/sakila/5.7/sakila/category.ibd b/spec/data/sakila/5.7/sakila/category.ibd new file mode 100644 index 00000000..f3442386 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/category.ibd differ diff --git a/spec/data/sakila/5.7/sakila/city.ibd b/spec/data/sakila/5.7/sakila/city.ibd new file mode 100644 index 00000000..ba098fa8 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/city.ibd differ diff --git a/spec/data/sakila/5.7/sakila/country.ibd b/spec/data/sakila/5.7/sakila/country.ibd new file mode 100644 index 00000000..523bad1f Binary files /dev/null and b/spec/data/sakila/5.7/sakila/country.ibd differ diff --git a/spec/data/sakila/5.7/sakila/customer.ibd b/spec/data/sakila/5.7/sakila/customer.ibd new file mode 100644 index 00000000..453b93b8 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/customer.ibd differ diff --git a/spec/data/sakila/5.7/sakila/film.ibd b/spec/data/sakila/5.7/sakila/film.ibd new file mode 100644 index 00000000..981bc399 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/film.ibd differ diff --git a/spec/data/sakila/5.7/sakila/film_actor.ibd b/spec/data/sakila/5.7/sakila/film_actor.ibd new file mode 100644 index 00000000..865e0b9b Binary files /dev/null and b/spec/data/sakila/5.7/sakila/film_actor.ibd differ diff --git a/spec/data/sakila/5.7/sakila/film_category.ibd b/spec/data/sakila/5.7/sakila/film_category.ibd new file mode 100644 index 00000000..b62ad384 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/film_category.ibd differ diff --git a/spec/data/sakila/5.7/sakila/film_text.ibd b/spec/data/sakila/5.7/sakila/film_text.ibd new file mode 100644 index 00000000..628eea03 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/film_text.ibd differ diff --git a/spec/data/sakila/5.7/sakila/inventory.ibd b/spec/data/sakila/5.7/sakila/inventory.ibd new file mode 100644 index 00000000..6ae30713 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/inventory.ibd differ diff --git a/spec/data/sakila/5.7/sakila/language.ibd b/spec/data/sakila/5.7/sakila/language.ibd new file mode 100644 index 00000000..bd6b51c8 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/language.ibd differ diff --git a/spec/data/sakila/5.7/sakila/payment.ibd b/spec/data/sakila/5.7/sakila/payment.ibd new file mode 100644 index 00000000..7f23ad49 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/payment.ibd differ diff --git a/spec/data/sakila/5.7/sakila/rental.ibd b/spec/data/sakila/5.7/sakila/rental.ibd new file mode 100644 index 00000000..34dbb0c2 Binary files /dev/null and b/spec/data/sakila/5.7/sakila/rental.ibd differ diff --git a/spec/data/sakila/5.7/sakila/staff.ibd b/spec/data/sakila/5.7/sakila/staff.ibd new file mode 100644 index 00000000..3531c57a Binary files /dev/null and b/spec/data/sakila/5.7/sakila/staff.ibd differ diff --git a/spec/data/sakila/5.7/sakila/store.ibd b/spec/data/sakila/5.7/sakila/store.ibd new file mode 100644 index 00000000..ed9d9f6e Binary files /dev/null and b/spec/data/sakila/5.7/sakila/store.ibd differ diff --git a/spec/data/sakila/8.0/ibdata1 b/spec/data/sakila/8.0/ibdata1 new file mode 100644 index 00000000..de0ac7bf Binary files /dev/null and b/spec/data/sakila/8.0/ibdata1 differ diff --git a/spec/data/sakila/8.0/mysql.ibd b/spec/data/sakila/8.0/mysql.ibd new file mode 100644 index 00000000..c8fffdbd Binary files /dev/null and b/spec/data/sakila/8.0/mysql.ibd differ diff --git a/spec/data/sakila/8.0/sakila/actor.ibd b/spec/data/sakila/8.0/sakila/actor.ibd new file mode 100644 index 00000000..c327fca5 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/actor.ibd differ diff --git a/spec/data/sakila/8.0/sakila/address.ibd b/spec/data/sakila/8.0/sakila/address.ibd new file mode 100644 index 00000000..d0d77417 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/address.ibd differ diff --git a/spec/data/sakila/8.0/sakila/category.ibd b/spec/data/sakila/8.0/sakila/category.ibd new file mode 100644 index 00000000..9f25f016 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/category.ibd differ diff --git a/spec/data/sakila/8.0/sakila/city.ibd b/spec/data/sakila/8.0/sakila/city.ibd new file mode 100644 index 00000000..ac850651 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/city.ibd differ diff --git a/spec/data/sakila/8.0/sakila/country.ibd b/spec/data/sakila/8.0/sakila/country.ibd new file mode 100644 index 00000000..a0872366 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/country.ibd differ diff --git a/spec/data/sakila/8.0/sakila/customer.ibd b/spec/data/sakila/8.0/sakila/customer.ibd new file mode 100644 index 00000000..31c35fb0 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/customer.ibd differ diff --git a/spec/data/sakila/8.0/sakila/film.ibd b/spec/data/sakila/8.0/sakila/film.ibd new file mode 100644 index 00000000..dff40599 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/film.ibd differ diff --git a/spec/data/sakila/8.0/sakila/film_actor.ibd b/spec/data/sakila/8.0/sakila/film_actor.ibd new file mode 100644 index 00000000..32ec10ac Binary files /dev/null and b/spec/data/sakila/8.0/sakila/film_actor.ibd differ diff --git a/spec/data/sakila/8.0/sakila/film_category.ibd b/spec/data/sakila/8.0/sakila/film_category.ibd new file mode 100644 index 00000000..88b5f746 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/film_category.ibd differ diff --git a/spec/data/sakila/8.0/sakila/film_text.ibd b/spec/data/sakila/8.0/sakila/film_text.ibd new file mode 100644 index 00000000..37f632a7 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/film_text.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_1.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_1.ibd new file mode 100644 index 00000000..11333d38 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_1.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_2.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_2.ibd new file mode 100644 index 00000000..9e9223f6 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_2.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_3.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_3.ibd new file mode 100644 index 00000000..6d2c6adb Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_3.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_4.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_4.ibd new file mode 100644 index 00000000..b0cd94e0 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_4.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_5.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_5.ibd new file mode 100644 index 00000000..a7b6a682 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_5.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_6.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_6.ibd new file mode 100644 index 00000000..29157a0e Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_00000000000000f1_index_6.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_being_deleted.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_being_deleted.ibd new file mode 100644 index 00000000..7164f685 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_being_deleted.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_being_deleted_cache.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_being_deleted_cache.ibd new file mode 100644 index 00000000..19caf294 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_being_deleted_cache.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_config.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_config.ibd new file mode 100644 index 00000000..fc4f2b41 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_config.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_deleted.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_deleted.ibd new file mode 100644 index 00000000..67315cc0 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_deleted.ibd differ diff --git a/spec/data/sakila/8.0/sakila/fts_0000000000000451_deleted_cache.ibd b/spec/data/sakila/8.0/sakila/fts_0000000000000451_deleted_cache.ibd new file mode 100644 index 00000000..94f8374a Binary files /dev/null and b/spec/data/sakila/8.0/sakila/fts_0000000000000451_deleted_cache.ibd differ diff --git a/spec/data/sakila/8.0/sakila/inventory.ibd b/spec/data/sakila/8.0/sakila/inventory.ibd new file mode 100644 index 00000000..003c2f0e Binary files /dev/null and b/spec/data/sakila/8.0/sakila/inventory.ibd differ diff --git a/spec/data/sakila/8.0/sakila/language.ibd b/spec/data/sakila/8.0/sakila/language.ibd new file mode 100644 index 00000000..0a83cb48 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/language.ibd differ diff --git a/spec/data/sakila/8.0/sakila/payment.ibd b/spec/data/sakila/8.0/sakila/payment.ibd new file mode 100644 index 00000000..0da2714e Binary files /dev/null and b/spec/data/sakila/8.0/sakila/payment.ibd differ diff --git a/spec/data/sakila/8.0/sakila/rental.ibd b/spec/data/sakila/8.0/sakila/rental.ibd new file mode 100644 index 00000000..e930f2d3 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/rental.ibd differ diff --git a/spec/data/sakila/8.0/sakila/staff.ibd b/spec/data/sakila/8.0/sakila/staff.ibd new file mode 100644 index 00000000..08e2c953 Binary files /dev/null and b/spec/data/sakila/8.0/sakila/staff.ibd differ diff --git a/spec/data/sakila/8.0/sakila/store.ibd b/spec/data/sakila/8.0/sakila/store.ibd new file mode 100644 index 00000000..5bfe38ae Binary files /dev/null and b/spec/data/sakila/8.0/sakila/store.ibd differ diff --git a/spec/data/sakila/8.0/sys/sys_config.ibd b/spec/data/sakila/8.0/sys/sys_config.ibd new file mode 100644 index 00000000..8aad6b39 Binary files /dev/null and b/spec/data/sakila/8.0/sys/sys_config.ibd differ diff --git a/spec/data/sakila/8.0/undo_001 b/spec/data/sakila/8.0/undo_001 new file mode 100644 index 00000000..0dfcbc88 Binary files /dev/null and b/spec/data/sakila/8.0/undo_001 differ diff --git a/spec/data/sakila/8.0/undo_002 b/spec/data/sakila/8.0/undo_002 new file mode 100644 index 00000000..d16e7ded Binary files /dev/null and b/spec/data/sakila/8.0/undo_002 differ diff --git a/spec/data/sakila/8.4/ibdata1 b/spec/data/sakila/8.4/ibdata1 new file mode 100644 index 00000000..97e05284 Binary files /dev/null and b/spec/data/sakila/8.4/ibdata1 differ diff --git a/spec/data/sakila/8.4/mysql.ibd b/spec/data/sakila/8.4/mysql.ibd new file mode 100644 index 00000000..6c759f37 Binary files /dev/null and b/spec/data/sakila/8.4/mysql.ibd differ diff --git a/spec/data/sakila/8.4/sakila/actor.ibd b/spec/data/sakila/8.4/sakila/actor.ibd new file mode 100644 index 00000000..76a6f76a Binary files /dev/null and b/spec/data/sakila/8.4/sakila/actor.ibd differ diff --git a/spec/data/sakila/8.4/sakila/address.ibd b/spec/data/sakila/8.4/sakila/address.ibd new file mode 100644 index 00000000..521b68d3 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/address.ibd differ diff --git a/spec/data/sakila/8.4/sakila/category.ibd b/spec/data/sakila/8.4/sakila/category.ibd new file mode 100644 index 00000000..2e5f492f Binary files /dev/null and b/spec/data/sakila/8.4/sakila/category.ibd differ diff --git a/spec/data/sakila/8.4/sakila/city.ibd b/spec/data/sakila/8.4/sakila/city.ibd new file mode 100644 index 00000000..4f84a723 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/city.ibd differ diff --git a/spec/data/sakila/8.4/sakila/country.ibd b/spec/data/sakila/8.4/sakila/country.ibd new file mode 100644 index 00000000..547b1244 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/country.ibd differ diff --git a/spec/data/sakila/8.4/sakila/customer.ibd b/spec/data/sakila/8.4/sakila/customer.ibd new file mode 100644 index 00000000..7f53e058 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/customer.ibd differ diff --git a/spec/data/sakila/8.4/sakila/film.ibd b/spec/data/sakila/8.4/sakila/film.ibd new file mode 100644 index 00000000..00818a46 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/film.ibd differ diff --git a/spec/data/sakila/8.4/sakila/film_actor.ibd b/spec/data/sakila/8.4/sakila/film_actor.ibd new file mode 100644 index 00000000..0aa3340d Binary files /dev/null and b/spec/data/sakila/8.4/sakila/film_actor.ibd differ diff --git a/spec/data/sakila/8.4/sakila/film_category.ibd b/spec/data/sakila/8.4/sakila/film_category.ibd new file mode 100644 index 00000000..b1018a29 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/film_category.ibd differ diff --git a/spec/data/sakila/8.4/sakila/film_text.ibd b/spec/data/sakila/8.4/sakila/film_text.ibd new file mode 100644 index 00000000..5f1f75c8 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/film_text.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_1.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_1.ibd new file mode 100644 index 00000000..9c0c314c Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_1.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_2.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_2.ibd new file mode 100644 index 00000000..e234b45c Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_2.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_3.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_3.ibd new file mode 100644 index 00000000..4a2caab8 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_3.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_4.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_4.ibd new file mode 100644 index 00000000..b15db73f Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_4.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_5.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_5.ibd new file mode 100644 index 00000000..113e4c7c Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_5.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_6.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_6.ibd new file mode 100644 index 00000000..daae505a Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_00000000000000b6_index_6.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_being_deleted.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_being_deleted.ibd new file mode 100644 index 00000000..1dc37ca2 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_being_deleted.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_being_deleted_cache.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_being_deleted_cache.ibd new file mode 100644 index 00000000..8a159fda Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_being_deleted_cache.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_config.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_config.ibd new file mode 100644 index 00000000..2bf2c002 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_config.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_deleted.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_deleted.ibd new file mode 100644 index 00000000..d0debe6e Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_deleted.ibd differ diff --git a/spec/data/sakila/8.4/sakila/fts_0000000000000431_deleted_cache.ibd b/spec/data/sakila/8.4/sakila/fts_0000000000000431_deleted_cache.ibd new file mode 100644 index 00000000..5b595a30 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/fts_0000000000000431_deleted_cache.ibd differ diff --git a/spec/data/sakila/8.4/sakila/inventory.ibd b/spec/data/sakila/8.4/sakila/inventory.ibd new file mode 100644 index 00000000..b3f51fc7 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/inventory.ibd differ diff --git a/spec/data/sakila/8.4/sakila/language.ibd b/spec/data/sakila/8.4/sakila/language.ibd new file mode 100644 index 00000000..2f8ad499 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/language.ibd differ diff --git a/spec/data/sakila/8.4/sakila/payment.ibd b/spec/data/sakila/8.4/sakila/payment.ibd new file mode 100644 index 00000000..cba253df Binary files /dev/null and b/spec/data/sakila/8.4/sakila/payment.ibd differ diff --git a/spec/data/sakila/8.4/sakila/rental.ibd b/spec/data/sakila/8.4/sakila/rental.ibd new file mode 100644 index 00000000..2e66b1f5 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/rental.ibd differ diff --git a/spec/data/sakila/8.4/sakila/staff.ibd b/spec/data/sakila/8.4/sakila/staff.ibd new file mode 100644 index 00000000..5df89bdf Binary files /dev/null and b/spec/data/sakila/8.4/sakila/staff.ibd differ diff --git a/spec/data/sakila/8.4/sakila/store.ibd b/spec/data/sakila/8.4/sakila/store.ibd new file mode 100644 index 00000000..06b88110 Binary files /dev/null and b/spec/data/sakila/8.4/sakila/store.ibd differ diff --git a/spec/data/sakila/8.4/undo_001 b/spec/data/sakila/8.4/undo_001 new file mode 100644 index 00000000..19f695d5 Binary files /dev/null and b/spec/data/sakila/8.4/undo_001 differ diff --git a/spec/data/sakila/8.4/undo_002 b/spec/data/sakila/8.4/undo_002 new file mode 100644 index 00000000..1e1ba26e Binary files /dev/null and b/spec/data/sakila/8.4/undo_002 differ diff --git a/spec/innodb/checksum_spec.rb b/spec/innodb/checksum_spec.rb index aeedec3f..467fb6bf 100644 --- a/spec/innodb/checksum_spec.rb +++ b/spec/innodb/checksum_spec.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Checksum do - describe '#fold_pair' do - it 'returns a Integer' do + describe "#fold_pair" do + it "returns a Integer" do Innodb::Checksum.fold_pair(0x00, 0x00).should be_an_instance_of Integer end - it 'calculates correct values' do + it "calculates correct values" do Innodb::Checksum.fold_pair(0x00, 0x00).should eql 3_277_101_703 Innodb::Checksum.fold_pair(0x00, 0xff).should eql 3_277_088_390 Innodb::Checksum.fold_pair(0xff, 0x00).should eql 3_277_088_120 @@ -16,23 +16,23 @@ end end - describe '#fold_enumerator' do - it 'returns a Integer' do + describe "#fold_enumerator" do + it "returns a Integer" do Innodb::Checksum.fold_enumerator(0..255).should be_an_instance_of Integer end - it 'calculates correct values' do + it "calculates correct values" do Innodb::Checksum.fold_enumerator(0..255).should eql 1_406_444_672 end end - describe '#fold_string' do - it 'returns a Integer' do - Innodb::Checksum.fold_string('hello world').should be_an_instance_of Integer + describe "#fold_string" do + it "returns a Integer" do + Innodb::Checksum.fold_string("hello world").should be_an_instance_of Integer end - it 'calculates correct values' do - Innodb::Checksum.fold_string('hello world').should eql 2_249_882_843 + it "calculates correct values" do + Innodb::Checksum.fold_string("hello world").should eql 2_249_882_843 end end end diff --git a/spec/innodb/data_dictionary_spec.rb b/spec/innodb/data_dictionary_spec.rb deleted file mode 100644 index d8f6dbd3..00000000 --- a/spec/innodb/data_dictionary_spec.rb +++ /dev/null @@ -1,210 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Innodb::DataDictionary do - before :all do - @system = Innodb::System.new('spec/data/sakila/compact/ibdata1') - @dict = @system.data_dictionary - end - - describe '#mtype_prtype_to_type_string' do - it 'produces the correct type string or symbol' do - type = Innodb::DataDictionary.mtype_prtype_to_type_string(6, 1794, 2, 0) - type.should eql :SMALLINT - end - end - - describe '#mtype_prtype_to_data_type' do - it 'produces the correct type array' do - type = Innodb::DataDictionary.mtype_prtype_to_data_type(6, 1794, 2, 0) - type.should be_an_instance_of Array - type.should eql %i[SMALLINT NOT_NULL UNSIGNED] - end - end - - describe '#data_dictionary_indexes' do - it 'is a Hash' do - @dict.data_dictionary_indexes.should be_an_instance_of Hash - end - - it 'contains Hashes' do - key = @dict.data_dictionary_indexes.keys.first - @dict.data_dictionary_indexes[key].should be_an_instance_of Hash - end - end - - describe '#data_dictionary_index' do - it 'returns Innodb::Index objects' do - index = @dict.data_dictionary_index(:SYS_TABLES, :PRIMARY) - index.should be_an_instance_of Innodb::Index - end - end - - describe '#each_data_dictionary_index_root_page_number' do - it 'is an enumerator' do - is_enumerator?(@dict.each_data_dictionary_index_root_page_number).should be_truthy - end - end - - describe '#each_data_dictionary_index' do - it 'is an enumerator' do - is_enumerator?(@dict.each_data_dictionary_index).should be_truthy - end - end - - describe '#each_record_from_data_dictionary_index' do - it 'is an enumerator' do - is_enumerator?(@dict.each_record_from_data_dictionary_index(:SYS_TABLES, :PRIMARY)).should be_truthy - end - end - - describe '#each_table' do - it 'is an enumerator' do - is_enumerator?(@dict.each_table).should be_truthy - end - end - - describe '#each_column' do - it 'is an enumerator' do - is_enumerator?(@dict.each_column).should be_truthy - end - end - - describe '#each_index' do - it 'is an enumerator' do - is_enumerator?(@dict.each_index).should be_truthy - end - end - - describe '#each_field' do - it 'is an enumerator' do - is_enumerator?(@dict.each_field).should be_truthy - end - end - - describe '#table_by_id' do - it 'finds the correct table' do - table = @dict.table_by_id(19) - table.should be_an_instance_of Hash - table['NAME'].should eql 'sakila/film' - end - end - - describe '#table_by_name' do - it 'finds the correct table' do - table = @dict.table_by_name('sakila/film') - table.should be_an_instance_of Hash - table['NAME'].should eql 'sakila/film' - end - end - - describe '#table_by_space_id' do - it 'finds the correct table' do - table = @dict.table_by_space_id(7) - table.should be_an_instance_of Hash - table['NAME'].should eql 'sakila/film' - end - end - - describe '#column_by_name' do - it 'finds the correct column' do - column = @dict.column_by_name('sakila/film', 'film_id') - column.should be_an_instance_of Hash - column['NAME'].should eql 'film_id' - end - end - - describe '#index_by_id' do - it 'finds the correct index' do - index = @dict.index_by_id(27) - index.should be_an_instance_of Hash - index['NAME'].should eql 'PRIMARY' - end - end - - describe '#index_by_name' do - it 'finds the correct index' do - index = @dict.index_by_name('sakila/film', 'PRIMARY') - index.should be_an_instance_of Hash - index['NAME'].should eql 'PRIMARY' - end - end - - describe '#each_index_by_space_id' do - it 'is an enumerator' do - is_enumerator?(@dict.each_index_by_space_id(7)).should be_truthy - end - end - - describe '#each_index_by_table_id' do - it 'is an enumerator' do - is_enumerator?(@dict.each_index_by_table_id(19)).should be_truthy - end - end - - describe '#each_index_by_table_name' do - it 'is an enumerator' do - is_enumerator?(@dict.each_index_by_table_name('sakila/film')).should be_truthy - end - end - - describe '#each_field_by_index_id' do - it 'is an enumerator' do - is_enumerator?(@dict.each_field_by_index_id(27)).should be_truthy - end - end - - describe '#each_field_by_index_name' do - it 'is an enumerator' do - is_enumerator?(@dict.each_field_by_index_name('sakila/film', 'PRIMARY')).should be_truthy - end - end - - describe '#each_column_by_table_id' do - it 'is an enumerator' do - is_enumerator?(@dict.each_column_by_table_id(19)).should be_truthy - end - end - - describe '#each_column_by_table_name' do - it 'is an enumerator' do - is_enumerator?(@dict.each_column_by_table_name('sakila/film')).should be_truthy - end - end - - describe '#each_column_in_index_by_name' do - it 'is an enumerator' do - is_enumerator?(@dict.each_column_in_index_by_name('sakila/film', 'PRIMARY')).should be_truthy - end - end - - describe '#each_column_not_in_index_by_name' do - it 'is an enumerator' do - is_enumerator?(@dict.each_column_not_in_index_by_name('sakila/film', 'PRIMARY')).should be_truthy - end - end - - describe '#clustered_index_name_by_table_name' do - end - - describe '#each_column_description_by_index_name' do - it 'is an enumerator' do - is_enumerator?(@dict.each_column_description_by_index_name('sakila/film', 'PRIMARY')).should be_truthy - end - end - - describe '#record_describer_by_index_name' do - it 'returns an Innodb::RecordDescriber' do - desc = @dict.record_describer_by_index_name('sakila/film', 'PRIMARY') - desc.should be_an_instance_of Innodb::RecordDescriber - end - end - - describe '#record_describer_by_index_id' do - it 'returns an Innodb::RecordDescriber' do - desc = @dict.record_describer_by_index_id(27) - desc.should be_an_instance_of Innodb::RecordDescriber - end - end -end diff --git a/spec/innodb/data_type_spec.rb b/spec/innodb/data_type_spec.rb index 5d059c90..309625fb 100644 --- a/spec/innodb/data_type_spec.rb +++ b/spec/innodb/data_type_spec.rb @@ -1,53 +1,36 @@ # frozen_string_literal: true -require 'spec_helper' -require 'stringio' +require "spec_helper" +require "stringio" describe Innodb::DataType do - it 'makes proper data type names' do - Innodb::DataType.make_name('BIGINT', [], %i[UNSIGNED]).should eql 'BIGINT UNSIGNED' - Innodb::DataType.make_name('SMALLINT', [], []).should eql 'SMALLINT' - Innodb::DataType.make_name('VARCHAR', [32], []).should eql 'VARCHAR(32)' - Innodb::DataType.make_name('CHAR', [16], []).should eql 'CHAR(16)' - Innodb::DataType.make_name('CHAR', [], []).should eql 'CHAR' - Innodb::DataType.make_name('VARBINARY', [48], []).should eql 'VARBINARY(48)' - Innodb::DataType.make_name('BINARY', [64], []).should eql 'BINARY(64)' - Innodb::DataType.make_name('BINARY', [], []).should eql 'BINARY' - end - - describe Innodb::DataType::CharacterType do - it 'handles optional width' do - Innodb::DataType.new(:CHAR, [], []).width.should eql 1 - Innodb::DataType.new(:CHAR, [16], []).width.should eql 16 + describe Innodb::DataType::Character do + it "handles optional length" do + Innodb::DataType.parse("CHAR", []).length.should eql 1 + Innodb::DataType.parse("CHAR(16)", []).length.should eql 16 end - end - describe Innodb::DataType::VariableCharacterType do - it 'throws an error on invalid modifiers' do - expect { Innodb::DataType.new(:VARCHAR, [], []) }.to raise_error 'Invalid width specification' - expect { Innodb::DataType.new(:VARCHAR, [1, 1], []) }.to raise_error 'Invalid width specification' + it "throws an error on invalid modifiers" do + expect { Innodb::DataType.parse("VARCHAR", []) }.to raise_error Innodb::DataType::InvalidSpecificationError + expect { Innodb::DataType.parse("VARCHAR(1,1)", []) }.to raise_error Innodb::DataType::InvalidSpecificationError end - end - describe Innodb::DataType::BinaryType do - it 'handles optional width' do - Innodb::DataType.new(:BINARY, [], []).width.should eql 1 - Innodb::DataType.new(:BINARY, [16], []).width.should eql 16 + it "handles optional length" do + Innodb::DataType.parse("BINARY", []).length.should eql 1 + Innodb::DataType.parse("BINARY(16)", []).length.should eql 16 end - end - describe Innodb::DataType::VariableBinaryType do - it 'throws an error on invalid modifiers' do - expect { Innodb::DataType.new(:VARBINARY, [], []) }.to raise_error 'Invalid width specification' - expect { Innodb::DataType.new(:VARBINARY, [1, 1], []) }.to raise_error 'Invalid width specification' + it "throws an error on invalid modifiers" do + expect { Innodb::DataType.parse("VARBINARY", []) }.to raise_error Innodb::DataType::InvalidSpecificationError + expect { Innodb::DataType.parse("VARBINARY(1,1)", []) }.to raise_error Innodb::DataType::InvalidSpecificationError end end - describe Innodb::DataType::IntegerType do + describe Innodb::DataType::Integer do before :all do @data = { offset: {}, - buffer: ''.dup, + buffer: "".dup, } # Bytes 0x00 through 0x0f at offset 0. @@ -73,18 +56,18 @@ @buffer.seek(@data[:offset][:bytes_00_0f]) end - it 'returns a TINYINT value correctly' do - data_type = Innodb::DataType.new(:TINYINT, [], []) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns a TINYINT value correctly" do + data_type = Innodb::DataType.parse("TINYINT", []) + data_type.should be_an_instance_of Innodb::DataType::Integer @buffer.seek(@data[:offset][:innodb_sint_pos]) data_type.value(@buffer.read(1)).should eql 0x00 @buffer.seek(@data[:offset][:innodb_sint_neg]) data_type.value(@buffer.read(1)).should eql -128 end - it 'returns a TINYINT UNSIGNED value correctly' do - data_type = Innodb::DataType.new(:TINYINT, [], %i[UNSIGNED]) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns a TINYINT UNSIGNED value correctly" do + data_type = Innodb::DataType.parse("TINYINT", %i[UNSIGNED]) + data_type.should be_an_instance_of Innodb::DataType::Integer data_type.value(@buffer.read(1)).should eql 0x00 data_type.value(@buffer.read(1)).should eql 0x01 data_type.value(@buffer.read(1)).should eql 0x02 @@ -93,18 +76,18 @@ data_type.value(@buffer.read(1)).should eql 0xff end - it 'returns a SMALLINT value correctly' do - data_type = Innodb::DataType.new(:SMALLINT, [], []) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns a SMALLINT value correctly" do + data_type = Innodb::DataType.parse("SMALLINT", []) + data_type.should be_an_instance_of Innodb::DataType::Integer @buffer.seek(@data[:offset][:innodb_sint_pos]) data_type.value(@buffer.read(2)).should eql 0x0001 @buffer.seek(@data[:offset][:innodb_sint_neg]) data_type.value(@buffer.read(2)).should eql -32_767 end - it 'returns a SMALLINT UNSIGNED value correctly' do - data_type = Innodb::DataType.new(:SMALLINT, [], %i[UNSIGNED]) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns a SMALLINT UNSIGNED value correctly" do + data_type = Innodb::DataType.parse("SMALLINT", %i[UNSIGNED]) + data_type.should be_an_instance_of Innodb::DataType::Integer data_type.value(@buffer.read(2)).should eql 0x0001 data_type.value(@buffer.read(2)).should eql 0x0203 data_type.value(@buffer.read(2)).should eql 0x0405 @@ -113,18 +96,18 @@ data_type.value(@buffer.read(2)).should eql 0xffff end - it 'returns a MEDIUMINT value correctly' do - data_type = Innodb::DataType.new(:MEDIUMINT, [], []) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns a MEDIUMINT value correctly" do + data_type = Innodb::DataType.parse("MEDIUMINT", []) + data_type.should be_an_instance_of Innodb::DataType::Integer @buffer.seek(@data[:offset][:innodb_sint_pos]) data_type.value(@buffer.read(3)).should eql 0x000102 @buffer.seek(@data[:offset][:innodb_sint_neg]) data_type.value(@buffer.read(3)).should eql -8_388_350 end - it 'returns a MEDIUMINT UNSIGNED value correctly' do - data_type = Innodb::DataType.new(:MEDIUMINT, [], %i[UNSIGNED]) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns a MEDIUMINT UNSIGNED value correctly" do + data_type = Innodb::DataType.parse("MEDIUMINT", %i[UNSIGNED]) + data_type.should be_an_instance_of Innodb::DataType::Integer data_type.value(@buffer.read(3)).should eql 0x000102 data_type.value(@buffer.read(3)).should eql 0x030405 data_type.value(@buffer.read(3)).should eql 0x060708 @@ -133,18 +116,18 @@ data_type.value(@buffer.read(3)).should eql 0xffffff end - it 'returns an INT value correctly' do - data_type = Innodb::DataType.new(:INT, [], []) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns an INT value correctly" do + data_type = Innodb::DataType.parse("INT", []) + data_type.should be_an_instance_of Innodb::DataType::Integer @buffer.seek(@data[:offset][:innodb_sint_pos]) data_type.value(@buffer.read(4)).should eql 0x00010203 @buffer.seek(@data[:offset][:innodb_sint_neg]) data_type.value(@buffer.read(4)).should eql -2_147_417_597 end - it 'returns an INT UNSIGNED value correctly' do - data_type = Innodb::DataType.new(:INT, [], %i[UNSIGNED]) - data_type.should be_an_instance_of Innodb::DataType::IntegerType + it "returns an INT UNSIGNED value correctly" do + data_type = Innodb::DataType.parse("INT", %i[UNSIGNED]) + data_type.should be_an_instance_of Innodb::DataType::Integer data_type.value(@buffer.read(4)).should eql 0x00010203 data_type.value(@buffer.read(4)).should eql 0x04050607 data_type.value(@buffer.read(4)).should eql 0x08090a0b @@ -153,16 +136,16 @@ data_type.value(@buffer.read(4)).should eql 0xffffffff end - it 'returns a BIGINT value correctly' do - data_type = Innodb::DataType.new(:BIGINT, [], []) + it "returns a BIGINT value correctly" do + data_type = Innodb::DataType.parse("BIGINT", []) @buffer.seek(@data[:offset][:innodb_sint_pos]) data_type.value(@buffer.read(8)).should eql 0x0001020304050607 @buffer.seek(@data[:offset][:innodb_sint_neg]) data_type.value(@buffer.read(8)).should eql -9_223_088_349_902_469_625 end - it 'returns a BIGINT UNSIGNED value correctly' do - data_type = Innodb::DataType.new(:BIGINT, [], %i[UNSIGNED]) + it "returns a BIGINT UNSIGNED value correctly" do + data_type = Innodb::DataType.parse("BIGINT", %i[UNSIGNED]) data_type.value(@buffer.read(8)).should eql 0x0001020304050607 data_type.value(@buffer.read(8)).should eql 0x08090a0b0c0d0e0f @buffer.seek(@data[:offset][:max_uint]) diff --git a/spec/innodb/date_and_time_types_spec.rb b/spec/innodb/date_and_time_types_spec.rb index 10986f5f..d6b7bd9a 100644 --- a/spec/innodb/date_and_time_types_spec.rb +++ b/spec/innodb/date_and_time_types_spec.rb @@ -1,43 +1,43 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" class DateTimeTypes < Innodb::RecordDescriber type :clustered - key 'c01', 'INT', :NOT_NULL - row 'c02', 'YEAR' - row 'c03', 'TIME' - row 'c04', 'DATE' - row 'c05', 'DATETIME' - row 'c06', 'TIMESTAMP' + key "c01", "INT", :NOT_NULL + row "c02", "YEAR" + row "c03", "TIME" + row "c04", "DATE" + row "c05", "DATETIME" + row "c06", "TIMESTAMP" end # Zero. -row0 = ['0000', '00:00:00', '0000-00-00', '0000-00-00 00:00:00', '0000-00-00 00:00:00'] +row0 = ["0000", "00:00:00", "0000-00-00", "0000-00-00 00:00:00", "0000-00-00 00:00:00"] # Minimum values. -row1 = ['1901', '-838:59:59', '1000-01-01', '1000-01-01 00:00:00', '1970-01-01 00:00:01'] +row1 = ["1901", "-838:59:59", "1000-01-01", "1000-01-01 00:00:00", "1970-01-01 00:00:01"] # Maximum values. -row2 = ['2155', '838:59:59', '9999-12-31', '9999-12-31 23:59:59', '2038-01-19 03:14:07'] +row2 = ["2155", "838:59:59", "9999-12-31", "9999-12-31 23:59:59", "2038-01-19 03:14:07"] # Random values. -row3 = ['2153', '20:47:10', '3275-11-07', '5172-01-24 13:36:22', '1985-03-16 18:35:56'] +row3 = ["2153", "20:47:10", "3275-11-07", "5172-01-24 13:36:22", "1985-03-16 18:35:56"] describe Innodb::RecordDescriber do before :all do - @space = Innodb::Space.new('spec/data/t_date_and_time_types.ibd') + @space = Innodb::Space.new("spec/data/t_date_and_time_types.ibd") @space.record_describer = DateTimeTypes.new end - describe '#index' do - it 'is an Innodb::Index' do + describe "#index" do + it "is an Innodb::Index" do @space.index(3).should be_an_instance_of Innodb::Index end end - describe '#each_record' do - it 'matches the expected values' do + describe "#each_record" do + it "matches the expected values" do rec = @space.index(3).each_record rec.next.row.map { |f| f[:value] }.should =~ row0 rec.next.row.map { |f| f[:value] }.should =~ row1 diff --git a/spec/innodb/index_spec.rb b/spec/innodb/index_spec.rb index 779bdeb0..3b74c065 100644 --- a/spec/innodb/index_spec.rb +++ b/spec/innodb/index_spec.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" class TTenKRowsDescriber < Innodb::RecordDescriber type :clustered - key 'i', :INT, :UNSIGNED, :NOT_NULL + key "i", :INT, :UNSIGNED, :NOT_NULL end describe Innodb::Index do before :all do - @space = Innodb::Space.new('spec/data/t_10k_rows.ibd') + @space = Innodb::Space.new("spec/data/t_10k_rows.ibd") @space.record_describer = TTenKRowsDescriber.new @index = @space.index(3) end - describe '#linear_search' do - it 'finds the correct row' do + describe "#linear_search" do + it "finds the correct row" do rec = @index.linear_search([500]) rec.key[0][:value].should eql 500 end - it 'handles failed searches' do + it "handles failed searches" do rec = @index.linear_search([999_999]) rec.should be_nil end - it 'can find boundary rows' do + it "can find boundary rows" do rec = @index.linear_search([1]) rec.key[0][:value].should eql 1 @@ -40,18 +40,18 @@ class TTenKRowsDescriber < Innodb::RecordDescriber end end - describe '#binary_search' do - it 'finds the correct row' do + describe "#binary_search" do + it "finds the correct row" do rec = @index.binary_search([500]) rec.key[0][:value].should eql 500 end - it 'handles failed searches' do + it "handles failed searches" do rec = @index.binary_search([999_999]) rec.should be_nil end - it 'can find boundary rows' do + it "can find boundary rows" do rec = @index.binary_search([1]) rec.key[0][:value].should eql 1 @@ -65,7 +65,7 @@ class TTenKRowsDescriber < Innodb::RecordDescriber rec.should be_nil end - it 'is much more efficient than linear_search' do + it "is much more efficient than linear_search" do Innodb::Stats.reset @index.linear_search([5_000]) linear_compares = Innodb::Stats.get(:compare_key) @@ -77,9 +77,9 @@ class TTenKRowsDescriber < Innodb::RecordDescriber ((linear_compares.to_f / binary_compares) > 10).should be_truthy end - it 'can find 200 random rows' do + it "can find 200 random rows" do missing_keys = {} - (200.times.map { (rand * 10_000 + 1).floor }).map do |i| + (200.times.map { ((rand * 10_000) + 1).floor }).map do |i| rec = @index.binary_search([i]) if rec.nil? missing_keys[i] = :missing_key @@ -91,8 +91,8 @@ class TTenKRowsDescriber < Innodb::RecordDescriber end end - describe '#min_page_at_level' do - it 'returns the min page' do + describe "#min_page_at_level" do + it "returns the min page" do page = @index.min_page_at_level(0) page.level.should eql 0 rec = page.min_record @@ -100,15 +100,15 @@ class TTenKRowsDescriber < Innodb::RecordDescriber end end - describe '#min_record' do - it 'returns the min record' do + describe "#min_record" do + it "returns the min record" do rec = @index.min_record rec.key[0][:value].should eql 1 end end - describe '#max_page_at_level' do - it 'returns the max page' do + describe "#max_page_at_level" do + it "returns the max page" do page = @index.max_page_at_level(0) page.level.should eql 0 rec = page.max_record @@ -116,22 +116,22 @@ class TTenKRowsDescriber < Innodb::RecordDescriber end end - describe '#max_record' do - it 'returns the max record' do + describe "#max_record" do + it "returns the max record" do rec = @index.max_record rec.key[0][:value].should eql 10_000 end end - describe '#cursor' do - it 'returns an Innodb::Index::IndexCursor' do + describe "#cursor" do + it "returns an Innodb::Index::IndexCursor" do @index.cursor.should be_an_instance_of Innodb::Index::IndexCursor end end describe Innodb::Index::IndexCursor do - describe '#record' do - it 'iterates in forward order' do + describe "#record" do + it "iterates in forward order" do cursor = @index.cursor(:min, :forward) previous = cursor.record @@ -142,7 +142,7 @@ class TTenKRowsDescriber < Innodb::RecordDescriber end end - it 'iterates in backward order' do + it "iterates in backward order" do cursor = @index.cursor(:max, :backward) previous = cursor.record @@ -153,7 +153,7 @@ class TTenKRowsDescriber < Innodb::RecordDescriber end end - it 'iterates across page boundaries' do + it "iterates across page boundaries" do cursor = @index.cursor # This will be the first record, from page 4. @@ -168,7 +168,7 @@ class TTenKRowsDescriber < Innodb::RecordDescriber rec.page.offset.should_not eql 4 end - it 'iterates back and forth' do + it "iterates back and forth" do cursor = @index.cursor(:min, :forward) 1.upto(900) do |v| @@ -184,7 +184,7 @@ class TTenKRowsDescriber < Innodb::RecordDescriber cursor.record.should be_nil end - it 'handles index bounds' do + it "handles index bounds" do cursor = @index.cursor(:min, :backward) cursor.record.key[0][:value].to_i.should eql 1 cursor.record.should be_nil @@ -195,8 +195,8 @@ class TTenKRowsDescriber < Innodb::RecordDescriber end end - describe '#each_record' do - it 'is an enumerator' do + describe "#each_record" do + it "is an enumerator" do is_enumerator?(@index.cursor.each_record).should be_truthy end end diff --git a/spec/innodb/list/history_spec.rb b/spec/innodb/list/history_spec.rb new file mode 100644 index 00000000..0122d8a3 --- /dev/null +++ b/spec/innodb/list/history_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Innodb::List::History do + before :all do + @system = Innodb::System.new("spec/data/sakila/compact/ibdata1") + @empty_list = @system.history.each_history_list.first&.list + end + + it "can read an empty list" do + @empty_list.empty?.should(be_truthy) + end +end diff --git a/spec/innodb/list/inode_spec.rb b/spec/innodb/list/inode_spec.rb new file mode 100644 index 00000000..66bc888e --- /dev/null +++ b/spec/innodb/list/inode_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Innodb::List::Inode do + before :all do + @system = Innodb::System.new("spec/data/sakila/compact/ibdata1") + @empty_list = @system.space_by_table_name("sakila/film").list(:full_inodes) + end + + it "can read an empty list" do + @empty_list.empty?.should(be_truthy) + end + + it "only iterates through INODE pages" do + @system.system_space.list(:full_inodes).each.map(&:type).should(eql(%i[INODE INODE])) + end +end diff --git a/spec/innodb/list/undo_page_spec.rb b/spec/innodb/list/undo_page_spec.rb new file mode 100644 index 00000000..935febf2 --- /dev/null +++ b/spec/innodb/list/undo_page_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Innodb::List::UndoPage do + # TODO: Add some kind of test here. +end diff --git a/spec/innodb/list/xdes_spec.rb b/spec/innodb/list/xdes_spec.rb new file mode 100644 index 00000000..0c4c6a29 --- /dev/null +++ b/spec/innodb/list/xdes_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Innodb::List::Xdes do + # TODO: Add some kind of test here. +end diff --git a/spec/innodb/log_block_spec.rb b/spec/innodb/log_block_spec.rb index 16cdf65d..79830ac7 100644 --- a/spec/innodb/log_block_spec.rb +++ b/spec/innodb/log_block_spec.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::LogBlock do before :all do - @log = Innodb::Log.new('spec/data/ib_logfile0') + @log = Innodb::Log.new("spec/data/sakila/compact/ib_logfile0") @block = @log.block(0) end - describe '#block' do - it 'has a correct checksum' do + describe "#block" do + it "has a correct checksum" do @block.checksum.should eql 1_706_444_976 end - it 'is not corrupt' do + it "is not corrupt" do @block.corrupt?.should eql false end - it 'returns a valid header' do + it "returns a valid header" do h = @block.header h.flush.should eql true h.block_number.should eql 17 diff --git a/spec/innodb/log_group_spec.rb b/spec/innodb/log_group_spec.rb index fd8eb8cf..9e0cba1a 100644 --- a/spec/innodb/log_group_spec.rb +++ b/spec/innodb/log_group_spec.rb @@ -1,81 +1,81 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::LogGroup do before :all do @log_files = %w[ - spec/data/ib_logfile0 - spec/data/ib_logfile1 + spec/data/sakila/compact/ib_logfile0 + spec/data/sakila/compact/ib_logfile1 ] @log_file_size = 5_242_880 @log_group = Innodb::LogGroup.new(@log_files) end - describe '#new' do - it 'returns an Innodb::LogGroup' do + describe "#new" do + it "returns an Innodb::LogGroup" do @log_group.should be_a Innodb::LogGroup end end - describe '#each_log' do + describe "#each_log" do subject { @log_group.each_log } - it 'is an enumerator' do + it "is an enumerator" do is_enumerator?(subject).should be_truthy end - it 'returns an Innodb::Log' do + it "returns an Innodb::Log" do @log_files.size.times { subject.next.should be_a Innodb::Log } expect { subject.next }.to raise_error(StopIteration) end end - describe '#each_block' do + describe "#each_block" do subject { @log_group.each_block } - it 'is an enumerator' do + it "is an enumerator" do is_enumerator?(subject).should be_truthy end - it 'returns an Innodb::LogBlock' do + it "returns an Innodb::LogBlock" do subject.next.last.should be_a Innodb::LogBlock end end - describe '#logs' do - it 'returns the number of logs' do + describe "#logs" do + it "returns the number of logs" do @log_group.logs.should eql @log_files.size end end - describe '#log_size' do - it 'returns the log file size' do + describe "#log_size" do + it "returns the log file size" do @log_group.log_size.should eql @log_file_size end end - describe '#size' do - it 'returns the log group size' do + describe "#size" do + it "returns the log group size" do @log_group.size.should eql @log_file_size * @log_files.size end end - describe '#capacity' do - it 'returns the group capacity size' do + describe "#capacity" do + it "returns the group capacity size" do capacity = (@log_file_size - Innodb::Log::LOG_HEADER_SIZE) * @log_files.size @log_group.capacity.should eql capacity end end - describe '#reader' do - it 'returns an instance of Innodb::LogReader' do + describe "#reader" do + it "returns an instance of Innodb::LogReader" do @log_group.reader.should be_a Innodb::LogReader end end - describe '#record' do - it 'returns an instance of Innodb::LogRecord' do + describe "#record" do + it "returns an instance of Innodb::LogRecord" do @log_group.record(8_204).should be_a Innodb::LogRecord end end diff --git a/spec/innodb/log_reader_spec.rb b/spec/innodb/log_reader_spec.rb index ccca01f4..40b044f7 100644 --- a/spec/innodb/log_reader_spec.rb +++ b/spec/innodb/log_reader_spec.rb @@ -1,42 +1,42 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::LogReader do before :all do - log_files = ['spec/data/ib_logfile0', 'spec/data/ib_logfile1'] + log_files = ["spec/data/sakila/compact/ib_logfile0", "spec/data/sakila/compact/ib_logfile1"] @group = Innodb::LogGroup.new(log_files) @reader = @group.reader end - describe '#seek' do - it 'repositions the reader' do + describe "#seek" do + it "repositions the reader" do @reader.seek(8_204).tell.should eql 8_204 end - it 'detects out of bounds seeks' do - expect { @reader.seek(8_192) }.to raise_error 'LSN 8192 is out of bounds' + it "detects out of bounds seeks" do + expect { @reader.seek(8_192) }.to raise_error "LSN 8192 is out of bounds" end end - describe '#tell' do - it 'returns the current LSN position' do + describe "#tell" do + it "returns the current LSN position" do @reader.seek(8_205).tell.should eql 8_205 @reader.seek(8_204).tell.should eql 8_204 end end - describe '#record' do - it 'returns an instance of Innodb::LogRecord' do + describe "#record" do + it "returns an instance of Innodb::LogRecord" do record = @reader.record record.should be_an_instance_of Innodb::LogRecord end - it 'repositions the reader after reading a record' do + it "repositions the reader after reading a record" do @reader.tell.should eql 8_207 end - it 'reads records across blocks' do + it "reads records across blocks" do 512.times { @reader.record.should_not be_nil } end end diff --git a/spec/innodb/log_record_spec.rb b/spec/innodb/log_record_spec.rb index c139df0b..1e4475cc 100644 --- a/spec/innodb/log_record_spec.rb +++ b/spec/innodb/log_record_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::LogRecord do before :all do log_files = %w[ - spec/data/ib_logfile0 - spec/data/ib_logfile1 + spec/data/sakila/compact/ib_logfile0 + spec/data/sakila/compact/ib_logfile1 ] @group = Innodb::LogGroup.new(log_files) end @@ -15,20 +15,20 @@ before(:all) do @rec = @group.reader.seek(8_204).record end - it 'has the correct size' do + it "has the correct size" do @rec.size.should eql 3 end - it 'has the correct LSN' do + it "has the correct LSN" do @rec.lsn.should eql [8_204, 8_207] end - it 'has the correct preamble' do + it "has the correct preamble" do p = @rec.preamble p.type.should eql :INIT_FILE_PAGE p.page_number.should eql 1 p.space.should eql 0 p.single_record.should eql false end - it 'has an empty payload' do + it "has an empty payload" do @rec.payload.should eql({}) end end @@ -37,20 +37,20 @@ before(:all) do @rec = @group.reader.seek(8_207).record end - it 'has the correct size' do + it "has the correct size" do @rec.size.should eql 3 end - it 'has the correct LSN' do + it "has the correct LSN" do @rec.lsn.should eql [8_207, 8_210] end - it 'has the correct preamble' do + it "has the correct preamble" do p = @rec.preamble p.type.should eql :IBUF_BITMAP_INIT p.page_number.should eql 1 p.space.should eql 0 p.single_record.should eql false end - it 'has an empty payload' do + it "has an empty payload" do @rec.payload.should eql({}) end end @@ -59,20 +59,20 @@ before(:all) do @rec = @group.reader.seek(1_589_112).record end - it 'has the correct size' do + it "has the correct size" do @rec.size.should eql 36 end - it 'has the correct LSN' do + it "has the correct LSN" do @rec.lsn.should eql [1_589_112, 1_589_148] end - it 'has the correct preamble' do + it "has the correct preamble" do p = @rec.preamble p.type.should eql :REC_INSERT p.page_number.should eql 9 p.space.should eql 0 p.single_record.should eql false end - it 'has the correct payload' do + it "has the correct payload" do @rec.payload.values.first.should include( mismatch_index: 0, page_offset: 101, diff --git a/spec/innodb/log_spec.rb b/spec/innodb/log_spec.rb index 96a8ca99..62cb3796 100644 --- a/spec/innodb/log_spec.rb +++ b/spec/innodb/log_spec.rb @@ -1,99 +1,97 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Log do - LOG_CHECKPOINT_FSP_MAGIC_N_VAL = 1_441_231_243 - before :all do - @log = Innodb::Log.new('spec/data/ib_logfile0') + @log = Innodb::Log.new("spec/data/sakila/compact/ib_logfile0") end - describe '#new' do - it 'defines a class' do + describe "#new" do + it "defines a class" do Innodb::Log.should be_an_instance_of Class end - it 'returns an Innodb::Log' do + it "returns an Innodb::Log" do @log.should be_an_instance_of Innodb::Log end end - describe '#size' do - it 'returns 5,242,880 bytes' do + describe "#size" do + it "returns 5,242,880 bytes" do @log.size.should eql 5_242_880 end end - describe '#blocks' do - it 'returns 10,236 blocks' do + describe "#blocks" do + it "returns 10,236 blocks" do @log.blocks.should eql 10_236 end end - describe '#block' do - it 'returns an Innodb::Block' do + describe "#block" do + it "returns an Innodb::Block" do @log.block(0).should be_an_instance_of Innodb::LogBlock end - it 'does not return an invalid block' do + it "does not return an invalid block" do @log.block(-1).should be_nil @log.block(10_236).should be_nil end end - describe '#block_data' do - it 'returns block data at offset' do + describe "#block_data" do + it "returns block data at offset" do @log.block_data(0).should_not be_nil - expect { @log.block_data(256) }.to raise_error 'Invalid block offset' - expect { @log.block_data(513) }.to raise_error 'Invalid block offset' + expect { @log.block_data(256) }.to raise_error "Invalid block offset" + expect { @log.block_data(513) }.to raise_error "Invalid block offset" end end - describe '#header' do - it 'returns a Innodb::Log::Header' do + describe "#header" do + it "returns a Innodb::Log::Header" do @log.header.should be_an_instance_of Innodb::Log::Header end - it 'has the right keys and values' do + it "has the right keys and values" do @log.header.size.should eql 4 @log.header.group_id.should eql 0 @log.header.start_lsn.should eql 8_192 @log.header.file_no.should eql 0 - @log.header.created_by.should eql ' ' + @log.header.created_by.should eql " " end end - describe '#checkpoint' do - it 'returns a Innodb::Log::CheckpointSet' do + describe "#checkpoint" do + it "returns a Innodb::Log::CheckpointSet" do @log.checkpoint.should be_an_instance_of Innodb::Log::CheckpointSet @log.checkpoint.size.should eql 2 end - it 'has a correct checkpoint_1' do + it "has a correct checkpoint_1" do c = @log.checkpoint.checkpoint_1 - c.number.should eql 10 - c.lsn.should eql 1_603_732 - c.lsn_offset.should eql 1_597_588 + c.number.should eql 14 + c.lsn.should eql 8_400_260 + c.lsn_offset.should eql 8_396_164 c.buffer_size.should eql 1_048_576 c.archived_lsn.should eql 18_446_744_073_709_551_615 - c.checksum_1.should eql 654_771_786 - c.checksum_2.should eql 1_113_429_956 - c.fsp_free_limit.should eql 5 - c.fsp_magic.should eql LOG_CHECKPOINT_FSP_MAGIC_N_VAL + c.checksum_1.should eql 2_424_759_900 + c.checksum_2.should eql 3_026_016_186 + c.fsp_free_limit.should eql 9 + c.fsp_magic.should eql 1_441_231_243 # LOG_CHECKPOINT_FSP_MAGIC_N_VAL end - it 'has a correct checkpoint_2' do + it "has a correct checkpoint_2" do c = @log.checkpoint.checkpoint_2 - c.number.should eql 11 - c.lsn.should eql 1_603_732 - c.lsn_offset.should eql 1_597_588 + c.number.should eql 15 + c.lsn.should eql 8_400_260 + c.lsn_offset.should eql 8_396_164 c.buffer_size.should eql 1_048_576 c.archived_lsn.should eql 18_446_744_073_709_551_615 - c.checksum_1.should eql 843_938_123 - c.checksum_2.should eql 674_570_893 - c.fsp_free_limit.should eql 5 - c.fsp_magic.should eql LOG_CHECKPOINT_FSP_MAGIC_N_VAL + c.checksum_1.should eql 125_194_589 + c.checksum_2.should eql 1_139_538_825 + c.fsp_free_limit.should eql 9 + c.fsp_magic.should eql 1_441_231_243 # LOG_CHECKPOINT_FSP_MAGIC_N_VAL end end end diff --git a/spec/innodb/mysql_collation_spec.rb b/spec/innodb/mysql_collation_spec.rb new file mode 100644 index 00000000..9ca5fad1 --- /dev/null +++ b/spec/innodb/mysql_collation_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Innodb::MysqlCollation do + describe ".collations" do + it "is an Array" do + Innodb::MysqlCollation.collations.should be_an_instance_of Array + end + + # The count/minmax here can be updated when lib/innodb/mysql_collations.rb is updated, but it should probably never + # decrease, only increase. Proceed with caution. + + it "has 287 entries" do + Innodb::MysqlCollation.collations.count.should eql 287 + end + + it "has entries from id 1 to 323" do + Innodb::MysqlCollation.collations.map(&:id).minmax.should eql [1, 323] + end + + it "has only Innodb::MysqlCollation entries" do + Innodb::MysqlCollation.collations.map(&:class).uniq.should eql [Innodb::MysqlCollation] + end + + it "has mbminlen values of 1, 2, 4" do + Innodb::MysqlCollation.collations.map(&:mbminlen).sort.uniq.should eql [1, 2, 4] + end + + it "has mbmaxlen values of 1, 2, 3, 4, 5" do + Innodb::MysqlCollation.collations.map(&:mbmaxlen).sort.uniq.should eql [1, 2, 3, 4, 5] + end + end + + describe ".by_id" do + it "can look up utf8mb4_general_ci by id" do + Innodb::MysqlCollation.by_id(45).name.should eql "utf8mb4_general_ci" + end + end + + describe ".by_name" do + it "can look up utf8mb4_general_ci by name" do + Innodb::MysqlCollation.by_name("utf8mb4_general_ci").id.should eql 45 + end + end + + describe "#fixed?" do + it "works properly for two example collations" do + Innodb::MysqlCollation.by_name("ascii_general_ci").fixed?.should be true + Innodb::MysqlCollation.by_name("utf8mb4_general_ci").fixed?.should be false + end + end + + describe "#variable?" do + it "works properly for two example collations" do + Innodb::MysqlCollation.by_name("ascii_general_ci").variable?.should be false + Innodb::MysqlCollation.by_name("utf8mb4_general_ci").variable?.should be true + end + end +end diff --git a/spec/innodb/numeric_types_spec.rb b/spec/innodb/numeric_types_spec.rb index 88c5c568..2cc051d3 100644 --- a/spec/innodb/numeric_types_spec.rb +++ b/spec/innodb/numeric_types_spec.rb @@ -1,32 +1,32 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" # rubocop:disable Style/NumericLiterals class NumericTypes < Innodb::RecordDescriber type :clustered - key 'c01', 'INT', :UNSIGNED, :NOT_NULL - row 'c02', 'TINYINT' - row 'c03', 'TINYINT', :UNSIGNED - row 'c04', 'SMALLINT' - row 'c05', 'SMALLINT', :UNSIGNED - row 'c06', 'MEDIUMINT' - row 'c07', 'MEDIUMINT', :UNSIGNED - row 'c08', 'INT' - row 'c09', 'INT', :UNSIGNED - row 'c10', 'BIGINT' - row 'c11', 'BIGINT', :UNSIGNED - row 'c12', 'FLOAT' - row 'c13', 'FLOAT' - row 'c14', 'DOUBLE' - row 'c15', 'DOUBLE' - row 'c16', 'DECIMAL(10,0)' - row 'c17', 'DECIMAL(10,0)', :UNSIGNED - row 'c18', 'DECIMAL(65,0)' - row 'c19', 'DECIMAL(35,30)' - row 'c20', 'BIT' - row 'c21', 'BIT(32)' - row 'c22', 'BIT(64)' + key "c01", "INT", :UNSIGNED, :NOT_NULL + row "c02", "TINYINT" + row "c03", "TINYINT", :UNSIGNED + row "c04", "SMALLINT" + row "c05", "SMALLINT", :UNSIGNED + row "c06", "MEDIUMINT" + row "c07", "MEDIUMINT", :UNSIGNED + row "c08", "INT" + row "c09", "INT", :UNSIGNED + row "c10", "BIGINT" + row "c11", "BIGINT", :UNSIGNED + row "c12", "FLOAT" + row "c13", "FLOAT" + row "c14", "DOUBLE" + row "c15", "DOUBLE" + row "c16", "DECIMAL(10,0)" + row "c17", "DECIMAL(10,0)", :UNSIGNED + row "c18", "DECIMAL(65,0)" + row "c19", "DECIMAL(35,30)" + row "c20", "BIT" + row "c21", "BIT(32)" + row "c22", "BIT(64)" end # Zero. @@ -45,13 +45,13 @@ class NumericTypes < Innodb::RecordDescriber 0.0, 0.0, 0.0, - '0.0', - '0.0', - '0.0', - '0.0', - '0b0', - '0b0', - '0b0', + "0.0", + "0.0", + "0.0", + "0.0", + "0b0", + "0b0", + "0b0", ] # Minus one. @@ -70,13 +70,13 @@ class NumericTypes < Innodb::RecordDescriber 0.0, -1.0, 0.0, - '-1.0', - '0.0', - '-1.0', - '-1.0', - '0b1', - '0b11111111111111111111111111111111', - '0b1111111111111111111111111111111111111111111111111111111111111111', + "-1.0", + "0.0", + "-1.0", + "-1.0", + "0b1", + "0b11111111111111111111111111111111", + "0b1111111111111111111111111111111111111111111111111111111111111111", ] # One. @@ -95,13 +95,13 @@ class NumericTypes < Innodb::RecordDescriber 1.0, 1.0, 1.0, - '1.0', - '1.0', - '1.0', - '1.0', - '0b1', - '0b1', - '0b1', + "1.0", + "1.0", + "1.0", + "1.0", + "0b1", + "0b1", + "0b1", ] # Minimum values. @@ -120,13 +120,13 @@ class NumericTypes < Innodb::RecordDescriber 0.0, -2.2250738585072014e-208, 0.0, - '-9999999999.0', - '0.0', - '-99999999999999999999999999999999999999999999999999999999999999999.0', - '-99999.999999999999999999999999999999', - '0b0', - '0b0', - '0b0', + "-9999999999.0", + "0.0", + "-99999999999999999999999999999999999999999999999999999999999999999.0", + "-99999.999999999999999999999999999999", + "0b0", + "0b0", + "0b0", ] # Maximum values. @@ -145,13 +145,13 @@ class NumericTypes < Innodb::RecordDescriber 3.4028234663852886e+38, 1.7976931348623157e+308, 1.7976931348623157e+308, - '9999999999.0', - '9999999999.0', - '99999999999999999999999999999999999999999999999999999999999999999.0', - '99999.999999999999999999999999999999', - '0b1', - '0b11111111111111111111111111111111', - '0b1111111111111111111111111111111111111111111111111111111111111111', + "9999999999.0", + "9999999999.0", + "99999999999999999999999999999999999999999999999999999999999999999.0", + "99999.999999999999999999999999999999", + "0b1", + "0b11111111111111111111111111111111", + "0b1111111111111111111111111111111111111111111111111111111111111111", ] # Random values. @@ -170,29 +170,29 @@ class NumericTypes < Innodb::RecordDescriber 2.3826953364781035e+38, -1.0024988592301854e+308, 3.8077578553713446e+307, - '-2118290683.0', - '7554694345.0', - '36896958284301606307227443682014665342058559023876912710455539626.0', - '59908.987290718443144993967601373349', - '0b0', - '0b1110000001101111100011001110100', - '0b1001001010001001000111001010000011000011110110011100000101000010', + "-2118290683.0", + "7554694345.0", + "36896958284301606307227443682014665342058559023876912710455539626.0", + "59908.987290718443144993967601373349", + "0b0", + "0b1110000001101111100011001110100", + "0b1001001010001001000111001010000011000011110110011100000101000010", ] describe Innodb::RecordDescriber do before :all do - @space = Innodb::Space.new('spec/data/t_numeric_types.ibd') + @space = Innodb::Space.new("spec/data/t_numeric_types.ibd") @space.record_describer = NumericTypes.new end - describe '#index' do - it 'is an Innodb::Index' do + describe "#index" do + it "is an Innodb::Index" do @space.index(3).should be_an_instance_of Innodb::Index end end - describe '#each_record' do - it 'matches the expected values' do + describe "#each_record" do + it "matches the expected values" do rec = @space.index(3).each_record rec.next.row.map { |f| f[:value] }.should =~ row0 rec.next.row.map { |f| f[:value] }.should =~ row1 diff --git a/spec/innodb/page/fsp_hdr_xdes_spec.rb b/spec/innodb/page/fsp_hdr_xdes_spec.rb index a38e3833..84355173 100644 --- a/spec/innodb/page/fsp_hdr_xdes_spec.rb +++ b/spec/innodb/page/fsp_hdr_xdes_spec.rb @@ -1,32 +1,32 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Page::FspHdrXdes do before :all do - @space = Innodb::Space.new('spec/data/t_empty.ibd') + @space = Innodb::Space.new("spec/data/t_empty.ibd") @page = @space.page(0) end - describe 'class' do - it 'registers itself as a specialized page type' do + describe "class" do + it "registers itself as a specialized page type" do Innodb::Page.specialization_for?(:FSP_HDR).should be_truthy Innodb::Page.specialization_for?(:XDES).should be_truthy end end - describe '#new' do - it 'returns an Innodb::Page::FspHdrXdes' do + describe "#new" do + it "returns an Innodb::Page::FspHdrXdes" do @page.should be_an_instance_of Innodb::Page::FspHdrXdes end - it 'is an Innodb::Page' do + it "is an Innodb::Page" do @page.should be_a Innodb::Page end end - describe '#each_list' do - it 'returns an appropriate set of lists' do + describe "#each_list" do + it "returns an appropriate set of lists" do @page.each_list.map { |name, _| name }.should include( :free, :free_frag, diff --git a/spec/innodb/page/index_spec.rb b/spec/innodb/page/index_spec.rb index d979256b..111f3855 100644 --- a/spec/innodb/page/index_spec.rb +++ b/spec/innodb/page/index_spec.rb @@ -1,35 +1,35 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Page::Index do before :all do - @space = Innodb::Space.new('spec/data/t_empty.ibd') + @space = Innodb::Space.new("spec/data/t_empty.ibd") @page = @space.page(3) end - describe 'class' do - it 'registers itself as a specialized page type' do + describe "class" do + it "registers itself as a specialized page type" do Innodb::Page.specialization_for?(:INDEX).should be_truthy end end - describe '#new' do - it 'returns an Innodb::Page::Index' do + describe "#new" do + it "returns an Innodb::Page::Index" do @page.should be_an_instance_of Innodb::Page::Index end - it 'is an Innodb::Page' do + it "is an Innodb::Page" do @page.should be_a Innodb::Page end end - describe '#page_header' do - it 'is a Innodb::Page::Index::PageHeader' do + describe "#page_header" do + it "is a Innodb::Page::Index::PageHeader" do @page.page_header.should be_an_instance_of Innodb::Page::Index::PageHeader end - it 'has the right keys and values' do + it "has the right keys and values" do @page.page_header.n_dir_slots.should eql 2 @page.page_header.heap_top.should eql 120 @page.page_header.garbage_offset.should eql 0 @@ -45,7 +45,7 @@ @page.page_header.format.should eql :compact end - it 'has helper functions' do + it "has helper functions" do @page.level.should eql @page.page_header[:level] @page.records.should eql @page.page_header[:n_recs] @page.directory_slots.should eql @page.page_header[:n_dir_slots] @@ -53,45 +53,45 @@ end end - describe '#free_space' do - it 'returns the amount of free space' do + describe "#free_space" do + it "returns the amount of free space" do @page.free_space.should eql 16_252 end end - describe '#used_space' do - it 'returns the amount of used space' do + describe "#used_space" do + it "returns the amount of used space" do @page.used_space.should eql 132 end end - describe '#record_space' do - it 'returns the amount of record space' do + describe "#record_space" do + it "returns the amount of record space" do @page.record_space.should eql 0 end end - describe '#fseg_header' do - it 'is a Innodb::Page::Index::FsegHeader' do + describe "#fseg_header" do + it "is a Innodb::Page::Index::FsegHeader" do @page.fseg_header.should be_an_instance_of Innodb::Page::Index::FsegHeader end - it 'has the right keys and values' do + it "has the right keys and values" do @page.fseg_header[:leaf].should be_an_instance_of Innodb::Inode @page.fseg_header[:internal].should be_an_instance_of Innodb::Inode end end - describe '#record_header' do + describe "#record_header" do before :all do @header = @page.record_header(@page.cursor(@page.pos_infimum)) end - it 'is a Innodb::Page::Index::RecordHeader' do + it "is a Innodb::Page::Index::RecordHeader" do @header.should be_an_instance_of Innodb::Page::Index::RecordHeader end - it 'has the right keys and values' do + it "has the right keys and values" do @header.type.should eql :infimum @header.next.should eql 112 @header.heap_number.should eql 0 @@ -101,8 +101,8 @@ end end - describe '#system_record' do - it 'can read infimum' do + describe "#system_record" do + it "can read infimum" do rec = @page.infimum rec.should be_an_instance_of Innodb::Record rec.record[:data].should eql "infimum\x00" @@ -110,17 +110,17 @@ rec.header[:type].should eql :infimum end - it 'can read supremum' do + it "can read supremum" do rec = @page.supremum rec.should be_an_instance_of Innodb::Record - rec.record[:data].should eql 'supremum' + rec.record[:data].should eql "supremum" rec.header.should be_an_instance_of Innodb::Page::Index::RecordHeader rec.header[:type].should eql :supremum end end - describe '#record_cursor' do - it 'returns an Innodb::Page::Index::RecordCursor' do + describe "#record_cursor" do + it "returns an Innodb::Page::Index::RecordCursor" do @page.record_cursor.should be_an_instance_of Innodb::Page::Index::RecordCursor end end diff --git a/spec/innodb/page/inode_spec.rb b/spec/innodb/page/inode_spec.rb index 2a5f3af8..35f26455 100644 --- a/spec/innodb/page/inode_spec.rb +++ b/spec/innodb/page/inode_spec.rb @@ -1,52 +1,52 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Page::Inode do before :all do - @space = Innodb::Space.new('spec/data/ibdata1') + @space = Innodb::Space.new("spec/data/sakila/compact/ibdata1") @page = @space.page(2) end - describe 'class' do - it 'registers itself as a specialized page type' do + describe "class" do + it "registers itself as a specialized page type" do Innodb::Page.specialization_for?(:INODE).should be_truthy end end - describe '#new' do - it 'returns an Innodb::Page::Inode' do + describe "#new" do + it "returns an Innodb::Page::Inode" do @page.should be_an_instance_of Innodb::Page::Inode end - it 'is an Innodb::Page' do + it "is an Innodb::Page" do @page.should be_a Innodb::Page end end - describe '#list_entry' do - it 'is a Innodb::List::Node' do + describe "#list_entry" do + it "is a Innodb::List::Node" do @page.list_entry.should be_an_instance_of Innodb::List::Node end - it 'has the right keys and values' do + it "has the right keys and values" do @page.list_entry.size.should eql 2 @page.list_entry[:prev].should eql nil - @page.list_entry[:next].should eql nil + @page.list_entry[:next].should eql Innodb::Page::Address.new(page: 243, offset: 38) end - it 'has helper functions' do + it "has helper functions" do @page.prev_address.should eql @page.list_entry[:prev] @page.next_address.should eql @page.list_entry[:next] end end - describe '#each_inode' do - it 'yields Innodb::Inode objects' do + describe "#each_inode" do + it "yields Innodb::Inode objects" do @page.each_inode.to_a.map(&:class).uniq.should eql [Innodb::Inode] end - it 'yields objects with the right keys and values' do + it "yields objects with the right keys and values" do inode = @page.each_inode.to_a.first inode.fseg_id.should eql 1 inode.not_full_n_used.should eql 0 @@ -58,12 +58,12 @@ end end - describe '#each_allocated_inode' do - it 'yields Innodb::Inode objects' do + describe "#each_allocated_inode" do + it "yields Innodb::Inode objects" do @page.each_allocated_inode.to_a.map(&:class).uniq.should eql [Innodb::Inode] end - it 'yields only allocated inodes' do + it "yields only allocated inodes" do @page.each_allocated_inode do |inode| inode.allocated?.should be_truthy end diff --git a/spec/innodb/page/trx_sys_spec.rb b/spec/innodb/page/trx_sys_spec.rb index f6c9bde0..46a142a7 100644 --- a/spec/innodb/page/trx_sys_spec.rb +++ b/spec/innodb/page/trx_sys_spec.rb @@ -1,35 +1,35 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Page::TrxSys do before :all do - @space = Innodb::Space.new('spec/data/ibdata1') + @space = Innodb::Space.new("spec/data/sakila/compact/ibdata1") @page = @space.page(5) end - describe 'class' do - it 'registers itself as a specialized page type' do + describe "class" do + it "registers itself as a specialized page type" do Innodb::Page.specialization_for?(:TRX_SYS).should be_truthy end end - describe '#new' do - it 'returns an Innodb::Page::TrxSys' do + describe "#new" do + it "returns an Innodb::Page::TrxSys" do @page.should be_an_instance_of Innodb::Page::TrxSys end - it 'is an Innodb::Page' do + it "is an Innodb::Page" do @page.should be_a Innodb::Page end end - describe '#trx_sys' do - it 'is a Innodb::Page::TrxSys::Header' do + describe "#trx_sys" do + it "is a Innodb::Page::TrxSys::Header" do @page.trx_sys.should be_an_instance_of Innodb::Page::TrxSys::Header end - it 'has the right keys and values' do + it "has the right keys and values" do @page.trx_sys.trx_id.should eql 1280 @page.trx_sys.rsegs.should be_an_instance_of Array @page.trx_sys.binary_log.should eql nil diff --git a/spec/innodb/page_spec.rb b/spec/innodb/page_spec.rb index 776a361d..6b03a46f 100644 --- a/spec/innodb/page_spec.rb +++ b/spec/innodb/page_spec.rb @@ -1,124 +1,124 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Page do before :all do - @space = Innodb::Space.new('spec/data/ibdata1') + @space = Innodb::Space.new("spec/data/sakila/compact/ibdata1") @page_data = @space.page_data(0) @page = @space.page(0) end - describe '::PAGE_TYPE' do - it 'is a Hash' do + describe "::PAGE_TYPE" do + it "is a Hash" do Innodb::Page::PAGE_TYPE.should be_an_instance_of Hash end - it 'has only Symbol keys' do + it "has only Symbol keys" do classes = Innodb::Page::PAGE_TYPE.keys.map(&:class).uniq classes.should eql [Symbol] end - it 'has only Hash values' do + it "has only Hash values" do classes = Innodb::Page::PAGE_TYPE.values.map(&:class).uniq classes.should eql [Hash] end end - describe '::PAGE_TYPE_BY_VALUE' do - it 'is a Hash' do + describe "::PAGE_TYPE_BY_VALUE" do + it "is a Hash" do Innodb::Page::PAGE_TYPE_BY_VALUE.should be_an_instance_of Hash end - it 'has only Integer keys' do + it "has only Integer keys" do classes = Innodb::Page::PAGE_TYPE_BY_VALUE.keys.map(&:class).uniq classes.should eql [Integer] end - it 'has only Symbol values' do + it "has only Symbol values" do classes = Innodb::Page::PAGE_TYPE_BY_VALUE.values.map(&:class).uniq classes.should eql [Symbol] end end - describe 'specialized_classes' do - it 'is a Hash' do + describe "specialized_classes" do + it "is a Hash" do Innodb::Page.specialized_classes.should be_an_instance_of Hash end - it 'has only Symbol keys' do + it "has only Symbol keys" do Innodb::Page.specialized_classes.keys.map(&:class).uniq.should eql [Symbol] end - it 'has only keys that are keys in ::PAGE_TYPE' do + it "has only keys that are keys in ::PAGE_TYPE" do Innodb::Page.specialized_classes.keys.all? { |k| Innodb::Page::PAGE_TYPE.include?(k) }.should be_truthy end - it 'has only Class values' do + it "has only Class values" do Innodb::Page.specialized_classes.values.map(&:class).uniq.should eql [Class] end - it 'has only values subclassing Innodb::Page' do - Innodb::Page.specialized_classes.values.map(&:superclass).uniq.should eql [Innodb::Page] + it "has only values subclassing Innodb::Page" do + Innodb::Page.specialized_classes.values.all? { |x| x.is_a?(Innodb::Page) } end end - describe '#new' do - it 'returns a class' do + describe "#new" do + it "returns a class" do Innodb::Page.new(@space, @page_data).should be_an_instance_of Innodb::Page end end - describe '#parse' do - it 'delegates to the right specialized class' do + describe "#parse" do + it "delegates to the right specialized class" do Innodb::Page.parse(@space, @page_data).should be_an_instance_of Innodb::Page::FspHdrXdes end end - describe '#cursor' do - it 'returns a cursor' do + describe "#cursor" do + it "returns a cursor" do @page.cursor(0).should be_an_instance_of BufferCursor end - it 'positions the cursor correctly' do + it "positions the cursor correctly" do @page.cursor(0).position.should eql 0 @page.cursor(4).position.should eql 4 end - it 'is reading reasonable data' do + it "is reading reasonable data" do # This will read the page number from page 0, which should be 0. @page.cursor(4).read_uint32.should eql 0 end end - describe '#maybe_undefined' do - it 'returns the value when the value is not UINT_MAX' do + describe "#maybe_undefined" do + it "returns the value when the value is not UINT_MAX" do Innodb::Page.maybe_undefined(5).should eql 5 end - it 'returns nil when the value is UINT_MAX' do + it "returns nil when the value is UINT_MAX" do Innodb::Page.maybe_undefined(4_294_967_295).should eql nil end end - describe '#fil_header' do - it 'returns a Innodb::Page::FilHeader' do + describe "#fil_header" do + it "returns a Innodb::Page::FilHeader" do @page.fil_header.should be_an_instance_of Innodb::Page::FilHeader end - it 'has the right keys and values' do + it "has the right keys and values" do @page.fil_header.size.should eql 8 - @page.fil_header[:checksum].should eql 2_067_631_406 + @page.fil_header[:checksum].should eql 3_774_490_636 @page.fil_header[:offset].should eql 0 @page.fil_header[:prev].should eql 0 @page.fil_header[:next].should eql 0 - @page.fil_header[:lsn].should eql 1_601_269 + @page.fil_header[:lsn].should eql 8_400_049 @page.fil_header[:type].should eql :FSP_HDR - @page.fil_header[:flush_lsn].should eql 1_603_732 + @page.fil_header[:flush_lsn].should eql 8_400_260 @page.fil_header[:space_id].should eql 0 end - it 'has working helper functions' do + it "has working helper functions" do @page.checksum.should eql @page.fil_header[:checksum] @page.offset.should eql @page.fil_header[:offset] @page.prev.should eql @page.fil_header[:prev] @@ -128,9 +128,9 @@ end end - describe '#checksum_innodb' do - it 'calculates the right checksum' do - @page.checksum_innodb.should eql 2_067_631_406 + describe "#checksum_innodb" do + it "calculates the right checksum" do + @page.checksum_innodb.should eql 3_774_490_636 @page.corrupt?.should eql false end end diff --git a/spec/innodb/record_describer_spec.rb b/spec/innodb/record_describer_spec.rb index 8ada1929..9d12f4f1 100644 --- a/spec/innodb/record_describer_spec.rb +++ b/spec/innodb/record_describer_spec.rb @@ -1,90 +1,90 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" class PkRecordDescriber < Innodb::RecordDescriber type :clustered - key 'c1', :BIGINT, :NOT_NULL, :UNSIGNED - row 'c2', :INT - row 'c3', 'VARCHAR(64)' - key 'c4', :INT, :NOT_NULL - row 'c5', 'VARCHAR(128)', :NOT_NULL - row 'c6', :MEDIUMINT, :UNSIGNED - row 'c7', 'VARBINARY(512)' - row 'c8', :BIGINT, :UNSIGNED - row 'c9', :BLOB + key "c1", :BIGINT, :NOT_NULL, :UNSIGNED + row "c2", :INT + row "c3", "VARCHAR(64)" + key "c4", :INT, :NOT_NULL + row "c5", "VARCHAR(128)", :NOT_NULL + row "c6", :MEDIUMINT, :UNSIGNED + row "c7", "VARBINARY(512)" + row "c8", :BIGINT, :UNSIGNED + row "c9", :BLOB end class SkRecordDescriber < Innodb::RecordDescriber type :secondary - row 'c1', :BIGINT, :NOT_NULL, :UNSIGNED - row 'c4', :INT, :NOT_NULL - key 'c6', :MEDIUMINT, :UNSIGNED - key 'c8', :BIGINT, :UNSIGNED + row "c1", :BIGINT, :NOT_NULL, :UNSIGNED + row "c4", :INT, :NOT_NULL + key "c6", :MEDIUMINT, :UNSIGNED + key "c8", :BIGINT, :UNSIGNED end describe Innodb::RecordDescriber do - context 'PkRecordDescriber' do + context "PkRecordDescriber" do before :all do - @space = Innodb::Space.new('spec/data/t_record_describer.ibd') + @space = Innodb::Space.new("spec/data/t_record_describer.ibd") @space.record_describer = PkRecordDescriber.new @index = @space.index(3) end - describe '#index' do - it 'is an Innodb::Index' do + describe "#index" do + it "is an Innodb::Index" do @index.should be_an_instance_of Innodb::Index end end - describe '#each_record' do - it 'iterates through records' do + describe "#each_record" do + it "iterates through records" do @index.each_record.to_a.size.should eql 210 end end - context '#min_record' do + context "#min_record" do before :all do @rec = @index.min_record end - it 'has one NULL field' do + it "has one NULL field" do @rec.header[:nulls].size.should eql 1 end - it 'has one externally stored field' do + it "has one externally stored field" do @rec.header[:externs].size.should eql 1 end - it '#transaction_id' do + it "#transaction_id" do @rec.transaction_id.should eql 2305 end - it '#roll_pointer' do + it "#roll_pointer" do @rec.roll_pointer.is_insert.should eql true @rec.roll_pointer.undo_log.offset.should eql 272 @rec.roll_pointer.undo_log.page.should eql 435 @rec.roll_pointer.rseg_id.should eql 2 end - it 'key is (1, 1)' do + it "key is (1, 1)" do fields = @rec.key.each fields.next[:value].should eql 1 fields.next[:value].should eql 1 end - it 'row is as expected' do + it "row is as expected" do fields = @rec.row.each fields.next[:value].should eql -1 - fields.next[:value].should eql '1' * 64 - fields.next[:value].should eql '1' * 128 + fields.next[:value].should eql "1" * 64 + fields.next[:value].should eql "1" * 128 fields.next[:value].should eql 1 fields.next[:value].should eql :NULL fields.next[:value].should eql 1 - fields.next[:value].should eql '1' * 768 # max prefix + fields.next[:value].should eql "1" * 768 # max prefix end - it 'external reference field is [6, 5, 38, 15_616]' do + it "external reference field is [6, 5, 38, 15_616]" do extern = @rec.row.last[:extern] extern[:space_id].should eql 6 extern[:page_number].should eql 5 @@ -93,12 +93,12 @@ class SkRecordDescriber < Innodb::RecordDescriber end end - context '#root.min_record' do + context "#root.min_record" do before :all do @rec = @index.root.min_record end - it '#header' do + it "#header" do @rec.header.type.should eql :node_pointer @rec.header.length.should eql 5 @rec.header.heap_number.should eql 2 @@ -110,50 +110,50 @@ class SkRecordDescriber < Innodb::RecordDescriber @rec.header.lengths.size.should eql 0 end - it '#child_page_number' do + it "#child_page_number" do @rec.child_page_number.should eql 10 end - it '#key' do + it "#key" do @rec.key.size.should eql 2 - @rec.fields['c1'].should eql 1 - @rec.fields['c4'].should eql 1 + @rec.fields["c1"].should eql 1 + @rec.fields["c4"].should eql 1 end end end - context 'SkRecordDescriber' do + context "SkRecordDescriber" do before :all do - @space = Innodb::Space.new('spec/data/t_record_describer.ibd') + @space = Innodb::Space.new("spec/data/t_record_describer.ibd") @space.record_describer = SkRecordDescriber.new @index = @space.index(4) end - describe '#index' do - it 'is an Innodb::Index' do + describe "#index" do + it "is an Innodb::Index" do @index.should be_an_instance_of Innodb::Index end end - describe '#each_record' do - it 'iterates through records' do + describe "#each_record" do + it "iterates through records" do @index.each_record.to_a.size.should eql 210 end end - context '#min_record' do + context "#min_record" do before :all do @rec = @index.min_record end - it 'key is (1, 1)' do + it "key is (1, 1)" do rec = @index.each_record.next fields = rec.key.each fields.next[:value].should eql 1 fields.next[:value].should eql 1 end - it 'row is (1, 1)' do + it "row is (1, 1)" do rec = @index.each_record.next fields = rec.row.each fields.next[:value].should eql 1 diff --git a/spec/innodb/space_spec.rb b/spec/innodb/space_spec.rb index fd57b91b..479b6465 100644 --- a/spec/innodb/space_spec.rb +++ b/spec/innodb/space_spec.rb @@ -1,37 +1,37 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Space do before :all do - @space = Innodb::Space.new('spec/data/ibdata1') - @space_ibd = Innodb::Space.new('spec/data/t_empty.ibd') + @space = Innodb::Space.new("spec/data/sakila/compact/ibdata1") + @space_ibd = Innodb::Space.new("spec/data/sakila/compact/sakila/film.ibd") end - describe 'DEFAULT_PAGE_SIZE' do - it 'is a Integer' do + describe "DEFAULT_PAGE_SIZE" do + it "is a Integer" do Innodb::Space::DEFAULT_PAGE_SIZE.should be_an_instance_of Integer end end - describe 'SYSTEM_SPACE_PAGE_MAP' do - it 'is a Hash' do + describe "SYSTEM_SPACE_PAGE_MAP" do + it "is a Hash" do Innodb::Space::SYSTEM_SPACE_PAGE_MAP.should be_an_instance_of Hash end end - describe '#new' do - it 'defines a class' do + describe "#new" do + it "defines a class" do Innodb::Space.should be_an_instance_of Class end - it 'returns an Innodb::Space' do + it "returns an Innodb::Space" do @space.should be_an_instance_of Innodb::Space end end - describe '#read_at_offset' do - it 'can read bytes from the file' do + describe "#read_at_offset" do + it "can read bytes from the file" do # This will read the page number from page 0, which should be 0. @space.read_at_offset(4, 4).should eql "\x00\x00\x00\x00" # This will read the page number from page 1, which should be 1. @@ -39,38 +39,38 @@ end end - describe '#page_size' do - it 'finds a 16 KiB page size' do + describe "#page_size" do + it "finds a 16 KiB page size" do @space.page_size.should eql 16_384 end end - describe '#pages' do - it 'returns 1,152 pages' do + describe "#pages" do + it "returns 1,152 pages" do @space.pages.should eql 1_152 end end - describe '#size' do - it 'returns 18,874,368 bytes' do + describe "#size" do + it "returns 18,874,368 bytes" do @space.size.should eql 18_874_368 end end - describe '#pages_per_extent' do - it 'returns 64 pages per extent' do + describe "#pages_per_extent" do + it "returns 64 pages per extent" do @space.pages_per_extent.should eql 64 end end - describe '#page_data' do - it 'returns 16 KiB of data' do + describe "#page_data" do + it "returns 16 KiB of data" do @space.page_data(0).size.should eql 16_384 end end - describe '#page' do - it 'reads and delegates pages correctly' do + describe "#page" do + it "reads and delegates pages correctly" do @space.page(0).should be_an_instance_of Innodb::Page::FspHdrXdes @space.page(1).should be_an_instance_of Innodb::Page::IbufBitmap @space.page(2).should be_an_instance_of Innodb::Page::Inode @@ -81,77 +81,77 @@ @space.page(7).should be_an_instance_of Innodb::Page::SysDataDictionaryHeader end - it 'should return nil for a page that does not exist' do + it "should return nil for a page that does not exist" do @space.page(2_000).should eql nil end end - describe '#fsp' do - it 'is a Innodb::Page::FspHdrXdes::Header' do + describe "#fsp" do + it "is a Innodb::Page::FspHdrXdes::Header" do @space.fsp.should be_an_instance_of Innodb::Page::FspHdrXdes::Header end end - describe '#system_space?' do - it 'can identify a system space' do + describe "#system_space?" do + it "can identify a system space" do @space.system_space?.should eql true end - it 'can identify a non-system space' do + it "can identify a non-system space" do @space_ibd.system_space?.should eql false end end - describe '#trx_sys' do - it 'should return a page for a system space' do + describe "#trx_sys" do + it "should return a page for a system space" do @space.trx_sys.should be_an_instance_of Innodb::Page::TrxSys end - it 'should return nil for a non-system space' do - @space_ibd.trx_sys.should eql nil + it "should return nil for a non-system space" do + expect { @space_ibd.trx_sys }.to raise_error(RuntimeError) end end - describe '#data_dictionary_page' do - it 'should return a page for a system space' do + describe "#data_dictionary_page" do + it "should return a page for a system space" do @space.data_dictionary_page.should be_an_instance_of Innodb::Page::SysDataDictionaryHeader end - it 'should return nil for a non-system space' do - @space_ibd.data_dictionary_page.should eql nil + it "should return nil for a non-system space" do + expect { @space_ibd.data_dictionary_page }.to raise_error(RuntimeError) end end - describe '#index' do - it 'is an Innodb::Index' do + describe "#index" do + it "is an Innodb::Index" do @space_ibd.index(3).should be_an_instance_of Innodb::Index end end - describe '#each_index' do - it 'is an enumerator' do + describe "#each_index" do + it "is an enumerator" do is_enumerator?(@space_ibd.each_index).should be_truthy end - it 'iterates through indexes' do - @space_ibd.each_index.to_a.size.should eql 1 + it "iterates through indexes" do + @space_ibd.each_index.to_a.size.should eql 4 end - it 'yields an Innodb::Index' do + it "yields an Innodb::Index" do @space_ibd.each_index.to_a.first.should be_an_instance_of Innodb::Index end end - describe '#each_page' do - it 'is an enumerator' do + describe "#each_page" do + it "is an enumerator" do is_enumerator?(@space_ibd.each_page).should be_truthy end - it 'iterates through pages' do - @space_ibd.each_page.to_a.size.should eql 6 + it "iterates through pages" do + @space_ibd.each_page.to_a.size.should eql 21 end - it 'yields an Array of [page_number, page]' do + it "yields an Array of [page_number, page]" do first_page = @space_ibd.each_page.to_a.first first_page.should be_an_instance_of Array first_page[0].should eql 0 @@ -159,18 +159,18 @@ end end - describe '#each_xdes_page_number' do - it 'is an enumerator' do + describe "#each_xdes_page_number" do + it "is an enumerator" do is_enumerator?(@space.each_xdes_page_number).should be_truthy end end - describe '#xdes_page_for_page' do - it 'is a Integer' do + describe "#xdes_page_for_page" do + it "is a Integer" do @space.xdes_page_for_page(0).should be_an_instance_of Integer end - it 'calculates the correct page number' do + it "calculates the correct page number" do @space.xdes_page_for_page(0).should eql 0 @space.xdes_page_for_page(1).should eql 0 @space.xdes_page_for_page(63).should eql 0 @@ -182,12 +182,12 @@ end end - describe '#xdes_entry_for_page' do - it 'is a Integer' do + describe "#xdes_entry_for_page" do + it "is a Integer" do @space.xdes_entry_for_page(0).should be_an_instance_of Integer end - it 'calculates the correct entry number' do + it "calculates the correct entry number" do @space.xdes_entry_for_page(0).should eql 0 @space.xdes_entry_for_page(1).should eql 0 @space.xdes_entry_for_page(63).should eql 0 @@ -206,56 +206,56 @@ end end - describe '#xdes_for_page' do - it 'is an Innodb::Xdes' do + describe "#xdes_for_page" do + it "is an Innodb::Xdes" do @space.xdes_for_page(0).should be_an_instance_of Innodb::Xdes end - it 'returns the correct XDES entry' do + it "returns the correct XDES entry" do xdes = @space.xdes_for_page(0) (xdes.start_page <= 0).should eql true (xdes.end_page >= 0).should eql true end end - describe '#each_xdes_page' do - it 'is an enumerator' do + describe "#each_xdes_page" do + it "is an enumerator" do is_enumerator?(@space_ibd.each_xdes_page).should be_truthy end - it 'iterates through extent descriptor pages' do + it "iterates through extent descriptor pages" do @space_ibd.each_xdes_page.to_a.size.should eql 1 end - it 'yields an Innodb::Page::FspHdrXdes' do + it "yields an Innodb::Page::FspHdrXdes" do @space_ibd.each_xdes_page.to_a.first.should be_an_instance_of Innodb::Page::FspHdrXdes end end - describe '#each_xdes' do - it 'is an enumerator' do + describe "#each_xdes" do + it "is an enumerator" do is_enumerator?(@space_ibd.each_xdes).should be_truthy end - it 'iterates through extent descriptor entries' do + it "iterates through extent descriptor entries" do @space_ibd.each_xdes.to_a.size.should eql 1 end - it 'yields an Innodb::Xdes' do + it "yields an Innodb::Xdes" do @space_ibd.each_xdes.to_a.first.should be_an_instance_of Innodb::Xdes end end - describe '#each_page_type_region' do - it 'is an enumerator' do + describe "#each_page_type_region" do + it "is an enumerator" do is_enumerator?(@space_ibd.each_page_type_region).should be_truthy end - it 'iterates through page type regions' do + it "iterates through page type regions" do @space_ibd.each_page_type_region.to_a.size.should eql 5 end - it 'yields a Hash with the right keys and values' do + it "yields a Hash with the right keys and values" do page_type_regions = @space_ibd.each_page_type_region.to_a page_type_regions[0].should be_an_instance_of Hash diff --git a/spec/innodb/stats_spec.rb b/spec/innodb/stats_spec.rb index 326fdacf..9e5027ec 100644 --- a/spec/innodb/stats_spec.rb +++ b/spec/innodb/stats_spec.rb @@ -1,39 +1,39 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Stats do before :each do Innodb::Stats.reset end - describe '#data' do - it 'is a Hash' do + describe "#data" do + it "is a Hash" do Innodb::Stats.data.should be_an_instance_of Hash end end - describe '#get' do - it 'returns 0 for an unused statistic' do + describe "#get" do + it "returns 0 for an unused statistic" do Innodb::Stats.get(:foo).should eql 0 end - it 'gets the statistic' do + it "gets the statistic" do Innodb::Stats.increment :foo Innodb::Stats.get(:foo).should eql 1 end end - describe '#increment' do - it 'increments the statistic' do + describe "#increment" do + it "increments the statistic" do Innodb::Stats.get(:foo).should eql 0 Innodb::Stats.increment :foo Innodb::Stats.get(:foo).should eql 1 end end - describe '#reset' do - it 'resets the statistics' do + describe "#reset" do + it "resets the statistics" do Innodb::Stats.data.size.should eql 0 Innodb::Stats.increment :foo Innodb::Stats.data.size.should eql 1 diff --git a/spec/innodb/sys_data_dictionary_spec.rb b/spec/innodb/sys_data_dictionary_spec.rb new file mode 100644 index 00000000..aa4bd837 --- /dev/null +++ b/spec/innodb/sys_data_dictionary_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Innodb::SysDataDictionary do + before :all do + @system = Innodb::System.new("spec/data/sakila/compact/ibdata1") + @dict = @system.data_dictionary + end + + describe "#mtype_prtype_to_type_string" do + it "produces the correct type string or symbol" do + type = Innodb::SysDataDictionary.mtype_prtype_to_type_string(6, 1794, 2, 0) + type.should eql :SMALLINT + end + end + + describe "#mtype_prtype_to_data_type" do + it "produces the correct type array" do + type = Innodb::SysDataDictionary.mtype_prtype_to_data_type(6, 1794, 2, 0) + type.should be_an_instance_of Array + type.should eql %i[SMALLINT NOT_NULL UNSIGNED] + end + end + + # TODO: Write new tests for SysDataDictionary +end diff --git a/spec/innodb/system_spec.rb b/spec/innodb/system_spec.rb index 4021957a..2da5928a 100644 --- a/spec/innodb/system_spec.rb +++ b/spec/innodb/system_spec.rb @@ -1,194 +1,115 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::System do before :all do - @system = Innodb::System.new('spec/data/sakila/compact/ibdata1') + @system = Innodb::System.new("spec/data/sakila/compact/ibdata1") end - describe '#system_space' do - it 'returns an Innodb::Space' do + describe "data_directory" do + it "cannot find tablespace files when the data directory is wrong" do + broken_system = Innodb::System.new("spec/data/sakila/compact/ibdata1", data_directory: "foo/bar") + space = broken_system.space_by_table_name("sakila/film") + space.should be_nil + end + end + + describe "data_directory" do + it "can find tablespace files using a specified data directory" do + working_system = Innodb::System.new( + "spec/data/sakila/compact/ibdata1", + data_directory: "spec/data/sakila/compact" + ) + space = working_system.space_by_table_name("sakila/film") + space.should be_an_instance_of(Innodb::Space) + end + end + + describe "#system_space" do + it "returns an Innodb::Space" do @system.system_space.should be_an_instance_of Innodb::Space end - it 'returns space 0' do + it "returns space 0" do @system.system_space.space_id.should eql 0 end end - describe '#add_space' do - it 'adds a space to the system' do - sys = Innodb::System.new('spec/data/sakila/compact/ibdata1') - space = Innodb::Space.new('spec/data/sakila/compact/sakila/film.ibd') + describe "#add_space" do + it "adds a space to the system" do + sys = Innodb::System.new("spec/data/sakila/compact/ibdata1") + space = Innodb::Space.new("spec/data/sakila/compact/sakila/film.ibd") sys.add_space(space) sys.spaces.keys.include?(7).should be_truthy end end - describe '#add_space_file' do - it 'adds a space to the system' do - sys = Innodb::System.new('spec/data/sakila/compact/ibdata1') - sys.add_space_file('spec/data/sakila/compact/sakila/film.ibd') + describe "#add_space_file" do + it "adds a space to the system" do + sys = Innodb::System.new("spec/data/sakila/compact/ibdata1") + sys.add_space_file("spec/data/sakila/compact/sakila/film.ibd") sys.spaces.keys.include?(7).should be_truthy end end - describe '#add_table' do - it 'adds a space to the system' do - sys = Innodb::System.new('spec/data/sakila/compact/ibdata1') - sys.add_table('sakila/film') + describe "#add_table" do + it "adds a space to the system" do + sys = Innodb::System.new("spec/data/sakila/compact/ibdata1") + sys.add_table("sakila/film") sys.spaces.keys.include?(7).should be_truthy end end - describe '#space_by_table_name' do - it 'returns an Innodb::Space' do - space = @system.space_by_table_name('sakila/film') + describe "#space_by_table_name" do + it "returns an Innodb::Space" do + space = @system.space_by_table_name("sakila/film") space.should be_an_instance_of Innodb::Space end end - describe '#space' do - it 'returns an Innodb::Space' do - sys = Innodb::System.new('spec/data/sakila/compact/ibdata1') - sys.add_table('sakila/film') + describe "#space" do + it "returns an Innodb::Space" do + sys = Innodb::System.new("spec/data/sakila/compact/ibdata1") + sys.add_table("sakila/film") sys.space(7).should be_an_instance_of Innodb::Space end end - describe '#space_by_table_name' do - it 'returns an Innodb::Space' do - sys = Innodb::System.new('spec/data/sakila/compact/ibdata1') - sys.add_table('sakila/film') - sys.space_by_table_name('sakila/film').should be_an_instance_of Innodb::Space - end - end - - describe '#each_table_name' do - it 'is an enumerator' do - is_enumerator?(@system.each_table_name).should be_truthy - end - - it 'iterates all tables in the system' do - expected = %w[ - SYS_FOREIGN - SYS_FOREIGN_COLS - sakila/actor - sakila/address - sakila/category - sakila/city - sakila/country - sakila/customer - sakila/film - sakila/film_actor - sakila/film_category - sakila/inventory - sakila/language - sakila/payment - sakila/rental - sakila/staff - sakila/store - ] - - actual = @system.each_table_name.to_a - result = actual.map { |n| expected.include?(n) }.uniq - result.should eql [true] - end - end - - describe '#each_column_name_by_table_name' do - it 'is an enumerator' do - is_enumerator?(@system.each_column_name_by_table_name('sakila/film')).should be_truthy - end - - it 'iterates all columns in the table' do - expected = %w[ - film_id - title - description - release_year - language_id - original_language_id - rental_duration - rental_rate - length - replacement_cost - rating - special_features - last_update - ] - - actual = @system.each_column_name_by_table_name('sakila/film').to_a - result = actual.map { |n| expected.include?(n) }.uniq - result.should eql [true] - end - end - - describe '#each_index_name_by_table_name' do - it 'is an enumerator' do - is_enumerator?(@system.each_index_name_by_table_name('sakila/film')).should be_truthy - end - - it 'iterates all indexes in the table' do - expected = %w[ - PRIMARY - idx_title - idx_fk_language_id - idx_fk_original_language_id - ] - - actual = @system.each_index_name_by_table_name('sakila/film').to_a - result = actual.map { |n| expected.include?(n) }.uniq - result.should eql [true] - end - end - - describe '#table_name_by_id' do - it 'returns the correct table name' do - @system.table_name_by_id(19).should eql 'sakila/film' - end - end - - describe '#index_name_by_id' do - it 'returns the correct index name' do - @system.index_name_by_id(27).should eql 'PRIMARY' - end - end - - describe '#table_and_index_name_by_id' do - it 'returns the correct table and index name' do - table, index = @system.table_and_index_name_by_id(27) - table.should eql 'sakila/film' - index.should eql 'PRIMARY' + describe "#space_by_table_name" do + it "returns an Innodb::Space" do + sys = Innodb::System.new("spec/data/sakila/compact/ibdata1") + sys.add_table("sakila/film") + sys.space_by_table_name("sakila/film").should be_an_instance_of Innodb::Space end end - describe '#index_by_name' do - it 'returns an Innodb::Index object' do - index = @system.index_by_name('sakila/film', 'PRIMARY') + describe "#index_by_name" do + it "returns an Innodb::Index object" do + index = @system.index_by_name("sakila/film", "PRIMARY") index.should be_an_instance_of Innodb::Index end end - describe '#each_orphan' do + describe "#each_orphan" do before :all do - @system = Innodb::System.new('spec/data/ibdata1') + # Tablespace has a missing tablespace file for test/t_empty. + @system = Innodb::System.new("spec/data/orphan/ibdata1") end - it 'has an orphan space' do - @system.space_by_table_name('test/t_empty').should be_nil + it "has an orphan space" do + @system.space_by_table_name("test/t_empty").should be_nil end - it 'is an enumerator' do + it "is an enumerator" do is_enumerator?(@system.each_orphan).should be_truthy end - it 'returns the orphan space' do - @system.each_orphan.next.should eql 'test/t_empty' + it "returns the orphan space" do + @system.each_orphan.next.should eql "test/t_empty" end end end diff --git a/spec/innodb/util/buffer_cursor_spec.rb b/spec/innodb/util/buffer_cursor_spec.rb index 7fadfacb..825ac9ac 100644 --- a/spec/innodb/util/buffer_cursor_spec.rb +++ b/spec/innodb/util/buffer_cursor_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe BufferCursor do before :all do @data = { offset: {}, - buffer: ''.dup, + buffer: "".dup, } # Bytes 0x00 through 0x0f at offset 0. @@ -19,7 +19,7 @@ # A test string. @data[:offset][:alphabet] = @data[:buffer].size - @data[:buffer] << 'abcdefghijklmnopqrstuvwxyz' + @data[:buffer] << "abcdefghijklmnopqrstuvwxyz" # InnoDB-compressed unsigned 32-bit integers. @data[:offset][:ic_uint32_00000000] = @data[:buffer].size @@ -91,22 +91,22 @@ @cursor = BufferCursor.new(@buffer, 0) end - describe '#new' do - it 'returns an BufferCursor' do + describe "#new" do + it "returns an BufferCursor" do @cursor.should be_an_instance_of BufferCursor end end - describe '#position' do - it 'returns the position of the cursor' do + describe "#position" do + it "returns the position of the cursor" do @cursor.position.should eql 0 @cursor.seek(1) @cursor.position.should eql 1 end end - describe '#seek' do - it 'moves the cursor to the provided position' do + describe "#seek" do + it "moves the cursor to the provided position" do @cursor.position.should eql 0 @cursor.seek(5) @cursor.position.should eql 5 @@ -115,8 +115,8 @@ end end - describe '#adjust' do - it 'adjusts the cursor forwards with positive values' do + describe "#adjust" do + it "adjusts the cursor forwards with positive values" do @cursor.position.should eql 0 @cursor.adjust(5) @cursor.position.should eql 5 @@ -124,7 +124,7 @@ @cursor.position.should eql 10 end - it 'adjusts the cursor backwards with negative values' do + it "adjusts the cursor backwards with negative values" do @cursor.position.should eql 0 @cursor.adjust(5) @cursor.position.should eql 5 @@ -133,25 +133,25 @@ end end - describe '#read_and_advance' do - it 'reads the number of bytes specified' do + describe "#read_and_advance" do + it "reads the number of bytes specified" do @cursor.read_and_advance(1).should eql "\x00" @cursor.read_and_advance(2).should eql "\x01\x02" @cursor.read_and_advance(3).should eql "\x03\x04\x05" end end - describe '#forward' do - it 'returns self' do + describe "#forward" do + it "returns self" do @cursor.forward.should eql @cursor end - it 'sets the direction to forwards' do + it "sets the direction to forwards" do @cursor.forward @cursor.direction.should eql :forward end - it 'reads data forwards' do + it "reads data forwards" do @cursor.seek(0) @cursor.forward @cursor.read_and_advance(1).should eql "\x00" @@ -159,17 +159,17 @@ end end - describe '#backward' do - it 'returns self' do + describe "#backward" do + it "returns self" do @cursor.backward.should eql @cursor end - it 'sets the direction to backward' do + it "sets the direction to backward" do @cursor.backward @cursor.direction.should eql :backward end - it 'reads data backward' do + it "reads data backward" do @cursor.seek(5) @cursor.backward @cursor.read_and_advance(1).should eql "\x04" @@ -177,13 +177,13 @@ end end - describe '#push and #pop' do - it 'returns self' do + describe "#push and #pop" do + it "returns self" do @cursor.push.should eql @cursor @cursor.pop.should eql @cursor end - it 'pushes and pops' do + it "pushes and pops" do @cursor.push(10) @cursor.position.should eql 10 @cursor.pop @@ -191,13 +191,13 @@ end end - describe '#peek' do - it 'passes through the block return value' do + describe "#peek" do + it "passes through the block return value" do @cursor.peek { true }.should eql true @cursor.peek { false }.should eql false end - it 'does not disturb the cursor position or direction on return' do + it "does not disturb the cursor position or direction on return" do @cursor.position.should eql 0 @cursor.direction.should eql :forward @cursor.peek do @@ -217,28 +217,28 @@ end end - describe '#read_bytes' do - it 'returns a raw byte string of the given length' do + describe "#read_bytes" do + it "returns a raw byte string of the given length" do @cursor.read_bytes(4).should eql "\x00\x01\x02\x03" end - it 'returns a string uncorrupted' do + it "returns a string uncorrupted" do @cursor.seek(@data[:offset][:alphabet]) - @cursor.read_bytes(4).should eql 'abcd' + @cursor.read_bytes(4).should eql "abcd" end end - describe '#read_hex' do - it 'returns a hex string of the given length' do - @cursor.read_hex(4).should eql '00010203' - @cursor.read_hex(4).should eql '04050607' - @cursor.read_hex(4).should eql '08090a0b' - @cursor.read_hex(4).should eql '0c0d0e0f' + describe "#read_hex" do + it "returns a hex string of the given length" do + @cursor.read_hex(4).should eql "00010203" + @cursor.read_hex(4).should eql "04050607" + @cursor.read_hex(4).should eql "08090a0b" + @cursor.read_hex(4).should eql "0c0d0e0f" end end - describe '#read_uint8' do - it 'reads 1 byte as uint8' do + describe "#read_uint8" do + it "reads 1 byte as uint8" do @cursor.read_uint8.should eql 0x00 @cursor.read_uint8.should eql 0x01 @cursor.read_uint8.should eql 0x02 @@ -248,8 +248,8 @@ end end - describe '#read_uint16' do - it 'returns 2 bytes as uint16' do + describe "#read_uint16" do + it "returns 2 bytes as uint16" do @cursor.read_uint16.should eql 0x0001 @cursor.read_uint16.should eql 0x0203 @cursor.read_uint16.should eql 0x0405 @@ -259,8 +259,8 @@ end end - describe '#read_uint24' do - it 'returns 3 bytes as uint24' do + describe "#read_uint24" do + it "returns 3 bytes as uint24" do @cursor.read_uint24.should eql 0x000102 @cursor.read_uint24.should eql 0x030405 @cursor.read_uint24.should eql 0x060708 @@ -270,8 +270,8 @@ end end - describe '#read_uint32' do - it 'returns 4 bytes as uint32' do + describe "#read_uint32" do + it "returns 4 bytes as uint32" do @cursor.read_uint32.should eql 0x00010203 @cursor.read_uint32.should eql 0x04050607 @cursor.read_uint32.should eql 0x08090a0b @@ -281,8 +281,8 @@ end end - describe '#read_uint64' do - it 'returns 8 bytes as uint64' do + describe "#read_uint64" do + it "returns 8 bytes as uint64" do @cursor.read_uint64.should eql 0x0001020304050607 @cursor.read_uint64.should eql 0x08090a0b0c0d0e0f @cursor.seek(@data[:offset][:max_uint]) @@ -290,177 +290,177 @@ end end - describe '#read_uint_by_size' do - it 'returns a uint8 for size 1' do + describe "#read_uint_by_size" do + it "returns a uint8 for size 1" do @cursor.read_uint_by_size(1).should eql 0x00 end - it 'returns a uint16 for size 2' do + it "returns a uint16 for size 2" do @cursor.read_uint_by_size(2).should eql 0x0001 end - it 'returns a uint24 for size 3' do + it "returns a uint24 for size 3" do @cursor.read_uint_by_size(3).should eql 0x000102 end - it 'returns a uint32 for size 4' do + it "returns a uint32 for size 4" do @cursor.read_uint_by_size(4).should eql 0x00010203 end - it 'returns a uint64 for size 8' do + it "returns a uint64 for size 8" do @cursor.read_uint_by_size(8).should eql 0x0001020304050607 end end - describe '#read_ic_uint32' do - it 'reads a 1-byte zero value correctly' do + describe "#read_ic_uint32" do + it "reads a 1-byte zero value correctly" do @cursor.seek(@data[:offset][:ic_uint32_00000000]) @cursor.read_ic_uint32.should eql 0 @cursor.position.should eql @data[:offset][:ic_uint32_00000000] + 1 end - it 'reads a 1-byte maximal value correctly' do + it "reads a 1-byte maximal value correctly" do @cursor.seek(@data[:offset][:ic_uint32_0000007f]) @cursor.read_ic_uint32.should eql 0x7f @cursor.position.should eql @data[:offset][:ic_uint32_0000007f] + 1 end - it 'reads a 2-byte maximal value correctly' do + it "reads a 2-byte maximal value correctly" do @cursor.seek(@data[:offset][:ic_uint32_00003fff]) @cursor.read_ic_uint32.should eql 0x3fff @cursor.position.should eql @data[:offset][:ic_uint32_00003fff] + 2 end - it 'reads a 3-byte maximal value correctly' do + it "reads a 3-byte maximal value correctly" do @cursor.seek(@data[:offset][:ic_uint32_001fffff]) @cursor.read_ic_uint32.should eql 0x1fffff @cursor.position.should eql @data[:offset][:ic_uint32_001fffff] + 3 end - it 'reads a 4-byte maximal value correctly' do + it "reads a 4-byte maximal value correctly" do @cursor.seek(@data[:offset][:ic_uint32_0fffffff]) @cursor.read_ic_uint32.should eql 0x0fffffff @cursor.position.should eql @data[:offset][:ic_uint32_0fffffff] + 4 end - it 'reads a 5-byte maximal value correctly' do + it "reads a 5-byte maximal value correctly" do @cursor.seek(@data[:offset][:ic_uint32_ffffffff]) @cursor.read_ic_uint32.should eql 0xffffffff @cursor.position.should eql @data[:offset][:ic_uint32_ffffffff] + 5 end end - describe '#read_ic_uint64' do - it 'reads a 5-byte zero value correctly' do + describe "#read_ic_uint64" do + it "reads a 5-byte zero value correctly" do @cursor.seek(@data[:offset][:ic_uint64_0000000000000000]) @cursor.read_ic_uint64.should eql 0 @cursor.position.should eql @data[:offset][:ic_uint64_0000000000000000] + 5 end - it 'reads a 5-byte interesting value 0x0000000100000001 correctly' do + it "reads a 5-byte interesting value 0x0000000100000001 correctly" do @cursor.seek(@data[:offset][:ic_uint64_0000000100000001]) @cursor.read_ic_uint64.should eql 0x0000000100000001 @cursor.position.should eql @data[:offset][:ic_uint64_0000000100000001] + 5 end - it 'reads a 5-byte interesting value 0x00000000ffffffff correctly' do + it "reads a 5-byte interesting value 0x00000000ffffffff correctly" do @cursor.seek(@data[:offset][:ic_uint64_00000000ffffffff]) @cursor.read_ic_uint64.should eql 0x00000000ffffffff @cursor.position.should eql @data[:offset][:ic_uint64_00000000ffffffff] + 5 end - it 'reads a 9-byte interesting value 0xffffffff00000000 correctly' do + it "reads a 9-byte interesting value 0xffffffff00000000 correctly" do @cursor.seek(@data[:offset][:ic_uint64_ffffffff00000000]) @cursor.read_ic_uint64.should eql 0xffffffff00000000 @cursor.position.should eql @data[:offset][:ic_uint64_ffffffff00000000] + 9 end - it 'reads a 7-byte interesting value 0x0000ffff0000ffff correctly' do + it "reads a 7-byte interesting value 0x0000ffff0000ffff correctly" do @cursor.seek(@data[:offset][:ic_uint64_0000ffff0000ffff]) @cursor.read_ic_uint64.should eql 0x0000ffff0000ffff @cursor.position.should eql @data[:offset][:ic_uint64_0000ffff0000ffff] + 7 end - it 'reads a 9-byte interesting value 0xffff0000ffff0000 correctly' do + it "reads a 9-byte interesting value 0xffff0000ffff0000 correctly" do @cursor.seek(@data[:offset][:ic_uint64_ffff0000ffff0000]) @cursor.read_ic_uint64.should eql 0xffff0000ffff0000 @cursor.position.should eql @data[:offset][:ic_uint64_ffff0000ffff0000] + 9 end - it 'reads a 9-byte maximal value correctly' do + it "reads a 9-byte maximal value correctly" do @cursor.seek(@data[:offset][:ic_uint64_ffffffffffffffff]) @cursor.read_ic_uint64.should eql 0xffffffffffffffff @cursor.position.should eql @data[:offset][:ic_uint64_ffffffffffffffff] + 9 end end - describe '#read_imc_uint64' do - it 'reads a 1-byte zero value correctly' do + describe "#read_imc_uint64" do + it "reads a 1-byte zero value correctly" do @cursor.seek(@data[:offset][:imc_uint64_0000000000000000]) @cursor.read_imc_uint64.should eql 0 @cursor.position.should eql @data[:offset][:imc_uint64_0000000000000000] + 1 end - it 'reads a 3-byte interesting value 0x0000000100000001 correctly' do + it "reads a 3-byte interesting value 0x0000000100000001 correctly" do @cursor.seek(@data[:offset][:imc_uint64_0000000100000001]) @cursor.read_imc_uint64.should eql 0x0000000100000001 @cursor.position.should eql @data[:offset][:imc_uint64_0000000100000001] + 3 end - it 'reads a 5-byte interesting value 0x00000000ffffffff correctly' do + it "reads a 5-byte interesting value 0x00000000ffffffff correctly" do @cursor.seek(@data[:offset][:imc_uint64_00000000ffffffff]) @cursor.read_imc_uint64.should eql 0x00000000ffffffff @cursor.position.should eql @data[:offset][:imc_uint64_00000000ffffffff] + 5 end - it 'reads a 7-byte interesting value 0xffffffff00000000 correctly' do + it "reads a 7-byte interesting value 0xffffffff00000000 correctly" do @cursor.seek(@data[:offset][:imc_uint64_ffffffff00000000]) @cursor.read_imc_uint64.should eql 0xffffffff00000000 @cursor.position.should eql @data[:offset][:imc_uint64_ffffffff00000000] + 7 end - it 'reads a 7-byte interesting value 0x0000ffff0000ffff correctly' do + it "reads a 7-byte interesting value 0x0000ffff0000ffff correctly" do @cursor.seek(@data[:offset][:imc_uint64_0000ffff0000ffff]) @cursor.read_imc_uint64.should eql 0x0000ffff0000ffff @cursor.position.should eql @data[:offset][:imc_uint64_0000ffff0000ffff] + 7 end - it 'reads a 11-byte interesting value 0xffff0000ffff0000 correctly' do + it "reads a 11-byte interesting value 0xffff0000ffff0000 correctly" do @cursor.seek(@data[:offset][:imc_uint64_ffff0000ffff0000]) @cursor.read_imc_uint64.should eql 0xffff0000ffff0000 @cursor.position.should eql @data[:offset][:imc_uint64_ffff0000ffff0000] + 11 end - it 'reads a 11-byte maximal value correctly' do + it "reads a 11-byte maximal value correctly" do @cursor.seek(@data[:offset][:imc_uint64_ffffffffffffffff]) @cursor.read_imc_uint64.should eql 0xffffffffffffffff @cursor.position.should eql @data[:offset][:imc_uint64_ffffffffffffffff] + 11 end end - describe '#read_bit_array' do - it 'returns an array of bits' do + describe "#read_bit_array" do + it "returns an array of bits" do @cursor.read_bit_array(64).uniq.sort.should eql [0, 1] end - it 'returns the right bits' do + it "returns the right bits" do @cursor.read_bit_array(8).should eql [0, 0, 0, 0, 0, 0, 0, 0] @cursor.read_bit_array(8).should eql [0, 0, 0, 0, 0, 0, 0, 1] @cursor.seek(@data[:offset][:max_uint]) @cursor.read_bit_array(8).should eql [1, 1, 1, 1, 1, 1, 1, 1] end - it 'can handle large bit arrays' do + it "can handle large bit arrays" do @cursor.read_bit_array(64).size.should eql 64 end end - describe '#trace!' do - it 'enables tracing globally' do + describe "#trace!" do + it "enables tracing globally" do BufferCursor.trace! - trace_string = ''.dup - trace_output = StringIO.new(trace_string, 'w') + trace_string = "".dup + trace_output = StringIO.new(trace_string, "w") c1 = BufferCursor.new(@buffer, 0) c1.trace_to(trace_output) @@ -478,10 +478,10 @@ end end - describe '#trace' do - it 'enables tracing per instance' do - trace_string = ''.dup - trace_output = StringIO.new(trace_string, 'w') + describe "#trace" do + it "enables tracing per instance" do + trace_string = "".dup + trace_output = StringIO.new(trace_string, "w") c1 = BufferCursor.new(@buffer, 0) c1.trace diff --git a/spec/innodb/xdes_spec.rb b/spec/innodb/xdes_spec.rb index e6e3a6a6..c0bcb203 100644 --- a/spec/innodb/xdes_spec.rb +++ b/spec/innodb/xdes_spec.rb @@ -1,50 +1,50 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb::Xdes do before :all do - @space = Innodb::Space.new('spec/data/ibdata1') + @space = Innodb::Space.new("spec/data/sakila/compact/ibdata1") @page = @space.page(0) @cursor = @page.cursor(@page.pos_xdes_array) @xdes0 = Innodb::Xdes.new(@page, @cursor) @xdes1 = Innodb::Xdes.new(@page, @cursor) end - describe '::STATES' do - it 'is a Hash' do + describe "::STATES" do + it "is a Hash" do Innodb::Xdes::STATES.should be_an_instance_of Hash end - it 'has only Integer keys' do + it "has only Integer keys" do classes = Innodb::Xdes::STATES.keys.map(&:class).uniq classes.should eql [Integer] end - it 'has only Symbol values' do + it "has only Symbol values" do classes = Innodb::Xdes::STATES.values.map(&:class).uniq classes.should eql [Symbol] end end - describe '#new' do - it 'returns an Innodb::Xdes' do + describe "#new" do + it "returns an Innodb::Xdes" do @xdes0.should be_an_instance_of Innodb::Xdes end end - describe '#read_xdes_entry' do - it 'calculates the start_page correctly' do + describe "#read_xdes_entry" do + it "calculates the start_page correctly" do @xdes0.start_page.should eql 0 @xdes1.start_page.should eql 64 end - it 'calculates the end_page correctly' do + it "calculates the end_page correctly" do @xdes0.end_page.should eql 63 @xdes1.end_page.should eql 127 end - it 'has the right methods and values' do + it "has the right methods and values" do @xdes0.start_page.should eql 0 @xdes0.fseg_id.should eql 0 @xdes0.this.should be_an_instance_of Innodb::Page::Address @@ -53,21 +53,21 @@ end end - describe '#xdes' do - it 'is a Hash' do + describe "#xdes" do + it "is a Hash" do @xdes0.xdes.should be_an_instance_of Innodb::Xdes::Entry end end - describe '#allocated_to_fseg?' do - it 'works correctly' do + describe "#allocated_to_fseg?" do + it "works correctly" do @space.xdes_for_page(0).allocated_to_fseg?.should eql false @space.xdes_for_page(64).allocated_to_fseg?.should eql true end end - describe '#page_status' do - it 'returns the status of a page' do + describe "#page_status" do + it "returns the status of a page" do status = @xdes0.page_status(0) status.should be_an_instance_of Innodb::Xdes::PageStatus status.size.should eql 2 @@ -76,16 +76,16 @@ end end - describe '#each_page_status' do - it 'is an enumerator' do + describe "#each_page_status" do + it "is an enumerator" do is_enumerator?(@xdes0.each_page_status).should be_truthy end - it 'yields Hashes' do + it "yields Hashes" do @xdes0.each_page_status.to_a.map { |v| v[1].class }.uniq.should eql [Innodb::Xdes::PageStatus] end - it 'yields Hashes with the right keys and values' do + it "yields Hashes with the right keys and values" do status = @xdes0.each_page_status.to_a.first[1] status.size.should eql 2 status[:free].should eql false @@ -93,20 +93,20 @@ end end - describe '#free_pages' do - it 'returns the number of free pages' do + describe "#free_pages" do + it "returns the number of free pages" do @xdes0.free_pages.should eql 0 end end - describe '#used_pages' do - it 'returns the number of used pages' do + describe "#used_pages" do + it "returns the number of used pages" do @xdes0.used_pages.should eql 64 end end - describe '#==' do - it 'compares by page and offset' do + describe "#==" do + it "compares by page and offset" do (@xdes0 == @xdes1).should eql false (@xdes0 == @space.xdes_for_page(0)).should eql true end diff --git a/spec/innodb_spec.rb b/spec/innodb_spec.rb index 8fee2c7d..d20ec81d 100644 --- a/spec/innodb_spec.rb +++ b/spec/innodb_spec.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" describe Innodb do - it 'is a Module' do + it "is a Module" do Innodb.should be_an_instance_of Module end - it 'has a version' do + it "has a version" do Innodb::VERSION.should be_an_instance_of String end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bc513c2a..6b6c142b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.join(File.dirname(__FILE__), '..', 'lib', 'innodb') +require File.join(File.dirname(__FILE__), "..", "lib", "innodb") RSpec.configure do |config| # Enable the below to allow easier fixing of deprecated RSpec syntax.