Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions scripts/ops/users_github_id_unique_index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
-- Required pre-deploy step: build the GitHub identity index without blocking
-- writes. Run this file with psql, which executes each statement in autocommit
-- mode and supports the conditional recovery command below. In SQL Editor,
-- check for and drop an invalid same-named index first, then run the CREATE
-- INDEX statement and validation block separately. CREATE INDEX CONCURRENTLY
-- cannot run inside a transaction.
--
-- Example (psql):
-- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \
-- -f scripts/ops/users_github_id_unique_index.sql

-- Keep recovery, creation, and validation in one serialized session. An
-- invalid index can also be a concurrent build that another deploy is still
-- running, so a second invocation must wait instead of dropping it.
SELECT pg_catalog.pg_advisory_lock(
pg_catalog.hashtextextended('public.users_github_id_key', 0)
);

-- A failed concurrent build leaves an invalid same-named index. Generate a
-- concurrent drop only for that recoverable state; valid indexes remain intact.
SELECT format(
'DROP INDEX CONCURRENTLY %I.%I',
index_namespace.nspname,
idx.relname
)
FROM pg_catalog.pg_class AS idx
INNER JOIN pg_catalog.pg_namespace AS index_namespace
ON idx.relnamespace = index_namespace.oid
INNER JOIN pg_catalog.pg_index AS index_meta
ON idx.oid = index_meta.indexrelid
WHERE index_namespace.nspname = 'public'
AND idx.relname = 'users_github_id_key'
AND NOT index_meta.indisvalid
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
\gexec
Comment thread
coderabbitai[bot] marked this conversation as resolved.

CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS users_github_id_key
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
ON public.users (github_id);
Comment thread
WcaleNieWolny marked this conversation as resolved.

DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_catalog.pg_class AS idx
INNER JOIN pg_catalog.pg_namespace AS idx_ns
ON idx.relnamespace = idx_ns.oid
INNER JOIN pg_catalog.pg_index AS index_meta
ON idx.oid = index_meta.indexrelid
INNER JOIN pg_catalog.pg_class AS indexed_table
ON index_meta.indrelid = indexed_table.oid
INNER JOIN pg_catalog.pg_namespace AS table_ns
ON indexed_table.relnamespace = table_ns.oid
INNER JOIN pg_catalog.pg_attribute AS indexed_column
ON indexed_table.oid = indexed_column.attrelid
AND indexed_column.attname = 'github_id'
AND NOT indexed_column.attisdropped
INNER JOIN pg_catalog.pg_am AS access_method
ON idx.relam = access_method.oid
WHERE idx_ns.nspname = 'public'
AND idx.relname = 'users_github_id_key'
AND table_ns.nspname = 'public'
AND indexed_table.relname = 'users'
AND index_meta.indisvalid
AND index_meta.indisready
AND index_meta.indisunique
AND index_meta.indislive
AND NOT index_meta.indnullsnotdistinct
AND index_meta.indpred IS NULL
AND index_meta.indexprs IS NULL
AND index_meta.indnkeyatts = 1
AND index_meta.indnatts = 1
AND index_meta.indkey[0] = indexed_column.attnum
AND access_method.amname = 'btree'
) THEN
RAISE EXCEPTION '%',
'Index public.users_github_id_key is not a valid, ready, unique '
|| 'btree index on public.users(github_id) with NULLS DISTINCT. '
|| 'Drop it and rerun this script.';
END IF;
END
$$;

SELECT pg_catalog.pg_advisory_unlock(
pg_catalog.hashtextextended('public.users_github_id_key', 0)
);
100 changes: 100 additions & 0 deletions supabase/migrations/20260802171300_enforce_unique_github_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
-- Wait for the concurrent prebuild session to finish before inspecting its
-- index. The transaction lock is released automatically with this migration.
SELECT pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended('public.users_github_id_key', 0)
);

DO $$
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
DECLARE
duplicate_github_id bigint;
duplicate_count bigint;
BEGIN
SELECT github_id, COUNT(*)
INTO duplicate_github_id, duplicate_count
FROM public.users
WHERE github_id IS NOT NULL
GROUP BY github_id
HAVING COUNT(*) > 1
ORDER BY github_id
LIMIT 1;

IF duplicate_github_id IS NOT NULL THEN
RAISE EXCEPTION
'Cannot enforce unique GitHub account links: '
'github_id % is linked to % users',
duplicate_github_id,
duplicate_count;
END IF;
END
$$;

-- Production operators must prebuild this index concurrently with
-- scripts/ops/users_github_id_unique_index.sql before applying the migration.
-- Refuse the blocking fallback on every populated database. Fresh local/test
-- databases are empty here, so they remain self-contained without allowing a
-- missed production pre-deploy step to silently lock writes.
DO $$
BEGIN
IF pg_catalog.to_regclass('public.users_github_id_key') IS NULL
AND EXISTS (SELECT 1 FROM public.users LIMIT 1)
THEN
RAISE EXCEPTION '%',
'Prebuild public.users_github_id_key concurrently with '
|| 'scripts/ops/users_github_id_unique_index.sql before applying '
|| 'this migration to a populated database.';
END IF;
END
$$;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

CREATE UNIQUE INDEX IF NOT EXISTS users_github_id_key
ON public.users (github_id);

