ObjectStorageSchema declares a database foreign key for every join and table possible value between the qbit tables, named fk_<table>_<column> (the same names kof22 uses):
CASCADE (observed state owned by its parent): connection/bucket capabilities, folders (and subfolders from their parent folder), objects, versions, references (by bucket), daily usage.
SET NULL (optional pointers into the index): object.folder_id, object_reference.object_id.
RESTRICT / NO ACTION (config and audit): bucket.connection_id, header and protection rules, the operation log.
Why: the qbit never deletes connection, bucket or folder rows (ConfigSync only upserts, folders are only created). Its only deletes are object rows (scan removal, trash-retention purge, hard-delete write-through) and reference rows (each reference scan rebuilds them), all through QQQ deletes with no associations. So an object delete must take its versions and leave references missing, and an admin deleting a bucket row must not silently drop its rules or audit rows. ConfigSync would re-create a configured bucket under a new id with its rules gone.
Each key is added by an idempotent DO block. The block first records orphans in qqq_fk_quarantine (the shared DDL), then clears them (nullable column) or deletes them with the full row recorded (required column). Running the statements again therefore upgrades an existing schema.
New indexes back keys that had none: ix_object_storage_bucket_1 (connection_id), ix_object_storage_header_rule_1, ix_object_storage_protection_rule_1.
Inserts are parents first everywhere, so nothing is deferred.
Builds on #19 (feature/object-id-indexes, the folder_id/object_id indexes). Merge #19 first; this diff includes its commit until then.
Tests
PostgresIndexTest.everyRelationIsAForeignKeyWithAnIndex: the guardrail. Every join and table possible-value field between qbit tables must have a DB foreign key with the declared ON DELETE rule, or an explicit exemption with a reason (none today). Every FK column must also lead an index. Red without the FK statements (lists all 15 relations), green with them.
foreignKeyUpgradeQuarantinesOrphansAndIsIdempotent: drops the keys, adds orphans and runs the statements twice. The orphans are quarantined once (NULLED and DELETED with the row) and all keys exist afterwards.
deletesFollowTheForeignKeyRules: scan removal cascades versions and sets references null; a bucket row with operation history cannot be deleted.
./mvnw verify green locally (130 tests, guardrails pass).
## Summary
- `ObjectStorageSchema` declares a database foreign key for every join and table possible value between the qbit tables, named `fk_<table>_<column>` (the same names kof22 uses):
- **CASCADE** (observed state owned by its parent): connection/bucket capabilities, folders (and subfolders from their parent folder), objects, versions, references (by bucket), daily usage.
- **SET NULL** (optional pointers into the index): `object.folder_id`, `object_reference.object_id`.
- **RESTRICT / NO ACTION** (config and audit): `bucket.connection_id`, header and protection rules, the operation log.
- Why: the qbit never deletes connection, bucket or folder rows (ConfigSync only upserts, folders are only created). Its only deletes are object rows (scan removal, trash-retention purge, hard-delete write-through) and reference rows (each reference scan rebuilds them), all through QQQ deletes with no associations. So an object delete must take its versions and leave references missing, and an admin deleting a bucket row must not silently drop its rules or audit rows. ConfigSync would re-create a configured bucket under a new id with its rules gone.
- Each key is added by an idempotent `DO` block. The block first records orphans in `qqq_fk_quarantine` (the shared DDL), then clears them (nullable column) or deletes them with the full row recorded (required column). Running the statements again therefore upgrades an existing schema.
- New indexes back keys that had none: `ix_object_storage_bucket_1` (connection_id), `ix_object_storage_header_rule_1`, `ix_object_storage_protection_rule_1`.
- Inserts are parents first everywhere, so nothing is deferred.
**Builds on #19** (`feature/object-id-indexes`, the folder_id/object_id indexes). Merge #19 first; this diff includes its commit until then.
## Tests
- `PostgresIndexTest.everyRelationIsAForeignKeyWithAnIndex`: the guardrail. Every join and table possible-value field between qbit tables must have a DB foreign key with the declared ON DELETE rule, or an explicit exemption with a reason (none today). Every FK column must also lead an index. Red without the FK statements (lists all 15 relations), green with them.
- `foreignKeyUpgradeQuarantinesOrphansAndIsIdempotent`: drops the keys, adds orphans and runs the statements twice. The orphans are quarantined once (NULLED and DELETED with the row) and all keys exist afterwards.
- `deletesFollowTheForeignKeyRules`: scan removal cascades versions and sets references null; a bucket row with operation history cannot be deleted.
- `./mvnw verify` green locally (130 tests, guardrails pass).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
ObjectStorageSchemadeclares a database foreign key for every join and table possible value between the qbit tables, namedfk_<table>_<column>(the same names kof22 uses):object.folder_id,object_reference.object_id.bucket.connection_id, header and protection rules, the operation log.DOblock. The block first records orphans inqqq_fk_quarantine(the shared DDL), then clears them (nullable column) or deletes them with the full row recorded (required column). Running the statements again therefore upgrades an existing schema.ix_object_storage_bucket_1(connection_id),ix_object_storage_header_rule_1,ix_object_storage_protection_rule_1.Builds on #19 (
feature/object-id-indexes, the folder_id/object_id indexes). Merge #19 first; this diff includes its commit until then.Tests
PostgresIndexTest.everyRelationIsAForeignKeyWithAnIndex: the guardrail. Every join and table possible-value field between qbit tables must have a DB foreign key with the declared ON DELETE rule, or an explicit exemption with a reason (none today). Every FK column must also lead an index. Red without the FK statements (lists all 15 relations), green with them.foreignKeyUpgradeQuarantinesOrphansAndIsIdempotent: drops the keys, adds orphans and runs the statements twice. The orphans are quarantined once (NULLED and DELETED with the row) and all keys exist afterwards.deletesFollowTheForeignKeyRules: scan removal cascades versions and sets references null; a bucket row with operation history cannot be deleted../mvnw verifygreen locally (130 tests, guardrails pass).🤖 Generated with Claude Code