module Sequel::SchemaDumper
Public Instance Methods
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 32 def column_schema_to_ruby_type(schema) type = schema[:db_type].downcase if database_type == :oracle type = type.sub(/ not null\z/, '') end case type when /\A(medium|small)?int(?:eger)?(?:\((\d+)\))?( unsigned)?\z/ 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/ {:type =>schema[:type] == :boolean ? TrueClass : Integer} when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/ {:type=>:Bignum} when /\A(?:real|float|double(?: precision)?|double\(\d+,\d+\))(?: unsigned)?\z/ {:type=>Float} when 'boolean', 'bit', 'bool' {:type=>TrueClass} when /\A(?:(?:tiny|medium|long|n)?text|clob)\z/ {:type=>String, :text=>true} when 'date' {:type=>Date} when /\A(?:small)?datetime\z/ {:type=>DateTime} when /\Atimestamp(?:\((\d+)\))?(?: with(?:out)? time zone)?\z/ {:type=>DateTime, :size=>($1.to_i if $1)} when /\Atime(?: with(?:out)? time zone)?\z/ {:type=>Time, :only_time=>true} when /\An?char(?:acter)?(?:\((\d+)\))?\z/ {:type=>String, :size=>($1.to_i if $1), :fixed=>true} when /\A(?:n?varchar2?|character varying|bpchar|string)(?:\((\d+)\))?\z/ {:type=>String, :size=>($1.to_i if $1)} when /\A(?:small)?money\z/ {:type=>BigDecimal, :size=>[19,2]} when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?(?: unsigned)?\z/ 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/ {:type=>File, :size=>($1.to_i if $1)} when /\A(?:year|(?:int )?identity)\z/ {:type=>Integer} else {:type=>String} end end
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 89 def dump_foreign_key_migration(options=OPTS) ts = _dump_tables(options) <<END_MIG Sequel.migration do change do #{ts.map{|t| dump_table_foreign_keys(t)}.reject{|x| x == ''}.join("\n\n").gsub(/^/, ' ')} end end END_MIG end
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 107 def dump_indexes_migration(options=OPTS) ts = _dump_tables(options) <<END_MIG Sequel.migration do change do #{ts.map{|t| dump_table_indexes(t, :add_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/, ' ')} end end END_MIG end
Return a string that contains a Sequel migration 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 131 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(_dump_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.map{|table, fks| dump_add_fk_constraints(table, fks.values)} sfka.join("\n\n").gsub(/^/, ' ') unless sfka.empty? end <<END_MIG Sequel.migration do change do #{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/, ' ')}#{"\n \n" if skipped_fks}#{skipped_fks} end end END_MIG end
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 160 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(/^/, ' ')}\nend" end
Private Instance Methods
Handle schema option to dump tables in a different schema. Such tables must be schema qualified for this to work correctly.
# File lib/sequel/extensions/schema_dumper.rb, line 170 def _dump_tables(opts) if opts[:schema] _literal_table_sort(tables(opts.merge(:qualify=>true))) else tables(opts).sort end end
Sort the given table by the literalized value.
# File lib/sequel/extensions/schema_dumper.rb, line 179 def _literal_table_sort(tables) tables.sort_by{|s| literal(s)} end
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 185 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
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 263 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]}.each do |fk| foreign_key fk[:columns], fk end end.dump_constraints.gsub(/^foreign_key /, ' add_foreign_key ') sfks << "\nend" end
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 276 def dump_table_foreign_keys(table, options=OPTS) if supports_foreign_key_parsing? fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns]} end if fks.nil? || fks.empty? '' else dump_add_fk_constraints(table, fks) end end
Return a Schema::CreateTableGenerator object that will recreate the table's schema. Takes the same options as dump_schema_migration.
# File lib/sequel/extensions/schema_dumper.rb, line 290 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 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
Return a string that containing add_index/drop_index method calls for creating the index migration.
# File lib/sequel/extensions/schema_dumper.rb, line 336 def dump_table_indexes(table, meth, options=OPTS) if supports_index_parsing? indexes = indexes(table).sort 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
Convert the parsed index information into options to the CreateTableGenerator's index method.
# File lib/sequel/extensions/schema_dumper.rb, line 351 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 the column in the passed Schema::CreateTableGenerator from the given name and parsed database schema.
# File lib/sequel/extensions/schema_dumper.rb, line 196 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 == {: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 if database_type == :mssql && schema[:max_length] h[:size] = schema[:max_length] end h else column_schema_to_ruby_type(schema) end type = col_opts.delete(:type) if col_opts.key?(:size) && col_opts[:size].nil? col_opts.delete(:size) if max_length = schema[:max_length] col_opts[:size] = max_length end end if schema[:generated] if options[:same_db] && database_type == :postgres col_opts[:generated_always_as] = column_schema_to_ruby_default_fallback(schema[:default], options) end else 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? end 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' || type == 'INTEGER' gen.foreign_key(name, table, col_opts) else gen.column(name, type, col_opts) if [Integer, :Bignum, Float, BigDecimal].include?(type) && schema[:db_type] =~ / unsigned\z/io gen.check(Sequel::SQL::Identifier.new(name) >= 0) end end end end
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 367 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 end end
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 385 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, literal(table)]}.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(_literal_table_sort(this_loop)) # Remove tables that were handled this loop this_loop.each{|t| table_fks.delete(t)} end [sorted_tables, skipped_foreign_keys] end