4200209d4a
Replace the per-row tree_modified_at bump triggers with AFTER ... FOR EACH STATEMENT triggers using transition tables: each DML statement now pays one bump covering the distinct ancestor chains of all affected rows (locked in id order so concurrent bumps over overlapping chains cannot deadlock), instead of one chain UPDATE per affected row. Value-based change detection replaces UPDATE OF column lists (PG forbids those with transition tables), so EXIF media_sort_date syncs and no-op updates no longer bump at all, and file moves now invalidate the source chain as well as the destination. Also fix a latent bug surfaced while testing this: the descendant path/lpath cascade (trg_folders_cascade_path) was declared AFTER UPDATE OF path, lpath, but rename/move statements SET name/parent_id, and BEFORE-trigger rewrites do not count for UPDATE OF — the cascade never fired, leaving every descendant folder with a stale path/lpath after any rename or move. The migration canonically repairs existing trees and re-creates the cascade on the columns the app actually writes. delete_expired_bulk now deletes in LIMIT-ed batches (1000 files / 100 folders per round, each its own implicit transaction) ordered by trashed_at and served by new partial indexes over trashed rows, so retention purges no longer hold one unbounded transaction. Verified against a local PG16 cluster: all migrations apply from scratch, the new one re-runs idempotently, and a 16-case behavioral battery passes (chain bumps for insert/update/delete/move/trash, EXIF invisibility, no-op invisibility, cascade repair, FK-cascade depth guards, batched purge shape and partial-index plan). https://claude.ai/code/session_01QxwJDHqQhbMkHK333QtMme