109 lines
5.1 KiB
SQL
109 lines
5.1 KiB
SQL
|
|
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
|
-- D0 / M3 — Drive constraints: NOT NULL, FK, indexes
|
||
|
|
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
|
-- Third of the D0 migration trio. Runs only after M2 (the backfill) has
|
||
|
|
-- populated every folder/file row with a `drive_id`. This migration is
|
||
|
|
-- the point of no easy rollback — once `drive_id` is NOT NULL and
|
||
|
|
-- foreign-keyed to `storage.drives`, dropping the column requires the
|
||
|
|
-- application code to first stop reading it.
|
||
|
|
--
|
||
|
|
-- What lands here:
|
||
|
|
-- * NOT NULL on `storage.folders.drive_id` and `storage.files.drive_id`.
|
||
|
|
-- * Foreign keys from both to `storage.drives(id)` with ON DELETE
|
||
|
|
-- CASCADE (deleting a drive removes its tree — matches the
|
||
|
|
-- post-D2 lifecycle plan).
|
||
|
|
-- * Indexes on `drive_id` for both tables (hot path: list-by-drive,
|
||
|
|
-- drive-aware Tantivy reseed, drive-quota counters).
|
||
|
|
--
|
||
|
|
-- The `user_id` column is intentionally left in place: dual-write during
|
||
|
|
-- the D0 release cycle is the rollback safety net. D7 drops user_id once
|
||
|
|
-- the new model has baked.
|
||
|
|
|
||
|
|
-- ── 1. NOT NULL on drive_id ────────────────────────────────────────────────
|
||
|
|
-- M2's post-flight check refused to commit if any row was missing
|
||
|
|
-- drive_id, so this should never fail. The check at column promotion
|
||
|
|
-- time is the belt; M2's pre-commit assertion was the suspenders.
|
||
|
|
|
||
|
|
ALTER TABLE storage.folders
|
||
|
|
ALTER COLUMN drive_id SET NOT NULL;
|
||
|
|
|
||
|
|
ALTER TABLE storage.files
|
||
|
|
ALTER COLUMN drive_id SET NOT NULL;
|
||
|
|
|
||
|
|
|
||
|
|
-- ── 2. Foreign keys to storage.drives ──────────────────────────────────────
|
||
|
|
-- ON DELETE CASCADE: when a drive is deleted (D3 ships the delete-drive
|
||
|
|
-- flow), every folder and file row carrying that drive_id is removed in
|
||
|
|
-- the same transaction. Trash retention does not apply — drive deletion
|
||
|
|
-- is the explicit "I'm done with this storage" gesture.
|
||
|
|
|
||
|
|
ALTER TABLE storage.folders
|
||
|
|
ADD CONSTRAINT folders_drive_id_fkey
|
||
|
|
FOREIGN KEY (drive_id) REFERENCES storage.drives(id) ON DELETE CASCADE;
|
||
|
|
|
||
|
|
ALTER TABLE storage.files
|
||
|
|
ADD CONSTRAINT files_drive_id_fkey
|
||
|
|
FOREIGN KEY (drive_id) REFERENCES storage.drives(id) ON DELETE CASCADE;
|
||
|
|
|
||
|
|
|
||
|
|
-- ── 3. Indexes on drive_id ─────────────────────────────────────────────────
|
||
|
|
-- The hot path that ranks every drive-aware query: "list folders in
|
||
|
|
-- drive X", "files in drive X for Tantivy reindex", "per-drive quota
|
||
|
|
-- aggregation". The existing `user_id` indexes are kept during dual-
|
||
|
|
-- write and dropped in D7 alongside the column.
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_folders_drive_id ON storage.folders (drive_id);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_files_drive_id ON storage.files (drive_id);
|
||
|
|
|
||
|
|
|
||
|
|
-- ── 4. Post-flight: confirm constraints landed ────────────────────────────
|
||
|
|
-- Belt-and-suspenders verification that the NOT NULL + FK actually
|
||
|
|
-- exist after the ALTERs above. Any failure here means PostgreSQL
|
||
|
|
-- silently no-op'd one of the constraint changes, which would be a
|
||
|
|
-- bug worth surfacing immediately.
|
||
|
|
|
||
|
|
DO $BODY$
|
||
|
|
DECLARE
|
||
|
|
folder_not_null BOOLEAN;
|
||
|
|
file_not_null BOOLEAN;
|
||
|
|
folder_fk_exists BOOLEAN;
|
||
|
|
file_fk_exists BOOLEAN;
|
||
|
|
BEGIN
|
||
|
|
SELECT NOT is_nullable::boolean INTO folder_not_null
|
||
|
|
FROM information_schema.columns
|
||
|
|
WHERE table_schema = 'storage'
|
||
|
|
AND table_name = 'folders'
|
||
|
|
AND column_name = 'drive_id';
|
||
|
|
|
||
|
|
SELECT NOT is_nullable::boolean INTO file_not_null
|
||
|
|
FROM information_schema.columns
|
||
|
|
WHERE table_schema = 'storage'
|
||
|
|
AND table_name = 'files'
|
||
|
|
AND column_name = 'drive_id';
|
||
|
|
|
||
|
|
SELECT EXISTS (
|
||
|
|
SELECT 1 FROM information_schema.table_constraints
|
||
|
|
WHERE table_schema = 'storage'
|
||
|
|
AND table_name = 'folders'
|
||
|
|
AND constraint_name = 'folders_drive_id_fkey'
|
||
|
|
AND constraint_type = 'FOREIGN KEY'
|
||
|
|
) INTO folder_fk_exists;
|
||
|
|
|
||
|
|
SELECT EXISTS (
|
||
|
|
SELECT 1 FROM information_schema.table_constraints
|
||
|
|
WHERE table_schema = 'storage'
|
||
|
|
AND table_name = 'files'
|
||
|
|
AND constraint_name = 'files_drive_id_fkey'
|
||
|
|
AND constraint_type = 'FOREIGN KEY'
|
||
|
|
) INTO file_fk_exists;
|
||
|
|
|
||
|
|
IF NOT folder_not_null OR NOT file_not_null
|
||
|
|
OR NOT folder_fk_exists OR NOT file_fk_exists THEN
|
||
|
|
RAISE EXCEPTION
|
||
|
|
'D0 M3 post-flight failed: '
|
||
|
|
'folder NOT NULL=%, file NOT NULL=%, folder FK=%, file FK=%. '
|
||
|
|
'All four must be true after this migration commits.',
|
||
|
|
folder_not_null, file_not_null, folder_fk_exists, file_fk_exists;
|
||
|
|
END IF;
|
||
|
|
END $BODY$;
|