134 lines
5.4 KiB
PL/PgSQL
134 lines
5.4 KiB
PL/PgSQL
-- ════════════════════════════════════════════════════════════════════════════
|
|
-- D0 / M5 — storage.copy_folder_tree drive_id + provenance
|
|
-- ════════════════════════════════════════════════════════════════════════════
|
|
-- The `copy_folder_tree` SQL function (initial_schema.sql) batches a
|
|
-- recursive folder copy in PL/pgSQL — its own INSERTs into
|
|
-- `storage.folders` and `storage.files`. D0's M3 made `drive_id` NOT
|
|
-- NULL on both tables; the function's pre-D0 body doesn't write it,
|
|
-- so any `/api/batch/folders/copy` call errors with "null value in
|
|
-- column drive_id" until this migration lands.
|
|
--
|
|
-- The replacement preserves every other semantic of the original:
|
|
-- - level-by-level INSERTs so the BEFORE INSERT trigger
|
|
-- (`trg_folders_path`) can resolve the parent's path/lpath from
|
|
-- rows inserted in the previous level.
|
|
-- - One batched file INSERT (zero-copy via blob hash) at the end.
|
|
-- - Returns the same shape: (new_root_id::text, folders_copied,
|
|
-- files_copied).
|
|
--
|
|
-- New columns written:
|
|
-- - drive_id: pulled from the source row (intra-drive copy — cross-
|
|
-- drive copies are a D2+ feature; the function preserves the
|
|
-- source's drive_id for both folders and files).
|
|
-- - created_by / updated_by: set to the source row's user_id,
|
|
-- matching the dual-write convention used by the Rust repos.
|
|
|
|
CREATE OR REPLACE FUNCTION storage.copy_folder_tree(
|
|
p_source_id UUID,
|
|
p_target_parent_id UUID, -- NULL = copy to root
|
|
p_dest_name TEXT DEFAULT NULL -- NULL = keep source folder name
|
|
) RETURNS TABLE(new_root_id TEXT, folders_copied BIGINT, files_copied BIGINT) AS $$
|
|
DECLARE
|
|
v_root_lpath ltree;
|
|
v_root_depth INT;
|
|
v_max_depth INT;
|
|
v_level INT;
|
|
v_folders BIGINT := 0;
|
|
v_files BIGINT := 0;
|
|
v_inserted BIGINT;
|
|
v_new_root UUID;
|
|
BEGIN
|
|
-- Validate source exists
|
|
SELECT fo.lpath, nlevel(fo.lpath)
|
|
INTO v_root_lpath, v_root_depth
|
|
FROM storage.folders fo
|
|
WHERE fo.id = p_source_id AND NOT fo.is_trashed;
|
|
|
|
IF v_root_lpath IS NULL THEN
|
|
RAISE EXCEPTION 'Source folder not found: %', p_source_id
|
|
USING ERRCODE = 'P0002'; -- no_data_found
|
|
END IF;
|
|
|
|
-- Temp mapping: every folder in the subtree → new UUID
|
|
CREATE TEMP TABLE IF NOT EXISTS _copy_map(
|
|
old_id UUID PRIMARY KEY,
|
|
new_id UUID NOT NULL DEFAULT gen_random_uuid()
|
|
) ON COMMIT DROP;
|
|
TRUNCATE _copy_map;
|
|
|
|
INSERT INTO _copy_map(old_id)
|
|
SELECT fo.id
|
|
FROM storage.folders fo
|
|
WHERE NOT fo.is_trashed
|
|
AND fo.lpath <@ v_root_lpath;
|
|
|
|
-- Remember new root ID
|
|
SELECT cm.new_id INTO v_new_root
|
|
FROM _copy_map cm WHERE cm.old_id = p_source_id;
|
|
|
|
-- Max depth for level iteration
|
|
SELECT MAX(nlevel(fo.lpath))
|
|
INTO v_max_depth
|
|
FROM storage.folders fo
|
|
JOIN _copy_map cm ON fo.id = cm.old_id;
|
|
|
|
-- ── Insert folders level by level ──
|
|
-- Each level is a separate INSERT so the BEFORE INSERT trigger
|
|
-- (trg_folders_path) can resolve the parent's path/lpath from rows
|
|
-- inserted in the previous level. drive_id + provenance threaded
|
|
-- through from the source row at each level.
|
|
FOR v_level IN v_root_depth .. v_max_depth LOOP
|
|
INSERT INTO storage.folders(
|
|
id, name, parent_id, user_id,
|
|
drive_id, created_by, updated_by
|
|
)
|
|
SELECT cm.new_id,
|
|
CASE WHEN fo.id = p_source_id AND p_dest_name IS NOT NULL
|
|
THEN p_dest_name ELSE fo.name END,
|
|
CASE WHEN fo.id = p_source_id THEN p_target_parent_id
|
|
ELSE pm.new_id END,
|
|
fo.user_id,
|
|
fo.drive_id,
|
|
fo.user_id,
|
|
fo.user_id
|
|
FROM storage.folders fo
|
|
JOIN _copy_map cm ON fo.id = cm.old_id
|
|
LEFT JOIN _copy_map pm ON fo.parent_id = pm.old_id
|
|
WHERE NOT fo.is_trashed
|
|
AND nlevel(fo.lpath) = v_level;
|
|
|
|
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
|
v_folders := v_folders + v_inserted;
|
|
END LOOP;
|
|
|
|
-- ── Batch copy all files (zero-copy: same blob_hash) ──
|
|
INSERT INTO storage.files(
|
|
name, folder_id, user_id, blob_hash, size, mime_type,
|
|
media_sort_date, drive_id, created_by, updated_by
|
|
)
|
|
SELECT f.name, cm.new_id, f.user_id, f.blob_hash, f.size, f.mime_type,
|
|
f.media_sort_date, f.drive_id, f.user_id, f.user_id
|
|
FROM storage.files f
|
|
JOIN _copy_map cm ON f.folder_id = cm.old_id
|
|
WHERE NOT f.is_trashed;
|
|
|
|
GET DIAGNOSTICS v_files = ROW_COUNT;
|
|
|
|
-- ── Batch increment blob ref_counts ──
|
|
IF v_files > 0 THEN
|
|
UPDATE storage.blobs b
|
|
SET ref_count = ref_count + hc.cnt
|
|
FROM (
|
|
SELECT f.blob_hash, COUNT(*)::int AS cnt
|
|
FROM storage.files f
|
|
JOIN _copy_map cm ON f.folder_id = cm.new_id
|
|
WHERE NOT f.is_trashed
|
|
GROUP BY f.blob_hash
|
|
) hc
|
|
WHERE b.hash = hc.blob_hash;
|
|
END IF;
|
|
|
|
RETURN QUERY SELECT v_new_root::text, v_folders, v_files;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|