aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters
diff options
context:
space:
mode:
authorJon Leighton <j@jonathanleighton.com>2010-10-28 18:06:01 +0100
committerJon Leighton <j@jonathanleighton.com>2010-10-28 18:06:01 +0100
commitfc276e5635821e65c04b8961170cc6bd3c11b923 (patch)
tree9bd4ed018858611248e9b6aa7392dced92bc814a /activerecord/lib/active_record/connection_adapters
parent8e53e058acea471eab7a1609dc150aa9fdbfa833 (diff)
parent6a3d6b7f1352efd3e7b931533740252b04850e27 (diff)
downloadrails-fc276e5635821e65c04b8961170cc6bd3c11b923.tar.gz
rails-fc276e5635821e65c04b8961170cc6bd3c11b923.tar.bz2
rails-fc276e5635821e65c04b8961170cc6bd3c11b923.zip
Merge branch 'master' into nested_has_many_through
Conflicts: activerecord/CHANGELOG activerecord/lib/active_record/association_preload.rb activerecord/lib/active_record/associations.rb activerecord/test/schema/schema.rb
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb135
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb72
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb61
6 files changed, 271 insertions, 61 deletions
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 646a78622c..6178130c00 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -3,8 +3,16 @@ module ActiveRecord
module DatabaseStatements
# Returns an array of record hashes with the column names as keys and
# column values as values.
- def select_all(sql, name = nil)
- select(sql, name)
+ def select_all(sql, name = nil, binds = [])
+ if supports_statement_cache?
+ select(sql, name, binds)
+ else
+ return select(sql, name) if binds.empty?
+ binds = binds.dup
+ select sql.gsub('?') {
+ quote(*binds.shift.reverse)
+ }, name
+ end
end
# Returns a record hash with the column names as keys and column values
@@ -39,6 +47,12 @@ module ActiveRecord
end
undef_method :execute
+ # Executes +sql+ statement in the context of this connection using
+ # +binds+ as the bind substitutes. +name+ is logged along with
+ # the executed +sql+ statement.
+ def exec(sql, name = 'SQL', binds = [])
+ end
+
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
insert_sql(sql, name, pk, id_value, sequence_name)
@@ -68,6 +82,12 @@ module ActiveRecord
nil
end
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ false
+ end
+
# Runs the given block in a database transaction, and returns the result
# of the block.
#
@@ -254,7 +274,7 @@ module ActiveRecord
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.
- def select(sql, name = nil)
+ def select(sql, name = nil, binds = [])
end
undef_method :select
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 0ee61d0b6f..d555308485 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/duplicable'
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
module QueryCache
@@ -49,32 +47,26 @@ module ActiveRecord
@query_cache.clear
end
- def select_all(*args)
+ def select_all(sql, name = nil, binds = [])
if @query_cache_enabled
- cache_sql(args.first) { super }
+ cache_sql(sql, binds) { super }
else
super
end
end
private
- def cache_sql(sql)
+ def cache_sql(sql, binds)
result =
- if @query_cache.has_key?(sql)
+ if @query_cache[sql].key?(binds)
ActiveSupport::Notifications.instrument("sql.active_record",
:sql => sql, :name => "CACHE", :connection_id => self.object_id)
- @query_cache[sql]
+ @query_cache[sql][binds]
else
- @query_cache[sql] = yield
+ @query_cache[sql][binds] = yield
end
- if Array === result
- result.collect { |row| row.dup }
- else
- result.duplicable? ? result.dup : result
- end
- rescue TypeError
- result
+ result.collect { |row| row.dup }
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index d8c92d0ad3..f3fba9a3a9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -12,6 +12,7 @@ require 'active_record/connection_adapters/abstract/connection_pool'
require 'active_record/connection_adapters/abstract/connection_specification'
require 'active_record/connection_adapters/abstract/query_cache'
require 'active_record/connection_adapters/abstract/database_limits'
+require 'active_record/result'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -40,7 +41,7 @@ module ActiveRecord
@active = nil
@connection, @logger = connection, logger
@query_cache_enabled = false
- @query_cache = {}
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
@instrumenter = ActiveSupport::Notifications.instrumenter
end
@@ -97,6 +98,12 @@ module ActiveRecord
quote_column_name(name)
end
+ # Returns a bind substitution value given a +column+ and list of current
+ # +binds+
+ def substitute_for(column, binds)
+ Arel.sql '?'
+ end
+
# REFERENTIAL INTEGRITY ====================================
# Override to turn off referential integrity while executing <tt>&block</tt>.
@@ -135,6 +142,13 @@ module ActiveRecord
# this should be overridden by concrete adapters
end
+ ###
+ # Clear any caching the database adapter may be doing, for example
+ # clearing the prepared statement cache. This is database specific.
+ def clear_cache!
+ # this should be overridden by concrete adapters
+ end
+
# Returns true if its required to reload the connection between requests for development mode.
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index a4b336dfaf..7d47d06ae1 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -3,6 +3,28 @@ require 'active_support/core_ext/kernel/requires'
require 'active_support/core_ext/object/blank'
require 'set'
+begin
+ require 'mysql'
+rescue LoadError
+ raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'"
+end
+
+unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash)
+ raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'"
+end
+
+class Mysql
+ class Time
+ ###
+ # This monkey patch is for test_additional_columns_from_join_table
+ def to_date
+ Date.new(year, month, day)
+ end
+ end
+ class Stmt; include Enumerable end
+ class Result; include Enumerable end
+end
+
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects.
@@ -15,18 +37,6 @@ module ActiveRecord
password = config[:password].to_s
database = config[:database]
- unless defined? Mysql
- begin
- require 'mysql'
- rescue LoadError
- raise "!!! Missing the mysql2 gem. Add it to your Gemfile: gem 'mysql2'"
- end
-
- unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash)
- raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'"
- end
- end
-
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
@@ -39,6 +49,30 @@ module ActiveRecord
module ConnectionAdapters
class MysqlColumn < Column #:nodoc:
+ class << self
+ def string_to_time(value)
+ return super unless Mysql::Time === value
+ new_time(
+ value.year,
+ value.month,
+ value.day,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ end
+
+ def string_to_dummy_time(v)
+ return super unless Mysql::Time === v
+ new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
+ end
+
+ def string_to_date(v)
+ return super unless Mysql::Time === v
+ new_date(v.year, v.month, v.day)
+ end
+ end
+
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
@@ -161,6 +195,7 @@ module ActiveRecord
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
+ @statements = {}
connect
end
@@ -168,6 +203,12 @@ module ActiveRecord
ADAPTER_NAME
end
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ true
+ end
+
def supports_migrations? #:nodoc:
true
end
@@ -252,6 +293,7 @@ module ActiveRecord
def reconnect!
disconnect!
+ clear_cache!
connect
end
@@ -272,14 +314,63 @@ module ActiveRecord
def select_rows(sql, name = nil)
@connection.query_with_result = true
- result = execute(sql, name)
- rows = []
- result.each { |row| rows << row }
- result.free
+ rows = exec_without_stmt(sql, name).rows
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
+ def clear_cache!
+ @statements.values.each do |cache|
+ cache[:stmt].close
+ end
+ @statements.clear
+ end
+
+ def exec(sql, name = 'SQL', binds = [])
+ log(sql, name) do
+ result = nil
+
+ cache = {}
+ if binds.empty?
+ stmt = @connection.prepare(sql)
+ else
+ cache = @statements[sql] ||= {
+ :stmt => @connection.prepare(sql)
+ }
+ stmt = cache[:stmt]
+ end
+
+ stmt.execute(*binds.map { |col, val|
+ col ? col.type_cast(val) : val
+ })
+ if metadata = stmt.result_metadata
+ cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
+ field.name
+ }
+
+ metadata.free
+ result = ActiveRecord::Result.new(cols, stmt.to_a)
+ end
+
+ stmt.free_result
+ stmt.close if binds.empty?
+
+ result
+ end
+ end
+
+ def exec_without_stmt(sql, name = 'SQL') # :nodoc:
+ # Some queries, like SHOW CREATE TABLE don't work through the prepared
+ # statement API. For those queries, we need to use this method. :'(
+ log(sql, name) do
+ result = @connection.query(sql)
+ cols = result.fetch_fields.map { |field| field.name }
+ rows = result.to_a
+ result.free
+ ActiveRecord::Result.new(cols, rows)
+ end
+ end
+
# Executes an SQL query and returns a MySQL::Result object. Note that you have to free
# the Result object after you're done using it.
def execute(sql, name = nil) #:nodoc:
@@ -308,7 +399,7 @@ module ActiveRecord
end
def begin_db_transaction #:nodoc:
- execute "BEGIN"
+ exec_without_stmt "BEGIN"
rescue Exception
# Transactions aren't supported
end
@@ -360,7 +451,8 @@ module ActiveRecord
select_all(sql).map do |table|
table.delete('Table_type')
- select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
+ sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
+ exec_without_stmt(sql).first['Create Table'] + ";\n\n"
end.join("")
end
@@ -614,12 +706,9 @@ module ActiveRecord
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
end
- def select(sql, name = nil)
+ def select(sql, name = nil, binds = [])
@connection.query_with_result = true
- result = execute(sql, name)
- rows = []
- result.each_hash { |row| rows << row }
- result.free
+ rows = exec(sql, name, binds).to_a
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 5949985e4d..58c0f85dc2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -211,6 +211,12 @@ module ActiveRecord
ADAPTER_NAME
end
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ true
+ end
+
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
@@ -220,11 +226,19 @@ module ActiveRecord
@local_tz = nil
@table_alias_length = nil
@postgresql_version = nil
+ @statements = {}
connect
@local_tz = execute('SHOW TIME ZONE').first["TimeZone"]
end
+ def clear_cache!
+ @statements.each_value do |value|
+ @connection.query "DEALLOCATE #{value}"
+ end
+ @statements.clear
+ end
+
# Is this connection alive and ready for queries?
def active?
if @connection.respond_to?(:status)
@@ -242,6 +256,7 @@ module ActiveRecord
# Close then reopen the connection.
def reconnect!
if @connection.respond_to?(:reset)
+ clear_cache!
@connection.reset
configure_connection
else
@@ -250,8 +265,14 @@ module ActiveRecord
end
end
+ def reset!
+ clear_cache!
+ super
+ end
+
# Close the connection.
def disconnect!
+ clear_cache!
@connection.close rescue nil
end
@@ -374,6 +395,12 @@ module ActiveRecord
end
end
+ # Set the authorized user for this session
+ def session_auth=(user)
+ clear_cache!
+ exec "SET SESSION AUTHORIZATION #{user}"
+ end
+
# REFERENTIAL INTEGRITY ====================================
def supports_disable_referential_integrity?() #:nodoc:
@@ -501,6 +528,35 @@ module ActiveRecord
end
end
+ def substitute_for(column, current_values)
+ Arel.sql("$#{current_values.length + 1}")
+ end
+
+ def exec(sql, name = 'SQL', binds = [])
+ return exec_no_cache(sql, name) if binds.empty?
+
+ log(sql, name) do
+ unless @statements.key? sql
+ nextkey = "a#{@statements.length + 1}"
+ @connection.prepare nextkey, sql
+ @statements[sql] = nextkey
+ end
+
+ key = @statements[sql]
+
+ # Clear the queue
+ @connection.get_last_result
+ @connection.send_query_prepared(key, binds.map { |col, val|
+ col ? col.type_cast(val) : val
+ })
+ @connection.block
+ result = @connection.get_last_result
+ ret = ActiveRecord::Result.new(result.fields, result_as_array(result))
+ result.clear
+ return ret
+ end
+ end
+
# Executes an UPDATE query and returns the number of affected tuples.
def update_sql(sql, name = nil)
super.cmd_tuples
@@ -920,6 +976,15 @@ module ActiveRecord
end
private
+ def exec_no_cache(sql, name)
+ log(sql, name) do
+ result = @connection.async_exec(sql)
+ ret = ActiveRecord::Result.new(result.fields, result_as_array(result))
+ result.clear
+ ret
+ end
+ end
+
# The internal PostgreSQL identifier of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
# The internal PostgreSQL identifier of the BYTEA data type.
@@ -974,11 +1039,8 @@ module ActiveRecord
# Executes a SELECT query and returns the results, performing any data type
# conversions that are required to be performed here instead of in PostgreSQLColumn.
- def select(sql, name = nil)
- fields, rows = select_raw(sql, name)
- rows.map do |row|
- Hash[fields.zip(row)]
- end
+ def select(sql, name = nil, binds = [])
+ exec(sql, name, binds).to_a
end
def select_raw(sql, name = nil)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index c0cc7ba20d..a4d7d12298 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -50,6 +50,7 @@ module ActiveRecord
def initialize(connection, logger, config)
super(connection, logger)
+ @statements = {}
@config = config
end
@@ -61,6 +62,12 @@ module ActiveRecord
sqlite_version >= '2.0.0'
end
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ true
+ end
+
def supports_migrations? #:nodoc:
true
end
@@ -79,9 +86,14 @@ module ActiveRecord
def disconnect!
super
+ clear_cache!
@connection.close rescue nil
end
+ def clear_cache!
+ @statements.clear
+ end
+
def supports_count_distinct? #:nodoc:
sqlite_version >= '3.2.6'
end
@@ -121,7 +133,7 @@ module ActiveRecord
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
- if value.acts_like?(:time) && value.respond_to?(:usec)
+ if value.respond_to?(:usec)
"#{super}.#{sprintf("%06d", value.usec)}"
else
super
@@ -131,6 +143,29 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def exec(sql, name = nil, binds = [])
+ log(sql, name) do
+
+ # Don't cache statements without bind values
+ if binds.empty?
+ stmt = @connection.prepare(sql)
+ cols = stmt.columns
+ else
+ cache = @statements[sql] ||= {
+ :stmt => @connection.prepare(sql)
+ }
+ stmt = cache[:stmt]
+ cols = cache[:cols] ||= stmt.columns
+ stmt.reset!
+ stmt.bind_params binds.map { |col, val|
+ col ? col.type_cast(val) : val
+ }
+ end
+
+ ActiveRecord::Result.new(cols, stmt.to_a)
+ end
+ end
+
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.execute(sql) }
end
@@ -151,9 +186,7 @@ module ActiveRecord
alias :create :insert_sql
def select_rows(sql, name = nil)
- execute(sql, name).map do |row|
- (0...(row.size / 2)).map { |i| row[i] }
- end
+ exec(sql, name).rows
end
def begin_db_transaction #:nodoc:
@@ -177,7 +210,7 @@ module ActiveRecord
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL
- execute(sql, name).map do |row|
+ exec(sql, name).map do |row|
row['name']
end
end
@@ -189,12 +222,12 @@ module ActiveRecord
end
def indexes(table_name, name = nil) #:nodoc:
- execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
+ exec("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
IndexDefinition.new(
table_name,
row['name'],
row['unique'].to_i != 0,
- execute("PRAGMA index_info('#{row['name']}')").map { |col|
+ exec("PRAGMA index_info('#{row['name']}')").map { |col|
col['name']
})
end
@@ -208,11 +241,11 @@ module ActiveRecord
end
def remove_index!(table_name, index_name) #:nodoc:
- execute "DROP INDEX #{quote_column_name(index_name)}"
+ exec "DROP INDEX #{quote_column_name(index_name)}"
end
def rename_table(name, new_name)
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ exec "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
end
# See: http://www.sqlite.org/lang_altertable.html
@@ -249,7 +282,7 @@ module ActiveRecord
def change_column_null(table_name, column_name, null, default = nil)
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")
+ exec("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
alter_table(table_name) do |definition|
definition[column_name].null = null
@@ -280,8 +313,8 @@ module ActiveRecord
end
protected
- def select(sql, name = nil) #:nodoc:
- execute(sql, name).map do |row|
+ def select(sql, name = nil, binds = []) #:nodoc:
+ exec(sql, name, binds).map do |row|
record = {}
row.each do |key, value|
record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String)
@@ -367,11 +400,11 @@ module ActiveRecord
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
quoted_to = quote_table_name(to)
- @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
+ exec("SELECT * FROM #{quote_table_name(from)}").each do |row|
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
sql << ')'
- @connection.execute sql
+ exec sql
end
end