1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Refactored the AbstractAdapter to be a lot less scary. Cleaned up the docs and style for the OSS adapters

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2339 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
David Heinemeier Hansson 2005-09-25 17:56:03 +00:00
parent ea65465422
commit b3df95985a
11 changed files with 799 additions and 717 deletions

View file

@ -0,0 +1,126 @@
module ActiveRecord
class Base
class ConnectionSpecification #:nodoc:
attr_reader :config, :adapter_method
def initialize (config, adapter_method)
@config, @adapter_method = config, adapter_method
end
end
# The class -> [adapter_method, config] map
@@defined_connections = {}
# Establishes the connection to the database. Accepts a hash as input where
# the :adapter key must be specified with the name of a database adapter (in lower-case)
# example for regular databases (MySQL, Postgresql, etc):
#
# ActiveRecord::Base.establish_connection(
# :adapter => "mysql",
# :host => "localhost",
# :username => "myuser",
# :password => "mypass",
# :database => "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
# :adapter => "sqlite",
# :dbfile => "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from yaml for example):
# ActiveRecord::Base.establish_connection(
# "adapter" => "sqlite",
# "dbfile" => "path/to/dbfile"
# )
#
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
def self.establish_connection(spec = nil)
case spec
when nil
raise AdapterNotSpecified unless defined? RAILS_ENV
establish_connection(RAILS_ENV)
when ConnectionSpecification
@@defined_connections[self] = spec
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
else
raise AdapterNotSpecified, "#{spec} database is not configured"
end
else
spec = spec.symbolize_keys
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
adapter_method = "#{spec[:adapter]}_connection"
unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
remove_connection
establish_connection(ConnectionSpecification.new(spec, adapter_method))
end
end
def self.active_connections #:nodoc:
if threaded_connections
Thread.current['active_connections'] ||= {}
else
@@active_connections ||= {}
end
end
# Locate the connection of the nearest super class. This can be an
# active or defined connections: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
# for (not necessarily the current class).
def self.retrieve_connection #:nodoc:
klass = self
ar_super = ActiveRecord::Base.superclass
until klass == ar_super
if conn = active_connections[klass]
return conn
elsif conn = @@defined_connections[klass]
klass.connection = conn
return self.connection
end
klass = klass.superclass
end
raise ConnectionNotEstablished
end
# Returns true if a connection that's accessible to this class have already been opened.
def self.connected?
klass = self
until klass == ActiveRecord::Base.superclass
if active_connections[klass]
return true
else
klass = klass.superclass
end
end
return false
end
# Remove the connection for this class. This will close the active
# connection and the defined connection (if they exist). The result
# can be used as argument for establish_connection, for easy
# re-establishing of the connection.
def self.remove_connection(klass=self)
conn = @@defined_connections[klass]
@@defined_connections.delete(klass)
active_connections[klass] = nil
conn.config if conn
end
# Set the connection for the class.
def self.connection=(spec)
raise ConnectionNotEstablished unless spec
conn = self.send(spec.adapter_method, spec.config)
active_connections[self] = conn
end
# Converts all strings in a hash to symbols.
def self.symbolize_strings_in_hash(hash) #:nodoc:
hash.symbolize_keys
end
end
end

View file

@ -0,0 +1,73 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
# TODO: Document me!
module DatabaseStatements
# Returns an array of record hashes with the column names as a keys and fields as values.
def select_all(sql, name = nil) end
# Returns a record hash with the column names as a keys and fields as values.
def select_one(sql, name = nil) end
# Returns a single value from a record
def select_value(sql, name = nil)
result = select_one(sql, name)
result.nil? ? nil : result.values.first
end
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(sql, name = nil)
result = select_all(sql, name)
result.map{ |v| v.values.first }
end
# Executes the statement
def execute(sql, name = nil) end
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil) end
# Executes the delete statement and returns the number of rows affected.
def delete(sql, name = nil) end
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
begin
if block_given?
begin_db_transaction if start_db_transaction
result = yield
commit_db_transaction if start_db_transaction
result
end
rescue Exception => database_transaction_rollback
rollback_db_transaction if start_db_transaction
raise
end
end
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
# Commits the transaction (and turns on auto-committing).
def commit_db_transaction() end
# Rolls back the transaction (and turns on auto-committing). Must be done if the transaction block
# raises an exception or returns false.
def rollback_db_transaction() end
def add_limit!(sql, options) #:nodoc:
return unless options
add_limit_offset!(sql, options)
end
def add_limit_offset!(sql, options) #:nodoc:
return if options[:limit].nil?
sql << " LIMIT #{options[:limit]}"
sql << " OFFSET #{options[:offset]}" if options.has_key?(:offset) and !options[:offset].nil?
end
end
end
end

