feat(drive): plan: ensure no root folder without drive
This commit is contained in:
@@ -394,6 +394,47 @@ visibility, not the CTE shared snapshot). FK timing works without
|
|||||||
`DEFERRABLE`: each FK is satisfied at the moment its row is written
|
`DEFERRABLE`: each FK is satisfied at the moment its row is written
|
||||||
because the referenced rows already exist.
|
because the referenced rows already exist.
|
||||||
|
|
||||||
|
#### DB-level invariant: no orphan root folder
|
||||||
|
|
||||||
|
`storage.drives.root_folder_id` is NULLable at the column level
|
||||||
|
(required by the four-write transaction above — the drive INSERT
|
||||||
|
can't reference a folder that doesn't exist yet). The "every drive
|
||||||
|
has a root folder" invariant is application-enforced by the atomic
|
||||||
|
creation transaction being the only mint path. But the symmetric
|
||||||
|
invariant — "every root folder belongs to a drive" — is *not* free
|
||||||
|
either: a `storage.folders` row with `parent_id IS NULL` whose
|
||||||
|
`drive_id` doesn't point at a drive whose `root_folder_id` equals
|
||||||
|
its `id` would be an orphan that the resolver can't reach (the
|
||||||
|
chroot can't land on it) but that still occupies the
|
||||||
|
`(parent_id IS NULL, name, drive_id)` unique slot.
|
||||||
|
|
||||||
|
D0 lands a DB-level guard for this case, as a defence-in-depth
|
||||||
|
layer behind the application invariant. Implementation: an
|
||||||
|
`AFTER INSERT OR UPDATE` constraint trigger on
|
||||||
|
`storage.folders` that fires when `NEW.parent_id IS NULL` and
|
||||||
|
refuses unless:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM storage.drives
|
||||||
|
WHERE id = NEW.drive_id
|
||||||
|
AND root_folder_id = NEW.id
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Declared `DEFERRABLE INITIALLY DEFERRED` so the four-write
|
||||||
|
transaction's order (folder INSERTed at step 2, drive's
|
||||||
|
`root_folder_id` UPDATEd at step 3) doesn't trip the trigger
|
||||||
|
mid-transaction — the check fires at COMMIT, by which point both
|
||||||
|
sides of the cycle are wired.
|
||||||
|
|
||||||
|
Test coverage: a Hurl/integration test that hand-rolls
|
||||||
|
`INSERT INTO storage.folders (… parent_id=NULL, drive_id=<existing>)`
|
||||||
|
*without* the matching drives.root_folder_id update — asserts the
|
||||||
|
trigger raises and the row is rejected. The atomic transaction
|
||||||
|
remains the only legitimate creation path; arbitrary SQL writes
|
||||||
|
that try to bypass it now hit a DB-level wall.
|
||||||
|
|
||||||
#### Capabilities matrix
|
#### Capabilities matrix
|
||||||
|
|
||||||
| Capability | `kind='personal'`, default (`default_for_user` set) | `kind='personal'`, secondary (`default_for_user IS NULL`) | `kind='shared'` |
|
| Capability | `kind='personal'`, default (`default_for_user` set) | `kind='personal'`, secondary (`default_for_user IS NULL`) | `kind='shared'` |
|
||||||
@@ -1242,6 +1283,13 @@ every storage query. We phase it for safety:
|
|||||||
CTE being the only creation path, not by a constraint.
|
CTE being the only creation path, not by a constraint.
|
||||||
Verification step 6 checks the invariant on the populated
|
Verification step 6 checks the invariant on the populated
|
||||||
dataset; ongoing enforcement is application-layer.
|
dataset; ongoing enforcement is application-layer.
|
||||||
|
8. Land the **no-orphan-root-folder constraint trigger** (§3,
|
||||||
|
"DB-level invariant"): `AFTER INSERT OR UPDATE` on
|
||||||
|
`storage.folders`, fires when `NEW.parent_id IS NULL`, refuses
|
||||||
|
unless the matching drive has its `root_folder_id` pointing at
|
||||||
|
the row. `DEFERRABLE INITIALLY DEFERRED` so the atomic
|
||||||
|
four-write transaction commits cleanly. Hurl test asserts the
|
||||||
|
trigger rejects hand-rolled orphan inserts.
|
||||||
|
|
||||||
**Keep `user_id`** on resources alongside `drive_id` for the entire
|
**Keep `user_id`** on resources alongside `drive_id` for the entire
|
||||||
Phase A release cycle. Code is updated to read `drive_id` everywhere;
|
Phase A release cycle. Code is updated to read `drive_id` everywhere;
|
||||||
|
|||||||
Reference in New Issue
Block a user