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 22 def column_schema_to_ruby_type(schema) 23 type = schema[:db_type].downcase 24 if database_type == :oracle 25 type = type.sub(/ not null\z/, '') 26 end 27 case type 28 when /\A(medium|small)?int(?:eger)?(?:\((\d+)\))?( unsigned)?\z/ 29 if !$1 && $2 && $2.to_i >= 10 && $3 30 # Unsigned integer type with 10 digits can potentially contain values which 31 # don't fit signed integer type, so use bigint type in target database. 32 {:type=>:Bignum} 33 else 34 {:type=>Integer} 35 end 36 when /\Atinyint(?:\((\d+)\))?(?: unsigned)?\z/ 37 {:type =>schema[:type] == :boolean ? TrueClass : Integer} 38 when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/ 39 {:type=>:Bignum} 40 when /\A(?:real|float(?: unsigned)?|double(?: precision)?|double\(\d+,\d+\)(?: unsigned)?)\z/ 41 {:type=>Float} 42 when 'boolean', 'bit', 'bool' 43 {:type=>TrueClass} 44 when /\A(?:(?:tiny|medium|long|n)?text|clob)\z/ 45 {:type=>String, :text=>true} 46 when 'date' 47 {:type=>Date} 48 when /\A(?:small)?datetime\z/ 49 {:type=>DateTime} 50 when /\Atimestamp(?:\((\d+)\))?(?: with(?:out)? time zone)?\z/ 51 {:type=>DateTime, :size=>($1.to_i if $1)} 52 when /\Atime(?: with(?:out)? time zone)?\z/ 53 {:type=>Time, :only_time=>true} 54 when /\An?char(?:acter)?(?:\((\d+)\))?\z/ 55 {:type=>String, :size=>($1.to_i if $1), :fixed=>true} 56 when /\A(?:n?varchar2?|character varying|bpchar|string)(?:\((\d+)\))?\z/ 57 {:type=>String, :size=>($1.to_i if $1)} 58 when /\A(?:small)?money\z/ 59 {:type=>BigDecimal, :size=>[19,2]} 60 when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?\z/ 61 s = [($1.to_i if $1), ($2.to_i if $2)].compact 62 {:type=>BigDecimal, :size=>(s.empty? ? nil : s)} 63 when /\A(?:bytea|(?:tiny|medium|long)?blob|(?:var)?binary)(?:\((\d+)\))?\z/ 64 {:type=>File, :size=>($1.to_i if $1)} 65 when /\A(?:year|(?:int )?identity)\z/ 66 {:type=>Integer} 67 else 68 {:type=>String} 69 end 70 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 79 def dump_foreign_key_migration(options=OPTS) 80 ts = tables(options) 81 <<END_MIG 82 Sequel.migration do 83 change do 84 #{ts.sort.map{|t| dump_table_foreign_keys(t)}.reject{|x| x == ''}.join("\n\n").gsub(/^/, ' ')} 85 end 86 end 87 END_MIG 88 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 97 def dump_indexes_migration(options=OPTS) 98 ts = tables(options) 99 <<END_MIG 100 Sequel.migration do 101 change do 102 #{ts.sort.map{|t| dump_table_indexes(t, :add_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/, ' ')} 103 end 104 end 105 END_MIG 106 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 121 def dump_schema_migration(options=OPTS) 122 options = options.dup 123 if options[:indexes] == false && !options.has_key?(:foreign_keys) 124 # Unless foreign_keys option is specifically set, disable if indexes 125 # are disabled, as foreign keys that point to non-primary keys rely 126 # on unique indexes being created first 127 options[:foreign_keys] = false 128 end 129 130 ts = sort_dumped_tables(tables(options), options) 131 skipped_fks = if sfk = options[:skipped_foreign_keys] 132 # Handle skipped foreign keys by adding them at the end via 133 # alter_table/add_foreign_key. Note that skipped foreign keys 134 # probably result in a broken down migration. 135 sfka = sfk.sort.map{|table, fks| dump_add_fk_constraints(table, fks.values)} 136 sfka.join("\n\n").gsub(/^/, ' ') unless sfka.empty? 137 end 138 139 <<END_MIG 140 Sequel.migration do 141 change do 142 #{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/, ' ')}#{"\n \n" if skipped_fks}#{skipped_fks} 143 end 144 end 145 END_MIG 146 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 150 def dump_table_schema(table, options=OPTS) 151 gen = dump_table_generator(table, options) 152 commands = [gen.dump_columns, gen.dump_constraints, gen.dump_indexes].reject{|x| x == ''}.join("\n\n") 153 "create_table(#{table.inspect}#{', :ignore_index_errors=>true' if !options[:same_db] && options[:indexes] != false && !gen.indexes.empty?}) do\n#{commands.gsub(/^/, ' ')}\nend" 154 end
Private Instance Methods
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 160 def column_schema_to_ruby_default_fallback(default, options) 161 if default.is_a?(String) && options[:same_db] && use_column_schema_to_ruby_default_fallback? 162 default = default.dup 163 def default.inspect 164 "Sequel::LiteralString.new(#{super})" 165 end 166 default 167 end 168 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 224 def dump_add_fk_constraints(table, fks) 225 sfks = String.new 226 sfks << "alter_table(#{table.inspect}) do\n" 227 sfks << create_table_generator do 228 fks.sort_by{|fk| fk[:columns]}.each do |fk| 229 foreign_key fk[:columns], fk 230 end 231 end.dump_constraints.gsub(/^foreign_key /, ' add_foreign_key ') 232 sfks << "\nend" 233 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 237 def dump_table_foreign_keys(table, options=OPTS) 238 if supports_foreign_key_parsing? 239 fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns]} 240 end 241 242 if fks.nil? || fks.empty? 243 '' 244 else 245 dump_add_fk_constraints(table, fks) 246 end 247 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 251 def dump_table_generator(table, options=OPTS) 252 s = schema(table, options).dup 253 pks = s.find_all{|x| x.last[:primary_key] == true}.map(&:first) 254 options = options.merge(:single_pk=>true) if pks.length == 1 255 m = method(:recreate_column) 256 im = method(:index_to_generator_opts) 257 258 if options[:indexes] != false && supports_index_parsing? 259 indexes = indexes(table).sort 260 end 261 262 if options[:foreign_keys] != false && supports_foreign_key_parsing? 263 fk_list = foreign_key_list(table) 264 265 if (sfk = options[:skipped_foreign_keys]) && (sfkt = sfk[table]) 266 fk_list.delete_if{|fk| sfkt.has_key?(fk[:columns])} 267 end 268 269 composite_fks, single_fks = fk_list.partition{|h| h[:columns].length > 1} 270 fk_hash = {} 271 272 single_fks.each do |fk| 273 column = fk.delete(:columns).first 274 fk.delete(:name) 275 fk_hash[column] = fk 276 end 277 278 s = s.map do |name, info| 279 if fk_info = fk_hash[name] 280 [name, fk_info.merge(info)] 281 else 282 [name, info] 283 end 284 end 285 end 286 287 create_table_generator do 288 s.each{|name, info| m.call(name, info, self, options)} 289 primary_key(pks) if !@primary_key && pks.length > 0 290 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes 291 composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks 292 end 293 end
Return a string that containing add_index/drop_index method calls for creating the index migration.
# File lib/sequel/extensions/schema_dumper.rb 297 def dump_table_indexes(table, meth, options=OPTS) 298 if supports_index_parsing? 299 indexes = indexes(table).sort 300 else 301 return '' 302 end 303 304 im = method(:index_to_generator_opts) 305 gen = create_table_generator do 306 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} 307 end 308 gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db]) 309 end
Convert the parsed index information into options to the CreateTableGenerator's index method.
# File lib/sequel/extensions/schema_dumper.rb 312 def index_to_generator_opts(table, name, index_opts, options=OPTS) 313 h = {} 314 if options[:index_names] != false && default_index_name(table, index_opts[:columns]) != name.to_s 315 if options[:index_names] == :namespace && !global_index_namespace? 316 h[:name] = "#{table}_#{name}".to_sym 317 else 318 h[:name] = name 319 end 320 end 321 h[:unique] = true if index_opts[:unique] 322 h[:deferrable] = true if index_opts[:deferrable] 323 h 324 end
Recreate the column in the passed Schema::CreateTableGenerator
from the given name and parsed database schema.
# File lib/sequel/extensions/schema_dumper.rb 171 def recreate_column(name, schema, gen, options) 172 if options[:single_pk] && schema_autoincrementing_primary_key?(schema) 173 type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema) 174 [:table, :key, :on_delete, :on_update, :deferrable].each{|f| type_hash[f] = schema[f] if schema[f]} 175 if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"} 176 type_hash.delete(:type) 177 elsif options[:same_db] && type_hash == {:type=>type_literal_generic_bignum_symbol(type_hash).to_s} 178 type_hash[:type] = :Bignum 179 end 180 181 unless gen.columns.empty? 182 type_hash[:keep_order] = true 183 end 184 185 if type_hash.empty? 186 gen.primary_key(name) 187 else 188 gen.primary_key(name, type_hash) 189 end 190 else 191 col_opts = if options[:same_db] 192 h = {:type=>schema[:db_type]} 193 if database_type == :mysql && h[:type] =~ /\Atimestamp/ 194 h[:null] = true 195 end 196 h 197 else 198 column_schema_to_ruby_type(schema) 199 end 200 type = col_opts.delete(:type) 201 col_opts.delete(:size) if col_opts[:size].nil? 202 col_opts[:default] = if schema[:ruby_default].nil? 203 column_schema_to_ruby_default_fallback(schema[:default], options) 204 else 205 schema[:ruby_default] 206 end 207 col_opts.delete(:default) if col_opts[:default].nil? 208 col_opts[:null] = false if schema[:allow_null] == false 209 if table = schema[:table] 210 [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]} 211 col_opts[:type] = type unless type == Integer || type == 'integer' 212 gen.foreign_key(name, table, col_opts) 213 else 214 gen.column(name, type, col_opts) 215 if [Integer, :Bignum, Float].include?(type) && schema[:db_type] =~ / unsigned\z/io 216 gen.check(Sequel::SQL::Identifier.new(name) >= 0) 217 end 218 end 219 end 220 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 328 def sort_dumped_tables(tables, options=OPTS) 329 if options[:foreign_keys] != false && supports_foreign_key_parsing? 330 table_fks = {} 331 tables.each{|t| table_fks[t] = foreign_key_list(t)} 332 # Remove self referential foreign keys, not important when sorting. 333 table_fks.each{|t, fks| fks.delete_if{|fk| fk[:table] == t}} 334 tables, skipped_foreign_keys = sort_dumped_tables_topologically(table_fks, []) 335 options[:skipped_foreign_keys] = skipped_foreign_keys 336 tables 337 else 338 tables.sort 339 end 340 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 346 def sort_dumped_tables_topologically(table_fks, sorted_tables) 347 skipped_foreign_keys = {} 348 349 until table_fks.empty? 350 this_loop = [] 351 352 table_fks.each do |table, fks| 353 fks.delete_if{|fk| !table_fks.has_key?(fk[:table])} 354 this_loop << table if fks.empty? 355 end 356 357 if this_loop.empty? 358 # No tables were changed this round, there must be a circular dependency. 359 # Break circular dependency by picking the table with the least number of 360 # outstanding foreign keys and skipping those foreign keys. 361 # The skipped foreign keys will be added at the end of the 362 # migration. 363 skip_table, skip_fks = table_fks.sort_by{|table, fks| [fks.length, table]}.first 364 skip_fks_hash = skipped_foreign_keys[skip_table] = {} 365 skip_fks.each{|fk| skip_fks_hash[fk[:columns]] = fk} 366 this_loop << skip_table 367 end 368 369 # Add sorted tables from this loop to the final list 370 sorted_tables.concat(this_loop.sort) 371 372 # Remove tables that were handled this loop 373 this_loop.each{|t| table_fks.delete(t)} 374 end 375 376 [sorted_tables, skipped_foreign_keys] 377 end