From 3c554dff31fabc6c6d6a5b8b87c48d0e07de35ae Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 16:58:08 +0200 Subject: [PATCH] test: cover test_combined_gte_and_lte_in_one_hash_uses_the_range_index --- test/test_orm_index.rb | 157 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/test/test_orm_index.rb b/test/test_orm_index.rb index d72c434..94704c8 100644 --- a/test/test_orm_index.rb +++ b/test/test_orm_index.rb @@ -637,3 +637,160 @@ class OrmIndexRangeRelationTest < Minitest::Test assert_equal 1, @store.range_hits end + def test_combined_gte_and_lte_in_one_hash_uses_the_range_index + @store.reset_counts! + results = RangeIndexedProduct.where(price: { gte: 10, lte: 30 }).order(:price).to_a + assert_equal %w[mid pricey], results.map { |p| p['name'] } + assert_equal 0, @store.scan_count + assert_equal 1, @store.range_hits + end + + def test_a_ruby_range_value_uses_the_range_index + @store.reset_counts! + results = RangeIndexedProduct.where(price: 10..30).order(:price).to_a + assert_equal %w[mid pricey], results.map { |p| p['name'] } + assert_equal 0, @store.scan_count + assert_equal 1, @store.range_hits + end + + def test_an_exclusive_end_ruby_range_excludes_the_boundary + @store.reset_counts! + results = RangeIndexedProduct.where(price: 15...25).to_a + assert_equal ['mid'], results.map { |p| p['name'] } + assert_equal 0, @store.scan_count + end + + def test_two_chained_wheres_narrow_the_same_column_via_and + @store.reset_counts! + results = RangeIndexedProduct.where(price: { gte: 10 }).where(price: { lte: 30 }).order(:price).to_a + assert_equal %w[mid pricey], results.map { |p| p['name'] } + assert_equal 0, @store.scan_count, 'the AND of two range clauses on the same column must still avoid a scan' + end + + def test_a_hash_mixing_a_range_operator_with_a_non_range_operator_falls_back_to_a_scan + @store.reset_counts! + results = RangeIndexedProduct.where(price: { gte: 10, ne: 25 }).order(:price).to_a + assert_equal %w[mid luxury], results.map { |p| p['name'] } + assert_equal 1, @store.scan_count, ':ne cannot be expressed as a contiguous bound, so this must be a full scan' + assert_equal 0, @store.range_hits + end + + def test_or_across_columns_still_falls_back_to_a_scan_when_only_one_side_is_indexed + @store.reset_counts! + results = RangeIndexedProduct.where(price: { gte: 30 }).or(RangeIndexedProduct.where(name: 'cheap')).to_a + assert_equal %w[cheap luxury], results.map { |p| p['name'] }.sort + assert_equal 1, @store.scan_count, 'name has no index, so OrNode cannot union candidates from both sides' + # The price side IS individually range-indexed, and OrNode#indexed_candidates + # tries it before discovering the name side has no covering index at all - + # that one probe is cheap (an O(log n) index lookup, not a scan) and its + # result is simply discarded once the other side comes back nil. + assert_equal 1, @store.range_hits + end +end + +class OrmIndexInListRelationTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-index-in-relation') + @store = CountingIndexStore.new(File.join(@dir, 'test.pstore')) + @store.create_table(:widgets, %w[id sku status]) + @store.add_index(:widgets, :sku) + + widget_class = Class.new(Prism::Orm::Model) { table_name :widgets } + widget_class.store = @store + Object.const_set(:InListWidget, widget_class) + + @a = InListWidget.create!(sku: 'AAA', status: 'active') + @b = InListWidget.create!(sku: 'BBB', status: 'active') + @c = InListWidget.create!(sku: 'CCC', status: 'retired') + end + + def teardown + FileUtils.remove_entry(@dir) + Object.send(:remove_const, :InListWidget) if Object.const_defined?(:InListWidget) + end + + def test_where_id_in_never_scans_even_with_no_add_index_call_for_id + @store.reset_counts! + results = InListWidget.where(id: [@a.id, @c.id]).to_a + assert_equal %w[AAA CCC], results.map { |w| w['sku'] }.sort + assert_equal 0, @store.scan_count + assert_equal 1, @store.in_hits + end + + def test_where_on_an_in_indexed_column_never_scans + @store.reset_counts! + results = InListWidget.where(sku: %w[AAA CCC]).to_a + assert_equal %w[AAA CCC], results.map { |w| w['sku'] }.sort + assert_equal 0, @store.scan_count + assert_equal 1, @store.in_hits + end + + def test_where_on_an_unindexed_column_in_list_falls_back_to_a_scan + @store.reset_counts! + results = InListWidget.where(status: ['retired']).to_a + assert_equal ['CCC'], results.map { |w| w['sku'] } + assert_equal 1, @store.scan_count + assert_equal 0, @store.in_hits + end + + def test_unknown_ids_are_silently_dropped_not_nil_rows + @store.reset_counts! + results = InListWidget.where(id: [@a.id, @a.id + 999]).to_a + assert_equal [@a.id], results.map(&:id) + end + + def test_two_chained_id_in_wheres_intersect_instead_of_scanning + @store.reset_counts! + results = InListWidget.where(id: [@a.id, @b.id, @c.id]).where(id: [@b.id, @c.id]).to_a + assert_equal [@b.id, @c.id].sort, results.map(&:id).sort + assert_equal 0, @store.scan_count + end + + def test_an_equality_condition_alongside_an_id_in_list_still_uses_an_index_and_stays_correct + @store.reset_counts! + results = InListWidget.where(id: [@a.id, @b.id, @c.id]).where(sku: 'BBB').to_a + assert_equal ['BBB'], results.map { |w| w['sku'] } + assert_equal 0, @store.scan_count + end +end + +class OrmIndexEqualityRangeStoreTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-index-eq-range-store') + @store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore')) + @store.create_table(:posts, %w[id featured position]) + end + + def teardown + FileUtils.remove_entry(@dir) + end + + def test_returns_nil_without_a_covering_composite_index + @store.insert(:posts, featured: true, position: 1) + assert_nil @store.lookup_by_index_equality_range(:posts, { 'featured' => true }, :position, gte: 0) + end + + def test_a_single_column_index_on_the_range_column_alone_does_not_cover_it + @store.add_index(:posts, :position) + @store.insert(:posts, featured: true, position: 1) + assert_nil @store.lookup_by_index_equality_range(:posts, { 'featured' => true }, :position, gte: 0) + end + + def test_finds_rows_within_the_prefix_and_bounds + @store.add_index(:posts, %i[featured position]) + @store.insert(:posts, featured: true, position: 1) + @store.insert(:posts, featured: true, position: 2) + @store.insert(:posts, featured: true, position: 3) + @store.insert(:posts, featured: false, position: 1) + + matches = @store.lookup_by_index_equality_range(:posts, { 'featured' => true }, :position, lt: 3) + assert_equal [1, 2], matches.map { |row| row['position'] }.sort + end + + def test_equality_key_order_from_the_caller_does_not_matter + wide = Prism::Orm::Store.new(File.join(@dir, 'order.pstore')) + wide.create_table(:tasks, %w[id project priority position]) + wide.add_index(:tasks, %i[project priority position]) + wide.insert(:tasks, project: 'a', priority: 'high', position: 1) + wide.insert(:tasks, project: 'a', priority: 'high', position: 2) +