aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record/association_preload.rb2
-rw-r--r--activerecord/lib/active_record/associations.rb32
-rw-r--r--activerecord/lib/active_record/autosave_association.rb11
-rw-r--r--activerecord/lib/active_record/base.rb49
-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
-rw-r--r--activerecord/lib/active_record/named_scope.rb6
-rw-r--r--activerecord/lib/active_record/reflection.rb4
-rw-r--r--activerecord/lib/active_record/relation.rb4
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb7
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb17
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb18
-rw-r--r--activerecord/lib/active_record/result.rb30
17 files changed, 386 insertions, 126 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index 664b0a7d59..bafd59fc02 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -254,8 +254,6 @@ module ActiveRecord
alias_method :preload_has_many_association, :preload_has_one_or_has_many_association
def preload_through_records(records, reflection, through_association)
- through_reflection = reflections[through_association]
-
# If the same through record is loaded twice, we want to return exactly the same
# object in the result, rather than two separate instances representing the same
# record. This is so that we can preload the source association for each record,
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 25be9a52ff..ee2e2765bc 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -2043,7 +2043,7 @@ module ActiveRecord
association_proxy = record.send("set_#{join_part.reflection.name}_target", association)
association_proxy.__send__(:set_inverse_instance, association, record)
end
-
+
# A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
@@ -2055,38 +2055,38 @@ module ActiveRecord
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
attr_reader :active_record
-
+
delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record
-
+
def initialize(active_record)
@active_record = active_record
@cached_record = {}
end
-
+
def ==(other)
raise NotImplementedError
end
-
+
# An Arel::Table for the active_record
def table
raise NotImplementedError
end
-
+
# The prefix to be used when aliasing columns in the active_record's table
def aliased_prefix
raise NotImplementedError
end
-
+
# The alias for the active_record's table
def aliased_table_name
raise NotImplementedError
end
-
+
# The alias for the primary key of the active_record's table
def aliased_primary_key
"#{aliased_prefix}_r0"
end
-
+
# An array of [column_name, alias] pairs for the table
def column_names_with_alias
unless defined?(@column_names_with_alias)
@@ -2116,7 +2116,7 @@ module ActiveRecord
class JoinBase < JoinPart # :nodoc:
# Extra joins provided when the JoinDependency was created
attr_reader :table_joins
-
+
def initialize(active_record, joins = nil)
super(active_record)
@table_joins = joins
@@ -2144,16 +2144,16 @@ module ActiveRecord
class JoinAssociation < JoinPart # :nodoc:
# The reflection of the association represented
attr_reader :reflection
-
+
# The JoinDependency object which this JoinAssociation exists within. This is mainly
# relevant for generating aliases which do not conflict with other joins which are
# part of the query.
attr_reader :join_dependency
-
+
# A JoinBase instance representing the active record we are joining onto.
# (So in Author.has_many :posts, the Author would be that base record.)
attr_reader :parent
-
+
# What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
attr_accessor :join_type
@@ -2165,7 +2165,7 @@ module ActiveRecord
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
-
+
if reflection.options[:polymorphic]
raise EagerLoadPolymorphicError.new(reflection)
end
@@ -2192,7 +2192,7 @@ module ActiveRecord
self.parent == join_part
end
end
-
+
def join_to(relation)
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context)
@@ -2282,7 +2282,7 @@ module ActiveRecord
self.join_type = Arel::OuterJoin
joining_relation.joins(self)
end
-
+
def table
if @tables.last.is_a?(Array)
@tables.last.first
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index f3f89fe7c3..0b89a49896 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -167,7 +167,16 @@ module ActiveRecord
else
if reflection.macro == :has_one
define_method(save_method) { save_has_one_association(reflection) }
- after_save save_method
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
else
define_method(save_method) { save_belongs_to_association(reflection) }
before_save save_method
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 78b3507dd9..06a388cd21 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -448,8 +448,8 @@ module ActiveRecord #:nodoc:
# # You can use the same string replacement techniques as you can with ActiveRecord#find
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
# > [#<Post:0x36bff9c @attributes={"first_name"=>"The Cheap Man Buys Twice"}>, ...]
- def find_by_sql(sql)
- connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
+ def find_by_sql(sql, binds = [])
+ connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) }
end
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -718,6 +718,7 @@ module ActiveRecord #:nodoc:
# end
# end
def reset_column_information
+ connection.clear_cache!
undefine_attribute_methods
@column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
@arel_engine = @relation = @arel_table = nil
@@ -972,14 +973,11 @@ module ActiveRecord #:nodoc:
super unless all_attributes_exists?(attribute_names)
if match.scope?
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
- def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
- options = args.extract_options! # options = args.extract_options!
- attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
- [:#{attribute_names.join(',:')}], args # [:user_name, :password], args
- ) # )
- #
- scoped(:conditions => attributes) # scoped(:conditions => attributes)
- end # end
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
+ attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
+ #
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
+ end # end
METHOD
send(method_id, *arguments)
end
@@ -988,31 +986,22 @@ module ActiveRecord #:nodoc:
end
end
- def construct_attributes_from_arguments(attribute_names, arguments)
- attributes = {}
- attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
- attributes
- end
-
# Similar in purpose to +expand_hash_conditions_for_aggregates+.
def expand_attribute_names_for_aggregates(attribute_names)
- expanded_attribute_names = []
- attribute_names.each do |attribute_name|
+ attribute_names.map { |attribute_name|
unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
- aggregate_mapping(aggregation).each do |field_attr, aggregate_attr|
- expanded_attribute_names << field_attr
+ aggregate_mapping(aggregation).map do |field_attr, _|
+ field_attr.to_sym
end
else
- expanded_attribute_names << attribute_name
+ attribute_name.to_sym
end
- end
- expanded_attribute_names
+ }.flatten
end
def all_attributes_exists?(attribute_names)
- expand_attribute_names_for_aggregates(attribute_names).all? { |name|
- column_methods_hash.include?(name.to_sym)
- }
+ (expand_attribute_names_for_aggregates(attribute_names) -
+ column_methods_hash.keys).empty?
end
protected
@@ -1139,11 +1128,17 @@ MSG
# Article.new.published # => true
# Article.create.published # => true
def default_scope(options = {})
+ reset_scoped_methods
self.default_scoping << construct_finder_arel(options, default_scoping.pop)
end
def current_scoped_methods #:nodoc:
- scoped_methods.last
+ method = scoped_methods.last
+ if method.respond_to?(:call)
+ relation.scoping { method.call }
+ else
+ method
+ end
end
def reset_scoped_methods #:nodoc:
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
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 6ab84df25b..0b92ba5caa 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -97,14 +97,14 @@ module ActiveRecord
#
# Article.published.new.published # => true
# Article.published.create.published # => true
- def scope(name, scope_options = {}, &block)
+ def scope(name, scope_options = {})
name = name.to_sym
valid_scope_name?(name)
- extension = Module.new(&block) if block_given?
+ extension = Module.new(&Proc.new) if block_given?
scopes[name] = lambda do |*args|
- options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options
+ options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options
relation = if options.is_a?(Hash)
scoped.apply_finder_options(options)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 824674ee1d..6eb2057f66 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -36,7 +36,7 @@ module ActiveRecord
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
+ reflections.values.grep(AggregateReflection)
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
@@ -58,7 +58,7 @@ module ActiveRecord
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
- association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
+ association_reflections = reflections.values.grep(AssociationReflection)
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index f129b54f9a..58f7b74198 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class Relation
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
- MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having]
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from]
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
@@ -61,7 +61,7 @@ module ActiveRecord
def to_a
return @records if loaded?
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
preload = @preload_values
preload += @includes_values unless eager_loading?
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index b763e22ec6..fe1ef2e2e3 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -288,7 +288,12 @@ module ActiveRecord
def find_one(id)
id = id.id if ActiveRecord::Base === id
- record = where(primary_key.eq(id)).first
+ column = primary_key.column
+
+ substitute = connection.substitute_for(column, @bind_values)
+ relation = where(primary_key.eq(substitute))
+ relation.bind_values = [[column, id]]
+ record = relation.first
unless record
conditions = arel.where_sql
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 59ce76ea42..9c399d3333 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -6,7 +6,8 @@ module ActiveRecord
extend ActiveSupport::Concern
attr_accessor :includes_values, :eager_load_values, :preload_values,
- :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values,
+ :select_values, :group_values, :order_values, :joins_values,
+ :where_values, :having_values, :bind_values,
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value
def includes(*args)
@@ -62,6 +63,12 @@ module ActiveRecord
relation
end
+ def bind(value)
+ relation = clone
+ relation.bind_values += [value]
+ relation
+ end
+
def where(opts, *rest)
relation = clone
relation.where_values += build_where(opts, rest) unless opts.blank?
@@ -117,8 +124,10 @@ module ActiveRecord
relation
end
- def extending(*modules, &block)
- modules << Module.new(&block) if block_given?
+ def extending(*modules)
+ modules << Module.new(&Proc.new) if block_given?
+
+ return self if modules.empty?
relation = clone
relation.send(:apply_modules, modules.flatten)
@@ -233,7 +242,7 @@ module ActiveRecord
@implicit_readonly = false
arel.project(*selects)
else
- arel.project(Arel::SqlLiteral.new(@klass.quoted_table_name + '.*'))
+ arel.project(Arel.sql(@klass.quoted_table_name + '.*'))
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index dcddc3dac4..648a02f1cc 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -26,19 +26,23 @@ module ActiveRecord
merged_relation = merged_relation.joins(r.joins_values)
- merged_wheres = @where_values
+ merged_wheres = @where_values + r.where_values
- r.where_values.each do |w|
+ # Remove duplicates, last one wins.
+ seen = {}
+ merged_wheres = merged_wheres.reverse.reject { |w|
+ nuke = false
if w.respond_to?(:operator) && w.operator == :==
- merged_wheres = merged_wheres.reject {|p| p.respond_to?(:operator) && p.operator == :== && p.operand1.name == w.operand1.name }
+ name = w.left.name
+ nuke = seen[name]
+ seen[name] = true
end
-
- merged_wheres += [w]
- end
+ nuke
+ }.reverse
merged_relation.where_values = merged_wheres
- Relation::SINGLE_VALUE_METHODS.reject {|m| m == :lock}.each do |method|
+ (Relation::SINGLE_VALUE_METHODS - [:lock]).each do |method|
value = r.send(:"#{method}_value")
merged_relation.send(:"#{method}_value=", value) unless value.nil?
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
new file mode 100644
index 0000000000..8deff1478f
--- /dev/null
+++ b/activerecord/lib/active_record/result.rb
@@ -0,0 +1,30 @@
+module ActiveRecord
+ ###
+ # This class encapsulates a Result returned from calling +exec+ on any
+ # database connection adapter. For example:
+ #
+ # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo')
+ # x # => #<ActiveRecord::Result:0xdeadbeef>
+ class Result
+ include Enumerable
+
+ attr_reader :columns, :rows
+
+ def initialize(columns, rows)
+ @columns = columns
+ @rows = rows
+ @hash_rows = nil
+ end
+
+ def each
+ hash_rows.each { |row| yield row }
+ end
+
+ private
+ def hash_rows
+ @hash_rows ||= @rows.map { |row|
+ ActiveSupport::OrderedHash[@columns.zip(row)]
+ }
+ end
+ end
+end