feat(6846): replace_from_backup rolls the store back to a full backup by id
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m38s
CI & Build / integration (push) Successful in 2m1s
CI & Build / Build & push image (push) Successful in 20s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m38s
CI & Build / integration (push) Successful in 2m1s
CI & Build / Build & push image (push) Successful in 20s
One transaction: - Refuse unless the backup is a full one and, on a live instance, carries this instance's id. A fresh instance (no records, at most the restoring account) takes any full backup and adopts its id. - Clear first: search vectors and the CSS consumer map (derived); every backed-up table nothing outside the backup points at, whole; the settings the backup carries; and the identity-table rows the backup lacks. A share granted by a removed user goes before the user. - Write the backup through the same _build_* helpers as the copy-in restore, now _load_v2(keep_ids=...). Identity tables (users, projects, milestones, notes, rulebooks, topics, rules, systems, design systems, code shapes, family decisions) go through _place. That keeps the backup's id and merges, so API keys, shares and forge connections of a surviving row stay. - Reset id sequences past the restored max, and sign every session out. reindex_after_restore runs the four embedding backfills afterwards. A unit guard checks that no row kept for outside references cascades from a table replaced whole. Integration tests cover edits, deletions and new rows put back, a surviving API key, sessions ending, fresh ids afterwards, foreign or id-less backups refused, a user export refused, a failure part-way changing nothing, and a fresh instance adopting the id. Milestone 582, step 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -685,3 +685,41 @@ def test_rule_rows_carry_the_kind_so_a_preference_does_not_restore_as_a_rule():
|
||||
arose_from_id=None, created_at=stamp, updated_at=stamp,
|
||||
)
|
||||
assert backup._rule_rows([row])[0]["kind"] == "preference"
|
||||
|
||||
|
||||
def test_a_rollback_never_cascades_into_a_row_it_keeps_for_outside_references():
|
||||
"""A rollback replaces most backed-up tables whole, and keeps an identity
|
||||
table's rows in place so what points at them from OUTSIDE the backup (API
|
||||
keys, shares, forge connections) survives. If such a kept row had an
|
||||
ON DELETE CASCADE foreign key into a table replaced whole, clearing that
|
||||
table would delete the kept row, and its keys and shares with it."""
|
||||
from grimoire.models import Base
|
||||
|
||||
tables = Base.metadata.tables
|
||||
backed_up = set(backup._BACKED_UP)
|
||||
referenced_from_outside = {
|
||||
fk.column.table.name
|
||||
for table in Base.metadata.sorted_tables if table.name not in backed_up
|
||||
for fk in table.foreign_keys if fk.column.table.name in backed_up
|
||||
}
|
||||
assert {"users", "projects", "notes"} <= referenced_from_outside
|
||||
assert referenced_from_outside <= set(backup._IDENTITY_TABLES)
|
||||
safe_targets = set(backup._IDENTITY_TABLES) | set(backup._CATALOG_TABLES) | (
|
||||
set(tables) - backed_up
|
||||
)
|
||||
unsafe = sorted(
|
||||
f"{name}.{fk.parent.name} -> {fk.column.table.name}"
|
||||
for name in referenced_from_outside
|
||||
for fk in tables[name].foreign_keys
|
||||
if (fk.ondelete or "").upper() == "CASCADE" and fk.column.table.name not in safe_targets
|
||||
)
|
||||
assert unsafe == []
|
||||
|
||||
|
||||
def test_every_rollback_table_list_names_real_backed_up_tables():
|
||||
from grimoire.models import Base
|
||||
|
||||
assert set(backup._IDENTITY_TABLES) <= set(backup._BACKED_UP)
|
||||
assert set(backup._CATALOG_TABLES) <= set(backup._BACKED_UP)
|
||||
assert set(backup._DERIVED_TABLES) <= set(backup._NOT_INCLUDED)
|
||||
assert set(backup._DERIVED_TABLES) <= set(Base.metadata.tables)
|
||||
|
||||
Reference in New Issue
Block a user