feat(pg): Work projection, reads and create on the facade (L-W-1 2/4)
Requested by Nikola · project thread
Summary
Slice L-W-1, PR 2 of 4, on top of the Works schema from #770. The running app still uses SQLite only.
Before: the PostgreSQL facade had no Works methods. Any Work call on it raised NotPortedError.
After: the facade creates and reads Works and returns the same JSON as SQLite. Fifteen methods are @ported with their PRKSDatabase signatures:
- create:
add_work,generate_id; - reads:
get_work,get_all_works,get_works_browse_catalog,get_recent_browse,get_recently_added_browse,get_recent_works,get_recently_added_works,get_work_summaries_by_ids_ordered,get_primary_thumbnail_fields; - ETags:
etag_for_representation,etag_works_catalog,etag_recent_works,etag_recently_added_works.
How:
backend/domain/works/projection.pybuilds today's Work JSON from the Work, its primary Manifestation and that Manifestation's primary Asset. It keeps SQLite's key order, row ordering and timestamp text. Manifestation and active-Asset counts come fromGROUP BY work_idderived tables, not per-row subqueries.queries.pyruns insidelibrary_query, a REPEATABLE READ READ ONLY snapshot, so a multi-statement projection never mixes pre- and post-write state.commands.pyruns insidelibrary_command, which takes the Library maintenance lock, authorizes, and checks the epoch.- Create is SERIALIZABLE. It inserts the Work, the primary Manifestation, the Asset for a source and the notes in one transaction, so the deferred pointer keys are checked at COMMIT.
- Create checks that the bound actor holds
EDIT_LIBRARYbefore it looks at or hashes a managed file, so a caller who may not edit can't learn whether a storage key exists. The command transaction re-authorizes and checks the epoch. - Create refuses a managed PDF whose storage key an Asset of another Library already names (one data root serves every Library, §6.2). The check runs in the SERIALIZABLE transaction, so two Libraries racing for one key cannot both commit. The full §11.2 rule stays with L-A-1.
backend/pg_facade/context.pybinds theRequestContexta facade call acts for through a context variable, soPRKSDatabasesignatures andserver.pystay as they are. A call with no bound context is refused, never run as an implicit owner. The parity fixture binds a signed-in owner of a fresh Library.
Related issues
Scope / non-goals
- Reads and create only. Mutations (open, metadata, notes, source, delete, with §7.2 revision locking) are PR 3. The sync handlers (CREATE_WORK, DELETE_WORK, MARK_WORK_OPENED, SET_WORK_METADATA_FIELD, SET_WORK_SOURCE, notes) are PR 4.
- No
server.pychange. No job is enqueued. People, Organization and Research files are untouched. - Roles, tags, folders and playlists appear in the Work JSON as empty until their lanes port them. Any test that creates them reaches an unported method, so it is skipped and never counted as parity.
- Documented PostgreSQL differences, each pinned by a test:
- A managed file must exist, because create hashes it.
- A video with a file is refused, because a stream stores no bytes.
- A kind-only pdf with no file reads
source_kindNone. - Source presentation fields need an Asset.
- A managed PDF another Library's Asset names is refused.
Validation
Local, PostgreSQL 18 cluster, on head 2612b3c (master 79a7ad0 merged in):
scripts/test-postgres(all oftests/pg): 550 tests OK.scripts/test-postgres --parity: OK.tests.test_works_core_parity,tests.test_works_projection_shape: OK. Each ported method has a direct parity test against a SQLitePRKSDatabase.tests/pg/test_works_facade.pycovers refusal without an actor or with the wrong actor, refusal before any managed file is touched (existing and missing keys, unbound and reader callers), two-Library isolation, the cross-Library PDF refusal and race, the rows create writes and their pointer keys, timestamp precision and ordering, and concurrent creates.scripts/check_pg_schema.py: matches the snapshot (no schema change).scripts/check_invariants.py: OK.scripts/check_correlated_catalog_counts.pyagainst master: OK.scripts/check_schema_change.py: OK.ruff check --config ruff.toml prks_app.py backend: clean.- Pyright:
pyrightconfig.typed-slice.json0 errors.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.
CI on 2612b3c: every GitHub Actions job passed, including the Full E2E Gate (all six shards and pointer capture), PostgreSQL helper tests, backend parity, unit/API/contract, Pyright, Ruff, ESLint, Vue, CodeQL and the workflow audits.
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 intests/test_engineering_invariants.py, plusAGENTS.md. This branch already carries both lanes' entries from master. #777 touches the adjacentAGENTS.mdlane line, so whichever of the two merges second needs that one line resolved. tests/backend_parity.pygains the session-context binding for thepostgresleg. L-J-1 does not edit it.tests/test_pg_facade_registry.pynow uses a bare Library stand-in for its synthetic registrations, becausePostgresLibrarynow has real Works methods.- PRs 3 and 4 are built and tested locally on top of this branch. They open after this one.
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