View file

@ -0,0 +1,42 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
# TODO: Document me!
module Quoting
def quote(value, column = nil)
case value
when String
if column && column.type == :binary
"'#{quote_string(column.string_to_binary(value))}'" # ' (for ruby-mode)
elsif column && [:integer, :float].include?(column.type)
value.to_s
else
"'#{quote_string(value)}'" # ' (for ruby-mode)
end
when NilClass then "NULL"
when TrueClass then (column && column.type == :boolean ? quoted_true : "1")
when FalseClass then (column && column.type == :boolean ? quoted_false : "0")
when Float, Fixnum, Bignum then value.to_s
when Date then "'#{value.to_s}'"
when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
else "'#{quote_string(value.to_yaml)}'"
end
end
def quote_string(s)
s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
end
def quote_column_name(name)
name
end
def quoted_true
"'t'"
end
def quoted_false
"'f'"
end
end
end
end

View file

@ -0,0 +1,168 @@
module ActiveRecord
module ConnectionAdapters #:nodoc:
class Column #:nodoc:
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, null = true)
@name, @type, @null = name, simplified_type(sql_type), null
# have to do this one separately because type_cast depends on #type
@default = type_cast(default)
@limit = extract_limit(sql_type) unless sql_type.nil?
end
def klass
case type
when :integer then Fixnum
when :float then Float
when :datetime then Time
when :date then Date
when :timestamp then Time
when :time then Time
when :text, :string then String
when :binary then String
when :boolean then Object
end
end
def type_cast(value)
if value.nil? then return nil end
case type
when :string then value
when :text then value
when :integer then value.to_i rescue value ? 1 : 0
when :float then value.to_f
when :datetime then string_to_time(value)
when :timestamp then string_to_time(value)
when :time then string_to_dummy_time(value)
when :date then string_to_date(value)
when :binary then binary_to_string(value)
when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
else value
end
end
def human_name
Base.human_attribute_name(@name)
end
def string_to_binary(value)
value
end
def binary_to_string(value)
value
end
private
def string_to_date(string)
return string unless string.is_a?(String)
date_array = ParseDate.parsedate(string.to_s)
# treat 0000-00-00 as nil
Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
end
def string_to_time(string)
return string unless string.is_a?(String)
time_array = ParseDate.parsedate(string.to_s).compact
# treat 0000-00-00 00:00:00 as nil
Time.send(Base.default_timezone, *time_array) rescue nil
end
def string_to_dummy_time(string)
return string unless string.is_a?(String)
time_array = ParseDate.parsedate(string.to_s)
# pad the resulting array with dummy date information
time_array[0] = 2000; time_array[1] = 1; time_array[2] = 1;
Time.send(Base.default_timezone, *time_array) rescue nil
end
def extract_limit(sql_type)
$1.to_i if sql_type =~ /\((.*)\)/
end
def simplified_type(field_type)
case field_type
when /int/i
:integer
when /float|double|decimal|numeric/i
:float
when /datetime/i
:datetime
when /timestamp/i
:timestamp
when /time/i
:time
when /date/i
:date
when /clob/i, /text/i
:text
when /blob/i, /binary/i
:binary
when /char/i, /string/i
:string
when /boolean/i
:boolean
end
end
end
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
end
class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null) #:nodoc:
def to_sql
column_sql = "#{name} #{type_to_sql(type.to_sym, limit)}"
add_column_options!(column_sql, :null => null, :default => default)
column_sql
end
alias to_s :to_sql
private
def type_to_sql(name, limit)
base.type_to_sql(name, limit) rescue name
end
def add_column_options!(sql, options)
base.add_column_options!(sql, options.merge(:column => self))
end
end
class TableDefinition #:nodoc:
attr_accessor :columns
def initialize(base)
@columns = []
@base = base
end
def primary_key(name)
column(name, native[:primary_key])
end
def [](name)
@columns.find {|column| column.name == name}
end
def column(name, type, options = {})
column = self[name] || ColumnDefinition.new(@base, name, type)
column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym]
column.default = options[:default]
column.null = options[:null]
@columns << column unless @columns.include? column
self
end
def to_sql
@columns * ', '
end
private
def native
@base.native_database_types
end
end
end
end

View file

