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
|
import uuid
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from pydantic import BaseModel
|
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.engine import base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
@ -41,13 +42,27 @@ class AlchemyExporter(BaseService):
|
|||||||
self.session_maker = sessionmaker(bind=self.engine)
|
self.session_maker = sessionmaker(bind=self.engine)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_uuid(value: str) -> bool:
|
def is_uuid(value: Any) -> bool:
|
||||||
try:
|
try:
|
||||||
uuid.UUID(value)
|
uuid.UUID(value)
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
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:
|
def convert_types(self, data: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
walks the dictionary to restore all things that look like string representations of their complex types
|
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
|
data[key] = self.DateTimeParser(time=value).time
|
||||||
return data
|
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:
|
def dump_schema(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Returns the schema of the SQLAlchemy database as a python dictionary. This dictionary is wrapped by
|
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:
|
if not rows:
|
||||||
continue
|
continue
|
||||||
table = self.meta.tables[table_name]
|
table = self.meta.tables[table_name]
|
||||||
|
rows = self.clean_rows(db_dump, table, rows)
|
||||||
|
|
||||||
connection.execute(table.delete())
|
connection.execute(table.delete())
|
||||||
connection.execute(insert(table), rows)
|
connection.execute(insert(table), rows)
|
||||||
|
@ -69,7 +69,7 @@ class BackupV2(BaseService):
|
|||||||
shutil.copytree(f, self.directories.DATA_DIR / f.name)
|
shutil.copytree(f, self.directories.DATA_DIR / f.name)
|
||||||
|
|
||||||
def restore(self, backup_path: Path) -> None:
|
def restore(self, backup_path: Path) -> None:
|
||||||
self.logger.info("initially backup restore")
|
self.logger.info("initializing backup restore")
|
||||||
|
|
||||||
backup = BackupFile(backup_path)
|
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"
|
backup_version_44e8d670719d_2 = CWD / "backups/backup_version_44e8d670719d_2.zip"
|
||||||
"""44e8d670719d: add extras to shopping lists, list items, and ingredient foods"""
|
"""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"
|
backup_version_ba1e4a6cfe99_1 = CWD / "backups/backup_version_ba1e4a6cfe99_1.zip"
|
||||||
"""ba1e4a6cfe99: added plural names and alias tables for foods and units"""
|
"""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_1,
|
||||||
test_data.backup_version_44e8d670719d_2,
|
test_data.backup_version_44e8d670719d_2,
|
||||||
|
test_data.backup_version_44e8d670719d_3,
|
||||||
test_data.backup_version_ba1e4a6cfe99_1,
|
test_data.backup_version_ba1e4a6cfe99_1,
|
||||||
test_data.backup_version_bcfdad6b7355_1,
|
test_data.backup_version_bcfdad6b7355_1,
|
||||||
],
|
],
|
||||||
ids=[
|
ids=[
|
||||||
"44e8d670719d_1: add extras to shopping lists, list items, and ingredient foods",
|
"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_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",
|
"ba1e4a6cfe99_1: added plural names and alias tables for foods and units",
|
||||||
"bcfdad6b7355_1: remove tool name and slug unique contraints",
|
"bcfdad6b7355_1: remove tool name and slug unique contraints",
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user