Ensure UUID migrations are idempotent (#398)

This commit is contained in:
shenlan 2025-10-04 22:59:56 +08:00 committed by GitHub
parent 4078d4b653
commit 438f18d7ec
3 changed files with 890 additions and 29 deletions

View 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;

View 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
$$;

View File

@ -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();