Skip to content

Commit 41d7471

Browse files
authored
Merge pull request #4841 from coalest/improve-distributions-by-country-report-performance
Perf: refactor DistributionsByCountry#report using raw SQL query
2 parents 8da1718 + f14d05d commit 41d7471

File tree

7 files changed

+170
-104
lines changed

7 files changed

+170
-104
lines changed

app/controllers/distributions_by_county_controller.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ class DistributionsByCountyController < ApplicationController
44

55
def report
66
setup_date_range_picker
7-
distributions = current_organization.distributions.includes(:partner).during(helpers.selected_range)
8-
@breakdown = DistributionByCountyReportService.new.get_breakdown(distributions)
7+
start_date = helpers.selected_range.first.iso8601
8+
end_date = helpers.selected_range.last.iso8601
9+
@breakdown = DistributionSummaryByCountyQuery.call(
10+
organization_id: current_organization.id,
11+
start_date: start_date,
12+
end_date: end_date
13+
)
914
end
1015
end
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
class DistributionSummaryByCountyQuery
2+
CountySummary = Data.define(:name, :quantity, :value)
3+
4+
# No need to send comments in the query
5+
SQL_MULTILINE_COMMENTS = /\/\*.*?\*\//
6+
7+
DISTRIBUTION_BY_COUNTY_SQL = <<~SQL.squish.gsub(SQL_MULTILINE_COMMENTS, "").freeze
8+
/* Calculate total item quantity and value per distribution */
9+
WITH distribution_totals AS
10+
(
11+
SELECT DISTINCT d.id,
12+
d.partner_id,
13+
COALESCE(SUM(li.quantity) OVER (PARTITION BY d.id), 0) AS quantity,
14+
COALESCE(SUM(COALESCE(i.value_in_cents, 0) * li.quantity) OVER (PARTITION BY d.id), 0) AS value
15+
FROM distributions d
16+
JOIN line_items li ON li.itemizable_id = d.id AND li.itemizable_type = 'Distribution'
17+
JOIN items i ON i.id = li.item_id
18+
WHERE d.issued_at BETWEEN :start_date AND :end_date
19+
AND d.organization_id = :organization_id
20+
GROUP BY d.id, li.id, i.id
21+
),
22+
/* Match distribution totals with client share and counties.
23+
If distribution has no associated county, set county name to "Unspecified"
24+
and set region to ZZZ so it will be last when sorted */
25+
totals_by_county AS
26+
(
27+
SELECT dt.id,
28+
dt.quantity,
29+
dt.value,
30+
COALESCE(psa.client_share::float / 100, 1) AS percentage,
31+
COALESCE(c.name, 'Unspecified') county_name,
32+
COALESCE(c.region, 'ZZZ') county_region
33+
FROM distribution_totals dt
34+
LEFT JOIN partners p ON p.id = dt.partner_id
35+
LEFT JOIN partner_profiles pp ON pp.partner_id = p.id
36+
LEFT JOIN partner_served_areas psa ON psa.partner_profile_id = pp.id
37+
LEFT JOIN counties c ON c.id = psa.county_id
38+
UNION
39+
/* Previous behavior was to add a row for unspecified counties
40+
even if all distributions have an associated county */
41+
SELECT 0 AS id,
42+
0 AS quantity,
43+
0 AS value,
44+
1 AS percentage,
45+
'Unspecified' AS county_name,
46+
'ZZZ' AS county_region
47+
)
48+
/* Distribution value and quantities per county share may not be whole numbers,
49+
so we cast to an integer for rounding purposes */
50+
SELECT tbc.county_name AS name,
51+
SUM((tbc.quantity * percentage)::int) AS quantity,
52+
SUM((tbc.value * percentage)::int) AS value
53+
FROM totals_by_county tbc
54+
GROUP BY county_name, county_region
55+
ORDER BY county_region ASC;
56+
SQL
57+
58+
class << self
59+
def call(organization_id:, start_date: nil, end_date: nil)
60+
params = {
61+
organization_id: organization_id,
62+
start_date: start_date || "1000-01-01",
63+
end_date: end_date || "3000-01-01"
64+
}
65+
66+
execute(to_sql(DISTRIBUTION_BY_COUNTY_SQL, **params)).to_a.map(&to_county_summary)
67+
end
68+
69+
private
70+
71+
def execute(sql)
72+
ActiveRecord::Base.connection.execute(sql)
73+
end
74+
75+
def to_sql(query, organization_id:, start_date:, end_date:)
76+
ActiveRecord::Base.sanitize_sql_array(
77+
[
78+
query,
79+
organization_id: organization_id,
80+
start_date: start_date,
81+
end_date: end_date
82+
]
83+
)
84+
end
85+
86+
def to_county_summary
87+
->(params) { CountySummary.new(**params) }
88+
end
89+
end
90+
end

