Files
Meal-Planner/backend/alembic/versions/0015_normalize_pantry_aisles.py
T
admin f5fb7558c4 fix(migration): simplify aisle migration + add persistent backup script
- Drop the empty batch_alter_table block and the meaningless
  set_config call from migration 0015. Temp tables still persist
  for the migration's session (Alembic's transactional_ddl).
- New backend/scripts/persist_aisle_backup.sql creates
  public.ingredient_aisle_backup_0015 and
  public.grocery_item_aisle_backup_0015 permanent tables for
  operators who want a recoverable record beyond the migration.
- Update Review/sprint2-verification.md, Review/ui-nielsen-audit.md
  and fix-ui-audit.md with the correct container-based deploy
  steps: docker compose exec db psql -U mealplanner -d mealplanner
  -f /dev/stdin < ...sql. Host psql is not available on the
  deployment host; the db runs inside the container.
2026-06-03 17:41:59 -07:00

98 lines
3.1 KiB
Python

"""Normalize ingredient.aisle and grocery_item.aisle to canonical labels.
Revision ID: 0015
Revises: 0014
Create Date: 2026-06-02
Normalizes free-text aisle values on `ingredient.aisle` and `grocery_item.aisle`
to a fixed canonical set. Runs in a single transaction (Alembic default); both
op.execute calls share the same session, so the temp backup tables persist
for the duration of the upgrade.
Backup tables: the temp tables `ingredient_aisle_backup` and
`grocery_item_aisle_backup` are created for the migration's session. They
auto-drop when the session ends. If you need a persistent backup, run
`backend/scripts/persist_aisle_backup.sql` BEFORE this migration.
Deploy via Docker (the db runs inside a container; no host psql required):
# 1. dry-run preview:
docker compose exec db psql -U mealplanner -d mealplanner \\
-f /dev/stdin < backend/scripts/dry_run_aisle_migration.sql
# 2. apply:
docker compose exec backend alembic upgrade head
"""
from typing import Sequence, Union
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "0015"
down_revision: Union[str, None] = "0014"
branch_labels: Union[Sequence[str], None] = None
depends_on: Union[Sequence[str], None] = None
NORMALIZATION_RULES = [
("canned goods", "Pantry"),
("canned", "Pantry"),
("freezer", "Frozen"),
("frozen", "Frozen"),
("produce", "Produce"),
("fruit", "Produce"),
("vegetable", "Produce"),
("dairy", "Dairy & Eggs"),
("eggs", "Dairy & Eggs"),
("cheese", "Dairy & Eggs"),
("milk", "Dairy & Eggs"),
("yogurt", "Dairy & Eggs"),
("meat", "Meat & Seafood"),
("seafood", "Meat & Seafood"),
("fish", "Meat & Seafood"),
("chicken", "Meat & Seafood"),
("beef", "Meat & Seafood"),
("pork", "Meat & Seafood"),
("meat_seafood", "Meat & Seafood"),
("bakery", "Bakery"),
("bread", "Bakery"),
("beverage", "Beverages"),
("beverages", "Beverages"),
("drinks", "Beverages"),
("spice", "Spices"),
("spices", "Spices"),
("seasoning", "Spices"),
("pantry", "Pantry"),
("dry", "Pantry"),
("snack", "Pantry"),
("snacks", "Pantry"),
]
CASE_EXPR = "CASE LOWER(COALESCE(aisle, '')) " + " ".join(
f"WHEN '{src}' THEN '{dst}' " for src, dst in NORMALIZATION_RULES
) + " WHEN NULLIF(LOWER(COALESCE(aisle, '')), '') IS NULL THEN NULL " + " ELSE 'Other' END"
def _normalize(table: str) -> None:
op.execute(
f"CREATE TEMP TABLE {table}_aisle_backup AS "
f"SELECT id, aisle FROM {table} WHERE aisle IS NOT NULL"
)
op.execute(f"UPDATE {table} SET aisle = {CASE_EXPR} WHERE aisle IS NOT NULL")
def upgrade() -> None:
_normalize("ingredient")
_normalize("grocery_item")
def downgrade() -> None:
# The temp backup tables only exist for the migration's session.
# Restoring the pre-normalization state is not possible from this
# migration alone. Operators must restore from a database snapshot
# taken before upgrade.
raise NotImplementedError(
"Cannot reverse aisle normalization without an external backup. "
"Restore the database from a snapshot taken before 0015 was applied."
)