From a1d53f8e47e92b6556339fd3c401bc98bf8cc970 Mon Sep 17 00:00:00 2001 From: Stockboy Steve Date: Tue, 31 Mar 2026 17:56:13 +0000 Subject: [PATCH] fix: change users.id and FK columns from uuid to text for Better-Auth compatibility Better-Auth generates nanoid-style text IDs (e.g. pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI), but the users table used PostgreSQL uuid type, causing registration failures: ERROR: invalid input syntax for type uuid: "pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI" Changes: - User.id: removed UUIDPrimaryKeyMixin, use explicit text PK - UserStoreAccount.user_id: Mapped[uuid.UUID] -> Mapped[str] - Purchase.user_id: Mapped[uuid.UUID] -> Mapped[str] - UserResponse schema: id field from UUID -> str - New Alembic migration 004_fix_user_id_text: drops FKs, alters column types, re-adds FKs (using id::text cast) Co-Authored-By: Paperclip --- alembic/versions/004_fix_user_id_text.py | 122 +++++++++++++++++++++++ src/cartsnitch_api/models/purchase.py | 2 +- src/cartsnitch_api/models/user.py | 7 +- src/cartsnitch_api/schemas.py | 2 +- 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 alembic/versions/004_fix_user_id_text.py diff --git a/alembic/versions/004_fix_user_id_text.py b/alembic/versions/004_fix_user_id_text.py new file mode 100644 index 0000000..a52bf9d --- /dev/null +++ b/alembic/versions/004_fix_user_id_text.py @@ -0,0 +1,122 @@ +"""Fix users.id UUID->text type mismatch for Better-Auth compatibility. + +Better-Auth generates nanoid-style text IDs (e.g. pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI), +but the users table was using PostgreSQL uuid type. When Better-Auth tries to INSERT +a new user, Postgres throws: + ERROR: invalid input syntax for type uuid: "pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI" + +The sessions, accounts, and verifications tables already use text IDs — only users, +user_store_accounts.user_id, and purchases.user_id needed fixing. + +Revision ID: 004_fix_user_id_text +Revises: 003_make_users_hashed_password_nullable +Create Date: 2026-03-31 +""" + +import sqlalchemy as sa +from sqlalchemy import text + +from alembic import op + +revision = "004_fix_user_id_text" +down_revision = "003_make_users_hashed_password_nullable" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Step 1: Drop existing FK constraints + op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey")) + op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")) + + # Step 2: Alter users.id from uuid to text + op.alter_column( + "users", + "id", + type_=sa.Text(), + existing_type=sa.UUID(), + postgresql_using="id::text", + ) + + # Step 3: Alter user_store_accounts.user_id from uuid to text + op.alter_column( + "user_store_accounts", + "user_id", + type_=sa.Text(), + existing_type=sa.UUID(), + postgresql_using="user_id::text", + ) + + # Step 4: Alter purchases.user_id from uuid to text + op.alter_column( + "purchases", + "user_id", + type_=sa.Text(), + existing_type=sa.UUID(), + postgresql_using="user_id::text", + ) + + # Step 5: Re-add FK constraints + op.execute( + text( + "ALTER TABLE user_store_accounts " + "ADD CONSTRAINT user_store_accounts_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) + op.execute( + text( + "ALTER TABLE purchases " + "ADD CONSTRAINT purchases_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) + + +def downgrade() -> None: + # Drop FK constraints + op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey")) + op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")) + + # Revert users.id from text to uuid + op.alter_column( + "users", + "id", + type_=sa.UUID(), + existing_type=sa.Text(), + postgresql_using="id::uuid", + ) + + # Revert user_store_accounts.user_id from text to uuid + op.alter_column( + "user_store_accounts", + "user_id", + type_=sa.UUID(), + existing_type=sa.Text(), + postgresql_using="user_id::uuid", + ) + + # Revert purchases.user_id from text to uuid + op.alter_column( + "purchases", + "user_id", + type_=sa.UUID(), + existing_type=sa.Text(), + postgresql_using="user_id::uuid", + ) + + # Re-add FK constraints (PostgreSQL will auto-name them) + op.execute( + text( + "ALTER TABLE user_store_accounts " + "ADD CONSTRAINT user_store_accounts_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) + op.execute( + text( + "ALTER TABLE purchases " + "ADD CONSTRAINT purchases_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) diff --git a/src/cartsnitch_api/models/purchase.py b/src/cartsnitch_api/models/purchase.py index f57fde9..5a56cba 100644 --- a/src/cartsnitch_api/models/purchase.py +++ b/src/cartsnitch_api/models/purchase.py @@ -32,7 +32,7 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "purchases" - user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False) + user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False) store_location_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("store_locations.id")) receipt_id: Mapped[str] = mapped_column(String(200), nullable=False) diff --git a/src/cartsnitch_api/models/user.py b/src/cartsnitch_api/models/user.py index 56482b0..2c87644 100644 --- a/src/cartsnitch_api/models/user.py +++ b/src/cartsnitch_api/models/user.py @@ -4,7 +4,7 @@ import uuid from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint +from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from cartsnitch_api.constants import AccountStatus @@ -16,11 +16,12 @@ if TYPE_CHECKING: from cartsnitch_api.models.store import Store -class User(UUIDPrimaryKeyMixin, TimestampMixin, Base): +class User(TimestampMixin, Base): """Application user.""" __tablename__ = "users" + 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) display_name: Mapped[str | None] = mapped_column(String(100)) @@ -36,7 +37,7 @@ class UserStoreAccount(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "user_store_accounts" __table_args__ = (UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),) - user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False) + user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False) session_data: Mapped[dict | None] = mapped_column(EncryptedJSON) session_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) diff --git a/src/cartsnitch_api/schemas.py b/src/cartsnitch_api/schemas.py index 1ba727e..6547963 100644 --- a/src/cartsnitch_api/schemas.py +++ b/src/cartsnitch_api/schemas.py @@ -16,7 +16,7 @@ class UpdateUserRequest(BaseModel): class UserResponse(BaseModel): - id: UUID + id: str email: str display_name: str created_at: datetime