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 230 def dump_add_fk_constraints(table, fks) 231 sfks = String.new 232 sfks << "alter_table(#{table.inspect}) do\n" 233 sfks << create_table_generator do 234 fks.sort_by{|fk| fk[:columns]}.each do |fk| 235 foreign_key fk[:columns], fk 236 end 237 end.dump_constraints.gsub(/^foreign_key /, ' add_foreign_key ') 238 sfks << "\nend" 239 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 243 def dump_table_foreign_keys(table, options=OPTS) 244 if supports_foreign_key_parsing? 245 fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns]} 246 end 247 248 if fks.nil? || fks.empty? 249 '' 250 else 251 dump_add_fk_constraints(table, fks) 252 end 253 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 257 def dump_table_generator(table, options=OPTS) 258 s = schema(table, options).dup 259 pks = s.find_all{|x| x.last[:primary_key] == true}.map(&:first) 260 options = options.merge(:single_pk=>true) if pks.length == 1 261 m = method(:recreate_column) 262 im = method(:index_to_generator_opts) 263 264 if options[:indexes] != false && supports_index_parsing? 265 indexes = indexes(table).sort 266 end 267 268 if options[:foreign_keys] != false && supports_foreign_key_parsing? 269 fk_list = foreign_key_list(table) 270 271 if (sfk = options[:skipped_foreign_keys]) && (sfkt = sfk[table]) 272 fk_list.delete_if{|fk| sfkt.has_key?(fk[:columns])} 273 end 274 275 composite_fks, single_fks = fk_list.partition{|h| h[:columns].length > 1} 276 fk_hash = {} 277 278 single_fks.each do |fk| 279 column = fk.delete(:columns).first 280 fk.delete(:name) 281 fk_hash[column] = fk 282 end 283 284 s = s.map do |name, info| 285 if fk_info = fk_hash[name] 286 [name, fk_info.merge(info)] 287 else 288 [name, info] 289 end 290 end 291 end 292 293 create_table_generator do 294 s.each{|name, info| m.call(name, info, self, options)} 295 primary_key(pks) if !@primary_key && pks.length > 0 296 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes 297 composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks 298 end 299 end
Return a string that containing add_index/drop_index method calls for creating the index migration.
# File lib/sequel/extensions/schema_dumper.rb 303 def dump_table_indexes(table, meth, options=OPTS) 304 if supports_index_parsing? 305 indexes = indexes(table).sort 306 else 307 return '' 308 end 309 310 im = method(:index_to_generator_opts) 311 gen = create_table_generator do 312 indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} 313 end 314 gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db]) 315 end
Convert the parsed index information into options to the CreateTableGenerator's index method.
# File lib/sequel/extensions/schema_dumper.rb 318 def index_to_generator_opts(table, name, index_opts, options=OPTS) 319 h = {} 320 if options[:index_names] != false && default_index_name(table, index_opts[:columns]) != name.to_s 321 if options[:index_names] == :namespace && !global_index_namespace? 322 h[:name] = "#{table}_#{name}".to_sym 323 else 324 h[:name] = name 325 end 326 end 327 h[:unique] = true if index_opts[:unique] 328 h[:deferrable] = true if index_opts[:deferrable] 329 h 330 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 if schema[:generated] 203 if options[:same_db] && database_type == :postgres 204 col_opts[:generated_always_as] = column_schema_to_ruby_default_fallback(schema[:default], options) 205 end 206 else 207 col_opts[:default] = if schema[:ruby_default].nil? 208 column_schema_to_ruby_default_fallback(schema[:default], options) 209 else 210 schema[:ruby_default] 211 end 212 col_opts.delete(:default) if col_opts[:default].nil? 213 end 214 col_opts[:null] = false if schema[:allow_null] == false 215 if table = schema[:table] 216 [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]} 217 col_opts[:type] = type unless type == Integer || type == 'integer' 218 gen.foreign_key(name, table, col_opts) 219 else 220 gen.column(name, type, col_opts) 221 if [Integer, :Bignum, Float].include?(type) && schema[:db_type] =~ / unsigned\z/io 222 gen.check(Sequel::SQL::Identifier.new(name) >= 0) 223 end 224 end 225 end 226 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 334 def sort_dumped_tables(tables, options=OPTS) 335 if options[:foreign_keys] != false && supports_foreign_key_parsing? 336 table_fks = {} 337 tables.each{|t| table_fks[t] = foreign_key_list(t)} 338 # Remove self referential foreign keys, not important when sorting. 339 table_fks.each{|t, fks| fks.delete_if{|fk| fk[:table] == t}} 340 tables, skipped_foreign_keys = sort_dumped_tables_topologically(table_fks, []) 341 options[:skipped_foreign_keys] = skipped_foreign_keys 342 tables 343 else 344 tables.sort 345 end 346 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 352 def sort_dumped_tables_topologically(table_fks, sorted_tables) 353 skipped_foreign_keys = {} 354 355 until table_fks.empty? 356 this_loop = [] 357 358 table_fks.each do |table, fks| 359 fks.delete_if{|fk| !table_fks.has_key?(fk[:table])} 360 this_loop << table if fks.empty? 361 end 362 363 if this_loop.empty? 364 # No tables were changed this round, there must be a circular dependency. 365 # Break circular dependency by picking the table with the least number of 366 # outstanding foreign keys and skipping those foreign keys. 367 # The skipped foreign keys will be added at the end of the 368 # migration. 369 skip_table, skip_fks = table_fks.sort_by{|table, fks| [fks.length, table]}.first 370 skip_fks_hash = skipped_foreign_keys[skip_table] = {} 371 skip_fks.each{|fk| skip_fks_hash[fk[:columns]] = fk} 372 this_loop << skip_table 373 end 374 375 # Add sorted tables from this loop to the final list 376 sorted_tables.concat(this_loop.sort) 377 378 # Remove tables that were handled this loop 379 this_loop.each{|t| table_fks.delete(t)} 380 end 381 382 [sorted_tables, skipped_foreign_keys] 383 end