@ -0,0 +1,137 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
# TODO: Document me!
module SchemaStatements
def native_database_types #:nodoc:
{}
end
# 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
def create_table(name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
yield table_definition
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{name} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
end
def drop_table(name)
execute "DROP TABLE #{name}"
end
def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}"
add_column_options!(add_column_sql, options)
execute(add_column_sql)
end
def remove_column(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP #{column_name}"
end
def change_column(table_name, column_name, type, options = {})
raise NotImplementedError, "change_column is not implemented"
end
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end
def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented"
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"
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
# Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
def structure_dump #:nodoc:
end
def initialize_schema_information #:nodoc:
begin
execute "CREATE TABLE schema_info (version #{type_to_sql(:integer)})"
execute "INSERT INTO schema_info (version) VALUES(0)"
rescue ActiveRecord::StatementInvalid
# Schema has been intialized
end
end
def dump_schema_information #:nodoc:
begin
if (current_schema = ActiveRecord::Migrator.current_version) > 0
return "INSERT INTO schema_info (version) VALUES (#{current_schema});"
end
rescue ActiveRecord::StatementInvalid
# No Schema Info
end
end
def type_to_sql(type, limit = nil) #:nodoc:
native = native_database_types[type]
limit ||= native[:limit]
column_type_sql = native[:name]
column_type_sql << "(#{limit})" if limit
column_type_sql
end
def add_column_options!(sql, options) #:nodoc:
sql << " NOT NULL" if options[:null] == false
sql << " DEFAULT #{quote(options[:default], options[:column])}" unless options[:default].nil?
end
end
end
end

View file

@ -1,6 +1,12 @@
require 'benchmark'
require 'date'
require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
require 'active_record/connection_adapters/abstract/quoting'
require 'active_record/connection_adapters/abstract/connection_specification'
# Method that requires a library, ensuring that rubygems is loaded
# This is used in the database adaptors to require DB drivers. Reasons:
# (1) database drivers are the only third-party library that Rails depend upon
@ -25,491 +31,35 @@ def require_library_or_gem(library_name)
end
module ActiveRecord
class Base
class ConnectionSpecification #:nodoc:
attr_reader :config, :adapter_method
def initialize (config, adapter_method)
@config, @adapter_method = config, adapter_method
end
end
# The class -> [adapter_method, config] map
@@defined_connections = {}
# Establishes the connection to the database. Accepts a hash as input where
# the :adapter key must be specified with the name of a database adapter (in lower-case)
# example for regular databases (MySQL, Postgresql, etc):
#
# ActiveRecord::Base.establish_connection(
# :adapter => "mysql",
# :host => "localhost",
# :username => "myuser",
# :password => "mypass",
# :database => "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
# :adapter => "sqlite",
# :dbfile => "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from yaml for example):
# ActiveRecord::Base.establish_connection(
# "adapter" => "sqlite",
# "dbfile" => "path/to/dbfile"
# )
#
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
def self.establish_connection(spec = nil)
case spec
when nil
raise AdapterNotSpecified unless defined? RAILS_ENV
establish_connection(RAILS_ENV)
when ConnectionSpecification
@@defined_connections[self] = spec
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
else
raise AdapterNotSpecified, "#{spec} database is not configured"
end
else
spec = spec.symbolize_keys
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
adapter_method = "#{spec[:adapter]}_connection"
unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
remove_connection
establish_connection(ConnectionSpecification.new(spec, adapter_method))
end
end
def self.active_connections #:nodoc:
if threaded_connections
Thread.current['active_connections'] ||= {}
else
@@active_connections ||= {}
end
end
# Locate the connection of the nearest super class. This can be an
# active or defined connections: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
# for (not necessarily the current class).
def self.retrieve_connection #:nodoc:
klass = self
ar_super = ActiveRecord::Base.superclass
until klass == ar_super
if conn = active_connections[klass]
return conn
elsif conn = @@defined_connections[klass]
klass.connection = conn
return self.connection
end
klass = klass.superclass
end
raise ConnectionNotEstablished
end
# Returns true if a connection that's accessible to this class have already been opened.
def self.connected?
klass = self
until klass == ActiveRecord::Base.superclass
if active_connections[klass]
return true
else
klass = klass.superclass
end
end
return false
end
# Remove the connection for this class. This will close the active
# connection and the defined connection (if they exist). The result
# can be used as argument for establish_connection, for easy
# re-establishing of the connection.
def self.remove_connection(klass=self)
conn = @@defined_connections[klass]
@@defined_connections.delete(klass)
active_connections[klass] = nil
conn.config if conn
end
# Set the connection for the class.
def self.connection=(spec)
raise ConnectionNotEstablished unless spec
conn = self.send(spec.adapter_method, spec.config)
active_connections[self] = conn
end
# Converts all strings in a hash to symbols.
def self.symbolize_strings_in_hash(hash) #:nodoc:
hash.symbolize_keys
end
end
module ConnectionAdapters # :nodoc:
class Column # :nodoc:
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, null = true)
@name, @type, @null = name, simplified_type(sql_type), null
# have to do this one separately because type_cast depends on #type
@default = type_cast(default)
@limit = extract_limit(sql_type) unless sql_type.nil?
end
def klass
case type
when :integer then Fixnum
when :float then Float
when :datetime then Time
when :date then Date
when :timestamp then Time
when :time then Time
when :text, :string then String
when :binary then String
when :boolean then Object
end
end
def type_cast(value)
if value.nil? then return nil end
case type
when :string then value
when :text then value
when :integer then value.to_i rescue value ? 1 : 0
when :float then value.to_f
when :datetime then string_to_time(value)
when :timestamp then string_to_time(value)
when :time then string_to_dummy_time(value)
when :date then string_to_date(value)
when :binary then binary_to_string(value)
when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
else value
end
end
def human_name
Base.human_attribute_name(@name)
end
def string_to_binary(value)
value
end
def binary_to_string(value)
value
end
private
def string_to_date(string)
return string unless string.is_a?(String)
date_array = ParseDate.parsedate(string.to_s)
# treat 0000-00-00 as nil
Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
end
def string_to_time(string)
return string unless string.is_a?(String)
time_array = ParseDate.parsedate(string.to_s).compact
# treat 0000-00-00 00:00:00 as nil
Time.send(Base.default_timezone, *time_array) rescue nil
end
def string_to_dummy_time(string)
return string unless string.is_a?(String)
time_array = ParseDate.parsedate(string.to_s)
# pad the resulting array with dummy date information
time_array[0] = 2000; time_array[1] = 1; time_array[2] = 1;
Time.send(Base.default_timezone, *time_array) rescue nil
end
def extract_limit(sql_type)
$1.to_i if sql_type =~ /\((.*)\)/
end
def simplified_type(field_type)
case field_type
when /int/i
:integer
when /float|double|decimal|numeric/i
:float
when /datetime/i
:datetime
when /timestamp/i
:timestamp
when /time/i
:time
when /date/i
:date
when /clob/i, /text/i
:text
when /blob/i, /binary/i
:binary
when /char/i, /string/i
:string
when /boolean/i
:boolean
end
end
end
# All the concrete database adapters follow the interface laid down in this class.
# You can use this interface directly by borrowing the database connection from the Base with
# Base.connection.
class AbstractAdapter
include Quoting, DatabaseStatements, SchemaStatements
@@row_even = true
def initialize(connection, logger = nil) # :nodoc:
def initialize(connection, logger = nil) #:nodoc:
@connection, @logger = connection, logger
@runtime = 0
end
# Returns an array of record hashes with the column names as a keys and fields as values.
def select_all(sql, name = nil) end
# Returns a record hash with the column names as a keys and fields as values.
def select_one(sql, name = nil) end
# Returns a single value from a record
def select_value(sql, name = nil)
result = select_one(sql, name)
result.nil? ? nil : result.values.first
# Returns the human-readable name of the adapter. Use mixed case - one can always use downcase if needed.
def adapter_name
'Abstract'
end
# Returns true for database adapters that has implemented the schema statements.
def supports_migrations?
false
end
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(sql, name = nil)
result = select_all(sql, name)
result.map{ |v| v.values.first }
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
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil) end
# Executes the delete statement and returns the number of rows affected.
def delete(sql, name = nil) end
def reset_runtime # :nodoc:
def reset_runtime #:nodoc:
rt = @runtime
@runtime = 0
return rt
end
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
begin
if block_given?
begin_db_transaction if start_db_transaction
result = yield
commit_db_transaction if start_db_transaction
result
end
rescue Exception => database_transaction_rollback
rollback_db_transaction if start_db_transaction
raise
end
end
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
# Commits the transaction (and turns on auto-committing).
def commit_db_transaction() end
# Rolls back the transaction (and turns on auto-committing). Must be done if the transaction block
# raises an exception or returns false.
def rollback_db_transaction() end
def quoted_true() "'t'" end
def quoted_false() "'f'" end
def quote(value, column = nil)
case value
when String
if column && column.type == :binary
"'#{quote_string(column.string_to_binary(value))}'" # ' (for ruby-mode)
elsif column && [:integer, :float].include?(column.type)
value.to_s
else
"'#{quote_string(value)}'" # ' (for ruby-mode)
end
when NilClass then "NULL"
when TrueClass then (column && column.type == :boolean ? quoted_true : "1")
when FalseClass then (column && column.type == :boolean ? quoted_false : "0")
when Float, Fixnum, Bignum then value.to_s
when Date then "'#{value.to_s}'"
when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
else "'#{quote_string(value.to_yaml)}'"
end
end
def quote_string(s)
s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
end
def quote_column_name(name)
name
end
# Returns the human-readable name of the adapter. Use mixed case - one can always use downcase if needed.
def adapter_name()
'Abstract'
end
# Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
def structure_dump() end
def add_limit!(sql, options)
return unless options
add_limit_offset!(sql, options)
end
def add_limit_offset!(sql, options)
return if options[:limit].nil?
sql << " LIMIT #{options[:limit]}"
sql << " OFFSET #{options[:offset]}" if options.has_key?(:offset) and !options[:offset].nil?
end
def initialize_schema_information
begin
execute "CREATE TABLE schema_info (version #{type_to_sql(:integer)})"
execute "INSERT INTO schema_info (version) VALUES(0)"
rescue ActiveRecord::StatementInvalid
# Schema has been intialized
end
end
def dump_schema_information
begin
if (current_schema = ActiveRecord::Migrator.current_version) > 0
return "INSERT INTO schema_info (version) VALUES (#{current_schema});"
end
rescue ActiveRecord::StatementInvalid
# No Schema Info
end
end
def create_table(name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
yield table_definition
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{name} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
end
def drop_table(name)
execute "DROP TABLE #{name}"
end
def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}"
add_column_options!(add_column_sql, options)
execute(add_column_sql)
end
def remove_column(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP #{column_name}"
end
def change_column(table_name, column_name, type, options = {})
raise NotImplementedError, "change_column is not implemented"
end
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end
def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented"
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"
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?
false
end
def native_database_types
{}
end
def type_to_sql(type, limit = nil)
native = native_database_types[type]
limit ||= native[:limit]
column_type_sql = native[:name]
column_type_sql << "(#{limit})" if limit
column_type_sql
end
def add_column_options!(sql, options)
sql << " NOT NULL" if options[:null] == false
sql << " DEFAULT #{quote(options[:default], options[:column])}" unless options[:default].nil?
end
protected
def log(sql, name)
begin
@ -561,61 +111,5 @@ module ActiveRecord
end
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)}"
add_column_options!(column_sql, :null => null, :default => default)
column_sql
end
alias to_s :to_sql
private
def type_to_sql(name, limit)
base.type_to_sql(name, limit) rescue name
end
def add_column_options!(sql, options)
base.add_column_options!(sql, options.merge(:column => self))
end
end
class TableDefinition
attr_accessor :columns
def initialize(base)
@columns = []
@base = base
end
def primary_key(name)
column(name, native[:primary_key])
end
def [](name)
@columns.find {|column| column.name == name}
end
def column(name, type, options = {})
column = self[name] || ColumnDefinition.new(@base, name, type)
column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym]
column.default = options[:default]
column.null = options[:null]
@columns << column unless @columns.include? column
self
end
def to_sql
@columns * ', '
end
private
def native
@base.native_database_types
end
end
end
end

