test: cover test_combined_gte_and_lte_in_one_hash_uses_the_range_index
This commit is contained in:
1 parent
a1118e05ac
commit
3c554dff31
1 file changed
+157
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user