diff options
author | Jamis Buck <jamis@37signals.com> | 2005-09-23 13:29:33 +0000 |
---|---|---|
committer | Jamis Buck <jamis@37signals.com> | 2005-09-23 13:29:33 +0000 |
commit | 7dc45818dc43c163700efc9896a0f3feafa31138 (patch) | |
tree | b9a263bfd2416bfbc9843b563dcc0e2565f2f877 | |
parent | 436d54c3f19fc80713d975482a28a95cfa60a1e4 (diff) | |
download | rails-7dc45818dc43c163700efc9896a0f3feafa31138.tar.gz rails-7dc45818dc43c163700efc9896a0f3feafa31138.tar.bz2 rails-7dc45818dc43c163700efc9896a0f3feafa31138.zip |
Add ActiveRecord::SchemaDumper for dumping a DB schema to a pure-ruby file, making it easier to consolidate large migration lists and port database schemas between databases.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2312 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
-rw-r--r-- | activerecord/CHANGELOG | 2 | ||||
-rwxr-xr-x | activerecord/lib/active_record.rb | 2 | ||||
-rwxr-xr-x | activerecord/lib/active_record/connection_adapters/abstract_adapter.rb | 59 | ||||
-rwxr-xr-x | activerecord/lib/active_record/connection_adapters/mysql_adapter.rb | 23 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb | 29 | ||||
-rw-r--r-- | activerecord/lib/active_record/schema.rb | 23 | ||||
-rw-r--r-- | activerecord/lib/active_record/schema_dumper.rb | 87 | ||||
-rw-r--r-- | activerecord/test/adapter_test.rb | 38 | ||||
-rw-r--r-- | activerecord/test/ar_schema_test.rb | 31 | ||||
-rw-r--r-- | activerecord/test/migration_test.rb | 4 | ||||
-rw-r--r-- | activerecord/test/schema_dumper_test.rb | 19 |
11 files changed, 292 insertions, 25 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 15f6816f75..a486e167ef 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Add ActiveRecord::SchemaDumper for dumping a DB schema to a pure-ruby file, making it easier to consolidate large migration lists and port database schemas between databases. + * Fixed migrations for Windows when using more than 10 [David Naseby] * Fixed that the create_x method from belongs_to wouldn't save the association properly #2042 [Florian Weber] diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d7e7201e73..874df2efc8 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -71,4 +71,4 @@ RAILS_CONNECTION_ADAPTERS.each do |adapter| require "active_record/connection_adapters/#{adapter}_adapter" end -require 'active_record/query_cache'
\ No newline at end of file +require 'active_record/query_cache' diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cbc8e7c273..1d09fa0f37 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -152,13 +152,13 @@ module ActiveRecord module ConnectionAdapters # :nodoc: class Column # :nodoc: - attr_reader :name, :default, :type, :limit + attr_reader :name, :default, :type, :limit, :null # The name should contain the name of the column, such as "name" in "name varchar(250)" # The default should contain the type-casted default of the column, such as 1 in "count int(11) DEFAULT 1" # The type parameter should either contain :integer, :float, :datetime, :date, :text, or :string # The sql_type is just used for extracting the limit, such as 10 in "varchar(10)" - def initialize(name, default, sql_type = nil) - @name, @default, @type = name, type_cast(default), simplified_type(sql_type) + def initialize(name, default, sql_type = nil, null = true) + @name, @default, @type, @null = name, type_cast(default), simplified_type(sql_type), null @limit = extract_limit(sql_type) unless sql_type.nil? end @@ -275,6 +275,12 @@ module ActiveRecord # Returns a record hash with the column names as a keys and fields as values. def select_one(sql, name = nil) end + # Returns an array of table names for the current database. + def tables(name = nil) end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) end + # Returns an array of column objects for the table specified by +table_name+. def columns(table_name, name = nil) end @@ -423,12 +429,46 @@ module ActiveRecord raise NotImplementedError, "rename_column is not implemented" end - def add_index(table_name, column_name, index_type = '') - execute "CREATE #{index_type} INDEX #{table_name}_#{column_name.to_a.first}_index ON #{table_name} (#{column_name.to_a.join(", ")})" - end + # Create a new index on the given table. By default, it will be named + # <code>"#{table_name}_#{column_name.to_a.first}_index"</code>, but you + # can explicitly name the index by passing <code>:name => "..."</code> + # as the last parameter. Unique indexes may be created by passing + # <code>:unique => true</code>. + def add_index(table_name, column_name, options = {}) + index_name = "#{table_name}_#{column_name.to_a.first}_index" - def remove_index(table_name, column_name) - execute "DROP INDEX #{table_name}_#{column_name}_index ON #{table_name}" + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name] || index_name + else + index_type = options + end + + execute "CREATE #{index_type} INDEX #{index_name} ON #{table_name} (#{column_name.to_a.join(", ")})" + end + + # Remove the given index from the table. + # + # remove_index :my_table, :column => :foo + # remove_index :my_table, :name => :my_index_on_foo + # + # The first version will remove the index named + # <code>"#{my_table}_#{column}_index"</code> from the table. The + # second removes the named column from the table. + def remove_index(table_name, options = {}) + if Hash === options # legacy support + if options[:column] + index_name = "#{table_name}_#{options[:column]}_index" + elsif options[:name] + index_name = options[:name] + else + raise ArgumentError, "You must specify the index name" + end + else + index_name = "#{table_name}_#{options}_index" + end + + execute "DROP INDEX #{index_name} ON #{table_name}" end def supports_migrations? @@ -504,6 +544,9 @@ module ActiveRecord end end + class IndexDefinition < Struct.new(:table, :name, :unique, :columns) + end + class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null) def to_sql column_sql = "#{name} #{type_to_sql(type.to_sym, limit)}" diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 6dbf315396..426d035bae 100755 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -102,10 +102,31 @@ module ActiveRecord result.nil? ? nil : result.first end + def tables(name = nil) + tables = [] + execute("SHOW TABLES", name).each { |field| tables << field[0] } + tables + end + + def indexes(table_name, name = nil) + indexes = [] + current_index = nil + execute("SHOW KEYS FROM #{table_name}", name).each do |row| + if current_index != row[2] + next if row[2] == "PRIMARY" # skip the primary key + current_index = row[2] + indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", []) + end + + indexes.last.columns << row[4] + end + indexes + end + def columns(table_name, name = nil) sql = "SHOW FIELDS FROM #{table_name}" columns = [] - execute(sql, name).each { |field| columns << Column.new(field[0], field[4], field[1]) } + execute(sql, name).each { |field| columns << Column.new(field[0], field[4], field[1], field[2] == "YES") } columns end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index af2de4f706..116e20999a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -150,17 +150,27 @@ module ActiveRecord def commit_db_transaction() @connection.commit end def rollback_db_transaction() @connection.rollback end - - def tables - execute('.table').map { |table| Table.new(table) } + def tables(name = nil) + execute("SELECT name FROM sqlite_master WHERE type = 'table'", name).map do |row| + row[0] + end end def columns(table_name, name = nil) table_structure(table_name).map { |field| - SQLiteColumn.new(field['name'], field['dflt_value'], field['type']) + SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0") } end + def indexes(table_name, name = nil) + execute("PRAGMA index_list(#{table_name})", name).map do |row| + index = IndexDefinition.new(table_name, row['name']) + index.unique = row['unique'] != '0' + index.columns = execute("PRAGMA index_info(#{index.name})").map { |col| col['name'] } + index + end + end + def primary_key(table_name) column = table_structure(table_name).find {|field| field['pk'].to_i == 1} column ? column['name'] : nil @@ -222,17 +232,6 @@ module ActiveRecord end end - def indexes(table_name) - execute("PRAGMA index_list(#{table_name})").map do |index| - index_info = execute("PRAGMA index_info(#{index['name']})") - { - :name => index['name'], - :unique => index['unique'].to_i == 1, - :columns => index_info.map {|info| info['name']} - } - end - end - def alter_table(table_name, options = {}) #:nodoc: altered_table_name = "altered_#{table_name}" caller = lambda {|definition| yield definition if block_given?} diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb new file mode 100644 index 0000000000..1cb4aec3c8 --- /dev/null +++ b/activerecord/lib/active_record/schema.rb @@ -0,0 +1,23 @@ +module ActiveRecord + + class Schema < Migration #:nodoc: + private_class_method :new + + def self.define(info={}, &block) + instance_eval(&block) + + unless info.empty? + initialize_schema_information + cols = columns('schema_info') + + info = info.map do |k,v| + v = quote(v, cols.detect { |c| c.name == k.to_s }) + "#{k} = #{v}" + end + + update "UPDATE schema_info SET #{info.join(", ")}" + end + end + end + +end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb new file mode 100644 index 0000000000..e086c17841 --- /dev/null +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -0,0 +1,87 @@ +module ActiveRecord + + # This class is used to dump the database schema for some connection to some + # output format (i.e., ActiveRecord::Schema). + class SchemaDumper + private_class_method :new + + def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT) + new(connection).dump(stream) + stream + end + + def dump(stream) + header(stream) + tables(stream) + trailer(stream) + stream + end + + private + + def initialize(connection) + @connection = connection + @types = @connection.native_database_types + @info = @connection.select_one("SELECT * FROM schema_info") rescue nil + end + + def header(stream) + define_params = @info ? ":version => #{@info['version']}" : "" + + stream.puts <<HEADER +# This file is autogenerated. Instead of editing this file, please use the +# migrations feature of ActiveRecord to incrementally modify your database, and +# then regenerate this schema definition. + +require 'active_record/schema' + +ActiveRecord::Schema.define(#{define_params}) do + +HEADER + end + + def trailer(stream) + stream.puts "end" + end + + def tables(stream) + @connection.tables.sort.each do |tbl| + next if tbl == "schema_info" + table(tbl, stream) + end + end + + def table(table, stream) + columns = @connection.columns(table) + + stream.print " create_table #{table.inspect}" + stream.print ", :id => false" if !columns.detect { |c| c.name == "id" } + stream.puts " do |t|" + + columns.each do |column| + next if column.name == "id" + stream.print " t.column #{column.name.inspect}, #{column.type.inspect}" + stream.print ", :limit => #{column.limit.inspect}" if column.limit != @types[column.type][:limit] + stream.print ", :default => #{column.default.inspect}" if column.default + stream.print ", :null => false" if !column.null + stream.puts + end + + stream.puts " end" + stream.puts + + indexes(table, stream) + end + + def indexes(table, stream) + indexes = @connection.indexes(table) + indexes.each do |index| + stream.print " add_index #{index.table.inspect}, #{index.columns.inspect}, :name => #{index.name.inspect}" + stream.print ", :unique => true" if index.unique + stream.puts + end + stream.puts unless indexes.empty? + end + end + +end diff --git a/activerecord/test/adapter_test.rb b/activerecord/test/adapter_test.rb new file mode 100644 index 0000000000..4496da8585 --- /dev/null +++ b/activerecord/test/adapter_test.rb @@ -0,0 +1,38 @@ +require 'abstract_unit' + +class AdapterTest < Test::Unit::TestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def test_tables + if @connection.respond_to?(:tables) + tables = @connection.tables + assert tables.include?("accounts") + assert tables.include?("authors") + assert tables.include?("tasks") + assert tables.include?("topics") + else + warn "#{@connection.class} does not respond to #tables" + end + end + + def test_indexes + if @connection.respond_to?(:indexes) + indexes = @connection.indexes("accounts") + assert indexes.empty? + + @connection.add_index :accounts, :firm_id + indexes = @connection.indexes("accounts") + assert_equal "accounts", indexes.first.table + assert_equal "accounts_firm_id_index", indexes.first.name + assert !indexes.first.unique + assert_equal ["firm_id"], indexes.first.columns + else + warn "#{@connection.class} does not respond to #indexes" + end + + ensure + @connection.remove_index :accounts, :firm_id rescue nil + end +end diff --git a/activerecord/test/ar_schema_test.rb b/activerecord/test/ar_schema_test.rb new file mode 100644 index 0000000000..2b7c0c3fc7 --- /dev/null +++ b/activerecord/test/ar_schema_test.rb @@ -0,0 +1,31 @@ +require 'abstract_unit' +require "#{File.dirname(__FILE__)}/../lib/active_record/schema" + +if ActiveRecord::Base.connection.supports_migrations? + + class ActiveRecordSchemaTest < Test::Unit::TestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table :fruits rescue nil + end + + def test_schema_define + ActiveRecord::Schema.define(:version => 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :size, :string + t.column :texture, :string + t.column :flavor, :string + end + end + + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + assert_nothing_raised { @connection.select_all "SELECT * FROM schema_info" } + assert_equal 7, @connection.select_one("SELECT version FROM schema_info")['version'].to_i + end + end + +end diff --git a/activerecord/test/migration_test.rb b/activerecord/test/migration_test.rb index f98986cdfa..fa08cf4cf3 100644 --- a/activerecord/test/migration_test.rb +++ b/activerecord/test/migration_test.rb @@ -32,12 +32,16 @@ if ActiveRecord::Base.connection.supports_migrations? def test_add_index Person.connection.add_column "people", "last_name", :string + Person.connection.add_column "people", "administrator", :boolean assert_nothing_raised { Person.connection.add_index("people", "last_name") } assert_nothing_raised { Person.connection.remove_index("people", "last_name") } assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } assert_nothing_raised { Person.connection.remove_index("people", "last_name") } + + assert_nothing_raised { Person.connection.add_index("people", %w(last_name first_name administrator), :name => "named_admin") } + assert_nothing_raised { Person.connection.remove_index("people", :name => "named_admin") } end def test_create_table_adds_id diff --git a/activerecord/test/schema_dumper_test.rb b/activerecord/test/schema_dumper_test.rb new file mode 100644 index 0000000000..e24724c9f8 --- /dev/null +++ b/activerecord/test/schema_dumper_test.rb @@ -0,0 +1,19 @@ +require 'abstract_unit' +require "#{File.dirname(__FILE__)}/../lib/active_record/schema_dumper" +require 'stringio' + +if ActiveRecord::Base.connection.respond_to?(:tables) + + class SchemaDumperTest < Test::Unit::TestCase + def test_schema_dump + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + output = stream.string + + assert_match %r{create_table "accounts"}, output + assert_match %r{create_table "authors"}, output + assert_no_match %r{create_table "schema_info"}, output + end + end + +end |