test: cover reset_counts! and index_hits
This commit is contained in:
1 parent
cc4ec7e3d5
commit
a1118e05ac
1 file changed
+158
@@ -479,3 +479,161 @@ class CountingIndexStore < Prism::Orm::Store
|
||||
result
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user