schema_dumper.rb 9.0 KB
Newer Older
1 2
require 'stringio'

3
module ActiveRecord
R
Rizwan Reza 已提交
4 5
  # = Active Record Schema Dumper
  #
6 7
  # This class is used to dump the database schema for some connection to some
  # output format (i.e., ActiveRecord::Schema).
8
  class SchemaDumper #:nodoc:
9
    private_class_method :new
A
Aaron Patterson 已提交
10

P
Pratik Naik 已提交
11 12
    ##
    # :singleton-method:
A
Aaron Patterson 已提交
13
    # A list of tables which should not be dumped to the schema.
14 15
    # Acceptable values are strings as well as regexp.
    # This setting is only used if ActiveRecord::Base.schema_format == :ruby
A
Aaron Patterson 已提交
16
    cattr_accessor :ignore_tables
17
    @@ignore_tables = []
18

W
wangjohn 已提交
19 20 21 22 23 24 25 26 27 28 29 30 31
    class << self
      def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base)
        new(connection, generate_options(config)).dump(stream)
        stream
      end

      private
        def generate_options(config)
          {
            table_name_prefix: config.table_name_prefix,
            table_name_suffix: config.table_name_suffix
          }
        end
32 33 34 35
    end

    def dump(stream)
      header(stream)
36
      extensions(stream)
37 38 39 40 41 42 43
      tables(stream)
      trailer(stream)
      stream
    end

    private

W
wangjohn 已提交
44
      def initialize(connection, options = {})
45
        @connection = connection
46
        @version = Migrator::current_version rescue nil
W
wangjohn 已提交
47
        @options = options
48 49 50
      end

      def header(stream)
51 52 53
        define_params = @version ? "version: #{@version}" : ""

        stream.puts <<HEADER
54
# This file is auto-generated from the current state of the database. Instead
R
Rizwan Reza 已提交
55 56
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
57
#
58
# Note that this schema.rb definition is the authoritative source for your
R
Rizwan Reza 已提交
59 60 61
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
62 63
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
64
# It's strongly recommended that you check this file into your version control system.
65

