Draft: Add NOT NULL constraints on the placeholder-table columns
What does this MR do and why?
Stacked on !254114 (finalize) → !254095 (sync index) → !253935 (merged) (base). Adds the NOT NULL constraint on the two columns whose backfill BBMs !254114 finalizes:
import_source_user_placeholder_references.expires_atimport_placeholder_memberships.retention_expires_at
Per not_null_constraints.md's large-table process, the constraint is added after the backfill BBM is finalized, same milestone as the finalize migration in the doc's own worked example (both 16.10) - here that's 19.6.
Validates immediately (add_not_null_constraint's default), not validate: false. That option is explicitly labeled "Optional. For very large tables" in the doc, with its own worked example being merge_request_diffs (over_limit tier, >100GB). These two tables are medium/small tier, and by the time this migration runs the backfill is already confirmed complete via finalization — there's nothing left to violate the constraint, and VALIDATE CONSTRAINT's scan cost is comparable to a plain table scan (~110s measured earlier for the 52M-row table), well inside the post-deploy budget. No separate async-validation follow-up needed — this replaces what was previously planned as a 19.7 task.
Each migration declares DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS referencing its BBM's queued_migration_version, so the Migration::UnfinishedDependencies rubocop cop verifies finalized_by is actually set in the dictionary before this can merge — i.e., it enforces the same ordering this MR's stacking already implies.
Do not merge until !254114 has actually merged and its finalized_by values are live (the cop should catch this anyway, but flagging explicitly).
Full context: #576118
References
Database review data
Both constraints created and validated on a local dev cluster via db:migrate:up (bypassing the still-blocked finalize migrations ahead of them in timestamp order, which correctly can't run until required stop 19.5 passes). Confirmed in the migration log that add_not_null_constraint runs ADD CONSTRAINT ... NOT VALID followed immediately by VALIDATE CONSTRAINT, both in the same migration:
ALTER TABLE import_source_user_placeholder_references
ADD CONSTRAINT check_f6632cc492
CHECK ( expires_at IS NOT NULL )
NOT VALID;
-- -> 0.0015s
ALTER TABLE import_source_user_placeholder_references VALIDATE CONSTRAINT check_f6632cc492;
-- -> 0.0012sALTER TABLE import_placeholder_memberships
ADD CONSTRAINT check_cb5199bf23
CHECK ( retention_expires_at IS NOT NULL )
NOT VALID;
-- -> 0.0016s
ALTER TABLE import_placeholder_memberships VALIDATE CONSTRAINT check_cb5199bf23;
-- -> 0.0013sTrivial locally (tiny local table); at real scale the VALIDATE CONSTRAINT step's cost is bounded by a table scan, not row content — for import_source_user_placeholder_references (~52M rows) that's on the order of the ~110s full-table EXPLAIN measured earlier in !253935 (merged), comfortably inside the post-deploy time budget. No new queries introduced.
Screenshots or screen recordings
Not applicable — database schema change, no UI changes.
How to set up and validate locally
- Merge/rebase on top of the latest
georgekoltsov/not-null-constraint-placeholder-referencesbase chain. bundle exec rails db:migrate(regeneratesdb/structure.sql) — or, if the finalize migrations ahead of these are still blocked by the required-stop check in your environment, test these two in isolation withbundle exec rake db:migrate:up VERSION=<timestamp>.- Run the specs:
bundle exec rspec spec/migrations/20260908140000_add_not_null_constraint_to_import_source_user_placeholder_references_expires_at_spec.rb bundle exec rspec spec/migrations/20260908140100_add_not_null_constraint_to_import_placeholder_memberships_retention_expires_at_spec.rb
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.