feat(pg): Works schema with deferred ownership keys (L-W-1 1/4)
Requested by Nikola · project thread
Summary
Slice L-W-1, PR 1 of 4: the Works-family schema on PostgreSQL. Nothing in the running app uses it yet.
Before: the PostgreSQL baseline had no Works tables. On SQLite, Work/Manifestation ownership is guarded by ten triggers, a retirement marker and a CHECK(0) guard table.
After: backend/db/schema/100_works.sql creates all eight Works tables of §6.5: works, manifestations, assets (catalogue only), manifestation_relations, manifestation_identifiers, work_notes, sync_work_lifecycle and duplicate_decisions. With these in place, later 0.8 Works slices add no canonical table. Ownership is enforced only by keys, with no triggers:
- Every row is library-scoped (PG6), and each child reaches its parent through a composite key that includes
library_id. works.primary_manifestation_id(required) andcitation_manifestation_idareDEFERRABLE INITIALLY DEFERREDcomposite FKs tomanifestations (library_id, id, work_id), so they are checked at COMMIT. The same holds formanifestations.primary_asset_idtoassets (library_id, id, manifestation_id), and forassets.supersedes_asset_id(same Manifestation), so a superseding pair moves together, or the link clears and one end moves, in either order.assets (manifestation_id, work_id)follows its Manifestation byON UPDATE CASCADE. Relations have two deferred same-Work endpoint keys.- Per §6.4 there is no
origin_work_idand nocontent_generation. Amanaged_fileAsset must carry its storage key, that key'scontent_sha256and anorigin, and no stream identity. Anexternal_streammust name itsprovider,provider_idandurl(non-empty), and records no locator, byte size, hash, origin or provenance. - Revision-owning rows are never cascaded (§5.2). The keys from
manifestations,assetsandwork_notesto their parents areNO ACTION, so the delete command removes each level explicitly (notes, Assets, Manifestations, then the Work) and tombstones its scopes; a delete that skips a level fails at the statement. Onlymanifestation_relationsandmanifestation_identifiers, which own no scope, cascade from their Manifestation. A schema test readspg_constraintand fails if any key onworks,manifestations,assetsorwork_notesisON DELETE CASCADE: L-W-1's share of the §13.2 revision-ownership gate. sync_work_lifecycleandduplicate_decisionshave no entity FK, so they outlive the rows. A Library that still has Works cannot be deleted.- Parent keys for later lanes:
rolesandargument_sourcesreferencemanifestations (library_id, id, work_id), andannotationsreferencesassets (library_id, id, work_id).
backend/domain/works/integrity.py holds the final-state query (§4.1). It covers the two rules no constraint can express: active Assets with no primary Asset, and an inactive primary Asset.
Related issues
Scope / non-goals
- Schema and schema tests only. The projection, commands, facade methods and sync handlers come in PRs 2–4 (plan: projection/reads/create, then mutations, then sync handlers).
manifestation_credit_overridesis not included; it comes with #60 Slice K.- No
server.pychange. No job is enqueued.
Validation
Local, PostgreSQL 18.1 cluster (builtin C.UTF-8 locale), on head 1c1c3b3:
tests/pg/test_works_schema.py: 36 tests passing. These are the #60 §4.1 negative tests as schema tests:- each contradictory pointer, owner or cross-Library row is refused;
- Work-without-primary, pointer-target move and delete, moving a primary Asset without changing the pointer, and moving one end of a supersedes pair each fail at COMMIT (the test asserts every statement ran first);
- legal paths commit: pointer swap then move, moving the only primary Asset out in either order, moving a supersedes pair, and the emptied-Work delete and merge outcomes;
- a failed transition leaves the pointers and owner columns unchanged;
- level-by-level Work delete (and refusal of a delete that skips a level), the no-cascade key check, relation rules, kind/locator/hash/stream-identity checks, ID formats, lifecycle and duplicate-decision checks, and the integrity query.
- Mutation check: with
DEFERRABLEremoved frommanifestations_primary_asset_fkey, 5 of these tests fail. scripts/test-postgres(all oftests/pg): OK.scripts/test-postgres --parity: OK, unchanged at 0 ported.scripts/check_pg_schema.py: the fresh schema matches the regeneratedsnapshot.json.scripts/check_schema_change.py: OK.- Ruff,
scripts/check_invariants.py, Pyright typed slice: clean. - No browser E2E locally: nothing here is reachable from the app.
GitHub CI on 1c1c3b3: all required checks green, including the Full E2E Gate (six shards), PostgreSQL helper tests and backend parity. Codacy reports its usual accepted "action required" items.
Migration / compatibility impact
New PostgreSQL baseline file (pre-1.0, no forward migration). SQLite is unchanged.
Risks / follow-up
- Shared files with L-J-1 (#627):
snapshot.json,pyrightconfig.typed-slice.json, thePYRIGHT_TYPED_SLICE_SCOPESlist inscripts/check_invariants.pyand its pinned copy intests/test_engineering_invariants.py. Whichever PR merges second integrates master and regenerates withscripts/check_pg_schema.py --write. tests/pg/test_migrate.pyno longer assumes040_sync.sqlis the last baseline file. Its three affected tests now derive the last file, and the pinned file list gains100_works.sql.- Choices for review:
supersedes_asset_idis limited to the same Manifestation (#60 §9.1);derived_from_asset_idmay be any Asset of the Library.- Asset
origindropslegacy, since no legacy rows exist. work_notes.bodymay be empty; PR 2's projection decides''vs. no row against the SQLite fixtures.duplicate_decisionsstores(entity_type, low_id, high_id)withlow_id < high_idinCcollation.
Checklist
- Relevant tests/checks were run and are listed above.
- Documentation/generated files were updated when required.
- UI evidence is included when applicable.
- No unrelated changes are intentionally included.
🤖 Generated with Claude Code