Skip to content

Commit 6a89ac6

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 d673f82 commit 6a89ac6

5 files changed

Lines changed: 129 additions & 26 deletions

File tree

app/models/patient.rb

Lines changed: 10 additions & 19 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:
@@ -495,32 +495,23 @@ def self.match_existing(
495495
return [patient]
496496
end
497497

498-
scope =
499-
Patient.where(
500-
"given_name ILIKE ? AND family_name ILIKE ?",
501-
given_name,
502-
family_name
503-
).where(date_of_birth:)
498+
scope = Patient.where(given_name:, family_name:).where(date_of_birth:)
504499

505500
if address_postcode.present?
506501
scope =
507502
if include_3_out_of_4_matches
508503
scope
509504
.or(
510-
Patient.where(
511-
"given_name ILIKE ? AND family_name ILIKE ?",
512-
given_name,
513-
family_name
514-
).where(address_postcode:)
505+
Patient.where(given_name:, family_name:).where(address_postcode:)
515506
)
516507
.or(
517-
Patient.where("given_name ILIKE ?", given_name).where(
508+
Patient.where(given_name:).where(
518509
date_of_birth:,
519510
address_postcode:
520511
)
521512
)
522513
.or(
523-
Patient.where("family_name ILIKE ?", family_name).where(
514+
Patient.where(family_name:).where(
524515
date_of_birth:,
525516
address_postcode:
526517
)
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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.1].define(version: 2026_02_09_161431) do
13+
ActiveRecord::Schema[8.1].define(version: 2026_02_17_142242) 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

@@ -698,9 +699,9 @@
698699
t.integer "ethnic_background"
699700
t.string "ethnic_background_other"
700701
t.integer "ethnic_group"
701-
t.string "family_name", null: false
702+
t.citext "family_name", null: false
702703
t.integer "gender_code", default: 0, null: false
703-
t.string "given_name", null: false
704+
t.citext "given_name", null: false
704705
t.bigint "gp_practice_id"
705706
t.boolean "home_educated"
706707
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)