MySQL · 4024 bytes Raw Blame History
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