feat(server): system collections become permanent — soft-delete + in-place restore + query_type-keyed provisioning

The old model hard-deleted system collections and re-created them at
the tail, resurrecting stale dedups and breaking ordering. Redesign
(user-directed B-final):

- schema.sql: collections.deleted_at TIMESTAMPTZ (idempotent ALTER —
  the whole schema is re-applied on every boot via executeSchema).
- queries.sql: every collection list/read query filters
  deleted_at IS NULL; new GetSystemCollectionByQueryType :one
  (unfiltered, sees tombstones), TombstoneSystemCollection :exec,
  ClearCollectionItems :exec, RestoreSystemCollectionRow :one
  (in-place metadata reset + un-tombstone; collections have no
  updated_at column — ResetUpdatedAt was a 500 caught by tests).
  sqlc regenerated.
- collection_service.DeleteCollection: system collections are
  tombstoned (preserving position/ownership); user collections keep
  the hard delete. No ErrNoRows to the client (delete is idempotent).
- dashboard_service.RestoreSystemCollection: resolve by query_type;
  found → in-place restore (ClearCollectionItems + metadata reset,
  un-tombstoned); missing → provision fresh. "Want to Read"/etc.
  stay at their home positions forever.
- handlers/collections.CreateCollection: name-taken is 409, not 500.
- handlers/auth.CreateDefaultCollectionsForUser: idempotent by
  (user_id, query_type) — update-in-place when a row for that query
  type exists (heals renames/tombstones), insert when missing; this
  also fixes the duplicate-system-collections regression at source.
This commit is contained in:
John O'Keefe committed 2026-10-09 11:04:24 -04:00
1 parent 31f939002f
commit 7ce73213ae
9 files changed
+284 -39

No files matched your search

+11
View File
@@ -30,6 +30,7 @@ type Querier interface {
// Cleanup expired OPDS tokens
CleanupExpiredOpdsTokens(ctx context.Context) error
CleanupExpiredRefreshTokens(ctx context.Context, dollar_1 float64) error
ClearCollectionItems(ctx context.Context, collectionID pgtype.UUID) error
ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error
@@ -305,6 +306,12 @@ type Querier interface {
GetSyncConflict(ctx context.Context, id pgtype.UUID) (SyncConflicts, error)
GetSyncQueueItem(ctx context.Context, id pgtype.UUID) (SyncQueue, error)
GetSyncQueueStats(ctx context.Context, deviceID pgtype.UUID) (GetSyncQueueStatsRow, error)
// System collections are permanent: identity is (user_id, query_type).
// Restore/reset operate IN PLACE (the row id — and everything referencing
// it: shelf mappings, prefs order, links — never churns). Deleted sections
// are TOMBSTONED (deleted_at) and stay invisible until restored.
// Unfiltered by design: restore/seed must resolve tombstoned rows too.
GetSystemCollectionByQueryType(ctx context.Context, arg GetSystemCollectionByQueryTypeParams) (Collections, error)
// Dashboard collections queries
GetSystemCollectionsForDashboard(ctx context.Context, userID pgtype.UUID) ([]Collections, error)
// SYSTEM CONFIG QUERIES
@@ -425,6 +432,9 @@ type Querier interface {
RestoreMediaBookmarkByID(ctx context.Context, arg RestoreMediaBookmarkByIDParams) (int64, error)
RestoreMediaHighlightByID(ctx context.Context, arg RestoreMediaHighlightByIDParams) (int64, error)
RestoreMediaNoteByID(ctx context.Context, arg RestoreMediaNoteByIDParams) (int64, error)
// Full reset, in place: canonical metadata, back on the dashboard,
// un-tombstoned. Item assignments are cleared by the caller ("full").
RestoreSystemCollectionRow(ctx context.Context, arg RestoreSystemCollectionRowParams) (Collections, error)
RevokeAllUserRefreshTokens(ctx context.Context, userID pgtype.UUID) error
RevokeDevice(ctx context.Context, id pgtype.UUID) error
// Revoke OPDS token
@@ -449,6 +459,7 @@ type Querier interface {
TombstoneMediaHighlightByID(ctx context.Context, id pgtype.UUID) error
TombstoneMediaNoteByDedupKey(ctx context.Context, arg TombstoneMediaNoteByDedupKeyParams) error
TombstoneMediaNoteByID(ctx context.Context, id pgtype.UUID) error
TombstoneSystemCollection(ctx context.Context, id pgtype.UUID) error
// Update collection
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error)
UpdateDashboardPreferences(ctx context.Context, arg UpdateDashboardPreferencesParams) (UserDashboardPreferences, error)