Skip to content

Commit a7a02ec

Browse files
committed
Enforce uniqueness constraint on user_proxy_exercise_exercises
Similar to e9ab9f8, the table user_proxy_exercise_exercises lacked uniqueness constraints and a foreign key constraint. The model, in turn, has proper validations already in place. The SQL query used in the migration aims to automatically delete duplicate entries. To do so, it builds a list of non-unique entries, sorts them according to their dense_rank() and (per User and ProxyExercise) removes all but the first.
1 parent dc9d321 commit a7a02ec

File tree

3 files changed

+49
-6
lines changed

3 files changed

+49
-6
lines changed

app/models/proxy_exercise.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ def get_matching_exercise(user)
6969
raise "Unknown algorithm #{algorithm}"
7070
end
7171

72-
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user:,
72+
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create!(user:,
7373
exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
7474
matching_exercise
7575
end
76+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
77+
retry
7678
end
7779

7880
def find_matching_exercise(user)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
class AddUniquenessConstraintToUserProxyExerciseExercises < ActiveRecord::Migration[8.0]
4+
class UserProxyExerciseExercise < ApplicationRecord
5+
end
6+
7+
def change
8+
change_column_null :user_proxy_exercise_exercises, :user_id, false
9+
change_column_null :user_proxy_exercise_exercises, :user_type, false
10+
change_column_null :user_proxy_exercise_exercises, :proxy_exercise_id, false
11+
change_column_null :user_proxy_exercise_exercises, :exercise_id, false
12+
13+
add_foreign_key :user_proxy_exercise_exercises, :proxy_exercises
14+
add_foreign_key :user_proxy_exercise_exercises, :exercises
15+
16+
up_only do
17+
# We cannot add a unique index to a table that has duplicate rows.
18+
# Hence, we keep the oldest row(s) and remove the others.
19+
execute <<~SQL.squish
20+
WITH duplicates AS (SELECT
21+
dense_rank() over (PARTITION BY a.user_id, a.user_type, a.proxy_exercise_id, a.exercise_id ORDER BY a.created_at) AS row,
22+
a.id AS upee_id
23+
FROM user_proxy_exercise_exercises a
24+
JOIN user_proxy_exercise_exercises b
25+
ON a.user_id = b.user_id
26+
AND a.user_type = b.user_type
27+
AND a.proxy_exercise_id = b.proxy_exercise_id
28+
AND a.exercise_id = b.exercise_id
29+
AND a.id != b.id
30+
ORDER BY a.created_at)
31+
DELETE FROM user_proxy_exercise_exercises
32+
WHERE id IN (SELECT upee_id FROM duplicates WHERE row != 1)
33+
SQL
34+
end
35+
36+
add_index :user_proxy_exercise_exercises, %i[user_id user_type proxy_exercise_id], unique: true
37+
end
38+
end

db/schema.rb

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)