Add secondary indexes and Foreign Key to partitioned web_hook_logs
What does this MR do?
Related issue: #323676 (closed)
This is the third step for partitioning the web_hook_logs
table (&5558 (closed)), and the last one necessary to bring the schema of the partitioned web_hook_logs
(web_hook_logs_part_0c5294f417
) on par with the regular web_hook_logs
before we can swap them in the next milestone.
It adds the foreign key and the secondary indexes defined for web_hook_logs
to the partitioned table.
- Foreign key added:
fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
- Indexes added:
index_web_hook_logs_on_created_at_and_web_hook_id btree (created_at, web_hook_id)
index_web_hook_logs_on_web_hook_id btree (web_hook_id)
Issue with Foreign Keys on Partitioned Tables
Starting on PG11, we can add Foreign Keys on Partitioned Tables, referencing other non-partitioned tables.
But there is a catch on all current versions of PostgreSQL (up to the current PG13):
https://www.postgresql.org/docs/11/sql-altertable.html
ADD table_constraint [ NOT VALID ]
[..] Also, foreign key constraints on partitioned tables may not be declared NOT VALID at present.
So, using add_concurrent_foreign_key
(even with validate: true) results to an error:
PG::WrongObjectType: ERROR: cannot add NOT VALID foreign key on partitioned table "web_hook_logs_part_0c5294f417"
referencing relation "web_hooks"
DETAIL: This feature is not yet supported on partitioned tables.
The reason for that is that we set the constraint to NOT VALID
and THEN validate it --> this is not supported for partitioned tables
Also, PostgreSQL will currently ignore NOT VALID constraints on partitions when adding a valid FK to the partitioned table, so they have to also be validated before we can add the final FK. This is the error we get if we try to add a valid FK on a partitioned table while it has been added as a NOT VALID
one on the partitions:
-- add_foreign_key(:web_hook_logs_part_0c5294f417, :web_hooks, {:column=>:web_hook_id, :on_delete=>:cascade, :name=>"fk_rails_bb3355782d", :validate=>true})
rake aborted!
StandardError: An error has occurred, all later migrations canceled:
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "pg_constraint_conrelid_contypid_conname_index"
DETAIL: Key (conrelid, contypid, conname)=(8047040, 0, fk_rails_bb3355782d) already exists.
So our only option would be to add the foreign key using add_foreign_key
. The problem with that is the known issue with add_foreign_key
, as it locks and validates the tables when the FK is added.
For that reason, we have also added a new Partitioning Migration Helper (add_concurrent_partitioned_foreign_key
) which:
- Adds the foreign key first to each partition by using
add_concurrent_foreign_key
and validating it - Once all partitions have a foreign key, add it also to the partitioned table using the simple
add_foreign_key
helper - For the reasons mentioned above, this method does not include an option to delay the validation, we have to enforce
validate: true
.
With respect to structure.sql
, the end result is exactly the same as using add_foreign_key
directly on the partitioned table.
Migrations
db:migrate
== 20210412183800 AddPartitionedWebHookLogIndexes: migrating ==================
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_created_at_and_web_hook_id")
-> 0.0007s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", [:created_at, :web_hook_id], {:name=>"index_eecfac613f", :algorithm=>:concurrently})
-> 0.0009s
-- execute("SET statement_timeout TO 0")
-> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", [:created_at, :web_hook_id], {:name=>"index_eecfac613f", :algorithm=>:concurrently})
-> 0.0035s
-- execute("RESET ALL")
-> 0.0007s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", [:created_at, :web_hook_id], {:name=>"index_5dc0dc60d8", :algorithm=>:concurrently})
-> 0.0008s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", [:created_at, :web_hook_id], {:name=>"index_5dc0dc60d8", :algorithm=>:concurrently})
-> 0.0026s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", [:created_at, :web_hook_id], {:name=>"index_b7bc5cf1cc", :algorithm=>:concurrently})
-> 0.0011s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", [:created_at, :web_hook_id], {:name=>"index_b7bc5cf1cc", :algorithm=>:concurrently})
-> 0.0023s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", [:created_at, :web_hook_id], {:name=>"index_b7d0090183", :algorithm=>:concurrently})
-> 0.0007s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", [:created_at, :web_hook_id], {:name=>"index_b7d0090183", :algorithm=>:concurrently})
-> 0.0028s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", [:created_at, :web_hook_id], {:name=>"index_78c85c2c10", :algorithm=>:concurrently})
-> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", [:created_at, :web_hook_id], {:name=>"index_78c85c2c10", :algorithm=>:concurrently})
-> 0.0028s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", [:created_at, :web_hook_id], {:name=>"index_d0cba9e188", :algorithm=>:concurrently})
-> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", [:created_at, :web_hook_id], {:name=>"index_d0cba9e188", :algorithm=>:concurrently})
-> 0.0027s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", [:created_at, :web_hook_id], {:name=>"index_b1292a72b5", :algorithm=>:concurrently})
-> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", [:created_at, :web_hook_id], {:name=>"index_b1292a72b5", :algorithm=>:concurrently})
-> 0.0030s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", [:created_at, :web_hook_id], {:name=>"index_9cd16f1883", :algorithm=>:concurrently})
-> 0.0008s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", [:created_at, :web_hook_id], {:name=>"index_9cd16f1883", :algorithm=>:concurrently})
-> 0.0030s
-- add_index(:web_hook_logs_part_0c5294f417, [:created_at, :web_hook_id], {:name=>"index_web_hook_logs_part_on_created_at_and_web_hook_id"})
-> 0.0031s
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_web_hook_id")
-> 0.0006s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", :web_hook_id, {:name=>"index_80bf138a51", :algorithm=>:concurrently})
-> 0.0012s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", :web_hook_id, {:name=>"index_80bf138a51", :algorithm=>:concurrently})
-> 0.0033s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", :web_hook_id, {:name=>"index_d6756f0be7", :algorithm=>:concurrently})
-> 0.0015s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", :web_hook_id, {:name=>"index_d6756f0be7", :algorithm=>:concurrently})
-> 0.0027s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", :web_hook_id, {:name=>"index_a0034b7fa9", :algorithm=>:concurrently})
-> 0.0011s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", :web_hook_id, {:name=>"index_a0034b7fa9", :algorithm=>:concurrently})
-> 0.0028s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", :web_hook_id, {:name=>"index_b0d3af5ca1", :algorithm=>:concurrently})
-> 0.0015s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", :web_hook_id, {:name=>"index_b0d3af5ca1", :algorithm=>:concurrently})
-> 0.0036s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", :web_hook_id, {:name=>"index_37c3fb440a", :algorithm=>:concurrently})
-> 0.0013s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", :web_hook_id, {:name=>"index_37c3fb440a", :algorithm=>:concurrently})
-> 0.0028s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", :web_hook_id, {:name=>"index_9325dbf04d", :algorithm=>:concurrently})
-> 0.0011s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", :web_hook_id, {:name=>"index_9325dbf04d", :algorithm=>:concurrently})
-> 0.0025s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", :web_hook_id, {:name=>"index_4a63751fa3", :algorithm=>:concurrently})
-> 0.0014s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", :web_hook_id, {:name=>"index_4a63751fa3", :algorithm=>:concurrently})
-> 0.0029s
-- transaction_open?()
-> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", :web_hook_id, {:name=>"index_fb2dcc655b", :algorithm=>:concurrently})
-> 0.0017s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", :web_hook_id, {:name=>"index_fb2dcc655b", :algorithm=>:concurrently})
-> 0.0032s
-- add_index(:web_hook_logs_part_0c5294f417, :web_hook_id, {:name=>"index_web_hook_logs_part_on_web_hook_id"})
-> 0.0034s
== 20210412183800 AddPartitionedWebHookLogIndexes: migrated (0.1508s) =========
== 20210412183810 AddPartitionedWebHookLogFk: migrating =======================
-- foreign_keys(:web_hook_logs_part_0c5294f417)
-> 0.0027s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000")
-> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0017s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104")
-> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0011s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0013s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105")
-> 0.0025s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0011s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0012s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106")
-> 0.0017s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0009s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0013s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107")
-> 0.0022s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0013s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0013s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108")
-> 0.0018s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0014s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0015s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109")
-> 0.0021s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0010s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0013s
-- transaction_open?()
-> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110")
-> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
-> 0.0010s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
-> 0.0014s
-- add_foreign_key(:web_hook_logs_part_0c5294f417, :web_hooks, {:column=>:web_hook_id, :on_delete=>:cascade, :name=>"fk_rails_bb3355782d", :validate=>true})
-> 0.0031s
== 20210412183810 AddPartitionedWebHookLogFk: migrated (0.0873s) ==============
db:rollback
== 20210412183810 AddPartitionedWebHookLogFk: reverting =======================
-- foreign_keys(:web_hook_logs_part_0c5294f417)
-> 0.0024s
-- remove_foreign_key(:web_hook_logs_part_0c5294f417, {:column=>:web_hook_id})
-> 0.0036s
== 20210412183810 AddPartitionedWebHookLogFk: reverted (0.0112s) ==============
== 20210412183800 AddPartitionedWebHookLogIndexes: reverting ==================
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_web_hook_id")
-> 0.0009s
-- remove_index(:web_hook_logs_part_0c5294f417, {:name=>"index_web_hook_logs_part_on_web_hook_id"})
-> 0.0014s
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_created_at_and_web_hook_id")
-> 0.0006s
-- remove_index(:web_hook_logs_part_0c5294f417, {:name=>"index_web_hook_logs_part_on_created_at_and_web_hook_id"})
-> 0.0014s
== 20210412183800 AddPartitionedWebHookLogIndexes: reverted (0.0423s) =========
Additional Checks that everything went as planned
Double Checking the table schemas:
details
gitlabhq_development=# \d web_hook_logs
Table "public.web_hook_logs"
... ... ...
Indexes:
"web_hook_logs_pkey" PRIMARY KEY, btree (id)
"index_web_hook_logs_on_created_at_and_web_hook_id" btree (created_at, web_hook_id)
"index_web_hook_logs_on_web_hook_id" btree (web_hook_id)
Foreign-key constraints:
"fk_rails_666826e111" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
Triggers:
table_sync_trigger_b99eb6998c AFTER INSERT OR DELETE OR UPDATE ON web_hook_logs FOR EACH ROW EXECUTE PROCEDURE table_sync_function_29bc99d6db()
gitlabhq_development=# \d web_hook_logs_part_0c5294f417
Table "public.web_hook_logs_part_0c5294f417"
... ... ...
Partition key: RANGE (created_at)
Indexes:
"web_hook_logs_part_0c5294f417_pkey" PRIMARY KEY, btree (id, created_at)
"index_web_hook_logs_part_on_created_at_and_web_hook_id" btree (created_at, web_hook_id)
"index_web_hook_logs_part_on_web_hook_id" btree (web_hook_id)
Foreign-key constraints:
"fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
gitlabhq_development=# \d gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104
Table "gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104"
... ... ...
Partition of: web_hook_logs_part_0c5294f417 FOR VALUES FROM ('2021-04-01 00:00:00') TO ('2021-05-01 00:00:00')
Indexes:
"web_hook_logs_part_0c5294f417_202104_pkey" PRIMARY KEY, btree (id, created_at)
"index_5dc0dc60d8" btree (created_at, web_hook_id)
"index_d6756f0be7" btree (web_hook_id)
Foreign-key constraints:
"fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
Adding a new partition:
details
gitlabhq_development=# CREATE TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111
PARTITION OF web_hook_logs_part_0c5294f417
FOR VALUES FROM ('2021-11-01') TO ('2021-12-01');
gitlabhq_development=# \d gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111
Table "gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111"
... ... ...
Partition of: web_hook_logs_part_0c5294f417 FOR VALUES FROM ('2021-11-01 00:00:00') TO ('2021-12-01 00:00:00')
Indexes:
"web_hook_logs_part_0c5294f417_202111_pkey" PRIMARY KEY, btree (id, created_at)
"web_hook_logs_part_0c5294f417_202111_created_at_web_hook_id_idx" btree (created_at, web_hook_id)
"web_hook_logs_part_0c5294f417_202111_web_hook_id_idx" btree (web_hook_id)
Foreign-key constraints:
"fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
Testing the PartitionCreator
:
details
gitlabhq_development=# DROP TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110;
gitlabhq_development=# DROP TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111;
pry(main)> Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions
... ... ...
(5.3ms) CREATE TABLE IF NOT EXISTS "gitlab_partitions_dynamic"."web_hook_logs_part_0c5294f417_202110"
... ... ...
gitlabhq_development=# \d gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110
Table "gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110"
... ... ...
Partition of: web_hook_logs_part_0c5294f417 FOR VALUES FROM ('2021-10-01 00:00:00') TO ('2021-11-01 00:00:00')
Indexes:
"web_hook_logs_part_0c5294f417_202110_pkey" PRIMARY KEY, btree (id, created_at)
"web_hook_logs_part_0c5294f417_202110_created_at_web_hook_id_idx" btree (created_at, web_hook_id)
"web_hook_logs_part_0c5294f417_202110_web_hook_id_idx" btree (web_hook_id)
Foreign-key constraints:
"fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
Does this MR meet the acceptance criteria?
Conformity
-
📋 Does this MR need a changelog?-
I have included a changelog entry.
-
- [-] Documentation (if required)
-
Code review guidelines -
Merge request performance guidelines -
Style guides -
Database guides - [-] Separation of EE specific content
Availability and Testing
-
Review and add/update tests for this feature/bug. Consider all test levels. See the Test Planning Process. - [-] Tested in all supported browsers
- [-] Informed Infrastructure department of a default or new setting change, if applicable per definition of done
Security
If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:
- [-] Label as security and @ mention
@gitlab-com/gl-security/appsec
- [-] The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
- [-] Security reports checked/validated by a reviewer from the AppSec team