aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters
diff options
context:
space:
mode:
authorDmitry Polushkin <dmitry.polushkin@gmail.com>2011-12-31 01:10:42 +0000
committerDmitry Polushkin <dmitry.polushkin@gmail.com>2011-12-31 01:10:42 +0000
commit04bc40ff501b1bf81bec7ce3937cb06c896ffc69 (patch)
tree88a33663195900df8a7307aefa2c9aaa561c3973 /activerecord/lib/active_record/connection_adapters
parent84eece0a823e9c601ea99a8709f24605a19bcbfd (diff)
parented17983ec56dec689a0311c7f8fcbeba9874e5a4 (diff)
downloadrails-04bc40ff501b1bf81bec7ce3937cb06c896ffc69.tar.gz
rails-04bc40ff501b1bf81bec7ce3937cb06c896ffc69.tar.bz2
rails-04bc40ff501b1bf81bec7ce3937cb06c896ffc69.zip
Merge branch 'master' of git://github.com/rails/rails
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb294
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb153
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb81
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb94
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb83
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb88
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb96
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb284
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb71
15 files changed, 870 insertions, 581 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 20863e73aa..b8f99adc22 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,7 +1,7 @@
require 'thread'
require 'monitor'
require 'set'
-require 'active_support/core_ext/module/synchronization'
+require 'active_support/core_ext/module/deprecation'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -9,6 +9,13 @@ module ActiveRecord
class ConnectionTimeoutError < ConnectionNotEstablished
end
+ # Raised when a connection pool is full and another connection is requested
+ class PoolFullError < ConnectionNotEstablished
+ def initialize size, timeout
+ super("Connection pool of size #{size} and timeout #{timeout}s is full")
+ end
+ end
+
module ConnectionAdapters
# Connection pool base class for managing Active Record database
# connections.
@@ -57,10 +64,35 @@ module ActiveRecord
# * +wait_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
class ConnectionPool
- attr_accessor :automatic_reconnect
- attr_reader :spec, :connections
- attr_reader :columns, :columns_hash, :primary_keys, :tables
- attr_reader :column_defaults
+ # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
+ # A reaper instantiated with a nil frequency will never reap the
+ # connection pool.
+ #
+ # Configure the frequency by setting "reaping_frequency" in your
+ # database yaml file.
+ class Reaper
+ attr_reader :pool, :frequency
+
+ def initialize(pool, frequency)
+ @pool = pool
+ @frequency = frequency
+ end
+
+ def run
+ return unless frequency
+ Thread.new(frequency, pool) { |t, p|
+ while true
+ sleep t
+ p.reap
+ end
+ }
+ end
+ end
+
+ include MonitorMixin
+
+ attr_accessor :automatic_reconnect, :timeout
+ attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
@@ -69,88 +101,22 @@ module ActiveRecord
#
# The default ConnectionPool maximum size is 5.
def initialize(spec)
+ super()
+
@spec = spec
# The cache of reserved connections mapped to threads
@reserved_connections = {}
- # The mutex used to synchronize pool access
- @connection_mutex = Monitor.new
- @queue = @connection_mutex.new_cond
@timeout = spec.config[:wait_timeout] || 5
+ @reaper = Reaper.new self, spec.config[:reaping_frequency]
+ @reaper.run
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@connections = []
- @checked_out = []
@automatic_reconnect = true
- @tables = {}
- @visitor = nil
-
- @columns = Hash.new do |h, table_name|
- h[table_name] = with_connection do |conn|
-
- # Fetch a list of columns
- conn.columns(table_name, "#{table_name} Columns").tap do |columns|
-
- # set primary key information
- columns.each do |column|
- column.primary = column.name == primary_keys[table_name]
- end
- end
- end
- end
-
- @columns_hash = Hash.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
- [col.name, col]
- }]
- end
-
- @column_defaults = Hash.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
- [col.name, col.default]
- }]
- end
-
- @primary_keys = Hash.new do |h, table_name|
- h[table_name] = with_connection do |conn|
- table_exists?(table_name) ? conn.primary_key(table_name) : 'id'
- end
- end
- end
-
- # A cached lookup for table existence.
- def table_exists?(name)
- return true if @tables.key? name
-
- with_connection do |conn|
- conn.tables.each { |table| @tables[table] = true }
- @tables[name] = true if !@tables.key?(name) && conn.table_exists?(name)
- end
-
- @tables.key? name
- end
-
- # Clears out internal caches:
- #
- # * columns
- # * columns_hash
- # * tables
- def clear_cache!
- @columns.clear
- @columns_hash.clear
- @column_defaults.clear
- @tables.clear
- end
-
- # Clear out internal caches for table with +table_name+.
- def clear_table_cache!(table_name)
- @columns.delete table_name
- @columns_hash.delete table_name
- @column_defaults.delete table_name
- @primary_keys.delete table_name
end
# Retrieve the connection associated with the current thread, or call
@@ -165,7 +131,7 @@ module ActiveRecord
# Check to see if there is an active connection in this connection
# pool.
def active_connection?
- @reserved_connections.key? current_connection_id
+ active_connections.any?
end
# Signal that the thread is finished with the current connection.
@@ -181,7 +147,7 @@ module ActiveRecord
# connection when finished.
def with_connection
connection_id = current_connection_id
- fresh_connection = true unless @reserved_connections[connection_id]
+ fresh_connection = true unless active_connection?
yield connection
ensure
release_connection(connection_id) if fresh_connection
@@ -189,94 +155,80 @@ module ActiveRecord
# Returns true if a connection has already been opened.
def connected?
- !@connections.empty?
+ synchronize { @connections.any? }
end
# Disconnects all connections in the pool, and clears the pool.
def disconnect!
- @reserved_connections.each do |name,conn|
- checkin conn
- end
- @reserved_connections = {}
- @connections.each do |conn|
- conn.disconnect!
+ synchronize do
+ @reserved_connections = {}
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect!
+ end
+ @connections = []
end
- @connections = []
end
# Clears the cache which maps classes.
def clear_reloadable_connections!
- @reserved_connections.each do |name, conn|
- checkin conn
- end
- @reserved_connections = {}
- @connections.each do |conn|
- conn.disconnect! if conn.requires_reloading?
- end
- @connections.delete_if do |conn|
- conn.requires_reloading?
+ synchronize do
+ @reserved_connections = {}
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect! if conn.requires_reloading?
+ end
+ @connections.delete_if do |conn|
+ conn.requires_reloading?
+ end
end
end
# Verify active connections and remove and disconnect connections
# associated with stale threads.
def verify_active_connections! #:nodoc:
- clear_stale_cached_connections!
- @connections.each do |connection|
- connection.verify!
+ synchronize do
+ @connections.each do |connection|
+ connection.verify!
+ end
end
end
- # Return any checked-out connections back to the pool by threads that
- # are no longer alive.
- def clear_stale_cached_connections!
- keys = @reserved_connections.keys - Thread.list.find_all { |t|
- t.alive?
- }.map { |thread| thread.object_id }
- keys.each do |key|
- checkin @reserved_connections[key]
- @reserved_connections.delete(key)
- end
+ def clear_stale_cached_connections! # :nodoc:
end
+ deprecate :clear_stale_cached_connections!
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
- # This is done by either returning an existing connection, or by creating
- # a new connection. If the maximum number of connections for this pool has
- # already been reached, but the pool is empty (i.e. they're all being used),
- # then this method will wait until a thread has checked in a connection.
- # The wait time is bounded however: if no connection can be checked out
- # within the timeout specified for this pool, then a ConnectionTimeoutError
- # exception will be raised.
+ # This is done by either returning and leasing existing connection, or by
+ # creating a new connection and leasing it.
+ #
+ # If all connections are leased and the pool is at capacity (meaning the
+ # number of currently leased connections is greater than or equal to the
+ # size limit set), an ActiveRecord::PoolFullError exception will be raised.
#
# Returns: an AbstractAdapter object.
#
# Raises:
- # - ConnectionTimeoutError: no connection can be obtained from the pool
- # within the timeout period.
+ # - PoolFullError: no connection can be obtained from the pool.
def checkout
# Checkout an available connection
- @connection_mutex.synchronize do
- loop do
- conn = if @checked_out.size < @connections.size
- checkout_existing_connection
- elsif @connections.size < @size
- checkout_new_connection
- end
- return conn if conn
-
- @queue.wait(@timeout)
-
- if(@checked_out.size < @connections.size)
- next
- else
- clear_stale_cached_connections!
- if @size == @checked_out.size
- raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
- end
- end
+ synchronize do
+ # Try to find a connection that hasn't been leased, and lease it
+ conn = connections.find { |c| c.lease }
+
+ # If all connections were leased, and we have room to expand,
+ # create a new connection and lease it.
+ if !conn && connections.size < size
+ conn = checkout_new_connection
+ conn.lease
+ end
+ if conn
+ checkout_and_verify conn
+ else
+ raise PoolFullError.new(size, timeout)
end
end
end
@@ -287,56 +239,69 @@ module ActiveRecord
# +conn+: an AbstractAdapter object, which was obtained by earlier by
# calling +checkout+ on this pool.
def checkin(conn)
- @connection_mutex.synchronize do
+ synchronize do
conn.run_callbacks :checkin do
- @checked_out.delete conn
- @queue.signal
+ conn.expire
end
end
end
- synchronize :clear_reloadable_connections!, :verify_active_connections!,
- :connected?, :disconnect!, :with => :@connection_mutex
+ # Remove a connection from the connection pool. The connection will
+ # remain open and active but will no longer be managed by this pool.
+ def remove(conn)
+ synchronize do
+ @connections.delete conn
- private
+ # FIXME: we might want to store the key on the connection so that removing
+ # from the reserved hash will be a little easier.
+ thread_id = @reserved_connections.keys.find { |k|
+ @reserved_connections[k] == conn
+ }
+ @reserved_connections.delete thread_id if thread_id
+ end
+ end
- def new_connection
- connection = ActiveRecord::Base.send(spec.adapter_method, spec.config)
+ # Removes dead connections from the pool. A dead connection can occur
+ # if a programmer forgets to close a connection at the end of a thread
+ # or a thread dies unexpectedly.
+ def reap
+ synchronize do
+ stale = Time.now - @timeout
+ connections.dup.each do |conn|
+ remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
+ end
+ end
+ end
- # TODO: This is a bit icky, and in the long term we may want to change the method
- # signature for connections. Also, if we switch to have one visitor per
- # connection (and therefore per thread), we can get rid of the thread-local
- # variable in Arel::Visitors::ToSql.
- @visitor ||= connection.class.visitor_for(self)
- connection.visitor = @visitor
+ private
- connection
+ def new_connection
+ ActiveRecord::Base.send(spec.adapter_method, spec.config)
end
def current_connection_id #:nodoc:
- Thread.current.object_id
+ ActiveRecord::Base.connection_id ||= Thread.current.object_id
end
def checkout_new_connection
raise ConnectionNotEstablished unless @automatic_reconnect
c = new_connection
+ c.pool = self
@connections << c
- checkout_and_verify(c)
- end
-
- def checkout_existing_connection
- c = (@connections - @checked_out).first
- checkout_and_verify(c)
+ c
end
def checkout_and_verify(c)
c.run_callbacks :checkout do
c.verify!
- @checked_out << c
end
c
end
+
+ def active_connections
+ @connections.find_all { |c| c.in_use? }
+ end
end
# ConnectionHandler is a collection of ConnectionPool objects. It is used
@@ -367,10 +332,12 @@ module ActiveRecord
def initialize(pools = {})
@connection_pools = pools
+ @class_to_pool = {}
end
def establish_connection(name, spec)
- @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
+ @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec)
+ @class_to_pool[name] = @connection_pools[spec]
end
# Returns true if there are any active connections among the connection
@@ -421,19 +388,20 @@ module ActiveRecord
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def remove_connection(klass)
- pool = @connection_pools.delete(klass.name)
+ pool = @class_to_pool.delete(klass.name)
return nil unless pool
+ @connection_pools.delete pool.spec
pool.automatic_reconnect = false
pool.disconnect!
pool.spec.config
end
def retrieve_connection_pool(klass)
- pool = @connection_pools[klass.name]
+ pool = @class_to_pool[klass.name]
return pool if pool
- return nil if ActiveRecord::Base == klass
- retrieve_connection_pool klass.superclass
+ return nil if ActiveRecord::Model == klass
+ retrieve_connection_pool klass.active_record_super
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
deleted file mode 100644
index c08c0263b9..0000000000
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-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
-
- ##
- # :singleton-method:
- # The connection handler
- class_attribute :connection_handler, :instance_writer => false
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
-
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- self.class.connection
- end
-
- # Establishes the connection to the database. Accepts a hash as input where
- # the <tt>:adapter</tt> 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",
- # :database => "path/to/dbfile"
- # )
- #
- # Also accepts keys as strings (for parsing from YAML for example):
- #
- # ActiveRecord::Base.establish_connection(
- # "adapter" => "sqlite",
- # "database" => "path/to/dbfile"
- # )
- #
- # Or a URL:
- #
- # ActiveRecord::Base.establish_connection(
- # "postgres://myuser:mypass@localhost/somedatabase"
- # )
- #
- # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
- # may be returned on an error.
- def self.establish_connection(spec = ENV["DATABASE_URL"])
- case spec
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- establish_connection(Rails.env)
- when ConnectionSpecification
- self.connection_handler.establish_connection(name, spec)
- when Symbol, String
- if configuration = configurations[spec.to_s]
- establish_connection(configuration)
- elsif spec.is_a?(String) && hash = connection_url_to_hash(spec)
- establish_connection(hash)
- 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
-
- begin
- require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
- rescue LoadError => e
- raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})"
- end
-
- adapter_method = "#{spec[:adapter]}_connection"
- unless respond_to?(adapter_method)
- raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
- end
-
- remove_connection
- establish_connection(ConnectionSpecification.new(spec, adapter_method))
- end
- end
-
- def self.connection_url_to_hash(url) # :nodoc:
- config = URI.parse url
- adapter = config.scheme
- adapter = "postgresql" if adapter == "postgres"
- spec = { :adapter => adapter,
- :username => config.user,
- :password => config.password,
- :port => config.port,
- :database => config.path.sub(%r{^/},""),
- :host => config.host }
- spec.reject!{ |_,value| !value }
- if config.query
- options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
- spec.merge!(options)
- end
- spec
- end
-
- class << self
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work unrelated
- # to any of the specific Active Records.
- def connection
- retrieve_connection
- end
-
- # Returns the configuration of the associated connection as a hash:
- #
- # ActiveRecord::Base.connection_config
- # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
- #
- # Please use only for reading.
- def connection_config
- connection_pool.spec.config
- end
-
- def connection_pool
- connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
- end
-
- def retrieve_connection
- connection_handler.retrieve_connection(self)
- end
-
- # Returns true if Active Record is connected.
- def connected?
- connection_handler.connected?(self)
- end
-
- def remove_connection(klass = self)
- connection_handler.remove_connection(klass)
- end
-
- def clear_active_connections!
- connection_handler.clear_active_connections!
- end
-
- delegate :clear_reloadable_connections!,
- :clear_all_connections!,:verify_active_connections!, :to => :connection_handler
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index dc4a53034b..eb8cff9610 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -130,7 +130,7 @@ module ActiveRecord
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
- # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
+ # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
#
# It is safe to call this method if a database transaction is already open,
@@ -341,7 +341,7 @@ module ActiveRecord
# Send a rollback message to all records after they have been rolled back. If rollback
# is false, only rollback records since the last save point.
- def rollback_transaction_records(rollback) #:nodoc
+ def rollback_transaction_records(rollback)
if rollback
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
@@ -361,7 +361,7 @@ module ActiveRecord
end
# Send a commit message to all records after they have been committed.
- def commit_transaction_records #:nodoc
+ def commit_transaction_records
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
unless records.blank?
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 82f564e41d..132ca10f79 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,7 +6,7 @@ require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
@@ -46,13 +46,13 @@ module ActiveRecord
# +change_table+ is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
- # def self.up
+ # def up
# create_table :foo do |t|
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
# end
# end
#
- # def self.down
+ # def down
# ...
# end
# end
@@ -66,6 +66,7 @@ module ActiveRecord
def initialize(base)
@columns = []
+ @columns_hash = {}
@base = base
end
@@ -86,7 +87,7 @@ module ActiveRecord
# Returns a ColumnDefinition for the column with name +name+.
def [](name)
- @columns.find {|column| column.name.to_s == name.to_s}
+ @columns_hash[name.to_s]
end
# Instantiates a new column for the table.
@@ -224,35 +225,38 @@ module ActiveRecord
# t.references :taggable, :polymorphic => { :default => 'Photo' }
# end
def column(name, type, options = {})
- column = self[name] || ColumnDefinition.new(@base, name, type)
- if options[:limit]
- column.limit = options[:limit]
- elsif native[type.to_sym].is_a?(Hash)
- column.limit = native[type.to_sym][:limit]
+ name = name.to_s
+ type = type.to_sym
+
+ column = self[name] || new_column_definition(@base, name, type)
+
+ limit = options.fetch(:limit) do
+ native[type][:limit] if native[type].is_a?(Hash)
end
+
+ column.limit = limit
column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
- @columns << column unless @columns.include? column
+ column.scale = options[:scale]
+ column.default = options[:default]
+ column.null = options[:null]
self
end
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- #
- column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) }
- end # end
+ def #{column_type}(*args) # def string(*args)
+ options = args.extract_options! # options = args.extract_options!
+ column_names = args # column_names = args
+ type = :'#{column_type}' # type = :string
+ column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) }
+ end # end
EOV
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
def timestamps(*args)
- options = args.extract_options!
+ options = { :null => false }.merge(args.extract_options!)
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
@@ -275,9 +279,16 @@ module ActiveRecord
end
private
- def native
- @base.native_database_types
- end
+ def new_column_definition(base, name, type)
+ definition = ColumnDefinition.new base, name, type
+ @columns << definition
+ @columns_hash[name] = definition
+ definition
+ end
+
+ def native
+ @base.native_database_types
+ end
end
# Represents an SQL table in an abstract way for updating a table.
@@ -453,13 +464,13 @@ module ActiveRecord
def #{column_type}(*args) # def string(*args)
options = args.extract_options! # options = args.extract_options!
column_names = args # column_names = args
- #
+ type = :'#{column_type}' # type = :string
column_names.each do |name| # column_names.each do |name|
- column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
+ column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type)
if options[:limit] # if options[:limit]
column.limit = options[:limit] # column.limit = options[:limit]
- elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash)
- column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit]
+ elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash)
+ column.limit = native[type][:limit] # column.limit = native[type][:limit]
end # end
column.precision = options[:precision] # column.precision = options[:precision]
column.scale = options[:scale] # column.scale = options[:scale]
@@ -479,4 +490,3 @@ module ActiveRecord
end
end
-
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 8e3ba1297e..20d3b4a1ef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/array/wrap'
+require 'active_support/deprecation/reporting'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -15,8 +16,6 @@ module ActiveRecord
table_name[0...table_alias_length].gsub(/\./, '_')
end
- # def tables(name = nil) end
-
# Checks to see if the table +table_name+ exists on the database.
#
# === Example
@@ -113,7 +112,7 @@ module ActiveRecord
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
#
# Also note that this just sets the primary key in the table. You additionally
- # need to configure the primary key in the model via the +set_primary_key+ macro.
+ # need to configure the primary key in the model via +self.primary_key=+.
# Models do NOT auto-detect the primary key from their table definition.
#
# [<tt>:options</tt>]
@@ -302,15 +301,8 @@ module ActiveRecord
# Adds a new index to the table. +column_name+ can be a single Symbol, or
# an Array of Symbols.
#
- # The index will be named after the table and the first column name,
- # unless you pass <tt>:name</tt> as an option.
- #
- # When creating an index on multiple columns, the first column is used as a name
- # for the index. For example, when you specify an index on two columns
- # [<tt>:first</tt>, <tt>:last</tt>], the DBMS creates an index for both columns as well as an
- # index for the first column <tt>:first</tt>. Using just the first name for this index
- # makes sense, because you will never have to create a singular index with this
- # name.
+ # The index will be named after the table and the column name(s), unless
+ # you pass <tt>:name</tt> as an option.
#
# ===== Examples
#
@@ -339,6 +331,14 @@ module ActiveRecord
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
#
# Note: SQLite doesn't support index length
+ #
+ # ====== Creating an index with a sort order (desc or asc, asc is the default)
+ # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc})
+ # generates
+ # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
+ #
+ # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it)
+ #
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
@@ -405,7 +405,7 @@ module ActiveRecord
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
- migrated = select_values("SELECT version FROM #{sm_table}")
+ migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version")
migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n")
end
@@ -426,6 +426,7 @@ module ActiveRecord
si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix
if table_exists?(si_table)
+ ActiveRecord::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table"
old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i
assume_migrated_upto_version(old_version)
@@ -507,8 +508,8 @@ module ActiveRecord
# ===== Examples
# add_timestamps(:suppliers)
def add_timestamps(table_name)
- add_column table_name, :created_at, :datetime
- add_column table_name, :updated_at, :datetime
+ add_column table_name, :created_at, :datetime, :null => false
+ add_column table_name, :updated_at, :datetime, :null => false
end
# Removes the timestamp columns (created_at and updated_at) from the table definition.
@@ -520,9 +521,29 @@ module ActiveRecord
end
protected
+ def add_index_sort_order(option_strings, column_names, options = {})
+ if options.is_a?(Hash) && order = options[:order]
+ case order
+ when Hash
+ column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)}
+ when String
+ column_names.each {|name| option_strings[name] += " #{order.upcase}"}
+ end
+ end
+
+ return option_strings
+ end
+
# Overridden by the mysql adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
- column_names.map {|name| quote_column_name(name) }
+ option_strings = Hash[column_names.map {|name| [name, '']}]
+
+ # add index sort order if supported
+ if supports_index_sort_order?
+ option_strings = add_index_sort_order(option_strings, column_names, options)
+ end
+
+ column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
def options_include_default?(options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 443e61b527..edea414db7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -3,12 +3,15 @@ require 'bigdecimal'
require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_support/deprecation'
+require 'active_record/connection_adapters/schema_cache'
+require 'monitor'
module ActiveRecord
module ConnectionAdapters # :nodoc:
extend ActiveSupport::Autoload
autoload :Column
+ autoload :ConnectionSpecification
autoload_under 'abstract' do
autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
@@ -24,7 +27,6 @@ module ActiveRecord
autoload :ConnectionPool
autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool'
autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool'
- autoload :ConnectionSpecification
autoload :QueryCache
end
@@ -47,39 +49,44 @@ module ActiveRecord
include DatabaseLimits
include QueryCache
include ActiveSupport::Callbacks
+ include MonitorMixin
define_callbacks :checkout, :checkin
- attr_accessor :visitor
-
- def initialize(connection, logger = nil) #:nodoc:
- @active = nil
- @connection, @logger = connection, logger
+ attr_accessor :visitor, :pool
+ attr_reader :schema_cache, :last_use, :in_use
+ alias :in_use? :in_use
+
+ def initialize(connection, logger = nil, pool = nil) #:nodoc:
+ super()
+
+ @active = nil
+ @connection = connection
+ @in_use = false
+ @instrumenter = ActiveSupport::Notifications.instrumenter
+ @last_use = false
+ @logger = logger
+ @open_transactions = 0
+ @pool = pool
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
@query_cache_enabled = false
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @open_transactions = 0
- @instrumenter = ActiveSupport::Notifications.instrumenter
- @visitor = nil
- end
-
- # Returns a visitor instance for this adaptor, which conforms to the Arel::ToSql interface
- def self.visitor_for(pool) # :nodoc:
- adapter = pool.spec.config[:adapter]
-
- if Arel::Visitors::VISITORS[adapter]
- ActiveSupport::Deprecation.warn(
- "Arel::Visitors::VISITORS is deprecated and will be removed. Database adapters " \
- "should define a visitor_for method which returns the appropriate visitor for " \
- "the database. For example, MysqlAdapter.visitor_for(pool) returns " \
- "Arel::Visitors::MySQL.new(pool)."
- )
-
- Arel::Visitors::VISITORS[adapter].new(pool)
- else
- Arel::Visitors::ToSql.new(pool)
+ @schema_cache = SchemaCache.new self
+ @visitor = nil
+ end
+
+ def lease
+ synchronize do
+ unless in_use
+ @in_use = true
+ @last_use = Time.now
+ end
end
end
+ def expire
+ @in_use = false
+ end
+
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
@@ -130,6 +137,17 @@ module ActiveRecord
false
end
+ # Does this adapter support index sort order?
+ def supports_index_sort_order?
+ false
+ end
+
+ # Does this adapter support explain? As of this writing sqlite3,
+ # mysql2, and postgresql are the only ones that do.
+ def supports_explain?
+ false
+ end
+
# QUOTING ==================================================
# Override to return the quoted table name. Defaults to column quoting.
@@ -238,10 +256,19 @@ module ActiveRecord
node
end
+ def case_insensitive_comparison(table, attribute, column, value)
+ table[attribute].lower.eq(table.lower(value))
+ end
+
def current_savepoint_name
"active_record_#{open_transactions}"
end
+ # Check the connection back in to the connection pool
+ def close
+ pool.checkin self
+ end
+
protected
def log(sql, name = "SQL", binds = [])
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 4b7c74e0b8..560773ca86 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -4,6 +4,13 @@ module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
class Column < ConnectionAdapters::Column # :nodoc:
+ attr_reader :collation
+
+ def initialize(name, default, sql_type = nil, null = true, collation = nil)
+ super(name, default, sql_type, null)
+ @collation = collation
+ end
+
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
@@ -28,6 +35,10 @@ module ActiveRecord
raise NotImplementedError
end
+ def case_sensitive?
+ collation && !collation.match(/_ci$/)
+ end
+
private
def simplified_type(field_type)
@@ -116,10 +127,7 @@ module ActiveRecord
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
- end
-
- def self.visitor_for(pool) # :nodoc:
- Arel::Visitors::MySQL.new(pool)
+ @visitor = Arel::Visitors::MySQL.new self
end
def adapter_name #:nodoc:
@@ -144,6 +152,12 @@ module ActiveRecord
true
end
+ # Technically MySQL allows to create indexes with the sort order syntax
+ # but at the moment (5.5) it doesn't yet implement them
+ def supports_index_sort_order?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -157,8 +171,8 @@ module ActiveRecord
end
# Overridden by the adapters to instantiate their specific Column type.
- def new_column(field, default, type, null) # :nodoc:
- Column.new(field, default, type, null)
+ def new_column(field, default, type, null, collation) # :nodoc:
+ Column.new(field, default, type, null, collation)
end
# Must return the Mysql error number from the exception, if the exception has an
@@ -298,11 +312,11 @@ module ActiveRecord
sql = "SHOW TABLES"
end
- select_all(sql).map do |table|
+ select_all(sql).map { |table|
table.delete('Table_type')
sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
exec_without_stmt(sql).first['Create Table'] + ";\n\n"
- end.join("")
+ }.join
end
# Drops the database specified on the +name+ attribute
@@ -349,8 +363,10 @@ module ActiveRecord
show_variable 'collation_database'
end
- def tables(name = nil, database = nil) #:nodoc:
- sql = ["SHOW TABLES", database].compact.join(' IN ')
+ def tables(name = nil, database = nil, like = nil) #:nodoc:
+ sql = "SHOW TABLES "
+ sql << "IN #{database} " if database
+ sql << "LIKE #{quote(like)}" if like
execute_and_free(sql, 'SCHEMA') do |result|
result.collect { |field| field.first }
@@ -358,7 +374,8 @@ module ActiveRecord
end
def table_exists?(name)
- return true if super
+ return false unless name
+ return true if tables(nil, nil, name).any?
name = name.to_s
schema, table = name.split('.', 2)
@@ -368,7 +385,7 @@ module ActiveRecord
schema = nil
end
- tables(nil, schema).include? table
+ tables(nil, schema, table).any?
end
# Returns an array of indexes for the given table.
@@ -393,10 +410,10 @@ module ActiveRecord
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name, name = nil)#:nodoc:
- sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
+ sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
- new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
+ new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation])
end
end
end
@@ -485,9 +502,14 @@ module ActiveRecord
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
- execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result|
- keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] }
- keys.length == 1 ? [keys.first, nil] : nil
+ execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
+ create_table = each_hash(result).first[:"Create Table"]
+ if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/
+ keys = $1.split(",").map { |key| key.gsub(/`/, "") }
+ keys.length == 1 ? [keys.first, nil] : nil
+ else
+ nil
+ end
end
end
@@ -501,23 +523,43 @@ module ActiveRecord
Arel::Nodes::Bin.new(node)
end
+ def case_insensitive_comparison(table, attribute, column, value)
+ if column.case_sensitive?
+ super
+ else
+ table[attribute].eq(value)
+ end
+ end
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end
protected
+ def add_index_length(option_strings, column_names, options = {})
+ if options.is_a?(Hash) && length = options[:length]
+ case length
+ when Hash
+ column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name)}
+ when Fixnum
+ column_names.each {|name| option_strings[name] += "(#{length})"}
+ end
+ end
+
+ return option_strings
+ end
+
def quoted_columns_for_index(column_names, options = {})
- length = options[:length] if options.is_a?(Hash)
+ option_strings = Hash[column_names.map {|name| [name, '']}]
- case length
- when Hash
- column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
- when Fixnum
- column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
- else
- column_names.map {|name| quote_column_name(name) }
- end
+ # add index length
+ option_strings = add_index_length(option_strings, column_names, options)
+
+ # add index sort order
+ option_strings = add_index_sort_order(option_strings, column_names, options)
+
+ column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
def translate_exception(exception, message)
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index a7856539b7..6aa4a2c5b3 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -80,9 +80,10 @@ module ActiveRecord
when :decimal then klass.value_to_decimal(value)
when :datetime, :timestamp then klass.string_to_time(value)
when :time then klass.string_to_dummy_time(value)
- when :date then klass.string_to_date(value)
+ when :date then klass.value_to_date(value)
when :binary then klass.binary_to_string(value)
when :boolean then klass.value_to_boolean(value)
+ when :hstore then klass.cast_hstore(value)
else value
end
end
@@ -97,9 +98,10 @@ module ActiveRecord
when :decimal then "#{klass}.value_to_decimal(#{var_name})"
when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.string_to_date(#{var_name})"
+ when :date then "#{klass}.value_to_date(#{var_name})"
when :binary then "#{klass}.binary_to_string(#{var_name})"
when :boolean then "#{klass}.value_to_boolean(#{var_name})"
+ when :hstore then "#{klass}.cast_hstore(#{var_name})"
else var_name
end
end
@@ -132,11 +134,15 @@ module ActiveRecord
value
end
- def string_to_date(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_date(string) || fallback_string_to_date(string)
+ def value_to_date(value)
+ if value.is_a?(String)
+ return nil if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
end
def string_to_time(string)
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
new file mode 100644
index 0000000000..8491d42b86
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -0,0 +1,83 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class ConnectionSpecification #:nodoc:
+ attr_reader :config, :adapter_method
+
+ def initialize(config, adapter_method)
+ @config, @adapter_method = config, adapter_method
+ end
+
+ def initialize_dup(original)
+ @config = original.config.dup
+ end
+
+ ##
+ # Builds a ConnectionSpecification from user input
+ class Resolver # :nodoc:
+ attr_reader :config, :klass, :configurations
+
+ def initialize(config, configurations)
+ @config = config
+ @configurations = configurations
+ end
+
+ def spec
+ case config
+ when nil
+ raise AdapterNotSpecified unless defined?(Rails.env)
+ resolve_string_connection Rails.env
+ when Symbol, String
+ resolve_string_connection config.to_s
+ when Hash
+ resolve_hash_connection config
+ end
+ end
+
+ private
+ def resolve_string_connection(spec) # :nodoc:
+ hash = configurations.fetch(spec) do |k|
+ connection_url_to_hash(k)
+ end
+
+ raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
+
+ resolve_hash_connection hash
+ end
+
+ def resolve_hash_connection(spec) # :nodoc:
+ spec = spec.symbolize_keys
+
+ raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+
+ begin
+ require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
+ rescue LoadError => e
+ raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
+ end
+
+ adapter_method = "#{spec[:adapter]}_connection"
+
+ ConnectionSpecification.new(spec, adapter_method)
+ end
+
+ def connection_url_to_hash(url) # :nodoc:
+ config = URI.parse url
+ adapter = config.scheme
+ adapter = "postgresql" if adapter == "postgres"
+ spec = { :adapter => adapter,
+ :username => config.user,
+ :password => config.password,
+ :port => config.port,
+ :database => config.path.sub(%r{^/},""),
+ :host => config.host }
+ spec.reject!{ |_,value| !value }
+ if config.query
+ options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+ spec.merge!(options)
+ end
+ spec
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 8b574518e5..6086c32dbe 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,12 +1,12 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.6'
+gem 'mysql2', '~> 0.3.10'
require 'mysql2'
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects.
- def self.mysql2_connection(config)
+ def mysql2_connection(config)
config[:username] = 'root' if config[:username].nil?
if Mysql2::Client.const_defined? :FOUND_ROWS
@@ -35,6 +35,10 @@ module ActiveRecord
configure_connection
end
+ def supports_explain?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
@@ -47,8 +51,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null) # :nodoc:
- Column.new(field, default, type, null)
+ def new_column(field, default, type, null, collation) # :nodoc:
+ Column.new(field, default, type, null, collation)
end
def error_number(exception)
@@ -93,6 +97,80 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel)}"
+ start = Time.now
+ result = exec_query(sql, 'EXPLAIN', binds)
+ elapsed = Time.now - start
+
+ ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = 'NULL' if item.nil?
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ '| ' + cells.join(' | ') + ' |'
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
+
# FIXME: re-enable the following once a "better" query_cache solution is in core
#
# The overrides below perform much better than the originals in AbstractAdapter
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index a1824fe396..e432c5af32 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -18,9 +18,9 @@ class Mysql
end
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects.
- def self.mysql_connection(config) # :nodoc:
+ def mysql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@@ -150,8 +150,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null) # :nodoc:
- Column.new(field, default, type, null)
+ def new_column(field, default, type, null, collation) # :nodoc:
+ Column.new(field, default, type, null, collation)
end
def error_number(exception) # :nodoc:
@@ -224,52 +224,48 @@ module ActiveRecord
@statements.clear
end
- if "<3".respond_to?(:encode)
- # Taken from here:
- # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
- # Author: TOMITA Masahiro <tommy@tmtm.org>
- ENCODINGS = {
- "armscii8" => nil,
- "ascii" => Encoding::US_ASCII,
- "big5" => Encoding::Big5,
- "binary" => Encoding::ASCII_8BIT,
- "cp1250" => Encoding::Windows_1250,
- "cp1251" => Encoding::Windows_1251,
- "cp1256" => Encoding::Windows_1256,
- "cp1257" => Encoding::Windows_1257,
- "cp850" => Encoding::CP850,
- "cp852" => Encoding::CP852,
- "cp866" => Encoding::IBM866,
- "cp932" => Encoding::Windows_31J,
- "dec8" => nil,
- "eucjpms" => Encoding::EucJP_ms,
- "euckr" => Encoding::EUC_KR,
- "gb2312" => Encoding::EUC_CN,
- "gbk" => Encoding::GBK,
- "geostd8" => nil,
- "greek" => Encoding::ISO_8859_7,
- "hebrew" => Encoding::ISO_8859_8,
- "hp8" => nil,
- "keybcs2" => nil,
- "koi8r" => Encoding::KOI8_R,
- "koi8u" => Encoding::KOI8_U,
- "latin1" => Encoding::ISO_8859_1,
- "latin2" => Encoding::ISO_8859_2,
- "latin5" => Encoding::ISO_8859_9,
- "latin7" => Encoding::ISO_8859_13,
- "macce" => Encoding::MacCentEuro,
- "macroman" => Encoding::MacRoman,
- "sjis" => Encoding::SHIFT_JIS,
- "swe7" => nil,
- "tis620" => Encoding::TIS_620,
- "ucs2" => Encoding::UTF_16BE,
- "ujis" => Encoding::EucJP_ms,
- "utf8" => Encoding::UTF_8,
- "utf8mb4" => Encoding::UTF_8,
- }
- else
- ENCODINGS = Hash.new { |h,k| h[k] = k }
- end
+ # Taken from here:
+ # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
+ # Author: TOMITA Masahiro <tommy@tmtm.org>
+ ENCODINGS = {
+ "armscii8" => nil,
+ "ascii" => Encoding::US_ASCII,
+ "big5" => Encoding::Big5,
+ "binary" => Encoding::ASCII_8BIT,
+ "cp1250" => Encoding::Windows_1250,
+ "cp1251" => Encoding::Windows_1251,
+ "cp1256" => Encoding::Windows_1256,
+ "cp1257" => Encoding::Windows_1257,
+ "cp850" => Encoding::CP850,
+ "cp852" => Encoding::CP852,
+ "cp866" => Encoding::IBM866,
+ "cp932" => Encoding::Windows_31J,
+ "dec8" => nil,
+ "eucjpms" => Encoding::EucJP_ms,
+ "euckr" => Encoding::EUC_KR,
+ "gb2312" => Encoding::EUC_CN,
+ "gbk" => Encoding::GBK,
+ "geostd8" => nil,
+ "greek" => Encoding::ISO_8859_7,
+ "hebrew" => Encoding::ISO_8859_8,
+ "hp8" => nil,
+ "keybcs2" => nil,
+ "koi8r" => Encoding::KOI8_R,
+ "koi8u" => Encoding::KOI8_U,
+ "latin1" => Encoding::ISO_8859_1,
+ "latin2" => Encoding::ISO_8859_2,
+ "latin5" => Encoding::ISO_8859_9,
+ "latin7" => Encoding::ISO_8859_13,
+ "macce" => Encoding::MacCentEuro,
+ "macroman" => Encoding::MacRoman,
+ "sjis" => Encoding::SHIFT_JIS,
+ "swe7" => nil,
+ "tis620" => Encoding::TIS_620,
+ "ucs2" => Encoding::UTF_16BE,
+ "ujis" => Encoding::EucJP_ms,
+ "utf8" => Encoding::UTF_8,
+ "utf8mb4" => Encoding::UTF_8,
+ }
# Get the client encoding for this database
def client_encoding
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 5402918b1d..d2361b5378 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -7,9 +7,9 @@ gem 'pg', '~> 0.11'
require 'pg'
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects
- def self.postgresql_connection(config) # :nodoc:
+ def postgresql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
@@ -49,6 +49,42 @@ module ActiveRecord
super
end
end
+
+ def cast_hstore(object)
+ if Hash === object
+ object.map { |k,v|
+ "#{escape_hstore(k)}=>#{escape_hstore(v)}"
+ }.join ', '
+ else
+ kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o|
+ unescape_hstore(o[1...-1])
+ }
+ Hash[kvs.each_slice(2).to_a]
+ end
+ end
+
+ private
+ HSTORE_ESCAPE = {
+ ' ' => '\\ ',
+ '\\' => '\\\\',
+ '"' => '\\"',
+ '=' => '\\=',
+ }
+ HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys)
+ HSTORE_UNESCAPE = HSTORE_ESCAPE.invert
+ HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys)
+
+ def unescape_hstore(value)
+ value.gsub(HSTORE_UNESCAPE_RE) do |match|
+ HSTORE_UNESCAPE[match]
+ end
+ end
+
+ def escape_hstore(value)
+ value.gsub(HSTORE_ESCAPE_RE) do |match|
+ HSTORE_ESCAPE[match]
+ end
+ end
end
# :startdoc:
@@ -79,53 +115,55 @@ module ActiveRecord
# Maps PostgreSQL-specific data types to logical Rails types.
def simplified_type(field_type)
case field_type
- # Numeric and monetary types
- when /^(?:real|double precision)$/
- :float
- # Monetary types
- when 'money'
- :decimal
- # Character types
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
- :string
- # Binary data types
- when 'bytea'
- :binary
- # Date/time types
- when /^timestamp with(?:out)? time zone$/
- :datetime
- when 'interval'
- :string
- # Geometric types
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
- :string
- # Network address types
- when /^(?:cidr|inet|macaddr)$/
- :string
- # Bit strings
- when /^bit(?: varying)?(?:\(\d+\))?$/
- :string
- # XML type
- when 'xml'
- :xml
- # tsvector type
- when 'tsvector'
- :tsvector
- # Arrays
- when /^\D+\[\]$/
- :string
- # Object identifier types
- when 'oid'
- :integer
- # UUID type
- when 'uuid'
- :string
- # Small and big integer types
- when /^(?:small|big)int$/
- :integer
- # Pass through all types that are not specific to PostgreSQL.
- else
- super
+ # Numeric and monetary types
+ when /^(?:real|double precision)$/
+ :float
+ # Monetary types
+ when 'money'
+ :decimal
+ when 'hstore'
+ :hstore
+ # Character types
+ when /^(?:character varying|bpchar)(?:\(\d+\))?$/
+ :string
+ # Binary data types
+ when 'bytea'
+ :binary
+ # Date/time types
+ when /^timestamp with(?:out)? time zone$/
+ :datetime
+ when 'interval'
+ :string
+ # Geometric types
+ when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
+ :string
+ # Network address types
+ when /^(?:cidr|inet|macaddr)$/
+ :string
+ # Bit strings
+ when /^bit(?: varying)?(?:\(\d+\))?$/
+ :string
+ # XML type
+ when 'xml'
+ :xml
+ # tsvector type
+ when 'tsvector'
+ :tsvector
+ # Arrays
+ when /^\D+\[\]$/
+ :string
+ # Object identifier types
+ when 'oid'
+ :integer
+ # UUID type
+ when 'uuid'
+ :string
+ # Small and big integer types
+ when /^(?:small|big)int$/
+ :integer
+ # Pass through all types that are not specific to PostgreSQL.
+ else
+ super
end
end
@@ -215,6 +253,10 @@ module ActiveRecord
options = args.extract_options!
column(args[0], 'tsvector', options)
end
+
+ def hstore(name, options = {})
+ column(name, 'hstore', options)
+ end
end
ADAPTER_NAME = 'PostgreSQL'
@@ -247,6 +289,10 @@ module ActiveRecord
true
end
+ def supports_index_sort_order?
+ true
+ end
+
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -278,13 +324,24 @@ module ActiveRecord
cache.clear
end
+ def delete(sql_key)
+ dealloc cache[sql_key]
+ cache.delete sql_key
+ end
+
private
def cache
@cache[$$]
end
def dealloc(key)
- @connection.query "DEALLOCATE #{key}"
+ @connection.query "DEALLOCATE #{key}" if connection_active?
+ end
+
+ def connection_active?
+ @connection.status == PGconn::CONNECTION_OK
+ rescue PGError
+ false
end
end
@@ -292,6 +349,7 @@ module ActiveRecord
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
@connection_parameters, @config = connection_parameters, config
+ @visitor = Arel::Visitors::PostgreSQL.new self
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@local_tz = nil
@@ -308,10 +366,6 @@ module ActiveRecord
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
end
- def self.visitor_for(pool) # :nodoc:
- Arel::Visitors::PostgreSQL.new(pool)
- end
-
# Clears the prepared statements cache.
def clear_cache!
@statements.clear
@@ -378,6 +432,11 @@ module ActiveRecord
true
end
+ # Returns true.
+ def supports_explain?
+ true
+ end
+
# Returns the configured supported identifier length supported by PostgreSQL
def table_alias_length
@table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i
@@ -502,6 +561,48 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # PostgreSQL shell:
+ #
+ # QUERY PLAN
+ # ------------------------------------------------------------------------------
+ # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
+ # Join Filter: (posts.user_id = users.id)
+ # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
+ # Index Cond: (id = 1)
+ # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
+ # Filter: (posts.user_id = 1)
+ # (6 rows)
+ #
+ def pp(result)
+ header = result.columns.first
+ lines = result.rows.map(&:first)
+
+ # We add 2 because there's one char of padding at both sides, note
+ # the extra hyphens in the example above.
+ width = [header, *lines].map(&:length).max + 2
+
+ pp = []
+
+ pp << header.center(width).rstrip
+ pp << '-' * width
+
+ pp += lines.map {|line| " #{line}"}
+
+ nrows = result.rows.length
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ pp << "(#{nrows} #{rows_label})"
+
+ pp.join("\n") + "\n"
+ end
+ end
+
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
def select_rows(sql, name = nil)
@@ -743,16 +844,15 @@ module ActiveRecord
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
- schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
result = query(<<-SQL, name)
- SELECT distinct i.relname, d.indisunique, d.indkey, t.oid
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
ORDER BY i.relname
SQL
@@ -761,7 +861,8 @@ module ActiveRecord
index_name = row[0]
unique = row[1] == 't'
indkey = row[2].split(" ")
- oid = row[3]
+ inddef = row[3]
+ oid = row[4]
columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")]
SELECT a.attnum, a.attname
@@ -771,7 +872,12 @@ module ActiveRecord
SQL
column_names = columns.values_at(*indkey).compact
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names)
+
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+
+ column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders)
end.compact
end
@@ -815,7 +921,7 @@ module ActiveRecord
# Returns the active schema search path.
def schema_search_path
- @schema_search_path ||= query('SHOW search_path')[0][0]
+ @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
end
# Returns the current client message level.
@@ -911,12 +1017,14 @@ module ActiveRecord
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(name, new_name)
+ clear_cache!
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
+ clear_cache!
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
@@ -925,6 +1033,7 @@ module ActiveRecord
# Changes the column of a table.
def change_column(table_name, column_name, type, options = {})
+ clear_cache!
quoted_table_name = quote_table_name(table_name)
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
@@ -935,10 +1044,12 @@ module ActiveRecord
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default)
+ clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
end
def change_column_null(table_name, column_name, null, default = nil)
+ clear_cache!
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
@@ -947,6 +1058,7 @@ module ActiveRecord
# Renames a column in a table.
def rename_column(table_name, column_name, new_column_name)
+ clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
end
@@ -1030,26 +1142,54 @@ module ActiveRecord
end
private
+ FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
+
def exec_no_cache(sql, binds)
@connection.async_exec(sql)
end
def exec_cache(sql, binds)
- unless @statements.key? sql
- nextkey = @statements.next_key
- @connection.prepare nextkey, sql
- @statements[sql] = nextkey
+ begin
+ stmt_key = prepare_statement sql
+
+ # Clear the queue
+ @connection.get_last_result
+ @connection.send_query_prepared(stmt_key, binds.map { |col, val|
+ type_cast(val, col)
+ })
+ @connection.block
+ @connection.get_last_result
+ rescue PGError => e
+ # Get the PG code for the failure. Annoyingly, the code for
+ # prepared statements whose return value may have changed is
+ # FEATURE_NOT_SUPPORTED. Check here for more details:
+ # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
+ code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ if FEATURE_NOT_SUPPORTED == code
+ @statements.delete sql_key(sql)
+ retry
+ else
+ raise e
+ end
end
+ end
- key = @statements[sql]
+ # Returns the statement identifier for the client side cache
+ # of statements
+ def sql_key(sql)
+ "#{schema_search_path}-#{sql}"
+ end
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
+ # Prepare the statement if it hasn't been prepared, return
+ # the statement key.
+ def prepare_statement(sql)
+ sql_key = sql_key(sql)
+ unless @statements.key? sql_key
+ nextkey = @statements.next_key
+ @connection.prepare nextkey, sql
+ @statements[sql_key] = nextkey
+ end
+ @statements[sql_key]
end
# The internal PostgreSQL identifier of the money data type.
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
new file mode 100644
index 0000000000..4e8932a695
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class SchemaCache
+ attr_reader :columns, :columns_hash, :primary_keys, :tables
+ attr_reader :connection
+
+ def initialize(conn)
+ @connection = conn
+ @tables = {}
+
+ @columns = Hash.new do |h, table_name|
+ h[table_name] = conn.columns(table_name, "#{table_name} Columns")
+ end
+
+ @columns_hash = Hash.new do |h, table_name|
+ h[table_name] = Hash[columns[table_name].map { |col|
+ [col.name, col]
+ }]
+ end
+
+ @primary_keys = Hash.new do |h, table_name|
+ h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil
+ end
+ end
+
+ # A cached lookup for table existence.
+ def table_exists?(name)
+ return @tables[name] if @tables.key? name
+
+ @tables[name] = connection.table_exists?(name)
+ end
+
+ # Clears out internal caches
+ def clear!
+ @columns.clear
+ @columns_hash.clear
+ @primary_keys.clear
+ @tables.clear
+ end
+
+ # Clear out internal caches for table with +table_name+.
+ def clear_table_cache!(table_name)
+ @columns.delete table_name
+ @columns_hash.delete table_name
+ @primary_keys.delete table_name
+ @tables.delete table_name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 0a0da0b5d3..ee5d10859c 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,12 +1,12 @@
require 'active_record/connection_adapters/sqlite_adapter'
-gem 'sqlite3', '~> 1.3.4'
+gem 'sqlite3', '~> 1.3.5'
require 'sqlite3'
module ActiveRecord
- class Base
+ module ConnectionHandling
# sqlite3 adapter reuses sqlite_connection.
- def self.sqlite3_connection(config) # :nodoc:
+ def sqlite3_connection(config) # :nodoc:
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -47,11 +47,7 @@ module ActiveRecord
# Returns the current database encoding format as a string, eg: 'UTF-8'
def encoding
- if @connection.respond_to?(:encoding)
- @connection.encoding.to_s
- else
- @connection.execute('PRAGMA encoding')[0]['encoding']
- end
+ @connection.encoding.to_s
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 1932a849ee..69750a911d 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -16,7 +16,7 @@ module ActiveRecord
end
def binary_to_string(value)
- if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT
+ if value.encoding != Encoding::ASCII_8BIT
value = value.force_encoding(Encoding::ASCII_8BIT)
end
@@ -89,10 +89,7 @@ module ActiveRecord
@statements = StatementPool.new(@connection,
config.fetch(:statement_limit) { 1000 })
@config = config
- end
-
- def self.visitor_for(pool) # :nodoc:
- Arel::Visitors::SQLite.new(pool)
+ @visitor = Arel::Visitors::SQLite.new self
end
def adapter_name #:nodoc:
@@ -125,6 +122,11 @@ module ActiveRecord
true
end
+ # Returns true.
+ def supports_explain?
+ true
+ end
+
def requires_reloading?
true
end
@@ -157,6 +159,10 @@ module ActiveRecord
sqlite_version >= '3.1.0'
end
+ def supports_index_sort_order?
+ sqlite_version >= '3.3.0'
+ end
+
def native_database_types #:nodoc:
{
:primary_key => default_primary_key_type,
@@ -195,29 +201,40 @@ module ActiveRecord
end
end
- if "<3".encoding_aware?
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- @logger.error "Binary data inserted for `string` type on column `#{column.name}`"
- value.encode! 'utf-8'
- end
- value
- end
- else
- def type_cast(value, column) # :nodoc:
- return super unless BigDecimal === value
+ def type_cast(value, column) # :nodoc:
+ return value.to_f if BigDecimal === value
+ return super unless String === value
+ return super unless column && value
- value.to_f
+ value = super
+ if column.type == :string && value.encoding == Encoding::ASCII_8BIT
+ @logger.error "Binary data inserted for `string` type on column `#{column.name}`"
+ value.encode! 'utf-8'
end
+ value
end
# DATABASE STATEMENTS ======================================
+ def explain(arel, binds = [])
+ sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ end
+
+ class ExplainPrettyPrinter
+ # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
+ # the output of the SQLite shell:
+ #
+ # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
+ # 0|1|1|SCAN TABLE posts (~100000 rows)
+ #
+ def pp(result) # :nodoc:
+ result.rows.map do |row|
+ row.join('|')
+ end.join("\n") + "\n"
+ end
+ end
+
def exec_query(sql, name = nil, binds = [])
log(sql, name, binds) do
@@ -304,18 +321,23 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def tables(name = 'SCHEMA') #:nodoc:
+ def tables(name = 'SCHEMA', table_name = nil) #:nodoc:
sql = <<-SQL
SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL
+ sql << " AND name = #{quote_table_name(table_name)}" if table_name
exec_query(sql, name).map do |row|
row['name']
end
end
+ def table_exists?(name)
+ name && tables('SCHEMA', name).any?
+ end
+
# Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+.
def columns(table_name, name = nil) #:nodoc:
table_structure(table_name).map do |field|
@@ -413,6 +435,8 @@ module ActiveRecord
self.limit = options[:limit] if options.include?(:limit)
self.default = options[:default] if include_default
self.null = options[:null] if options.include?(:null)
+ self.precision = options[:precision] if options.include?(:precision)
+ self.scale = options[:scale] if options.include?(:scale)
end
end
end
@@ -467,6 +491,7 @@ module ActiveRecord
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
+ :precision => column.precision, :scale => column.scale,
:null => column.null)
end
@definition.primary_key(primary_key(from)) if primary_key(from)