aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJamis Buck <jamis@37signals.com>2005-09-23 13:29:33 +0000
committerJamis Buck <jamis@37signals.com>2005-09-23 13:29:33 +0000
commit7dc45818dc43c163700efc9896a0f3feafa31138 (patch)
treeb9a263bfd2416bfbc9843b563dcc0e2565f2f877
parent436d54c3f19fc80713d975482a28a95cfa60a1e4 (diff)
downloadrails-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/CHANGELOG2
-rwxr-xr-xactiverecord/lib/active_record.rb2
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb59
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/mysql_adapter.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb29
-rw-r--r--activerecord/lib/active_record/schema.rb23
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb87
-rw-r--r--activerecord/test/adapter_test.rb38
-rw-r--r--activerecord/test/ar_schema_test.rb31
-rw-r--r--activerecord/test/migration_test.rb4
-rw-r--r--activerecord/test/schema_dumper_test.rb19
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