-- Keep this index-shape validation synchronized with
-- scripts/ops/users_github_id_unique_index.sql.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_catalog.pg_class AS idx
INNER JOIN pg_catalog.pg_namespace AS idx_ns
ON idx.relnamespace = idx_ns.oid
INNER JOIN pg_catalog.pg_index AS index_meta
ON idx.oid = index_meta.indexrelid
INNER JOIN pg_catalog.pg_class AS indexed_table
ON index_meta.indrelid = indexed_table.oid
INNER JOIN pg_catalog.pg_namespace AS table_ns
ON indexed_table.relnamespace = table_ns.oid
INNER JOIN pg_catalog.pg_attribute AS indexed_column
ON indexed_table.oid = indexed_column.attrelid
AND indexed_column.attname = 'github_id'
AND NOT indexed_column.attisdropped
INNER JOIN pg_catalog.pg_am AS access_method
ON idx.relam = access_method.oid
WHERE idx_ns.nspname = 'public'
AND idx.relname = 'users_github_id_key'
AND table_ns.nspname = 'public'
AND indexed_table.relname = 'users'
AND index_meta.indisvalid
AND index_meta.indisready
AND index_meta.indisunique
AND index_meta.indislive
AND NOT index_meta.indnullsnotdistinct
AND index_meta.indpred IS NULL
AND index_meta.indexprs IS NULL
AND index_meta.indnkeyatts = 1
AND index_meta.indnatts = 1
AND index_meta.indkey[0] = indexed_column.attnum
AND access_method.amname = 'btree'
) THEN
RAISE EXCEPTION '%',
'Index public.users_github_id_key is not a valid, ready, unique '
|| 'btree index on public.users(github_id) with NULLS DISTINCT. '
|| 'Drop it and rerun scripts/ops/users_github_id_unique_index.sql '
|| 'before applying this migration.';
END IF;
END
$$;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

ALTER TABLE public.users
ADD CONSTRAINT users_github_id_key
UNIQUE USING INDEX users_github_id_key;
122 changes: 121 additions & 1 deletion supabase/tests/65_test_github_user_id.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
BEGIN;

SELECT plan(3);
SELECT plan(9);

SELECT tests.create_supabase_user(
'github_id_unique_first',
'github_id_unique_first@test.local'
);
SELECT tests.create_supabase_user(
'github_id_unique_second',
'github_id_unique_second@test.local'
);

INSERT INTO public.users (id, email, created_at, updated_at)
VALUES
(
tests.get_supabase_uid('github_id_unique_first'),
'github_id_unique_first@test.local',
NOW(),
NOW()
),
(
tests.get_supabase_uid('github_id_unique_second'),
'github_id_unique_second@test.local',
NOW(),
NOW()
)
ON CONFLICT (id) DO NOTHING;

SELECT ok(
EXISTS (
Expand Down Expand Up @@ -37,6 +62,101 @@ SELECT is(
'users.github_id is nullable'
);

SELECT is(
(
SELECT contype::text
FROM pg_constraint
WHERE conrelid = 'public.users'::regclass
AND conname = 'users_github_id_key'
),
'u',
'users.github_id has the named unique constraint'
);

SELECT ok(
EXISTS (
SELECT 1
FROM pg_catalog.pg_constraint AS unique_constraint
INNER JOIN pg_catalog.pg_index AS index_meta
ON unique_constraint.conindid = index_meta.indexrelid
INNER JOIN pg_catalog.pg_attribute AS indexed_column
ON unique_constraint.conrelid = indexed_column.attrelid
AND indexed_column.attname = 'github_id'
AND NOT indexed_column.attisdropped
INNER JOIN pg_catalog.pg_class AS idx
ON index_meta.indexrelid = idx.oid
INNER JOIN pg_catalog.pg_am AS access_method
ON idx.relam = access_method.oid
WHERE unique_constraint.conrelid = 'public.users'::regclass
AND unique_constraint.conname = 'users_github_id_key'
AND index_meta.indisvalid
AND index_meta.indisready
AND index_meta.indisunique
AND index_meta.indislive
AND NOT index_meta.indnullsnotdistinct
AND index_meta.indpred IS NULL
AND index_meta.indexprs IS NULL
AND index_meta.indnkeyatts = 1
AND index_meta.indnatts = 1
AND index_meta.indkey[0] = indexed_column.attnum
AND access_method.amname = 'btree'
),
'the constraint uses the validated GitHub ID index'
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

UPDATE public.users
SET github_id = 123456789
WHERE id = tests.get_supabase_uid('github_id_unique_first');

SELECT throws_ok(
$$
UPDATE public.users
SET github_id = 123456789
WHERE id = tests.get_supabase_uid('github_id_unique_second');
$$,
'23505',
'duplicate key value violates unique constraint "users_github_id_key"',
'different users cannot save the same non-null GitHub ID'
);

SELECT lives_ok(
$$
UPDATE public.users
SET github_id = NULL
WHERE id IN (
tests.get_supabase_uid('github_id_unique_first'),
tests.get_supabase_uid('github_id_unique_second')
);
$$,
'multiple users may have a null GitHub ID'
);

UPDATE public.users
SET github_id = 123456789
WHERE id = tests.get_supabase_uid('github_id_unique_first');

SELECT lives_ok(
$$
UPDATE public.users
SET github_id = 123456789
WHERE id = tests.get_supabase_uid('github_id_unique_first');
$$,
'a user may re-save their own GitHub ID'
);

SELECT lives_ok(
$$
UPDATE public.users
SET github_id = NULL
WHERE id = tests.get_supabase_uid('github_id_unique_first');

UPDATE public.users
SET github_id = 123456789
WHERE id = tests.get_supabase_uid('github_id_unique_second');
$$,
'clearing a GitHub link lets another user claim that ID'
);

SELECT * FROM finish(); -- noqa: AM04

ROLLBACK;
Loading