module Sequel::SchemaDumper

Public Instance Methods

column_schema_to_ruby_type(schema) click to toggle source

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_migration(options=OPTS) click to toggle source

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_migration(options=OPTS) click to toggle source

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
dump_schema_migration(options=OPTS) click to toggle source

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
dump_table_schema(table, options=OPTS) click to toggle source

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

column_schema_to_ruby_default_fallback(default, options) click to toggle source

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
dump_add_fk_constraints(table, fks) click to toggle source

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
dump_table_foreign_keys(table, options=OPTS) click to toggle source

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
dump_table_generator(table, options=OPTS) click to toggle source

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
dump_table_indexes(table, meth, options=OPTS) click to toggle source

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
index_to_generator_opts(table, name, index_opts, options=OPTS) click to toggle source

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_column(name, schema, gen, options) click to toggle source

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_dumped_tables(tables, options=OPTS) click to toggle source

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
sort_dumped_tables_topologically(table_fks, sorted_tables) click to toggle source

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
use_column_schema_to_ruby_default_fallback?() click to toggle source

Don't use a literal string fallback on MySQL, since the defaults it uses aren't valid literal SQL values.

    # File lib/sequel/extensions/schema_dumper.rb
381 def use_column_schema_to_ruby_default_fallback?
382   database_type != :mysql
383 end