module Sequel::SchemaDumper

Public Instance Methods

column_schema_to_ruby_type(schema) click to toggle source

Convert the column schema information to a hash of column options, one of which must be :type. The other options added should modify that type (e.g. :size). If a database type is not recognized, return it as a String type.

# File lib/sequel/extensions/schema_dumper.rb, line 22
def column_schema_to_ruby_type(schema)
  case schema[:db_type].downcase
  when /\A(medium|small)?int(?:eger)?(?:\((\d+)\))?( unsigned)?\z/o
    if !$1 && $2 && $2.to_i >= 10 && $3
      # Unsigned integer type with 10 digits can potentially contain values which
      # don't fit signed integer type, so use bigint type in target database.
      {:type=>:Bignum}
    else
      {:type=>Integer}
    end
  when /\Atinyint(?:\((\d+)\))?(?: unsigned)?\z/o
    {:type =>schema[:type] == :boolean ? TrueClass : Integer}
  when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/o
    {:type=>:Bignum}
  when /\A(?:real|float|double(?: precision)?|double\(\d+,\d+\)(?: unsigned)?)\z/o
    {:type=>Float}
  when 'boolean', 'bit', 'bool'
    {:type=>TrueClass}
  when /\A(?:(?:tiny|medium|long|n)?text|clob)\z/o
    {:type=>String, :text=>true}
  when 'date'
    {:type=>Date}
  when /\A(?:small)?datetime\z/o
    {:type=>DateTime}
  when /\Atimestamp(?:\((\d+)\))?(?: with(?:out)? time zone)?\z/o
    {:type=>DateTime, :size=>($1.to_i if $1)}
  when /\Atime(?: with(?:out)? time zone)?\z/o
    {:type=>Time, :only_time=>true}
  when /\An?char(?:acter)?(?:\((\d+)\))?\z/o
    {:type=>String, :size=>($1.to_i if $1), :fixed=>true}
  when /\A(?:n?varchar|character varying|bpchar|string)(?:\((\d+)\))?\z/o
    {:type=>String, :size=>($1.to_i if $1)}
  when /\A(?:small)?money\z/o
    {:type=>BigDecimal, :size=>[19,2]}
  when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?\z/o
    s = [($1.to_i if $1), ($2.to_i if $2)].compact
    {:type=>BigDecimal, :size=>(s.empty? ? nil : s)}
  when /\A(?:bytea|(?:tiny|medium|long)?blob|(?:var)?binary)(?:\((\d+)\))?\z/o
    {:type=>File, :size=>($1.to_i if $1)}
  when /\A(?:year|(?:int )?identity)\z/o
    {:type=>Integer}
  else
    {:type=>String}
  end
end
dump_foreign_key_migration(options=OPTS) click to toggle source

Dump foreign key constraints for all tables as a migration. This complements the :foreign_keys=>false option to dump_schema_migration. This only dumps the constraints (not the columns) using alter_table/add_foreign_key with an array of columns.

Note that the migration this produces does not have a down block, so you cannot reverse it.

# File lib/sequel/extensions/schema_dumper.rb, line 75
    def dump_foreign_key_migration(options=OPTS)
      ts = tables(options)
      <<END_MIG
Sequel.migration do
  change do
#{ts.sort_by(&:to_s).map{|t| dump_table_foreign_keys(t)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, '    ')}
  end
end
END_MIG
    end
dump_indexes_migration(options=OPTS) click to toggle source

Dump indexes for all tables as a migration. This complements the :indexes=>false option to dump_schema_migration. Options:

:same_db

Create a dump for the same database type, so don't ignore errors if the index statements fail.

:index_names

If set to false, don't record names of indexes. If set to :namespace, prepend the table name to the index name if the database does not use a global index namespace.

# File lib/sequel/extensions/schema_dumper.rb, line 93
    def dump_indexes_migration(options=OPTS)
      ts = tables(options)
      <<END_MIG