View file

@ -81,12 +81,21 @@ module ActiveRecord
"Lost connection to MySQL server during query",
"MySQL server has gone away"
]
def supports_migrations?
def initialize(connection, logger, connection_options=nil)
super(connection, logger)
@connection_options = connection_options
end
def adapter_name #:nodoc:
'MySQL'
end
def supports_migrations? #:nodoc:
true
end
def native_database_types
def native_database_types #:nodoc
{
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
@ -102,58 +111,42 @@ module ActiveRecord
}
end
def initialize(connection, logger, connection_options=nil)
super(connection, logger)
@connection_options = connection_options
# QUOTING ==================================================
def quote_column_name(name) #:nodoc:
"`#{name}`"
end
def adapter_name
'MySQL'
def quote_string(string) #:nodoc:
Mysql::quote(string)
end
def select_all(sql, name = nil)
def quoted_true
"1"
end
def quoted_false
"0"
end
def quote_column_name(name)
"`#{name}`"
end
# DATABASE STATEMENTS ======================================
def select_all(sql, name = nil) #:nodoc:
select(sql, name)
end
def select_one(sql, name = nil)
def select_one(sql, name = nil) #:nodoc:
result = select(sql, name)
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 << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
columns
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
execute(sql, name = nil)
id_value || @connection.insert_id
end
def execute(sql, name = nil, retries = 2)
def execute(sql, name = nil, retries = 2) #:nodoc:
unless @logger
@connection.query(sql)
else
@ -175,52 +168,39 @@ module ActiveRecord
end
end
def update(sql, name = nil)
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name = nil)
id_value || @connection.insert_id
end
def update(sql, name = nil) #:nodoc:
execute(sql, name)
@connection.affected_rows
end
alias_method :delete, :update
alias_method :delete, :update #:nodoc:
def begin_db_transaction
def begin_db_transaction #:nodoc:
execute "BEGIN"
rescue Exception
# Transactions aren't supported
end
def commit_db_transaction
def commit_db_transaction #:nodoc:
execute "COMMIT"
rescue Exception
# Transactions aren't supported
end
def rollback_db_transaction
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
rescue Exception
# Transactions aren't supported
end
def quoted_true() "1" end
def quoted_false() "0" end
def quote_column_name(name)
"`#{name}`"
end
def quote_string(string)
Mysql::quote(string)
end
def structure_dump
select_all("SHOW TABLES").inject("") do |structure, table|
structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
end
end
def add_limit_offset!(sql, options)
def add_limit_offset!(sql, options) #:nodoc
if options[:limit]
if options[:offset].blank?
sql << " LIMIT #{options[:limit]}"
@ -230,26 +210,68 @@ module ActiveRecord
end
end
def recreate_database(name)
# SCHEMA STATEMENTS ========================================
def structure_dump #:nodoc:
select_all("SHOW TABLES").inject("") do |structure, table|
structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
end
end
def recreate_database(name) #:nodoc:
drop_database(name)
create_database(name)
end
def drop_database(name)
execute "DROP DATABASE IF EXISTS #{name}"
end
def create_database(name)
def create_database(name) #:nodoc:
execute "CREATE DATABASE #{name}"
end
def change_column_default(table_name, column_name, default)
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS #{name}"
end
def tables(name = nil) #:nodoc:
tables = []
execute("SHOW TABLES", name).each { |field| tables << field[0] }
tables
end
def indexes(table_name, name = nil)#:nodoc:
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)#:nodoc:
sql = "SHOW FIELDS FROM #{table_name}"
columns = []
execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
columns
end
def create_table(name, options = {}) #:nodoc:
super(name, {:options => "ENGINE=InnoDB"}.merge(options))
end
def change_column_default(table_name, column_name, default) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
change_column(table_name, column_name, current_type, { :default => default })
end
def change_column(table_name, column_name, type, options = {})
def change_column(table_name, column_name, type, options = {}) #:nodoc:
options[:default] ||= select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}"
@ -257,14 +279,11 @@ module ActiveRecord
execute(change_column_sql)
end
def rename_column(table_name, column_name, new_column_name)
def rename_column(table_name, column_name, new_column_name) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
end
def create_table(name, options = {})
super(name, {:options => "ENGINE=InnoDB"}.merge(options))
end
private
def select(sql, name = nil)
@ -278,4 +297,4 @@ module ActiveRecord
end
end
end
end
end

