All sibling services on the box manage schema with Alembic (async env, `alembic upgrade head` in the compose command). matchmaking-v2 was the outlier (Base.metadata.create_all + no migration for the new user_feed.exhausted_at column). Bring it onto the pattern: - alembic.ini + alembic/env.py (async, URL from settings.DATABASE_URL, target = Base.metadata) + script.py.mako — copied/adapted from user-service. - versions/0001_baseline: the current prod schema (user_feed WITHOUT exhausted_at + opportunity_state). - versions/0002_pool_and_exhausted: CREATE user_job_pool + ADD user_feed.exhausted_at (additive). - Dockerfile: COPY alembic.ini + alembic/. requirements: alembic>=1.13.0. Verified both paths: fresh DB → upgrade head builds all 3 tables; prod-like (existing tables+data) → stamp 0001 → upgrade runs ONLY 0002, existing rows survive. 64 tests pass. PROD ADOPTION (one-time): `alembic stamp 0001_baseline` on RDS before the first deploy, then the compose's `alembic upgrade head` applies 0002.
52 lines
2.2 KiB
Python
52 lines
2.2 KiB
Python
"""baseline — the schema as it exists in prod today (from the cutover's create_all):
|
|
user_feed (WITHOUT exhausted_at) + opportunity_state. On prod this is STAMPED (the tables already
|
|
exist); on a fresh DB `upgrade` creates them. The exhausted_at column + user_job_pool are added in 0002.
|
|
|
|
Revision ID: 0001_baseline
|
|
Revises:
|
|
Create Date: 2026-06-26
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
revision = "0001_baseline"
|
|
down_revision = None
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"user_feed",
|
|
sa.Column("user_id", sa.String(), primary_key=True),
|
|
sa.Column("prefs", sa.JSON(), nullable=True),
|
|
sa.Column("opportunities", sa.JSON(), nullable=True),
|
|
sa.Column("sources", sa.JSON(), nullable=True),
|
|
sa.Column("engine", sa.String(), nullable=True),
|
|
sa.Column("scanned", sa.Integer(), nullable=True),
|
|
sa.Column("query_sig", sa.String(), nullable=True),
|
|
sa.Column("cursors", sa.JSON(), nullable=True),
|
|
sa.Column("search_count", sa.Integer(), server_default="0", nullable=True),
|
|
sa.Column("salary_peak_sum", sa.Float(), server_default="0", nullable=True),
|
|
sa.Column("salary_peak_count", sa.Integer(), server_default="0", nullable=True),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True),
|
|
)
|
|
op.create_table(
|
|
"opportunity_state",
|
|
sa.Column("user_id", sa.String(), primary_key=True),
|
|
sa.Column("opportunity_id", sa.String(), primary_key=True),
|
|
sa.Column("status", sa.String(), nullable=True),
|
|
sa.Column("tailored_resume_id", sa.String(), nullable=True),
|
|
sa.Column("tailored_version_id", sa.String(), nullable=True),
|
|
sa.Column("cover_letter_id", sa.String(), nullable=True),
|
|
sa.Column("seen", sa.Boolean(), server_default="false", nullable=True),
|
|
sa.Column("viewed", sa.Boolean(), server_default="false", nullable=True),
|
|
sa.Column("saved", sa.Boolean(), server_default="false", nullable=True),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table("opportunity_state")
|
|
op.drop_table("user_feed")
|