Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ jobs:
with:
node-version: "20"
cache: npm
cache-dependency-path: frontend/package.json
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
working-directory: frontend
run: npm install
run: npm ci

- name: Lint (ESLint)
working-directory: frontend
Expand Down
Binary file modified README.md
Binary file not shown.
43 changes: 43 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[alembic]
script_location = alembic
prepend_sys_path = .

# sqlalchemy.url is set dynamically in env.py from Pydantic Settings.
# Do not set it here to avoid committing connection strings.
sqlalchemy.url =

[post_write_hooks]

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Empty file added backend/alembic/__init__.py
Empty file.
83 changes: 83 additions & 0 deletions backend/alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Alembic async migration environment.

Uses SQLAlchemy's asyncio runner so migrations execute against the same
asyncpg dialect as the application. The database URL is taken from
Pydantic Settings, never from alembic.ini, so no credentials are stored
in the repository.

Usage:
cd backend
alembic upgrade head
alembic revision --autogenerate -m "describe change"
alembic downgrade -1
"""

import asyncio
from logging.config import fileConfig

# Import all models so Base.metadata is populated for autogenerate.
import app.models # noqa: F401
from app.config import settings
from app.models.base import Base
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config

from alembic import context

alembic_config = context.config

if alembic_config.config_file_name is not None:
fileConfig(alembic_config.config_file_name)

target_metadata = Base.metadata


def run_migrations_offline() -> None:
"""Emit SQL to stdout without a live DB connection (dry-run mode)."""
context.configure(
url=settings.database_url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()


def do_run_migrations(connection: Connection) -> None:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()


async def run_async_migrations() -> None:
"""Create an async engine and run migrations inside a sync connection."""
section = alembic_config.get_section(alembic_config.config_ini_section, {})
section["sqlalchemy.url"] = settings.database_url

connectable = async_engine_from_config(
section,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)

await connectable.dispose()


def run_migrations_online() -> None:
asyncio.run(run_async_migrations())


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
26 changes: 26 additions & 0 deletions backend/alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from collections.abc import Sequence

import sqlalchemy as sa
${imports if imports else ""}
from alembic import op

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: str | None = ${repr(down_revision)}
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
depends_on: str | Sequence[str] | None = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
Loading
Loading