Skip to content

Commit cf1b4f6

Browse files
authored
Merge pull request #5798 from simpledotorg/SIMPLEBACK-34
Simpleback 34
2 parents 554cc9d + 605e109 commit cf1b4f6

11 files changed

Lines changed: 337 additions & 2 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
class Api::V4::PatientScoresController < Api::V4::SyncController
2+
def sync_to_user
3+
__sync_to_user__("patient_scores")
4+
end
5+
6+
def current_facility_records
7+
@current_facility_records ||=
8+
PatientScore
9+
.for_sync
10+
.where(patient: current_facility.prioritized_patients.select(:id))
11+
.order(:updated_at, :id)
12+
.limit(limit)
13+
.offset((current_page - 1) * limit)
14+
.to_a
15+
end
16+
17+
def other_facility_records
18+
[]
19+
end
20+
21+
private
22+
23+
def transform_to_response(patient_score)
24+
Api::V4::PatientScoreTransformer.to_response(patient_score)
25+
end
26+
27+
def current_page
28+
page = process_token[:next_page].to_i
29+
page < 1 ? 1 : page
30+
end
31+
32+
def response_process_token
33+
{
34+
current_facility_id: current_facility.id,
35+
next_page: current_facility_records.empty? ? 1 : current_page + 1,
36+
resync_token: resync_token,
37+
sync_region_id: current_sync_region.id
38+
}
39+
end
40+
end

app/models/patient_score.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class PatientScore < ApplicationRecord
2+
include Mergeable
3+
include Discard::Model
4+
5+
belongs_to :patient, optional: true
6+
7+
validates :device_created_at, presence: true
8+
validates :device_updated_at, presence: true
9+
validates :score_type, presence: true
10+
validates :score_value, presence: true, numericality: true
11+
12+
scope :for_sync, -> { with_discarded }
13+
end

app/schema/api/v4/models.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ def patient_attribute
117117
required: %w[id patient_id height weight created_at updated_at]}
118118
end
119119

