test: cover OrmOwnedRecordsPendingIndexTest and teardown
This commit is contained in:
1 parent
6b2d8f51a2
commit
c7b1f948c7
1 file changed
+131
@@ -0,0 +1,131 @@
|
||||
require 'minitest/autorun'
|
||||
require 'prism'
|
||||
require 'tmpdir'
|
||||
|
||||
# The shape AccountController#moderation actually issues (examples/blog): find
|
||||
# the ids of every record a user owns (an equality lookup on a foreign key,
|
||||
# like Store#lookup_by_index), then filter a second table down to just the
|
||||
# ones whose foreign key is IN that id list AND some other column equals a
|
||||
# fixed value (Comment.pending.where(post_id: owned_post_ids) in the real
|
||||
# app). That second query is an AndNode of a HashNode Array (IN-list) and a
|
||||
# HashNode scalar (equality) - ConditionNode#indexed_candidates only tries
|
||||
# lookup_by_index_in for a column actually present in #indexable_in_lists,
|
||||
# so it is the post_id side (an Array) that must be indexed for this to
|
||||
# avoid a scan; the leftover status equality is filtered from that already-
|
||||
# narrow candidate set in memory afterward by Relation#rows, exactly the way
|
||||
# OrmHasManyPolymorphicForeignKeyIndexTest proves for a two-column equality.
|
||||
class OrmOwnedRecordsPendingIndexTest < Minitest::Test
|
||||
def setup
|
||||
@dir = Dir.mktmpdir('prism-owned-pending-index-store')
|
||||
@store = CountingOwnedPendingIndexStore.new(File.join(@dir, 'test.pstore'))
|
||||
@store.create_table(:mod_owners, %w[id name])
|
||||
@store.create_table(:mod_posts, %w[id user_id title])
|
||||
@store.create_table(:mod_comments, %w[id post_id status body])
|
||||
|
||||
owner_class = Class.new(Prism::Orm::Model) do
|
||||
table_name :mod_owners
|
||||
has_many :posts, class_name: 'ModPost', foreign_key: 'user_id'
|
||||
end
|
||||
post_class = Class.new(Prism::Orm::Model) { table_name :mod_posts }
|
||||
comment_class = Class.new(Prism::Orm::Model) { table_name :mod_comments }
|
||||
[owner_class, post_class, comment_class].each { |klass| klass.store = @store }
|
||||
Object.const_set(:ModOwner, owner_class)
|
||||
Object.const_set(:ModPost, post_class)
|
||||
Object.const_set(:ModComment, comment_class)
|
||||
|
||||
@alice = ModOwner.create!(name: 'alice')
|
||||
@bob = ModOwner.create!(name: 'bob')
|
||||
@alice_post = ModPost.create!(user_id: @alice.id, title: 'alices post')
|
||||
@bob_post = ModPost.create!(user_id: @bob.id, title: 'bobs post')
|
||||
|
||||
# 0 = pending, 1 = approved - plain integers here (not a real enum
|
||||
# module) since this test is only about index selection, not Enum's
|
||||
# label<->value translation, which already has its own coverage.
|
||||
ModComment.create!(post_id: @alice_post.id, status: 0, body: 'waiting on alice')
|
||||
ModComment.create!(post_id: @alice_post.id, status: 1, body: 'already approved')
|
||||
ModComment.create!(post_id: @bob_post.id, status: 0, body: 'waiting on bob')
|
||||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.remove_entry(@dir)
|
||||
%i[ModOwner ModPost ModComment].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) }
|
||||
end
|
||||
|
||||
def test_without_a_post_id_index_the_pending_lookup_falls_back_to_a_scan
|
||||
@store.reset_counts!
|
||||
bodies = ModComment.where(status: 0).where(post_id: [@alice_post.id]).to_a.map { |c| c['body'] }
|
||||
assert_equal ['waiting on alice'], bodies
|
||||
assert_equal 1, @store.scan_count
|
||||
assert_equal 0, @store.in_list_hits
|
||||
end
|
||||
|
||||
def test_indexed_post_id_answers_the_in_list_and_filters_status_in_memory
|
||||
@store.add_index(:mod_comments, :post_id)
|
||||
|
||||
@store.reset_counts!
|
||||
bodies = ModComment.where(status: 0).where(post_id: [@alice_post.id, @bob_post.id]).to_a.map { |c| c['body'] }.sort
|
||||
assert_equal ['waiting on alice', 'waiting on bob'], bodies,
|
||||
'the already-approved comment on the same post must be filtered out, not just deduped away'
|
||||
assert_equal 0, @store.scan_count
|
||||
assert_equal 1, @store.in_list_hits
|
||||
end
|
||||
|
||||
def test_an_owner_with_no_posts_yields_an_empty_in_list_without_scanning
|
||||
@store.add_index(:mod_posts, :user_id)
|
||||
@store.add_index(:mod_comments, :post_id)
|
||||
carol = ModOwner.create!(name: 'carol')
|
||||
|
||||
@store.reset_counts!
|
||||
owned_ids = carol.posts.pluck('id')
|
||||
assert_equal [], owned_ids
|
||||
assert_equal 0, @store.scan_count, 'carol owns no posts, but that is still an index hit (zero rows), not a scan'
|
||||
|
||||
bodies = ModComment.where(status: 0).where(post_id: owned_ids).to_a
|
||||
assert_equal [], bodies,
|
||||
'lookup_by_index_in on an empty Array is still an index hit (zero rows), not a fallback scan'
|
||||
assert_equal 0, @store.scan_count
|
||||
end
|
||||
|
||||
def test_both_steps_of_the_real_controller_query_stay_index_backed_once_both_columns_are_indexed
|
||||
@store.add_index(:mod_posts, :user_id)
|
||||
@store.add_index(:mod_comments, :post_id)
|
||||
|
||||
@store.reset_counts!
|
||||
owned_ids = @alice.posts.pluck('id')
|
||||
assert_equal [@alice_post.id], owned_ids
|
||||
|
||||
bodies = ModComment.where(status: 0).where(post_id: owned_ids).to_a.map { |c| c['body'] }
|
||||
assert_equal ['waiting on alice'], bodies
|
||||
assert_equal 0, @store.scan_count, 'neither the owned-posts lookup nor the pending-comments lookup should scan'
|
||||
end
|
||||
end
|
||||
|
||||
# Same wrapper shape as CountingFkIndexStore (test_orm_has_many_foreign_key_
|
||||
# index.rb), extended with an IN-list hit counter since this file is
|
||||
# specifically exercising Store#lookup_by_index_in rather than plain
|
||||
# Store#lookup_by_index.
|
||||
class CountingOwnedPendingIndexStore < Prism::Orm::Store
|
||||
attr_reader :scan_count, :in_list_hits
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@scan_count = 0
|
||||
@in_list_hits = 0
|
||||
end
|
||||
|
||||
def all(table)
|
||||
@scan_count += 1
|
||||
super
|
||||
end
|
||||
|
||||
def lookup_by_index_in(table, column, values)
|
||||
result = super
|
||||
@in_list_hits += 1 unless result.nil?
|
||||
result
|
||||
end
|
||||
|
||||
def reset_counts!
|
||||
@scan_count = 0
|
||||
@in_list_hits = 0
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user