| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 |
- """The schema has to be sortable, because backup and restore sort it.
- `metadata.sorted_tables` is asked for the order in three places: the backup
- export, the restore's import loop, and the loop that puts foreign keys back
- afterwards. Three nullable SET NULL links used to close a loop --
- print_archives.library_file_id -> library_files.folder_id ->
- library_folders.archive_id -> print_archives -- and SQLAlchemy answered a
- sort it could not make, with a warning on every backup and every restore:
- Cannot correctly sort tables; there are unresolvable cycles between
- tables "library_files, library_folders, print_archives" ... this warning
- may raise an error in a future release.
- Two things were wrong with living on that. The order it returns can place a
- child before its parent, which is what once imported library_files ahead of
- library_folders and killed a restore on a ForeignKeyViolation. And the
- sentence at the end is a promise: if it ever becomes an error, backup and
- restore break on the same upgrade.
- One edge of the loop is marked use_alter, which takes it out of the sort
- graph without taking the constraint out of the database.
- """
- from __future__ import annotations
- import importlib
- import pkgutil
- import warnings
- from sqlalchemy import create_engine, inspect
- from backend.app.core.database import Base
- def _all_models_imported() -> None:
- """Base.metadata is filled by imports, so a partial import means a partial
- schema -- and a cycle in a table nobody imported would not be found here."""
- import backend.app.models as models
- for module in pkgutil.iter_modules(models.__path__):
- importlib.import_module(f"backend.app.models.{module.name}")
- def test_the_schema_sorts_without_a_cycle_warning():
- _all_models_imported()
- with warnings.catch_warnings(record=True) as caught:
- warnings.simplefilter("always")
- assert Base.metadata.sorted_tables
- cycles = [str(w.message) for w in caught if "cycles" in str(w.message)]
- assert not cycles, (
- f"a new foreign key has closed a loop in the schema; backup and restore sort these tables: {cycles}"
- )
- def test_the_tables_that_used_to_cycle_sort_parents_first():
- """The property the warning took away. Order is what the restore's import
- loop follows, and a child ahead of its parent is a FK violation."""
- _all_models_imported()
- order = [t.name for t in Base.metadata.sorted_tables]
- assert order.index("library_folders") < order.index("library_files"), (
- "library_files.folder_id points at library_folders"
- )
- assert order.index("library_files") < order.index("print_archives"), (
- "print_archives.library_file_id points at library_files"
- )
- def test_the_altered_constraint_still_exists_on_sqlite():
- """use_alter asks for ALTER TABLE ADD CONSTRAINT, and SQLite has no such
- statement. It inlines the key into CREATE TABLE instead -- but if that ever
- stopped being true, deleting an archive would leave a dangling
- library_folders.archive_id rather than nulling it, silently."""
- _all_models_imported()
- engine = create_engine("sqlite://")
- Base.metadata.create_all(engine)
- keys = inspect(engine).get_foreign_keys("library_folders")
- archive_link = [k for k in keys if k["referred_table"] == "print_archives"]
- assert archive_link, f"library_folders lost its archive key: {keys}"
- assert archive_link[0]["constrained_columns"] == ["archive_id"]
- assert archive_link[0]["options"].get("ondelete") == "SET NULL"
|