Skip to content

Commit 192fcc1

Browse files
Introduce case insensitive collation for patient names
- Searching/querying on these names does not care for case sensitivity - Names will still be stored in their "cased" format but all querying is case insensitive - Replace ILIKE statements without wildcards with simple `=` checks - Replace ILIKE with LIKE otherwise as case insensitivity is irrelevant now
1 parent cc2b892 commit 192fcc1

6 files changed

Lines changed: 128 additions & 40 deletions

File tree

app/lib/patient_matcher.rb

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,19 @@ def self.from_relation(
1313
nhs_number = normalise_nhs_number(nhs_number)
1414
address_postcode = normalise_postcode(address_postcode)
1515

16-
given_name_ilike = ActiveRecord::Base.sanitize_sql_like(given_name)
17-
family_name_ilike = ActiveRecord::Base.sanitize_sql_like(family_name)
18-
1916
if nhs_number.present? && (patient = relation.find_by(nhs_number:)).present?
2017
return [patient]
2118
end
2219

23-
scope =
24-
relation.where(
25-
"given_name ILIKE ? AND family_name ILIKE ?",
26-
given_name_ilike,
27-
family_name_ilike
28-
).where(date_of_birth:)
20+
scope = relation.where(given_name:, family_name:, date_of_birth:)
2921

3022
if address_postcode.present?
3123
scope =
3224
if include_3_out_of_4_matches
3325
scope
34-
.or(
35-
relation.where(
36-
"given_name ILIKE ? AND family_name ILIKE ?",
37-
given_name_ilike,
38-
family_name_ilike
39-
).where(address_postcode:)
40-
)
41-
.or(
42-
relation.where("given_name ILIKE ?", given_name_ilike).where(
43-
date_of_birth:,
44-
address_postcode:
45-
)
46-
)
47-
.or(
48-
relation.where("family_name ILIKE ?", family_name_ilike).where(
49-
date_of_birth:,
50-
address_postcode:
51-
)
52-
)
26+
.or(relation.where(given_name:, family_name:, address_postcode:))
27+
.or(relation.where(given_name:, date_of_birth:, address_postcode:))
28+
.or(relation.where(family_name:, date_of_birth:, address_postcode:))
5329
else
5430
scope.where(address_postcode:)
5531
end

app/models/patient.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
# ethnic_background :integer
1717
# ethnic_background_other :string
1818
# ethnic_group :integer
19-
# family_name :string not null
19+
# family_name :citext not null
2020
# gender_code :integer default("not_known"), not null
21-
# given_name :string not null
21+
# given_name :citext not null
2222
# home_educated :boolean
2323
# invalidated_at :datetime
2424
# local_authority_mhclg_code :string
@@ -227,22 +227,22 @@ class Patient < ApplicationRecord
227227
)
228228
end
229229

230-
ilike_scope =
230+
like_scope =
231231
terms.reduce(self) do |scope, term|
232232
if term.length < 3
233233
scope.and where(
234-
"family_name ILIKE :term || '%' OR given_name ILIKE :term || '%'",
234+
"family_name LIKE :term || '%' OR given_name LIKE :term || '%'",
235235
term:
236236
)
237237
else
238238
scope.and where(
239-
"family_name ILIKE '%' || :term || '%' OR given_name ILIKE '%' || :term || '%'",
239+
"family_name LIKE '%' || :term || '%' OR given_name LIKE '%' || :term || '%'",
240240
term:
241241
)
242242
end
243243
end
244244

