feat(pg): Work open, metadata, notes and delete on the facade (L-W-1 3/4)
Requested by Nikola · project thread
Summary
Slice L-W-1, PR 3 of 4, on top of the Work reads and create from #775. The running app still uses SQLite only.
Before: on PostgreSQL a Work could be created and read, but not opened, edited or deleted. Those facade calls raised NotPortedError.
After: the facade records opens, patches metadata and notes, and deletes Works with SQLite's results and revision behavior. Five more methods are @ported, each with a direct parity test: mark_work_opened, update_work_metadata, get_work_metadata_state, get_work_notes_state and delete_work_record.
How:
backend/domain/works/revisions.pyimplements §7.2 revision locking. It ensures the revision row exists, locks itFOR UPDATE, and advances it only when the value changes.metadata.py,notes.pyandsource.pyhold the field, note and source decision tables. They import the SQLite sync modules' validation, codecs and conflict shapes rather than copying them. The sync handlers in PR 4 call these commands.- Every command locks the Work row first, so an edit and a delete of one Work queue behind each other instead of deadlocking.
- Delete removes the Work's notes, Assets, Manifestations and the Work level by level, because the keys are NO ACTION (§5.2). It tombstones every revision scope of the Work and reports whether another Asset of the same Library still names the managed file. It never touches the file on disk: claiming and removing bytes belongs to L-A-1 (§11.2).
- An open goes through a max-register at server time, so concurrent opens converge to the latest.
Related issues
Scope / non-goals
- Mutations only. The sync handlers (CREATE_WORK, DELETE_WORK, MARK_WORK_OPENED, SET_WORK_METADATA_FIELD, SET_WORK_SOURCE and the note operations) are PR 4.
- No
server.pychange and no schema change. People, Organization and Research files are untouched. - Documented PostgreSQL differences, each pinned by a test:
update_work_metadata(file_path=...)raisesNotPortedError. Adopting or replacing a managed file belongs to the objects lane.text_contentthrough a patch is written as the Research Note with its revision. SQLite writes the column without one.- An Asset field (
thumb_page,source_mime) on a Work with no file or stream is refused. delete_work_recordreturns the path but never claims or removes the file.
Validation
Local, PostgreSQL 18 cluster, on master 2aad8e1 plus this branch:
scripts/test-postgres(all oftests/pg): 596 tests OK.scripts/test-postgres --parity: OK.tests/pg/test_works_mutations.pycovers:- every decision-table row for fields, notes and sources;
- concurrent writers of one scope: one is acknowledged and the rest get
REVISION_CONFLICT; - writers of different fields never blocking each other out;
- concurrent opens converging to the latest;
- delete and edit ordering in both directions;
- the tombstones a delete leaves;
- another Library being unable to touch a Work;
- a reader being refused every change.
tests/test_works_mutations_parity.py,tests/test_works_core_parity.py,tests/test_works_projection_shape.py: OK. Shared parity setup moved intotests/works_parity_helpers.py.scripts/check_pg_schema.py: matches the snapshot.scripts/check_invariants.py,scripts/check_correlated_catalog_counts.py --base origin/master,scripts/check_schema_change.py: OK.ruff check --config ruff.toml prks_app.py backendand the changed tests: clean.- Pyright:
pyrightconfig.typed-slice.json0 errors, now including the four new Works modules.pyrightconfig.json0 errors; its 3 warnings are the unresolvedpymupdfimport in this environment. tests.test_engineering_invariants,tests.test_pg_facade_registry: OK.- No browser E2E locally: nothing here is reachable from the app.
Migration / compatibility impact
None. No schema change, and SQLite behavior is unchanged.
Risks / follow-up
- Shared files with L-J-1 (#627):
pyrightconfig.typed-slice.json,PYRIGHT_TYPED_SLICE_SCOPESinscripts/check_invariants.pyand its pinned copy, andAGENTS.md. The new Works entries sit before the Jobs entries, and both lanes' lines are kept. ActorTests.reader_contextmoved to the sharedWorksFacadeTestCaseso the mutation tests can use it.- PR 4 (sync handlers) is built locally on top of this branch and opens after this one merges.
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 by Claude Code
Summary by cubic
On PostgreSQL, Works could be created and read but not opened, edited, or deleted; those calls raised NotPortedError. This PR ports the five mutation facade methods — mark_work_opened, update_work_metadata, get_work_metadata_state, get_work_notes_state, and delete_work_record — with parity tests. The running app still uses SQLite; the sync handlers land in PR 4.
Behavior and implementation notes
- Field, note, and source decision tables import the SQLite sync modules' validation, codecs, and conflict shapes; revision locking moves to per-scope
FOR UPDATErows (§7.2), advancing only when a value changes. Each field is stored on the Work, primary Manifestation, or primary Asset row that owns it. - Every command locks the Work row first, then reads the Manifestation and Asset in a fresh statement, so a command queued behind a concurrent writer decides on the values that writer committed. An edit and a delete of one Work queue behind each other instead of deadlocking.
- Opens go through a server-time max-register, so concurrent opens converge to the latest.
- Delete removes the Work's notes, Assets, Manifestations, and Work level by level, tombstones every revision scope of the Work, and reports whether another Asset of the Library still names the managed file; it never touches the file on disk.
update_work_metadata(file_path=...)raisesNotPortedError(managed files belong to the objects lane),text_contentis written as the Research Note with a revision, and an Asset field on a Work with no file or stream is refused.- No schema change; SQLite behavior is unchanged.