module Sequel::SchemaDumper

Constants

IGNORE_INDEX_ERRORS_KEY

:nocov:

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

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

Sort the given table by the literalized value.

    # File lib/sequel/extensions/schema_dumper.rb
185 def _literal_table_sort(tables)
186   tables.sort_by{|s| literal(s)}
187 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
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
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
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
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
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
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
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
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
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
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
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
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
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].start_with?("timestamp")
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/i
261         gen.check(Sequel::SQL::Identifier.new(name) >= 0)
262       end
263     end
264   end
265 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
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_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
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
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
426 def use_column_schema_to_ruby_default_fallback?
427   database_type != :mysql
428 end