| 1 | -- SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | -- |
| 3 | -- S16 repo lifecycle. |
| 4 | -- |
| 5 | -- Two tables, both auxiliary to repos: |
| 6 | -- |
| 7 | -- repo_redirects — one row per (old_owner, old_name) → repo_id |
| 8 | -- so /old/repo 301s to the current path. |
| 9 | -- Written on rename and on transfer-accept. |
| 10 | -- A single repo accumulates redirect rows |
| 11 | -- over its lifetime; the current name is |
| 12 | -- always on `repos.name` itself. |
| 13 | -- |
| 14 | -- repo_transfer_requests — pending transfer offers. The recipient |
| 15 | -- accepts or declines; expiration is 7 |
| 16 | -- days; the sender can cancel. |
| 17 | |
| 18 | -- +goose Up |
| 19 | |
| 20 | CREATE TABLE repo_redirects ( |
| 21 | old_owner_user_id bigint, |
| 22 | old_owner_org_id bigint, |
| 23 | old_name citext NOT NULL, |
| 24 | repo_id bigint NOT NULL REFERENCES repos(id) ON DELETE CASCADE, |
| 25 | redirected_at timestamptz NOT NULL DEFAULT now(), |
| 26 | |
| 27 | -- Exactly one old-owner FK is set, mirroring repos.owner_xor. |
| 28 | CONSTRAINT repo_redirects_old_owner_xor CHECK ( |
| 29 | (old_owner_user_id IS NOT NULL AND old_owner_org_id IS NULL) |
| 30 | OR (old_owner_user_id IS NULL AND old_owner_org_id IS NOT NULL) |
| 31 | ), |
| 32 | CONSTRAINT repo_redirects_old_name_length CHECK (char_length(old_name::text) BETWEEN 1 AND 100) |
| 33 | ); |
| 34 | |
| 35 | -- Lookup index for /{old_owner}/{old_name} → repo_id. Two partial |
| 36 | -- indexes mirror the unique index pattern on `repos`. |
| 37 | CREATE UNIQUE INDEX repo_redirects_user_old_idx |
| 38 | ON repo_redirects (old_owner_user_id, old_name) |
| 39 | WHERE old_owner_user_id IS NOT NULL; |
| 40 | |
| 41 | CREATE UNIQUE INDEX repo_redirects_org_old_idx |
| 42 | ON repo_redirects (old_owner_org_id, old_name) |
| 43 | WHERE old_owner_org_id IS NOT NULL; |
| 44 | |
| 45 | |
| 46 | CREATE TYPE transfer_principal_kind AS ENUM ('user', 'org'); |
| 47 | CREATE TYPE transfer_status AS ENUM ('pending', 'accepted', 'declined', 'canceled', 'expired'); |
| 48 | |
| 49 | CREATE TABLE repo_transfer_requests ( |
| 50 | id bigserial PRIMARY KEY, |
| 51 | repo_id bigint NOT NULL REFERENCES repos(id) ON DELETE CASCADE, |
| 52 | from_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| 53 | to_principal_kind transfer_principal_kind NOT NULL, |
| 54 | to_principal_id bigint NOT NULL, |
| 55 | created_by bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| 56 | created_at timestamptz NOT NULL DEFAULT now(), |
| 57 | expires_at timestamptz NOT NULL, |
| 58 | status transfer_status NOT NULL DEFAULT 'pending', |
| 59 | accepted_at timestamptz, |
| 60 | declined_at timestamptz, |
| 61 | canceled_at timestamptz, |
| 62 | |
| 63 | -- Status invariants. We don't validate the (status, *_at) cross- |
| 64 | -- invariant here — the lifecycle orchestrator owns those updates |
| 65 | -- in transactions and is the single writer. |
| 66 | CONSTRAINT repo_transfer_requests_status_terminal CHECK ( |
| 67 | (status = 'pending' AND accepted_at IS NULL AND declined_at IS NULL AND canceled_at IS NULL) OR |
| 68 | (status = 'accepted' AND accepted_at IS NOT NULL) OR |
| 69 | (status = 'declined' AND declined_at IS NOT NULL) OR |
| 70 | (status = 'canceled' AND canceled_at IS NOT NULL) OR |
| 71 | (status = 'expired') |
| 72 | ) |
| 73 | ); |
| 74 | |
| 75 | -- Recipient inbox lookup: "show me pending transfers offered to user X." |
| 76 | CREATE INDEX repo_transfer_requests_recipient_idx |
| 77 | ON repo_transfer_requests (to_principal_kind, to_principal_id, status) |
| 78 | WHERE status = 'pending'; |
| 79 | |
| 80 | -- Sender lookup + history. |
| 81 | CREATE INDEX repo_transfer_requests_repo_idx |
| 82 | ON repo_transfer_requests (repo_id, created_at DESC); |
| 83 | |
| 84 | -- Expiry sweep (the worker that flips pending → expired runs over this). |
| 85 | CREATE INDEX repo_transfer_requests_expiry_idx |
| 86 | ON repo_transfer_requests (expires_at) |
| 87 | WHERE status = 'pending'; |
| 88 | |
| 89 | -- +goose Down |
| 90 | DROP TABLE IF EXISTS repo_transfer_requests; |
| 91 | DROP TYPE IF EXISTS transfer_status; |
| 92 | DROP TYPE IF EXISTS transfer_principal_kind; |
| 93 | DROP TABLE IF EXISTS repo_redirects; |
| 94 |