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
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_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
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_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
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
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
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
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
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

_dump_tables(opts) click to toggle source

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
_literal_table_sort(tables) click to toggle source

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
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
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
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
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
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
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
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
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
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
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
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
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_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
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_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
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
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
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
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
421 def use_column_schema_to_ruby_default_fallback?
422   database_type != :mysql
423 end