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 = tables(options) 92 <<END_MIG 93 Sequel.migration do 94 change do 95 #{ts.sort.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 = tables(options) 110 <<END_MIG 111 Sequel.migration do 112 change do 113 #{ts.sort.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(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
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 171 def column_schema_to_ruby_default_fallback(default, options) 172 if default.is_a?(String) && options[:same_db] && use_column_schema_to_ruby_default_fallback? 173 default = default.dup 174 def default.inspect 175 "Sequel::LiteralString.new(#{super})" 176 end 177 default 178 end 179 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 241 def dump_add_fk_constraints(table, fks) 242 sfks = String.new 243 sfks << "alter_table(#{table.inspect}) do\n" 244 sfks << create_table_generator do 245 fks.sort_by{|fk| fk[:columns]}.each do |fk| 246 foreign_key fk[:columns], fk 247 end 248 end.dump_constraints.gsub(/^foreign_key /, ' add_foreign_key ') 249 sfks << "\nend" 250 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 254 def dump_table_foreign_keys(table, options=OPTS) 255 if supports_foreign_key_parsing? 256 fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns]} 257 end 258 259 if fks.nil? || fks.empty? 260 '' 261 else 262 dump_add_fk_constraints(table, fks) 263 end 264 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 268 def dump_table_generator(table, options=OPTS) 269 s = schema(table, options).dup 270 pks = s.find_all{|x| x.last[:primary_key] == true}.map(&:first) 271 options = options.merge(:single_pk=>true) if pks.length == 1 272 m = method(:recreate_column) 273 im = method(:index_to_generator_opts) 274 275 if options[:indexes] != false && supports_index_parsing? 276 indexes = indexes(table).sort 277 end 278 279 if options[:foreign_keys] != false && supports_foreign_key_parsing? 280 fk_list = foreign_key_list(table) 281 282 if (sfk = options[:skipped_foreign_keys]) && (sfkt = sfk[table]) 283 fk_list.delete_if{|fk| sfkt.has_key?(fk[:columns])} 284 end 285 286 composite_fks, single_fks = fk_list.partition{|h| h[:columns].length > 1} 287 fk_hash = {} 288 289 single_fks.each do |fk| 290 column = fk.delete(:columns).first 291 fk.delete(:name) 292 fk_hash[column] = fk 293 end 294 295 s = s.map do |name, info| 296 if fk_info = fk_hash[name] 297 [name, fk_info.merge(info)] 298 else 299 [name, info] 300 end 301 end 302 end 303 304 create_table_generator do 305 s.each{|name, info| m.call(name, info, self, options)} 306 primary_key(pks) if !@primary_key && pks.length > 0 307 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes 308 composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks 309 end 310 end
Return a string that containing add_index/drop_index method calls for creating the index migration.
# File lib/sequel/extensions/schema_dumper.rb 314 def dump_table_indexes(table, meth, options=OPTS) 315 if supports_index_parsing? 316 indexes = indexes(table).sort 317 else 318 return '' 319 end 320 321 im = method(:index_to_generator_opts) 322 gen = create_table_generator do 323 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} 324 end 325 gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db]) 326 end
Convert the parsed index information into options to the CreateTableGenerator’s index method.
# File lib/sequel/extensions/schema_dumper.rb 329 def index_to_generator_opts(table, name, index_opts, options=OPTS) 330 h = {} 331 if options[:index_names] != false && default_index_name(table, index_opts[:columns]) != name.to_s 332 if options[:index_names] == :namespace && !global_index_namespace? 333 h[:name] = "#{table}_#{name}".to_sym 334 else 335 h[:name] = name 336 end 337 end 338 h[:unique] = true if index_opts[:unique] 339 h[:deferrable] = true if index_opts[:deferrable] 340 h 341 end
Recreate the column in the passed Schema::CreateTableGenerator
from the given name and parsed database schema.
# File lib/sequel/extensions/schema_dumper.rb 182 def recreate_column(name, schema, gen, options) 183 if options[:single_pk] && schema_autoincrementing_primary_key?(schema) 184 type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema) 185 [:table, :key, :on_delete, :on_update, :deferrable].each{|f| type_hash[f] = schema[f] if schema[f]} 186 if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"} 187 type_hash.delete(:type) 188 elsif options[:same_db] && type_hash == {:type=>type_literal_generic_bignum_symbol(type_hash).to_s} 189 type_hash[:type] = :Bignum 190 end 191 192 unless gen.columns.empty? 193 type_hash[:keep_order] = true 194 end 195 196 if type_hash.empty? 197 gen.primary_key(name) 198 else 199 gen.primary_key(name, type_hash) 200 end 201 else 202 col_opts = if options[:same_db] 203 h = {:type=>schema[:db_type]} 204 if database_type == :mysql && h[:type] =~ /\Atimestamp/ 205 h[:null] = true 206 end 207 h 208 else 209 column_schema_to_ruby_type(schema) 210 end 211 type = col_opts.delete(:type) 212 col_opts.delete(:size) if col_opts[:size].nil? 213 if schema[:generated] 214 if options[:same_db] && database_type == :postgres 215 col_opts[:generated_always_as] = column_schema_to_ruby_default_fallback(schema[:default], options) 216 end 217 else 218 col_opts[:default] = if schema[:ruby_default].nil? 219 column_schema_to_ruby_default_fallback(schema[:default], options) 220 else 221 schema[:ruby_default] 222 end 223 col_opts.delete(:default) if col_opts[:default].nil? 224 end 225 col_opts[:null] = false if schema[:allow_null] == false 226 if table = schema[:table] 227 [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]} 228 col_opts[:type] = type unless type == Integer || type == 'integer' 229 gen.foreign_key(name, table, col_opts) 230 else 231 gen.column(name, type, col_opts) 232 if [Integer, :Bignum, Float, BigDecimal].include?(type) && schema[:db_type] =~ / unsigned\z/io 233 gen.check(Sequel::SQL::Identifier.new(name) >= 0) 234 end 235 end 236 end 237 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 345 def sort_dumped_tables(tables, options=OPTS) 346 if options[:foreign_keys] != false && supports_foreign_key_parsing? 347 table_fks = {} 348 tables.each{|t| table_fks[t] = foreign_key_list(t)} 349 # Remove self referential foreign keys, not important when sorting. 350 table_fks.each{|t, fks| fks.delete_if{|fk| fk[:table] == t}} 351 tables, skipped_foreign_keys = sort_dumped_tables_topologically(table_fks, []) 352 options[:skipped_foreign_keys] = skipped_foreign_keys 353 tables 354 else 355 tables.sort 356 end 357 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 363 def sort_dumped_tables_topologically(table_fks, sorted_tables) 364 skipped_foreign_keys = {} 365 366 until table_fks.empty? 367 this_loop = [] 368 369 table_fks.each do |table, fks| 370 fks.delete_if{|fk| !table_fks.has_key?(fk[:table])} 371 this_loop << table if fks.empty? 372 end 373 374 if this_loop.empty? 375 # No tables were changed this round, there must be a circular dependency. 376 # Break circular dependency by picking the table with the least number of 377 # outstanding foreign keys and skipping those foreign keys. 378 # The skipped foreign keys will be added at the end of the 379 # migration. 380 skip_table, skip_fks = table_fks.sort_by{|table, fks| [fks.length, table]}.first 381 skip_fks_hash = skipped_foreign_keys[skip_table] = {} 382 skip_fks.each{|fk| skip_fks_hash[fk[:columns]] = fk} 383 this_loop << skip_table 384 end 385 386 # Add sorted tables from this loop to the final list 387 sorted_tables.concat(this_loop.sort) 388 389 # Remove tables that were handled this loop 390 this_loop.each{|t| table_fks.delete(t)} 391 end 392 393 [sorted_tables, skipped_foreign_keys] 394 end