206a780494
plan:
```
What step 5 is actually doing
Goal: stop writing to storage.files.user_id / storage.folders.user_id on new INSERTs. This is a prerequisite for step 6 (dropping the column entirely). The column has existed since the pre-D0 single-owner era; every read that used to key on it has already been migrated to drive-membership grants over the last N days.
What has to change together for this to be safe:
┌──────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Piece │ What changes │ Why │
├──────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Migration │ Swap storage.files uniqueness indexes from (folder_id, name, user_id) to (drive_id, folder_id, name) │ Otherwise, two new rows with user_id = NULL would both be allowed (PG treats NULLs as distinct) — │
│ (a) │ │ uniqueness silently breaks │
├──────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Migration │ ALTER user_id DROP NOT NULL on both tables │ Otherwise, dropping the INSERT bind violates NOT NULL and every write 500s │
│ (b) │ │ │
├──────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Migration │ Drop dead user_id-leading indexes │ Cheap cleanup — nothing scans them anymore │
│ (c) │ │ │
├──────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ PL/pgSQL (d) │ Rewrite storage.copy_folder_tree without user_id in the INSERT column list │ Cross-drive copy runs entirely in SQL, needs the same treatment │
├──────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Rust (e) │ ~12 INSERT column-list drops across folder_db_repository, file_blob_write_repository, drive_pg_repository, dedup_service, │ Actual write path │
│ │ folder_service, load-seed.rs │ │
├──────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Rust (f) │ Simplify folder_db_repository::create_folder's parent lookup to only fetch drive_id (was fetching (user_id, drive_id)) │ It's fetching a value it no longer needs │
└──────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────┘
```
171 lines
6.6 KiB
PL/PgSQL
171 lines
6.6 KiB
PL/PgSQL
-- ─────────────────────────────────────────────────────────────────────────
|
|
-- D7 step 5 — drop `user_id` from `storage.copy_folder_tree` INSERTs.
|
|
--
|
|
-- Companion to `20260902000000_files_folders_user_id_nullable.sql`. That
|
|
-- migration made both `storage.files.user_id` and `storage.folders.user_id`
|
|
-- nullable; this one stops writing to them from the copy-tree flow so
|
|
-- copied rows leave the column NULL — provenance moves entirely to the
|
|
-- `created_by` / `updated_by` §14 columns, which the PL/pgSQL already
|
|
-- preserved from source.
|
|
--
|
|
-- No behavioural change apart from the write-time projection: reads no
|
|
-- longer key on `files.user_id` (all migrated to drive-membership
|
|
-- predicates), the uniqueness constraints don't include `user_id`
|
|
-- (companion migration swapped them to drive-scoped), and provenance
|
|
-- was already flowing through `created_by` / `updated_by`.
|
|
--
|
|
-- Identical function signature and return shape — no caller update
|
|
-- needed.
|
|
|
|
CREATE OR REPLACE FUNCTION storage.copy_folder_tree(
|
|
p_source_id UUID,
|
|
p_target_parent_id UUID, -- NULL = copy to root (keeps source drive)
|
|
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;
|
|
v_dest_drive_id 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;
|
|
|
|
-- Resolve destination drive_id once up front (cross-drive copy path).
|
|
IF p_target_parent_id IS NULL THEN
|
|
SELECT fo.drive_id INTO v_dest_drive_id
|
|
FROM storage.folders fo
|
|
WHERE fo.id = p_source_id;
|
|
ELSE
|
|
SELECT fo.drive_id INTO v_dest_drive_id
|
|
FROM storage.folders fo
|
|
WHERE fo.id = p_target_parent_id AND NOT fo.is_trashed;
|
|
IF v_dest_drive_id IS NULL THEN
|
|
RAISE EXCEPTION 'Target parent folder not found: %', p_target_parent_id
|
|
USING ERRCODE = 'P0002';
|
|
END IF;
|
|
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;
|
|
|
|
SELECT cm.new_id INTO v_new_root
|
|
FROM _copy_map cm WHERE cm.old_id = p_source_id;
|
|
|
|
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 ──
|
|
-- Post-D7: `user_id` intentionally omitted from the column list so
|
|
-- copied rows leave the (now-nullable) column NULL. Provenance is
|
|
-- carried by `created_by` / `updated_by` (§14 columns) — preserved
|
|
-- from source so authorship survives the copy.
|
|
FOR v_level IN v_root_depth .. v_max_depth LOOP
|
|
INSERT INTO storage.folders(
|
|
id, name, parent_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,
|
|
v_dest_drive_id,
|
|
fo.created_by,
|
|
fo.updated_by
|
|
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;
|
|
|
|
-- Temp mapping for files src→dst (dst ids pre-allocated so we can
|
|
-- reference them in the dead-property duplication below).
|
|
CREATE TEMP TABLE IF NOT EXISTS _copy_file_map(
|
|
old_id UUID PRIMARY KEY,
|
|
new_id UUID NOT NULL DEFAULT gen_random_uuid()
|
|
) ON COMMIT DROP;
|
|
TRUNCATE _copy_file_map;
|
|
|
|
INSERT INTO _copy_file_map(old_id)
|
|
SELECT f.id
|
|
FROM storage.files f
|
|
JOIN _copy_map cm ON f.folder_id = cm.old_id
|
|
WHERE NOT f.is_trashed;
|
|
|
|
-- ── Batch copy all files (zero-copy: same blob_hash) ──
|
|
-- Post-D7: `user_id` omitted. Provenance via `created_by`/`updated_by`.
|
|
INSERT INTO storage.files(
|
|
id, name, folder_id, blob_hash, size, mime_type,
|
|
media_sort_date, drive_id, created_by, updated_by
|
|
)
|
|
SELECT fm.new_id, f.name, cm.new_id, f.blob_hash, f.size,
|
|
f.mime_type, f.media_sort_date, v_dest_drive_id, f.created_by,
|
|
f.updated_by
|
|
FROM storage.files f
|
|
JOIN _copy_map cm ON f.folder_id = cm.old_id
|
|
JOIN _copy_file_map fm ON fm.old_id = f.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;
|
|
|
|
-- Duplicate dead properties per RFC 4918 §8.8 — id-keyed store.
|
|
INSERT INTO storage.webdav_dead_properties
|
|
(folder_id, namespace, local_name, value)
|
|
SELECT cm.new_id, dp.namespace, dp.local_name, dp.value
|
|
FROM storage.webdav_dead_properties dp
|
|
JOIN _copy_map cm ON dp.folder_id = cm.old_id;
|
|
|
|
INSERT INTO storage.webdav_dead_properties
|
|
(file_id, namespace, local_name, value)
|
|
SELECT fm.new_id, dp.namespace, dp.local_name, dp.value
|
|
FROM storage.webdav_dead_properties dp
|
|
JOIN _copy_file_map fm ON dp.file_id = fm.old_id;
|
|
|
|
RETURN QUERY SELECT v_new_root::text, v_folders, v_files;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|