Add a database default for project_authorizations.is_unique

What this does

Gives project_authorizations.is_unique a database default of true, following the procedure for changing column defaults.

  • A post-deployment migration setting the default.
  • SafelyChangeColumnDefault and attribute :is_unique, default: true on ProjectAuthorization.
  • The before_create :assign_is_unique callback is replaced by that attribute default, which does the same job through the documented mechanism.

Why

The column has no default today, so every write path sets it by hand. project_authorizations_for_migration, the table this one is being swapped for, has no is_unique column at all. Giving the column a default is what lets the application stop setting it.

The default is load bearing until the swap happens. The partial unique index index_unique_project_authorizations_on_unique_project_user covers only rows where is_unique is true, and it is the only thing preventing duplicate (project_id, user_id) rows on this table. A row written with NULL falls outside it.

Why the concern is needed

Rails is configured with partial_inserts, so an INSERT omits a column whose in-memory value equals the cached default and lets the database supply it. While a default is in flux, a process can hold a stale schema cache and fall back to a default it cannot see. SafelyChangeColumnDefault forces the column into every INSERT, and the attribute default keeps the written value owned by the application rather than the schema cache.

What is deliberately left alone

The two write paths that bypass the model are untouched:

  • ProjectAuthorizations::Changes#insert_all_in_batches
  • the upsert in ProjectAuthorization.find_or_create_authorization_for

Neither the concern nor the attribute default reaches insert_all or upsert, and post-deployment migrations run after the code is deployed. Dropping those writes now would leave a window where rows are written with NULL and escape the partial index.

Notes for database review

No table rewrite. change_column_default is catalog only in PostgreSQL 11 and later, so this does not touch the table itself, which is classified over_limit. It takes a brief ACCESS EXCLUSIVE lock to update the catalog.

Existing rows are unchanged. change_column_default does not backfill, so rows with NULL stay NULL and remain outside the partial index. That is the existing duplicate problem tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/418205 and is neither improved nor made worse here.

Follow-up in 19.5

Once the default has shipped everywhere, a second MR removes columns_changing_default, the SafelyChangeColumnDefault include, the attribute default, both remaining writes, and an error message in ee/app/services/onboarding/finish_service.rb that interpolates the column. The procedure requires waiting a full minor release, because self managed bundles a minor release into a single zero downtime deployment.

References

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/627241

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Diane Russel

Merge request reports

Loading
Loading