66
ActiveRecord::Schema.define(#{define_params}) do
67

68
HEADER
69 70 71 72 73 74
      end

      def trailer(stream)
        stream.puts "end"
      end

75 76 77
      def extensions(stream)
        return unless @connection.supports_extensions?
        extensions = @connection.extensions
78 79 80 81 82 83
        if extensions.any?
          stream.puts "  # These are extensions that must be enabled in order to support this database"
          extensions.each do |extension|
            stream.puts "  enable_extension #{extension.inspect}"
          end
          stream.puts
84 85 86
        end
      end

87
      def tables(stream)
88
        sorted_tables = @connection.data_sources.sort - @connection.views
89 90 91

        sorted_tables.each do |table_name|
          table(table_name, stream) unless ignored?(table_name)
92
        end
93 94 95 96

        # dump foreign keys at the end to make sure all dependent tables exist.
        if @connection.supports_foreign_keys?
          sorted_tables.each do |tbl|
97
            foreign_keys(tbl, stream) unless ignored?(tbl)
98 99
          end
        end
100 101 102
      end

      def table(table, stream)
103
        columns = @connection.columns(table)
104 105
        begin
          tbl = StringIO.new
106

107
          # first dump primary key column
108
          pk = @connection.primary_key(table)
A
Aaron Patterson 已提交
109

110
          tbl.print "  create_table #{remove_prefix_and_suffix(table).inspect}"
111 112 113 114 115

          case pk
          when String
            tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id'
            pkcol = columns.detect { |c| c.name == pk }
116
            pkcolspec = @connection.column_spec_for_primary_key(pkcol)
117
            if pkcolspec.present?
118 119 120
              pkcolspec.each do |key, value|
                tbl.print ", #{key}: #{value}"
              end
121
            end
122 123
          when Array
            tbl.print ", primary_key: #{pk.inspect}"
124
          else
125
            tbl.print ", id: false"
126
          end
127
          tbl.print ", force: :cascade"
128 129

          table_options = @connection.table_options(table)
130 131 132 133
          if table_options.present?
            table_options.each do |key, value|
              tbl.print ", #{key}: #{value.inspect}" if value.present?
            end
134
          end
135

136 137
          tbl.puts " do |t|"

138
          # then dump all non-primary key columns
139
          column_specs = columns.map do |column|
140
            raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
141
            next if column.name == pk
142
            @connection.column_spec(column)
143
          end.compact
144 145

          # find all migration keys used in this table
146
          keys = @connection.migration_keys
147 148

          # figure out the lengths for each column based on above keys
A
Aaron Patterson 已提交
149 150 151 152 153
          lengths = keys.map { |key|
            column_specs.map { |spec|
              spec[key] ? spec[key].length + 2 : 0
            }.max
          }
154 155 156 157 158 159 160 161 162 163 164 165

          # the string we're going to sprintf our values against, with standardized column widths
          format_string = lengths.map{ |len| "%-#{len}s" }

          # find the max length for the 'type' column, which is special
          type_length = column_specs.map{ |column| column[:type].length }.max

          # add column type definition to our format string
          format_string.unshift "    t.%-#{type_length}s "

          format_string *= ''

166 167
          column_specs.each do |colspec|
            values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
168
            values.unshift colspec[:type]
169
            tbl.print((format_string % values).gsub(/,\s*$/, ''))
170 171 172
            tbl.puts
          end

173 174
          indexes_in_create(table, tbl)

175 176
          tbl.puts "  end"
          tbl.puts
A
Aaron Patterson 已提交
177

178 179 180 181 182
          tbl.rewind
          stream.print tbl.read
        rescue => e
          stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
          stream.puts "#   #{e.message}"
183 184
          stream.puts
        end
A
Aaron Patterson 已提交
185

186
        stream
187 188
      end

189
      # Keep it for indexing materialized views
190
      def indexes(table, stream)
191 192
        if (indexes = @connection.indexes(table)).any?
          add_index_statements = indexes.map do |index|
193 194
            table_name = remove_prefix_and_suffix(index.table).inspect
            "  add_index #{([table_name]+index_parts(index)).join(', ')}"
195 196 197
          end

          stream.puts add_index_statements.sort.join("\n")
198
          stream.puts
199 200
        end
      end
201

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
      def indexes_in_create(table, stream)
        if (indexes = @connection.indexes(table)).any?
          index_statements = indexes.map do |index|
            "    t.index #{index_parts(index).join(', ')}"
          end
          stream.puts index_statements.sort.join("\n")
        end
      end

      def index_parts(index)
        index_parts = [
          index.columns.inspect,
          "name: #{index.name.inspect}",
        ]
        index_parts << 'unique: true' if index.unique

        index_lengths = (index.lengths || []).compact
        index_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?

        index_orders = index.orders || {}
        index_parts << "order: #{index.orders.inspect}" if index_orders.any?
        index_parts << "where: #{index.where.inspect}" if index.where
        index_parts << "using: #{index.using.inspect}" if index.using
        index_parts << "type: #{index.type.inspect}" if index.type
        index_parts << "comment: #{index.comment.inspect}" if index.comment
        index_parts
      end

Y
Yves Senn 已提交
230 231 232 233
      def foreign_keys(table, stream)
        if (foreign_keys = @connection.foreign_keys(table)).any?
          add_foreign_key_statements = foreign_keys.map do |foreign_key|
            parts = [
T
Tee Parham 已提交
234 235 236
              "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}",
              remove_prefix_and_suffix(foreign_key.to_table).inspect,
            ]
Y
Yves Senn 已提交
237 238

            if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
239
              parts << "column: #{foreign_key.column.inspect}"
Y
Yves Senn 已提交
240 241 242
            end

            if foreign_key.custom_primary_key?
243
              parts << "primary_key: #{foreign_key.primary_key.inspect}"
Y
Yves Senn 已提交
244 245 246
            end

            if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
247
              parts << "name: #{foreign_key.name.inspect}"
Y
Yves Senn 已提交
248 249
            end

250 251
            parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update
            parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
252

253
            "  #{parts.join(', ')}"
Y
Yves Senn 已提交
254 255 256 257 258 259
          end

          stream.puts add_foreign_key_statements.sort.join("\n")
        end
      end

260
      def remove_prefix_and_suffix(table)
W
wangjohn 已提交
261
        table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/,  "\\2")
262
      end
263 264

      def ignored?(table_name)
265
        [ActiveRecord::Base.schema_migrations_table_name, ActiveRecord::Base.internal_metadata_table_name, ignore_tables].flatten.any? do |ignored|
266
          ignored === remove_prefix_and_suffix(table_name)
267 268
        end
      end
269
  end
J
Jeremy Kemper 已提交
270
end