| 1 |
require 'stringio' |
|---|
| 2 |
require 'bigdecimal' |
|---|
| 3 |
|
|---|
| 4 |
module ActiveRecord |
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
class SchemaDumper |
|---|
| 8 |
private_class_method :new |
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
cattr_accessor :ignore_tables |
|---|
| 14 |
@@ignore_tables = [] |
|---|
| 15 |
|
|---|
| 16 |
def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT) |
|---|
| 17 |
new(connection).dump(stream) |
|---|
| 18 |
stream |
|---|
| 19 |
end |
|---|
| 20 |
|
|---|
| 21 |
def dump(stream) |
|---|
| 22 |
header(stream) |
|---|
| 23 |
tables(stream) |
|---|
| 24 |
trailer(stream) |
|---|
| 25 |
stream |
|---|
| 26 |
end |
|---|
| 27 |
|
|---|
| 28 |
private |
|---|
| 29 |
|
|---|
| 30 |
def initialize(connection) |
|---|
| 31 |
@connection = connection |
|---|
| 32 |
@types = @connection.native_database_types |
|---|
| 33 |
@version = Migrator::current_version rescue nil |
|---|
| 34 |
end |
|---|
| 35 |
|
|---|
| 36 |
def header(stream) |
|---|
| 37 |
define_params = @version ? ":version => #{@version}" : "" |
|---|
| 38 |
|
|---|
| 39 |
stream.puts <<HEADER |
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 |
|
|---|
| 43 |
|
|---|
| 44 |
|
|---|
| 45 |
|
|---|
| 46 |
|
|---|
| 47 |
|
|---|
| 48 |
|
|---|
| 49 |
|
|---|
| 50 |
|
|---|
| 51 |
ActiveRecord::Schema.define( |
|---|
| 52 |
|
|---|
| 53 |
HEADER |
|---|
| 54 |
end |
|---|
| 55 |
|
|---|
| 56 |
def trailer(stream) |
|---|
| 57 |
stream.puts "end" |
|---|
| 58 |
end |
|---|
| 59 |
|
|---|
| 60 |
def tables(stream) |
|---|
| 61 |
@connection.tables.sort.each do |tbl| |
|---|
| 62 |
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| |
|---|
| 63 |
case ignored |
|---|
| 64 |
when String; tbl == ignored |
|---|
| 65 |
when Regexp; tbl =~ ignored |
|---|
| 66 |
else |
|---|
| 67 |
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' |
|---|
| 68 |
end |
|---|
| 69 |
end |
|---|
| 70 |
table(tbl, stream) |
|---|
| 71 |
end |
|---|
| 72 |
end |
|---|
| 73 |
|
|---|
| 74 |
def table(table, stream) |
|---|
| 75 |
columns = @connection.columns(table) |
|---|
| 76 |
begin |
|---|
| 77 |
tbl = StringIO.new |
|---|
| 78 |
|
|---|
| 79 |
if @connection.respond_to?(:pk_and_sequence_for) |
|---|
| 80 |
pk, pk_seq = @connection.pk_and_sequence_for(table) |
|---|
| 81 |
end |
|---|
| 82 |
pk ||= 'id' |
|---|
| 83 |
|
|---|
| 84 |
tbl.print " create_table #{table.inspect}" |
|---|
| 85 |
if columns.detect { |c| c.name == pk } |
|---|
| 86 |
if pk != 'id' |
|---|
| 87 |
tbl.print %Q(, :primary_key => "#{pk}") |
|---|
| 88 |
end |
|---|
| 89 |
else |
|---|
| 90 |
tbl.print ", :id => false" |
|---|
| 91 |
end |
|---|
| 92 |
tbl.print ", :force => true" |
|---|
| 93 |
tbl.puts " do |t|" |
|---|
| 94 |
|
|---|
| 95 |
column_specs = columns.map do |column| |
|---|
| 96 |
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? |
|---|
| 97 |
next if column.name == pk |
|---|
| 98 |
spec = {} |
|---|
| 99 |
spec[:name] = column.name.inspect |
|---|
| 100 |
spec[:type] = column.type.to_s |
|---|
| 101 |
spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal |
|---|
| 102 |
spec[:precision] = column.precision.inspect if !column.precision.nil? |
|---|
| 103 |
spec[:scale] = column.scale.inspect if !column.scale.nil? |
|---|
| 104 |
spec[:null] = 'false' if !column.null |
|---|
| 105 |
spec[:default] = default_string(column.default) if !column.default.nil? |
|---|
| 106 |
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} |
|---|
| 107 |
spec |
|---|
| 108 |
end.compact |
|---|
| 109 |
|
|---|
| 110 |
|
|---|
| 111 |
keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten |
|---|
| 112 |
|
|---|
| 113 |
|
|---|
| 114 |
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } |
|---|
| 115 |
|
|---|
| 116 |
|
|---|
| 117 |
format_string = lengths.map{ |len| "%-#{len}s" } |
|---|
| 118 |
|
|---|
| 119 |
|
|---|
| 120 |
type_length = column_specs.map{ |column| column[:type].length }.max |
|---|
| 121 |
|
|---|
| 122 |
|
|---|
| 123 |
format_string.unshift " t.%-#{type_length}s " |
|---|
| 124 |
|
|---|
| 125 |
format_string *= '' |
|---|
| 126 |
|
|---|
| 127 |
column_specs.each do |colspec| |
|---|
| 128 |
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len } |
|---|
| 129 |
values.unshift colspec[:type] |
|---|
| 130 |
tbl.print((format_string % values).gsub(/,\s*$/, '')) |
|---|
| 131 |
tbl.puts |
|---|
| 132 |
end |
|---|
| 133 |
|
|---|
| 134 |
tbl.puts " end" |
|---|
| 135 |
tbl.puts |
|---|
| 136 |
|
|---|
| 137 |
indexes(table, tbl) |
|---|
| 138 |
|
|---|
| 139 |
tbl.rewind |
|---|
| 140 |
stream.print tbl.read |
|---|
| 141 |
rescue => e |
|---|
| 142 |
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" |
|---|
| 143 |
stream.puts "# #{e.message}" |
|---|
| 144 |
stream.puts |
|---|
| 145 |
end |
|---|
| 146 |
|
|---|
| 147 |
stream |
|---|
| 148 |
end |
|---|
| 149 |
|
|---|
| 150 |
def default_string(value) |
|---|
| 151 |
case value |
|---|
| 152 |
when BigDecimal |
|---|
| 153 |
value.to_s |
|---|
| 154 |
when Date, DateTime, Time |
|---|
| 155 |
"'" + value.to_s(:db) + "'" |
|---|
| 156 |
else |
|---|
| 157 |
value.inspect |
|---|
| 158 |
end |
|---|
| 159 |
end |
|---|
| 160 |
|
|---|
| 161 |
def indexes(table, stream) |
|---|
| 162 |
indexes = @connection.indexes(table) |
|---|
| 163 |
indexes.each do |index| |
|---|
| 164 |
stream.print " add_index #{index.table.inspect}, #{index.columns.inspect}, :name => #{index.name.inspect}" |
|---|
| 165 |
stream.print ", :unique => true" if index.unique |
|---|
| 166 |
stream.puts |
|---|
| 167 |
end |
|---|
| 168 |
stream.puts unless indexes.empty? |
|---|
| 169 |
end |
|---|
| 170 |
end |
|---|
| 171 |
end |
|---|