From 11e15aec0144517407ba048c81b080dd45313f8d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 21:21:15 +0200 Subject: [PATCH] test: cover OrmEagerLoadingTest and teardown --- test/test_orm_eager_loading.rb | 127 +++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 test/test_orm_eager_loading.rb diff --git a/test/test_orm_eager_loading.rb b/test/test_orm_eager_loading.rb new file mode 100644 index 0000000..78fe16b --- /dev/null +++ b/test/test_orm_eager_loading.rb @@ -0,0 +1,127 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' + +class OrmEagerLoadingTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-eager-store') + @store = CountingStore.new(File.join(@dir, 'test.pstore')) + @store.create_table(:authors, %w[id name]) + @store.create_table(:posts, %w[id title author_id]) + @store.create_table(:bios, %w[id author_id headline]) + + author_class = Class.new(Prism::Orm::Model) do + table_name :authors + has_many :posts, class_name: 'Post', foreign_key: 'author_id' + has_one :bio, class_name: 'Bio', foreign_key: 'author_id' + end + post_class = Class.new(Prism::Orm::Model) do + table_name :posts + belongs_to :author, class_name: 'Author', foreign_key: 'author_id' + end + bio_class = Class.new(Prism::Orm::Model) do + table_name :bios + belongs_to :author, class_name: 'Author', foreign_key: 'author_id' + end + [author_class, post_class, bio_class].each { |klass| klass.store = @store } + Object.const_set(:Author, author_class) + Object.const_set(:Post, post_class) + Object.const_set(:Bio, bio_class) + end + + def teardown + FileUtils.remove_entry(@dir) + %i[Author Post Bio].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) } + end + + def test_includes_has_many_avoids_n_plus_one_queries + 3.times do |i| + author = Author.create!(name: "author#{i}") + 2.times { |j| Post.create!(title: "post#{i}-#{j}", author_id: author.id) } + end + + @store.reset_query_count! + authors = Author.all.includes(:posts).to_a + baseline_queries = @store.query_count + + authors.each { |author| author.posts.each(&:title) } + assert_equal baseline_queries, @store.query_count, + "expected preloaded #posts to be free, but it issued #{@store.query_count - baseline_queries} more queries" + + total_posts = authors.sum { |author| author.posts.size } + assert_equal 6, total_posts + end + + def test_includes_belongs_to_preloads_single_target + author = Author.create!(name: 'solo') + Post.create!(title: 'a', author_id: author.id) + Post.create!(title: 'b', author_id: author.id) + + @store.reset_query_count! + posts = Post.all.includes(:author).to_a + baseline = @store.query_count + names = posts.map { |post| post.author.name } + + assert_equal baseline, @store.query_count + assert_equal %w[solo solo], names + end + + def test_includes_has_one_preloads + author = Author.create!(name: 'writer') + Bio.create!(headline: 'hello there', author_id: author.id) + + loaded = Author.all.includes(:bio).to_a.first + assert_equal 'hello there', loaded.bio.headline + end + + def test_includes_unknown_association_raises + Author.create!(name: 'x') + assert_raises(ArgumentError) { Author.all.includes(:nope).to_a } + end + + def test_includes_reduces_queries_compared_to_lazy_access + 3.times do |i| + author = Author.create!(name: "author#{i}") + 2.times { |j| Post.create!(title: "post#{i}-#{j}", author_id: author.id) } + end + + @store.reset_query_count! + Author.all.to_a.each { |author| author.posts.to_a } + lazy_queries = @store.query_count + + @store.reset_query_count! + Author.all.includes(:posts).to_a.each { |author| author.posts.to_a } + eager_queries = @store.query_count + + assert_equal 4, lazy_queries # 1 authors query + 1 posts query per author (N+1) + assert_equal 2, eager_queries # 1 authors query + 1 batched posts query + assert_operator eager_queries, :<, lazy_queries + end +end + +# Wraps Store#all/#find to count reads, so tests can assert on actual +# query volume instead of trusting behavior "looks" batched. +class CountingStore < Prism::Orm::Store + def initialize(*) + super + @query_count = 0 + end + + def query_count + @query_count + end + + def reset_query_count! + @query_count = 0 + end + + def all(table) + @query_count += 1 + super + end + + def find(table, id) + @query_count += 1 + super + end +end