Sequel.migration do
  change do
#{ts.sort_by(&:to_s).map{|t| dump_table_indexes(t, :add_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, '    ')}
  end
end
END_MIG
    end
dump_schema_migration(options=OPTS) click to toggle source

Return a string that contains a Sequel::Migration subclass that when run would recreate the database structure. Options:

:same_db

Don't attempt to translate database types to ruby types. If this isn't set to true, all database types will be translated to ruby types, but there is no guarantee that the migration generated will yield the same type. Without this set, types that aren't recognized will be translated to a string-like type.

:foreign_keys

If set to false, don't dump foreign_keys (they can be added later via dump_foreign_key_migration)

:indexes

If set to false, don't dump indexes (they can be added later via dump_index_migration).

:index_names

If set to false, don't record names of indexes. If set to :namespace, prepend the table name to the index name.

# File lib/sequel/extensions/schema_dumper.rb, line 117
    def dump_schema_migration(options=OPTS)
      options = options.dup
      if options[:indexes] == false && !options.has_key?(:foreign_keys)
        # Unless foreign_keys option is specifically set, disable if indexes
        # are disabled, as foreign keys that point to non-primary keys rely
        # on unique indexes being created first
        options[:foreign_keys] = false
      end

      ts = sort_dumped_tables(tables(options), options)
      skipped_fks = if sfk = options[:skipped_foreign_keys]
        # Handle skipped foreign keys by adding them at the end via
        # alter_table/add_foreign_key.  Note that skipped foreign keys
        # probably result in a broken down migration.
        sfka = sfk.sort_by{|table, fks| table.to_s}.map{|table, fks| dump_add_fk_constraints(table, fks.values)}
        sfka.join("\n\n").gsub(/^/o, '    ') unless sfka.empty?
      end

      <<END_MIG
Sequel.migration do
  change do
#{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/o, '    ')}#{"\n    \n" if skipped_fks}#{skipped_fks}
  end
end
END_MIG
    end
dump_table_schema(table, options=OPTS) click to toggle source

Return a string with a create table block that will recreate the given table's schema. Takes the same options as dump_schema_migration.

# File lib/sequel/extensions/schema_dumper.rb, line 146
def dump_table_schema(table, options=OPTS)
  gen = dump_table_generator(table, options)
  commands = [gen.dump_columns, gen.dump_constraints, gen.dump_indexes].reject{|x| x == ''}.join("\n\n")
  "create_table(#{table.inspect}#{', :ignore_index_errors=>true' if !options[:same_db] && options[:indexes] != false && !gen.indexes.empty?}) do\n#{commands.gsub(/^/o, '  ')}\nend"
end

Private Instance Methods

column_schema_to_ruby_default_fallback(default, options) click to toggle source

If a database default exists and can't be converted, and we are dumping with :same_db, return a string with the inspect method modified a literal string is created if the code is evaled.

# File lib/sequel/extensions/schema_dumper.rb, line 156
def column_schema_to_ruby_default_fallback(default, options)
  if default.is_a?(String) && options[:same_db] && use_column_schema_to_ruby_default_fallback?
    default = default.dup
    def default.inspect
      "Sequel::LiteralString.new(#{super})"
    end
    default
  end
end
dump_add_fk_constraints(table, fks) click to toggle source

For the table and foreign key metadata array, return an alter_table string that would add the foreign keys if run in a migration.

# File lib/sequel/extensions/schema_dumper.rb, line 220
def dump_add_fk_constraints(table, fks)
  sfks = String.new
  sfks << "alter_table(#{table.inspect}) do\n"
  sfks << create_table_generator do
    fks.sort_by{|fk| fk[:columns].map(&:to_s)}.each do |fk|
      foreign_key fk[:columns], fk
    end
  end.dump_constraints.gsub(/^foreign_key /, '  add_foreign_key ')
  sfks << "\nend"
end
dump_table_foreign_keys(table, options=OPTS) click to toggle source

