"""add users table and user_id columns Revision ID: 0008 Revises: 0007 Create Date: 2026-02-11 00:00:00.000000 """ from alembic import op revision = "0008" down_revision = "0007" branch_labels = None depends_on = None def upgrade() -> None: # Create users table op.execute(""" CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username TEXT UNIQUE NOT NULL, email TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', created_at TIMESTAMPTZ DEFAULT now() ) """) op.execute("CREATE INDEX IF NOT EXISTS ix_users_username ON users (username)") # Add user_id columns WITHOUT FK constraints first (so we can assign existing rows) op.execute(""" DO $$ BEGIN ALTER TABLE notes ADD COLUMN user_id INTEGER; EXCEPTION WHEN duplicate_column THEN NULL; END $$ """) op.execute(""" DO $$ BEGIN ALTER TABLE conversations ADD COLUMN user_id INTEGER; EXCEPTION WHEN duplicate_column THEN NULL; END $$ """) op.execute(""" DO $$ BEGIN ALTER TABLE settings ADD COLUMN user_id INTEGER NOT NULL DEFAULT 1; EXCEPTION WHEN duplicate_column THEN NULL; END $$ """) # Assign all existing data to user_id=1 (the first registered user gets SERIAL id=1) op.execute("UPDATE notes SET user_id = 1 WHERE user_id IS NULL") op.execute("UPDATE conversations SET user_id = 1 WHERE user_id IS NULL") op.execute("UPDATE settings SET user_id = 1 WHERE user_id != 1") # Restructure settings PK: drop old single-column PK, create composite (user_id, key) op.execute(""" DO $$ BEGIN ALTER TABLE settings DROP CONSTRAINT settings_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$ """) op.execute(""" DO $$ BEGIN ALTER TABLE settings ADD CONSTRAINT settings_pkey PRIMARY KEY (user_id, key); EXCEPTION WHEN duplicate_object THEN NULL; END $$ """) # Now add FK constraints with NOT VALID (skips validation of existing rows # where user_id=1 has no matching user yet — becomes valid once first user registers) op.execute(""" DO $$ BEGIN ALTER TABLE notes ADD CONSTRAINT notes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE NOT VALID; EXCEPTION WHEN duplicate_object THEN NULL; END $$ """) op.execute(""" DO $$ BEGIN ALTER TABLE conversations ADD CONSTRAINT conversations_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE NOT VALID; EXCEPTION WHEN duplicate_object THEN NULL; END $$ """) op.execute(""" DO $$ BEGIN ALTER TABLE settings ADD CONSTRAINT settings_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE NOT VALID; EXCEPTION WHEN duplicate_object THEN NULL; END $$ """) op.execute("CREATE INDEX IF NOT EXISTS ix_notes_user_id ON notes (user_id)") op.execute("CREATE INDEX IF NOT EXISTS ix_conversations_user_id ON conversations (user_id)") def downgrade() -> None: # Reverse settings changes op.execute(""" DO $$ BEGIN ALTER TABLE settings DROP CONSTRAINT settings_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$ """) op.execute("ALTER TABLE settings ALTER COLUMN user_id DROP NOT NULL") op.execute(""" DO $$ BEGIN ALTER TABLE settings ADD CONSTRAINT settings_pkey PRIMARY KEY (key); EXCEPTION WHEN duplicate_object THEN NULL; END $$ """) op.execute("ALTER TABLE settings DROP COLUMN IF EXISTS user_id") op.execute("DROP INDEX IF EXISTS ix_conversations_user_id") op.execute("ALTER TABLE conversations DROP COLUMN IF EXISTS user_id") op.execute("DROP INDEX IF EXISTS ix_notes_user_id") op.execute("ALTER TABLE notes DROP COLUMN IF EXISTS user_id") op.execute("DROP INDEX IF EXISTS ix_users_username") op.execute("DROP TABLE IF EXISTS users")