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 = tables(options)
92       <<END_MIG
93 Sequel.migration do
94   change do
95 #{ts.sort.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 = tables(options)
110       <<END_MIG
111 Sequel.migration do
112   change do
113 #{ts.sort.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(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

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
171 def column_schema_to_ruby_default_fallback(default, options)
172   if default.is_a?(String) && options[:same_db] && use_column_schema_to_ruby_default_fallback?
173     default = default.dup
174     def default.inspect
175       "Sequel::LiteralString.new(#{super})"
176     end
177     default
178   end
179 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
241 def dump_add_fk_constraints(table, fks)
242   sfks = String.new
243   sfks << "alter_table(#{table.inspect}) do\n"
244   sfks << create_table_generator do
245     fks.sort_by{|fk| fk[:columns]}.each do |fk|
246       foreign_key fk[:columns], fk
247     end
248   end.dump_constraints.gsub(/^foreign_key /, '  add_foreign_key ')
249   sfks << "\nend"
250 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
254 def dump_table_foreign_keys(table, options=OPTS)
255   if supports_foreign_key_parsing?
256     fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns]}
257   end
258 
259   if fks.nil? || fks.empty?
260     ''
261   else
262     dump_add_fk_constraints(table, fks)
263   end
264 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
268 def dump_table_generator(table, options=OPTS)
269   s = schema(table, options).dup
270   pks = s.find_all{|x| x.last[:primary_key] == true}.map(&:first)
271   options = options.merge(:single_pk=>true) if pks.length == 1
272   m = method(:recreate_column)
273   im = method(:index_to_generator_opts)
274 
275   if options[:indexes] != false && supports_index_parsing?
276     indexes = indexes(table).sort
277   end
278 
279   if options[:foreign_keys] != false && supports_foreign_key_parsing?
280     fk_list = foreign_key_list(table)
281     
282     if (sfk = options[:skipped_foreign_keys]) && (sfkt = sfk[table])
283       fk_list.delete_if{|fk| sfkt.has_key?(fk[:columns])}
284     end
285 
286     composite_fks, single_fks = fk_list.partition{|h| h[:columns].length > 1}
287     fk_hash = {}
288 
289     single_fks.each do |fk|
290       column = fk.delete(:columns).first
291       fk.delete(:name)
292       fk_hash[column] = fk
293     end
294 
295     s = s.map do |name, info|
296       if fk_info = fk_hash[name]
297         [name, fk_info.merge(info)]
298       else
299         [name, info]
300       end
301     end
302   end
303 
304   create_table_generator do
305     s.each{|name, info| m.call(name, info, self, options)}
306     primary_key(pks) if !@primary_key && pks.length > 0
307     indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes
308     composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks
309   end
310 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
314 def dump_table_indexes(table, meth, options=OPTS)
315   if supports_index_parsing?
316     indexes = indexes(table).sort
317   else
318     return ''
319   end
320 
321   im = method(:index_to_generator_opts)
322   gen = create_table_generator do
323     indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))}
324   end
325   gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db])
326 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
329 def index_to_generator_opts(table, name, index_opts, options=OPTS)
330   h = {}
331   if options[:index_names] != false && default_index_name(table, index_opts[:columns]) != name.to_s
332     if options[:index_names] == :namespace && !global_index_namespace?
333       h[:name] = "#{table}_#{name}".to_sym
334     else
335       h[:name] = name
336     end
337   end
338   h[:unique] = true if index_opts[:unique]
339   h[:deferrable] = true if index_opts[:deferrable]
340   h
341 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
182 def recreate_column(name, schema, gen, options)
183   if options[:single_pk] && schema_autoincrementing_primary_key?(schema)
184     type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
185     [:table, :key, :on_delete, :on_update, :deferrable].each{|f| type_hash[f] = schema[f] if schema[f]}
186     if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"}
187       type_hash.delete(:type)
188     elsif options[:same_db] && type_hash == {:type=>type_literal_generic_bignum_symbol(type_hash).to_s}
189       type_hash[:type] = :Bignum
190     end
191 
192     unless gen.columns.empty?
193       type_hash[:keep_order] = true
194     end
195 
196     if type_hash.empty?
197       gen.primary_key(name)
198     else
199       gen.primary_key(name, type_hash)
200     end
201   else
202     col_opts = if options[:same_db]
203       h = {:type=>schema[:db_type]}
204       if database_type == :mysql && h[:type] =~ /\Atimestamp/
205         h[:null] = true
206       end
207       h
208     else
209       column_schema_to_ruby_type(schema)
210     end
211     type = col_opts.delete(:type)
212     col_opts.delete(:size) if col_opts[:size].nil?
213     if schema[:generated]
214       if options[:same_db] && database_type == :postgres
215         col_opts[:generated_always_as] = column_schema_to_ruby_default_fallback(schema[:default], options)
216       end
217     else
218       col_opts[:default] = if schema[:ruby_default].nil?
219         column_schema_to_ruby_default_fallback(schema[:default], options)
220       else
221         schema[:ruby_default]
222       end
223       col_opts.delete(:default) if col_opts[:default].nil?
224     end
225     col_opts[:null] = false if schema[:allow_null] == false
226     if table = schema[:table]
227       [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]}
228       col_opts[:type] = type unless type == Integer || type == 'integer'
229       gen.foreign_key(name, table, col_opts)
230     else
231       gen.column(name, type, col_opts)
232       if [Integer, :Bignum, Float, BigDecimal].include?(type) && schema[:db_type] =~ / unsigned\z/io
233         gen.check(Sequel::SQL::Identifier.new(name) >= 0)
234       end
235     end
236   end
237 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
345 def sort_dumped_tables(tables, options=OPTS)
346   if options[:foreign_keys] != false && supports_foreign_key_parsing?
347     table_fks = {}
348     tables.each{|t| table_fks[t] = foreign_key_list(t)}
349     # Remove self referential foreign keys, not important when sorting.
350     table_fks.each{|t, fks| fks.delete_if{|fk| fk[:table] == t}}
351     tables, skipped_foreign_keys = sort_dumped_tables_topologically(table_fks, [])
352     options[:skipped_foreign_keys] = skipped_foreign_keys
353     tables
354   else
355     tables.sort
356   end
357 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
363 def sort_dumped_tables_topologically(table_fks, sorted_tables)
364   skipped_foreign_keys = {}
365 
366   until table_fks.empty? 
367     this_loop = []
368 
369     table_fks.each do |table, fks|
370       fks.delete_if{|fk| !table_fks.has_key?(fk[:table])}
371       this_loop << table if fks.empty?
372     end
373 
374     if this_loop.empty?
375       # No tables were changed this round, there must be a circular dependency.
376       # Break circular dependency by picking the table with the least number of
377       # outstanding foreign keys and skipping those foreign keys.
378       # The skipped foreign keys will be added at the end of the
379       # migration.
380       skip_table, skip_fks = table_fks.sort_by{|table, fks| [fks.length, table]}.first
381       skip_fks_hash = skipped_foreign_keys[skip_table] = {}
382       skip_fks.each{|fk| skip_fks_hash[fk[:columns]] = fk}
383       this_loop << skip_table
384     end
385 
386     # Add sorted tables from this loop to the final list
387     sorted_tables.concat(this_loop.sort)
388 
389     # Remove tables that were handled this loop
390     this_loop.each{|t| table_fks.delete(t)}
391   end
392 
393   [sorted_tables, skipped_foreign_keys]
394 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
398 def use_column_schema_to_ruby_default_fallback?
399   database_type != :mysql
400 end