feat(drive): ensure drive_id updated on file|folder moved to another drive
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
-- D6: cross-drive folder moves must propagate `drive_id` to the moved
|
||||
-- folder's subtree (descendant folders + files), not just `lpath`.
|
||||
--
|
||||
-- Today's `cascade_folder_path()` trigger only rewrites `path` + `lpath`
|
||||
-- on descendants — it leaves `drive_id` untouched. That worked when
|
||||
-- moves were intra-drive (drive_id never changed), but after D5 the
|
||||
-- `forbid_cross_drive_move` policy gate exposed the gap: a successful
|
||||
-- cross-drive move (gate off OR not yet enforced) leaves the subtree
|
||||
-- in an inconsistent state — lpath rooted in drive B but `drive_id`
|
||||
-- column still drive A on every descendant row. Any drive-id-scoped
|
||||
-- query then returns the wrong drive's content.
|
||||
--
|
||||
-- The fix is to extend the cascade trigger so a change in the parent
|
||||
-- folder's `drive_id` (the only thing that changes drive_id during a
|
||||
-- move) cascades to every descendant folder + every descendant file.
|
||||
-- Files cascade too because `storage.files.drive_id` is the canonical
|
||||
-- per-file drive-membership signal (D0 dual-write).
|
||||
--
|
||||
-- Migration is idempotent via `CREATE OR REPLACE FUNCTION`.
|
||||
|
||||
CREATE OR REPLACE FUNCTION storage.cascade_folder_path()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF pg_trigger_depth() > 1 THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF OLD.path IS DISTINCT FROM NEW.path OR OLD.lpath IS DISTINCT FROM NEW.lpath THEN
|
||||
-- Single batch update: rewrite path/lpath for every descendant
|
||||
-- folder at once via the GiST lpath index.
|
||||
UPDATE storage.folders
|
||||
SET path = NEW.path || substr(path, length(OLD.path) + 1),
|
||||
lpath = NEW.lpath || subpath(lpath, nlevel(OLD.lpath))
|
||||
WHERE lpath <@ OLD.lpath
|
||||
AND id != NEW.id;
|
||||
END IF;
|
||||
|
||||
-- D6: cascade `drive_id` to every descendant folder + file when the
|
||||
-- moved row's drive_id has changed (cross-drive move). The GiST
|
||||
-- index covers the folder predicate; `storage.files.drive_id` is
|
||||
-- updated through the folder→file FK relation since files only
|
||||
-- carry `folder_id` directly (drive_id is a denormalised dual-write).
|
||||
--
|
||||
-- Triggered on the column-list `AFTER UPDATE OF path, lpath, drive_id`
|
||||
-- registration below — so this branch only runs when the explicit
|
||||
-- move statement on the moved row sets `drive_id` to a new value.
|
||||
-- The descendant batch UPDATE that fires from the path/lpath branch
|
||||
-- above doesn't touch drive_id, so the trigger doesn't recurse on
|
||||
-- the per-descendant rewrite.
|
||||
IF OLD.drive_id IS DISTINCT FROM NEW.drive_id THEN
|
||||
UPDATE storage.folders
|
||||
SET drive_id = NEW.drive_id
|
||||
WHERE lpath <@ NEW.lpath
|
||||
AND drive_id = OLD.drive_id;
|
||||
|
||||
UPDATE storage.files f
|
||||
SET drive_id = NEW.drive_id
|
||||
FROM storage.folders fo
|
||||
WHERE f.folder_id = fo.id
|
||||
AND fo.lpath <@ NEW.lpath
|
||||
AND f.drive_id = OLD.drive_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Re-register the trigger with `drive_id` added to the column list so the
|
||||
-- trigger fires when a move sets a new drive_id on the moved row. (CREATE
|
||||
-- OR REPLACE TRIGGER replaces the same name in place; no DROP needed.)
|
||||
CREATE OR REPLACE TRIGGER trg_folders_cascade_path
|
||||
AFTER UPDATE OF path, lpath, drive_id ON storage.folders
|
||||
FOR EACH ROW EXECUTE FUNCTION storage.cascade_folder_path();
|
||||
Reference in New Issue
Block a user