fix(api): make alembic migrations idempotent for fresh databases

- 001: guard has_table check; skip if session_data already TEXT
- 002: guard each ADD COLUMN / CREATE TABLE; guard password migration
- 003: guard has_table; guard nullable check
- 004: guard has_table; skip if users.id already TEXT
- env.py: add Base.metadata.create_all after run_migrations to bootstrap fresh DBs
- api/user.py: make hashed_password nullable; add email_verified, image, email_inbound_token fields

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-04 16:18:32 +00:00
parent f761b08aaf
commit 5c8fe9a62b
7 changed files with 147 additions and 73 deletions
+10 -2
View File
@@ -4,7 +4,8 @@ import secrets
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint
import sqlalchemy as sa
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from cartsnitch_api.constants import AccountStatus
@@ -23,13 +24,20 @@ class User(TimestampMixin, Base):
id: Mapped[str] = mapped_column(Text, primary_key=True)
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
display_name: Mapped[str | None] = mapped_column(String(100))
email_verified: Mapped[bool] = mapped_column(
Boolean, nullable=False, server_default="false"
)
image: Mapped[str | None] = mapped_column(Text, nullable=True)
email_inbound_token: Mapped[str] = mapped_column(
String(22),
nullable=False,
unique=True,
default=lambda: secrets.token_urlsafe(16),
server_default=sa.text(
"replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
),
)
# Relationships