Merge branch 'master' into sprockets
This commit is contained in:
commit
5df076ad09
3
Gemfile
3
Gemfile
|
@ -38,6 +38,9 @@ platforms :mri_19 do
|
|||
end
|
||||
|
||||
platforms :ruby do
|
||||
if ENV["RB_FSEVENT"]
|
||||
gem 'rb-fsevent'
|
||||
end
|
||||
gem 'json'
|
||||
gem 'yajl-ruby'
|
||||
gem "nokogiri", ">= 1.4.4"
|
||||
|
|
|
@ -14,7 +14,7 @@ module AbstractController
|
|||
# Override AbstractController::Base's process_action to run the
|
||||
# process_action callbacks around the normal behavior.
|
||||
def process_action(method_name, *args)
|
||||
run_callbacks(:process_action, method_name) do
|
||||
run_callbacks(:process_action, action_name) do
|
||||
super
|
||||
end
|
||||
end
|
||||
|
|
|
@ -107,7 +107,7 @@ module ActionDispatch
|
|||
if @options[:format] == false
|
||||
@options.delete(:format)
|
||||
path
|
||||
elsif path.include?(":format") || path.end_with?('/')
|
||||
elsif path.include?(":format") || path.end_with?('/') || path.match(/^\/?\*/)
|
||||
path
|
||||
else
|
||||
"#{path}(.:format)"
|
||||
|
@ -364,6 +364,13 @@ module ActionDispatch
|
|||
# match 'path' => 'c#a', :defaults => { :format => 'jpg' }
|
||||
#
|
||||
# See <tt>Scoping#defaults</tt> for its scope equivalent.
|
||||
#
|
||||
# [:anchor]
|
||||
# Boolean to anchor a #match pattern. Default is true. When set to
|
||||
# false, the pattern matches any request prefixed with the given path.
|
||||
#
|
||||
# # Matches any request starting with 'path'
|
||||
# match 'path' => 'c#a', :anchor => false
|
||||
def match(path, options=nil)
|
||||
mapping = Mapping.new(@set, @scope, path, options || {})
|
||||
app, conditions, requirements, defaults, as, anchor = mapping.to_route
|
||||
|
|
|
@ -46,6 +46,13 @@ module ActionDispatch
|
|||
mapper.match '/one/two/', :to => 'posts#index', :as => :main
|
||||
assert_equal '/one/two(.:format)', fakeset.conditions.first[:path_info]
|
||||
end
|
||||
|
||||
def test_map_wildcard
|
||||
fakeset = FakeSet.new
|
||||
mapper = Mapper.new fakeset
|
||||
mapper.match '/*path', :to => 'pages#show', :as => :page
|
||||
assert_equal '/*path', fakeset.conditions.first[:path_info]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -505,6 +505,21 @@ class FilterTest < ActionController::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
class ImplicitActionsController < ActionController::Base
|
||||
before_filter :find_only, :only => :edit
|
||||
before_filter :find_except, :except => :edit
|
||||
|
||||
private
|
||||
|
||||
def find_only
|
||||
@only = 'Only'
|
||||
end
|
||||
|
||||
def find_except
|
||||
@except = 'Except'
|
||||
end
|
||||
end
|
||||
|
||||
def test_sweeper_should_not_block_rendering
|
||||
response = test_process(SweeperTestController)
|
||||
assert_equal 'hello world', response.body
|
||||
|
@ -783,6 +798,18 @@ class FilterTest < ActionController::TestCase
|
|||
assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
|
||||
end
|
||||
|
||||
def test_filters_obey_only_and_except_for_implicit_actions
|
||||
test_process(ImplicitActionsController, 'show')
|
||||
assert_equal 'Except', assigns(:except)
|
||||
assert_nil assigns(:only)
|
||||
assert_equal 'show', response.body
|
||||
|
||||
test_process(ImplicitActionsController, 'edit')
|
||||
assert_equal 'Only', assigns(:only)
|
||||
assert_nil assigns(:except)
|
||||
assert_equal 'edit', response.body
|
||||
end
|
||||
|
||||
private
|
||||
def test_process(controller, action = "show")
|
||||
@controller = controller.is_a?(Class) ? controller.new : controller
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
edit
|
|
@ -0,0 +1 @@
|
|||
show
|
|
@ -106,8 +106,14 @@ module ActiveModel
|
|||
if block_given?
|
||||
sing.send :define_method, name, &block
|
||||
else
|
||||
value = value.to_s if value
|
||||
sing.send(:define_method, name) { value && value.dup }
|
||||
if name =~ /^[a-zA-Z_]\w*[!?=]?$/
|
||||
sing.class_eval <<-eorb, __FILE__, __LINE__ + 1
|
||||
def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end
|
||||
eorb
|
||||
else
|
||||
value = value.to_s if value
|
||||
sing.send(:define_method, name) { value }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,10 +9,6 @@ class ModelWithAttributes
|
|||
define_method(:bar) do
|
||||
'original bar'
|
||||
end
|
||||
|
||||
define_method(:zomg) do
|
||||
'original zomg'
|
||||
end
|
||||
end
|
||||
|
||||
def attributes
|
||||
|
@ -102,13 +98,6 @@ class AttributeMethodsTest < ActiveModel::TestCase
|
|||
assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
|
||||
end
|
||||
|
||||
def test_defined_methods_always_return_duped_string
|
||||
ModelWithAttributes.define_attr_method(:zomg, 'lol')
|
||||
assert_equal 'lol', ModelWithAttributes.zomg
|
||||
ModelWithAttributes.zomg << 'bbq'
|
||||
assert_equal 'lol', ModelWithAttributes.zomg
|
||||
end
|
||||
|
||||
test '#define_attr_method generates attribute method' do
|
||||
ModelWithAttributes.define_attr_method(:bar, 'bar')
|
||||
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
*Rails 3.1.0 (unreleased)*
|
||||
|
||||
* Associations with a :through option can now use *any* association as the
|
||||
through or source association, including other associations which have a
|
||||
:through option and has_and_belongs_to_many associations
|
||||
|
||||
[Jon Leighton]
|
||||
|
||||
* The configuration for the current database connection is now accessible via
|
||||
ActiveRecord::Base.connection_config. [fxn]
|
||||
|
||||
|
|
|
@ -52,14 +52,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection)
|
||||
through_reflection = reflection.through_reflection
|
||||
source_reflection = reflection.source_reflection
|
||||
super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
|
||||
|
@ -78,6 +70,12 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc
|
||||
def initialize(owner, reflection)
|
||||
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
|
||||
end
|
||||
end
|
||||
|
||||
class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection)
|
||||
super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
|
||||
|
@ -142,8 +140,11 @@ module ActiveRecord
|
|||
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
|
||||
end
|
||||
|
||||
autoload :Preloader, 'active_record/associations/preloader'
|
||||
autoload :JoinDependency, 'active_record/associations/join_dependency'
|
||||
autoload :Preloader, 'active_record/associations/preloader'
|
||||
autoload :JoinDependency, 'active_record/associations/join_dependency'
|
||||
autoload :AssociationScope, 'active_record/associations/association_scope'
|
||||
autoload :AliasTracker, 'active_record/associations/alias_tracker'
|
||||
autoload :JoinHelper, 'active_record/associations/join_helper'
|
||||
|
||||
# Clears out the association cache.
|
||||
def clear_association_cache #:nodoc:
|
||||
|
@ -548,6 +549,49 @@ module ActiveRecord
|
|||
# belongs_to :tag, :inverse_of => :taggings
|
||||
# end
|
||||
#
|
||||
# === Nested Associations
|
||||
#
|
||||
# You can actually specify *any* association with the <tt>:through</tt> option, including an
|
||||
# association which has a <tt>:through</tt> option itself. For example:
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# has_many :comments, :through => :posts
|
||||
# has_many :commenters, :through => :comments
|
||||
# end
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :comments
|
||||
# end
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# belongs_to :commenter
|
||||
# end
|
||||
#
|
||||
# @author = Author.first
|
||||
# @author.commenters # => People who commented on posts written by the author
|
||||
#
|
||||
# An equivalent way of setting up this association this would be:
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# has_many :commenters, :through => :posts
|
||||
# end
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :comments
|
||||
# has_many :commenters, :through => :comments
|
||||
# end
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# belongs_to :commenter
|
||||
# end
|
||||
#
|
||||
# When using nested association, you will not be able to modify the association because there
|
||||
# is not enough information to know what modification to make. For example, if you tried to
|
||||
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
|
||||
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
|
||||
#
|
||||
# === Polymorphic Associations
|
||||
#
|
||||
# Polymorphic associations on models are not restricted on what types of models they
|
||||
|
@ -1068,10 +1112,10 @@ module ActiveRecord
|
|||
# [:as]
|
||||
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
||||
# [:through]
|
||||
# Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>,
|
||||
# Specifies an association through which to perform the query. This can be any other type
|
||||
# of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
|
||||
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
|
||||
# source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>,
|
||||
# <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
|
||||
# source reflection.
|
||||
#
|
||||
# If the association on the join model is a +belongs_to+, the collection can be modified
|
||||
# and the records on the <tt>:through</tt> model will be automatically created and removed
|
||||
|
@ -1198,10 +1242,10 @@ module ActiveRecord
|
|||
# you want to do a join but not include the joined columns. Do not forget to include the
|
||||
# primary and foreign keys, otherwise it will raise an error.
|
||||
# [:through]
|
||||
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>
|
||||
# and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You
|
||||
# can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt>
|
||||
# association on the join model.
|
||||
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
|
||||
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
|
||||
# source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt>
|
||||
# or <tt>belongs_to</tt> association on the join model.
|
||||
# [:source]
|
||||
# Specifies the source association name used by <tt>has_one :through</tt> queries.
|
||||
# Only use it if the name cannot be inferred from the association.
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
require 'active_support/core_ext/string/conversions'
|
||||
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
# Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
|
||||
# ActiveRecord::Associations::ThroughAssociationScope
|
||||
class AliasTracker # :nodoc:
|
||||
attr_reader :aliases, :table_joins
|
||||
|
||||
# table_joins is an array of arel joins which might conflict with the aliases we assign here
|
||||
def initialize(table_joins = [])
|
||||
@aliases = Hash.new
|
||||
@table_joins = table_joins
|
||||
end
|
||||
|
||||
def aliased_table_for(table_name, aliased_name = nil)
|
||||
table_alias = aliased_name_for(table_name, aliased_name)
|
||||
|
||||
if table_alias == table_name
|
||||
Arel::Table.new(table_name)
|
||||
else
|
||||
Arel::Table.new(table_name).alias(table_alias)
|
||||
end
|
||||
end
|
||||
|
||||
def aliased_name_for(table_name, aliased_name = nil)
|
||||
aliased_name ||= table_name
|
||||
|
||||
initialize_count_for(table_name) if aliases[table_name].nil?
|
||||
|
||||
if aliases[table_name].zero?
|
||||
# If it's zero, we can have our table_name
|
||||
aliases[table_name] = 1
|
||||
table_name
|
||||
else
|
||||
# Otherwise, we need to use an alias
|
||||
aliased_name = connection.table_alias_for(aliased_name)
|
||||
|
||||
initialize_count_for(aliased_name) if aliases[aliased_name].nil?
|
||||
|
||||
# Update the count
|
||||
aliases[aliased_name] += 1
|
||||
|
||||
if aliases[aliased_name] > 1
|
||||
"#{truncate(aliased_name)}_#{aliases[aliased_name]}"
|
||||
else
|
||||
aliased_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pluralize(table_name)
|
||||
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_count_for(name)
|
||||
aliases[name] = 0
|
||||
|
||||
unless Arel::Table === table_joins
|
||||
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
|
||||
quoted_name = connection.quote_table_name(name).downcase
|
||||
|
||||
aliases[name] += table_joins.map { |join|
|
||||
# Table names + table aliases
|
||||
join.left.downcase.scan(
|
||||
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
|
||||
).size
|
||||
}.sum
|
||||
end
|
||||
|
||||
aliases[name]
|
||||
end
|
||||
|
||||
def truncate(name)
|
||||
name[0..connection.table_alias_length-3]
|
||||
end
|
||||
|
||||
def connection
|
||||
ActiveRecord::Base.connection
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -93,23 +93,9 @@ module ActiveRecord
|
|||
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
|
||||
# actually gets built.
|
||||
def construct_scope
|
||||
@association_scope = association_scope if klass
|
||||
end
|
||||
|
||||
def association_scope
|
||||
scope = klass.unscoped
|
||||
scope = scope.create_with(creation_attributes)
|
||||
scope = scope.apply_finder_options(options.slice(:readonly, :include))
|
||||
scope = scope.where(interpolate(options[:conditions]))
|
||||
if select = select_value
|
||||
scope = scope.select(select)
|
||||
if klass
|
||||
@association_scope = AssociationScope.new(self).scope
|
||||
end
|
||||
scope = scope.extending(*Array.wrap(options[:extend]))
|
||||
scope.where(construct_owner_conditions)
|
||||
end
|
||||
|
||||
def aliased_table
|
||||
klass.arel_table
|
||||
end
|
||||
|
||||
# Set the inverse association, if possible
|
||||
|
@ -174,42 +160,24 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def select_value
|
||||
options[:select]
|
||||
end
|
||||
|
||||
# Implemented by (some) subclasses
|
||||
def creation_attributes
|
||||
{ }
|
||||
end
|
||||
|
||||
# Returns a hash linking the owner to the association represented by the reflection
|
||||
def construct_owner_attributes(reflection = reflection)
|
||||
attributes = {}
|
||||
if reflection.macro == :belongs_to
|
||||
attributes[reflection.association_primary_key] = owner[reflection.foreign_key]
|
||||
else
|
||||
|
||||
if [:has_one, :has_many].include?(reflection.macro) && !options[:through]
|
||||
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
|
||||
|
||||
if reflection.options[:as]
|
||||
attributes[reflection.type] = owner.class.base_class.name
|
||||
end
|
||||
end
|
||||
attributes
|
||||
end
|
||||
|
||||
# Builds an array of arel nodes from the owner attributes hash
|
||||
def construct_owner_conditions(table = aliased_table, reflection = reflection)
|
||||
conditions = construct_owner_attributes(reflection).map do |attr, value|
|
||||
table[attr].eq(value)
|
||||
end
|
||||
table.create_and(conditions)
|
||||
attributes
|
||||
end
|
||||
|
||||
# Sets the owner attributes on the given record
|
||||
def set_owner_attributes(record)
|
||||
if owner.persisted?
|
||||
construct_owner_attributes.each { |key, value| record[key] = value }
|
||||
creation_attributes.each { |key, value| record[key] = value }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
class AssociationScope #:nodoc:
|
||||
include JoinHelper
|
||||
|
||||
attr_reader :association, :alias_tracker
|
||||
|
||||
delegate :klass, :owner, :reflection, :interpolate, :to => :association
|
||||
delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection
|
||||
|
||||
def initialize(association)
|
||||
@association = association
|
||||
@alias_tracker = AliasTracker.new
|
||||
end
|
||||
|
||||
def scope
|
||||
scope = klass.unscoped
|
||||
scope = scope.extending(*Array.wrap(options[:extend]))
|
||||
|
||||
# It's okay to just apply all these like this. The options will only be present if the
|
||||
# association supports that option; this is enforced by the association builder.
|
||||
scope = scope.apply_finder_options(options.slice(
|
||||
:readonly, :include, :order, :limit, :joins, :group, :having, :offset))
|
||||
|
||||
if options[:through] && !options[:include]
|
||||
scope = scope.includes(source_options[:include])
|
||||
end
|
||||
|
||||
if select = select_value
|
||||
scope = scope.select(select)
|
||||
end
|
||||
|
||||
add_constraints(scope)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select_value
|
||||
select_value = options[:select]
|
||||
|
||||
if reflection.collection?
|
||||
select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
|
||||
end
|
||||
|
||||
if reflection.macro == :has_and_belongs_to_many
|
||||
select_value ||= reflection.klass.arel_table[Arel.star]
|
||||
end
|
||||
|
||||
select_value
|
||||
end
|
||||
|
||||
def add_constraints(scope)
|
||||
tables = construct_tables
|
||||
|
||||
chain.each_with_index do |reflection, i|
|
||||
table, foreign_table = tables.shift, tables.first
|
||||
|
||||
if reflection.source_macro == :has_and_belongs_to_many
|
||||
join_table = tables.shift
|
||||
|
||||
scope = scope.joins(join(
|
||||
join_table,
|
||||
table[reflection.active_record_primary_key].
|
||||
eq(join_table[reflection.association_foreign_key])
|
||||
))
|
||||
|
||||
table, foreign_table = join_table, tables.first
|
||||
end
|
||||
|
||||
if reflection.source_macro == :belongs_to
|
||||
key = reflection.association_primary_key
|
||||
foreign_key = reflection.foreign_key
|
||||
else
|
||||
key = reflection.foreign_key
|
||||
foreign_key = reflection.active_record_primary_key
|
||||
end
|
||||
|
||||
if reflection == chain.last
|
||||
scope = scope.where(table[key].eq(owner[foreign_key]))
|
||||
|
||||
conditions[i].each do |condition|
|
||||
if options[:through] && condition.is_a?(Hash)
|
||||
condition = { table.name => condition }
|
||||
end
|
||||
|
||||
scope = scope.where(interpolate(condition))
|
||||
end
|
||||
else
|
||||
constraint = table[key].eq(foreign_table[foreign_key])
|
||||
join = join(foreign_table, constraint)
|
||||
|
||||
scope = scope.joins(join)
|
||||
|
||||
unless conditions[i].empty?
|
||||
scope = scope.where(sanitize(conditions[i], table))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
def alias_suffix
|
||||
reflection.name
|
||||
end
|
||||
|
||||
def table_name_for(reflection)
|
||||
if reflection == self.reflection
|
||||
# If this is a polymorphic belongs_to, we want to get the klass from the
|
||||
# association because it depends on the polymorphic_type attribute of
|
||||
# the owner
|
||||
klass.table_name
|
||||
else
|
||||
reflection.table_name
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,24 +7,24 @@ module ActiveRecord::Associations::Builder
|
|||
def build
|
||||
reflection = super
|
||||
check_validity(reflection)
|
||||
redefine_destroy
|
||||
define_after_destroy_method
|
||||
reflection
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redefine_destroy
|
||||
# Don't use a before_destroy callback since users' before_destroy
|
||||
# callbacks will be executed after the association is wiped out.
|
||||
def define_after_destroy_method
|
||||
name = self.name
|
||||
model.send(:include, Module.new {
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
def destroy # def destroy
|
||||
super # super
|
||||
#{name}.clear # posts.clear
|
||||
end # end
|
||||
RUBY
|
||||
})
|
||||
model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
|
||||
def #{after_destroy_method_name}
|
||||
association(#{name.to_sym.inspect}).delete_all
|
||||
end
|
||||
eoruby
|
||||
model.after_destroy after_destroy_method_name
|
||||
end
|
||||
|
||||
def after_destroy_method_name
|
||||
"has_and_belongs_to_many_after_destroy_for_#{name}"
|
||||
end
|
||||
|
||||
# TODO: These checks should probably be moved into the Reflection, and we should not be
|
||||
|
|
|
@ -331,11 +331,6 @@ module ActiveRecord
|
|||
@scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
|
||||
end
|
||||
|
||||
def association_scope
|
||||
options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset)
|
||||
super.apply_finder_options(options)
|
||||
end
|
||||
|
||||
def load_target
|
||||
if find_target?
|
||||
targets = []
|
||||
|
@ -373,14 +368,6 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
|
||||
def select_value
|
||||
super || uniq_select_value
|
||||
end
|
||||
|
||||
def uniq_select_value
|
||||
options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
|
||||
end
|
||||
|
||||
def custom_counter_sql
|
||||
if options[:counter_sql]
|
||||
interpolate(options[:counter_sql])
|
||||
|
|
|
@ -26,10 +26,6 @@ module ActiveRecord
|
|||
record
|
||||
end
|
||||
|
||||
def association_scope
|
||||
super.joins(construct_joins)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def count_records
|
||||
|
@ -48,24 +44,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def construct_joins
|
||||
right = join_table
|
||||
left = reflection.klass.arel_table
|
||||
|
||||
condition = left[reflection.klass.primary_key].eq(
|
||||
right[reflection.association_foreign_key])
|
||||
|
||||
right.create_join(right, right.create_on(condition))
|
||||
end
|
||||
|
||||
def construct_owner_conditions
|
||||
super(join_table)
|
||||
end
|
||||
|
||||
def select_value
|
||||
super || reflection.klass.arel_table[Arel.star]
|
||||
end
|
||||
|
||||
def invertible_for?(record)
|
||||
false
|
||||
end
|
||||
|
|
|
@ -94,8 +94,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
alias creation_attributes construct_owner_attributes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,7 +34,9 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def insert_record(record, validate = true)
|
||||
ensure_not_nested
|
||||
return if record.new_record? && !record.save(:validate => validate)
|
||||
|
||||
through_record(record).save!
|
||||
update_counter(1)
|
||||
record
|
||||
|
@ -59,6 +61,8 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def build_record(attributes)
|
||||
ensure_not_nested
|
||||
|
||||
record = super(attributes)
|
||||
|
||||
inverse = source_reflection.inverse_of
|
||||
|
@ -93,6 +97,8 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def delete_records(records, method)
|
||||
ensure_not_nested
|
||||
|
||||
through = owner.association(through_reflection.name)
|
||||
scope = through.scoped.where(construct_join_attributes(*records))
|
||||
|
||||
|
|
|
@ -39,14 +39,8 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def association_scope
|
||||
super.order(options[:order])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
alias creation_attributes construct_owner_attributes
|
||||
|
||||
# The reason that the save param for replace is false, if for create (not just build),
|
||||
# is because the setting of the foreign keys is actually handled by the scoping when
|
||||
# the record is instantiated, and so they are set straight away and do not need to be
|
||||
|
|
|
@ -12,6 +12,8 @@ module ActiveRecord
|
|||
private
|
||||
|
||||
def create_through_record(record)
|
||||
ensure_not_nested
|
||||
|
||||
through_proxy = owner.association(through_reflection.name)
|
||||
through_record = through_proxy.send(:load_target)
|
||||
|
||||
|
|
|
@ -5,18 +5,16 @@ module ActiveRecord
|
|||
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
|
||||
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
|
||||
|
||||
attr_reader :join_parts, :reflections, :table_aliases, :active_record
|
||||
attr_reader :join_parts, :reflections, :alias_tracker, :active_record
|
||||
|
||||
def initialize(base, associations, joins)
|
||||
@active_record = base
|
||||
@table_joins = joins
|
||||
@join_parts = [JoinBase.new(base)]
|
||||
@associations = {}
|
||||
@reflections = []
|
||||
@table_aliases = Hash.new do |h,name|
|
||||
h[name] = count_aliases_from_table_joins(name.downcase)
|
||||
end
|
||||
@table_aliases[base.table_name] = 1
|
||||
@active_record = base
|
||||
@table_joins = joins
|
||||
@join_parts = [JoinBase.new(base)]
|
||||
@associations = {}
|
||||
@reflections = []
|
||||
@alias_tracker = AliasTracker.new(joins)
|
||||
@alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
|
||||
build(associations)
|
||||
end
|
||||
|
||||
|
@ -45,20 +43,6 @@ module ActiveRecord
|
|||
}.flatten
|
||||
end
|
||||
|
||||
def count_aliases_from_table_joins(name)
|
||||
return 0 if Arel::Table === @table_joins
|
||||
|
||||
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
|
||||
quoted_name = active_record.connection.quote_table_name(name).downcase
|
||||
|
||||
@table_joins.map { |join|
|
||||
# Table names + table aliases
|
||||
join.left.downcase.scan(
|
||||
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
|
||||
).size
|
||||
}.sum
|
||||
end
|
||||
|
||||
def instantiate(rows)
|
||||
primary_key = join_base.aliased_primary_key
|
||||
parents = {}
|
||||
|
|
|
@ -2,6 +2,8 @@ module ActiveRecord
|
|||
module Associations
|
||||
class JoinDependency # :nodoc:
|
||||
class JoinAssociation < JoinPart # :nodoc:
|
||||
include JoinHelper
|
||||
|
||||
# The reflection of the association represented
|
||||
attr_reader :reflection
|
||||
|
||||
|
@ -18,10 +20,15 @@ module ActiveRecord
|
|||
attr_accessor :join_type
|
||||
|
||||
# These implement abstract methods from the superclass
|
||||
attr_reader :aliased_prefix, :aliased_table_name
|
||||
attr_reader :aliased_prefix
|
||||
|
||||
delegate :options, :through_reflection, :source_reflection, :to => :reflection
|
||||
attr_reader :tables
|
||||
|
||||
delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
|
||||
delegate :table, :table_name, :to => :parent, :prefix => :parent
|
||||
delegate :alias_tracker, :to => :join_dependency
|
||||
|
||||
alias :alias_suffix :parent_table_name
|
||||
|
||||
def initialize(reflection, join_dependency, parent = nil)
|
||||
reflection.check_validity!
|
||||
|
@ -37,14 +44,7 @@ module ActiveRecord
|
|||
@parent = parent
|
||||
@join_type = Arel::InnerJoin
|
||||
@aliased_prefix = "t#{ join_dependency.join_parts.size }"
|
||||
|
||||
# This must be done eagerly upon initialisation because the alias which is produced
|
||||
# depends on the state of the join dependency, but we want it to work the same way
|
||||
# every time.
|
||||
allocate_aliases
|
||||
@table = Arel::Table.new(
|
||||
table_name, :as => aliased_table_name, :engine => arel_engine
|
||||
)
|
||||
@tables = construct_tables.reverse
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
|
@ -60,7 +60,55 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def join_to(relation)
|
||||
send("join_#{reflection.macro}_to", relation)
|
||||
tables = @tables.dup
|
||||
foreign_table = parent_table
|
||||
|
||||
# The chain starts with the target table, but we want to end with it here (makes
|
||||
# more sense in this context), so we reverse
|
||||
chain.reverse.each_with_index do |reflection, i|
|
||||
table = tables.shift
|
||||
|
||||
case reflection.source_macro
|
||||
when :belongs_to
|
||||
key = reflection.association_primary_key
|
||||
foreign_key = reflection.foreign_key
|
||||
when :has_and_belongs_to_many
|
||||
# Join the join table first...
|
||||
relation.from(join(
|
||||
table,
|
||||
table[reflection.foreign_key].
|
||||
eq(foreign_table[reflection.active_record_primary_key])
|
||||
))
|
||||
|
||||
foreign_table, table = table, tables.shift
|
||||
|
||||
key = reflection.association_primary_key
|
||||
foreign_key = reflection.association_foreign_key
|
||||
else
|
||||
key = reflection.foreign_key
|
||||
foreign_key = reflection.active_record_primary_key
|
||||
end
|
||||
|
||||
constraint = table[key].eq(foreign_table[foreign_key])
|
||||
|
||||
if reflection.klass.finder_needs_type_condition?
|
||||
constraint = table.create_and([
|
||||
constraint,
|
||||
reflection.klass.send(:type_condition, table)
|
||||
])
|
||||
end
|
||||
|
||||
relation.from(join(table, constraint))
|
||||
|
||||
unless conditions[i].empty?
|
||||
relation.where(sanitize(conditions[i], table))
|
||||
end
|
||||
|
||||
# The current table in this iteration becomes the foreign table in the next
|
||||
foreign_table = table
|
||||
end
|
||||
|
||||
relation
|
||||
end
|
||||
|
||||
def join_relation(joining_relation)
|
||||
|
@ -68,211 +116,28 @@ module ActiveRecord
|
|||
joining_relation.joins(self)
|
||||
end
|
||||
|
||||
attr_reader :table
|
||||
# More semantic name given we are talking about associations
|
||||
alias_method :target_table, :table
|
||||
|
||||
protected
|
||||
|
||||
def aliased_table_name_for(name, suffix = nil)
|
||||
aliases = @join_dependency.table_aliases
|
||||
|
||||
if aliases[name] != 0 # We need an alias
|
||||
connection = active_record.connection
|
||||
|
||||
name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
||||
aliases[name] += 1
|
||||
name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
|
||||
else
|
||||
aliases[name] += 1
|
||||
end
|
||||
|
||||
name
|
||||
def table
|
||||
tables.last
|
||||
end
|
||||
|
||||
def pluralize(table_name)
|
||||
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
|
||||
def aliased_table_name
|
||||
table.table_alias || table.name
|
||||
end
|
||||
|
||||
def conditions
|
||||
@conditions ||= reflection.conditions.reverse
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allocate_aliases
|
||||
@aliased_table_name = aliased_table_name_for(table_name)
|
||||
|
||||
if reflection.macro == :has_and_belongs_to_many
|
||||
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
|
||||
elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
|
||||
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
|
||||
end
|
||||
end
|
||||
|
||||
def process_conditions(conditions, table_name)
|
||||
def interpolate(conditions)
|
||||
if conditions.respond_to?(:to_proc)
|
||||
conditions = instance_eval(&conditions)
|
||||
end
|
||||
|
||||
Arel.sql(sanitize_sql(conditions, table_name))
|
||||
end
|
||||
|
||||
def sanitize_sql(condition, table_name)
|
||||
active_record.send(:sanitize_sql, condition, table_name)
|
||||
end
|
||||
|
||||
def join_target_table(relation, condition)
|
||||
conditions = [condition]
|
||||
|
||||
# If the target table is an STI model then we must be sure to only include records of
|
||||
# its type and its sub-types.
|
||||
unless active_record.descends_from_active_record?
|
||||
sti_column = target_table[active_record.inheritance_column]
|
||||
subclasses = active_record.descendants
|
||||
sti_condition = sti_column.eq(active_record.sti_name)
|
||||
|
||||
conditions << subclasses.inject(sti_condition) { |attr,subclass|
|
||||
attr.or(sti_column.eq(subclass.sti_name))
|
||||
}
|
||||
end
|
||||
|
||||
# If the reflection has conditions, add them
|
||||
if options[:conditions]
|
||||
conditions << process_conditions(options[:conditions], aliased_table_name)
|
||||
end
|
||||
|
||||
ands = relation.create_and(conditions)
|
||||
|
||||
join = relation.create_join(
|
||||
target_table,
|
||||
relation.create_on(ands),
|
||||
join_type)
|
||||
|
||||
relation.from join
|
||||
end
|
||||
|
||||
def join_has_and_belongs_to_many_to(relation)
|
||||
join_table = Arel::Table.new(
|
||||
options[:join_table]
|
||||
).alias(@aliased_join_table_name)
|
||||
|
||||
fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
|
||||
klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
|
||||
|
||||
relation = relation.join(join_table, join_type)
|
||||
relation = relation.on(
|
||||
join_table[fk].
|
||||
eq(parent_table[reflection.active_record.primary_key])
|
||||
)
|
||||
|
||||
join_target_table(
|
||||
relation,
|
||||
target_table[reflection.klass.primary_key].
|
||||
eq(join_table[klass_fk])
|
||||
)
|
||||
end
|
||||
|
||||
def join_has_many_to(relation)
|
||||
if reflection.options[:through]
|
||||
join_has_many_through_to(relation)
|
||||
elsif reflection.options[:as]
|
||||
join_has_many_polymorphic_to(relation)
|
||||
instance_eval(&conditions)
|
||||
else
|
||||
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
|
||||
primary_key = options[:primary_key] || parent.primary_key
|
||||
|
||||
join_target_table(
|
||||
relation,
|
||||
target_table[foreign_key].
|
||||
eq(parent_table[primary_key])
|
||||
)
|
||||
conditions
|
||||
end
|
||||
end
|
||||
alias :join_has_one_to :join_has_many_to
|
||||
|
||||
def join_has_many_through_to(relation)
|
||||
join_table = Arel::Table.new(
|
||||
through_reflection.klass.table_name
|
||||
).alias @aliased_join_table_name
|
||||
|
||||
jt_conditions = []
|
||||
first_key = second_key = nil
|
||||
|
||||
if through_reflection.macro == :belongs_to
|
||||
jt_primary_key = through_reflection.foreign_key
|
||||
jt_foreign_key = through_reflection.association_primary_key
|
||||
else
|
||||
jt_primary_key = through_reflection.active_record_primary_key
|
||||
jt_foreign_key = through_reflection.foreign_key
|
||||
|
||||
if through_reflection.options[:as] # has_many :through against a polymorphic join
|
||||
jt_conditions <<
|
||||
join_table["#{through_reflection.options[:as]}_type"].
|
||||
eq(parent.active_record.base_class.name)
|
||||
end
|
||||
end
|
||||
|
||||
case source_reflection.macro
|
||||
when :has_many
|
||||
second_key = options[:foreign_key] || primary_key
|
||||
|
||||
if source_reflection.options[:as]
|
||||
first_key = "#{source_reflection.options[:as]}_id"
|
||||
else
|
||||
first_key = through_reflection.klass.base_class.to_s.foreign_key
|
||||
end
|
||||
|
||||
unless through_reflection.klass.descends_from_active_record?
|
||||
jt_conditions <<
|
||||
join_table[through_reflection.active_record.inheritance_column].
|
||||
eq(through_reflection.klass.sti_name)
|
||||
end
|
||||
when :belongs_to
|
||||
first_key = primary_key
|
||||
|
||||
if reflection.options[:source_type]
|
||||
second_key = source_reflection.association_foreign_key
|
||||
|
||||
jt_conditions <<
|
||||
join_table[reflection.source_reflection.foreign_type].
|
||||
eq(reflection.options[:source_type])
|
||||
else
|
||||
second_key = source_reflection.foreign_key
|
||||
end
|
||||
end
|
||||
|
||||
jt_conditions <<
|
||||
parent_table[jt_primary_key].
|
||||
eq(join_table[jt_foreign_key])
|
||||
|
||||
if through_reflection.options[:conditions]
|
||||
jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
|
||||
end
|
||||
|
||||
relation = relation.join(join_table, join_type).on(*jt_conditions)
|
||||
|
||||
join_target_table(
|
||||
relation,
|
||||
target_table[first_key].eq(join_table[second_key])
|
||||
)
|
||||
end
|
||||
|
||||
def join_has_many_polymorphic_to(relation)
|
||||
join_target_table(
|
||||
relation,
|
||||
target_table["#{reflection.options[:as]}_id"].
|
||||
eq(parent_table[parent.primary_key]).and(
|
||||
target_table["#{reflection.options[:as]}_type"].
|
||||
eq(parent.active_record.base_class.name))
|
||||
)
|
||||
end
|
||||
|
||||
def join_belongs_to_to(relation)
|
||||
foreign_key = options[:foreign_key] || reflection.foreign_key
|
||||
primary_key = options[:primary_key] || reflection.klass.primary_key
|
||||
|
||||
join_target_table(
|
||||
relation,
|
||||
target_table[primary_key].eq(parent_table[foreign_key])
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
# Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope
|
||||
module JoinHelper #:nodoc:
|
||||
|
||||
def join_type
|
||||
Arel::InnerJoin
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def construct_tables
|
||||
tables = []
|
||||
chain.each do |reflection|
|
||||
tables << alias_tracker.aliased_table_for(
|
||||
table_name_for(reflection),
|
||||
table_alias_for(reflection, reflection != self.reflection)
|
||||
)
|
||||
|
||||
if reflection.source_macro == :has_and_belongs_to_many
|
||||
tables << alias_tracker.aliased_table_for(
|
||||
(reflection.source_reflection || reflection).options[:join_table],
|
||||
table_alias_for(reflection, true)
|
||||
)
|
||||
end
|
||||
end
|
||||
tables
|
||||
end
|
||||
|
||||
def table_name_for(reflection)
|
||||
reflection.table_name
|
||||
end
|
||||
|
||||
def table_alias_for(reflection, join = false)
|
||||
name = alias_tracker.pluralize(reflection.name)
|
||||
name << "_#{alias_suffix}"
|
||||
name << "_join" if join
|
||||
name
|
||||
end
|
||||
|
||||
def join(table, constraint)
|
||||
table.create_join(table, table.create_on(constraint), join_type)
|
||||
end
|
||||
|
||||
def sanitize(conditions, table)
|
||||
conditions = conditions.map do |condition|
|
||||
condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name)
|
||||
condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
|
||||
condition
|
||||
end
|
||||
|
||||
conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,8 +19,9 @@ module ActiveRecord
|
|||
source_reflection.name, options
|
||||
).run
|
||||
|
||||
through_records.each do |owner, owner_through_records|
|
||||
owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten!
|
||||
through_records.each do |owner, records|
|
||||
records.map! { |r| r.send(source_reflection.name) }.flatten!
|
||||
records.compact!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,79 +3,24 @@ module ActiveRecord
|
|||
module Associations
|
||||
module ThroughAssociation #:nodoc:
|
||||
|
||||
delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection
|
||||
delegate :source_reflection, :through_reflection, :chain, :to => :reflection
|
||||
|
||||
protected
|
||||
|
||||
# We merge in these scopes for two reasons:
|
||||
#
|
||||
# 1. To get the default_scope conditions for any of the other reflections in the chain
|
||||
# 2. To get the type conditions for any STI models in the chain
|
||||
def target_scope
|
||||
super.merge(through_reflection.klass.scoped)
|
||||
end
|
||||
|
||||
def association_scope
|
||||
scope = super.joins(construct_joins)
|
||||
scope = add_conditions(scope)
|
||||
unless options[:include]
|
||||
scope = scope.includes(source_options[:include])
|
||||
scope = super
|
||||
chain[1..-1].each do |reflection|
|
||||
scope = scope.merge(reflection.klass.scoped)
|
||||
end
|
||||
scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This scope affects the creation of the associated records (not the join records). At the
|
||||
# moment we only support creating on a :through association when the source reflection is a
|
||||
# belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
|
||||
# this scope has can legitimately be empty.
|
||||
def creation_attributes
|
||||
{ }
|
||||
end
|
||||
|
||||
def aliased_through_table
|
||||
name = through_reflection.table_name
|
||||
|
||||
reflection.table_name == name ?
|
||||
through_reflection.klass.arel_table.alias(name + "_join") :
|
||||
through_reflection.klass.arel_table
|
||||
end
|
||||
|
||||
def construct_owner_conditions
|
||||
super(aliased_through_table, through_reflection)
|
||||
end
|
||||
|
||||
def construct_joins
|
||||
right = aliased_through_table
|
||||
left = reflection.klass.arel_table
|
||||
|
||||
conditions = []
|
||||
|
||||
if source_reflection.macro == :belongs_to
|
||||
reflection_primary_key = source_reflection.association_primary_key
|
||||
source_primary_key = source_reflection.foreign_key
|
||||
|
||||
if options[:source_type]
|
||||
column = source_reflection.foreign_type
|
||||
conditions <<
|
||||
right[column].eq(options[:source_type])
|
||||
end
|
||||
else
|
||||
reflection_primary_key = source_reflection.foreign_key
|
||||
source_primary_key = source_reflection.active_record_primary_key
|
||||
|
||||
if source_options[:as]
|
||||
column = "#{source_options[:as]}_type"
|
||||
conditions <<
|
||||
left[column].eq(through_reflection.klass.name)
|
||||
end
|
||||
end
|
||||
|
||||
conditions <<
|
||||
left[reflection_primary_key].eq(right[source_primary_key])
|
||||
|
||||
right.create_join(
|
||||
right,
|
||||
right.create_on(right.create_and(conditions)))
|
||||
end
|
||||
|
||||
# Construct attributes for :through pointing to owner and associate. This is used by the
|
||||
# methods which create and delete records on the association.
|
||||
#
|
||||
|
@ -112,37 +57,8 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# The reason that we are operating directly on the scope here (rather than passing
|
||||
# back some arel conditions to be added to the scope) is because scope.where([x, y])
|
||||
# has a different meaning to scope.where(x).where(y) - the first version might
|
||||
# perform some substitution if x is a string.
|
||||
def add_conditions(scope)
|
||||
unless through_reflection.klass.descends_from_active_record?
|
||||
scope = scope.where(through_reflection.klass.send(:type_condition))
|
||||
end
|
||||
|
||||
scope = scope.where(interpolate(source_options[:conditions]))
|
||||
scope.where(through_conditions)
|
||||
end
|
||||
|
||||
# If there is a hash of conditions then we make sure the keys are scoped to the
|
||||
# through table name if left ambiguous.
|
||||
def through_conditions
|
||||
conditions = interpolate(through_options[:conditions])
|
||||
|
||||
if conditions.is_a?(Hash)
|
||||
Hash[conditions.map { |key, value|
|
||||
unless value.is_a?(Hash) || key.to_s.include?('.')
|
||||
key = aliased_through_table.name + '.' + key.to_s
|
||||
end
|
||||
|
||||
[key, value]
|
||||
}]
|
||||
else
|
||||
conditions
|
||||
end
|
||||
end
|
||||
|
||||
# Note: this does not capture all cases, for example it would be crazy to try to
|
||||
# properly support stale-checking for nested associations.
|
||||
def stale_state
|
||||
if through_reflection.macro == :belongs_to
|
||||
owner[through_reflection.foreign_key].to_s
|
||||
|
@ -153,6 +69,12 @@ module ActiveRecord
|
|||
through_reflection.macro == :belongs_to &&
|
||||
!owner[through_reflection.foreign_key].nil?
|
||||
end
|
||||
|
||||
def ensure_not_nested
|
||||
if reflection.nested?
|
||||
raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,7 +70,14 @@ module ActiveRecord
|
|||
if cache_attribute?(attr_name)
|
||||
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
||||
end
|
||||
generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__)
|
||||
if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/
|
||||
generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__)
|
||||
else
|
||||
generated_attribute_methods.module_eval do
|
||||
define_method("_#{symbol}") { eval(access_code) }
|
||||
alias_method(symbol, "_#{symbol}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@ module ActiveRecord
|
|||
def define_method_attribute(attr_name)
|
||||
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
||||
method_body, line = <<-EOV, __LINE__ + 1
|
||||
def _#{attr_name}(reload = false)
|
||||
def _#{attr_name}
|
||||
cached = @attributes_cache['#{attr_name}']
|
||||
return cached if cached && !reload
|
||||
return cached if cached
|
||||
time = _read_attribute('#{attr_name}')
|
||||
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
||||
end
|
||||
|
@ -41,12 +41,13 @@ module ActiveRecord
|
|||
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
||||
method_body, line = <<-EOV, __LINE__ + 1
|
||||
def #{attr_name}=(original_time)
|
||||
time = original_time.dup unless original_time.nil?
|
||||
time = original_time
|
||||
unless time.acts_like?(:time)
|
||||
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
|
||||
end
|
||||
time = time.in_time_zone rescue nil if time
|
||||
write_attribute(:#{attr_name}, (time || original_time))
|
||||
write_attribute(:#{attr_name}, original_time)
|
||||
@attributes_cache["#{attr_name}"] = time
|
||||
end
|
||||
EOV
|
||||
generated_attribute_methods.module_eval(method_body, __FILE__, line)
|
||||
|
|
|
@ -10,7 +10,13 @@ module ActiveRecord
|
|||
module ClassMethods
|
||||
protected
|
||||
def define_method_attribute=(attr_name)
|
||||
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
|
||||
if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/
|
||||
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
|
||||
else
|
||||
generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
|
||||
write_attribute(attr_name, new_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -973,8 +973,8 @@ module ActiveRecord #:nodoc:
|
|||
relation
|
||||
end
|
||||
|
||||
def type_condition
|
||||
sti_column = arel_table[inheritance_column.to_sym]
|
||||
def type_condition(table = arel_table)
|
||||
sti_column = table[inheritance_column.to_sym]
|
||||
sti_names = ([self] + descendants).map { |model| model.sti_name }
|
||||
|
||||
sti_column.in(sti_names)
|
||||
|
@ -995,7 +995,7 @@ module ActiveRecord #:nodoc:
|
|||
if parent < ActiveRecord::Base && !parent.abstract_class?
|
||||
contained = parent.table_name
|
||||
contained = contained.singularize if parent.pluralize_table_names
|
||||
contained << '_'
|
||||
contained += '_'
|
||||
end
|
||||
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
|
||||
else
|
||||
|
|
|
@ -237,7 +237,6 @@ module ActiveRecord
|
|||
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
|
||||
# generates
|
||||
# SELECT * FROM suppliers LIMIT 10 OFFSET 50
|
||||
|
||||
def add_limit_offset!(sql, options)
|
||||
if limit = options[:limit]
|
||||
sql << " LIMIT #{sanitize_limit(limit)}"
|
||||
|
@ -272,6 +271,10 @@ module ActiveRecord
|
|||
execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
|
||||
end
|
||||
|
||||
def null_insert_value
|
||||
Arel.sql 'DEFAULT'
|
||||
end
|
||||
|
||||
def empty_insert_statement_value
|
||||
"VALUES(DEFAULT)"
|
||||
end
|
||||
|
|
|
@ -279,12 +279,11 @@ module ActiveRecord
|
|||
raise NotImplementedError, "change_column is not implemented"
|
||||
end
|
||||
|
||||
# Sets a new default value for a column. If you want to set the default
|
||||
# value to +NULL+, you are out of luck. You need to
|
||||
# DatabaseStatements#execute the appropriate SQL statement yourself.
|
||||
# Sets a new default value for a column.
|
||||
# ===== Examples
|
||||
# change_column_default(:suppliers, :qualification, 'new')
|
||||
# change_column_default(:accounts, :authorized, 1)
|
||||
# change_column_default(:users, :email, nil)
|
||||
def change_column_default(table_name, column_name, default)
|
||||
raise NotImplementedError, "change_column_default is not implemented"
|
||||
end
|
||||
|
|
|
@ -453,7 +453,7 @@ module ActiveRecord
|
|||
# If a pk is given, fallback to default sequence name.
|
||||
# Don't fetch last insert id for a table without a pk.
|
||||
if pk && sequence_name ||= default_sequence_name(table, pk)
|
||||
last_insert_id(table, sequence_name)
|
||||
last_insert_id(sequence_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1038,8 +1038,9 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
# Returns the current ID of a table's sequence.
|
||||
def last_insert_id(table, sequence_name) #:nodoc:
|
||||
Integer(select_value("SELECT currval('#{sequence_name}')"))
|
||||
def last_insert_id(sequence_name) #:nodoc:
|
||||
r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]])
|
||||
Integer(r.rows.first.first)
|
||||
end
|
||||
|
||||
# Executes a SELECT query and returns the results, performing any data type
|
||||
|
|
|
@ -336,6 +336,10 @@ module ActiveRecord
|
|||
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
|
||||
end
|
||||
|
||||
def null_insert_value
|
||||
Arel.sql 'NULL'
|
||||
end
|
||||
|
||||
def empty_insert_statement_value
|
||||
"VALUES(NULL)"
|
||||
end
|
||||
|
|
|
@ -270,17 +270,9 @@ module ActiveRecord
|
|||
# Creates a record with values matching those of the instance attributes
|
||||
# and returns its id.
|
||||
def create
|
||||
if id.nil? && connection.prefetch_primary_key?(self.class.table_name)
|
||||
self.id = connection.next_sequence_value(self.class.sequence_name)
|
||||
end
|
||||
|
||||
attributes_values = arel_attributes_values(!id.nil?)
|
||||
|
||||
new_id = if attributes_values.empty?
|
||||
self.class.unscoped.insert connection.empty_insert_statement_value
|
||||
else
|
||||
self.class.unscoped.insert attributes_values
|
||||
end
|
||||
new_id = self.class.unscoped.insert attributes_values
|
||||
|
||||
self.id ||= new_id
|
||||
|
||||
|
|
|
@ -262,16 +262,30 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def through_reflection
|
||||
false
|
||||
end
|
||||
|
||||
def through_reflection_foreign_key
|
||||
nil
|
||||
end
|
||||
|
||||
def source_reflection
|
||||
nil
|
||||
end
|
||||
|
||||
# A chain of reflections from this one back to the owner. For more see the explanation in
|
||||
# ThroughReflection.
|
||||
def chain
|
||||
[self]
|
||||
end
|
||||
|
||||
# An array of arrays of conditions. Each item in the outside array corresponds to a reflection
|
||||
# in the #chain. The inside arrays are simply conditions (and each condition may itself be
|
||||
# a hash, array, arel predicate, etc...)
|
||||
def conditions
|
||||
conditions = [options[:conditions]].compact
|
||||
conditions << { type => active_record.base_class.name } if options[:as]
|
||||
[conditions]
|
||||
end
|
||||
|
||||
alias :source_macro :macro
|
||||
|
||||
def has_inverse?
|
||||
@options[:inverse_of]
|
||||
end
|
||||
|
@ -363,7 +377,7 @@ module ActiveRecord
|
|||
# Holds all the meta-data about a :through association as it was specified
|
||||
# in the Active Record class.
|
||||
class ThroughReflection < AssociationReflection #:nodoc:
|
||||
delegate :association_primary_key, :foreign_type, :to => :source_reflection
|
||||
delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection
|
||||
|
||||
# Gets the source of the through reflection. It checks both a singularized
|
||||
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
||||
|
@ -392,6 +406,88 @@ module ActiveRecord
|
|||
@through_reflection ||= active_record.reflect_on_association(options[:through])
|
||||
end
|
||||
|
||||
# Returns an array of reflections which are involved in this association. Each item in the
|
||||
# array corresponds to a table which will be part of the query for this association.
|
||||
#
|
||||
# The chain is built by recursively calling #chain on the source reflection and the through
|
||||
# reflection. The base case for the recursion is a normal association, which just returns
|
||||
# [self] as its #chain.
|
||||
def chain
|
||||
@chain ||= begin
|
||||
chain = source_reflection.chain + through_reflection.chain
|
||||
chain[0] = self # Use self so we don't lose the information from :source_type
|
||||
chain
|
||||
end
|
||||
end
|
||||
|
||||
# Consider the following example:
|
||||
#
|
||||
# class Person
|
||||
# has_many :articles
|
||||
# has_many :comment_tags, :through => :articles
|
||||
# end
|
||||
#
|
||||
# class Article
|
||||
# has_many :comments
|
||||
# has_many :comment_tags, :through => :comments, :source => :tags
|
||||
# end
|
||||
#
|
||||
# class Comment
|
||||
# has_many :tags
|
||||
# end
|
||||
#
|
||||
# There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags,
|
||||
# but only Comment.tags will be represented in the #chain. So this method creates an array
|
||||
# of conditions corresponding to the chain. Each item in the #conditions array corresponds
|
||||
# to an item in the #chain, and is itself an array of conditions from an arbitrary number
|
||||
# of relevant reflections, plus any :source_type or polymorphic :as constraints.
|
||||
def conditions
|
||||
@conditions ||= begin
|
||||
conditions = source_reflection.conditions
|
||||
|
||||
# Add to it the conditions from this reflection if necessary.
|
||||
conditions.first << options[:conditions] if options[:conditions]
|
||||
|
||||
through_conditions = through_reflection.conditions
|
||||
|
||||
if options[:source_type]
|
||||
through_conditions.first << { foreign_type => options[:source_type] }
|
||||
end
|
||||
|
||||
# Recursively fill out the rest of the array from the through reflection
|
||||
conditions += through_conditions
|
||||
|
||||
# And return
|
||||
conditions
|
||||
end
|
||||
end
|
||||
|
||||
# The macro used by the source association
|
||||
def source_macro
|
||||
source_reflection.source_macro
|
||||
end
|
||||
|
||||
# A through association is nested iff there would be more than one join table
|
||||
def nested?
|
||||
chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
|
||||
end
|
||||
|
||||
# We want to use the klass from this reflection, rather than just delegate straight to
|
||||
# the source_reflection, because the source_reflection may be polymorphic. We still
|
||||
# need to respect the source_reflection's :primary_key option, though.
|
||||
def association_primary_key
|
||||
@association_primary_key ||= begin
|
||||
# Get the "actual" source reflection if the immediate source reflection has a
|
||||
# source reflection itself
|
||||
source_reflection = self.source_reflection
|
||||
while source_reflection.source_reflection
|
||||
source_reflection = source_reflection.source_reflection
|
||||
end
|
||||
|
||||
source_reflection.options[:primary_key] || klass.primary_key
|
||||
end
|
||||
end
|
||||
|
||||
# Gets an array of possible <tt>:through</tt> source reflection names:
|
||||
#
|
||||
# [:singularized, :pluralized]
|
||||
|
@ -429,10 +525,6 @@ module ActiveRecord
|
|||
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
|
||||
end
|
||||
|
||||
unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
||||
raise HasManyThroughSourceAssociationMacroError.new(self)
|
||||
end
|
||||
|
||||
if macro == :has_one && through_reflection.collection?
|
||||
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
|
||||
end
|
||||
|
@ -440,14 +532,6 @@ module ActiveRecord
|
|||
check_validity_of_inverse!
|
||||
end
|
||||
|
||||
def through_reflection_primary_key
|
||||
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key
|
||||
end
|
||||
|
||||
def through_reflection_foreign_key
|
||||
through_reflection.foreign_key if through_reflection.belongs_to?
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
# get the class_name of the belongs_to association of the through reflection
|
||||
|
|
|
@ -30,15 +30,26 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def insert(values)
|
||||
im = arel.compile_insert values
|
||||
im.into @table
|
||||
|
||||
primary_key_value = nil
|
||||
|
||||
if primary_key && Hash === values
|
||||
primary_key_value = values[values.keys.find { |k|
|
||||
k.name == primary_key
|
||||
}]
|
||||
|
||||
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
|
||||
primary_key_value = connection.next_sequence_value(klass.sequence_name)
|
||||
values[klass.arel_table[klass.primary_key]] = primary_key_value
|
||||
end
|
||||
end
|
||||
|
||||
im = arel.create_insert
|
||||
im.into @table
|
||||
|
||||
if values.empty? # empty insert
|
||||
im.values = im.create_values [connection.null_insert_value], []
|
||||
else
|
||||
im.insert values
|
||||
end
|
||||
|
||||
@klass.connection.insert(
|
||||
|
|
|
@ -261,7 +261,7 @@ module ActiveRecord
|
|||
)
|
||||
|
||||
join_nodes.each do |join|
|
||||
join_dependency.table_aliases[join.left.name.downcase] = 1
|
||||
join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
|
||||
end
|
||||
|
||||
join_dependency.graft(*stashed_association_joins)
|
||||
|
|
|
@ -15,17 +15,17 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
|
|||
|
||||
def test_eager_association_loading_with_cascaded_two_levels
|
||||
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
|
||||
assert_equal 2, authors.size
|
||||
assert_equal 3, authors.size
|
||||
assert_equal 5, authors[0].posts.size
|
||||
assert_equal 1, authors[1].posts.size
|
||||
assert_equal 3, authors[1].posts.size
|
||||
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
|
||||
end
|
||||
|
||||
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
|
||||
authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
|
||||
assert_equal 2, authors.size
|
||||
assert_equal 3, authors.size
|
||||
assert_equal 5, authors[0].posts.size
|
||||
assert_equal 1, authors[1].posts.size
|
||||
assert_equal 3, authors[1].posts.size
|
||||
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
|
||||
assert_equal 1, authors[0].categorizations.size
|
||||
assert_equal 2, authors[1].categorizations.size
|
||||
|
@ -51,24 +51,24 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
|
|||
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
|
||||
assert_equal 3, categories.count
|
||||
assert_equal 3, 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
|
||||
assert_equal 3, categories.count
|
||||
assert_equal 3, 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
|
||||
assert_equal 3, categories.count
|
||||
assert_equal 3, categories.all.size
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -81,15 +81,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
|
|||
|
||||
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
|
||||
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
|
||||
assert_equal 2, authors.size
|
||||
assert_equal 3, authors.size
|
||||
assert_equal 5, authors[0].posts.size
|
||||
assert_equal 1, authors[1].posts.size
|
||||
assert_equal 3, authors[1].posts.size
|
||||
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
|
||||
end
|
||||
|
||||
def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
|
||||
authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id")
|
||||
assert_equal 2, authors.size
|
||||
assert_equal 3, authors.size
|
||||
assert_equal 5, authors[0].posts.size
|
||||
assert_equal authors(:david).name, authors[0].name
|
||||
assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq
|
||||
|
@ -157,9 +157,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
|
|||
|
||||
def test_eager_association_loading_where_first_level_returns_nil
|
||||
authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC')
|
||||
assert_equal [authors(:mary), authors(:david)], authors
|
||||
assert_equal [authors(:bob), authors(:mary), authors(:david)], authors
|
||||
assert_no_queries do
|
||||
authors[1].post_about_thinking.comments.first
|
||||
authors[2].post_about_thinking.comments.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,8 +68,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
|||
|
||||
def test_with_ordering
|
||||
list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
|
||||
[:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments,
|
||||
:authorless, :thinking, :welcome
|
||||
[:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other,
|
||||
:sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome
|
||||
].each_with_index do |post, index|
|
||||
assert_equal posts(post), list[index]
|
||||
end
|
||||
|
@ -97,25 +97,25 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
|||
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
|
||||
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
|
||||
posts = Post.find(:all, :include=>:comments)
|
||||
assert_equal 7, posts.size
|
||||
assert_equal 11, posts.size
|
||||
end
|
||||
|
||||
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
|
||||
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
|
||||
posts = Post.find(:all, :include=>:comments)
|
||||
assert_equal 7, posts.size
|
||||
assert_equal 11, posts.size
|
||||
end
|
||||
|
||||
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
|
||||
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
|
||||
posts = Post.find(:all, :include=>:categories)
|
||||
assert_equal 7, posts.size
|
||||
assert_equal 11, posts.size
|
||||
end
|
||||
|
||||
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
|
||||
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
|
||||
posts = Post.find(:all, :include=>:categories)
|
||||
assert_equal 7, posts.size
|
||||
assert_equal 11, posts.size
|
||||
end
|
||||
|
||||
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
|
||||
|
|
|
@ -642,12 +642,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
def test_find_grouped
|
||||
all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories)
|
||||
grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories)
|
||||
assert_equal 4, all_posts_from_category1.size
|
||||
assert_equal 1, grouped_posts_of_category1.size
|
||||
assert_equal 5, all_posts_from_category1.size
|
||||
assert_equal 2, grouped_posts_of_category1.size
|
||||
end
|
||||
|
||||
def test_find_scoped_grouped
|
||||
assert_equal 4, categories(:general).posts_grouped_by_title.size
|
||||
assert_equal 5, categories(:general).posts_grouped_by_title.size
|
||||
assert_equal 1, categories(:technology).posts_grouped_by_title.size
|
||||
end
|
||||
|
||||
|
|
|
@ -17,9 +17,10 @@ require 'models/developer'
|
|||
require 'models/subscriber'
|
||||
require 'models/book'
|
||||
require 'models/subscription'
|
||||
require 'models/categorization'
|
||||
require 'models/category'
|
||||
require 'models/essay'
|
||||
require 'models/category'
|
||||
require 'models/owner'
|
||||
require 'models/categorization'
|
||||
require 'models/member'
|
||||
require 'models/membership'
|
||||
require 'models/club'
|
||||
|
@ -27,7 +28,7 @@ require 'models/club'
|
|||
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
|
||||
:owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
|
||||
:subscribers, :books, :subscriptions, :developers, :categorizations
|
||||
:subscribers, :books, :subscriptions, :developers, :categorizations, :essays
|
||||
|
||||
# Dummies to force column loads so query counts are clean.
|
||||
def setup
|
||||
|
@ -656,6 +657,25 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
|||
assert author.comments.include?(comment)
|
||||
end
|
||||
|
||||
def test_has_many_through_polymorphic_with_primary_key_option
|
||||
assert_equal [categories(:general)], authors(:david).essay_categories
|
||||
|
||||
authors = Author.joins(:essay_categories).where('categories.id' => categories(:general).id)
|
||||
assert_equal authors(:david), authors.first
|
||||
|
||||
assert_equal [owners(:blackbeard)], authors(:david).essay_owners
|
||||
|
||||
authors = Author.joins(:essay_owners).where("owners.name = 'blackbeard'")
|
||||
assert_equal authors(:david), authors.first
|
||||
end
|
||||
|
||||
def test_has_many_through_with_primary_key_option
|
||||
assert_equal [categories(:general)], authors(:david).essay_categories_2
|
||||
|
||||
authors = Author.joins(:essay_categories_2).where('categories.id' => categories(:general).id)
|
||||
assert_equal authors(:david), authors.first
|
||||
end
|
||||
|
||||
def test_size_of_through_association_should_increase_correctly_when_has_many_association_is_added
|
||||
post = posts(:thinking)
|
||||
readers = post.readers.size
|
||||
|
@ -679,10 +699,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_joining_has_many_through_belongs_to
|
||||
posts = Post.joins(:author_categorizations).
|
||||
posts = Post.joins(:author_categorizations).order('posts.id').
|
||||
where('categorizations.id' => categorizations(:mary_thinking_sti).id)
|
||||
|
||||
assert_equal [posts(:eager_other)], posts
|
||||
assert_equal [posts(:eager_other), posts(:misc_by_mary), posts(:other_by_mary)], posts
|
||||
end
|
||||
|
||||
def test_select_chosen_fields_only
|
||||
|
|
|
@ -9,13 +9,16 @@ require 'models/member_detail'
|
|||
require 'models/minivan'
|
||||
require 'models/dashboard'
|
||||
require 'models/speedometer'
|
||||
require 'models/category'
|
||||
require 'models/author'
|
||||
require 'models/essay'
|
||||
require 'models/owner'
|
||||
require 'models/post'
|
||||
require 'models/comment'
|
||||
|
||||
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
|
||||
:dashboards, :speedometers, :authors, :posts, :comments
|
||||
:dashboards, :speedometers, :authors, :posts, :comments, :categories, :essays, :owners
|
||||
|
||||
def setup
|
||||
@member = members(:groucho)
|
||||
|
@ -242,6 +245,25 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_has_one_through_polymorphic_with_primary_key_option
|
||||
assert_equal categories(:general), authors(:david).essay_category
|
||||
|
||||
authors = Author.joins(:essay_category).where('categories.id' => categories(:general).id)
|
||||
assert_equal authors(:david), authors.first
|
||||
|
||||
assert_equal owners(:blackbeard), authors(:david).essay_owner
|
||||
|
||||
authors = Author.joins(:essay_owner).where("owners.name = 'blackbeard'")
|
||||
assert_equal authors(:david), authors.first
|
||||
end
|
||||
|
||||
def test_has_one_through_with_primary_key_option
|
||||
assert_equal categories(:general), authors(:david).essay_category_2
|
||||
|
||||
authors = Author.joins(:essay_category_2).where('categories.id' => categories(:general).id)
|
||||
assert_equal authors(:david), authors.first
|
||||
end
|
||||
|
||||
def test_has_one_through_with_default_scope_on_join_model
|
||||
assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_post
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ require "cases/helper"
|
|||
require 'models/tag'
|
||||
require 'models/tagging'
|
||||
require 'models/post'
|
||||
require 'models/rating'
|
||||
require 'models/item'
|
||||
require 'models/comment'
|
||||
require 'models/author'
|
||||
|
@ -288,7 +289,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_has_many_going_through_join_model_with_custom_foreign_key
|
||||
assert_equal [], posts(:thinking).authors
|
||||
assert_equal [authors(:bob)], posts(:thinking).authors
|
||||
assert_equal [authors(:mary)], posts(:authorless).authors
|
||||
end
|
||||
|
||||
|
@ -305,7 +306,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_has_many_through_with_custom_primary_key_on_has_many_source
|
||||
assert_equal [authors(:david)], posts(:thinking).authors_using_custom_pk
|
||||
assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order('authors.id')
|
||||
end
|
||||
|
||||
def test_both_scoped_and_explicit_joins_should_be_respected
|
||||
|
@ -399,14 +400,6 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_through
|
||||
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags }
|
||||
end
|
||||
|
||||
def test_has_many_through_habtm
|
||||
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories }
|
||||
end
|
||||
|
||||
def test_eager_load_has_many_through_has_many
|
||||
author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id'
|
||||
SpecialComment.new; VerySpecialComment.new
|
||||
|
|
|
@ -0,0 +1,546 @@
|
|||
require "cases/helper"
|
||||
require 'models/author'
|
||||
require 'models/post'
|
||||
require 'models/person'
|
||||
require 'models/reference'
|
||||
require 'models/job'
|
||||
require 'models/reader'
|
||||
require 'models/comment'
|
||||
require 'models/tag'
|
||||
require 'models/tagging'
|
||||
require 'models/subscriber'
|
||||
require 'models/book'
|
||||
require 'models/subscription'
|
||||
require 'models/rating'
|
||||
require 'models/member'
|
||||
require 'models/member_detail'
|
||||
require 'models/member_type'
|
||||
require 'models/sponsor'
|
||||
require 'models/club'
|
||||
require 'models/organization'
|
||||
require 'models/category'
|
||||
require 'models/categorization'
|
||||
require 'models/membership'
|
||||
require 'models/essay'
|
||||
|
||||
class NestedThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
|
||||
:people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details,
|
||||
:member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts,
|
||||
:categorizations, :memberships, :essays
|
||||
|
||||
# Through associations can either use the has_many or has_one macros.
|
||||
#
|
||||
# has_many
|
||||
# - Source reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
|
||||
# - Through reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
|
||||
#
|
||||
# has_one
|
||||
# - Source reflection can be has_one or belongs_to
|
||||
# - Through reflection can be has_one or belongs_to
|
||||
#
|
||||
# Additionally, the source reflection and/or through reflection may be subject to
|
||||
# polymorphism and/or STI.
|
||||
#
|
||||
# When testing these, we need to make sure it works via loading the association directly, or
|
||||
# joining the association, or including the association. We also need to ensure that associations
|
||||
# are readonly where relevant.
|
||||
|
||||
# has_many through
|
||||
# Source: has_many through
|
||||
# Through: has_many
|
||||
def test_has_many_through_has_many_with_has_many_through_source_reflection
|
||||
general = tags(:general)
|
||||
assert_equal [general, general], authors(:david).tags
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_with_has_many_through_source_reflection_preload
|
||||
authors = assert_queries(5) { Author.includes(:tags).to_a }
|
||||
general = tags(:general)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [general, general], authors.first.tags
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_with_has_many_through_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Author.where('tags.id' => tags(:general).id),
|
||||
[authors(:david)], :tags
|
||||
)
|
||||
|
||||
# This ensures that the polymorphism of taggings is being observed correctly
|
||||
authors = Author.joins(:tags).where('taggings.taggable_type' => 'FakeModel')
|
||||
assert authors.empty?
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_many
|
||||
# Through: has_many through
|
||||
def test_has_many_through_has_many_through_with_has_many_source_reflection
|
||||
luke, david = subscribers(:first), subscribers(:second)
|
||||
assert_equal [luke, david, david], authors(:david).subscribers.order('subscribers.nick')
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_through_with_has_many_source_reflection_preload
|
||||
luke, david = subscribers(:first), subscribers(:second)
|
||||
authors = assert_queries(4) { Author.includes(:subscribers).to_a }
|
||||
assert_no_queries do
|
||||
assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick)
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_through_with_has_many_source_reflection_preload_via_joins
|
||||
# All authors with subscribers where one of the subscribers' nick is 'alterself'
|
||||
assert_includes_and_joins_equal(
|
||||
Author.where('subscribers.nick' => 'alterself'),
|
||||
[authors(:david)], :subscribers
|
||||
)
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_one through
|
||||
# Through: has_one
|
||||
def test_has_many_through_has_one_with_has_one_through_source_reflection
|
||||
assert_equal [member_types(:founding)], members(:groucho).nested_member_types
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_with_has_one_through_source_reflection_preload
|
||||
members = assert_queries(4) { Member.includes(:nested_member_types).to_a }
|
||||
founding = member_types(:founding)
|
||||
assert_no_queries do
|
||||
assert_equal [founding], members.first.nested_member_types
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_with_has_one_through_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Member.where('member_types.id' => member_types(:founding).id),
|
||||
[members(:groucho)], :nested_member_types
|
||||
)
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_one
|
||||
# Through: has_one through
|
||||
def test_has_many_through_has_one_through_with_has_one_source_reflection
|
||||
assert_equal [sponsors(:moustache_club_sponsor_for_groucho)], members(:groucho).nested_sponsors
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
|
||||
members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
|
||||
mustache = sponsors(:moustache_club_sponsor_for_groucho)
|
||||
assert_no_queries do
|
||||
assert_equal [mustache], members.first.nested_sponsors
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Member.where('sponsors.id' => sponsors(:moustache_club_sponsor_for_groucho).id),
|
||||
[members(:groucho)], :nested_sponsors
|
||||
)
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_many through
|
||||
# Through: has_one
|
||||
def test_has_many_through_has_one_with_has_many_through_source_reflection
|
||||
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
|
||||
|
||||
assert_equal [groucho_details, other_details],
|
||||
members(:groucho).organization_member_details.order('member_details.id')
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
|
||||
members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
|
||||
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [groucho_details, other_details], members.first.organization_member_details.sort_by(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
|
||||
[members(:groucho), members(:some_other_guy)], :organization_member_details
|
||||
)
|
||||
|
||||
members = Member.joins(:organization_member_details).
|
||||
where('member_details.id' => 9)
|
||||
assert members.empty?
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_many
|
||||
# Through: has_one through
|
||||
def test_has_many_through_has_one_through_with_has_many_source_reflection
|
||||
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
|
||||
|
||||
assert_equal [groucho_details, other_details],
|
||||
members(:groucho).organization_member_details_2.order('member_details.id')
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_through_with_has_many_source_reflection_preload
|
||||
members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) }
|
||||
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_through_with_has_many_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
|
||||
[members(:groucho), members(:some_other_guy)], :organization_member_details_2
|
||||
)
|
||||
|
||||
members = Member.joins(:organization_member_details_2).
|
||||
where('member_details.id' => 9)
|
||||
assert members.empty?
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_and_belongs_to_many
|
||||
# Through: has_many
|
||||
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection
|
||||
general, cooking = categories(:general), categories(:cooking)
|
||||
|
||||
assert_equal [general, cooking], authors(:bob).post_categories.order('categories.id')
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
|
||||
authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) }
|
||||
general, cooking = categories(:general), categories(:cooking)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [general, cooking], authors[2].post_categories.sort_by(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Author.where('categories.id' => categories(:cooking).id),
|
||||
[authors(:bob)], :post_categories
|
||||
)
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_many
|
||||
# Through: has_and_belongs_to_many
|
||||
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection
|
||||
greetings, more = comments(:greetings), comments(:more_greetings)
|
||||
|
||||
assert_equal [greetings, more], categories(:technology).post_comments.order('comments.id')
|
||||
end
|
||||
|
||||
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
|
||||
categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) }
|
||||
greetings, more = comments(:greetings), comments(:more_greetings)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [greetings, more], categories[1].post_comments.sort_by(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Category.where('comments.id' => comments(:more_greetings).id).order('comments.id'),
|
||||
[categories(:general), categories(:technology)], :post_comments
|
||||
)
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_many through a habtm
|
||||
# Through: has_many through
|
||||
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection
|
||||
greetings, more = comments(:greetings), comments(:more_greetings)
|
||||
|
||||
assert_equal [greetings, more], authors(:bob).category_post_comments.order('comments.id')
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
|
||||
authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
|
||||
greetings, more = comments(:greetings), comments(:more_greetings)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [greetings, more], authors[2].category_post_comments.sort_by(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Author.where('comments.id' => comments(:does_it_hurt).id).order('authors.id'),
|
||||
[authors(:david), authors(:mary)], :category_post_comments
|
||||
)
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: belongs_to
|
||||
# Through: has_many through
|
||||
def test_has_many_through_has_many_through_with_belongs_to_source_reflection
|
||||
assert_equal [tags(:general), tags(:general)], authors(:david).tagging_tags
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload
|
||||
authors = assert_queries(5) { Author.includes(:tagging_tags).to_a }
|
||||
general = tags(:general)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [general, general], authors.first.tagging_tags
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Author.where('tags.id' => tags(:general).id),
|
||||
[authors(:david)], :tagging_tags
|
||||
)
|
||||
end
|
||||
|
||||
# has_many through
|
||||
# Source: has_many through
|
||||
# Through: belongs_to
|
||||
def test_has_many_through_belongs_to_with_has_many_through_source_reflection
|
||||
welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
|
||||
|
||||
assert_equal [welcome_general, thinking_general],
|
||||
categorizations(:david_welcome_general).post_taggings.order('taggings.id')
|
||||
end
|
||||
|
||||
def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload
|
||||
categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a.sort_by(&:id) }
|
||||
welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings.sort_by(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Categorization.where('taggings.id' => taggings(:welcome_general).id).order('taggings.id'),
|
||||
[categorizations(:david_welcome_general)], :post_taggings
|
||||
)
|
||||
end
|
||||
|
||||
# has_one through
|
||||
# Source: has_one through
|
||||
# Through: has_one
|
||||
def test_has_one_through_has_one_with_has_one_through_source_reflection
|
||||
assert_equal member_types(:founding), members(:groucho).nested_member_type
|
||||
end
|
||||
|
||||
def test_has_one_through_has_one_with_has_one_through_source_reflection_preload
|
||||
members = assert_queries(4) { Member.includes(:nested_member_type).to_a.sort_by(&:id) }
|
||||
founding = member_types(:founding)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal founding, members.first.nested_member_type
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_one_through_has_one_with_has_one_through_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Member.where('member_types.id' => member_types(:founding).id),
|
||||
[members(:groucho)], :nested_member_type
|
||||
)
|
||||
end
|
||||
|
||||
# has_one through
|
||||
# Source: belongs_to
|
||||
# Through: has_one through
|
||||
def test_has_one_through_has_one_through_with_belongs_to_source_reflection
|
||||
assert_equal categories(:general), members(:groucho).club_category
|
||||
end
|
||||
|
||||
def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload
|
||||
members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) }
|
||||
general = categories(:general)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal general, members.first.club_category
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload_via_joins
|
||||
assert_includes_and_joins_equal(
|
||||
Member.where('categories.id' => categories(:technology).id),
|
||||
[members(:blarpy_winkup)], :club_category
|
||||
)
|
||||
end
|
||||
|
||||
def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection
|
||||
author = authors(:david)
|
||||
assert_equal [tags(:general)], author.distinct_tags
|
||||
end
|
||||
|
||||
def test_distinct_has_many_through_a_has_many_through_association_on_through_reflection
|
||||
author = authors(:david)
|
||||
assert_equal [subscribers(:first), subscribers(:second)],
|
||||
author.distinct_subscribers.order('subscribers.nick')
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_a_table_referenced_multiple_times
|
||||
author = authors(:bob)
|
||||
assert_equal [posts(:misc_by_bob), posts(:misc_by_mary), posts(:other_by_bob), posts(:other_by_mary)],
|
||||
author.similar_posts.sort_by(&:id)
|
||||
|
||||
# Mary and Bob both have posts in misc, but they are the only ones.
|
||||
authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
|
||||
assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id)
|
||||
|
||||
# Check the polymorphism of taggings is being observed correctly (in both joins)
|
||||
authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel')
|
||||
assert authors.empty?
|
||||
authors = Author.joins(:similar_posts).where('taggings_authors_join.taggable_type' => 'FakeModel')
|
||||
assert authors.empty?
|
||||
end
|
||||
|
||||
def test_has_many_through_with_foreign_key_option_on_through_reflection
|
||||
assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order('posts.id')
|
||||
assert_equal [authors(:david)], references(:david_unicyclist).agents_posts_authors
|
||||
|
||||
references = Reference.joins(:agents_posts_authors).where('authors.id' => authors(:david).id)
|
||||
assert_equal [references(:david_unicyclist)], references
|
||||
end
|
||||
|
||||
def test_has_many_through_with_foreign_key_option_on_source_reflection
|
||||
assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order('people.id')
|
||||
|
||||
jobs = Job.joins(:agents)
|
||||
assert_equal [jobs(:unicyclist), jobs(:unicyclist)], jobs
|
||||
end
|
||||
|
||||
def test_has_many_through_with_sti_on_through_reflection
|
||||
ratings = posts(:sti_comments).special_comments_ratings.sort_by(&:id)
|
||||
assert_equal [ratings(:special_comment_rating), ratings(:sub_special_comment_rating)], ratings
|
||||
|
||||
# Ensure STI is respected in the join
|
||||
scope = Post.joins(:special_comments_ratings).where(:id => posts(:sti_comments).id)
|
||||
assert scope.where("comments.type" => "Comment").empty?
|
||||
assert !scope.where("comments.type" => "SpecialComment").empty?
|
||||
assert !scope.where("comments.type" => "SubSpecialComment").empty?
|
||||
end
|
||||
|
||||
def test_has_many_through_with_sti_on_nested_through_reflection
|
||||
taggings = posts(:sti_comments).special_comments_ratings_taggings
|
||||
assert_equal [taggings(:special_comment_rating)], taggings
|
||||
|
||||
scope = Post.joins(:special_comments_ratings_taggings).where(:id => posts(:sti_comments).id)
|
||||
assert scope.where("comments.type" => "Comment").empty?
|
||||
assert !scope.where("comments.type" => "SpecialComment").empty?
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_writers_should_raise_error
|
||||
david = authors(:david)
|
||||
subscriber = subscribers(:first)
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
david.subscribers = [subscriber]
|
||||
end
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
david.subscriber_ids = [subscriber.id]
|
||||
end
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
david.subscribers << subscriber
|
||||
end
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
david.subscribers.delete(subscriber)
|
||||
end
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
david.subscribers.clear
|
||||
end
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
david.subscribers.build
|
||||
end
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
david.subscribers.create
|
||||
end
|
||||
end
|
||||
|
||||
def test_nested_has_one_through_writers_should_raise_error
|
||||
groucho = members(:groucho)
|
||||
founding = member_types(:founding)
|
||||
|
||||
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
|
||||
groucho.nested_member_type = founding
|
||||
end
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_conditions_on_through_associations
|
||||
assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_conditions_on_through_associations_preload
|
||||
assert Author.where('tags.id' => 100).joins(:misc_post_first_blue_tags).empty?
|
||||
|
||||
authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) }
|
||||
blue = tags(:blue)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [blue], authors[2].misc_post_first_blue_tags
|
||||
end
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins
|
||||
# Pointless condition to force single-query loading
|
||||
assert_includes_and_joins_equal(
|
||||
Author.where('tags.id = tags.id'),
|
||||
[authors(:bob)], :misc_post_first_blue_tags
|
||||
)
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_conditions_on_source_associations
|
||||
assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags_2
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_conditions_on_source_associations_preload
|
||||
authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a.sort_by(&:id) }
|
||||
blue = tags(:blue)
|
||||
|
||||
assert_no_queries do
|
||||
assert_equal [blue], authors[2].misc_post_first_blue_tags_2
|
||||
end
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
|
||||
# Pointless condition to force single-query loading
|
||||
assert_includes_and_joins_equal(
|
||||
Author.where('tags.id = tags.id'),
|
||||
[authors(:bob)], :misc_post_first_blue_tags_2
|
||||
)
|
||||
end
|
||||
|
||||
def test_nested_has_many_through_with_foreign_key_option_on_the_source_reflection_through_reflection
|
||||
assert_equal [categories(:general)], organizations(:nsa).author_essay_categories
|
||||
|
||||
organizations = Organization.joins(:author_essay_categories).
|
||||
where('categories.id' => categories(:general).id)
|
||||
assert_equal [organizations(:nsa)], organizations
|
||||
|
||||
assert_equal categories(:general), organizations(:nsa).author_owned_essay_category
|
||||
|
||||
organizations = Organization.joins(:author_owned_essay_category).
|
||||
where('categories.id' => categories(:general).id)
|
||||
assert_equal [organizations(:nsa)], organizations
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_includes_and_joins_equal(query, expected, association)
|
||||
actual = assert_queries(1) { query.joins(association).to_a.uniq }
|
||||
assert_equal expected, actual
|
||||
|
||||
actual = assert_queries(1) { query.includes(association).to_a.uniq }
|
||||
assert_equal expected, actual
|
||||
end
|
||||
end
|
|
@ -118,22 +118,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_read_attributes_before_type_cast_on_datetime
|
||||
developer = Developer.find(:first)
|
||||
if current_adapter?(:Mysql2Adapter, :OracleAdapter)
|
||||
# Mysql2 and Oracle adapters keep the value in Time instance
|
||||
assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s(:db)
|
||||
else
|
||||
assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s
|
||||
in_time_zone "Pacific Time (US & Canada)" do
|
||||
record = @target.new
|
||||
|
||||
record.written_on = "345643456"
|
||||
assert_equal "345643456", record.written_on_before_type_cast
|
||||
assert_equal nil, record.written_on
|
||||
|
||||
record.written_on = "2009-10-11 12:13:14"
|
||||
assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast
|
||||
assert_equal Time.zone.parse("2009-10-11 12:13:14"), record.written_on
|
||||
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
|
||||
end
|
||||
|
||||
developer.created_at = "345643456"
|
||||
|
||||
assert_equal developer.created_at_before_type_cast, "345643456"
|
||||
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, Time.parse("2010-03-21 21:23:32")
|
||||
end
|
||||
|
||||
def test_hash_content
|
||||
|
|
|
@ -45,6 +45,8 @@ class ReadonlyTitlePost < Post
|
|||
attr_readonly :title
|
||||
end
|
||||
|
||||
class Weird < ActiveRecord::Base; end
|
||||
|
||||
class Boolean < ActiveRecord::Base; end
|
||||
|
||||
class BasicsTest < ActiveRecord::TestCase
|
||||
|
@ -477,6 +479,16 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert_equal "changed", post.body
|
||||
end
|
||||
|
||||
def test_non_valid_identifier_column_name
|
||||
weird = Weird.create('a$b' => 'value')
|
||||
weird.reload
|
||||
assert_equal 'value', weird.send('a$b')
|
||||
|
||||
weird.update_attribute('a$b', 'value2')
|
||||
weird.reload
|
||||
assert_equal 'value2', weird.send('a$b')
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_date
|
||||
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
|
||||
topic = Topic.find(1)
|
||||
|
|
|
@ -25,7 +25,7 @@ class EachTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_each_should_execute_if_id_is_in_select
|
||||
assert_queries(4) do
|
||||
assert_queries(6) do
|
||||
Post.find_each(:select => "id, title, type", :batch_size => 2) do |post|
|
||||
assert_kind_of Post, post
|
||||
end
|
||||
|
|
|
@ -124,11 +124,13 @@ class FinderTest < ActiveRecord::TestCase
|
|||
def test_find_all_with_limit_and_offset_and_multiple_order_clauses
|
||||
first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0
|
||||
second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3
|
||||
last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6
|
||||
third_three_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6
|
||||
last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 9
|
||||
|
||||
assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] }
|
||||
assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] }
|
||||
assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] }
|
||||
assert_equal [[2,7],[2,9],[2,11]], third_three_posts.map { |p| [p.author_id, p.id] }
|
||||
assert_equal [[3,8],[3,10]], last_posts.map { |p| [p.author_id, p.id] }
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -13,5 +13,39 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
|
|||
sicp.destroy
|
||||
end
|
||||
end
|
||||
assert !sicp.destroyed?
|
||||
end
|
||||
|
||||
test "not destroying a student with lessons leaves student<=>lesson association intact" do
|
||||
# test a normal before_destroy doesn't destroy the habtm joins
|
||||
begin
|
||||
sicp = Lesson.new(:name => "SICP")
|
||||
ben = Student.new(:name => "Ben Bitdiddle")
|
||||
# add a before destroy to student
|
||||
Student.class_eval do
|
||||
before_destroy do
|
||||
raise ActiveRecord::Rollback unless lessons.empty?
|
||||
end
|
||||
end
|
||||
ben.lessons << sicp
|
||||
ben.save!
|
||||
ben.destroy
|
||||
assert !ben.reload.lessons.empty?
|
||||
ensure
|
||||
# get rid of it so Student is still like it was
|
||||
Student.reset_callbacks(:destroy)
|
||||
end
|
||||
end
|
||||
|
||||
test "not destroying a lesson with students leaves student<=>lesson association intact" do
|
||||
# test a more aggressive before_destroy doesn't destroy the habtm joins and still throws the exception
|
||||
sicp = Lesson.new(:name => "SICP")
|
||||
ben = Student.new(:name => "Ben Bitdiddle")
|
||||
sicp.students << ben
|
||||
sicp.save!
|
||||
assert_raises LessonError do
|
||||
sicp.destroy
|
||||
end
|
||||
assert !sicp.reload.students.empty?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -207,31 +207,31 @@ class IdentityMapTest < ActiveRecord::TestCase
|
|||
|
||||
def test_find_with_preloaded_associations
|
||||
assert_queries(2) do
|
||||
posts = Post.preload(:comments)
|
||||
posts = Post.preload(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
# With IM we'll retrieve post object from previous query, it'll have comments
|
||||
# already preloaded from first call
|
||||
assert_queries(1) do
|
||||
posts = Post.preload(:comments).to_a
|
||||
posts = Post.preload(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
assert_queries(2) do
|
||||
posts = Post.preload(:author)
|
||||
posts = Post.preload(:author).order('posts.id')
|
||||
assert posts.first.author
|
||||
end
|
||||
|
||||
# With IM we'll retrieve post object from previous query, it'll have comments
|
||||
# already preloaded from first call
|
||||
assert_queries(1) do
|
||||
posts = Post.preload(:author).to_a
|
||||
posts = Post.preload(:author).order('posts.id')
|
||||
assert posts.first.author
|
||||
end
|
||||
|
||||
assert_queries(1) do
|
||||
posts = Post.preload(:author, :comments).to_a
|
||||
posts = Post.preload(:author, :comments).order('posts.id')
|
||||
assert posts.first.author
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
@ -239,22 +239,22 @@ class IdentityMapTest < ActiveRecord::TestCase
|
|||
|
||||
def test_find_with_included_associations
|
||||
assert_queries(2) do
|
||||
posts = Post.includes(:comments)
|
||||
posts = Post.includes(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
assert_queries(1) do
|
||||
posts = Post.scoped.includes(:comments)
|
||||
posts = Post.scoped.includes(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
assert_queries(2) do
|
||||
posts = Post.includes(:author)
|
||||
posts = Post.includes(:author).order('posts.id')
|
||||
assert posts.first.author
|
||||
end
|
||||
|
||||
assert_queries(1) do
|
||||
posts = Post.includes(:author, :comments).to_a
|
||||
posts = Post.includes(:author, :comments).order('posts.id')
|
||||
assert posts.first.author
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
|
|
@ -181,7 +181,11 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
|
|||
def test_should_allow_except_option_for_list_of_authors
|
||||
ActiveRecord::Base.include_root_in_json = false
|
||||
authors = [@david, @mary]
|
||||
assert_equal %([{"id":1},{"id":2}]), ActiveSupport::JSON.encode(authors, :except => [:name, :author_address_id, :author_address_extra_id])
|
||||
encoded = ActiveSupport::JSON.encode(authors, :except => [
|
||||
:name, :author_address_id, :author_address_extra_id,
|
||||
:organization_id, :owned_essay_id
|
||||
])
|
||||
assert_equal %([{"id":1},{"id":2}]), encoded
|
||||
ensure
|
||||
ActiveRecord::Base.include_root_in_json = true
|
||||
end
|
||||
|
@ -196,7 +200,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
|
|||
)
|
||||
|
||||
['"name":"David"', '"posts":[', '{"id":1}', '{"id":2}', '{"id":4}',
|
||||
'{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[{"id":7}]'].each do |fragment|
|
||||
'{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[', '{"id":7}', '{"id":9}'].each do |fragment|
|
||||
assert json.include?(fragment), json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,9 +7,16 @@ require 'models/subscriber'
|
|||
require 'models/ship'
|
||||
require 'models/pirate'
|
||||
require 'models/price_estimate'
|
||||
require 'models/tagging'
|
||||
require 'models/essay'
|
||||
require 'models/author'
|
||||
require 'models/organization'
|
||||
require 'models/post'
|
||||
require 'models/tagging'
|
||||
require 'models/category'
|
||||
require 'models/book'
|
||||
require 'models/subscriber'
|
||||
require 'models/subscription'
|
||||
require 'models/tag'
|
||||
require 'models/sponsor'
|
||||
|
||||
class ReflectionTest < ActiveRecord::TestCase
|
||||
|
@ -195,10 +202,54 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books)
|
||||
end
|
||||
|
||||
def test_chain
|
||||
expected = [
|
||||
Organization.reflect_on_association(:author_essay_categories),
|
||||
Author.reflect_on_association(:essays),
|
||||
Organization.reflect_on_association(:authors)
|
||||
]
|
||||
actual = Organization.reflect_on_association(:author_essay_categories).chain
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
def test_conditions
|
||||
expected = [
|
||||
[{ :tags => { :name => 'Blue' } }],
|
||||
[{ :taggings => { :comment => 'first' } }, { "taggable_type" => "Post" }],
|
||||
[{ :posts => { :title => ['misc post by bob', 'misc post by mary'] } }]
|
||||
]
|
||||
actual = Author.reflect_on_association(:misc_post_first_blue_tags).conditions
|
||||
assert_equal expected, actual
|
||||
|
||||
expected = [
|
||||
[{ :tags => { :name => 'Blue' } }, { :taggings => { :comment => 'first' } }, { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }],
|
||||
[{ "taggable_type" => "Post" }],
|
||||
[]
|
||||
]
|
||||
actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
def test_nested?
|
||||
assert !Author.reflect_on_association(:comments).nested?
|
||||
assert Author.reflect_on_association(:tags).nested?
|
||||
|
||||
# Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is
|
||||
# a nested through association
|
||||
assert Category.reflect_on_association(:post_comments).nested?
|
||||
end
|
||||
|
||||
def test_association_primary_key
|
||||
assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s
|
||||
# Normal association
|
||||
assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s
|
||||
assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s
|
||||
assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s
|
||||
assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s
|
||||
|
||||
# Through association (uses the :primary_key option from the source reflection)
|
||||
assert_equal "nick", Author.reflect_on_association(:subscribers).association_primary_key.to_s
|
||||
assert_equal "name", Author.reflect_on_association(:essay_category).association_primary_key.to_s
|
||||
assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested
|
||||
end
|
||||
|
||||
def test_active_record_primary_key
|
||||
|
|
|
@ -335,7 +335,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
records = klass.all
|
||||
assert_equal 1, records.length
|
||||
assert_equal 3, records.length
|
||||
assert_equal 2, records.first.author_id
|
||||
end
|
||||
|
||||
|
|
|
@ -165,7 +165,7 @@ class RelationTest < ActiveRecord::TestCase
|
|||
|
||||
def test_finding_with_complex_order
|
||||
tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
|
||||
assert_equal 2, tags.length
|
||||
assert_equal 3, tags.length
|
||||
end
|
||||
|
||||
def test_finding_with_order_limit_and_offset
|
||||
|
@ -281,27 +281,27 @@ class RelationTest < ActiveRecord::TestCase
|
|||
|
||||
def test_find_with_preloaded_associations
|
||||
assert_queries(2) do
|
||||
posts = Post.preload(:comments)
|
||||
posts = Post.preload(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do
|
||||
posts = Post.preload(:comments).to_a
|
||||
posts = Post.preload(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
assert_queries(2) do
|
||||
posts = Post.preload(:author)
|
||||
posts = Post.preload(:author).order('posts.id')
|
||||
assert posts.first.author
|
||||
end
|
||||
|
||||
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do
|
||||
posts = Post.preload(:author).to_a
|
||||
posts = Post.preload(:author).order('posts.id')
|
||||
assert posts.first.author
|
||||
end
|
||||
|
||||
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do
|
||||
posts = Post.preload(:author, :comments).to_a
|
||||
posts = Post.preload(:author, :comments).order('posts.id')
|
||||
assert posts.first.author
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
@ -309,22 +309,22 @@ class RelationTest < ActiveRecord::TestCase
|
|||
|
||||
def test_find_with_included_associations
|
||||
assert_queries(2) do
|
||||
posts = Post.includes(:comments)
|
||||
posts = Post.includes(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do
|
||||
posts = Post.scoped.includes(:comments)
|
||||
posts = Post.scoped.includes(:comments).order('posts.id')
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
||||
assert_queries(2) do
|
||||
posts = Post.includes(:author)
|
||||
posts = Post.includes(:author).order('posts.id')
|
||||
assert posts.first.author
|
||||
end
|
||||
|
||||
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do
|
||||
posts = Post.includes(:author, :comments).to_a
|
||||
posts = Post.includes(:author, :comments).order('posts.id')
|
||||
assert posts.first.author
|
||||
assert posts.first.comments.first
|
||||
end
|
||||
|
@ -538,7 +538,7 @@ class RelationTest < ActiveRecord::TestCase
|
|||
|
||||
def test_last
|
||||
authors = Author.scoped
|
||||
assert_equal authors(:mary), authors.last
|
||||
assert_equal authors(:bob), authors.last
|
||||
end
|
||||
|
||||
def test_destroy_all
|
||||
|
@ -618,22 +618,22 @@ class RelationTest < ActiveRecord::TestCase
|
|||
def test_count
|
||||
posts = Post.scoped
|
||||
|
||||
assert_equal 7, posts.count
|
||||
assert_equal 7, posts.count(:all)
|
||||
assert_equal 7, posts.count(:id)
|
||||
assert_equal 11, posts.count
|
||||
assert_equal 11, posts.count(:all)
|
||||
assert_equal 11, posts.count(:id)
|
||||
|
||||
assert_equal 1, posts.where('comments_count > 1').count
|
||||
assert_equal 5, posts.where(:comments_count => 0).count
|
||||
assert_equal 9, posts.where(:comments_count => 0).count
|
||||
end
|
||||
|
||||
def test_count_with_distinct
|
||||
posts = Post.scoped
|
||||
|
||||
assert_equal 3, posts.count(:comments_count, :distinct => true)
|
||||
assert_equal 7, posts.count(:comments_count, :distinct => false)
|
||||
assert_equal 11, posts.count(:comments_count, :distinct => false)
|
||||
|
||||
assert_equal 3, posts.select(:comments_count).count(:distinct => true)
|
||||
assert_equal 7, posts.select(:comments_count).count(:distinct => false)
|
||||
assert_equal 11, posts.select(:comments_count).count(:distinct => false)
|
||||
end
|
||||
|
||||
def test_count_explicit_columns
|
||||
|
@ -643,7 +643,7 @@ class RelationTest < ActiveRecord::TestCase
|
|||
assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq
|
||||
assert_equal 0, posts.where('id is not null').select('comments_count').count
|
||||
|
||||
assert_equal 7, posts.select('comments_count').count('id')
|
||||
assert_equal 11, posts.select('comments_count').count('id')
|
||||
assert_equal 0, posts.select('comments_count').count
|
||||
assert_equal 0, posts.count(:comments_count)
|
||||
assert_equal 0, posts.count('comments_count')
|
||||
|
@ -658,12 +658,12 @@ class RelationTest < ActiveRecord::TestCase
|
|||
def test_size
|
||||
posts = Post.scoped
|
||||
|
||||
assert_queries(1) { assert_equal 7, posts.size }
|
||||
assert_queries(1) { assert_equal 11, posts.size }
|
||||
assert ! posts.loaded?
|
||||
|
||||
best_posts = posts.where(:comments_count => 0)
|
||||
best_posts.to_a # force load
|
||||
assert_no_queries { assert_equal 5, best_posts.size }
|
||||
assert_no_queries { assert_equal 9, best_posts.size }
|
||||
end
|
||||
|
||||
def test_count_complex_chained_relations
|
||||
|
|
|
@ -3,7 +3,13 @@ david:
|
|||
name: David
|
||||
author_address_id: 1
|
||||
author_address_extra_id: 2
|
||||
organization_id: No Such Agency
|
||||
owned_essay_id: A Modest Proposal
|
||||
|
||||
mary:
|
||||
id: 2
|
||||
name: Mary
|
||||
|
||||
bob:
|
||||
id: 3
|
||||
name: Bob
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
awdr:
|
||||
author_id: 1
|
||||
id: 1
|
||||
name: "Agile Web Development with Rails"
|
||||
|
||||
rfr:
|
||||
author_id: 1
|
||||
id: 2
|
||||
name: "Ruby for Rails"
|
||||
|
|
|
@ -12,3 +12,8 @@ sti_test:
|
|||
id: 3
|
||||
name: Special category
|
||||
type: SpecialCategory
|
||||
|
||||
cooking:
|
||||
id: 4
|
||||
name: Cooking
|
||||
type: Category
|
||||
|
|
|
@ -21,3 +21,11 @@ sti_test_sti_habtm:
|
|||
general_hello:
|
||||
category_id: 1
|
||||
post_id: 4
|
||||
|
||||
general_misc_by_bob:
|
||||
category_id: 1
|
||||
post_id: 8
|
||||
|
||||
cooking_misc_by_bob:
|
||||
category_id: 4
|
||||
post_id: 8
|
||||
|
|
|
@ -15,3 +15,9 @@ mary_thinking_general:
|
|||
author_id: 2
|
||||
post_id: 2
|
||||
category_id: 1
|
||||
|
||||
bob_misc_by_bob_technology:
|
||||
id: 4
|
||||
author_id: 3
|
||||
post_id: 8
|
||||
category_id: 2
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
boring_club:
|
||||
name: Banana appreciation society
|
||||
category_id: 1
|
||||
moustache_club:
|
||||
name: Moustache and Eyebrow Fancier Club
|
||||
crazy_club:
|
||||
name: Skull and bones
|
||||
name: Skull and bones
|
||||
category_id: 2
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
david_modest_proposal:
|
||||
name: A Modest Proposal
|
||||
writer_type: Author
|
||||
writer_id: David
|
||||
category_id: General
|
||||
author_id: David
|
|
@ -0,0 +1,8 @@
|
|||
groucho:
|
||||
id: 1
|
||||
member_id: 1
|
||||
organization: nsa
|
||||
some_other_guy:
|
||||
id: 2
|
||||
member_id: 2
|
||||
organization: nsa
|
|
@ -6,3 +6,6 @@ some_other_guy:
|
|||
id: 2
|
||||
name: Englebert Humperdink
|
||||
member_type_id: 2
|
||||
blarpy_winkup:
|
||||
id: 3
|
||||
name: Blarpy Winkup
|
||||
|
|
|
@ -18,3 +18,10 @@ other_guys_membership:
|
|||
member_id: 2
|
||||
favourite: false
|
||||
type: CurrentMembership
|
||||
|
||||
blarpy_winkup_crazy_club:
|
||||
joined_on: <%= 4.weeks.ago.to_s(:db) %>
|
||||
club: crazy_club
|
||||
member_id: 3
|
||||
favourite: false
|
||||
type: CurrentMembership
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
blackbeard:
|
||||
owner_id: 1
|
||||
name: blackbeard
|
||||
essay_id: A Modest Proposal
|
||||
|
||||
ashley:
|
||||
owner_id: 2
|
||||
|
|
|
@ -52,3 +52,31 @@ eager_other:
|
|||
title: eager loading with OR'd conditions
|
||||
body: hello
|
||||
type: Post
|
||||
|
||||
misc_by_bob:
|
||||
id: 8
|
||||
author_id: 3
|
||||
title: misc post by bob
|
||||
body: hello
|
||||
type: Post
|
||||
|
||||
misc_by_mary:
|
||||
id: 9
|
||||
author_id: 2
|
||||
title: misc post by mary
|
||||
body: hello
|
||||
type: Post
|
||||
|
||||
other_by_bob:
|
||||
id: 10
|
||||
author_id: 3
|
||||
title: other post by bob
|
||||
body: hello
|
||||
type: Post
|
||||
|
||||
other_by_mary:
|
||||
id: 11
|
||||
author_id: 2
|
||||
title: other post by mary
|
||||
body: hello
|
||||
type: Post
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
normal_comment_rating:
|
||||
id: 1
|
||||
comment_id: 8
|
||||
value: 1
|
||||
|
||||
special_comment_rating:
|
||||
id: 2
|
||||
comment_id: 6
|
||||
value: 1
|
||||
|
||||
sub_special_comment_rating:
|
||||
id: 3
|
||||
comment_id: 12
|
||||
value: 1
|
|
@ -26,3 +26,53 @@ godfather:
|
|||
orphaned:
|
||||
id: 5
|
||||
tag_id: 1
|
||||
|
||||
misc_post_by_bob:
|
||||
id: 6
|
||||
tag_id: 2
|
||||
taggable_id: 8
|
||||
taggable_type: Post
|
||||
|
||||
misc_post_by_mary:
|
||||
id: 7
|
||||
tag_id: 2
|
||||
taggable_id: 9
|
||||
taggable_type: Post
|
||||
|
||||
misc_by_bob_blue_first:
|
||||
id: 8
|
||||
tag_id: 3
|
||||
taggable_id: 8
|
||||
taggable_type: Post
|
||||
comment: first
|
||||
|
||||
misc_by_bob_blue_second:
|
||||
id: 9
|
||||
tag_id: 3
|
||||
taggable_id: 8
|
||||
taggable_type: Post
|
||||
comment: second
|
||||
|
||||
other_by_bob_blue:
|
||||
id: 10
|
||||
tag_id: 3
|
||||
taggable_id: 10
|
||||
taggable_type: Post
|
||||
comment: first
|
||||
|
||||
other_by_mary_blue:
|
||||
id: 11
|
||||
tag_id: 3
|
||||
taggable_id: 11
|
||||
taggable_type: Post
|
||||
comment: first
|
||||
|
||||
special_comment_rating:
|
||||
id: 12
|
||||
taggable_id: 2
|
||||
taggable_type: Rating
|
||||
|
||||
normal_comment_rating:
|
||||
id: 13
|
||||
taggable_id: 1
|
||||
taggable_type: Rating
|
||||
|
|
|
@ -4,4 +4,8 @@ general:
|
|||
|
||||
misc:
|
||||
id: 2
|
||||
name: Misc
|
||||
name: Misc
|
||||
|
||||
blue:
|
||||
id: 3
|
||||
name: Blue
|
||||
|
|
|
@ -94,16 +94,50 @@ class Author < ActiveRecord::Base
|
|||
has_many :author_favorites
|
||||
has_many :favorite_authors, :through => :author_favorites, :order => 'name'
|
||||
|
||||
has_many :tagging, :through => :posts # through polymorphic has_one
|
||||
has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many
|
||||
has_many :tags, :through => :posts # through has_many :through
|
||||
has_many :tagging, :through => :posts
|
||||
has_many :taggings, :through => :posts
|
||||
has_many :tags, :through => :posts
|
||||
has_many :similar_posts, :through => :tags, :source => :tagged_posts, :uniq => true
|
||||
has_many :distinct_tags, :through => :posts, :source => :tags, :select => "DISTINCT tags.*", :order => "tags.name"
|
||||
has_many :post_categories, :through => :posts, :source => :categories
|
||||
has_many :tagging_tags, :through => :taggings, :source => :tag
|
||||
has_many :tags_with_primary_key, :through => :posts
|
||||
|
||||
has_many :books
|
||||
has_many :subscriptions, :through => :books
|
||||
has_many :subscribers, :through => :subscriptions, :order => "subscribers.nick" # through has_many :through (on through reflection)
|
||||
has_many :distinct_subscribers, :through => :subscriptions, :source => :subscriber, :select => "DISTINCT subscribers.*", :order => "subscribers.nick"
|
||||
|
||||
has_one :essay, :primary_key => :name, :as => :writer
|
||||
has_one :essay_category, :through => :essay, :source => :category
|
||||
has_one :essay_owner, :through => :essay, :source => :owner
|
||||
|
||||
belongs_to :author_address, :dependent => :destroy
|
||||
has_one :essay_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id
|
||||
has_one :essay_category_2, :through => :essay_2, :source => :category
|
||||
|
||||
has_many :essays, :primary_key => :name, :as => :writer
|
||||
has_many :essay_categories, :through => :essays, :source => :category
|
||||
has_many :essay_owners, :through => :essays, :source => :owner
|
||||
|
||||
has_many :essays_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id
|
||||
has_many :essay_categories_2, :through => :essays_2, :source => :category
|
||||
|
||||
belongs_to :owned_essay, :primary_key => :name, :class_name => 'Essay'
|
||||
has_one :owned_essay_category, :through => :owned_essay, :source => :category
|
||||
|
||||
belongs_to :author_address, :dependent => :destroy
|
||||
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
|
||||
|
||||
has_many :post_categories, :through => :posts, :source => :categories
|
||||
has_many :category_post_comments, :through => :categories, :source => :post_comments
|
||||
|
||||
has_many :misc_posts, :class_name => 'Post',
|
||||
:conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }
|
||||
has_many :misc_post_first_blue_tags, :through => :misc_posts, :source => :first_blue_tags
|
||||
|
||||
has_many :misc_post_first_blue_tags_2, :through => :posts, :source => :first_blue_tags_2,
|
||||
:conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }
|
||||
|
||||
scope :relation_include_posts, includes(:posts)
|
||||
scope :relation_include_tags, includes(:tags)
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Book < ActiveRecord::Base
|
||||
has_many :authors
|
||||
|
||||
has_many :citations, :foreign_key => 'book1_id'
|
||||
has_many :references, :through => :citations, :source => :reference_of, :uniq => true
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ class Categorization < ActiveRecord::Base
|
|||
belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name
|
||||
belongs_to :author
|
||||
|
||||
has_many :post_taggings, :through => :author, :source => :taggings
|
||||
|
||||
belongs_to :author_using_custom_pk, :class_name => 'Author', :foreign_key => :author_id, :primary_key => :author_address_extra_id
|
||||
has_many :authors_using_custom_pk, :class_name => 'Author', :foreign_key => :id, :primary_key => :category_id
|
||||
end
|
||||
|
|
|
@ -22,6 +22,8 @@ class Category < ActiveRecord::Base
|
|||
end
|
||||
|
||||
has_many :categorizations
|
||||
has_many :post_comments, :through => :posts, :source => :comments
|
||||
|
||||
has_many :authors, :through => :categorizations
|
||||
has_many :authors_with_select, :through => :categorizations, :source => :author, :select => 'authors.*, categorizations.post_id'
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ class Club < ActiveRecord::Base
|
|||
has_many :current_memberships
|
||||
has_one :sponsor
|
||||
has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member"
|
||||
belongs_to :category
|
||||
|
||||
private
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ class Comment < ActiveRecord::Base
|
|||
:conditions => { "posts.author_id" => 1 }
|
||||
|
||||
belongs_to :post, :counter_cache => true
|
||||
has_many :ratings
|
||||
|
||||
def self.what_are_you
|
||||
'a comment...'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
class Essay < ActiveRecord::Base
|
||||
belongs_to :writer, :primary_key => :name, :polymorphic => true
|
||||
belongs_to :category, :primary_key => :name
|
||||
has_one :owner, :primary_key => :name
|
||||
end
|
||||
|
|
|
@ -2,4 +2,6 @@ class Job < ActiveRecord::Base
|
|||
has_many :references
|
||||
has_many :people, :through => :references
|
||||
belongs_to :ideal_reference, :class_name => 'Reference'
|
||||
|
||||
has_many :agents, :through => :people
|
||||
end
|
||||
|
|
|
@ -11,6 +11,17 @@ class Member < ActiveRecord::Base
|
|||
has_one :organization, :through => :member_detail
|
||||
belongs_to :member_type
|
||||
|
||||
has_many :nested_member_types, :through => :member_detail, :source => :member_type
|
||||
has_one :nested_member_type, :through => :member_detail, :source => :member_type
|
||||
|
||||
has_many :nested_sponsors, :through => :sponsor_club, :source => :sponsor
|
||||
has_one :nested_sponsor, :through => :sponsor_club, :source => :sponsor
|
||||
|
||||
has_many :organization_member_details, :through => :member_detail
|
||||
has_many :organization_member_details_2, :through => :organization, :source => :member_details
|
||||
|
||||
has_one :club_category, :through => :club, :source => :category
|
||||
|
||||
has_many :current_memberships
|
||||
has_one :club_through_many, :through => :current_memberships, :source => :club
|
||||
|
||||
|
|
|
@ -2,4 +2,6 @@ class MemberDetail < ActiveRecord::Base
|
|||
belongs_to :member
|
||||
belongs_to :organization
|
||||
has_one :member_type, :through => :member
|
||||
|
||||
has_many :organization_member_details, :through => :organization, :source => :member_details
|
||||
end
|
||||
|
|
|
@ -2,5 +2,11 @@ class Organization < ActiveRecord::Base
|
|||
has_many :member_details
|
||||
has_many :members, :through => :member_details
|
||||
|
||||
has_many :authors, :primary_key => :name
|
||||
has_many :author_essay_categories, :through => :authors, :source => :essay_categories
|
||||
|
||||
has_one :author, :primary_key => :name
|
||||
has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category
|
||||
|
||||
scope :clubs, { :from => 'clubs' }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,6 +21,9 @@ class Person < ActiveRecord::Base
|
|||
has_many :agents_of_agents, :through => :agents, :source => :agents
|
||||
belongs_to :number1_fan, :class_name => 'Person'
|
||||
|
||||
has_many :agents_posts, :through => :agents, :source => :posts
|
||||
has_many :agents_posts_authors, :through => :agents_posts, :source => :author
|
||||
|
||||
scope :males, :conditions => { :gender => 'M' }
|
||||
scope :females, :conditions => { :gender => 'F' }
|
||||
end
|
||||
|
|
|
@ -49,6 +49,9 @@ class Post < ActiveRecord::Base
|
|||
has_many :special_comments
|
||||
has_many :nonexistant_comments, :class_name => 'Comment', :conditions => 'comments.id < 0'
|
||||
|
||||
has_many :special_comments_ratings, :through => :special_comments, :source => :ratings
|
||||
has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings
|
||||
|
||||
has_and_belongs_to_many :categories
|
||||
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
|
||||
|
||||
|
@ -70,11 +73,17 @@ class Post < ActiveRecord::Base
|
|||
has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy
|
||||
has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify
|
||||
|
||||
has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'"
|
||||
has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => { :tags => { :name => 'Misc' } }
|
||||
has_many :funky_tags, :through => :taggings, :source => :tag
|
||||
has_many :super_tags, :through => :taggings
|
||||
has_many :tags_with_primary_key, :through => :taggings, :source => :tag_with_primary_key
|
||||
has_one :tagging, :as => :taggable
|
||||
|
||||
has_many :first_taggings, :as => :taggable, :class_name => 'Tagging', :conditions => { :taggings => { :comment => 'first' } }
|
||||
has_many :first_blue_tags, :through => :first_taggings, :source => :tag, :conditions => { :tags => { :name => 'Blue' } }
|
||||
|
||||
has_many :first_blue_tags_2, :through => :taggings, :source => :blue_tag, :conditions => { :taggings => { :comment => 'first' } }
|
||||
|
||||
has_many :invalid_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0'
|
||||
has_many :invalid_tags, :through => :invalid_taggings, :source => :tag
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
class Rating < ActiveRecord::Base
|
||||
belongs_to :comment
|
||||
has_many :taggings, :as => :taggable
|
||||
end
|
|
@ -2,6 +2,8 @@ class Reference < ActiveRecord::Base
|
|||
belongs_to :person
|
||||
belongs_to :job
|
||||
|
||||
has_many :agents_posts_authors, :through => :person
|
||||
|
||||
class << self
|
||||
attr_accessor :make_comments
|
||||
end
|
||||
|
|
|
@ -6,6 +6,8 @@ class Tagging < ActiveRecord::Base
|
|||
belongs_to :tag, :include => :tagging
|
||||
belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id'
|
||||
belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
|
||||
belongs_to :blue_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => { :tags => { :name => 'Blue' } }
|
||||
belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
|
||||
belongs_to :interpolated_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => proc { "1 = #{1}" }
|
||||
belongs_to :taggable, :polymorphic => true, :counter_cache => true
|
||||
has_many :things, :through => :taggable
|
||||
|
|
|
@ -49,6 +49,8 @@ ActiveRecord::Schema.define do
|
|||
t.string :name, :null => false
|
||||
t.integer :author_address_id
|
||||
t.integer :author_address_extra_id
|
||||
t.string :organization_id
|
||||
t.string :owned_essay_id
|
||||
end
|
||||
|
||||
create_table :author_addresses, :force => true do |t|
|
||||
|
@ -75,6 +77,7 @@ ActiveRecord::Schema.define do
|
|||
end
|
||||
|
||||
create_table :books, :force => true do |t|
|
||||
t.integer :author_id
|
||||
t.column :name, :string
|
||||
end
|
||||
|
||||
|
@ -123,6 +126,7 @@ ActiveRecord::Schema.define do
|
|||
|
||||
create_table :clubs, :force => true do |t|
|
||||
t.string :name
|
||||
t.integer :category_id
|
||||
end
|
||||
|
||||
create_table :collections, :force => true do |t|
|
||||
|
@ -216,6 +220,8 @@ ActiveRecord::Schema.define do
|
|||
t.string :name
|
||||
t.string :writer_id
|
||||
t.string :writer_type
|
||||
t.string :category_id
|
||||
t.string :author_id
|
||||
end
|
||||
|
||||
create_table :events, :force => true do |t|
|
||||
|
@ -393,6 +399,7 @@ ActiveRecord::Schema.define do
|
|||
t.string :name
|
||||
t.column :updated_at, :datetime
|
||||
t.column :happy_at, :datetime
|
||||
t.string :essay_id
|
||||
end
|
||||
|
||||
create_table :paint_colors, :force => true do |t|
|
||||
|
@ -482,6 +489,11 @@ ActiveRecord::Schema.define do
|
|||
t.string :type
|
||||
end
|
||||
|
||||
create_table :ratings, :force => true do |t|
|
||||
t.integer :comment_id
|
||||
t.integer :value
|
||||
end
|
||||
|
||||
create_table :readers, :force => true do |t|
|
||||
t.integer :post_id, :null => false
|
||||
t.integer :person_id, :null => false
|
||||
|
@ -553,6 +565,7 @@ ActiveRecord::Schema.define do
|
|||
t.column :super_tag_id, :integer
|
||||
t.column :taggable_type, :string
|
||||
t.column :taggable_id, :integer
|
||||
t.string :comment
|
||||
end
|
||||
|
||||
create_table :tasks, :force => true do |t|
|
||||
|
@ -677,6 +690,9 @@ ActiveRecord::Schema.define do
|
|||
t.integer :molecule_id
|
||||
t.string :name
|
||||
end
|
||||
create_table :weirds, :force => true do |t|
|
||||
t.string 'a$b'
|
||||
end
|
||||
|
||||
except 'SQLite' do
|
||||
# fk_test_has_fk should be before fk_test_has_pk
|
||||
|
|
|
@ -29,7 +29,7 @@ module ActiveSupport
|
|||
def convert_json_to_yaml(json) #:nodoc:
|
||||
require 'strscan' unless defined? ::StringScanner
|
||||
scanner, quoting, marks, pos, times = ::StringScanner.new(json), false, [], nil, []
|
||||
while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
|
||||
while scanner.scan_until(/(\\['"]|['":,\\]|\\.|[\]])/)
|
||||
case char = scanner[1]
|
||||
when '"', "'"
|
||||
if !quoting
|
||||
|
@ -43,7 +43,7 @@ module ActiveSupport
|
|||
end
|
||||
quoting = false
|
||||
end
|
||||
when ":",","
|
||||
when ":",",", "]"
|
||||
marks << scanner.pos - 1 unless quoting
|
||||
when "\\"
|
||||
scanner.skip(/\\/)
|
||||
|
@ -70,9 +70,11 @@ module ActiveSupport
|
|||
left_pos.each_with_index do |left, i|
|
||||
scanner.pos = left.succ
|
||||
chunk = scanner.peek(right_pos[i] - scanner.pos + 1)
|
||||
# overwrite the quotes found around the dates with spaces
|
||||
while times.size > 0 && times[0] <= right_pos[i]
|
||||
chunk.insert(times.shift - scanner.pos - 1, '! ')
|
||||
if ActiveSupport.parse_json_times
|
||||
# overwrite the quotes found around the dates with spaces
|
||||
while times.size > 0 && times[0] <= right_pos[i]
|
||||
chunk.insert(times.shift - scanner.pos - 1, '! ')
|
||||
end
|
||||
end
|
||||
chunk.gsub!(/\\([\\\/]|u[[:xdigit:]]{4})/) do
|
||||
ustr = $1
|
||||
|
|
|
@ -17,6 +17,8 @@ class TestJSONDecoding < ActiveSupport::TestCase
|
|||
%({"matzue": "松江", "asakusa": "浅草"}) => {"matzue" => "松江", "asakusa" => "浅草"},
|
||||
%({"a": "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
|
||||
%({"a": "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
|
||||
%(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)],
|
||||
%(["2007-01-01 01:12:34 Z", "2007-01-01 01:12:35 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34), Time.utc(2007, 1, 1, 1, 12, 35)],
|
||||
# no time zone
|
||||
%({"a": "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
|
||||
# invalid date
|
||||
|
@ -72,13 +74,11 @@ class TestJSONDecoding < ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if backends.include?("JSONGem")
|
||||
test "json decodes time json with time parsing disabled" do
|
||||
test "json decodes time json with time parsing disabled with the #{backend} backend" do
|
||||
ActiveSupport.parse_json_times = false
|
||||
expected = {"a" => "2007-01-01 01:12:34 Z"}
|
||||
ActiveSupport::JSON.with_backend "JSONGem" do
|
||||
ActiveSupport::JSON.with_backend backend do
|
||||
assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -747,6 +747,22 @@ h4. Specifying Conditions on Eager Loaded Associations
|
|||
|
||||
Even though Active Record lets you specify conditions on the eager loaded associations just like +joins+, the recommended way is to use "joins":#joining-tables instead.
|
||||
|
||||
However if you must do this, you may use +where+ as you would normally.
|
||||
|
||||
<ruby>
|
||||
Post.includes(:comments).where("comments.visible", true)
|
||||
</ruby>
|
||||
|
||||
This would generate a query which contains a +LEFT OUTER JOIN+ whereas the +joins+ method would generate one using the +INNER JOIN+ function instead.
|
||||
|
||||
<ruby>
|
||||
SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible)
|
||||
</ruby>
|
||||
|
||||
If there was no +where+ condition, this would generate the normal set of two queries.
|
||||
|
||||
If, in the case of this +includes+ query, there were no comments for any posts, all the posts would still be loaded. By using +joins+ (an INNER JOIN), the join conditions *must* match, otherwise no records will be returned.
|
||||
|
||||
h3. Scopes
|
||||
|
||||
Scoping allows you to specify commonly-used ARel queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as +where+, +joins+ and +includes+. All scope methods will return an +ActiveRecord::Relation+ object which will allow for further methods (such as other scopes) to be called on it.
|
||||
|
|
|
@ -65,7 +65,7 @@ end
|
|||
|
||||
If you want a more complicated expiration scheme, you can use cache sweepers to expire cached objects when things change. This is covered in the section on Sweepers.
|
||||
|
||||
Note: Page caching ignores all parameters. For example +/products?page=1+ will be written out to the filesystem as +products.html+ with no reference to the +page+ parameter. Thus, if someone requests +/products?page=2+ later, they will get the cached first page. Be careful when page caching GET parameters in the URL!
|
||||
NOTE: Page caching ignores all parameters. For example +/products?page=1+ will be written out to the filesystem as +products.html+ with no reference to the +page+ parameter. Thus, if someone requests +/products?page=2+ later, they will get the cached first page. Be careful when page caching GET parameters in the URL!
|
||||
|
||||
INFO: Page caching runs in an after filter. Thus, invalid requests won't generate spurious cache entries as long as you halt them. Typically, a redirection in some before filter that checks request preconditions does the job.
|
||||
|
||||
|
|
|
@ -371,7 +371,7 @@ Now create a ticket with your patch. Go to the "new ticket":http://rails.lightho
|
|||
|
||||
h4. Get Some Feedback
|
||||
|
||||
Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the rubyonrails-core mailing list or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know.
|
||||
Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the "rubyonrails-core mailing list":http://groups.google.com/group/rubyonrails-core/ or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know.
|
||||
|
||||
h4. Iterate as Necessary
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ Usually run this as the root user:
|
|||
# gem install rails
|
||||
</shell>
|
||||
|
||||
TIP. If you're working on Windows, you should be aware that the vast majority of Rails development is done in Unix environments. While Ruby and Rails themselves install easily using for example "Ruby Installer":http://rubyinstaller.org/, the supporting ecosystem often assumes you are able to build C-based rubygems and work in a command window. If at all possible, we suggest that you install a Linux virtual machine and use that for Rails development, instead of using Windows.
|
||||
TIP. If you're working on Windows, you can quickly install Ruby and Rails with "Rails Installer":http://railsinstaller.org.
|
||||
|
||||
h4. Creating the Blog Application
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ task :default => :test
|
|||
end
|
||||
|
||||
def generate_test_dummy(force = false)
|
||||
opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript)
|
||||
opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip)
|
||||
opts[:force] = force
|
||||
|
||||
invoke Rails::Generators::AppGenerator,
|
||||
|
|
|
@ -31,26 +31,34 @@ module Rails
|
|||
app_generators(&block)
|
||||
end
|
||||
|
||||
# First configurable block to run. Called before any initializers are run.
|
||||
def before_configuration(&block)
|
||||
ActiveSupport.on_load(:before_configuration, :yield => true, &block)
|
||||
end
|
||||
|
||||
# Third configurable block to run. Does not run if config.cache_classes
|
||||
# set to false.
|
||||
def before_eager_load(&block)
|
||||
ActiveSupport.on_load(:before_eager_load, :yield => true, &block)
|
||||
end
|
||||
|
||||
# Second configurable block to run. Called before frameworks initialize.
|
||||
def before_initialize(&block)
|
||||
ActiveSupport.on_load(:before_initialize, :yield => true, &block)
|
||||
end
|
||||
|
||||
# Last configurable block to run. Called after frameworks initialize.
|
||||
def after_initialize(&block)
|
||||
ActiveSupport.on_load(:after_initialize, :yield => true, &block)
|
||||
end
|
||||
|
||||
# Array of callbacks defined by #to_prepare.
|
||||
def to_prepare_blocks
|
||||
@@to_prepare_blocks ||= []
|
||||
end
|
||||
|
||||
# Defines generic callbacks to run before #after_initialize. Useful for
|
||||
# Rails::Railtie subclasses.
|
||||
def to_prepare(&blk)
|
||||
to_prepare_blocks << blk if blk
|
||||
end
|
||||
|
|
|
@ -26,8 +26,7 @@ module SharedGeneratorTests
|
|||
|
||||
def test_plugin_new_generate_pretend
|
||||
run_generator ["testapp", "--pretend"]
|
||||
|
||||
default_files.each{ |path| assert_no_file path }
|
||||
default_files.each{ |path| assert_no_file File.join("testapp",path) }
|
||||
end
|
||||
|
||||
def test_invalid_database_option_raises_an_error
|
||||
|
|
Loading…
Reference in New Issue