diff options
15 files changed, 339 insertions, 84 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index af15dc37c9..037131d610 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,51 +1,5 @@ ## Rails 4.0.0 (unreleased) ## -* AR::Relation#order: make new order prepend old one. - - User.order("name asc").order("created_at desc") - # SELECT * FROM users ORDER BY created_at desc, name asc - - This also affects order defined in `default_scope` or any kind of associations. - - *Bogdan Gusiev* - -* `Model.all` now returns an `ActiveRecord::Relation`, rather than an - array of records. Use `Model.to_a` or `Relation#to_a` if you really - want an array. - - In some specific cases, this may cause breakage when upgrading. - However in most cases the `ActiveRecord::Relation` will just act as a - lazy-loaded array and there will be no problems. - - Note that calling `Model.all` with options (e.g. - `Model.all(conditions: '...')` was already deprecated, but it will - still return an array in order to make the transition easier. - - `Model.scoped` is deprecated in favour of `Model.all`. - - `Relation#all` still returns an array, but is deprecated (since it - would serve no purpose if we made it return a `Relation`). - - *Jon Leighton* - -* Added an `update_columns` method. This new method updates the given attributes on an object, - without calling save, hence skipping validations and callbacks. - Example: - - User.first.update_columns(name: "sebastian", age: 25) # => true - - *Sebastian Martinez + Rafael Mendonça França* - -* Removed `:finder_sql` and `:counter_sql` collection association options. Please - use scopes instead. - - *Jon Leighton* - -* Removed `:insert_sql` and `:delete_sql` `has_and_belongs_to_many` - association options. Please use `has_many :through` instead. - - *Jon Leighton* - * The migration generator now creates a join table with (commented) indexes every time the migration name contains the word `join_table`: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index edf82eb170..774b1ec201 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1108,6 +1108,15 @@ module ActiveRecord # a +belongs_to+, and the records which get deleted are the join records, rather than # the associated records. # + # [:finder_sql] + # Specify a complete SQL statement to fetch the association. This is a good way to go for complex + # associations that depend on multiple tables. May be supplied as a string or a proc where interpolation is + # required. Note: When this option is used, +find_in_collection+ + # is _not_ added. + # [:counter_sql] + # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is + # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by + # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. # [:extend] # Specify a named module for extending the proxy. See "Association extensions". # [:include] @@ -1179,6 +1188,14 @@ module ActiveRecord # has_many :tags, :as => :taggable # has_many :reports, :readonly => true # has_many :subscribers, :through => :subscriptions, :source => :user + # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { + # %Q{ + # SELECT DISTINCT * + # FROM people p, post_subscriptions ps + # WHERE ps.post_id = #{id} AND ps.person_id = p.id + # ORDER BY p.first_name + # } + # } def has_many(name, scope = nil, options = {}, &extension) Builder::HasMany.build(self, name, scope, options, &extension) end @@ -1542,6 +1559,18 @@ module ActiveRecord # such as <tt>last_name, first_name DESC</tt> # [:uniq] # If true, duplicate associated objects will be ignored by accessors and query methods. + # [:finder_sql] + # Overwrite the default generated SQL statement used to fetch the association with a manual statement + # [:counter_sql] + # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is + # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by + # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. + # [:delete_sql] + # Overwrite the default generated SQL statement used to remove links between the associated + # classes with a manual statement. + # [:insert_sql] + # Overwrite the default generated SQL statement used to add links between the associated classes + # with a manual statement. # [:extend] # Anonymous module for extending the proxy, see "Association extensions". # [:include] @@ -1578,6 +1607,8 @@ module ActiveRecord # has_and_belongs_to_many :nations, :class_name => "Country" # has_and_belongs_to_many :categories, :join_table => "prods_cats" # has_and_belongs_to_many :categories, :readonly => true + # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => + # proc { |record| "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" } def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 9e464ff681..14755602da 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -140,6 +140,14 @@ module ActiveRecord reset end + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + owner.send(:instance_exec, record, &sql) + else + sql + end + end + # We can't dump @reflection since it contains the scope proc def marshal_dump reflection = @reflection diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index cb97490ef1..1303822868 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -5,7 +5,7 @@ module ActiveRecord attr_reader :association, :alias_tracker - delegate :klass, :owner, :reflection, :to => :association + delegate :klass, :owner, :reflection, :interpolate, :to => :association delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection def initialize(association) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index b28d6a746c..af81af4ad2 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -3,7 +3,7 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def valid_options - super + [:table_name, :before_add, :after_add, :before_remove, :after_remove] + super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove] end attr_reader :block_extension, :extension_module diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index a30e2dab26..7f79ef25f2 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:join_table, :association_foreign_key] + super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql] end def build diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 30522e3a5d..14a24e02dc 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -44,7 +44,7 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader - if loaded? + if loaded? || options[:finder_sql] load_target.map do |record| record.send(reflection.association_primary_key) end @@ -79,7 +79,11 @@ module ActiveRecord if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } else - scoped.find(*args) + if options[:finder_sql] + find_by_scan(*args) + else + scoped.find(*args) + end end end @@ -166,26 +170,35 @@ module ActiveRecord end end - # Count all records using SQL. Construct options and pass them with + # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the + # association, it will be used for the query. Otherwise, construct options and pass them with # scope to the target class's +count+. def count(column_name = nil, count_options = {}) column_name, count_options = nil, column_name if column_name.is_a?(Hash) - if association_scope.uniq_value - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= reflection.klass.primary_key - count_options[:distinct] = true - end + if options[:counter_sql] || options[:finder_sql] + unless count_options.blank? + raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" + end - value = scoped.count(column_name, count_options) + reflection.klass.count_by_sql(custom_counter_sql) + else + if association_scope.uniq_value + # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. + column_name ||= reflection.klass.primary_key + count_options[:distinct] = true + end - limit = options[:limit] - offset = options[:offset] + value = scoped.count(column_name, count_options) - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value + limit = options[:limit] + offset = options[:offset] + + if limit || offset + [ [value - offset.to_i, 0].max, limit.to_i ].min + else + value + end end end @@ -310,6 +323,7 @@ module ActiveRecord if record.new_record? include_in_memory?(record) else + load_target if options[:finder_sql] loaded? ? target.include?(record) : scoped.exists?(record) end else @@ -344,8 +358,31 @@ module ActiveRecord private + def custom_counter_sql + if options[:counter_sql] + interpolate(options[:counter_sql]) + else + # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ + interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do + count_with = $2.to_s + count_with = '*' if count_with.blank? || count_with =~ /,/ + "SELECT #{$1}COUNT(#{count_with}) FROM" + end + end + end + + def custom_finder_sql + interpolate(options[:finder_sql]) + end + def find_target - records = scoped.to_a + records = + if options[:finder_sql] + reflection.klass.find_by_sql(custom_finder_sql) + else + scoped.all + end + records.each { |record| set_inverse_instance(record) } records end @@ -484,6 +521,7 @@ module ActiveRecord # Otherwise, go to the database only if none of the following are true: # * target already loaded # * owner is new record + # * custom :finder_sql exists # * target contains new or changed record(s) # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) @@ -492,6 +530,7 @@ module ActiveRecord else !(loaded? || owner.new_record? || + options[:finder_sql] || target.any? { |record| record.new_record? || record.changed? } || args.first.kind_of?(Integer)) end @@ -508,6 +547,20 @@ module ActiveRecord end end + # If using a custom finder_sql, #find scans the entire collection. + def find_by_scan(*args) + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq + + if ids.size == 1 + id = ids.first + record = load_target.detect { |r| id == r.id } + expects_array ? [ record ] : record + else + load_target.select { |r| ids.include?(r.id) } + end + end + # Fetches the first/last using SQL if possible, otherwise from the target array. def first_or_last(type, *args) args.shift if args.first.is_a?(Hash) && args.first.empty? diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index e5b40f3911..93618721bb 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -18,12 +18,16 @@ module ActiveRecord end end - stmt = join_table.compile_insert( - join_table[reflection.foreign_key] => owner.id, - join_table[reflection.association_foreign_key] => record.id - ) + if options[:insert_sql] + owner.connection.insert(interpolate(options[:insert_sql], record)) + else + stmt = join_table.compile_insert( + join_table[reflection.foreign_key] => owner.id, + join_table[reflection.association_foreign_key] => record.id + ) - owner.connection.insert stmt + owner.connection.insert stmt + end record end @@ -35,17 +39,22 @@ module ActiveRecord end def delete_records(records, method) - relation = join_table - condition = relation[reflection.foreign_key].eq(owner.id) - - unless records == :all - condition = condition.and( - relation[reflection.association_foreign_key] - .in(records.map { |x| x.id }.compact) - ) - end + if sql = options[:delete_sql] + records = load_target if records == :all + records.each { |record| owner.connection.delete(interpolate(sql, record)) } + else + relation = join_table + condition = relation[reflection.foreign_key].eq(owner.id) - owner.connection.delete(relation.where(condition).compile_delete) + unless records == :all + condition = condition.and( + relation[reflection.association_foreign_key] + .in(records.map { |x| x.id }.compact) + ) + end + + owner.connection.delete(relation.where(condition).compile_delete) + end end def invertible_for?(record) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 41c6ca92cc..669b7e03c2 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -33,7 +33,13 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = has_cached_counter? ? owner[cached_counter_attribute_name] : scoped.count + count = if has_cached_counter? + owner.send(:read_attribute, cached_counter_attribute_name) + elsif options[:counter_sql] || options[:finder_sql] + reflection.klass.count_by_sql(custom_counter_sql) + else + scoped.count + end # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index e48ac84592..9846d81a9b 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -65,6 +65,16 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base :foreign_key => "developer_id" end +class DeveloperWithCounterSQL < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, + :class_name => "DeveloperWithCounterSQL", + :join_table => "developers_projects", + :association_foreign_key => "project_id", + :foreign_key => "developer_id", + :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" } +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings @@ -351,6 +361,31 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, david.projects(true).size end + def test_deleting_with_sql + david = Developer.find(1) + active_record = Project.find(1) + active_record.developers.reload + assert_equal 3, active_record.developers_by_sql.size + + active_record.developers_by_sql.delete(david) + assert_equal 2, active_record.developers_by_sql(true).size + end + + def test_deleting_array_with_sql + active_record = Project.find(1) + active_record.developers.reload + assert_equal 3, active_record.developers_by_sql.size + + active_record.developers_by_sql.delete(Developer.all) + assert_equal 0, active_record.developers_by_sql(true).size + end + + def test_deleting_all_with_sql + project = Project.find(1) + project.developers_by_sql.delete_all + assert_equal 0, project.developers_by_sql.size + end + def test_deleting_all david = Developer.find(1) david.projects.reload @@ -499,6 +534,25 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert ! project.developers.include?(developer) end + def test_find_in_association_with_custom_finder_sql + assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find" + + active_record = projects(:active_record) + active_record.developers_with_finder_sql.reload + assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find" + end + + def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations + # interpolate once: + assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation" + # interpolate again, for a different project id + assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation" + end + + def test_find_in_association_with_custom_finder_sql_and_string_id + assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find" + end + def test_find_with_merged_options assert_equal 1, projects(:active_record).limited_developers.size assert_equal 1, projects(:active_record).limited_developers.to_a.size @@ -734,6 +788,21 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, david.projects.count end + def test_count_with_counter_sql + developer = DeveloperWithCounterSQL.create(:name => 'tekin') + developer.project_ids = [projects(:active_record).id] + developer.save + developer.reload + assert_equal 1, developer.projects.count + end + + unless current_adapter?(:PostgreSQLAdapter) + def test_count_with_finder_sql + assert_equal 3, projects(:active_record).developers_with_finder_sql.count + assert_equal 3, projects(:active_record).developers_with_multiline_finder_sql.count + end + end + def test_association_proxy_transaction_method_starts_transaction_in_association_class Post.expects(:transaction) Category.first.posts.transaction do diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 6b675d3d54..4d6d6a24fb 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -20,6 +20,43 @@ require 'models/car' require 'models/bulb' require 'models/engine' +class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" + end + def test_should_fail + assert_raise(ArgumentError) do + Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) + end + end +end + +class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items" + end + def test_should_fail + assert_raise(ArgumentError) do + Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) + end + end +end + +class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items" + end + + def test_should_count_distinct_results + invoice = Invoice.new + invoice.custom_line_items << LineItem.new(:amount => 0) + invoice.custom_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 1, invoice.custom_line_items.count + end +end + class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -270,6 +307,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name end + def test_finding_using_sql + firm = Firm.scoped(:order => "id").first + first_client = firm.clients_using_sql.first + assert_not_nil first_client + assert_equal "Microsoft", first_client.name + assert_equal 1, firm.clients_using_sql.size + assert_equal 1, Firm.scoped(:order => "id").first.clients_using_sql.size + end + + def test_finding_using_sql_take_into_account_only_uniq_ids + firm = Firm.scoped(:order => "id").first + client = firm.clients_using_sql.first + assert_equal client, firm.clients_using_sql.find(client.id, client.id) + assert_equal client, firm.clients_using_sql.find(client.id, client.id.to_s) + end + + def test_counting_using_sql + assert_equal 1, Firm.scoped(:order => "id").first.clients_using_counter_sql.size + assert Firm.scoped(:order => "id").first.clients_using_counter_sql.any? + assert_equal 0, Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.size + assert !Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.any? + end + + def test_counting_non_existant_items_using_sql + assert_equal 0, Firm.scoped(:order => "id").first.no_clients_using_counter_sql.size + end + + def test_counting_using_finder_sql + assert_equal 2, Firm.find(4).clients_using_sql.count + end + def test_belongs_to_sanity c = Client.new assert_nil c.firm @@ -297,6 +365,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) } end + def test_find_string_ids_when_using_finder_sql + firm = Firm.scoped(:order => "id").first + + client = firm.clients_using_finder_sql.find("2") + assert_kind_of Client, client + + client_ary = firm.clients_using_finder_sql.find(["2"]) + assert_kind_of Array, client_ary + assert_equal client, client_ary.first + + client_ary = firm.clients_using_finder_sql.find("2", "3") + assert_kind_of Array, client_ary + assert_equal 2, client_ary.size + assert client_ary.include?(client) + end + def test_find_all firm = Firm.all.merge!(:order => "id").first assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length @@ -1124,6 +1208,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids end + def test_get_ids_for_unloaded_finder_sql_associations_loads_them + company = companies(:first_firm) + assert !company.clients_using_sql.loaded? + assert_equal [companies(:second_client).id], company.clients_using_sql_ids + assert company.clients_using_sql.loaded? + end + def test_get_ids_for_ordered_association assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids end @@ -1184,6 +1275,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert ! firm.clients.loaded? end + def test_include_loads_collection_if_target_uses_finder_sql + firm = companies(:first_firm) + client = firm.clients_using_sql.first + + firm.reload + assert ! firm.clients_using_sql.loaded? + assert firm.clients_using_sql.include?(client) + assert firm.clients_using_sql.loaded? + end + + def test_include_returns_false_for_non_matching_record_to_verify_scoping firm = companies(:first_firm) client = Client.create!(:name => 'Not Associated') diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c803b93746..588da68ec1 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -189,8 +189,8 @@ class ReflectionTest < ActiveRecord::TestCase def test_reflection_of_all_associations # FIXME these assertions bust a lot - assert_equal 34, Firm.reflect_on_all_associations.size - assert_equal 24, Firm.reflect_on_all_associations(:has_many).size + assert_equal 39, Firm.reflect_on_all_associations.size + assert_equal 29, Firm.reflect_on_all_associations(:has_many).size assert_equal 10, Firm.reflect_on_all_associations(:has_one).size assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index ff6b7d085f..4c57cf1050 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -36,7 +36,9 @@ module Namespaced end class Firm < Company - has_many :clients, -> { order "id" }, :dependent => :destroy, + has_many :clients, -> { order "id" }, :dependent => :destroy, :counter_sql => + "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " + + "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )", :before_remove => :log_before_remove, :after_remove => :log_after_remove has_many :unsorted_clients, :class_name => "Client" @@ -51,6 +53,17 @@ class Firm < Company has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client" has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client" + has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" } + has_many :clients_using_counter_sql, :class_name => "Client", + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " }, + :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" } + has_many :clients_using_zero_counter_sql, :class_name => "Client", + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }, + :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" } + has_many :no_clients_using_counter_sql, :class_name => "Client", + :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000', + :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' + has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1' has_many :plain_clients, :class_name => 'Client' has_many :readonly_clients, -> { readonly }, :class_name => 'Client' has_many :clients_using_primary_key, :class_name => 'Client', diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 0d14fa1be1..e5ceda2928 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -11,6 +11,7 @@ module MyApplication has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client" has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client" has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" + has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 6b53ead34d..afe06f4a4c 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -7,6 +7,15 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer" has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer" has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer" + has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" } + has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc { + "SELECT + t.*, j.* + FROM + developers_projects j, + developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" + } + has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" } has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"}, :after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"}, :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, |