120+
def patient_score
121+
{type: :object,
122+
properties: {
123+
id: {"$ref" => "#/definitions/uuid"},
124+
patient_id: {"$ref" => "#/definitions/uuid"},
125+
score_type: {"$ref" => "#/definitions/non_empty_string"},
126+
score_value: {type: :number},
127+
deleted_at: {"$ref" => "#/definitions/nullable_timestamp"},
128+
created_at: {"$ref" => "#/definitions/timestamp"},
129+
updated_at: {"$ref" => "#/definitions/timestamp"}
130+
},
131+
required: %w[id patient_id score_type score_value created_at updated_at]}
132+
end
133+
120134
def patient_phone_number
121135
{
122136
type: :object,
@@ -458,6 +472,8 @@ def definitions
458472
patient: patient,
459473
patient_attribute: patient_attribute,
460474
patient_attributes: Api::CommonDefinitions.array_of("patient_attribute"),
475+
patient_score: patient_score,
476+
patient_scores: Api::CommonDefinitions.array_of("patient_score"),
461477
patient_business_identifier: Api::V3::Models.patient_business_identifier,
462478
patient_business_identifiers: Api::CommonDefinitions.array_of("patient_business_identifier"),
463479
phone_number: Api::V3::Models.phone_number,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class Api::V4::PatientScoreTransformer < Api::V4::Transformer
2+
class << self
3+
def to_response(payload)
4+
super(payload)
5+
.merge({
6+
"score_type" => payload["score_type"],
7+
"score_value" => payload["score_value"].to_f
8+
})
9+
end
10+
11+
def from_request(payload)
12+
super(payload)
13+
.merge({
14+
"score_type" => payload["score_type"],
15+
"score_value" => payload["score_value"].to_f
16+
})
17+
end
18+
end
19+
end

config/routes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@
7171
get "sync", to: "cvd_risks#sync_to_user"
7272
post "sync", to: "cvd_risks#sync_from_user"
7373
end
74+
75+
scope :patient_scores do
76+
get "sync", to: "patient_scores#sync_to_user"
77+
end
7478
end
7579

7680
namespace :webview do
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
class SpreadPatientScoresUpdatedAt < ActiveRecord::Migration[6.1]
4+
def up
5+
# Find the clustered timestamp that's causing sync pagination issues
6+
# This happens when bulk inserts share the same updated_at
7+
clustered_timestamps = PatientScore.group(:updated_at)
8+
.having("count(*) > 1000")
9+
.count
10+
.keys
11+
12+
return if clustered_timestamps.empty?
13+
14+
clustered_timestamps.each do |clustered_timestamp|
15+
Rails.logger.info "Spreading updated_at for PatientScores with timestamp: #{clustered_timestamp}"
16+
17+
# First, check if device_updated_at is well-distributed
18+
device_updated_distribution = PatientScore
19+
.where(updated_at: clustered_timestamp)
20+
.group(:device_updated_at)
21+
.count
22+
.sort_by { |_, n| -n }
23+
.first(5)
24+
25+
max_device_cluster = device_updated_distribution.first&.last || 0
26+
27+
if max_device_cluster < 1000
28+
# device_updated_at is well-distributed, use it
29+
Rails.logger.info "Using device_updated_at (max cluster: #{max_device_cluster})"
30+
PatientScore
31+
.where(updated_at: clustered_timestamp)
32+
.update_all("updated_at = device_updated_at")
33+
else
34+
# device_updated_at is also clustered, spread by id with millisecond offsets
35+
Rails.logger.info "Spreading by id with millisecond offsets (device_updated_at max cluster: #{max_device_cluster})"
36+
ActiveRecord::Base.connection.execute(<<-SQL.squish)
37+
UPDATE patient_scores ps
38+
SET updated_at = '#{clustered_timestamp}'::timestamp
39+
+ (sub.row_num * interval '1 millisecond')
40+
FROM (
41+
SELECT id, row_number() OVER (ORDER BY id) AS row_num
42+
FROM patient_scores
43+
WHERE updated_at = '#{clustered_timestamp}'
44+
) sub
45+
WHERE ps.id = sub.id
46+
SQL
47+
end
48+
end
49+
end
50+
51+
def down
52+
# This migration cannot be reversed as we don't track original values
53+
raise ActiveRecord::IrreversibleMigration
54+
end
55+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class CreatePatientScores < ActiveRecord::Migration[6.1]
2+
def up
3+
create_table :patient_scores, id: :uuid do |t|
4+
t.references :patient, null: false, foreign_key: true, type: :uuid
5+
t.string :score_type, null: false, limit: 100
6+
t.decimal :score_value, precision: 5, scale: 2, null: false
7+
t.datetime :device_created_at, null: false
8+
t.datetime :device_updated_at, null: false
9+
t.datetime :deleted_at
10+
11+
t.timestamps
12+
end
13+
14+
add_index :patient_scores, [:patient_id, :score_type]
15+
add_index :patient_scores, :updated_at
16+
end
17+
18+
def down
19+
drop_table :patient_scores
20+
end
21+
end

db/structure.sql

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2709,6 +2709,23 @@ CREATE TABLE public.patient_phone_numbers (
27092709
);
27102710

27112711

2712+
--
2713+
-- Name: patient_scores; Type: TABLE; Schema: public; Owner: -
2714+
--
2715+
2716+
CREATE TABLE public.patient_scores (
2717+
id uuid NOT NULL,
2718+
patient_id uuid NOT NULL,
2719+
score_type character varying(100) NOT NULL,
2720+
score_value numeric(5,2) NOT NULL,
2721+
device_created_at timestamp without time zone NOT NULL,
2722+
device_updated_at timestamp without time zone NOT NULL,
2723+
deleted_at timestamp without time zone,
2724+
created_at timestamp(6) without time zone NOT NULL,
2725+
updated_at timestamp(6) without time zone NOT NULL
2726+
);
2727+
2728+
27122729
--
27132730
-- Name: prescription_drugs; Type: TABLE; Schema: public; Owner: -
27142731
--
@@ -7424,6 +7441,14 @@ ALTER TABLE ONLY public.patient_phone_numbers
74247441
ADD CONSTRAINT patient_phone_numbers_pkey PRIMARY KEY (id);
74257442

74267443

7444+
--
7445+
-- Name: patient_scores patient_scores_pkey; Type: CONSTRAINT; Schema: public; Owner: -
7446+
--
7447+
7448+
ALTER TABLE ONLY public.patient_scores
7449+
ADD CONSTRAINT patient_scores_pkey PRIMARY KEY (id);
7450+
7451+
74277452
--
74287453
-- Name: patients patients_pkey; Type: CONSTRAINT; Schema: public; Owner: -
74297454
--
@@ -8527,6 +8552,20 @@ CREATE INDEX index_patient_phone_numbers_on_dnd_status ON public.patient_phone_n
85278552
CREATE INDEX index_patient_phone_numbers_on_patient_id ON public.patient_phone_numbers USING btree (patient_id);
85288553

85298554

8555+
--
8556+
-- Name: index_patient_scores_on_patient_id_and_score_type; Type: INDEX; Schema: public; Owner: -
8557+
--
8558+
8559+
CREATE INDEX index_patient_scores_on_patient_id_and_score_type ON public.patient_scores USING btree (patient_id, score_type);
8560+
8561+
8562+
--
8563+
-- Name: index_patient_scores_on_updated_at; Type: INDEX; Schema: public; Owner: -
8564+
--
8565+
8566+
CREATE INDEX index_patient_scores_on_updated_at ON public.patient_scores USING btree (updated_at);
8567+
8568+
85308569
--
85318570
-- Name: index_patient_registrations_per_day_per_facilities; Type: INDEX; Schema: public; Owner: -
85328571
--
@@ -9165,6 +9204,14 @@ ALTER TABLE ONLY public.patient_phone_numbers
91659204
ADD CONSTRAINT fk_rails_0145dd0b05 FOREIGN KEY (patient_id) REFERENCES public.patients(id);
91669205

91679206

9207+
--
9208+
-- Name: patient_scores fk_rails_0209112204; Type: FK CONSTRAINT; Schema: public; Owner: -
9209+
--
9210+
9211+
ALTER TABLE ONLY public.patient_scores
9212+
ADD CONSTRAINT fk_rails_0209112204 FOREIGN KEY (patient_id) REFERENCES public.patients(id);
9213+
9214+
91689215
--
91699216
-- Name: facility_groups fk_rails_0ba9e6af98; Type: FK CONSTRAINT; Schema: public; Owner: -
91709217
--
@@ -9713,6 +9760,7 @@ INSERT INTO "schema_migrations" (version) VALUES
97139760
('20260127150000'),
97149761
('20260128094448'),
97159762
('20260205110957'),
9763+
('20260209112204'),
97169764
('20260212195326'),
97179765
('20260224063659'),
97189766
('20260316093605'),

spec/controllers/api/v3/patients_controller_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,15 @@ def create_record_list(n, options = {})
298298
.except("merged_by_user_id")
299299
.except("merged_into_patient_id")
300300
.except("test_data")
301-
.except("deleted_by_user_id"))
301+
.except("deleted_by_user_id")
302+
.except("diagnosed_confirmed_at"))
302303
.to eq(updated_patient_payload.with_int_timestamps
303304
.except("id")
304305
.except("address")
305306
.except("phone_numbers")
306307
.except("business_identifiers")
307-
.except("registration_facility_id"))
308+
.except("registration_facility_id")
309+
.except("diagnosed_confirmed_at"))
308310
end
309311
end
310312

0 commit comments

Comments
 (0)