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
15 changes: 13 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ jobs:

pytest:
runs-on: ubuntu-latest
name: pytest (${{ matrix.name }})
strategy:
fail-fast: false
matrix:
include:
- name: app
run: |
uv run alembic upgrade head
uv run pytest . --cov=. --cov-report xml
# pytest-alembic upgrades an empty database itself, so no `alembic upgrade` first.
- name: migrations
run: uv run pytest tests/migrations --override-ini=addopts=
services:
postgres:
image: postgres:latest
Expand All @@ -52,8 +64,7 @@ jobs:
- run: uv python pin 3.14
- run: |
uv sync --all-extras --all-groups --frozen --no-install-project
uv run alembic upgrade head
uv run pytest . --cov=. --cov-report xml
${{ matrix.run }}
env:
SERVICE_ENVIRONMENT: ci
PYTHONDONTWRITEBYTECODE: 1
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Almost everything runs through Docker Compose: the app and Postgres come up toge

- `just test` cycles the DB (downgrade to `base`, upgrade to `head`) before pytest and tears the stack down before and after. Pass pytest args through, e.g. `just test tests/test_decks.py::test_post_decks -x`.
- pytest runs with `--cov=. --cov-report term-missing --cov-fail-under=100` (`pyproject.toml`), so `just test` fails unless tests cover every line of new code.
- `just test-migrations` runs the `pytest-alembic` suite in `tests/migrations/` against an empty database: single head, upgrade, up/down consistency for every revision, and models matching the migrated schema (`alembic check` as a test). `just test` skips that directory (`--ignore` in `addopts`, `omit` in coverage) because it migrates the schema under the transaction-rollback fixture, so the recipe clears `addopts`. CI runs both suites as separate jobs.
- `just migration -m "message"` autogenerates an Alembic revision **against an already-upgraded DB** — the recipe enforces this, so don't run autogen by hand.
- `just lint` runs `eof-fixer`, `ruff format`, `ruff check --fix`, then `ty check`.

Expand Down
2 changes: 1 addition & 1 deletion app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ class Card(BigIntAuditBase):
front: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False)
back: orm.Mapped[str | None] = orm.mapped_column(sa.String, nullable=True)
hint: orm.Mapped[str | None] = orm.mapped_column(sa.String, nullable=True)
deck_id: orm.Mapped[int] = orm.mapped_column(sa.Integer, sa.ForeignKey("decks.id"))
deck_id: orm.Mapped[int] = orm.mapped_column(sa.BigInteger, sa.ForeignKey("decks.id"))
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ sh:
test *args: down && down
docker compose run application sh -c "sleep 1 && uv run alembic downgrade base && uv run alembic upgrade head && uv run pytest {{ args }}"

test-migrations *args: down && down
docker compose run application sh -c "sleep 1 && uv run pytest tests/migrations --override-ini=addopts= {{ args }}"

run:
docker compose run --service-ports application sh -c "sleep 1 && uv run alembic upgrade head && uv run python -m app"

Expand Down
33 changes: 33 additions & 0 deletions migrations/versions/2026-10-16_bigint_ids_and_required_deck_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""bigint ids and required deck_id.

Revision ID: 8e4a2c7b1f6d
Revises: 5d1c0f3a9b2e
Create Date: 2026-10-16 22:00:00.000000

"""

import sqlalchemy as sa
from alembic import op


# revision identifiers, used by Alembic.
revision = "8e4a2c7b1f6d"
down_revision = "5d1c0f3a9b2e"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.alter_column("decks", "id", type_=sa.BigInteger(), existing_type=sa.Integer(), existing_nullable=False)
op.execute("ALTER SEQUENCE decks_id_seq AS bigint")
op.alter_column("cards", "id", type_=sa.BigInteger(), existing_type=sa.Integer(), existing_nullable=False)
op.execute("ALTER SEQUENCE cards_id_seq AS bigint")
op.alter_column("cards", "deck_id", type_=sa.BigInteger(), existing_type=sa.Integer(), nullable=False)


def downgrade() -> None:
op.alter_column("cards", "deck_id", type_=sa.Integer(), existing_type=sa.BigInteger(), nullable=True)
op.execute("ALTER SEQUENCE cards_id_seq AS integer")
op.alter_column("cards", "id", type_=sa.Integer(), existing_type=sa.BigInteger(), existing_nullable=False)
op.execute("ALTER SEQUENCE decks_id_seq AS integer")
op.alter_column("decks", "id", type_=sa.Integer(), existing_type=sa.BigInteger(), existing_nullable=False)
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dev = [
"pytest-cov",
"pytest-asyncio",
"asgi_lifespan",
"pytest-alembic",
]
lint = [
"ruff",
Expand Down Expand Up @@ -73,7 +74,7 @@ isort.no-lines-before = ["standard-library", "local-folder"]
]

[tool.pytest.ini_options]
addopts = "--cov=. --cov-report term-missing --cov-fail-under=100"
addopts = "--cov=. --cov-report term-missing --cov-fail-under=100 --ignore=tests/migrations"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

Expand All @@ -85,3 +86,4 @@ exclude_also = [
[tool.coverage.run]
concurrency = ["thread", "greenlet"]
disable_warnings = ["couldnt-parse"]
omit = ["tests/migrations/*"]
Empty file added tests/migrations/__init__.py
Empty file.
14 changes: 14 additions & 0 deletions tests/migrations/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import typing

import pytest
from sqlalchemy import Engine, create_engine

from app.settings import settings


@pytest.fixture
def alembic_engine() -> typing.Iterator[Engine]:
# Replaces pytest-alembic's default in-memory SQLite engine.
engine: typing.Final = create_engine(settings.db_dsn_parsed.set(drivername="postgresql+psycopg"))
yield engine
engine.dispose()
12 changes: 12 additions & 0 deletions tests/migrations/test_migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pytest_alembic.tests import (
test_model_definitions_match_ddl,
test_single_head_revision,
test_up_down_consistency,
test_upgrade,
)


_ = test_single_head_revision
_ = test_upgrade
_ = test_model_definitions_match_ddl
_ = test_up_down_consistency
18 changes: 17 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading