Ensure UUID migrations are idempotent (#398)
This commit is contained in:
parent
4078d4b653
commit
438f18d7ec
68
account/sql/20251004-ensure-uuid-schema.down.sql
Normal file
68
account/sql/20251004-ensure-uuid-schema.down.sql
Normal file
@ -0,0 +1,68 @@
|
||||
-- Drop trigger before removing supporting function
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_trigger
|
||||
WHERE tgname = 'trg_users_set_updated_at'
|
||||
AND tgrelid = 'public.users'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER trg_users_set_updated_at ON public.users;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.set_updated_at();
|
||||
|
||||
-- Drop indexes introduced by the migration
|
||||
DROP INDEX IF EXISTS public.idx_sessions_user_uuid;
|
||||
DROP INDEX IF EXISTS public.idx_identities_provider;
|
||||
DROP INDEX IF EXISTS public.idx_identities_user_uuid;
|
||||
|
||||
-- Drop foreign keys
|
||||
ALTER TABLE public.sessions
|
||||
DROP CONSTRAINT IF EXISTS sessions_user_fk;
|
||||
|
||||
ALTER TABLE public.identities
|
||||
DROP CONSTRAINT IF EXISTS identities_user_fk;
|
||||
|
||||
-- Drop unique constraints
|
||||
ALTER TABLE public.identities
|
||||
DROP CONSTRAINT IF EXISTS identities_provider_external_id_uk;
|
||||
|
||||
ALTER TABLE public.users
|
||||
DROP CONSTRAINT IF EXISTS users_username_uk;
|
||||
|
||||
ALTER TABLE public.users
|
||||
DROP CONSTRAINT IF EXISTS users_email_uk;
|
||||
|
||||
-- Remove generated column but retain supporting timestamps
|
||||
ALTER TABLE public.users
|
||||
DROP COLUMN IF EXISTS email_verified;
|
||||
|
||||
-- Restore uuid columns to neutral defaults
|
||||
ALTER TABLE public.sessions
|
||||
ALTER COLUMN uuid DROP DEFAULT,
|
||||
ALTER COLUMN uuid DROP NOT NULL;
|
||||
|
||||
ALTER TABLE public.identities
|
||||
ALTER COLUMN uuid DROP DEFAULT,
|
||||
ALTER COLUMN uuid DROP NOT NULL,
|
||||
ALTER COLUMN user_uuid DROP NOT NULL;
|
||||
|
||||
ALTER TABLE public.users
|
||||
ALTER COLUMN uuid DROP DEFAULT,
|
||||
ALTER COLUMN uuid DROP NOT NULL;
|
||||
|
||||
ALTER TABLE public.sessions
|
||||
ALTER COLUMN user_uuid DROP NOT NULL;
|
||||
|
||||
-- Drop primary keys added by the migration
|
||||
ALTER TABLE public.sessions
|
||||
DROP CONSTRAINT IF EXISTS sessions_pkey;
|
||||
|
||||
ALTER TABLE public.identities
|
||||
DROP CONSTRAINT IF EXISTS identities_pkey;
|
||||
|
||||
ALTER TABLE public.users
|
||||
DROP CONSTRAINT IF EXISTS users_pkey;
|
||||
663
account/sql/20251004-ensure-uuid-schema.up.sql
Normal file
663
account/sql/20251004-ensure-uuid-schema.up.sql
Normal file
@ -0,0 +1,663 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Ensure uuid columns are of the UUID type
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
AND column_name = 'uuid'
|
||||
AND udt_name <> 'uuid'
|
||||
) THEN
|
||||
ALTER TABLE public.users
|
||||
ALTER COLUMN uuid TYPE uuid USING uuid::uuid;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'identities'
|
||||
AND column_name = 'uuid'
|
||||
AND udt_name <> 'uuid'
|
||||
) THEN
|
||||
ALTER TABLE public.identities
|
||||
ALTER COLUMN uuid TYPE uuid USING uuid::uuid;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'identities'
|
||||
AND column_name = 'user_uuid'
|
||||
AND udt_name <> 'uuid'
|
||||
) THEN
|
||||
ALTER TABLE public.identities
|
||||
ALTER COLUMN user_uuid TYPE uuid USING user_uuid::uuid;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'sessions'
|
||||
AND column_name = 'uuid'
|
||||
AND udt_name <> 'uuid'
|
||||
) THEN
|
||||
ALTER TABLE public.sessions
|
||||
ALTER COLUMN uuid TYPE uuid USING uuid::uuid;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'sessions'
|
||||
AND column_name = 'user_uuid'
|
||||
AND udt_name <> 'uuid'
|
||||
) THEN
|
||||
ALTER TABLE public.sessions
|
||||
ALTER COLUMN user_uuid TYPE uuid USING user_uuid::uuid;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Fill missing UUIDs before enforcing constraints
|
||||
UPDATE public.users SET uuid = gen_random_uuid() WHERE uuid IS NULL;
|
||||
UPDATE public.identities SET uuid = gen_random_uuid() WHERE uuid IS NULL;
|
||||
UPDATE public.sessions SET uuid = gen_random_uuid() WHERE uuid IS NULL;
|
||||
|
||||
-- Ensure NOT NULL on uuid columns
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
AND column_name = 'uuid'
|
||||
AND is_nullable = 'YES'
|
||||
) THEN
|
||||
ALTER TABLE public.users
|
||||
ALTER COLUMN uuid SET NOT NULL;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'identities'
|
||||
AND column_name = 'uuid'
|
||||
AND is_nullable = 'YES'
|
||||
) THEN
|
||||
ALTER TABLE public.identities
|
||||
ALTER COLUMN uuid SET NOT NULL;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'sessions'
|
||||
AND column_name = 'uuid'
|
||||
AND is_nullable = 'YES'
|
||||
) THEN
|
||||
ALTER TABLE public.sessions
|
||||
ALTER COLUMN uuid SET NOT NULL;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'identities'
|
||||
AND column_name = 'user_uuid'
|
||||
AND is_nullable = 'YES'
|
||||
) THEN
|
||||
ALTER TABLE public.identities
|
||||
ALTER COLUMN user_uuid SET NOT NULL;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'sessions'
|
||||
AND column_name = 'user_uuid'
|
||||
AND is_nullable = 'YES'
|
||||
) THEN
|
||||
ALTER TABLE public.sessions
|
||||
ALTER COLUMN user_uuid SET NOT NULL;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Ensure defaults for uuid columns
|
||||
DO $$
|
||||
DECLARE
|
||||
current_default text;
|
||||
target_attnum int;
|
||||
BEGIN
|
||||
SELECT attnum INTO target_attnum
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.users'::regclass
|
||||
AND attname = 'uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF target_attnum IS NOT NULL THEN
|
||||
SELECT pg_get_expr(adbin, adrelid)
|
||||
INTO current_default
|
||||
FROM pg_attrdef
|
||||
WHERE adrelid = 'public.users'::regclass
|
||||
AND adnum = target_attnum;
|
||||
|
||||
IF current_default IS DISTINCT FROM 'gen_random_uuid()' THEN
|
||||
ALTER TABLE public.users
|
||||
ALTER COLUMN uuid SET DEFAULT gen_random_uuid();
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
current_default text;
|
||||
target_attnum int;
|
||||
BEGIN
|
||||
SELECT attnum INTO target_attnum
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.identities'::regclass
|
||||
AND attname = 'uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF target_attnum IS NOT NULL THEN
|
||||
SELECT pg_get_expr(adbin, adrelid)
|
||||
INTO current_default
|
||||
FROM pg_attrdef
|
||||
WHERE adrelid = 'public.identities'::regclass
|
||||
AND adnum = target_attnum;
|
||||
|
||||
IF current_default IS DISTINCT FROM 'gen_random_uuid()' THEN
|
||||
ALTER TABLE public.identities
|
||||
ALTER COLUMN uuid SET DEFAULT gen_random_uuid();
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
current_default text;
|
||||
target_attnum int;
|
||||
BEGIN
|
||||
SELECT attnum INTO target_attnum
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.sessions'::regclass
|
||||
AND attname = 'uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF target_attnum IS NOT NULL THEN
|
||||
SELECT pg_get_expr(adbin, adrelid)
|
||||
INTO current_default
|
||||
FROM pg_attrdef
|
||||
WHERE adrelid = 'public.sessions'::regclass
|
||||
AND adnum = target_attnum;
|
||||
|
||||
IF current_default IS DISTINCT FROM 'gen_random_uuid()' THEN
|
||||
ALTER TABLE public.sessions
|
||||
ALTER COLUMN uuid SET DEFAULT gen_random_uuid();
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Ensure supporting columns on users table
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN IF NOT EXISTS email_verified_at timestamptz;
|
||||
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN IF NOT EXISTS updated_at timestamptz;
|
||||
|
||||
UPDATE public.users
|
||||
SET updated_at = now()
|
||||
WHERE updated_at IS NULL;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
current_default text;
|
||||
target_attnum int;
|
||||
BEGIN
|
||||
SELECT attnum INTO target_attnum
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.users'::regclass
|
||||
AND attname = 'updated_at'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF target_attnum IS NOT NULL THEN
|
||||
SELECT pg_get_expr(adbin, adrelid)
|
||||
INTO current_default
|
||||
FROM pg_attrdef
|
||||
WHERE adrelid = 'public.users'::regclass
|
||||
AND adnum = target_attnum;
|
||||
|
||||
IF current_default IS DISTINCT FROM 'now()' THEN
|
||||
ALTER TABLE public.users
|
||||
ALTER COLUMN updated_at SET DEFAULT now();
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Recreate email_verified as a generated column
|
||||
DO $$
|
||||
DECLARE
|
||||
att_generated char(1);
|
||||
BEGIN
|
||||
SELECT a.attgenerated
|
||||
INTO att_generated
|
||||
FROM pg_attribute a
|
||||
WHERE a.attrelid = 'public.users'::regclass
|
||||
AND a.attname = 'email_verified'
|
||||
AND NOT a.attisdropped;
|
||||
|
||||
IF att_generated IS NULL THEN
|
||||
EXECUTE 'ALTER TABLE public.users ADD COLUMN email_verified boolean GENERATED ALWAYS AS (email_verified_at IS NOT NULL) STORED';
|
||||
ELSIF att_generated <> 's' THEN
|
||||
EXECUTE 'ALTER TABLE public.users DROP COLUMN email_verified';
|
||||
EXECUTE 'ALTER TABLE public.users ADD COLUMN email_verified boolean GENERATED ALWAYS AS (email_verified_at IS NOT NULL) STORED';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Ensure updated_at trigger function
|
||||
CREATE OR REPLACE FUNCTION public.set_updated_at()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at := now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Ensure trigger exists
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_trigger
|
||||
WHERE tgname = 'trg_users_set_updated_at'
|
||||
AND tgrelid = 'public.users'::regclass
|
||||
AND NOT tgisinternal
|
||||
) THEN
|
||||
CREATE TRIGGER trg_users_set_updated_at
|
||||
BEFORE UPDATE ON public.users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_updated_at();
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Ensure primary keys
|
||||
DO $$
|
||||
DECLARE
|
||||
existing text;
|
||||
BEGIN
|
||||
SELECT conname INTO existing
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.users'::regclass
|
||||
AND contype = 'p'
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF existing IS NULL THEN
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_pkey PRIMARY KEY (uuid);
|
||||
ELSIF existing <> 'users_pkey' THEN
|
||||
EXECUTE format('ALTER TABLE public.users RENAME CONSTRAINT %I TO users_pkey', existing);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
existing text;
|
||||
BEGIN
|
||||
SELECT conname INTO existing
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.identities'::regclass
|
||||
AND contype = 'p'
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF existing IS NULL THEN
|
||||
ALTER TABLE public.identities
|
||||
ADD CONSTRAINT identities_pkey PRIMARY KEY (uuid);
|
||||
ELSIF existing <> 'identities_pkey' THEN
|
||||
EXECUTE format('ALTER TABLE public.identities RENAME CONSTRAINT %I TO identities_pkey', existing);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
existing text;
|
||||
BEGIN
|
||||
SELECT conname INTO existing
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.sessions'::regclass
|
||||
AND contype = 'p'
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF existing IS NULL THEN
|
||||
ALTER TABLE public.sessions
|
||||
ADD CONSTRAINT sessions_pkey PRIMARY KEY (uuid);
|
||||
ELSIF existing <> 'sessions_pkey' THEN
|
||||
EXECUTE format('ALTER TABLE public.sessions RENAME CONSTRAINT %I TO sessions_pkey', existing);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Ensure foreign keys on user_uuid columns
|
||||
DO $$
|
||||
DECLARE
|
||||
fk_name text;
|
||||
user_uuid_att smallint;
|
||||
users_uuid_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO user_uuid_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.identities'::regclass
|
||||
AND attname = 'user_uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
SELECT attnum INTO users_uuid_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.users'::regclass
|
||||
AND attname = 'uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF user_uuid_att IS NOT NULL AND users_uuid_att IS NOT NULL THEN
|
||||
SELECT conname INTO fk_name
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.identities'::regclass
|
||||
AND contype = 'f'
|
||||
AND conkey = ARRAY[user_uuid_att]
|
||||
AND confrelid = 'public.users'::regclass
|
||||
AND confkey = ARRAY[users_uuid_att]
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF fk_name IS NULL THEN
|
||||
ALTER TABLE public.identities
|
||||
ADD CONSTRAINT identities_user_fk FOREIGN KEY (user_uuid)
|
||||
REFERENCES public.users(uuid) ON DELETE CASCADE;
|
||||
ELSIF fk_name <> 'identities_user_fk' THEN
|
||||
EXECUTE format('ALTER TABLE public.identities RENAME CONSTRAINT %I TO identities_user_fk', fk_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
fk_name text;
|
||||
user_uuid_att smallint;
|
||||
users_uuid_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO user_uuid_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.sessions'::regclass
|
||||
AND attname = 'user_uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
SELECT attnum INTO users_uuid_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.users'::regclass
|
||||
AND attname = 'uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF user_uuid_att IS NOT NULL AND users_uuid_att IS NOT NULL THEN
|
||||
SELECT conname INTO fk_name
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.sessions'::regclass
|
||||
AND contype = 'f'
|
||||
AND conkey = ARRAY[user_uuid_att]
|
||||
AND confrelid = 'public.users'::regclass
|
||||
AND confkey = ARRAY[users_uuid_att]
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF fk_name IS NULL THEN
|
||||
ALTER TABLE public.sessions
|
||||
ADD CONSTRAINT sessions_user_fk FOREIGN KEY (user_uuid)
|
||||
REFERENCES public.users(uuid) ON DELETE CASCADE;
|
||||
ELSIF fk_name <> 'sessions_user_fk' THEN
|
||||
EXECUTE format('ALTER TABLE public.sessions RENAME CONSTRAINT %I TO sessions_user_fk', fk_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Ensure unique constraints
|
||||
DO $$
|
||||
DECLARE
|
||||
constraint_name text;
|
||||
email_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO email_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.users'::regclass
|
||||
AND attname = 'email'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF email_att IS NOT NULL THEN
|
||||
SELECT conname INTO constraint_name
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.users'::regclass
|
||||
AND contype = 'u'
|
||||
AND conkey = ARRAY[email_att]
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF constraint_name IS NULL THEN
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_email_uk UNIQUE (email);
|
||||
ELSIF constraint_name <> 'users_email_uk' THEN
|
||||
EXECUTE format('ALTER TABLE public.users RENAME CONSTRAINT %I TO users_email_uk', constraint_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
constraint_name text;
|
||||
username_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO username_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.users'::regclass
|
||||
AND attname = 'username'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF username_att IS NOT NULL THEN
|
||||
SELECT conname INTO constraint_name
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.users'::regclass
|
||||
AND contype = 'u'
|
||||
AND conkey = ARRAY[username_att]
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF constraint_name IS NULL THEN
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_username_uk UNIQUE (username);
|
||||
ELSIF constraint_name <> 'users_username_uk' THEN
|
||||
EXECUTE format('ALTER TABLE public.users RENAME CONSTRAINT %I TO users_username_uk', constraint_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
constraint_name text;
|
||||
provider_att smallint;
|
||||
external_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO provider_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.identities'::regclass
|
||||
AND attname = 'provider'
|
||||
AND NOT attisdropped;
|
||||
|
||||
SELECT attnum INTO external_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.identities'::regclass
|
||||
AND attname = 'external_id'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF provider_att IS NOT NULL AND external_att IS NOT NULL THEN
|
||||
SELECT conname INTO constraint_name
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'public.identities'::regclass
|
||||
AND contype = 'u'
|
||||
AND conkey = ARRAY[provider_att, external_att]
|
||||
ORDER BY conname
|
||||
LIMIT 1;
|
||||
|
||||
IF constraint_name IS NULL THEN
|
||||
ALTER TABLE public.identities
|
||||
ADD CONSTRAINT identities_provider_external_id_uk UNIQUE (provider, external_id);
|
||||
ELSIF constraint_name <> 'identities_provider_external_id_uk' THEN
|
||||
EXECUTE format('ALTER TABLE public.identities RENAME CONSTRAINT %I TO identities_provider_external_id_uk', constraint_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Ensure indexes
|
||||
DO $$
|
||||
DECLARE
|
||||
idx_name text;
|
||||
user_uuid_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO user_uuid_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.identities'::regclass
|
||||
AND attname = 'user_uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF user_uuid_att IS NOT NULL THEN
|
||||
SELECT cls.relname INTO idx_name
|
||||
FROM pg_index idx
|
||||
JOIN pg_class cls ON cls.oid = idx.indexrelid
|
||||
WHERE idx.indrelid = 'public.identities'::regclass
|
||||
AND idx.indisunique = FALSE
|
||||
AND idx.indkey = ARRAY[user_uuid_att]::int2vector
|
||||
LIMIT 1;
|
||||
|
||||
IF idx_name IS NULL THEN
|
||||
CREATE INDEX IF NOT EXISTS idx_identities_user_uuid ON public.identities (user_uuid);
|
||||
ELSIF idx_name <> 'idx_identities_user_uuid' THEN
|
||||
EXECUTE format('ALTER INDEX %I RENAME TO idx_identities_user_uuid', idx_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
idx_name text;
|
||||
provider_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO provider_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.identities'::regclass
|
||||
AND attname = 'provider'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF provider_att IS NOT NULL THEN
|
||||
SELECT cls.relname INTO idx_name
|
||||
FROM pg_index idx
|
||||
JOIN pg_class cls ON cls.oid = idx.indexrelid
|
||||
WHERE idx.indrelid = 'public.identities'::regclass
|
||||
AND idx.indisunique = FALSE
|
||||
AND idx.indkey = ARRAY[provider_att]::int2vector
|
||||
LIMIT 1;
|
||||
|
||||
IF idx_name IS NULL THEN
|
||||
CREATE INDEX IF NOT EXISTS idx_identities_provider ON public.identities (provider);
|
||||
ELSIF idx_name <> 'idx_identities_provider' THEN
|
||||
EXECUTE format('ALTER INDEX %I RENAME TO idx_identities_provider', idx_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
idx_name text;
|
||||
user_uuid_att smallint;
|
||||
BEGIN
|
||||
SELECT attnum INTO user_uuid_att
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'public.sessions'::regclass
|
||||
AND attname = 'user_uuid'
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF user_uuid_att IS NOT NULL THEN
|
||||
SELECT cls.relname INTO idx_name
|
||||
FROM pg_index idx
|
||||
JOIN pg_class cls ON cls.oid = idx.indexrelid
|
||||
WHERE idx.indrelid = 'public.sessions'::regclass
|
||||
AND idx.indisunique = FALSE
|
||||
AND idx.indkey = ARRAY[user_uuid_att]::int2vector
|
||||
LIMIT 1;
|
||||
|
||||
IF idx_name IS NULL THEN
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_uuid ON public.sessions (user_uuid);
|
||||
ELSIF idx_name <> 'idx_sessions_user_uuid' THEN
|
||||
EXECUTE format('ALTER INDEX %I RENAME TO idx_sessions_user_uuid', idx_name);
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
@ -1,35 +1,165 @@
|
||||
-- 启用扩展(只需执行一次)
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
-- 或者:CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL UNIQUE, -- 保留自增 id 作为内部用途
|
||||
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), -- 业务主键
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT,
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_totp_secret TEXT,
|
||||
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_secret_issued_at TIMESTAMPTZ,
|
||||
mfa_confirmed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
|
||||
|
||||
--
|
||||
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
|
||||
|
||||
--
|
||||
-- Name: set_updated_at(); Type: FUNCTION; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.set_updated_at() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at := now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
--
|
||||
-- Name: identities; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.identities (
|
||||
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_uuid uuid NOT NULL,
|
||||
provider text NOT NULL,
|
||||
external_id text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identities (
|
||||
id SERIAL UNIQUE,
|
||||
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES users(uuid) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL,
|
||||
external_id TEXT NOT NULL,
|
||||
UNIQUE(provider, external_id)
|
||||
--
|
||||
-- Name: sessions; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.sessions (
|
||||
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_uuid uuid NOT NULL,
|
||||
token text NOT NULL,
|
||||
expires_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id SERIAL UNIQUE,
|
||||
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES users(uuid) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
--
|
||||
-- Name: users; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.users (
|
||||
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
username text NOT NULL,
|
||||
password text NOT NULL,
|
||||
email text,
|
||||
email_verified_at timestamp with time zone,
|
||||
mfa_totp_secret text,
|
||||
mfa_enabled boolean DEFAULT false NOT NULL,
|
||||
mfa_secret_issued_at timestamp with time zone,
|
||||
mfa_confirmed_at timestamp with time zone,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone,
|
||||
email_verified boolean GENERATED ALWAYS AS (email_verified_at IS NOT NULL) STORED
|
||||
);
|
||||
|
||||
--
|
||||
-- Name: idx_identities_provider; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idx_identities_provider ON public.identities USING btree (provider);
|
||||
|
||||
--
|
||||
-- Name: idx_identities_user_uuid; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idx_identities_user_uuid ON public.identities USING btree (user_uuid);
|
||||
|
||||
--
|
||||
-- Name: idx_sessions_user_uuid; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idx_sessions_user_uuid ON public.sessions USING btree (user_uuid);
|
||||
|
||||
--
|
||||
-- Name: identities_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.identities
|
||||
ADD CONSTRAINT identities_pkey PRIMARY KEY (uuid);
|
||||
|
||||
--
|
||||
-- Name: identities_provider_external_id_uk; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.identities
|
||||
ADD CONSTRAINT identities_provider_external_id_uk UNIQUE (provider, external_id);
|
||||
|
||||
--
|
||||
-- Name: sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.sessions
|
||||
ADD CONSTRAINT sessions_pkey PRIMARY KEY (uuid);
|
||||
|
||||
--
|
||||
-- Name: users_email_uk; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_email_uk UNIQUE (email);
|
||||
|
||||
--
|
||||
-- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_pkey PRIMARY KEY (uuid);
|
||||
|
||||
--
|
||||
-- Name: users_username_uk; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_username_uk UNIQUE (username);
|
||||
|
||||
--
|
||||
-- Name: identities_user_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.identities
|
||||
ADD CONSTRAINT identities_user_fk FOREIGN KEY (user_uuid) REFERENCES public.users(uuid) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Name: sessions_user_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.sessions
|
||||
ADD CONSTRAINT sessions_user_fk FOREIGN KEY (user_uuid) REFERENCES public.users(uuid) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Name: trg_users_set_updated_at; Type: TRIGGER; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_users_set_updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user