245-
similarity_scope.or(ilike_scope).order(
245+
similarity_scope.or(like_scope).order(
246246
Arel.sql(
247247
"(STRICT_WORD_SIMILARITY(given_name, :query) + STRICT_WORD_SIMILARITY(family_name, :query)) DESC",
248248
query:
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# frozen_string_literal: true
2+
3+
class ConvertPatientNamesToCitext < ActiveRecord::Migration[8.1]
4+
disable_ddl_transaction!
5+
6+
def up
7+
enable_extension "citext"
8+
9+
# Drop indexes on given_name/family_name concurrently first so the
10+
# ALTER COLUMN TYPE doesn't hold an ACCESS EXCLUSIVE lock while
11+
# rebuilding them.
12+
remove_index :patients,
13+
column: %i[family_name given_name],
14+
name: "index_patients_on_names_family_first",
15+
algorithm: :concurrently,
16+
if_exists: true
17+
remove_index :patients,
18+
column: %i[given_name family_name],
19+
name: "index_patients_on_names_given_first",
20+
algorithm: :concurrently,
21+
if_exists: true
22+
remove_index :patients,
23+
column: :family_name,
24+
name: "index_patients_on_family_name_trigram",
25+
algorithm: :concurrently,
26+
if_exists: true
27+
remove_index :patients,
28+
column: :given_name,
29+
name: "index_patients_on_given_name_trigram",
30+
algorithm: :concurrently,
31+
if_exists: true
32+
33+
change_table :patients, bulk: true do |t|
34+
t.change :given_name, :citext, null: false
35+
t.change :family_name, :citext, null: false
36+
end
37+
38+
# Recreate all indexes concurrently.
39+
add_index :patients,
40+
%i[family_name given_name],
41+
name: "index_patients_on_names_family_first",
42+
algorithm: :concurrently
43+
add_index :patients,
44+
%i[given_name family_name],
45+
name: "index_patients_on_names_given_first",
46+
algorithm: :concurrently
47+
add_index :patients,
48+
:family_name,
49+
name: "index_patients_on_family_name_trigram",
50+
opclass: :gin_trgm_ops,
51+
using: :gin,
52+
algorithm: :concurrently
53+
add_index :patients,
54+
:given_name,
55+
name: "index_patients_on_given_name_trigram",
56+
opclass: :gin_trgm_ops,
57+
using: :gin,
58+
algorithm: :concurrently
59+
end
60+
61+
def down
62+
remove_index :patients,
63+
column: %i[family_name given_name],
64+
name: "index_patients_on_names_family_first",
65+
algorithm: :concurrently,
66+
if_exists: true
67+
remove_index :patients,
68+
column: %i[given_name family_name],
69+
name: "index_patients_on_names_given_first",
70+
algorithm: :concurrently,
71+
if_exists: true
72+
remove_index :patients,
73+
column: :family_name,
74+
name: "index_patients_on_family_name_trigram",
75+
algorithm: :concurrently,
76+
if_exists: true
77+
remove_index :patients,
78+
column: :given_name,
79+
name: "index_patients_on_given_name_trigram",
80+
algorithm: :concurrently,
81+
if_exists: true
82+
83+
change_table :patients, bulk: true do |t|
84+
t.change :given_name, :string, null: false
85+
t.change :family_name, :string, null: false
86+
end
87+
88+
add_index :patients,
89+
%i[family_name given_name],
90+
name: "index_patients_on_names_family_first",
91+
algorithm: :concurrently
92+
add_index :patients,
93+
%i[given_name family_name],
94+
name: "index_patients_on_names_given_first",
95+
algorithm: :concurrently
96+
add_index :patients,
97+
:family_name,
98+
name: "index_patients_on_family_name_trigram",
99+
opclass: :gin_trgm_ops,
100+
using: :gin,
101+
algorithm: :concurrently
102+
add_index :patients,
103+
:given_name,
104+
name: "index_patients_on_given_name_trigram",
105+
opclass: :gin_trgm_ops,
106+
using: :gin,
107+
algorithm: :concurrently
108+
109+
disable_extension "citext"
110+
end
111+
end

db/schema.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
ActiveRecord::Schema[8.1].define(version: 2026_03_03_152926) do
1414
# These are extensions that must be enabled in order to support this database
15+
enable_extension "citext"
1516
enable_extension "pg_catalog.plpgsql"
1617
enable_extension "pg_trgm"
1718

@@ -708,9 +709,9 @@
708709
t.integer "ethnic_background"
709710
t.string "ethnic_background_other"
710711
t.integer "ethnic_group"
711-
t.string "family_name", null: false
712+
t.citext "family_name", null: false
712713
t.integer "gender_code", default: 0, null: false
713-
t.string "given_name", null: false
714+
t.citext "given_name", null: false
714715
t.bigint "gp_practice_id"
715716
t.boolean "home_educated"
716717
t.datetime "invalidated_at"

spec/factories/patients.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
# ethnic_background :integer
1717
# ethnic_background_other :string
1818
# ethnic_group :integer
19-
# family_name :string not null
19+
# family_name :citext not null
2020
# gender_code :integer default("not_known"), not null
21-
# given_name :string not null
21+
# given_name :citext not null
2222
# home_educated :boolean
2323
# invalidated_at :datetime
2424
# local_authority_mhclg_code :string

spec/models/patient_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
# ethnic_background :integer
1717
# ethnic_background_other :string
1818
# ethnic_group :integer
19-
# family_name :string not null
19+
# family_name :citext not null
2020
# gender_code :integer default("not_known"), not null
21-
# given_name :string not null
21+
# given_name :citext not null
2222
# home_educated :boolean
2323
# invalidated_at :datetime
2424
# local_authority_mhclg_code :string

0 commit comments

Comments
 (0)