Complete PowerSync library persistence and synchronization - #5
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Deletion-time cascading updates/selects in papyrus/services/library_sync.py should be owner-scoped to prevent cross-tenant effects if inconsistent references ever exist.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR expands the PowerSync-backed persistence layer from books-only to the full owned library domain (shelves, tags, notes, annotations, bookmarks, and membership join tables), including schema/migrations, upload semantics (atomic mixed-table batches, ownership validation, tombstones), and PowerSync publication/stream configuration.
Changes:
- Add owned library domain tables + tombstones, promote selected book metadata into first-class columns, and introduce a bookmarks follow-up revision.
- Replace books-only upload handling with a generalized library mutation engine (validation, reference checks, cycle detection, tombstone semantics, and media cleanup).
- Expand PowerSync streams/publication grants and add regression tests for mixed-batch behavior, migration integrity, and offline conflict/deletion semantics.
File summaries
| File | Description |
|---|---|
| tests/test_powersync_sync_config.py | Asserts expanded streams are owner-filtered and setup script publishes the right tables. |
| tests/test_models.py | Verifies new library tables are registered in SQLAlchemy metadata with ownership columns. |
| tests/test_library_migration.py | Exercises the main library revision and validates backfill + metadata matches. |
| tests/test_bookmark_migration.py | Exercises the bookmark revision and validates constraints + metadata matches. |
| tests/services/test_library_validation.py | Validates type conversion, overflow handling, and legacy book payload normalization. |
| tests/api/routes/test_sync.py | Updates contract tests to reflect new supported/unsupported tables. |
| tests/api/routes/test_library_sync.py | Adds end-to-end upload regression tests for library entities and conflict semantics. |
| tests/api/routes/test_bookmark_sync.py | Adds end-to-end upload regression tests for bookmarks and tombstone behavior. |
| scripts/setup_local_powersync.sh | Expands grants/publication to include all new library tables. |
| powersync/sync-config.yaml | Adds owner-filtered streams for new tables and expands the books stream projection. |
| papyrus/services/sync.py | Switches upload service from books-only to mixed library mutation application + per-user serialization. |
| papyrus/services/library_validation.py | New conversion/normalization helpers for validating queued payload values. |
| papyrus/services/library_sync.py | New core mutation engine: ownership checks, reference validation, tombstones, and cascading deletes. |
| papyrus/schemas/sync.py | Expands allowed mutation tables/fields and enforces unknown-field rejection per table. |
| papyrus/models/sync.py | Adds promoted book metadata columns. |
| papyrus/models/library.py | Introduces owned library ORM models and tombstones. |
| papyrus/models/init.py | Exposes new library models via the central models import surface for Alembic metadata. |
| docs/powersync-sandbox.md | Documents rollout order, contract details, and validation steps for library sync. |
| alembic/versions/dcd3b384e6a4_add_owned_library_sync_and_promoted_.py | Adds library tables + promoted book columns and performs backfill. |
| alembic/versions/af0fea8d6317_add_owned_bookmark_sync.py | Adds bookmarks table and constraints in a follow-up additive revision. |
Review details
Suppressed comments (1)
papyrus/services/library_sync.py:98
- Shelf deletion reparents children based only on
parent_shelf_id. Because the FK does not enforce same-owner relationships, addingowner_user_id == user_idhere avoids the possibility of affecting another user's shelves if inconsistent cross-tenant references ever exist.
if table == "shelves":
await session.execute(
update(SyncShelf)
.where(SyncShelf.parent_shelf_id == row_id)
.values(parent_shelf_id=None, updated_at=datetime.now(UTC))
)
- Files reviewed: 20/20 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if table == "books": | ||
| for child_table in ("notes", "annotations", "bookmarks"): | ||
| model = MODELS[child_table] | ||
| result = await session.execute(select(model).where(model.book_id == row_id)) |
Summary
Persist and synchronize the complete library: books, shelves, topics (tags), notes, annotations, bookmarks, and book memberships. Previously, non-book library records lived only in client memory and could not survive restart or propagate between devices.
Migration and rollout
Apply additive revisions
dcd3b384e6a4andaf0fea8d6317. Deploy schema/upload support first, refresh replication grants and publication withscripts/setup_local_powersync.sh, activate the expanded PowerSync configuration, and verify a replication checkpoint before updating clients. Seedocs/powersync-sandbox.mdfor commands and validation.No key regeneration or database reset is required. Downgrading removes the newly added domain tables and promoted columns; preserve their data before a downgrade. Keep deletion tombstones while offline clients may still upload old changes.
Validation
Companion client: PapyrusReader/client#23. Workspace: PapyrusReader/papyrus#1. Merge/deploy this server support before the client rollout.