Skip to content

Commit

Permalink
add query matcher for multi-query unions and preloading
Browse files Browse the repository at this point in the history
  • Loading branch information
ezekg committed Mar 9, 2024
1 parent 09cd449 commit bdbf11d
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 57 deletions.
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ group :test do
gem 'rspec-rails', '~> 6.0.3'
gem 'rspec-expectations', '~> 3.12.1'
gem 'anbt-sql-formatter'
gem 'db-query-matchers'
gem 'factory_bot_rails', '~> 6.2'
gem 'database_cleaner', '~> 2.0'
gem 'webmock', '~> 3.14.0'
Expand Down
8 changes: 0 additions & 8 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,6 @@ GEM
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.3)
db-query-matchers (0.12.0)
activesupport (>= 4.0, < 7.2)
rspec (>= 3.0)
diff-lcs (1.5.0)
dotenv (2.7.6)
dotenv-rails (2.7.6)
Expand Down Expand Up @@ -408,10 +405,6 @@ GEM
rack (>= 1.4)
rexml (3.2.5)
rotp (6.2.0)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.1)
Expand Down Expand Up @@ -531,7 +524,6 @@ DEPENDENCIES
cucumber-rails (~> 2.5)
cuke_modeler (~> 3.19)
database_cleaner (~> 2.0)
db-query-matchers
dotenv-rails
ed25519
elif (~> 0.1.0)
Expand Down
210 changes: 163 additions & 47 deletions spec/lib/union_of_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -338,21 +338,14 @@
end
end

# TODO(ezekg) Need to match on multiple queries since this is done in 2
# parts, for performance reasons.
skip 'should produce a union query' do
it 'should produce a union query' do
user = create(:user, account:)
license = create(:license, owner: user)
license = create(:license, account:, owner: user)
machine = create(:machine, license:)

expect(user.machines.to_sql).to match_sql <<~SQL.squish
SELECT
"machines".*
FROM
"machines"
INNER JOIN "licenses" ON "machines"."license_id" = "licenses"."id"
WHERE
"licenses"."id" IN (
expect { user.machines }.to(
match_queries(count: 2) do |queries|
expect(queries.first).to match_sql <<~SQL.squish
SELECT
"licenses"."id"
FROM
Expand All @@ -363,7 +356,7 @@
FROM
"licenses"
WHERE
"licenses"."user_id" = '#{record.id}'
"licenses"."user_id" = '#{user.id}'
)
UNION
(
Expand All @@ -373,13 +366,24 @@
"licenses"
INNER JOIN "license_users" ON "licenses"."id" = "license_users"."license_id"
WHERE
"license_users"."user_id" = '#{record.id}'
"license_users"."user_id" = '#{user.id}'
)
) "licenses"
)
ORDER BY
"machines"."created_at" ASC
SQL
SQL

expect(queries.second).to match_sql <<~SQL.squish
SELECT
"machines".*
FROM
"machines"
INNER JOIN "licenses" ON "machines"."license_id" = "licenses"."id"
WHERE
"licenses"."id" IN ('#{license.id}')
ORDER BY
"machines"."created_at" ASC
SQL
end
)
end

it 'should produce a deep union join' do
Expand Down Expand Up @@ -413,22 +417,15 @@
SQL
end

# TODO(ezekg) Need to match on multiple queries here
skip 'should produce a deep union query' do
it 'should produce a deep union query' do
user = create(:user, account:)
license = create(:license, owner: user)
license = create(:license, account:, owner: user)
machine = create(:machine, license:)
component = create(:component, machine:)

expect(record.components.to_sql).to match_sql <<~SQL.squish
SELECT
"machine_components".*
FROM
"machine_components"
INNER JOIN "machines" ON "machine_components"."machine_id" = "machines"."id"
INNER JOIN "licenses" ON "machines"."license_id" = "licenses"."id"
WHERE
"licenses"."id" IN (
expect { user.components }.to(
match_queries(count: 2) do |queries|
expect(queries.first).to match_sql <<~SQL.squish
SELECT
"licenses"."id"
FROM
Expand All @@ -439,7 +436,7 @@
FROM
"licenses"
WHERE
"licenses"."user_id" = '#{record.id}'
"licenses"."user_id" = '#{user.id}'
)
UNION
(
Expand All @@ -448,14 +445,26 @@
FROM
"licenses"
INNER JOIN "license_users" ON "licenses"."id" = "license_users"."license_id"
WHERE
"license_users"."user_id" = '#{record.id}'
WHERE
"license_users"."user_id" = '#{user.id}'
)
) "licenses"
)
ORDER BY
"machine_components"."created_at" ASC
SQL
SQL

expect(queries.second).to match_sql <<~SQL.squish
SELECT
"machine_components".*
FROM
"machine_components"
INNER JOIN "machines" ON "machine_components"."machine_id" = "machines"."id"
INNER JOIN "licenses" ON "machines"."license_id" = "licenses"."id"
WHERE
"licenses"."id" IN ('#{license.id}')
ORDER BY
"machine_components"."created_at" ASC
SQL
end
)
end

