Add Govern policy store tables and models
What does this MR do and why?
Adds the foundational data layer for the Policy Store (Policies v2) experiment per GOVERN-008: the first two tables, their models, and a new Govern bounded context.
govern_policies— the store's core record: authoredrules/actions/policy_scope(jsonb) plus the compiledscope_rego. Owned by a top-level group (namespace_id) for the experiment.organization_idis carried on every row purely as the cells sharding key (copied from the group's organization), so the later move to organization-owned policies stays additive (relaxnamespace_idto nullable + a partial unique index) with no rebuild.govern_policy_enforcements— tracks enforcement state per policy and entity, for trigger routing and reconciliation. Uses an intentional polymorphic(entity_type, entity_id)reference per GOVERN-008 with no FK (allowlisted inspec/db/schema_spec.rb, same pattern asiam_outbox): the store is designed for extraction into a standalone service, so it must not hold FKs into feature tables. Integrity is application-owned.Govern::Policy/Govern::PolicyEnforcementmodels under a newGovernbounded context. Keeping the Policy Store behind one namespace and one table prefix draws the extraction seam now, while the code still lives in the monolith.
The tables ship dark: nothing writes to them until the ActiveRecord repository adapter for gems/gitlab-policy-store lands in a follow-up. JSON schema validation for the jsonb columns is also deferred to a follow-up MR; the columns are registered in the jsonb validation TODO list until then.
Why group-anchored and not organization-anchored: Organizations are not open to new features yet, all GitLab.com customers still live in the default organization, and there is no org-level licensing — the full analysis is in work item 606850.
Database
Five migrations, all creating or referencing new empty tables. No queries against existing tables are added or changed, so there are no Database Lab query plans.
| Migration | Contents |
|---|---|
create_govern_policies |
table + unique (organization_id, namespace_id, name) + namespace_id index; text limits at 4096 (max allowed by Migration/PreventLargeBlobInDatabase) |
add_organizations_fk_to_govern_policies |
organization_id → organizations, ON DELETE CASCADE |
add_namespaces_fk_to_govern_policies |
namespace_id → namespaces, ON DELETE CASCADE |
create_govern_policy_enforcements |
table + unique (organization_id, govern_policy_id, entity_type, entity_id) + (entity_type, entity_id) index + govern_policy_id index; inline FK to govern_policies (both tables are new and empty) |
add_organizations_fk_to_govern_policy_enforcements |
organization_id → organizations, ON DELETE CASCADE |
Both tables are gitlab_sec with sharding key organization_id → organizations; unique indexes lead with the sharding key per the Cells guidance, and every FK column keeps a leading index. The models validate the invariant that a row's organization_id equals its group's namespace.organization_id.
Rollback verified with scripts/regenerate-schema --rollback-testing (all migrations revert cleanly).
Migration output
db:migrate output (main database)
main: == 20260729055331 CreateGovernPolicies: migrating =============================
main: -- create_table(:govern_policies)
main: == 20260729055331 CreateGovernPolicies: migrated (0.0506s) ====================
main: == 20260729055332 AddOrganizationsFkToGovernPolicies: migrating ===============
main: -- execute("LOCK TABLE organizations, govern_policies IN SHARE ROW EXCLUSIVE MODE")
main: -- execute("ALTER TABLE govern_policies ADD CONSTRAINT fk_b6e097f2d5 FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE NOT VALID;")
main: -- execute("SET statement_timeout TO 0")
main: -- execute("ALTER TABLE govern_policies VALIDATE CONSTRAINT fk_b6e097f2d5;")
main: -- execute("RESET statement_timeout")
main: == 20260729055332 AddOrganizationsFkToGovernPolicies: migrated (0.0228s) ======
main: == 20260729055333 AddNamespacesFkToGovernPolicies: migrating ==================
main: -- execute("LOCK TABLE namespaces, govern_policies IN SHARE ROW EXCLUSIVE MODE")
main: -- execute("ALTER TABLE govern_policies ADD CONSTRAINT fk_c00d55a290 FOREIGN KEY (namespace_id) REFERENCES namespaces (id) ON DELETE CASCADE NOT VALID;")
main: -- execute("SET statement_timeout TO 0")
main: -- execute("ALTER TABLE govern_policies VALIDATE CONSTRAINT fk_c00d55a290;")
main: -- execute("RESET statement_timeout")
main: == 20260729055333 AddNamespacesFkToGovernPolicies: migrated (0.0166s) =========
main: == 20260729055334 CreateGovernPolicyEnforcements: migrating ===================
main: -- create_table(:govern_policy_enforcements)
main: == 20260729055334 CreateGovernPolicyEnforcements: migrated (0.0124s) ==========
main: == 20260729055335 AddOrganizationsFkToGovernPolicyEnforcements: migrating =====
main: -- execute("LOCK TABLE organizations, govern_policy_enforcements IN SHARE ROW EXCLUSIVE MODE")
main: -- execute("ALTER TABLE govern_policy_enforcements ADD CONSTRAINT fk_f5400ecb85 FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE NOT VALID;")
main: -- execute("SET statement_timeout TO 0")
main: -- execute("ALTER TABLE govern_policy_enforcements VALIDATE CONSTRAINT fk_f5400ecb85;")
main: -- execute("RESET statement_timeout")
main: == 20260729055335 AddOrganizationsFkToGovernPolicyEnforcements: migrated (0.0167s)