From e70ae4c528f3d7daae3070be2029ef5c1c012947 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Fri, 31 Jul 2026 12:46:19 +0200 Subject: [PATCH] style(rubocop): resolve Rails/UniqueValidationWithoutIndex with DB indexes Add unique indexes backing uniqueness validations on 7 models: - AuthService: [uid, provider] (also fixes validation to use scope: instead of invalid constraint:) - Chapter: name, email - FeedbackRequest: [member_id, workshop_id], token - Invitation: [member_id, event_id, role] - MeetingInvitation: [member_id, meeting_id] - WorkshopInvitation: [member_id, workshop_id, role] - WorkshopSponsor: [sponsor_id, workshop_id] --- .rubocop_todo.yml | 11 +---------- app/models/auth_service.rb | 2 +- ...506_add_unique_indexes_for_unique_validations.rb | 13 +++++++++++++ db/schema.rb | 11 ++++++++++- 4 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 db/migrate/20260731104506_add_unique_indexes_for_unique_validations.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9de0e18d9..b2a22bea0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -184,13 +184,4 @@ Rails/SkipsModelValidations: - 'app/controllers/workshop_invitation_controller.rb' - 'app/services/invitation_logger.rb' -# Offense count: 8 -Rails/UniqueValidationWithoutIndex: - Exclude: - - 'app/models/auth_service.rb' - - 'app/models/chapter.rb' - - 'app/models/feedback_request.rb' - - 'app/models/invitation.rb' - - 'app/models/meeting_invitation.rb' - - 'app/models/workshop_invitation.rb' - - 'app/models/workshop_sponsor.rb' + diff --git a/app/models/auth_service.rb b/app/models/auth_service.rb index 4618b0617..4bc192056 100644 --- a/app/models/auth_service.rb +++ b/app/models/auth_service.rb @@ -1,4 +1,4 @@ class AuthService < ApplicationRecord belongs_to :member, optional: true - validates :uid, uniqueness: { constraint: :provider } + validates :uid, uniqueness: { scope: :provider } end diff --git a/db/migrate/20260731104506_add_unique_indexes_for_unique_validations.rb b/db/migrate/20260731104506_add_unique_indexes_for_unique_validations.rb new file mode 100644 index 000000000..9da086808 --- /dev/null +++ b/db/migrate/20260731104506_add_unique_indexes_for_unique_validations.rb @@ -0,0 +1,13 @@ +class AddUniqueIndexesForUniqueValidations < ActiveRecord::Migration[8.1] + def change + add_index :auth_services, %i[uid provider], unique: true + add_index :chapters, :name, unique: true + add_index :chapters, :email, unique: true + add_index :feedback_requests, %i[member_id workshop_id], unique: true + add_index :feedback_requests, :token, unique: true + add_index :invitations, %i[member_id event_id role], unique: true + add_index :meeting_invitations, %i[member_id meeting_id], unique: true + add_index :workshop_invitations, %i[member_id workshop_id role], unique: true + add_index :workshop_sponsors, %i[sponsor_id workshop_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9283847eb..e251480c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_07_29_151201) do +ActiveRecord::Schema[8.1].define(version: 2026_07_31_104506) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -75,6 +75,7 @@ t.string "uid" t.datetime "updated_at", precision: nil t.index ["member_id"], name: "index_auth_services_on_member_id" + t.index ["uid", "provider"], name: "index_auth_services_on_uid_and_provider", unique: true end create_table "bans", id: :serial, force: :cascade do |t| @@ -102,6 +103,8 @@ t.string "slug" t.string "time_zone", default: "London", null: false t.datetime "updated_at", precision: nil + t.index ["email"], name: "index_chapters_on_email", unique: true + t.index ["name"], name: "index_chapters_on_name", unique: true end create_table "chapters_events", id: :serial, force: :cascade do |t| @@ -242,7 +245,9 @@ t.string "token" t.datetime "updated_at", precision: nil t.integer "workshop_id" + t.index ["member_id", "workshop_id"], name: "index_feedback_requests_on_member_id_and_workshop_id", unique: true t.index ["member_id"], name: "index_feedback_requests_on_member_id" + t.index ["token"], name: "index_feedback_requests_on_token", unique: true t.index ["workshop_id"], name: "index_feedback_requests_on_workshop_id" end @@ -349,6 +354,7 @@ t.index ["event_id", "attending"], name: "index_invitations_event_attending" t.index ["event_id"], name: "index_invitations_on_event_id" t.index ["member_id", "attending"], name: "index_invitations_member_attending" + t.index ["member_id", "event_id", "role"], name: "index_invitations_on_member_id_and_event_id_and_role", unique: true t.index ["member_id"], name: "index_invitations_on_member_id" t.index ["verified_by_id"], name: "index_invitations_on_verified_by_id" end @@ -391,6 +397,7 @@ t.index ["meeting_id", "attending"], name: "index_meeting_invitations_meeting_attending" t.index ["meeting_id"], name: "index_meeting_invitations_on_meeting_id" t.index ["member_id", "attending"], name: "index_meeting_invitations_member_attending" + t.index ["member_id", "meeting_id"], name: "index_meeting_invitations_on_member_id_and_meeting_id", unique: true t.index ["member_id"], name: "index_meeting_invitations_on_member_id" end @@ -601,6 +608,7 @@ t.datetime "updated_at", precision: nil t.integer "workshop_id" t.index ["member_id", "attending"], name: "index_workshop_invitations_member_attending" + t.index ["member_id", "workshop_id", "role"], name: "idx_on_member_id_workshop_id_role_e3cea6bbfd", unique: true t.index ["member_id"], name: "index_workshop_invitations_on_member_id" t.index ["token"], name: "index_workshop_invitations_on_token", unique: true t.index ["workshop_id", "attending"], name: "index_workshop_invitations_workshop_attending" @@ -613,6 +621,7 @@ t.integer "sponsor_id" t.datetime "updated_at", precision: nil t.integer "workshop_id" + t.index ["sponsor_id", "workshop_id"], name: "index_workshop_sponsors_on_sponsor_id_and_workshop_id", unique: true t.index ["sponsor_id"], name: "index_workshop_sponsors_on_sponsor_id" t.index ["workshop_id", "host"], name: "index_workshop_sponsors_on_workshop_id_and_host" t.index ["workshop_id"], name: "index_workshop_sponsors_on_workshop_id"