Skip to content

Commit

Permalink
Add ActiveRecord::SchemaDumper for dumping a DB schema to a pure-ruby…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
jamis committed Sep 23, 2005
1 parent 436d54c commit 7dc4581
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 25 deletions.
2 changes: 2 additions & 0 deletions activerecord/CHANGELOG
@@ -1,5 +1,7 @@
*SVN* *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 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] * Fixed that the create_x method from belongs_to wouldn't save the association properly #2042 [Florian Weber]
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record.rb
Expand Up @@ -71,4 +71,4 @@
require "active_record/connection_adapters/#{adapter}_adapter" require "active_record/connection_adapters/#{adapter}_adapter"
end end


require 'active_record/query_cache' require 'active_record/query_cache'
Expand Up @@ -152,13 +152,13 @@ def self.symbolize_strings_in_hash(hash) #:nodoc:


module ConnectionAdapters # :nodoc: module ConnectionAdapters # :nodoc:
class Column # :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 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 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 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)" # The sql_type is just used for extracting the limit, such as 10 in "varchar(10)"
def initialize(name, default, sql_type = nil) def initialize(name, default, sql_type = nil, null = true)
@name, @default, @type = name, type_cast(default), simplified_type(sql_type) @name, @default, @type, @null = name, type_cast(default), simplified_type(sql_type), null
@limit = extract_limit(sql_type) unless sql_type.nil? @limit = extract_limit(sql_type) unless sql_type.nil?
end end


Expand Down Expand Up @@ -275,6 +275,12 @@ def select_all(sql, name = nil) end
# Returns a record hash with the column names as a keys and fields as values. # Returns a record hash with the column names as a keys and fields as values.
def select_one(sql, name = nil) end 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+. # Returns an array of column objects for the table specified by +table_name+.
def columns(table_name, name = nil) end def columns(table_name, name = nil) end


Expand Down Expand Up @@ -423,12 +429,46 @@ def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented" raise NotImplementedError, "rename_column is not implemented"
end end


def add_index(table_name, column_name, index_type = '') # Create a new index on the given table. By default, it will be named
execute "CREATE #{index_type} INDEX #{table_name}_#{column_name.to_a.first}_index ON #{table_name} (#{column_name.to_a.join(", ")})" # <code>"#{table_name}_#{column_name.to_a.first}_index"</code>, but you
end # 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) if Hash === options # legacy support, since this param was a string
execute "DROP INDEX #{table_name}_#{column_name}_index ON #{table_name}" 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 end


def supports_migrations? def supports_migrations?
Expand Down Expand Up @@ -504,6 +544,9 @@ def format_log_entry(message, dump = nil)
end end
end end


class IndexDefinition < Struct.new(:table, :name, :unique, :columns)
end

class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null) class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null)
def to_sql def to_sql
column_sql = "#{name} #{type_to_sql(type.to_sym, limit)}" column_sql = "#{name} #{type_to_sql(type.to_sym, limit)}"
Expand Down
Expand Up @@ -102,10 +102,31 @@ def select_one(sql, name = nil)
result.nil? ? nil : result.first result.nil? ? nil : result.first
end 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) def columns(table_name, name = nil)
sql = "SHOW FIELDS FROM #{table_name}" sql = "SHOW FIELDS FROM #{table_name}"
columns = [] 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 columns
end end


Expand Down
Expand Up @@ -150,17 +150,27 @@ def begin_db_transaction() @connection.transaction end
def commit_db_transaction() @connection.commit end def commit_db_transaction() @connection.commit end
def rollback_db_transaction() @connection.rollback end def rollback_db_transaction() @connection.rollback end



def tables(name = nil)
def tables execute("SELECT name FROM sqlite_master WHERE type = 'table'", name).map do |row|
execute('.table').map { |table| Table.new(table) } row[0]
end
end end


def columns(table_name, name = nil) def columns(table_name, name = nil)
table_structure(table_name).map { |field| 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 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) def primary_key(table_name)
column = table_structure(table_name).find {|field| field['pk'].to_i == 1} column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
column ? column['name'] : nil column ? column['name'] : nil
Expand Down Expand Up @@ -222,17 +232,6 @@ def table_structure(table_name)
end end
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: def alter_table(table_name, options = {}) #:nodoc:
altered_table_name = "altered_#{table_name}" altered_table_name = "altered_#{table_name}"
caller = lambda {|definition| yield definition if block_given?} caller = lambda {|definition| yield definition if block_given?}
Expand Down
23 changes: 23 additions & 0 deletions 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
87 changes: 87 additions & 0 deletions 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
38 changes: 38 additions & 0 deletions 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
31 changes: 31 additions & 0 deletions 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
4 changes: 4 additions & 0 deletions activerecord/test/migration_test.rb
Expand Up @@ -32,12 +32,16 @@ def teardown


def test_add_index def test_add_index
Person.connection.add_column "people", "last_name", :string 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.add_index("people", "last_name") }
assert_nothing_raised { Person.connection.remove_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.add_index("people", ["last_name", "first_name"]) }
assert_nothing_raised { Person.connection.remove_index("people", "last_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 end


def test_create_table_adds_id def test_create_table_adds_id
Expand Down

0 comments on commit 7dc4581

Please sign in to comment.