test: cover reset_counts! and index_hits

This commit is contained in:
dualfroz committed 2026-09-12 16:52:08 +02:00
1 parent cc4ec7e3d5
commit a1118e05ac
1 file changed
+158
+158
View File
@@ -479,3 +479,161 @@ class CountingIndexStore < Prism::Orm::Store
result result
end end
def reset_counts!
@scan_count = 0
@index_hits = 0
@range_hits = 0
@in_hits = 0
@eq_range_hits = 0
end
def index_hits
@index_hits
end
end
class OrmIndexRelationTest < Minitest::Test
def setup
@dir = Dir.mktmpdir('prism-index-relation')
@store = CountingIndexStore.new(File.join(@dir, 'test.pstore'))
@store.create_table(:posts, %w[id title status])
@store.add_index(:posts, :status)
post_class = Class.new(Prism::Orm::Model) { table_name :posts }
post_class.store = @store
Object.const_set(:IndexedPost, post_class)
IndexedPost.create!(title: 'a', status: 'draft')
IndexedPost.create!(title: 'b', status: 'published')
IndexedPost.create!(title: 'c', status: 'draft')
end
def teardown
FileUtils.remove_entry(@dir)
Object.send(:remove_const, :IndexedPost) if Object.const_defined?(:IndexedPost)
end
def test_where_on_an_indexed_column_never_scans_the_full_table
@store.reset_counts!
results = IndexedPost.where(status: 'draft').to_a
assert_equal ['a', 'c'], results.map { |post| post['title'] }.sort
assert_equal 0, @store.scan_count
assert_equal 1, @store.index_hits
end
def test_where_on_an_unindexed_column_falls_back_to_a_full_scan
@store.reset_counts!
results = IndexedPost.where(title: 'a').to_a
assert_equal ['a'], results.map { |post| post['title'] }
assert_equal 1, @store.scan_count
assert_equal 0, @store.index_hits
end
def test_chained_where_mixing_indexed_and_unindexed_columns_is_still_correct
@store.reset_counts!
results = IndexedPost.where(status: 'draft').where(title: 'c').to_a
assert_equal ['c'], results.map { |post| post['title'] }
assert_equal 0, @store.scan_count, 'the indexed equality should still narrow the candidate set'
end
def test_or_condition_cannot_use_the_index_but_is_still_correct
@store.reset_counts!
results = IndexedPost.where(status: 'draft').or(IndexedPost.where(title: 'b')).to_a
assert_equal ['a', 'b', 'c'], results.map { |post| post['title'] }.sort
assert_equal 1, @store.scan_count, 'an OR across columns cannot guarantee a single index key, so it must scan'
end
def test_find_by_uses_the_index
@store.reset_counts!
post = IndexedPost.find_by(status: 'published')
assert_equal 'b', post['title']
assert_equal 0, @store.scan_count
end
def test_or_condition_unions_index_hits_once_both_branches_are_indexed
@store.add_index(:posts, :title)
@store.reset_counts!
results = IndexedPost.where(status: 'draft').or(IndexedPost.where(title: 'b')).to_a
assert_equal ['a', 'b', 'c'], results.map { |post| post['title'] }.sort
assert_equal 0, @store.scan_count, 'both branches are indexed, so the union must never fall back to a scan'
assert_equal 2, @store.index_hits, 'one lookup per branch of the Or'
end
def test_or_condition_dedupes_a_row_matched_by_both_indexed_branches
@store.add_index(:posts, :title)
@store.reset_counts!
# post "a" is status draft AND title "a" - both branches of the Or match
# the same row, so the union must not return it twice.
results = IndexedPost.where(status: 'draft').or(IndexedPost.where(title: 'a')).to_a
assert_equal ['a', 'c'], results.map { |post| post['title'] }.sort
assert_equal 2, results.size
assert_equal 0, @store.scan_count
end
# `a.or(b).or(c)` builds OrNode(OrNode(a, b), c) - a left-nested tree, not
# a flat three-way union. OrNode#indexed_candidates only recurses into
# @left/@right, so this exercises that the recursion actually reaches the
# innermost branch rather than treating the outer node's @left (itself an
# OrNode) as an opaque leaf. Left untested since the index-union Or was
# first added, per prism-postep.md "kierunki dalsze".
def test_three_way_or_uses_the_index_on_every_branch
@store.add_index(:posts, :title)
IndexedPost.create!(title: 'd', status: 'published')
@store.reset_counts!
results = IndexedPost.where(status: 'draft')
.or(IndexedPost.where(title: 'b'))
.or(IndexedPost.where(title: 'd'))
.to_a
assert_equal ['a', 'b', 'c', 'd'], results.map { |post| post['title'] }.sort
assert_equal 0, @store.scan_count, 'every branch of the three-way Or is indexed, so it must never fall back to a scan'
assert_equal 3, @store.index_hits, 'one lookup per branch, including the nested inner Or'
end
def test_three_way_or_falls_back_to_a_scan_when_one_branch_is_unindexed
@store.reset_counts!
# title carries no index in this test (unlike the one above), so the
# whole union must scan rather than silently dropping that branch's
# matches while still answering the other two from their indexes.
results = IndexedPost.where(status: 'draft')
.or(IndexedPost.where(title: 'b'))
.or(IndexedPost.where(status: 'archived'))
.to_a
assert_equal ['a', 'b', 'c'], results.map { |post| post['title'] }.sort
assert_equal 1, @store.scan_count
end
end
class OrmIndexRangeRelationTest < Minitest::Test
def setup
@dir = Dir.mktmpdir('prism-index-range-relation')
@store = CountingIndexStore.new(File.join(@dir, 'test.pstore'))
@store.create_table(:products, %w[id name price])
@store.add_index(:products, :price)
product_class = Class.new(Prism::Orm::Model) { table_name :products }
product_class.store = @store
Object.const_set(:RangeIndexedProduct, product_class)
RangeIndexedProduct.create!(name: 'cheap', price: 5)
RangeIndexedProduct.create!(name: 'mid', price: 15)
RangeIndexedProduct.create!(name: 'pricey', price: 25)
RangeIndexedProduct.create!(name: 'luxury', price: 35)
end
def teardown
FileUtils.remove_entry(@dir)
Object.send(:remove_const, :RangeIndexedProduct) if Object.const_defined?(:RangeIndexedProduct)
end
def test_gte_on_an_indexed_column_never_scans_the_full_table
@store.reset_counts!
results = RangeIndexedProduct.where(price: { gte: 15 }).order(:price).to_a
assert_equal %w[mid pricey luxury], results.map { |p| p['name'] }
assert_equal 0, @store.scan_count
assert_equal 1, @store.range_hits
end