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 33 def column_schema_to_ruby_type(schema) 34 type = schema[:db_type].downcase 35 if database_type == :oracle 36 type = type.sub(/ not null\z/, '') 37 end 38 case type 39 when /\A(medium|small)?int(?:eger)?(?:\((\d+)\))?( unsigned)?\z/ 40 if !$1 && $2 && $2.to_i >= 10 && $3 41 # Unsigned integer type with 10 digits can potentially contain values which 42 # don't fit signed integer type, so use bigint type in target database. 43 {:type=>:Bignum} 44 else 45 {:type=>Integer} 46 end 47 when /\Atinyint(?:\((\d+)\))?(?: unsigned)?\z/ 48 {:type =>schema[:type] == :boolean ? TrueClass : Integer} 49 when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/ 50 {:type=>:Bignum} 51 when /\A(?:real|float|double(?: precision)?|double\(\d+,\d+\))(?: unsigned)?\z/ 52 {:type=>Float} 53 when 'boolean', 'bit', 'bool' 54 {:type=>TrueClass} 55 when /\A(?:(?:tiny|medium|long|n)?text|clob)\z/ 56 {:type=>String, :text=>true} 57 when 'date' 58 {:type=>Date} 59 when /\A(?:small)?datetime\z/ 60 {:type=>DateTime} 61 when /\Atimestamp(?:\((\d+)\))?(?: with(?:out)? time zone)?\z/ 62 {:type=>DateTime, :size=>($1.to_i if $1)} 63 when /\Atime(?: with(?:out)? time zone)?\z/ 64 {:type=>Time, :only_time=>true} 65 when /\An?char(?:acter)?(?:\((\d+)\))?\z/ 66 {:type=>String, :size=>($1.to_i if $1), :fixed=>true} 67 when /\A(?:n?varchar2?|character varying|bpchar|string)(?:\((\d+)\))?\z/ 68 {:type=>String, :size=>($1.to_i if $1)} 69 when /\A(?:small)?money\z/ 70 {:type=>BigDecimal, :size=>[19,2]} 71 when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?(?: unsigned)?\z/ 72 s = [($1.to_i if $1), ($2.to_i if $2)].compact 73 {:type=>BigDecimal, :size=>(s.empty? ? nil : s)} 74 when /\A(?:bytea|(?:tiny|medium|long)?blob|(?:var)?binary)(?:\((\d+)\))?\z/ 75 {:type=>File, :size=>($1.to_i if $1)} 76 when /\A(?:year|(?:int )?identity)\z/ 77 {:type=>Integer} 78 else 79 {:type=>String} 80 end 81 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 90 def dump_foreign_key_migration(options=OPTS) 91 ts = _dump_tables(options) 92 <<END_MIG 93 Sequel.migration do 94 change do 95 #{ts.map{|t| dump_table_foreign_keys(t)}.reject{|x| x == ''}.join("\n\n").gsub(/^/, ' ')} 96 end 97 end 98 END_MIG 99 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 108 def dump_indexes_migration(options=OPTS) 109 ts = _dump_tables(options) 110 <<END_MIG 111 Sequel.migration do 112 change do 113 #{ts.map{|t| dump_table_indexes(t, :add_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/, ' ')} 114 end 115 end 116 END_MIG 117 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 132 def dump_schema_migration(options=OPTS) 133 options = options.dup 134 if options[:indexes] == false && !options.has_key?(:foreign_keys) 135 # Unless foreign_keys option is specifically set, disable if indexes 136 # are disabled, as foreign keys that point to non-primary keys rely 137 # on unique indexes being created first 138 options[:foreign_keys] = false 139 end 140 141 ts = sort_dumped_tables(_dump_tables(options), options) 142 skipped_fks = if sfk = options[:skipped_foreign_keys] 143 # Handle skipped foreign keys by adding them at the end via 144 # alter_table/add_foreign_key. Note that skipped foreign keys 145 # probably result in a broken down migration. 146 sfka = sfk.sort.map{|table, fks| dump_add_fk_constraints(table, fks.values)} 147 sfka.join("\n\n").gsub(/^/, ' ') unless sfka.empty? 148 end 149 150 <<END_MIG 151 Sequel.migration do 152 change do 153 #{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/, ' ')}#{"\n \n" if skipped_fks}#{skipped_fks} 154 end 155 end 156 END_MIG 157 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 161 def dump_table_schema(table, options=OPTS) 162 gen = dump_table_generator(table, options) 163 commands = [gen.dump_columns, gen.dump_constraints, gen.dump_indexes].reject{|x| x == ''}.join("\n\n") 164 "create_table(#{table.inspect}#{', :ignore_index_errors=>true' if !options[:same_db] && options[:indexes] != false && !gen.indexes.empty?}) do\n#{commands.gsub(/^/, ' ')}\nend" 165 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 171 def _dump_tables(opts) 172 if opts[:schema] 173 _literal_table_sort(tables(opts.merge(:qualify=>true))) 174 else 175 tables(opts).sort 176 end 177 end
Sort the given table by the literalized value.
# File lib/sequel/extensions/schema_dumper.rb 180 def _literal_table_sort(tables) 181 tables.sort_by{|s| literal(s)} 182 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 186 def column_schema_to_ruby_default_fallback(default, options) 187 if default.is_a?(String) && options[:same_db] && use_column_schema_to_ruby_default_fallback? 188 default = default.dup 189 def default.inspect 190 "Sequel::LiteralString.new(#{super})" 191 end 192 default 193 end 194 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 264 def dump_add_fk_constraints(table, fks) 265 sfks = String.new 266 sfks << "alter_table(#{table.inspect}) do\n" 267 sfks << create_table_generator do 268 fks.sort_by{|fk| fk[:columns]}.each do |fk| 269 foreign_key fk[:columns], fk 270 end 271 end.dump_constraints.gsub(/^foreign_key /, ' add_foreign_key ') 272 sfks << "\nend" 273 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 277 def dump_table_foreign_keys(table, options=OPTS) 278 if supports_foreign_key_parsing? 279 fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns]} 280 end 281 282 if fks.nil? || fks.empty? 283 '' 284 else 285 dump_add_fk_constraints(table, fks) 286 end 287 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 291 def dump_table_generator(table, options=OPTS) 292 s = schema(table, options).dup 293 pks = s.find_all{|x| x.last[:primary_key] == true}.map(&:first) 294 options = options.merge(:single_pk=>true) if pks.length == 1 295 m = method(:recreate_column) 296 im = method(:index_to_generator_opts) 297 298 if options[:indexes] != false && supports_index_parsing? 299 indexes = indexes(table).sort 300 end 301 302 if options[:foreign_keys] != false && supports_foreign_key_parsing? 303 fk_list = foreign_key_list(table) 304 305 if (sfk = options[:skipped_foreign_keys]) && (sfkt = sfk[table]) 306 fk_list.delete_if{|fk| sfkt.has_key?(fk[:columns])} 307 end 308 309 composite_fks, single_fks = fk_list.partition{|h| h[:columns].length > 1} 310 fk_hash = {} 311 312 single_fks.each do |fk| 313 column = fk.delete(:columns).first 314 fk.delete(:name) 315 fk_hash[column] = fk 316 end 317 318 s = s.map do |name, info| 319 if fk_info = fk_hash[name] 320 [name, fk_info.merge(info)] 321 else 322 [name, info] 323 end 324 end 325 end 326 327 create_table_generator do 328 s.each{|name, info| m.call(name, info, self, options)} 329 primary_key(pks) if !@primary_key && pks.length > 0 330 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes 331 composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks 332 end 333 end
Return a string that containing add_index/drop_index method calls for creating the index migration.
# File lib/sequel/extensions/schema_dumper.rb 337 def dump_table_indexes(table, meth, options=OPTS) 338 if supports_index_parsing? 339 indexes = indexes(table).sort 340 else 341 return '' 342 end 343 344 im = method(:index_to_generator_opts) 345 gen = create_table_generator do 346 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} 347 end 348 gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db]) 349 end
Convert the parsed index information into options to the CreateTableGenerator’s index method.
# File lib/sequel/extensions/schema_dumper.rb 352 def index_to_generator_opts(table, name, index_opts, options=OPTS) 353 h = {} 354 if options[:index_names] != false && default_index_name(table, index_opts[:columns]) != name.to_s 355 if options[:index_names] == :namespace && !global_index_namespace? 356 h[:name] = "#{table}_#{name}".to_sym 357 else 358 h[:name] = name 359 end 360 end 361 h[:unique] = true if index_opts[:unique] 362 h[:deferrable] = true if index_opts[:deferrable] 363 h 364 end
Recreate the column in the passed Schema::CreateTableGenerator
from the given name and parsed database schema.
# File lib/sequel/extensions/schema_dumper.rb 197 def recreate_column(name, schema, gen, options) 198 if options[:single_pk] && schema_autoincrementing_primary_key?(schema) 199 type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema) 200 [:table, :key, :on_delete, :on_update, :deferrable].each{|f| type_hash[f] = schema[f] if schema[f]} 201 if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"} || type_hash == {:type=>"INTEGER"} 202 type_hash.delete(:type) 203 elsif options[:same_db] && type_hash == {:type=>type_literal_generic_bignum_symbol(type_hash).to_s} 204 type_hash[:type] = :Bignum 205 end 206 207 unless gen.columns.empty? 208 type_hash[:keep_order] = true 209 end 210 211 if type_hash.empty? 212 gen.primary_key(name) 213 else 214 gen.primary_key(name, type_hash) 215 end 216 else 217 col_opts = if options[:same_db] 218 h = {:type=>schema[:db_type]} 219 if database_type == :mysql && h[:type] =~ /\Atimestamp/ 220 h[:null] = true 221 end 222 if database_type == :mssql && schema[:max_length] 223 h[:size] = schema[:max_length] 224 end 225 h 226 else 227 column_schema_to_ruby_type(schema) 228 end 229 type = col_opts.delete(:type) 230 if col_opts.key?(:size) && col_opts[:size].nil? 231 col_opts.delete(:size) 232 if max_length = schema[:max_length] 233 col_opts[:size] = max_length 234 end 235 end 236 if schema[:generated] 237 if options[:same_db] && database_type == :postgres 238 col_opts[:generated_always_as] = column_schema_to_ruby_default_fallback(schema[:default], options) 239 end 240 else 241 col_opts[:default] = if schema[:ruby_default].nil? 242 column_schema_to_ruby_default_fallback(schema[:default], options) 243 else 244 schema[:ruby_default] 245 end 246 col_opts.delete(:default) if col_opts[:default].nil? 247 end 248 col_opts[:null] = false if schema[:allow_null] == false 249 if table = schema[:table] 250 [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]} 251 col_opts[:type] = type unless type == Integer || type == 'integer' || type == 'INTEGER' 252 gen.foreign_key(name, table, col_opts) 253 else 254 gen.column(name, type, col_opts) 255 if [Integer, :Bignum, Float, BigDecimal].include?(type) && schema[:db_type] =~ / unsigned\z/io 256 gen.check(Sequel::SQL::Identifier.new(name) >= 0) 257 end 258 end 259 end 260 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 368 def sort_dumped_tables(tables, options=OPTS) 369 if options[:foreign_keys] != false && supports_foreign_key_parsing? 370 table_fks = {} 371 tables.each{|t| table_fks[t] = foreign_key_list(t)} 372 # Remove self referential foreign keys, not important when sorting. 373 table_fks.each{|t, fks| fks.delete_if{|fk| fk[:table] == t}} 374 tables, skipped_foreign_keys = sort_dumped_tables_topologically(table_fks, []) 375 options[:skipped_foreign_keys] = skipped_foreign_keys 376 tables 377 else 378 tables 379 end 380 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 386 def sort_dumped_tables_topologically(table_fks, sorted_tables) 387 skipped_foreign_keys = {} 388 389 until table_fks.empty? 390 this_loop = [] 391 392 table_fks.each do |table, fks| 393 fks.delete_if{|fk| !table_fks.has_key?(fk[:table])} 394 this_loop << table if fks.empty? 395 end 396 397 if this_loop.empty? 398 # No tables were changed this round, there must be a circular dependency. 399 # Break circular dependency by picking the table with the least number of 400 # outstanding foreign keys and skipping those foreign keys. 401 # The skipped foreign keys will be added at the end of the 402 # migration. 403 skip_table, skip_fks = table_fks.sort_by{|table, fks| [fks.length, literal(table)]}.first 404 skip_fks_hash = skipped_foreign_keys[skip_table] = {} 405 skip_fks.each{|fk| skip_fks_hash[fk[:columns]] = fk} 406 this_loop << skip_table 407 end 408 409 # Add sorted tables from this loop to the final list 410 sorted_tables.concat(_literal_table_sort(this_loop)) 411 412 # Remove tables that were handled this loop 413 this_loop.each{|t| table_fks.delete(t)} 414 end 415 416 [sorted_tables, skipped_foreign_keys] 417 end