app/services/distribution_by_county_report_service.rb

Lines changed: 0 additions & 33 deletions
This file was deleted.

app/views/distributions_by_county/report.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
<% @breakdown.each do |bd| %>
5252
<tr>
5353
<td><%= bd.name %></td>
54-
<td class="numeric"><%= number_with_delimiter(bd.num_items) %></td>
55-
<td class="numeric"><%= dollar_presentation(bd.amount) %></td>
54+
<td class="numeric"><%= number_with_delimiter(bd.quantity) %></td>
55+
<td class="numeric"><%= dollar_presentation(bd.value) %></td>
5656
</tr>
5757
<% end %>
5858

spec/factories/partners.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@
2424
send_reminders { true }
2525
organization_id { Organization.try(:first).try(:id) || create(:organization).id }
2626

27+
transient do
28+
without_profile { false }
29+
end
30+
2731
trait :approved do
2832
status { :approved }
2933
end
3034

3135
trait :uninvited do
3236
status { :uninvited }
33-
34-
transient do
35-
without_profile { false }
36-
end
3737
end
3838

3939
trait :awaiting_review do
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
RSpec.describe DistributionSummaryByCountyQuery do
2+
let(:year) { Time.current.year }
3+
let(:issued_at_last_year) { Time.current.change(year: year - 1).to_datetime }
4+
let(:distributions) { [] }
5+
let(:organization_id) { organization.id }
6+
let(:start_date) { nil }
7+
let(:end_date) { nil }
8+
let(:params) { {organization_id:, start_date:, end_date:} }
9+
10+
include_examples "distribution_by_county"
11+
12+
before do
13+
create(:storage_location, organization: organization)
14+
end
15+
16+
describe "get_breakdown" do
17+
it "will have 100% unspecified shows if no served_areas" do
18+
create(:distribution, :with_items, item: item_1, organization: user.organization)
19+
breakdown = DistributionSummaryByCountyQuery.call(**params)
20+
expect(breakdown.size).to eq(1)
21+
expect(breakdown[0].quantity).to eq(100)
22+
expect(breakdown[0].value).to be_within(0.01).of(105000.0)
23+
end
24+
25+
it "divides the item numbers and values according to the partner profile" do
26+
create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1)
27+
breakdown = DistributionSummaryByCountyQuery.call(**params)
28+
expect(breakdown.size).to eq(5)
29+
expect(breakdown[4].quantity).to eq(0)
30+
expect(breakdown[4].value).to be_within(0.01).of(0)
31+
3.times do |i|
32+
expect(breakdown[i].quantity).to eq(25)
33+
expect(breakdown[i].value).to be_within(0.01).of(26250.0)
34+
end
35+
end
36+
37+
it "handles multiple partners with overlapping service areas properly" do
38+
create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: issued_at_present)
39+
create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_2, issued_at: issued_at_present)
40+
breakdown = DistributionSummaryByCountyQuery.call(**params)
41+
num_with_45 = 0
42+
num_with_20 = 0
43+
num_with_0 = 0
44+
# The result will have at least 1 45 and at least 1 20, and 1 0. Anything else will be either 45 or 25 or 20
45+
breakdown.each do |sa|
46+
if sa.quantity == 45
47+
expect(sa.value).to be_within(0.01).of(47250.0)
48+
num_with_45 += 1
49+
end
50+
51+
if sa.quantity == 25
52+
expect(sa.value).to be_within(0.01).of(26250.0)
53+
end
54+
if sa.quantity == 20
55+
expect(sa.value).to be_within(0.01).of(21000.0)
56+
num_with_20 += 1
57+
end
58+
if sa.quantity == 0
59+
expect(sa.value).to be_within(0.01).of(0)
60+
end
61+
end
62+
expect(num_with_45).to be > 0
63+
expect(num_with_20).to be > 0
64+
expect(num_with_0).to eq 0
65+
end
66+
end
67+
end

spec/services/distributions_by_county_report_service_spec.rb

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)