For the table given, get the list of foreign keys and return an alter_table string that would add the foreign keys if run in a migration.

# File lib/sequel/extensions/schema_dumper.rb, line 233
def dump_table_foreign_keys(table, options=OPTS)
  if supports_foreign_key_parsing?
    fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns].map(&:to_s)}
  end

  if fks.nil? || fks.empty?
    ''
  else
    dump_add_fk_constraints(table, fks)
  end
end
dump_table_generator(table, options=OPTS) click to toggle source

Return a Schema::Generator object that will recreate the table's schema. Takes the same options as dump_schema_migration.

# File lib/sequel/extensions/schema_dumper.rb, line 247
def dump_table_generator(table, options=OPTS)
  s = schema(table, options).dup
  pks = s.find_all{|x| x.last[:primary_key] == true}.map(&:first)
  options = options.merge(:single_pk=>true) if pks.length == 1
  m = method(:recreate_column)
  im = method(:index_to_generator_opts)

  if options[:indexes] != false && supports_index_parsing?
    indexes = indexes(table).sort_by{|k,v| k.to_s}
  end

  if options[:foreign_keys] != false && supports_foreign_key_parsing?
    fk_list = foreign_key_list(table)
    
    if (sfk = options[:skipped_foreign_keys]) && (sfkt = sfk[table])
      fk_list.delete_if{|fk| sfkt.has_key?(fk[:columns])}
    end

    composite_fks, single_fks = fk_list.partition{|h| h[:columns].length > 1}
    fk_hash = {}

    single_fks.each do |fk|
      column = fk.delete(:columns).first
      fk.delete(:name)
      fk_hash[column] = fk
    end

    s = s.map do |name, info|
      if fk_info = fk_hash[name]
        [name, fk_info.merge(info)]
      else
        [name, info]
      end
    end
  end

  create_table_generator do
    s.each{|name, info| m.call(name, info, self, options)}
    primary_key(pks) if !@primary_key && pks.length > 0
    indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes
    composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks
  end
end
dump_table_indexes(table, meth, options=OPTS) click to toggle source

Return a string that containing add_index/drop_index method calls for creating the index migration.

# File lib/sequel/extensions/schema_dumper.rb, line 293
def dump_table_indexes(table, meth, options=OPTS)
  if supports_index_parsing?
    indexes = indexes(table).sort_by{|k,v| k.to_s}
  else
    return ''
  end

  im = method(:index_to_generator_opts)
  gen = create_table_generator do
    indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))}
  end
  gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db])
end
index_to_generator_opts(table, name, index_opts, options=OPTS) click to toggle source

Convert the parsed index information into options to the Generators index method.

# File lib/sequel/extensions/schema_dumper.rb, line 308
def index_to_generator_opts(table, name, index_opts, options=OPTS)
  h = {}
  if options[:index_names] != false && default_index_name(table, index_opts[:columns]) != name.to_s
    if options[:index_names] == :namespace && !global_index_namespace?
      h[:name] = "#{table}_#{name}".to_sym
    else
      h[:name] = name
    end
  end
  h[:unique] = true if index_opts[:unique]
  h[:deferrable] = true if index_opts[:deferrable]
  h
end
recreate_column(name, schema, gen, options) click to toggle source

Recreate the column in the passed Schema::Generator from the given name and parsed database schema.

