"""Email/password columns on users + webUI auth (Stage 4). Revision ID: 0003 Revises: 0002 Create Date: 2026-08-12 Adds nullable email/password_hash/reset columns to the existing Telegram-only users table. Existing Telegram rows remain valid (telegram_id still satisfies the new users_identity_present check). Email is unique; both telegram_id and email may co-exist on one row (linking the two identities later is a no-op). """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "0003" down_revision: str | None = "0002" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.add_column("users", sa.Column("email", sa.Text(), nullable=True)) op.add_column("users", sa.Column("password_hash", sa.Text(), nullable=True)) op.add_column("users", sa.Column("password_reset_token_hash", sa.Text(), nullable=True)) op.add_column( "users", sa.Column("password_reset_expires_at", sa.DateTime(timezone=True), nullable=True) ) op.add_column( "users", sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")), ) op.create_unique_constraint("users_email_unique", "users", ["email"]) # A user must have at least one identity anchor (telegram_id OR email). op.create_check_constraint( "users_identity_present", "users", "telegram_id IS NOT NULL OR email IS NOT NULL", ) def downgrade() -> None: op.drop_constraint("users_identity_present", "users", type_="check") op.drop_constraint("users_email_unique", "users", type_="unique") op.drop_column("users", "is_active") op.drop_column("users", "password_reset_expires_at") op.drop_column("users", "password_reset_token_hash") op.drop_column("users", "password_hash") op.drop_column("users", "email")