aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/lib/active_record/association_preload.rb21
-rw-r--r--activerecord/lib/active_record/associations.rb49
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb66
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb19
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb20
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb22
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb37
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb56
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb39
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb24
-rw-r--r--activerecord/lib/active_record/autosave_association.rb4
-rw-r--r--activerecord/lib/active_record/base.rb5
-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.rb74
-rw-r--r--activerecord/lib/active_record/relation.rb13
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb7
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb9
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb25
-rw-r--r--activerecord/lib/active_record/result.rb30
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb58
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb51
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb32
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb62
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb7
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb26
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb6
-rw-r--r--activerecord/test/cases/helper.rb8
-rw-r--r--activerecord/test/cases/method_scoping_test.rb6
-rw-r--r--activerecord/test/cases/query_cache_test.rb8
-rw-r--r--activerecord/test/cases/relations_test.rb14
-rw-r--r--activerecord/test/connections/native_oracle/connection.rb4
-rw-r--r--activerecord/test/models/company.rb1
38 files changed, 807 insertions, 285 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index e6b367790b..911a5155fd 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -256,9 +256,6 @@ module ActiveRecord
end
def preload_through_records(records, reflection, through_association)
- through_reflection = reflections[through_association]
-
- through_records = []
if reflection.options[:source_type]
interface = reflection.source_reflection.options[:foreign_type]
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
@@ -267,16 +264,15 @@ module ActiveRecord
records.first.class.preload_associations(records, through_association, preload_options)
# Dont cache the association - we would only be caching a subset
- records.each do |record|
+ records.map { |record|
proxy = record.send(through_association)
if proxy.respond_to?(:target)
- through_records.concat Array.wrap(proxy.target)
- proxy.reset
+ Array.wrap(proxy.target).tap { proxy.reset }
else # this is a has_one :through reflection
- through_records << proxy if proxy
+ [proxy].compact
end
- end
+ }.flatten(1)
else
options = {}
options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions]
@@ -284,11 +280,10 @@ module ActiveRecord
options[:conditions] = reflection.options[:conditions]
records.first.class.preload_associations(records, through_association, options)
- records.each do |record|
- through_records.concat Array.wrap(record.send(through_association))
- end
+ records.map { |record|
+ Array.wrap(record.send(through_association))
+ }.flatten(1)
end
- through_records
end
def preload_belongs_to_association(records, reflection, preload_options={})
@@ -325,7 +320,7 @@ module ActiveRecord
klass = klass_name.constantize
table_name = klass.quoted_table_name
- primary_key = reflection.options[:primary_key] || klass.primary_key
+ primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s
column_type = klass.columns.detect{|c| c.name == primary_key}.type
ids = _id_map.keys.map do |id|
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 59d328f207..dc466eafc2 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1837,7 +1837,7 @@ module ActiveRecord
def initialize(base, associations, joins)
@join_parts = [JoinBase.new(base, joins)]
- @associations = associations
+ @associations = {}
@reflections = []
@base_records_hash = {}
@base_records_in_order = []
@@ -1917,30 +1917,58 @@ module ActiveRecord
protected
+ def cache_joined_association(association)
+ associations = []
+ parent = association.parent
+ while parent != join_base
+ associations.unshift(parent.reflection.name)
+ parent = parent.parent
+ end
+ ref = @associations
+ associations.each do |key|
+ ref = ref[key]
+ end
+ ref[association.reflection.name] ||= {}
+ end
+
def build(associations, parent = nil, join_type = Arel::InnerJoin)
parent ||= join_parts.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
- @reflections << reflection
- join_association = build_join_association(reflection, parent)
- join_association.join_type = join_type
- @join_parts << join_association
+ unless join_association = find_join_association(reflection, parent)
+ @reflections << reflection
+ join_association = build_join_association(reflection, parent)
+ join_association.join_type = join_type
+ @join_parts << join_association
+ cache_joined_association(join_association)
+ end
+ join_association
when Array
associations.each do |association|
build(association, parent, join_type)
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- build(name, parent, join_type)
- build(associations[name], nil, join_type)
+ join_association = build(name, parent, join_type)
+ build(associations[name], join_association, join_type)
end
else
raise ConfigurationError, associations.inspect
end
end
+ def find_join_association(name_or_reflection, parent)
+ if String === name_or_reflection
+ name_or_reflection = name_or_reflection.to_sym
+ end
+
+ join_associations.detect { |j|
+ j.reflection == name_or_reflection && j.parent == parent
+ }
+ end
+
def remove_uniq_by_reflection(reflection, records)
if reflection && reflection.collection?
records.each { |record| record.send(reflection.name).target.uniq! }
@@ -2013,7 +2041,7 @@ module ActiveRecord
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
+ # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
# is joining to the base. A JoinAssociation may result in more than one actual join
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
@@ -2092,8 +2120,7 @@ module ActiveRecord
def ==(other)
other.class == self.class &&
- other.active_record == active_record &&
- other.table_joins == table_joins
+ other.active_record == active_record
end
def aliased_prefix
@@ -2114,7 +2141,7 @@ module ActiveRecord
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
+ # relevant for generating aliases which do not conflict with other joins which are
# part of the query.
attr_reader :join_dependency
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index cb2d9e0a79..896e18af01 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -19,11 +19,6 @@ module ActiveRecord
# If you need to work on all current children, new and existing records,
# +load_target+ and the +loaded+ flag are your friends.
class AssociationCollection < AssociationProxy #:nodoc:
- def initialize(owner, reflection)
- super
- construct_sql
- end
-
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
def select(select = nil)
@@ -36,7 +31,7 @@ module ActiveRecord
end
def scoped
- with_scope(construct_scope) { @reflection.klass.scoped }
+ with_scope(@scope) { @reflection.klass.scoped }
end
def find(*args)
@@ -58,9 +53,7 @@ module ActiveRecord
merge_options_from_reflection!(options)
construct_find_options!(options)
- find_scope = construct_scope[:find].slice(:conditions, :order)
-
- with_scope(:find => find_scope) do
+ with_scope(:find => @scope[:find].slice(:conditions, :order)) do
relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods))
case args.first
@@ -178,17 +171,18 @@ module ActiveRecord
end
end
- # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
- # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
- # descendant's +construct_sql+ method will have set :counter_sql automatically.
- # Otherwise, construct options and pass them with scope to the target class's +count+.
+ # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
+ # association, it will be used for the query. Otherwise, construct options and pass them with
+ # scope to the target class's +count+.
def count(column_name = nil, options = {})
column_name, options = nil, column_name if column_name.is_a?(Hash)
- if @reflection.options[:counter_sql] && !options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- elsif @reflection.options[:counter_sql]
- @reflection.klass.count_by_sql(@counter_sql)
+ if @reflection.options[:counter_sql] || @reflection.options[:finder_sql]
+ unless options.blank?
+ raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
+ end
+
+ @reflection.klass.count_by_sql(custom_counter_sql)
else
if @reflection.options[:uniq]
@@ -197,7 +191,7 @@ module ActiveRecord
options.merge!(:distinct => true)
end
- value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
+ value = @reflection.klass.send(:with_scope, @scope) { @reflection.klass.count(column_name, options) }
limit = @reflection.options[:limit]
offset = @reflection.options[:offset]
@@ -377,18 +371,6 @@ module ActiveRecord
def construct_find_options!(options)
end
- def construct_counter_sql
- if @reflection.options[:counter_sql]
- @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
- elsif @reflection.options[:finder_sql]
- # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
- @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
- @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
- else
- @counter_sql = @finder_sql
- end
- end
-
def load_target
if !@owner.new_record? || foreign_key_present
begin
@@ -434,9 +416,9 @@ module ActiveRecord
elsif @reflection.klass.scopes[method]
@_named_scopes_cache ||= {}
@_named_scopes_cache[method] ||= {}
- @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) }
+ @_named_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) }
else
- with_scope(construct_scope) do
+ with_scope(@scope) do
if block_given?
@reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
else
@@ -446,9 +428,19 @@ module ActiveRecord
end
end
- # overloaded in derived Association classes to provide useful scoping depending on association type.
- def construct_scope
- {}
+ def custom_counter_sql
+ if @reflection.options[:counter_sql]
+ counter_sql = @reflection.options[:counter_sql]
+ else
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
+ counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
+ end
+
+ interpolate_sql(counter_sql)
+ end
+
+ def custom_finder_sql
+ interpolate_sql(@reflection.options[:finder_sql])
end
def reset_target!
@@ -462,7 +454,7 @@ module ActiveRecord
def find_target
records =
if @reflection.options[:finder_sql]
- @reflection.klass.find_by_sql(@finder_sql)
+ @reflection.klass.find_by_sql(custom_finder_sql)
else
find(:all)
end
@@ -494,7 +486,7 @@ module ActiveRecord
ensure_owner_is_not_new
scoped_where = scoped.where_values_hash
- create_scope = scoped_where ? construct_scope[:create].merge(scoped_where) : construct_scope[:create]
+ create_scope = scoped_where ? @scope[:create].merge(scoped_where) : @scope[:create]
record = @reflection.klass.send(:with_scope, :create => create_scope) do
@reflection.build_association(attrs)
end
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index f333f4d603..0c12c3737d 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -61,6 +61,7 @@ module ActiveRecord
reflection.check_validity!
Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
reset
+ construct_scope
end
# Returns the owner of the proxy.
@@ -203,6 +204,24 @@ module ActiveRecord
@reflection.klass.send :with_scope, *args, &block
end
+ # Construct the scope used for find/create queries on the target
+ def construct_scope
+ @scope = {
+ :find => construct_find_scope,
+ :create => construct_create_scope
+ }
+ end
+
+ # Implemented by subclasses
+ def construct_find_scope
+ raise NotImplementedError
+ end
+
+ # Implemented by (some) subclasses
+ def construct_create_scope
+ {}
+ end
+
private
# Forwards any missing method call to the \target.
def method_missing(method, *args)
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 2eb56e5cd3..34b6cd5576 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -50,19 +50,21 @@ module ActiveRecord
"find"
end
- options = @reflection.options.dup
- (options.keys - [:select, :include, :readonly]).each do |key|
- options.delete key
- end
- options[:conditions] = conditions
+ options = @reflection.options.dup.slice(:select, :include, :readonly)
- the_target = @reflection.klass.send(find_method,
- @owner[@reflection.primary_key_name],
- options
- ) if @owner[@reflection.primary_key_name]
+ the_target = with_scope(:find => @scope[:find]) do
+ @reflection.klass.send(find_method,
+ @owner[@reflection.primary_key_name],
+ options
+ ) if @owner[@reflection.primary_key_name]
+ end
set_inverse_instance(the_target, @owner)
the_target
end
+
+ def construct_find_scope
+ { :conditions => conditions }
+ end
def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index e429806b0c..a0df860623 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -44,20 +44,20 @@ module ActiveRecord
end
end
+ def construct_find_scope
+ { :conditions => conditions }
+ end
+
def find_target
return nil if association_class.nil?
- target =
- if @reflection.options[:conditions]
- association_class.find(
- @owner[@reflection.primary_key_name],
- :select => @reflection.options[:select],
- :conditions => conditions,
- :include => @reflection.options[:include]
- )
- else
- association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
- end
+ target = association_class.send(:with_scope, :find => @scope[:find]) do
+ association_class.find(
+ @owner[@reflection.primary_key_name],
+ :select => @reflection.options[:select],
+ :include => @reflection.options[:include]
+ )
+ end
set_inverse_instance(target, @owner)
target
end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index eb65234dfb..1fc9aba5cf 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -24,7 +24,7 @@ module ActiveRecord
protected
def construct_find_options!(options)
- options[:joins] = Arel::SqlLiteral.new @join_sql
+ options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins])
options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*'))
end
@@ -80,27 +80,26 @@ module ActiveRecord
).delete
end
end
+
+ def construct_joins
+ "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
+ end
- def construct_sql
- if @reflection.options[:finder_sql]
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
- else
- @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
- @finder_sql << " AND (#{conditions})" if conditions
- end
-
- @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
-
- construct_counter_sql
+ def construct_conditions
+ sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
+ sql << " AND (#{conditions})" if conditions
+ sql
end
- def construct_scope
- { :find => { :conditions => @finder_sql,
- :joins => @join_sql,
- :readonly => false,
- :order => @reflection.options[:order],
- :include => @reflection.options[:include],
- :limit => @reflection.options[:limit] } }
+ def construct_find_scope
+ {
+ :conditions => construct_conditions,
+ :joins => construct_joins,
+ :readonly => false,
+ :order => @reflection.options[:order],
+ :include => @reflection.options[:include],
+ :limit => @reflection.options[:limit]
+ }
end
# Join tables with additional columns on top of the two foreign keys must be considered
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 978fc74560..830a82980d 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -6,14 +6,10 @@ module ActiveRecord
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < AssociationCollection #:nodoc:
- def initialize(owner, reflection)
- @finder_sql = nil
- super
- end
protected
def owner_quoted_id
if @reflection.options[:primary_key]
- quote_value(@owner.send(@reflection.options[:primary_key]))
+ @owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
else
@owner.quoted_id
end
@@ -35,10 +31,10 @@ module ActiveRecord
def count_records
count = if has_cached_counter?
@owner.send(:read_attribute, cached_counter_attribute_name)
- elsif @reflection.options[:counter_sql]
- @reflection.klass.count_by_sql(@counter_sql)
+ elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql]
+ @reflection.klass.count_by_sql(custom_counter_sql)
else
- @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
+ @reflection.klass.count(@scope[:find].slice(:conditions, :joins, :include))
end
# If there's nothing in the database and @target has no new records
@@ -87,36 +83,32 @@ module ActiveRecord
false
end
- def construct_sql
- case
- when @reflection.options[:finder_sql]
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
-
- when @reflection.options[:as]
- @finder_sql =
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
- @finder_sql << " AND (#{conditions})" if conditions
-
- else
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- @finder_sql << " AND (#{conditions})" if conditions
+ def construct_conditions
+ if @reflection.options[:as]
+ sql =
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
+ else
+ sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
end
+ sql << " AND (#{conditions})" if conditions
+ sql
+ end
- construct_counter_sql
+ def construct_find_scope
+ {
+ :conditions => construct_conditions,
+ :readonly => false,
+ :order => @reflection.options[:order],
+ :limit => @reflection.options[:limit],
+ :include => @reflection.options[:include]
+ }
end
- def construct_scope
+ def construct_create_scope
create_scoping = {}
set_belongs_to_association_for(create_scoping)
- {
- :find => { :conditions => @finder_sql,
- :readonly => false,
- :order => @reflection.options[:order],
- :limit => @reflection.options[:limit],
- :include => @reflection.options[:include]},
- :create => create_scoping
- }
+ create_scoping
end
def we_can_set_the_inverse_on_this?(record)
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 97883d8393..437c8b1fd6 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -82,21 +82,7 @@ module ActiveRecord
def find_target
return [] unless target_reflection_has_associated_record?
- with_scope(construct_scope) { @reflection.klass.find(:all) }
- end
-
- def construct_sql
- case
- when @reflection.options[:finder_sql]
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
-
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- @finder_sql << " AND (#{conditions})" if conditions
- else
- @finder_sql = construct_conditions
- end
-
- construct_counter_sql
+ with_scope(@scope) { @reflection.klass.find(:all) }
end
def has_cached_counter?
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index a6e6bfa356..17901387e9 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -2,11 +2,6 @@ module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
class HasOneAssociation < AssociationProxy #:nodoc:
- def initialize(owner, reflection)
- super
- construct_sql
- end
-
def create(attrs = {}, replace_existing = true)
new_record(replace_existing) do |reflection|
attrs = merge_with_conditions(attrs)
@@ -79,33 +74,31 @@ module ActiveRecord
private
def find_target
- options = @reflection.options.dup
- (options.keys - [:select, :order, :include, :readonly]).each do |key|
- options.delete key
- end
- options[:conditions] = @finder_sql
+ options = @reflection.options.dup.slice(:select, :order, :include, :readonly)
- the_target = @reflection.klass.find(:first, options)
+ the_target = with_scope(:find => @scope[:find]) do
+ @reflection.klass.find(:first, options)
+ end
set_inverse_instance(the_target, @owner)
the_target
end
- def construct_sql
- case
- when @reflection.options[:as]
- @finder_sql =
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
- else
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
+ def construct_find_scope
+ if @reflection.options[:as]
+ sql =
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
+ else
+ sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
end
- @finder_sql << " AND (#{conditions})" if conditions
+ sql << " AND (#{conditions})" if conditions
+ { :conditions => sql }
end
- def construct_scope
+ def construct_create_scope
create_scoping = {}
set_belongs_to_association_for(create_scoping)
- { :create => create_scoping }
+ create_scoping
end
def new_record(replace_existing)
@@ -113,7 +106,7 @@ module ActiveRecord
# instance. Otherwise, if the target has not previously been loaded
# elsewhere, the instance we create will get orphaned.
load_target if replace_existing
- record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
+ record = @reflection.klass.send(:with_scope, :create => @scope[:create]) do
yield @reflection
end
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index fba0a2bfcc..7f28abf464 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -33,7 +33,7 @@ module ActiveRecord
private
def find_target
- with_scope(construct_scope) { @reflection.klass.find(:first) }
+ with_scope(@scope) { @reflection.klass.find(:first) }
end
end
end
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb
index cabb33c4a8..bd8e304e99 100644
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ b/activerecord/lib/active_record/associations/through_association_scope.rb
@@ -5,16 +5,20 @@ module ActiveRecord
protected
- def construct_scope
- { :create => construct_owner_attributes(@reflection),
- :find => { :conditions => construct_conditions,
- :joins => construct_joins,
- :include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
- :select => construct_select,
- :order => @reflection.options[:order],
- :limit => @reflection.options[:limit],
- :readonly => @reflection.options[:readonly],
- } }
+ def construct_find_scope
+ {
+ :conditions => construct_conditions,
+ :joins => construct_joins,
+ :include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
+ :select => construct_select,
+ :order => @reflection.options[:order],
+ :limit => @reflection.options[:limit],
+ :readonly => @reflection.options[:readonly]
+ }
+ end
+
+ def construct_create_scope
+ construct_owner_attributes(@reflection)
end
# Build SQL conditions from attributes, qualified by table name.
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 4a2c078e91..0b89a49896 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -322,8 +322,8 @@ module ActiveRecord
end
end
- # reconstruct the SQL queries now that we know the owner's id
- association.send(:construct_sql) if association.respond_to?(:construct_sql)
+ # reconstruct the scope now that we know the owner's id
+ association.send(:construct_scope) if association.respond_to?(:construct_scope)
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 053b796991..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
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 69bf2c0dcd..fc4d84a4f2 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
@@ -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|
+ row['unique'] != 0,
+ exec("PRAGMA index_info('#{row['name']}')").map { |col|
col['name']
})
end
@@ -202,17 +235,17 @@ module ActiveRecord
def primary_key(table_name) #:nodoc:
column = table_structure(table_name).find { |field|
- field['pk'].to_i == 1
+ field['pk'] == 1
}
column && column['name']
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,14 +313,13 @@ module ActiveRecord
end
protected
- def select(sql, name = nil) #:nodoc:
- execute(sql, name).map do |row|
- record = {}
- row.each do |key, value|
- record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String)
- end
- record
- end
+ def select(sql, name = nil, binds = []) #:nodoc:
+ result = exec(sql, name, binds)
+ columns = result.columns.map { |column|
+ column.sub(/^"?\w+"?\./, '')
+ }
+
+ result.rows.map { |row| Hash[columns.zip(row)] }
end
def table_structure(table_name)
@@ -367,11 +399,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/relation.rb b/activerecord/lib/active_record/relation.rb
index f129b54f9a..3b22be78cb 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?
@@ -319,8 +319,13 @@ module ActiveRecord
end
def where_values_hash
- Hash[@where_values.find_all {|w| w.respond_to?(:operator) && w.operator == :== }.map {|where|
- [where.operand1.name, where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2]
+ Hash[@where_values.find_all { |w|
+ w.respond_to?(:operator) && w.operator == :== && w.left.relation.name == table_name
+ }.map { |where|
+ [
+ where.left.name,
+ where.right.respond_to?(:value) ? where.right.value : where.right
+ ]
}]
end
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 64d2cb0203..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?
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 648a02f1cc..a61a3bd41c 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -28,17 +28,20 @@ module ActiveRecord
merged_wheres = @where_values + r.where_values
- # Remove duplicates, last one wins.
- seen = {}
- merged_wheres = merged_wheres.reverse.reject { |w|
- nuke = false
- if w.respond_to?(:operator) && w.operator == :==
- name = w.left.name
- nuke = seen[name]
- seen[name] = true
- end
- nuke
- }.reverse
+ unless @where_values.empty?
+ # Remove duplicates, last one wins.
+ seen = Hash.new { |h,table| h[table] = {} }
+ merged_wheres = merged_wheres.reverse.reject { |w|
+ nuke = false
+ if w.respond_to?(:operator) && w.operator == :==
+ name = w.left.name
+ table = w.left.relation.name
+ nuke = seen[table][name]
+ seen[table][name] = true
+ end
+ nuke
+ }.reverse
+ end
merged_relation.where_values = merged_wheres
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
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index f76a23a8ad..67bd8ec7e0 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -43,6 +43,64 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert @connection.active?
end
+ def test_bind_value_substitute
+ bind_param = @connection.substitute_for('foo', [])
+ assert_equal Arel.sql('?'), bind_param
+ end
+
+ def test_exec_no_binds
+ @connection.exec('drop table if exists ex')
+ @connection.exec(<<-eosql)
+ CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ `data` varchar(255))
+ eosql
+ result = @connection.exec('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
+
+ @connection.exec('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @connection.exec('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
+
+ def test_exec_with_binds
+ @connection.exec('drop table if exists ex')
+ @connection.exec(<<-eosql)
+ CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ `data` varchar(255))
+ eosql
+ @connection.exec('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @connection.exec(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
+
+ def test_exec_typecasts_bind_vals
+ @connection.exec('drop table if exists ex')
+ @connection.exec(<<-eosql)
+ CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ `data` varchar(255))
+ eosql
+ @connection.exec('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+
+ result = @connection.exec(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
+
# Test that MySQL allows multiple results for stored procedures
if Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
def test_multi_results
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 7b72151b57..b0fd2273df 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -5,6 +5,8 @@ module ActiveRecord
class PostgreSQLAdapterTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
+ @connection.exec('drop table if exists ex')
+ @connection.exec('create table ex(id serial primary key, data character varying(255))')
end
def test_table_alias_length
@@ -12,6 +14,55 @@ module ActiveRecord
@connection.table_alias_length
end
end
+
+ def test_exec_no_binds
+ result = @connection.exec('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
+
+ string = @connection.quote('foo')
+ @connection.exec("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ result = @connection.exec('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [['1', 'foo']], result.rows
+ end
+
+ def test_exec_with_binds
+ string = @connection.quote('foo')
+ @connection.exec("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ result = @connection.exec(
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]])
+
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [['1', 'foo']], result.rows
+ end
+
+ def test_exec_typecasts_bind_vals
+ string = @connection.quote('foo')
+ @connection.exec("INSERT INTO ex (id, data) VALUES (1, #{string})")
+
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+ result = @connection.exec(
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']])
+
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [['1', 'foo']], result.rows
+ end
+
+ def test_substitute_for
+ bind = @connection.substitute_for(nil, [])
+ assert_equal Arel.sql('$1'), bind
+
+ bind = @connection.substitute_for(nil, [nil])
+ assert_equal Arel.sql('$2'), bind
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index 6f372edc38..881631fb19 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -43,6 +43,36 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
end
end
+ def test_session_auth=
+ assert_raise(ActiveRecord::StatementInvalid) do
+ @connection.session_auth = 'DEFAULT'
+ @connection.execute "SELECT * FROM #{TABLE_NAME}"
+ end
+ end
+
+ def test_setting_auth_clears_stmt_cache
+ assert_nothing_raised do
+ set_session_auth
+ USERS.each do |u|
+ set_session_auth u
+ assert_equal u, @connection.exec("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name']
+ set_session_auth
+ end
+ end
+ end
+
+ def test_auth_with_bind
+ assert_nothing_raised do
+ set_session_auth
+ USERS.each do |u|
+ @connection.clear_cache!
+ set_session_auth u
+ assert_equal u, @connection.exec("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name']
+ set_session_auth
+ end
+ end
+ end
+
def test_schema_uniqueness
assert_nothing_raised do
set_session_auth
@@ -78,7 +108,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
private
def set_session_auth auth = nil
- @connection.execute "SET SESSION AUTHORIZATION #{auth || 'default'}"
+ @connection.session_auth = auth || 'default'
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 934cf72f72..973c51f3d7 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -3,6 +3,12 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class SQLite3AdapterTest < ActiveRecord::TestCase
+ def setup
+ @conn = Base.sqlite3_connection :database => ':memory:',
+ :adapter => 'sqlite3',
+ :timeout => 100
+ end
+
def test_connection_no_db
assert_raises(ArgumentError) do
Base.sqlite3_connection {}
@@ -38,18 +44,58 @@ module ActiveRecord
end
def test_connect
- conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 100
- assert conn, 'should have connection'
+ assert @conn, 'should have connection'
end
# sqlite3 defaults to UTF-8 encoding
def test_encoding
- conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 100
- assert_equal 'UTF-8', conn.encoding
+ assert_equal 'UTF-8', @conn.encoding
+ end
+
+ def test_bind_value_substitute
+ bind_param = @conn.substitute_for('foo', [])
+ assert_equal Arel.sql('?'), bind_param
+ end
+
+ def test_exec_no_binds
+ @conn.exec('create table ex(id int, data string)')
+ result = @conn.exec('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
+
+ @conn.exec('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @conn.exec('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
+
+ def test_exec_with_binds
+ @conn.exec('create table ex(id int, data string)')
+ @conn.exec('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @conn.exec(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
+
+ def test_exec_typecasts_bind_vals
+ @conn.exec('create table ex(id int, data string)')
+ @conn.exec('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
+
+ result = @conn.exec(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index cbaa4990f7..0fa4328826 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -81,6 +81,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key")
end
+ def test_eager_loading_with_primary_key_as_symbol
+ Firm.create("name" => "Apple")
+ Client.create("name" => "Citibank", :firm_name => "Apple")
+ citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key_symbols)
+ assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key_symbols")
+ end
+
def test_no_unexpected_aliasing
first_firm = companies(:first_firm)
another_firm = companies(:another_firm)
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index c7c32da9f3..271bb92ee8 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -3,6 +3,7 @@ require 'models/post'
require 'models/comment'
require 'models/author'
require 'models/categorization'
+require 'models/category'
require 'models/company'
require 'models/topic'
require 'models/reply'
@@ -45,6 +46,31 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').first
end
+ def test_cascaded_eager_association_loading_with_join_for_count
+ categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
+
+ assert_nothing_raised do
+ assert_equal 2, categories.count
+ assert_equal 2, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes
+ end
+ end
+
+ def test_cascaded_eager_association_loading_with_duplicated_includes
+ categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null")
+ assert_nothing_raised do
+ assert_equal 2, categories.count
+ assert_equal 2, categories.all.size
+ end
+ end
+
+ def test_cascaded_eager_association_loading_with_twice_includes_edge_cases
+ categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null")
+ assert_nothing_raised do
+ assert_equal 2, categories.count
+ assert_equal 2, categories.all.size
+ end
+ end
+
def test_eager_association_loading_with_join_for_count
authors = Author.joins(:special_posts).includes([:posts, :categorizations])
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 1750bf004a..ab9a65944f 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -103,7 +103,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
if current_adapter?(:MysqlAdapter)
def test_read_attributes_before_type_cast_on_boolean
bool = Boolean.create({ "value" => false })
- assert_equal "0", bool.reload.attributes_before_type_cast["value"]
+ assert_equal 0, bool.reload.attributes_before_type_cast["value"]
end
end
@@ -112,7 +112,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
developer = Developer.find(:first)
# Oracle adapter returns Time before type cast
unless current_adapter?(:OracleAdapter)
- assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"]
+ assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s
else
assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s(:db)
@@ -121,7 +121,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal developer.created_at, nil
developer.created_at = "2010-03-21 21:23:32"
- assert_equal developer.created_at_before_type_cast, "2010-03-21 21:23:32"
+ assert_equal developer.created_at_before_type_cast.to_s, "2010-03-21 21:23:32"
assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32")
end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 2d3047c875..f6ef155d66 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -55,6 +55,14 @@ ActiveRecord::Base.connection.class.class_eval do
end
alias_method_chain :execute, :query_record
+
+ def exec_with_query_record(sql, name = nil, binds = [], &block)
+ $queries_executed ||= []
+ $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
+ exec_without_query_record(sql, name, binds, &block)
+ end
+
+ alias_method_chain :exec, :query_record
end
ActiveRecord::Base.connection.class.class_eval {
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index f3d3d62830..0ffd0e2ab3 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -226,6 +226,12 @@ class MethodScopingTest < ActiveRecord::TestCase
assert Post.find(1).comments.include?(new_comment)
end
+ def test_scoped_create_with_join_and_merge
+ (Comment.where(:body => "but Who's Buying?").joins(:post) & Post.where(:body => 'Peace Sells...')).with_scope do
+ assert_equal({:body => "but Who's Buying?"}, Comment.scoped.scope_for_create)
+ end
+ end
+
def test_immutable_scope
options = { :conditions => "name = 'David'" }
Developer.send(:with_scope, :find => options) do
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 594db1d0ab..5bb21a54bd 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -22,6 +22,12 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_find_queries_with_cache_multi_record
+ Task.cache do
+ assert_queries(2) { Task.find(1); Task.find(1); Task.find(2) }
+ end
+ end
+
def test_count_queries_with_cache
Task.cache do
assert_queries(1) { Task.count; Task.count }
@@ -57,7 +63,7 @@ class QueryCacheTest < ActiveRecord::TestCase
# Oracle adapter returns count() as Fixnum or Float
if current_adapter?(:OracleAdapter)
assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
- elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' or current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter)
# Future versions of the sqlite3 adapter will return numeric
assert_instance_of Fixnum,
Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index b01a8bbef1..f4f3dc4d5a 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -19,6 +19,15 @@ class RelationTest < ActiveRecord::TestCase
fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
:taggings, :cars
+ def test_bind_values
+ relation = Post.scoped
+ assert_equal [], relation.bind_values
+
+ relation2 = relation.bind 'foo'
+ assert_equal %w{ foo }, relation2.bind_values
+ assert_equal [], relation.bind_values
+ end
+
def test_two_named_scopes_with_includes_should_not_drop_any_include
car = Car.incl_engines.incl_tyres.first
assert_no_queries { car.tyres.length }
@@ -491,6 +500,11 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_relation_merging_with_joins
+ comments = Comment.joins(:post).where(:body => 'Thank you for the welcome') & Post.where(:body => 'Such a lovely day')
+ assert_equal 1, comments.count
+ end
+
def test_count
posts = Post.scoped
diff --git a/activerecord/test/connections/native_oracle/connection.rb b/activerecord/test/connections/native_oracle/connection.rb
index 9a717018b2..c942036128 100644
--- a/activerecord/test/connections/native_oracle/connection.rb
+++ b/activerecord/test/connections/native_oracle/connection.rb
@@ -37,10 +37,10 @@ Course.establish_connection 'arunit2'
ActiveRecord::Base.connection.class.class_eval do
IGNORED_SELECT_SQL = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im]
- def select_with_query_record(sql, name = nil, return_column_names = false)
+ def select_with_query_record(sql, name = nil)
$queries_executed ||= []
$queries_executed << sql unless IGNORED_SELECT_SQL.any? { |r| sql =~ r }
- select_without_query_record(sql, name, return_column_names)
+ select_without_query_record(sql, name)
end
alias_method_chain :select, :query_record
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index be6dd71e3b..ee5f77b613 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -107,6 +107,7 @@ class Client < Company
belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1]
belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name"
+ belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name
belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true
# Record destruction so we can test whether firm.clients.clear has