Loading
feat(datastore): namespace lifecycle columns and state helpers (S33 Phase 1 Step 1)
S33 Phase 1, Step 1 of the implementation plan: the schema substrate for the GitLab API namespace lifecycle (S33 spec, Data Model).
- The five ADR-007 lifecycle columns on
namespaces(deleted_at,purged_at,blocked_at,disabled_at,suspended_at), alltimestamptz NULL, no defaults. - The anchor unique index becomes partial (
WHERE purged_at IS NULL), so a purged owner can re-onboard with a new row while live and soft-deleted rows keep holding their anchor.unique_namespaces_slugstays total: a purged slug is permanently retired (ADR-015). - State helpers in
internal/datastore/namespace_state.go:NamespaceStatus(derived status, ADR-007 precedence),WriteServiceableandReadServiceable(the spec's gating predicates). Free functions outside the generated Jet package so regeneration cannot clobber them. Steps 3-8 consume them; no callers yet. - Migration-guide update, paired with the migration that needed it:
CREATE INDEX CONCURRENTLYrequires a drop-before-create guard, because a failed build leaves anINVALIDindex thatIF NOT EXISTSsilently adopts on retry.
Size note: ~760 reviewable LOC, but ~650 of those are tests (unit plus schema/DML integration); the production surface is ~110 LOC.
Review notes:
- The retry guard keeps one residual window (a crash between the swap's
DROPandRENAMEleaves the retry rebuilding without a guard). It is documented in the migration and the guide rather than eliminated: the plan and squawk mandate theCONCURRENTLYpattern, and the table is empty pre-production. - The
Downis data-dependent once purged anchors re-onboard (the total-index rebuild collides); theDowncomment tells the operator to resolve duplicate-anchor rows first. A static test pins theDown's index shape, per the migration-test policy. - The helpers read a missing projection as active/serviceable.
FindByID/FindBySlugdo not select the five columns yet; hydration lands with gating enforcement in Step 3.
Spec coverage
Spec: docs/specs/S33-gitlab-api.md
Acceptance criteria
| # | Criterion | Tests |
|---|---|---|
| AC-1 | Provisioning 201 creates row, returns UUID/slug/status: active |
Step 5 (provisioning endpoint). Not tested in this MR. |
| AC-2 | Exact anchor replay returns 200, unaltered, no second row |
Step 5. The race-free anchor guard it relies on is pinned here by TestNamespacesConstraints_AnchorPartialUniquePurgedReonboard. |
| AC-3 | Mismatched replay returns 409, row unchanged |
Step 5. Not tested in this MR. |
| AC-4 | BLV enabled and unreachable: 503, no row |
Step 5. Not tested in this MR. |
| AC-5 | Syntactic/reserved/brand-list failure: 422, no row |
Steps 2 and 5. Not tested in this MR. |
| AC-6 | Slug taken by another namespace: 409 |
Step 5. The DB guard (total slug unique index, purged slugs retired) is pinned here by TestNamespacesSchema_SlugUniqueIndexStaysTotal, TestNamespacesConstraints_PurgedSlugStaysRetired. |
| AC-7 | Resolution returns anchor/slug/derived status; unknown UUID 404 |
Step 6. Not tested in this MR. |
| AC-8 | Condition endpoints touch exactly their column; derived status follows ADR-007 precedence | Step 7 (endpoints). The precedence derivation is covered here by TestNamespaceStatus, TestNamespaceState_StatusPredicateConsistency. |
| AC-9 | Suspended: download works, push rejected; blocked/disabled/deleted: both rejected | Step 3 (enforcement). The predicates are covered here by TestWriteServiceable, TestReadServiceable, TestNamespaceState_StatusPredicateConsistency. |
| AC-10 | Management-surface requests gated by the same predicate table | Step 3. Predicate coverage as AC-9. |
| AC-11 | Verifications: 204 all-in, opaque 400 otherwise |
Step 8. Not tested in this MR. |
| AC-12 | Resolution and conditions work in every state | Steps 6 and 7. Not tested in this MR. |
| AC-13 | OpenAPI document defines everything and validates in CI | Step 4 (contract rig). Not tested in this MR. |
Error cases
| # | Condition | Tests |
|---|---|---|
| E-1 | Missing/invalid service credential: 401 unauthorized |
Step 4. Not tested in this MR. |
| E-2 | Malformed JSON body: 400 bad_request |
Steps 4-8. Not tested in this MR. |
| E-3 | Slug fails validation: 422 unprocessable_entity |
Steps 2 and 5. Not tested in this MR. |
| E-4 | Slug taken: 409 conflict |
Step 5 (DB guard pinned here, see AC-6). |
| E-5 | Replay body disagrees: 409 conflict |
Step 5. Not tested in this MR. |
| E-6 | BLV enabled but unreachable: 503 service_unavailable |
Steps 4 and 5. Not tested in this MR. |
| E-7 | Unknown namespace UUID: 404 not_found |
Steps 6-8. Not tested in this MR. |
| E-8 | Unknown <action> segment: 404 not_found |
Step 7. Not tested in this MR. |
| E-9 | Verifications: unknown/foreign id, empty or oversized batch: 400 |
Step 8. Not tested in this MR. |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S-1 | Internet-facing surface behind the bootstrap-token stub | Step 4. Not tested in this MR. |
| S-2 | Opaque verifications response (no id enumeration) | Step 8. Not tested in this MR. |
| S-3 | No organization data leaves the registry | Steps 6 and 8. Not tested in this MR. |
| S-4 | Slug trust boundary: CHECK constraints, parameterized queries | Steps 2 and 5. The CHECK constraints pre-exist in the schema (S17); not re-tested in this MR. |
Step 1 plan acceptance
| Acceptance (plan Step 1) | Tests |
|---|---|
| Migration applies and reverts cleanly | TestMigrations_UpDownUp (existing, chain-generic per docs/dev/go-testing.md policy) |
| Five lifecycle columns: nullable timestamptz, no default | TestNamespacesSchema_LifecycleColumns |
Anchor index partial on purged_at IS NULL |
TestNamespacesSchema_AnchorUniqueIndexPartialOnPurged |
| Purged anchor re-insertable; soft-deleted anchor is not | TestNamespacesConstraints_AnchorPartialUniquePurgedReonboard |
| Slug index stays total; purged slug retired | TestNamespacesSchema_SlugUniqueIndexStaysTotal, TestNamespacesConstraints_PurgedSlugStaysRetired |
| Down restores the canonical total index | TestNamespacesSchema_DownRestoresTotalAnchorIndex (static SQL parse) |
| Status precedence order (ADR-007) | TestNamespaceStatus, TestNamespaceState_StatusPredicateConsistency |
| Both serviceability predicates per the gating table | TestWriteServiceable, TestReadServiceable, TestNamespaceState_StatusPredicateConsistency |
Database Review Evidence
Migrations
Note
Timings are from CI (db:migrate matrix, goose verbose) against an
empty database, in apply / rollback order per PG version.
Production-scale validation via Database Lab is not yet available. See
Database review evidence
for the matrix rationale and how to read the numbers.
| Migration | PG 16 | PG 17 | PG 18 |
|---|---|---|---|
20260717141847_add_namespaces_lifecycle_columns_and_partial_anchor_index.sql |
OK (11.16ms / 12.58ms) | OK (10.55ms / 13.23ms) | OK (12.19ms / 16.57ms) |
Related to #202 (closed)
Edited by João Pereira