Skip to content

Commit 7974fef

Browse files
authored
feat(Dr. Rai Reports): Optimize Titration Indicator Data (#5650)
**Story card:** [sc-16391](https://app.shortcut.com/simpledotorg/story/16391/optimize-data-load-for-titration-indicator) ## Because Running the SQL query within the reqyest lifecycle causes a timeout in environments which have plenty data. ## This addresses - Creating a table / model which would hold the data for the indicator - Load in the data from teh SQL query - Adding a rake task for populating the data; manually and on schedule ## Test instructions suite-tests
1 parent a1d43df commit 7974fef

File tree

15 files changed

+408
-8
lines changed

15 files changed

+408
-8
lines changed

app/components/dashboard/dr_rai_report.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@
249249
} else {
250250
if (target.ui == 'percent') {
251251
var previousPercentage
252-
if (indicator.previous_numerator < 1) {
252+
if (indicator.previous_numerator < 1 || indicator.denominator == '' || indicator.denominator === undefined) {
253253
previousPercentage = 0
254254
} else {
255255
previousPercentage = Math.round(indicator.previous_numerator / indicator.denominator * 100)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Contains methods required to transform data from their normal form to chartable data
2+
module DrRai
3+
module Chartable
4+
extend ActiveSupport::Concern
5+
6+
class_methods do
7+
def chartable_internal_keys *keys
8+
@chartable_internal_keys = keys.map(&:to_sym)
9+
end
10+
11+
def chartable_period_key key
12+
@chartable_period_key = key.to_sym
13+
end
14+
15+
def chartable_outer_grouping key
16+
@chartable_outer_grouping = key.to_sym
17+
end
18+
19+
def chartable
20+
result = {}
21+
all.each do |record|
22+
period_key = @chartable_period_key
23+
the_period = Period.quarter(record.send(period_key))
24+
25+
internal_keys = @chartable_internal_keys
26+
internal_data = internal_keys.map { |k| [k, record.send(k)] }.to_h
27+
28+
outer_grouping = record.send @chartable_outer_grouping
29+
if result.has_key? outer_grouping
30+
if result[outer_grouping].has_key?(the_period)
31+
internal_keys.each do |k|
32+
result[outer_grouping][the_period][k] += record.send(k)
33+
end
34+
else
35+
result[outer_grouping][the_period] = internal_data
36+
end
37+
else
38+
result[outer_grouping] = {
39+
the_period => internal_data
40+
}
41+
end
42+
end
43+
result
44+
end
45+
end
46+
end
47+
end

app/models/dr_rai/data.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module DrRai::Data
2+
def self.table_name_prefix
3+
"dr_rai_data_"
4+
end
5+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class DrRai::Data::Titration < ApplicationRecord
2+
include DrRai::Chartable
3+
4+
default_scope { where(month_date: 1.year.ago..Date.today) }
5+
6+
chartable_internal_keys :follow_up_count, :titrated_count
7+
chartable_period_key :month_date
8+
chartable_outer_grouping :facility_name
9+
end

app/models/dr_rai/titration_indicator.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ class TitrationIndicator < Indicator
44

55
def datasource(region)
66
@region = region
7-
@query ||= TitrationQuery.new(region).call
8-
@query[region.name]
7+
@source ||= DrRai::Data::Titration.chartable
8+
@source[region.name]
99
end
1010

1111
def display_name
@@ -17,11 +17,11 @@ def target_type_frontend
1717
end
1818

1919
def numerator_key
20-
"titrated"
20+
:titrated_count
2121
end
2222

2323
def denominator_key
24-
"patients"
24+
:follow_up_count
2525
end
2626

2727
def action_passive
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module DrRai
2+
# Query Factory
3+
#
4+
# When creating SQL-backed indicators, these indicators come with their own
5+
# queries. The query factory tells the query necessary for certain
6+
# indicators. By default, it vends one query for inserting, and another for
7+
# updating. The primary client for this is the DrRai::DataService which uses
8+
# this to populate the data in the tables.
9+
class QueryFactory
10+
attr_reader :from_date, :to_date
11+
12+
class << self
13+
def for klazz, from: nil, to: nil
14+
raise unless klazz < ApplicationRecord
15+
16+
instance = nil
17+
18+
if klazz <= Data::Titration
19+
instance = DrRai::TitrationQueryFactory.new(from, to)
20+
else
21+
raise "Unsupported"
22+
end
23+
24+
instance
25+
end
26+
end
27+
28+
def initialize from, to
29+
@from = from
30+
@to = to
31+
32+
set_date_boundaries!
33+
end
34+
35+
def inserter
36+
# Format should be
37+
# insert into <tbl> ([...columns]) [..query]
38+
raise "Unimplemented"
39+
end
40+
41+
def updater
42+
# Format should be
43+
# merge into <tbl> as t
44+
# using [...query]
45+
# on month_date
46+
# when not matched and [column compare - insert clause] then
47+
# insert values ([select values])
48+
# when matched and [column compare - update clause] then
49+
# update set [col = old_col <op> new_col];
50+
# see https://www.postgresql.org/docs/current/sql-merge.html#id-1.9.3.156.9
51+
raise "Unimplemented"
52+
end
53+
54+
private
55+
56+
def set_date_boundaries!
57+
@from_date = if @from.nil?
58+
1.year.ago.to_date
59+
else
60+
@from
61+
end
62+
63+
@to_date = if @to.nil?
64+
Date.today
65+
else
66+
@to
67+
end
68+
end
69+
end
70+
end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
module DrRai
2+
class TitrationQueryFactory < QueryFactory
3+
def inserter
4+
base_query do
5+
"insert into public.dr_rai_data_titrations (month_date, facility_name, follow_up_count, titrated_count, titration_rate)"
6+
end
7+
end
8+
9+
def updater
10+
<<-SQL
11+
merge into public.dr_rai_data_titrations as existing
12+
using (#{base_query { "" }}) as incoming
13+
on existing.month_date = incoming.month_date and existing.facility_name = incoming.facility_name
14+
when not matched
15+
insert (
16+
month_date,
17+
facility_name,
18+
follow_up_count,
19+
titrated_count,
20+
titration_rate,
21+
created_at,
22+
updated_at
23+
)
24+
values (
25+
incoming.month_date,
26+
incoming.facility_name,
27+
incoming.follow_up_count,
28+
incoming.titrated_count,
29+
incoming.titration_rate,
30+
now(), -- for created_at
31+
now(), -- for updated_at
32+
)
33+
when matched and existing.titration_rate != incoming.titration_rate
34+
update
35+
set
36+
follow_up_count = incoming.follow_up_count,
37+
titrated_count = incoming.titrated_count,
38+
titration_rate = incoming.titration_rate;
39+
SQL
40+
end
41+
42+
private
43+
44+
def base_query
45+
<<~SQL
46+
with facility_titrations as (
47+
select
48+
reporting_patient_states.month_date,
49+
facility_name,
50+
count(*) as follow_up_count,
51+
count(*) filter (where titrated) as titrated_count,
52+
count(*) filter (where titrated)::float / count(*) * 100 as titration_rate
53+
from reporting_patient_states
54+
inner join reporting_facilities
55+
on reporting_facilities.facility_id = reporting_patient_states.bp_facility_id
56+
where 1 = 1
57+
and "public"."reporting_patient_states"."month_date" between date '#{from_date}' and date '#{to_date}'
58+
and reporting_patient_states.months_since_bp = 0
59+
and reporting_patient_states.last_bp_state = 'uncontrolled'
60+
and reporting_patient_states.months_since_registration > 0
61+
group by 1, 2
62+
order by 1, 2
63+
),
64+
65+
selected_facility_titrations as (
66+
select
67+
month_date,
68+
facility_name,
69+
follow_up_count,
70+
titrated_count,
71+
titration_rate
72+
from facility_titrations
73+
where facility_name in (
74+
select facility_name
75+
from reporting_facilities
76+
)
77+
),
78+
79+
averages as (
80+
select
81+
month_date,
82+
'average' as facility_name,
83+
sum(follow_up_count) as follow_up_count,
84+
sum(titrated_count) as titrated_count,
85+
(sum(titrated_count)::float / sum(follow_up_count)) * 100 as titration_rate
86+
from facility_titrations
87+
group by month_date
88+
)
89+
90+
#{yield}
91+
(
92+
select * from selected_facility_titrations
93+
union all
94+
select * from averages
95+
);
96+
SQL
97+
end
98+
end
99+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module DrRai
2+
class DataService
3+
class << self
4+
def populate klazz, timeline: nil
5+
raise "Can only populate models" unless klazz < ApplicationRecord
6+
if timeline.present?
7+
raise "timeline must be Date range" unless timelines.is_a?(Range)
8+
end
9+
new(klazz, timeline).populate!
10+
end
11+
end
12+
13+
def initialize klazz, timeline
14+
@klazz = klazz
15+
@timeline = timeline
16+
@timeline = 1.year.ago.to_date..Date.today if @timeline.nil?
17+
@query_factory = QueryFactory.for(klazz, from: @timeline.begin, to: @timeline.end)
18+
end
19+
20+
def populate!
21+
@query = if inserting?
22+
@query_factory.inserter
23+
else
24+
@query_factory.updater
25+
end
26+
27+
ApplicationRecord.connection.exec_query(@query)
28+
end
29+
30+
private
31+
32+
def inserting?
33+
@klazz.where(month_date: @timeline).count == 0
34+
end
35+
end
36+
end

config/schedule.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ def local(time)
8888
rake "exotel_tasks:update_all_patients_phone_number_details"
8989
end
9090

91+
every :day, at: local("03:00 am"), roles: [:cron] do
92+
from = 1.month.ago.to_date.to_s
93+
to = Date.today.to_s
94+
rake "dr_rai:populate_titration_data[#{from}, #{to}]"
95+
end
96+
9197
every :day, at: local("01:00 am"), roles: [:cron] do
9298
runner "MarkPatientMobileNumbers.call"
9399
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class CreateDrRaiDataTitrations < ActiveRecord::Migration[6.1]
2+
def change
3+
create_table :dr_rai_data_titrations do |t|
4+
t.string :facility_name
5+
t.integer :titrated_count
6+
t.integer :follow_up_count
7+
t.datetime :month_date
8+
t.decimal :titration_rate, precision: 5, scale: 2
9+
10+
t.timestamps default: -> { "CURRENT_TIMESTAMP" }, null: false
11+
end
12+
end
13+
end

0 commit comments

Comments
 (0)