Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Handle Data With Invalid User #4325

57 changes: 57 additions & 0 deletions mealie/db/fixes/fix_migration_data.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,73 @@
from uuid import uuid4

from slugify import slugify
from sqlalchemy import and_, update
from sqlalchemy.orm import Session

from mealie.core import root_logger
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models.group.group import Group
from mealie.db.models.household.shopping_list import ShoppingList, ShoppingListMultiPurposeLabel
from mealie.db.models.labels import MultiPurposeLabel
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
from mealie.db.models.recipe.recipe import RecipeModel
from mealie.db.models.users.users import User

logger = root_logger.get_logger("init_db")


def fix_dangling_refs(session: Session):
REASSIGN_REF_TABLES = ["group_meal_plans", "recipes", "shopping_lists"]
DELETE_REF_TABLES = ["long_live_tokens", "password_reset_tokens", "recipe_comments", "recipe_timeline_events"]

groups = session.query(Group).all()
for group in groups:
# Find an arbitrary admin user in the group
default_user = session.query(User).filter(User.group_id == group.id, User.admin == True).first() # noqa: E712 - required for SQLAlchemy comparison
if not default_user:
# If there is no admin user, just pick the first user
default_user = session.query(User).filter(User.group_id == group.id).first()

# If there are no users in the group, we can't do anything
if not default_user:
continue

valid_user_ids = {user.id for user in group.users}

for table_name in REASSIGN_REF_TABLES:
table = SqlAlchemyBase.metadata.tables[table_name]
update_stmt = (
update(table)
.where(
and_(
~table.c.user_id.in_(valid_user_ids),
michael-genson marked this conversation as resolved.
Show resolved Hide resolved
table.c.group_id == group.id,
)
)
.values(user_id=default_user.id)
)
result = session.execute(update_stmt)

if result.rowcount:
logger.info(
f'Reassigned {result.rowcount} {"row" if result.rowcount == 1 else "rows"} '
f'in "{table_name}" table to default user ({default_user.id})'
)

for table_name in DELETE_REF_TABLES:
table = SqlAlchemyBase.metadata.tables[table_name]
delete_stmt = table.delete().where(~table.c.user_id.in_(valid_user_ids))
michael-genson marked this conversation as resolved.
Show resolved Hide resolved
result = session.execute(delete_stmt)

if result.rowcount:
logger.info(
f'Deleted {result.rowcount} {"row" if result.rowcount == 1 else "rows"} '
f'in "{table_name}" table with invalid user ids'
)

session.commit()


def fix_recipe_normalized_search_properties(session: Session):
recipes = session.query(RecipeModel).all()
recipes_fixed = False
Expand Down Expand Up @@ -144,6 +199,8 @@ def fix_normalized_unit_and_food_names(session: Session):

def fix_migration_data(session: Session):
logger.info("Checking for migration data fixes")

fix_dangling_refs(session)
fix_recipe_normalized_search_properties(session)
fix_shopping_list_label_settings(session)
fix_group_slugs(session)
Expand Down
9 changes: 9 additions & 0 deletions mealie/services/backups_v2/alchemy_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from alembic import command
from alembic.config import Config
from mealie.db import init_db
from mealie.db.fixes.fix_migration_data import fix_migration_data
from mealie.db.models._model_utils.guid import GUID
from mealie.services._base_service import BaseService

Expand Down Expand Up @@ -137,6 +138,14 @@ def dump(self) -> dict[str, list[dict]]:
Returns the entire SQLAlchemy database as a python dictionary. This dictionary is wrapped by
jsonable_encoder to ensure that the object can be converted to a json string.
"""

# run database fixes first so we aren't backing up bad data
with self.session_maker() as session:
try:
fix_migration_data(session)
except Exception:
self.logger.error("Error fixing migration data during export; continuing anyway")

with self.engine.connect() as connection:
self.meta.reflect(bind=self.engine) # http://docs.sqlalchemy.org/en/rel_0_9/core/reflection.html

Expand Down
Loading