# The schema_dumper extension supports dumping tables and indexes # in a Sequel::Migration format, so they can be restored on another # database (which can be the same type or a different type than # the current database). The main interface is through # Sequel::Database#dump_schema_migration. module Sequel class Database # 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. def dump_foreign_key_migration(options={}) ts = tables(options) <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. def dump_indexes_migration(options={}) ts = tables(options) <true' if !options[:same_db] && options[:indexes] != false && !gen.indexes.empty?}) do\n#{commands.gsub(/^/o, ' ')}\nend" end private # If a database default exists and can't be converted, return the string with the inspect # method modified so that .lit is always appended after it, only if the # :same_db option is used. 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.to_s def default.inspect "#{super}.lit" # core_sql use end default end end # Convert the given name and parsed database schema into an array with a method # name and arguments to it to pass to a Schema::Generator to recreate the column. def column_schema_to_generator_opts(name, schema, 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"} [:primary_key, name] else [:primary_key, name, type_hash] end else col_opts = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema) 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]} [:foreign_key, name, table, col_opts] else [:column, name, type, col_opts] end end end # 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. def column_schema_to_ruby_type(schema) case t = schema[:db_type].downcase when /\A(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?(?: unsigned)?\z/o {:type=>Integer} 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)?)\z/o {:type=>Float} when 'boolean' {: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 'year' {:type=>Integer} else {:type=>String} 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. def dump_add_fk_constraints(table, fks) sfks = "alter_table(#{table.inspect}) do\n" sfks << Schema::Generator.new(self) do fks.sort_by{|fk| fk[:columns].map{|c| c.to_s}}.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. def dump_table_foreign_keys(table, options={}) begin fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns].map{|c| c.to_s}} rescue Sequel::NotImplemented return '' end if fks.empty? '' else dump_add_fk_constraints(table, fks) end end # Return a Schema::Generator object that will recreate the # table's schema. Takes the same options as dump_schema_migration. def dump_table_generator(table, options={}) table = table.value.to_s if table.is_a?(SQL::Identifier) raise(Error, "must provide table as a Symbol, String, or Sequel::SQL::Identifier") unless [String, Symbol].any?{|c| table.is_a?(c)} s = schema(table).dup pks = s.find_all{|x| x.last[:primary_key] == true}.map{|x| x.first} options = options.merge(:single_pk=>true) if pks.length == 1 m = method(:column_schema_to_generator_opts) im = method(:index_to_generator_opts) if options[:indexes] != false begin indexes = indexes(table).sort_by{|k,v| k.to_s} rescue Sequel::NotImplemented nil end end if options[:foreign_keys] != false begin 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 rescue Sequel::NotImplemented nil end end Schema::Generator.new(self) do s.each{|name, info| send(*m.call(name, info, options))} primary_key(pks) if !@primary_key && pks.length > 0 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))} 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. def dump_table_indexes(table, meth, options={}) begin indexes = indexes(table).sort_by{|k,v| k.to_s} rescue Sequel::NotImplemented return '' end im = method(:index_to_generator_opts) gen = Schema::Generator.new(self) do indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))} end gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db]) end # Convert the parsed index information into options to the Generators index method. def index_to_generator_opts(table, name, index_opts) h = {} h[:name] = name unless default_index_name(table, index_opts[:columns]) == name.to_s h[:unique] = true if index_opts[:unique] h 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. def sort_dumped_tables(tables, options={}) sort_topologically = if options[:foreign_keys] != false begin foreign_key_list(:some_table_that_does_not_exist) true rescue Sequel::NotImplemented false rescue true end end if sort_topologically 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{|t| t.to_s} 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. 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{|t| t.to_s}) # Remove tables that were handled this loop this_loop.each{|t| table_fks.delete(t)} end [sorted_tables, skipped_foreign_keys] end # Don't use the "...".lit fallback on MySQL, since the defaults it uses aren't # valid literal SQL values. def use_column_schema_to_ruby_default_fallback? database_type != :mysql end end module Schema class Generator # Dump this generator's columns to a string that could be evaled inside # another instance to represent the same columns def dump_columns strings = [] cols = columns.dup cols.each do |x| x.delete(:on_delete) if x[:on_delete] == :no_action x.delete(:on_update) if x[:on_update] == :no_action end if pkn = primary_key_name cols.delete_if{|x| x[:name] == pkn} pk = @primary_key.dup pkname = pk.delete(:name) @db.serial_primary_key_options.each{|k,v| pk.delete(k) if v == pk[k]} strings << "primary_key #{pkname.inspect}#{opts_inspect(pk)}" end cols.each do |c| c = c.dup name = c.delete(:name) strings << if table = c.delete(:table) c.delete(:type) if c[:type] == Integer || c[:type] == 'integer' "foreign_key #{name.inspect}, #{table.inspect}#{opts_inspect(c)}" else type = c.delete(:type) opts = opts_inspect(c) if type.is_a?(Class) "#{type.name} #{name.inspect}#{opts}" else "column #{name.inspect}, #{type.inspect}#{opts}" end end end strings.join("\n") end # Dump this generator's constraints to a string that could be evaled inside # another instance to represent the same constraints def dump_constraints cs = constraints.map do |c| c = c.dup type = c.delete(:type) case type when :check raise(Error, "can't dump check/constraint specified with Proc") if c[:check].is_a?(Proc) name = c.delete(:name) if !name and c[:check].length == 1 and c[:check].first.is_a?(Hash) "check #{c[:check].first.inspect[1...-1]}" else "#{name ? "constraint #{name.inspect}," : 'check'} #{c[:check].map{|x| x.inspect}.join(', ')}" end when :foreign_key c.delete(:on_delete) if c[:on_delete] == :no_action c.delete(:on_update) if c[:on_update] == :no_action c.delete(:deferrable) unless c[:deferrable] cols = c.delete(:columns) table = c.delete(:table) "#{type} #{cols.inspect}, #{table.inspect}#{opts_inspect(c)}" else cols = c.delete(:columns) "#{type} #{cols.inspect}#{opts_inspect(c)}" end end cs.join("\n") end # Dump this generator's indexes to a string that could be evaled inside # another instance to represent the same indexes. Options: # * :add_index - Use add_index instead of index, so the methods # can be called outside of a generator but inside a migration. # The value of this option should be the table name to use. # * :drop_index - Same as add_index, but create drop_index statements. # * :ignore_errors - Add the ignore_errors option to the outputted indexes def dump_indexes(options={}) is = indexes.map do |c| c = c.dup cols = c.delete(:columns) if table = options[:add_index] || options[:drop_index] "#{options[:drop_index] ? 'drop' : 'add'}_index #{table.inspect}, #{cols.inspect}#{', :ignore_errors=>true' if options[:ignore_errors]}#{opts_inspect(c)}" else "index #{cols.inspect}#{opts_inspect(c)}" end end is.join("\n") end private # Return a string that converts the given options into one # suitable for literal ruby code, handling default values # that don't default to a literal interpretation. def opts_inspect(opts) if opts[:default] opts = opts.dup de = case d = opts.delete(:default) when BigDecimal, Sequel::SQL::Blob "#{d.class.name}.new(#{d.to_s.inspect})" when DateTime, Date "#{d.class.name}.parse(#{d.to_s.inspect})" when Time "#{d.class.name}.parse(#{d.strftime('%H:%M:%S').inspect})" else d.inspect end ", :default=>#{de}#{", #{opts.inspect[1...-1]}" if opts.length > 0}" else ", #{opts.inspect[1...-1]}" if opts.length > 0 end end end end end