# File lib/sequel/extensions/schema_dumper.rb, line 167
def recreate_column(name, schema, gen, options)
  if options[:single_pk] && schema_autoincrementing_primary_key?(schema)
    type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
    [:table, :key, :on_delete, :on_update, :deferrable].each{|f| type_hash[f] = schema[f] if schema[f]}
    if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"}
      type_hash.delete(:type)
    elsif options[:same_db] && type_hash == {:type=>type_literal_generic_bignum_symbol(type_hash).to_s}
      type_hash[:type] = :Bignum
    end

    unless gen.columns.empty?
      type_hash[:keep_order] = true
    end

    if type_hash.empty?
      gen.primary_key(name)
    else
      gen.primary_key(name, type_hash)
    end
  else
    col_opts = if options[:same_db]
      h = {:type=>schema[:db_type]}
      if database_type == :mysql && h[:type] =~ /\Atimestamp/
        h[:null] = true
      end
      h
    else
      column_schema_to_ruby_type(schema)
    end
    type = col_opts.delete(:type)
    col_opts.delete(:size) if col_opts[:size].nil?
    col_opts[:default] = if schema[:ruby_default].nil?
      column_schema_to_ruby_default_fallback(schema[:default], options)
    else
      schema[:ruby_default]
    end
    col_opts.delete(:default) if col_opts[:default].nil?
    col_opts[:null] = false if schema[:allow_null] == false
    if table = schema[:table]
      [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]}
      col_opts[:type] = type unless type == Integer || type == 'integer'
      gen.foreign_key(name, table, col_opts)
    else
      gen.column(name, type, col_opts)
      if [Integer, :Bignum, Float].include?(type) && schema[:db_type] =~ / unsigned\z/io
        gen.check(Sequel::SQL::Identifier.new(name) >= 0)
      end
    end
  end
end
sort_dumped_tables(tables, options=OPTS) click to toggle source

Sort the tables so that referenced tables are created before tables that reference them, and then by name. If foreign keys are disabled, just sort by name.

# File lib/sequel/extensions/schema_dumper.rb, line 324
def sort_dumped_tables(tables, options=OPTS)
  if options[:foreign_keys] != false && supports_foreign_key_parsing?
    table_fks = {}
    tables.each{|t| table_fks[t] = foreign_key_list(t)}
    # Remove self referential foreign keys, not important when sorting.
    table_fks.each{|t, fks| fks.delete_if{|fk| fk[:table] == t}}
    tables, skipped_foreign_keys = sort_dumped_tables_topologically(table_fks, [])
    options[:skipped_foreign_keys] = skipped_foreign_keys
    tables
  else
    tables.sort_by(&:to_s)
  end
end
sort_dumped_tables_topologically(table_fks, sorted_tables) click to toggle source

Do a topological sort of tables, so that referenced tables come before referencing tables. Returns an array of sorted tables and a hash of skipped foreign keys. The hash will be empty unless there are circular dependencies.

# File lib/sequel/extensions/schema_dumper.rb, line 342
def sort_dumped_tables_topologically(table_fks, sorted_tables)
  skipped_foreign_keys = {}

  until table_fks.empty? 
    this_loop = []

    table_fks.each do |table, fks|
      fks.delete_if{|fk| !table_fks.has_key?(fk[:table])}
      this_loop << table if fks.empty?
    end

    if this_loop.empty?
      # No tables were changed this round, there must be a circular dependency.
      # Break circular dependency by picking the table with the least number of
      # outstanding foreign keys and skipping those foreign keys.
      # The skipped foreign keys will be added at the end of the
      # migration.
      skip_table, skip_fks = table_fks.sort_by{|table, fks| [fks.length, table.to_s]}.first
      skip_fks_hash = skipped_foreign_keys[skip_table] = {}
      skip_fks.each{|fk| skip_fks_hash[fk[:columns]] = fk}
      this_loop << skip_table
    end

    # Add sorted tables from this loop to the final list
    sorted_tables.concat(this_loop.sort_by(&:to_s))

    # Remove tables that were handled this loop
    this_loop.each{|t| table_fks.delete(t)}
  end

  [sorted_tables, skipped_foreign_keys]
end
use_column_schema_to_ruby_default_fallback?() click to toggle source

Don't use a literal string fallback on MySQL, since the defaults it uses aren't valid literal SQL values.

# File lib/sequel/extensions/schema_dumper.rb, line 377
def use_column_schema_to_ruby_default_fallback?
  database_type != :mysql
end