mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
fix: Foreign Key Violations During Backup Restore (#2986)
* added more test data * added missing pytest id * add fk validation to backup restore * removed bad type imports * actually apply the invalid fk filter and clean up types * fix key name * added log when removing bad rows * removed unused import * bumped info to warning
This commit is contained in:
parent
b4c0a8b509
commit
2a5997a968
@ -2,10 +2,11 @@ import datetime
|
||||
import uuid
|
||||
from os import path
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import ForeignKeyConstraint, MetaData, create_engine, insert, text
|
||||
from sqlalchemy import ForeignKey, ForeignKeyConstraint, MetaData, Table, create_engine, insert, text
|
||||
from sqlalchemy.engine import base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
@ -41,13 +42,27 @@ class AlchemyExporter(BaseService):
|
||||
self.session_maker = sessionmaker(bind=self.engine)
|
||||
|
||||
@staticmethod
|
||||
def is_uuid(value: str) -> bool:
|
||||
def is_uuid(value: Any) -> bool:
|
||||
try:
|
||||
uuid.UUID(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_foreign_key(db_dump: dict[str, list[dict]], fk: ForeignKey, fk_value: Any) -> bool:
|
||||
if not fk_value:
|
||||
return True
|
||||
|
||||
foreign_table_name = fk.column.table.name
|
||||
foreign_field_name = fk.column.name
|
||||
|
||||
for row in db_dump.get(foreign_table_name, []):
|
||||
if row[foreign_field_name] == fk_value:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def convert_types(self, data: dict) -> dict:
|
||||
"""
|
||||
walks the dictionary to restore all things that look like string representations of their complex types
|
||||
@ -70,6 +85,33 @@ class AlchemyExporter(BaseService):
|
||||
data[key] = self.DateTimeParser(time=value).time
|
||||
return data
|
||||
|
||||
def clean_rows(self, db_dump: dict[str, list[dict]], table: Table, rows: list[dict]) -> list[dict]:
|
||||
"""
|
||||
Checks rows against foreign key restraints and removes any rows that would violate them
|
||||
"""
|
||||
|
||||
fks = table.foreign_keys
|
||||
|
||||
valid_rows = []
|
||||
for row in rows:
|
||||
is_valid_row = True
|
||||
for fk in fks:
|
||||
fk_value = row.get(fk.parent.name)
|
||||
if self.is_valid_foreign_key(db_dump, fk, row.get(fk.parent.name)):
|
||||
continue
|
||||
|
||||
is_valid_row = False
|
||||
self.logger.warning(
|
||||
f"Removing row from table {table.name} because of invalid foreign key {fk.parent.name}: {fk_value}"
|
||||
)
|
||||
self.logger.warning(f"Row: {row}")
|
||||
break
|
||||
|
||||
if is_valid_row:
|
||||
valid_rows.append(row)
|
||||
|
||||
return valid_rows
|
||||
|
||||
def dump_schema(self) -> dict:
|
||||
"""
|
||||
Returns the schema of the SQLAlchemy database as a python dictionary. This dictionary is wrapped by
|
||||
@ -125,6 +167,7 @@ class AlchemyExporter(BaseService):
|
||||
if not rows:
|
||||
continue
|
||||
table = self.meta.tables[table_name]
|
||||
rows = self.clean_rows(db_dump, table, rows)
|
||||
|
||||
connection.execute(table.delete())
|
||||
connection.execute(insert(table), rows)
|
||||
|
@ -69,7 +69,7 @@ class BackupV2(BaseService):
|
||||
shutil.copytree(f, self.directories.DATA_DIR / f.name)
|
||||
|
||||
def restore(self, backup_path: Path) -> None:
|
||||
self.logger.info("initially backup restore")
|
||||
self.logger.info("initializing backup restore")
|
||||
|
||||
backup = BackupFile(backup_path)
|
||||
|
||||
|
@ -10,6 +10,9 @@ backup_version_44e8d670719d_1 = CWD / "backups/backup_version_44e8d670719d_1.zip
|
||||
backup_version_44e8d670719d_2 = CWD / "backups/backup_version_44e8d670719d_2.zip"
|
||||
"""44e8d670719d: add extras to shopping lists, list items, and ingredient foods"""
|
||||
|
||||
backup_version_44e8d670719d_3 = CWD / "backups/backup_version_44e8d670719d_3.zip"
|
||||
"""44e8d670719d: add extras to shopping lists, list items, and ingredient foods"""
|
||||
|
||||
backup_version_ba1e4a6cfe99_1 = CWD / "backups/backup_version_ba1e4a6cfe99_1.zip"
|
||||
"""ba1e4a6cfe99: added plural names and alias tables for foods and units"""
|
||||
|
||||
|
BIN
tests/data/backups/backup_version_44e8d670719d_3.zip
Normal file
BIN
tests/data/backups/backup_version_44e8d670719d_3.zip
Normal file
Binary file not shown.
@ -73,12 +73,14 @@ def test_database_restore():
|
||||
[
|
||||
test_data.backup_version_44e8d670719d_1,
|
||||
test_data.backup_version_44e8d670719d_2,
|
||||
test_data.backup_version_44e8d670719d_3,
|
||||
test_data.backup_version_ba1e4a6cfe99_1,
|
||||
test_data.backup_version_bcfdad6b7355_1,
|
||||
],
|
||||
ids=[
|
||||
"44e8d670719d_1: add extras to shopping lists, list items, and ingredient foods",
|
||||
"44e8d670719d_2: add extras to shopping lists, list items, and ingredient foods",
|
||||
"44e8d670719d_3: add extras to shopping lists, list items, and ingredient foods",
|
||||
"ba1e4a6cfe99_1: added plural names and alias tables for foods and units",
|
||||
"bcfdad6b7355_1: remove tool name and slug unique contraints",
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user