View file

@ -1,14 +1,4 @@
# postgresql_adaptor.rb
# author: Luke Holden <lholden@cablelan.net>
# notes: Currently this adaptor does not pass the test_zero_date_fields
# and test_zero_datetime_fields unit tests in the BasicsTest test
# group.
#
# This is due to the fact that, in postgresql you can not have a
# totally zero timestamp. Instead null/nil should be used to
# represent no value.
#
# Author: Luke Holden <lholden@cablelan.net>
require 'active_record/connection_adapters/abstract_adapter'
require 'parsedate'
@ -60,6 +50,10 @@ module ActiveRecord
# * <tt>:encoding</tt> -- An optional client encoding that is using in a SET client_encoding TO <encoding> call on connection.
# * <tt>:min_messages</tt> -- An optional client min messages that is using in a SET client_min_messages TO <min_messages> call on connection.
class PostgreSQLAdapter < AbstractAdapter
def adapter_name
'PostgreSQL'
end
def native_database_types
{
:primary_key => "serial primary key",
@ -80,17 +74,71 @@ module ActiveRecord
true
end
def select_all(sql, name = nil)
# QUOTING ==================================================
def quote(value, column = nil)
if value.class == String && column && column.type == :binary
quote_bytea(value)
else
super
end
end
def quote_column_name(name)
%("#{name}")
end
# DATABASE STATEMENTS ======================================
def select_all(sql, name = nil) #:nodoc:
select(sql, name)
end
def select_one(sql, name = nil)
def select_one(sql, name = nil) #:nodoc:
result = select(sql, name)
result.nil? ? nil : result.first
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name)
table = sql.split(" ", 4)[2]
return id_value || last_insert_id(table, pk)
end
def query(sql, name = nil) #:nodoc:
log(sql, name) { @connection.query(sql) }
end
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.exec(sql) }
end
def update(sql, name = nil) #:nodoc:
execute(sql, name).cmdtuples
end
alias_method :delete, :update #:nodoc:
def begin_db_transaction #:nodoc:
execute "BEGIN"
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
end
# SCHEMA STATEMENTS ========================================
# Return the list of all tables in the schema search path.
def tables(name = nil)
def tables(name = nil) #:nodoc:
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
query(<<-SQL, name).map { |row| row[0] }
SELECT tablename
@ -99,7 +147,7 @@ module ActiveRecord
SQL
end
def indexes(table_name, name = nil)
def indexes(table_name, name = nil) #:nodoc:
result = query(<<-SQL, name)
SELECT i.relname, d.indisunique, a.attname
FROM pg_class t, pg_class i, pg_index d, pg_attribute a
@ -132,33 +180,27 @@ module ActiveRecord
indexes
end
def columns(table_name, name = nil)
def columns(table_name, name = nil) #:nodoc:
column_definitions(table_name).collect do |name, type, default, notnull|
Column.new(name, default_value(default), translate_field_type(type),
notnull == "f")
end
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
execute(sql, name)
table = sql.split(" ", 4)[2]
return id_value || last_insert_id(table, pk)
# Set the schema search path to a string of comma-separated schema names.
# Names beginning with $ are quoted (e.g. $user => '$user')
# See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html
def schema_search_path=(schema_csv) #:nodoc:
if schema_csv
execute "SET search_path TO #{schema_csv}"
@schema_search_path = nil
end
end
def query(sql, name = nil)
log(sql, name) { @connection.query(sql) }
def schema_search_path #:nodoc:
@schema_search_path ||= query('SHOW search_path')[0][0]
end
def execute(sql, name = nil)
log(sql, name) { @connection.exec(sql) }
end
def update(sql, name = nil)
execute(sql, name).cmdtuples
end
alias_method :delete, :update
def add_column(table_name, column_name, type, options = {})
native_type = native_database_types[type]
sql_commands = ["ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}"]
@ -170,57 +212,21 @@ module ActiveRecord
end
sql_commands.each { |cmd| execute(cmd) }
end
def begin_db_transaction() execute "BEGIN" end
def commit_db_transaction() execute "COMMIT" end
def rollback_db_transaction() execute "ROLLBACK" end
def quote(value, column = nil)
if value.class == String && column && column.type == :binary
quote_bytea(value)
else
super
end
end
def quote_column_name(name)
%("#{name}")
end
def adapter_name
'PostgreSQL'
end
# Set the schema search path to a string of comma-separated schema names.
# Names beginning with $ are quoted (e.g. $user => '$user')
# See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html
def schema_search_path=(schema_csv)
if schema_csv
execute "SET search_path TO #{schema_csv}"
@schema_search_path = nil
end
end
def schema_search_path
@schema_search_path ||= query('SHOW search_path')[0][0]
end
def change_column(table_name, column_name, type, options = {})
def change_column(table_name, column_name, type, options = {}) #:nodoc:
execute = "ALTER TABLE #{table_name} ALTER #{column_name} TYPE #{type}"
change_column_default(table_name, column_name, options[:default]) unless options[:default].nil?
end
def change_column_default(table_name, column_name, default)
def change_column_default(table_name, column_name, default) #:nodoc:
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT '#{default}'"
end
def rename_column(table_name, column_name, new_column_name)
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}"
end
def remove_index(table_name, options)
def remove_index(table_name, options) #:nodoc:
if Hash === options
index_name = options[:name]
else
@ -229,6 +235,7 @@ module ActiveRecord
execute "DROP INDEX #{index_name}"
end
private
BYTEA_COLUMN_TYPE_OID = 17

