"""seed canonical ingredients (and 30 starter recipes in P4-14) Revision ID: 0007 Revises: 0006 Create Date: 2026-05-05 """ from decimal import Decimal from alembic import op import sqlalchemy as sa revision = "0007" down_revision = "0006" branch_labels = None depends_on = None # Canonical ingredients used by the 30 starter recipes (P4-14 references them # by name_lower so this list stays the source of truth for both seed steps). INGREDIENTS = [ # (name, aliases, aisle, unit, typical_price) ("Chicken Thighs, Boneless Skinless", ["chicken thigh", "BSL chicken thighs"], "meat_seafood", "lb", Decimal("4.99")), ("Chicken Breast, Boneless Skinless", ["chicken breast", "BSL chicken breast"], "meat_seafood", "lb", Decimal("5.99")), ("Ground Beef, 85/15", ["ground beef", "hamburger"], "meat_seafood", "lb", Decimal("6.99")), ("Ground Turkey", ["turkey mince"], "meat_seafood", "lb", Decimal("5.49")), ("Pork Chops, Bone-In", ["pork chop"], "meat_seafood", "lb", Decimal("4.49")), ("Salmon Fillet", ["salmon"], "meat_seafood", "lb", Decimal("12.99")), ("Shrimp, Peeled", ["shrimp"], "meat_seafood", "lb", Decimal("9.99")), ("Eggs, Large", ["egg"], "dairy", "ea", Decimal("0.40")), ("Black Beans, Canned", ["black bean"], "pantry", "can", Decimal("1.29")), ("Chickpeas, Canned", ["garbanzo", "chickpea"], "pantry", "can", Decimal("1.49")), ("Yellow Onion", ["onion"], "produce", "ea", Decimal("0.99")), ("Garlic", ["garlic clove"], "produce", "clove", Decimal("0.10")), ("Carrot", ["carrots"], "produce", "ea", Decimal("0.50")), ("Celery", [], "produce", "stalk", Decimal("0.30")), ("Bell Pepper, Red", ["red pepper"], "produce", "ea", Decimal("1.49")), ("Bell Pepper, Green", ["green pepper"], "produce", "ea", Decimal("0.99")), ("Tomato, Roma", ["tomato"], "produce", "ea", Decimal("0.79")), ("Lemon", [], "produce", "ea", Decimal("0.79")), ("Lime", [], "produce", "ea", Decimal("0.50")), ("Cilantro", ["coriander leaf"], "produce", "bunch", Decimal("0.99")), ("Parsley, Italian", ["parsley"], "produce", "bunch", Decimal("0.99")), ("Spinach, Fresh", ["spinach"], "produce", "oz", Decimal("0.40")), ("Broccoli", [], "produce", "lb", Decimal("2.49")), ("Zucchini", [], "produce", "ea", Decimal("0.99")), ("Sweet Potato", [], "produce", "ea", Decimal("1.29")), ("Olive Oil", ["EVOO", "extra virgin olive oil"], "pantry", "tbsp", Decimal("0.20")), ("Soy Sauce", [], "pantry", "tbsp", Decimal("0.15")), ("Rice, Long-Grain White", ["white rice", "rice"], "pantry", "cup", Decimal("0.40")), ("Pasta, Penne", ["penne"], "pantry", "lb", Decimal("1.29")), ("Pasta, Spaghetti", ["spaghetti"], "pantry", "lb", Decimal("1.29")), ("Tortilla, Flour", ["flour tortilla"], "pantry", "ea", Decimal("0.30")), ("Tortilla, Corn", ["corn tortilla"], "pantry", "ea", Decimal("0.20")), ("Diced Tomatoes, Canned", ["canned tomato"], "pantry", "can", Decimal("1.49")), ("Chicken Broth", ["chicken stock"], "pantry", "cup", Decimal("0.30")), ("Coconut Milk, Canned", [], "pantry", "can", Decimal("2.49")), ("Salt, Kosher", ["kosher salt"], "pantry", "tsp", Decimal("0.01")), ("Black Pepper", ["pepper"], "pantry", "tsp", Decimal("0.02")), ("Cumin, Ground", ["cumin"], "pantry", "tsp", Decimal("0.10")), ("Paprika, Smoked", ["smoked paprika"], "pantry", "tsp", Decimal("0.10")), ("Italian Seasoning", [], "pantry", "tsp", Decimal("0.10")), ("Curry Powder", [], "pantry", "tsp", Decimal("0.10")), ("Ginger, Fresh", ["ginger root"], "produce", "tbsp", Decimal("0.30")), ("Cheddar Cheese, Sharp", ["cheddar"], "dairy", "oz", Decimal("0.40")), ("Mozzarella, Shredded", ["mozzarella"], "dairy", "oz", Decimal("0.45")), ("Parmesan, Grated", ["parm"], "dairy", "tbsp", Decimal("0.20")), ("Sour Cream", [], "dairy", "tbsp", Decimal("0.10")), ("Greek Yogurt, Plain", ["yogurt"], "dairy", "cup", Decimal("1.50")), ("Butter, Unsalted", ["butter"], "dairy", "tbsp", Decimal("0.20")), ("Milk, Whole", ["milk"], "dairy", "cup", Decimal("0.30")), ("Avocado", [], "produce", "ea", Decimal("1.49")), ("Salsa, Jarred", ["salsa"], "pantry", "tbsp", Decimal("0.15")), ] def _upsert_ingredients() -> None: bind = op.get_bind() for name, aliases, aisle, unit, typical_price in INGREDIENTS: bind.execute( sa.text( """ INSERT INTO ingredient (id, name, name_lower, aliases, aisle, unit, typical_price) VALUES ( gen_random_uuid(), :name, :name_lower, :aliases, :aisle, :unit, :typical_price ) ON CONFLICT (name_lower) DO UPDATE SET aliases = EXCLUDED.aliases, aisle = COALESCE(EXCLUDED.aisle, ingredient.aisle), unit = COALESCE(EXCLUDED.unit, ingredient.unit), typical_price = COALESCE(EXCLUDED.typical_price, ingredient.typical_price) """ ), { "name": name, "name_lower": name.lower(), "aliases": aliases, "aisle": aisle, "unit": unit, "typical_price": typical_price, }, ) def _insert_recipes() -> None: """Populated in P4-14.""" pass def _delete_recipes() -> None: """Populated in P4-14.""" pass def _delete_ingredients() -> None: """Best-effort: only remove rows whose name_lower matches one of our seeds. Pre-existing rows from migration 0002 may share names; their aliases/aisle will revert to whatever was in 0002 (NULL aliases pre-0006). For a clean rollback in this scenario, run alembic downgrade past 0002 as well. """ bind = op.get_bind() name_lowers = [name.lower() for name, *_ in INGREDIENTS] bind.execute( sa.text("DELETE FROM ingredient WHERE name_lower = ANY(:nl)"), {"nl": name_lowers}, ) def upgrade() -> None: _upsert_ingredients() _insert_recipes() def downgrade() -> None: _delete_recipes() _delete_ingredients()