it 'should produce a deeper union join' do
Expand Down Expand Up @@ -868,7 +877,7 @@
expect(license.association(:owner).loaded?).to be false
expect(license.association(:licensees).loaded?).to be false

expect { license.users }.to_not make_database_queries
expect { license.users }.to match_queries(count: 0)
expect(license.users.sort_by(&:id)).to eq license.reload.users.sort_by(&:id)
end
end
Expand Down Expand Up @@ -943,40 +952,147 @@
expect(user.association(:machines).loaded?).to be true
expect(user.association(:licenses).loaded?).to be false

expect { user.machines }.to_not make_database_queries
expect { user.machines }.to match_queries(count: 0)
expect(user.machines.sort_by(&:id)).to eq user.reload.machines.sort_by(&:id)
end
end

it 'should support preloading a union' do
licenses = License.preload(:users)

# FIXME(ezekg) How can I test the actual SQL used for preloading?
expect { licenses.to_a }.to make_database_queries(count: 4)
.and not_raise_error
expect { licenses }.to(
match_queries(count: 4) do |queries|
license_ids = licenses.ids.uniq
owner_ids = licenses.map(&:owner_id).compact.uniq
user_ids = licenses.flat_map(&:licensee_ids).uniq

expect(queries.first).to match_sql <<~SQL.squish
SELECT "licenses".* FROM "licenses" ORDER BY "licenses"."created_at" ASC
SQL

expect(queries.second).to match_sql <<~SQL.squish
SELECT
"license_users".*
FROM
"license_users"
WHERE
"license_users"."license_id" IN (
#{license_ids.map { "'#{_1}'" }.join(', ')}
)
ORDER BY
"license_users"."created_at" ASC
SQL

expect(queries.third).to match_sql <<~SQL.squish
SELECT
"users".*
FROM
"users"
WHERE
"users"."id" IN (
#{owner_ids.map { "'#{_1}'" }.join(', ')}
)
ORDER BY
"users"."created_at" ASC
SQL

expect(queries.fourth).to match_sql <<~SQL.squish
SELECT
"users".*
FROM
"users"
WHERE
"users"."id" IN (
#{user_ids.map { "'#{_1}'" }.join(', ')}
)
ORDER BY
"users"."created_at" ASC
SQL
end
)

licenses.each do |license|
expect(license.association(:users).loaded?).to be true
expect(license.association(:owner).loaded?).to be true
expect(license.association(:licensees).loaded?).to be true

expect { license.users }.to_not make_database_queries
expect { license.users }.to match_queries(count: 0)
expect(license.users.sort_by(&:id)).to eq license.reload.users.sort_by(&:id)
end
end

it 'should support preloading a through union' do
users = User.preload(:machines)

# FIXME(ezekg) How can I test the actual SQL used for preloading?
expect { users.to_a }.to make_database_queries(count: 5)
.and not_raise_error
expect { users }.to(
match_queries(count: 5) do |queries|
user_ids = users.ids.uniq
user_license_ids = users.flat_map(&:user_license_ids).reverse.uniq.reverse # order is significant
license_ids = users.flat_map(&:license_ids).uniq

expect(queries.first).to match_sql <<~SQL.squish
SELECT "users" . * FROM "users" ORDER BY "users"."created_at" ASC
SQL

expect(queries.second).to match_sql <<~SQL.squish
SELECT
"licenses".*
FROM
"licenses"
WHERE
"licenses"."user_id" IN (
#{user_ids.map { "'#{_1}'" }.join(', ')}
)
ORDER BY
"licenses"."created_at" ASC
SQL

expect(queries.third).to match_sql <<~SQL.squish
SELECT
"license_users".*
FROM
"license_users"
WHERE
"license_users"."user_id" IN (
#{user_ids.map { "'#{_1}'" }.join(', ')}
)
ORDER BY
"license_users"."created_at" ASC
SQL

expect(queries.fourth).to match_sql <<~SQL.squish
SELECT
"licenses".*
FROM
"licenses"
WHERE
"licenses"."id" IN (
#{user_license_ids.map { "'#{_1}'" }.join(', ')}
)
ORDER BY
"licenses"."created_at" ASC
SQL

expect(queries.fifth).to match_sql <<~SQL.squish
SELECT
"machines".*
FROM
"machines"
WHERE
"machines"."license_id" IN (
#{license_ids.map { "'#{_1}'" }.join(', ')}
)
ORDER BY
"machines"."created_at" ASC
SQL
end
)

users.each do |user|
expect(user.association(:machines).loaded?).to be true
expect(user.association(:licenses).loaded?).to be true

expect { user.machines }.to_not make_database_queries
expect { user.machines }.to match_queries(count: 0)
expect(user.machines.sort_by(&:id)).to eq user.reload.machines.sort_by(&:id)
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@

it 'should preload user statuses' do
statuses = nil
expect { statuses = User.preload(:any_active_licenses).collect(&:status) }.to make_database_queries(count: 4)
expect { statuses = User.preload(:any_active_licenses).collect(&:status) }.to match_queries(count: 4)
expect(statuses).to eq User.all.collect(&:status)
end

Expand Down
Loading

0 comments on commit bdbf11d

Please sign in to comment.