fix(595): permit unlimited user quota
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Heal + pin the "personal drives always have NULL quota_bytes"
|
||||
-- invariant from docs/plan/drive.md §7.
|
||||
--
|
||||
-- Bug (#595): `folder_service.rs::PersonalDriveLifecycleHook` was
|
||||
-- calling `create_personal_drive_atomic(user_id, Some(user.storage_quota_bytes()))`,
|
||||
-- baking the user's envelope quota into `storage.drives.quota_bytes`
|
||||
-- for every personal drive. Two conventions then collided at upload
|
||||
-- time:
|
||||
--
|
||||
-- * User-envelope check (`check_storage_quota`) treats `0` as
|
||||
-- unlimited (`quota <= 0 → Ok`).
|
||||
-- * Drive-quota check (`check_drive_quota`) treats `NULL` as
|
||||
-- unlimited but `Some(0)` as a literal zero-byte cap.
|
||||
--
|
||||
-- Setting user quota to 0 in the Admin UI ("unlimited" per the UI
|
||||
-- convention) therefore stamped `drives.quota_bytes = 0` on the
|
||||
-- personal drive at creation, and every subsequent upload was
|
||||
-- rejected with 507 Insufficient Storage.
|
||||
--
|
||||
-- Rust-side fix: `folder_service.rs` now passes `None`. This
|
||||
-- migration:
|
||||
--
|
||||
-- 1. NULLs every existing personal drive's `quota_bytes` so already-
|
||||
-- created users can upload immediately after deploy (Fix 2).
|
||||
-- 2. Adds a CHECK constraint so any future code path that tries to
|
||||
-- write a non-NULL quota on a personal drive fails at the DB
|
||||
-- layer instead of silently corrupting state (Fix 3).
|
||||
--
|
||||
-- Shared drives are untouched — their quota model is orthogonal and
|
||||
-- the "NULL = unlimited, positive = numeric cap, 0 = literal zero"
|
||||
-- semantics are the design (an admin can legitimately lock a shared
|
||||
-- drive at 0 bytes, e.g. archive-only).
|
||||
|
||||
-- ── 1. Heal existing personal-drive rows ────────────────────────────────
|
||||
--
|
||||
-- Every row today with `kind = 'personal'` should carry NULL. Set them
|
||||
-- to NULL unconditionally (a personal drive already at NULL is a no-op
|
||||
-- under IS DISTINCT FROM). Idempotent on re-run.
|
||||
UPDATE storage.drives
|
||||
SET quota_bytes = NULL
|
||||
WHERE kind = 'personal'
|
||||
AND quota_bytes IS DISTINCT FROM NULL;
|
||||
|
||||
-- ── 2. Pin the invariant at the schema layer ────────────────────────────
|
||||
--
|
||||
-- Uses `NOT VALID` + `VALIDATE CONSTRAINT` so the ALTER TABLE grabs
|
||||
-- only the fast metadata lock instead of scanning the whole table
|
||||
-- under an ACCESS EXCLUSIVE lock. The row heal above already satisfies
|
||||
-- every existing row, so the subsequent VALIDATE completes without
|
||||
-- error.
|
||||
ALTER TABLE storage.drives
|
||||
ADD CONSTRAINT drives_personal_quota_null
|
||||
CHECK (kind <> 'personal' OR quota_bytes IS NULL)
|
||||
NOT VALID;
|
||||
|
||||
ALTER TABLE storage.drives
|
||||
VALIDATE CONSTRAINT drives_personal_quota_null;
|
||||
|
||||
-- ── 3. Post-flight sanity ───────────────────────────────────────────────
|
||||
--
|
||||
-- Refuse to finish if any personal drive still carries a non-NULL
|
||||
-- quota (defense against a race where a concurrent transaction
|
||||
-- inserted a bad row between the UPDATE and the VALIDATE — the
|
||||
-- VALIDATE would already have failed in that case, but the explicit
|
||||
-- check makes the failure mode obvious in logs).
|
||||
DO $BODY$
|
||||
DECLARE
|
||||
bad BIGINT;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO bad
|
||||
FROM storage.drives
|
||||
WHERE kind = 'personal'
|
||||
AND quota_bytes IS NOT NULL;
|
||||
IF bad > 0 THEN
|
||||
RAISE EXCEPTION
|
||||
'Migration 20260916000000 left % personal drive(s) with a non-NULL quota_bytes',
|
||||
bad;
|
||||
END IF;
|
||||
END;
|
||||
$BODY$;
|
||||
Reference in New Issue
Block a user