You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The FastAPI project currently lacks a robust database migration management system. Without proper migration tooling, schema changes are difficult to track, version control, and deploy consistently across environments. This creates risks during database evolution and makes collaboration challenging when multiple developers work on schema changes.
Additional Challenge: The project uses async SQLAlchemy with aiosqlite, which requires special consideration when setting up Alembic to ensure compatibility with the async database engine and session management.
Project Architecture Note: The project has an unconventional but functional structure:
models/player_model.py → Pydantic models for API validation
This is opposite to the typical FastAPI convention where "schemas" are Pydantic and "models" are SQLAlchemy, but Alembic will work with the existing structure.
Required by: #542 (Add PostgreSQL support with unified migration-based initialization)
Proposed Solution
Implement Alembic as the database migration tool for the FastAPI project. Alembic will provide:
Automatic migration generation from SQLAlchemy model changes
Version-controlled schema changes that can be tracked in Git
Add alembic and asyncpg to pyproject.toml dependencies
Initialize: alembic init alembic
Configure alembic/env.py (see below)
3. Primary key design: UUID, not auto-increment integer
The Player schema uses a custom HyphenatedUUID type as the primary key (stored as String(36), hyphenated UUID format):
# schemas/player_schema.pyid=Column(
HyphenatedUUID(), # String(36) under the hoodprimary_key=True,
default=uuid4,
nullable=False,
)
Alembic autogenerate will detect HyphenatedUUID as String(36) and generate the column correctly. No manual column type override needed — but the generated migration should be reviewed to confirm the String(36) mapping.
Seed data in migrations must use deterministic UUID v5 values (stable across environments), not random UUID v4. This is consistent with the docstring in schemas/player_schema.py:
Records seeded by migration scripts use deterministic UUID v5 values so that IDs are stable across environments and can be safely referenced in tests.
4. Configure alembic/env.py
Read DATABASE_URL from the environment, supporting both SQLite and PostgreSQL:
importasyncioimportosfromlogging.configimportfileConfigfromsqlalchemyimportpoolfromsqlalchemy.engineimportConnectionfromsqlalchemy.ext.asyncioimportasync_engine_from_configfromalembicimportcontextfromdatabases.player_databaseimportBasefromschemas.player_schemaimportPlayer# Supports both SQLite (local) and PostgreSQL (Docker, see #542):# sqlite+aiosqlite:///players-sqlite3.db# postgresql+asyncpg://postgres:password@postgres:5432/playersdbdatabase_url=os.getenv("DATABASE_URL", "sqlite+aiosqlite:///players-sqlite3.db")
config=context.configconfig.set_main_option("sqlalchemy.url", database_url)
ifconfig.config_file_nameisnotNone:
fileConfig(config.config_file_name)
target_metadata=Base.metadatadefrun_migrations_offline() ->None:
url=config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True, # Required for SQLite ALTER TABLE support
)
withcontext.begin_transaction():
context.run_migrations()
defdo_run_migrations(connection: Connection) ->None:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True, # Required for SQLite ALTER TABLE support
)
withcontext.begin_transaction():
context.run_migrations()
asyncdefrun_async_migrations() ->None:
connectable=async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
asyncwithconnectable.connect() asconnection:
awaitconnection.run_sync(do_run_migrations)
awaitconnectable.dispose()
defrun_migrations_online() ->None:
asyncio.run(run_async_migrations())
ifcontext.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
render_as_batch=True: Required for SQLite, which does not support ALTER TABLE natively. Alembic uses a batch process (copy-transform-replace) instead. Harmless on PostgreSQL.
UUID primary key: HyphenatedUUID autogenerates as VARCHAR(36) — works in both SQLite and PostgreSQL
render_as_batch=True: Required for SQLite ALTER TABLE; harmless on PostgreSQL
Seed data: Use UUID v5 with a consistent namespace so values are deterministic across environments
7. Migration structure
Three migrations, each independently reversible:
alembic/versions/
001_create_players_table.py ← autogenerated from Player schema
002_seed_starting11.py ← manual: inserts 11 Starting XI players
003_seed_substitutes.py ← manual: inserts 14 Substitute players
Educational note: Splitting seed data across two migrations illustrates the incremental, composable nature of the migration system. alembic downgrade -1 removes only the substitutes, leaving the starting 11 intact.
Seed migrations are written manually (not autogenerated) and use op.bulk_insert() or raw op.execute():
# 002_seed_starting11.pydefupgrade() ->None:
op.execute(""" INSERT INTO players (id, firstName, ...) VALUES ('uuid-v5-value-1', 'Emiliano', ...), -- ... 10 more Starting XI players """)
defdowngrade() ->None:
op.execute("DELETE FROM players WHERE starting11 = true")
# 003_seed_substitutes.pydefupgrade() ->None:
op.execute(""" INSERT INTO players (id, firstName, ...) VALUES ('uuid-v5-value-12', 'Gerónimo', ...), -- ... 13 more Substitute players """)
defdowngrade() ->None:
op.execute("DELETE FROM players WHERE starting11 = false")
8. Migration Workflow
# Auto-generate schema migration from model changes
alembic revision --autogenerate -m "description"# Apply all pending migrations
alembic upgrade head
# Roll back last migration
alembic downgrade -1
9. Remove pre-seeded database
Remove committed .db file from git (if present)
Add *.db to .gitignore
Move seed data into an Alembic data migration using deterministic UUID v5 values
10. Update documentation
Document DATABASE_URL environment variable
Add migration workflow to README.md
Explain UUID v5 vs v4 seed strategy
Acceptance Criteria
Alembic installed and initialized
alembic/env.py reads DATABASE_URL from environment (SQLite default, PostgreSQL compatible)
alembic/env.py configured for async SQLAlchemy (run_async_migrations)
render_as_batch=True set for SQLite ALTER TABLE compatibility
target_metadata references Base.metadata from schemas/player_schema.py
HyphenatedUUID primary key generates as VARCHAR(36) in migration — verified in autogenerated script
002_seed_starting11.py seeds the 11 Starting XI players with deterministic UUID v5 values
003_seed_substitutes.py seeds the 14 Substitute players with deterministic UUID v5 values
Rolling back 003 removes only substitutes; 002 removes only Starting XI
Problem
The FastAPI project currently lacks a robust database migration management system. Without proper migration tooling, schema changes are difficult to track, version control, and deploy consistently across environments. This creates risks during database evolution and makes collaboration challenging when multiple developers work on schema changes.
Additional Challenge: The project uses async SQLAlchemy with
aiosqlite, which requires special consideration when setting up Alembic to ensure compatibility with the async database engine and session management.Project Architecture Note: The project has an unconventional but functional structure:
models/player_model.py→ Pydantic models for API validationschemas/player_schema.py→ SQLAlchemy ORM models (actual database tables)This is opposite to the typical FastAPI convention where "schemas" are Pydantic and "models" are SQLAlchemy, but Alembic will work with the existing structure.
Proposed Solution
Implement Alembic as the database migration tool for the FastAPI project. Alembic will provide:
Why Alembic over Prisma Client?
After evaluation, Alembic is the recommended choice for the following reasons:
Alembic Advantages:
Prisma Client Limitations:
Suggested Approach
1. Project Structure
2. Installation and Setup
alembicandasyncpgtopyproject.tomldependenciesalembic init alembicalembic/env.py(see below)3. Primary key design: UUID, not auto-increment integer
The
Playerschema uses a customHyphenatedUUIDtype as the primary key (stored asString(36), hyphenated UUID format):Alembic
autogeneratewill detectHyphenatedUUIDasString(36)and generate the column correctly. No manual column type override needed — but the generated migration should be reviewed to confirm theString(36)mapping.Seed data in migrations must use deterministic UUID v5 values (stable across environments), not random UUID v4. This is consistent with the docstring in
schemas/player_schema.py:4. Configure
alembic/env.pyRead
DATABASE_URLfrom the environment, supporting both SQLite and PostgreSQL:5. Apply migrations on startup
Update the lifespan handler in
main.py:6. Cross-dialect migration considerations
HyphenatedUUIDautogenerates asVARCHAR(36)— works in both SQLite and PostgreSQLrender_as_batch=True: Required for SQLiteALTER TABLE; harmless on PostgreSQL7. Migration structure
Three migrations, each independently reversible:
Seed migrations are written manually (not autogenerated) and use
op.bulk_insert()or rawop.execute():8. Migration Workflow
9. Remove pre-seeded database
.dbfile from git (if present)*.dbto.gitignore10. Update documentation
DATABASE_URLenvironment variableREADME.mdAcceptance Criteria
alembic/env.pyreadsDATABASE_URLfrom environment (SQLite default, PostgreSQL compatible)alembic/env.pyconfigured for async SQLAlchemy (run_async_migrations)render_as_batch=Trueset for SQLite ALTER TABLE compatibilitytarget_metadatareferencesBase.metadatafromschemas/player_schema.pyHyphenatedUUIDprimary key generates asVARCHAR(36)in migration — verified in autogenerated script002_seed_starting11.pyseeds the 11 Starting XI players with deterministic UUID v5 values003_seed_substitutes.pyseeds the 14 Substitute players with deterministic UUID v5 values003removes only substitutes;002removes only Starting XIalembic upgrade head)asyncpgadded topyproject.toml.dbfile removed from git;*.dbadded to.gitignoreDATABASE_URLvariableReferences
render_as_batch