diff --git a/lib/has_aux_table.rb b/lib/has_aux_table.rb index b76ae8a..11bd59b 100644 --- a/lib/has_aux_table.rb +++ b/lib/has_aux_table.rb @@ -235,10 +235,20 @@ module HasAuxTable if self.method_defined?(column_name.to_sym) raise "invariant: method #{column_name} already defined" end - - config.define_aux_attribute_delegate(column_name) - config.define_aux_attribute_delegate(:"#{column_name}?") - config.define_aux_attribute_delegate(:"#{column_name}=") + [ + "", + "_in_database", + "?", + "=", + "_changed?", + "_change", + %w[clear_ _change] + ].each do |mod| + prefix, suffix = mod.is_a?(Array) ? mod : ["", mod] + config.define_aux_attribute_delegate( + :"#{prefix}#{column_name}#{suffix}" + ) + end end result diff --git a/lib/has_aux_table/aux_table_config.rb b/lib/has_aux_table/aux_table_config.rb index 8f77fcc..a28504d 100644 --- a/lib/has_aux_table/aux_table_config.rb +++ b/lib/has_aux_table/aux_table_config.rb @@ -190,7 +190,10 @@ module HasAuxTable main_class.define_method(method_name) do |*args, **kwargs, &block| T.bind(self, ActiveRecord::Base) aux_model = config.aux_model_for(self) - T.unsafe(aux_model).public_send(method_name, *args, **kwargs, &block) + ret = + T.unsafe(aux_model).public_send(method_name, *args, **kwargs, &block) + puts "#{self.class.name}##{method_name} -> #{config.aux_association_name} -> (#{args.inspect} => #{ret})" + ret end end diff --git a/spec/active_record/has_aux_table_spec.rb b/spec/active_record/has_aux_table_spec.rb index c408dd4..dd4da66 100644 --- a/spec/active_record/has_aux_table_spec.rb +++ b/spec/active_record/has_aux_table_spec.rb @@ -879,25 +879,79 @@ RSpec.describe HasAuxTable do end describe "counter cache" do - before do - @reader = Reader.create!(name: "John Doe", reading_speed: 100) - @book = - Book.create!(title: "The Great Gatsby", author: "F. Scott Fitzgerald") + def verify_counter_cache(model, assoc_name, expected_count) + expect(model.send("#{assoc_name}_count")).to eq(expected_count) + expect(model.send(assoc_name).count).to eq(expected_count || 0) + model.reload + expect(model.send("#{assoc_name}_count")).to eq(expected_count) + expect(model.send(assoc_name).count).to eq(expected_count || 0) + end + + let(:reader) { Reader.create!(name: "John Doe", reading_speed: 100) } + let(:book) do + Book.create!(title: "The Great Gatsby", author: "F. Scott Fitzgerald") end it "updates counter caches that are on the aux model" do - @reader.read_books << @book - expect(@reader.read_book_joins_count).to eq(1) - expect(@reader.read_book_joins.count).to eq(1) - end - - it "updates counter caches that are on the main table of an aux model" do + verify_counter_cache(reader, :read_book_joins, nil) + reader.read_books << book + verify_counter_cache(reader, :read_book_joins, 1) end it "updates counter caches on a non-aux model" do - @reader.read_books << @book - expect(@book.read_book_joins_count).to eq(1) - expect(@book.read_book_joins.count).to eq(1) + verify_counter_cache(book, :read_book_joins, nil) + reader.read_books << book + verify_counter_cache(book, :read_book_joins, 1) + end + + it "has_one is a base class, belongs_to is a subclass, created via subclass" do + a = ModelA.create!(a_field1: "a_0") + verify_counter_cache(a, :model_bs, nil) + + 5.times do |i| + ModelB.create!(model_a: a, b_field1: "b_#{i}") + verify_counter_cache(a, :model_bs, i + 1) + end + end + + it "has_one is a base class, belongs_to is a subclass, created via association" do + a = ModelA.create!(a_field1: "a_0") + verify_counter_cache(a, :model_bs, nil) + + 5.times do |i| + a.model_bs.create!(b_field1: "b_#{i}") + verify_counter_cache(a, :model_bs, i + 1) + end + end + + it "has_one is a subclass, belongs_to is a subclass, created via subclass" do + a = ModelA2.create!(a_field1: "a2_0", a2_field1: "a2_0") + verify_counter_cache(a, :model_b2s, nil) + + 5.times do |i| + a.model_b2s.create!(b_field1: "b_#{i}") + verify_counter_cache(a, :model_b2s, i + 1) + end + end + + it "has_one is a subclass, belongs_to is a subclass, created via association" do + a = ModelA2.create!(a_field1: "a2_0", a2_field1: "a2_0") + verify_counter_cache(a, :model_b2s, nil) + + 5.times do |i| + a.model_b2s.create!(b_field1: "b_#{i}") + verify_counter_cache(a, :model_b2s, i + 1) + end + end + + it "has_one is subclass, belongs_to is a vanilla class" do + a = ModelA1.create!(a_field1: "a1_0", a1_field1: "a1_0") + verify_counter_cache(a, :model_cs, nil) + + 5.times do |i| + a.model_cs.create!(c_field1: "c_#{i}") + verify_counter_cache(a, :model_cs, i + 1) + end end end diff --git a/spec/spec_models.rb b/spec/spec_models.rb index d8ab095..c41872d 100644 --- a/spec/spec_models.rb +++ b/spec/spec_models.rb @@ -32,8 +32,6 @@ ActiveRecord::Schema.define do create_base_table :people do |t| t.string :name - t.integer :friends_count - t.integer :lovers_count t.timestamps t.create_aux :driver do |t| @@ -286,29 +284,89 @@ class User < ActiveRecord::Base has_many :posts, inverse_of: :user, class_name: "Post" end -# class TwitterUser < User -# aux_table :twitter -# validates :twitter_handle, presence: true -# has_many :posts, inverse_of: :user, class_name: "TwitterPost" -# end +ActiveRecord::Schema.define do + create_base_table :model_as do |t| + t.string :a_field1 + t.integer :model_bs_count -# class RedditUser < User -# aux_table :reddit -# validates :reddit_handle, presence: true -# has_many :posts, inverse_of: :user, class_name: "RedditPost" -# end + t.create_aux :a1 do |t| + t.integer :a1_field1 + t.integer :model_cs_count + end -# class Post < ActiveRecord::Base -# include HasAuxTable -# belongs_to :user, inverse_of: :posts -# end + t.create_aux :a2 do |t| + t.string :a2_field1 + t.integer :model_b2s_count + end + end -# class TwitterPost < Post -# aux_table :twitter -# belongs_to :user, inverse_of: :posts, class_name: "TwitterUser" -# end + create_base_table :model_bs do |t| + t.string :b_field1 + t.references :model_a, foreign_key: { to_table: :model_as } -# class RedditPost < Post -# aux_table :reddit -# belongs_to :user, inverse_of: :posts, class_name: "RedditUser" -# end + t.create_aux :b1 do |t| + t.integer :b1_field1 + end + + t.create_aux :b2 do |t| + t.references :model_a2, + foreign_key: { + to_table: :model_as_a2_aux, + primary_key: :base_table_id + } + end + end + + create_table :model_cs do |t| + t.string :c_field1 + t.references :model_a1, + foreign_key: { + to_table: :model_as_a1_aux, + primary_key: :base_table_id + } + end + + # A* has_many B + # A1 has_many C + # B belongs_to A + # C belongs_to A1 +end + +class ModelA < ActiveRecord::Base + include HasAuxTable + has_many :model_bs, inverse_of: :model_a + validates :a_field1, presence: true, uniqueness: true +end + +class ModelA1 < ModelA + aux_table :a1 + has_many :model_cs, inverse_of: :model_a1 + validates :a1_field1, presence: true, uniqueness: true +end + +class ModelA2 < ModelA + aux_table :a2 + has_many :model_b2s, inverse_of: :model_a2 + validates :a2_field1, presence: true, uniqueness: true +end + +class ModelB < ActiveRecord::Base + include HasAuxTable + belongs_to :model_a, inverse_of: :model_bs, counter_cache: true + validates :b_field1, presence: true, uniqueness: true +end + +class ModelB1 < ModelB + aux_table :b1 + validates :b1_field1, presence: true, uniqueness: true +end + +class ModelB2 < ModelB + aux_table :b2 + belongs_to :model_a2, inverse_of: :model_b2s, counter_cache: true +end + +class ModelC < ActiveRecord::Base + belongs_to :model_a1, inverse_of: :model_cs, counter_cache: true + validates :c_field1, presence: true, uniqueness: true +end