View file

@ -1,6 +1,5 @@
# sqlite_adapter.rb
# author: Luke Holden <lholden@cablelan.net>
# updated for SQLite3: Jamis Buck <jamis_buck@byu.edu>
# Author: Luke Holden <lholden@cablelan.net>
# Updated for SQLite3: Jamis Buck <jamis_buck@byu.edu>
require 'active_record/connection_adapters/abstract_adapter'
@ -87,7 +86,15 @@ module ActiveRecord
#
# * <tt>:dbfile</tt> -- Path to the database file.
class SQLiteAdapter < AbstractAdapter
def native_database_types
def adapter_name #:nodoc:
'SQLite'
end
def supports_migrations? #:nodoc:
true
end
def native_database_types #:nodoc:
{
:primary_key => "INTEGER PRIMARY KEY NOT NULL",
:string => { :name => "varchar", :limit => 255 },
@ -103,32 +110,41 @@ module ActiveRecord
}
end
def supports_migrations?
true
# QUOTING ==================================================
def quote_string(s) #:nodoc:
@connection.class.quote(s)
end
def execute(sql, name = nil)
#log(sql, name, @connection) { |connection| connection.execute(sql) }
def quote_column_name(name) #:nodoc:
"'#{name}'"
end
# DATABASE STATEMENTS ======================================
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.execute(sql) }
end
def update(sql, name = nil)
def update(sql, name = nil) #:nodoc:
execute(sql, name)
@connection.changes
end
def delete(sql, name = nil)
def delete(sql, name = nil) #:nodoc:
sql += " WHERE 1=1" unless sql =~ /WHERE/i
execute(sql, name)
@connection.changes
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name = nil)
id_value || @connection.last_insert_row_id
end
def select_all(sql, name = nil)
def select_all(sql, name = nil) #:nodoc:
execute(sql, name).map do |row|
record = {}
row.each_key do |key|
@ -140,29 +156,40 @@ module ActiveRecord
end
end
def select_one(sql, name = nil)
def select_one(sql, name = nil) #:nodoc:
result = select_all(sql, name)
result.nil? ? nil : result.first
end
def begin_db_transaction() @connection.transaction end
def commit_db_transaction() @connection.commit end
def rollback_db_transaction() @connection.rollback end
def begin_db_transaction #:nodoc:
@connection.transaction
end
def commit_db_transaction #:nodoc:
@connection.commit
end
def tables(name = nil)
def rollback_db_transaction #:nodoc:
@connection.rollback
end
# SCHEMA STATEMENTS ========================================
def tables(name = nil) #:nodoc:
execute("SELECT name FROM sqlite_master WHERE type = 'table'", name).map do |row|
row[0]
end
end
def columns(table_name, name = nil)
def columns(table_name, name = nil) #:nodoc:
table_structure(table_name).map { |field|
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
}
end
def indexes(table_name, name = nil)
def indexes(table_name, name = nil) #:nodoc:
execute("PRAGMA index_list(#{table_name})", name).map do |row|
index = IndexDefinition.new(table_name, row['name'])
index.unique = row['unique'] != '0'
@ -171,24 +198,12 @@ module ActiveRecord
end
end
def primary_key(table_name)
def primary_key(table_name) #:nodoc:
column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
column ? column['name'] : nil
end
def quote_string(s)
@connection.class.quote(s)
end
def quote_column_name(name)
"'#{name}'"
end
def adapter_name()
'SQLite'
end
def remove_index(table_name, options={})
def remove_index(table_name, options={}) #:nodoc:
if Hash === options
index_name = options[:name]
else
@ -198,25 +213,25 @@ module ActiveRecord
execute "DROP INDEX #{index_name}"
end
def add_column(table_name, column_name, type, options = {})
def add_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
end
def remove_column(table_name, column_name)
def remove_column(table_name, column_name) #:nodoc:
alter_table(table_name) do |definition|
definition.columns.delete(definition[column_name])
end
end
def change_column_default(table_name, column_name, default)
def change_column_default(table_name, column_name, default) #:nodoc:
alter_table(table_name) do |definition|
definition[column_name].default = default
end
end
def change_column(table_name, column_name, type, options = {})
def change_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition[column_name].instance_eval do
self.type = type
@ -226,7 +241,7 @@ module ActiveRecord
end
end
def rename_column(table_name, column_name, new_column_name)
def rename_column(table_name, column_name, new_column_name) #:nodoc:
alter_table(table_name, :rename => {column_name => new_column_name})
end

View file

@ -1,5 +1,6 @@
module ActiveRecord
class Schema < Migration #:nodoc:
# TODO: Document me!
class Schema < Migration
private_class_method :new
def self.define(info={}, &block)

View file

@ -1,7 +1,7 @@
module ActiveRecord
# This class is used to dump the database schema for some connection to some
# output format (i.e., ActiveRecord::Schema).
class SchemaDumper
class SchemaDumper #:nodoc:
private_class_method :new
def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)