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