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|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+))?\))?(?: unsigned)?\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
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
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
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
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
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
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
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
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
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_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     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, BigDecimal].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_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
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
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
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
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
387 def use_column_schema_to_ruby_default_fallback?
388   database_type != :mysql
389 end