From 6519338822bdc87a1db4e29b897002a937d41458 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 21 Apr 2026 21:15:34 -0400 Subject: [PATCH] fix(conflicts): use ListConflictsByUser in DismissAllResolved so resolved conflicts are found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DismissAllResolved was calling ListSyncConflictsByUser which filters to 'unresolved' conflicts only, so it could never find the user_resolved or bulk_resolved conflicts it was trying to delete. The query always returned an empty set, making dismiss-all a no-op. Fix the leading space in three SQL query name annotations (ListConflictsByUser, ListAllConflictsByUserAndStatus, CheckForProgressConflicts) that prevented sqlc from generating their Go functions. Regenerate the query code and swap DismissAllResolved to use ListConflictsByUser (no status filter) — the existing Go loop already filters by resolution_status. --- internal/database/querier.go | 3 + internal/database/queries.sql.go | 139 ++++++++++++++++++++++++++ internal/database/queries/queries.sql | 26 ++--- internal/handlers/conflicts.go | 2 +- 4 files changed, 156 insertions(+), 14 deletions(-) diff --git a/internal/database/querier.go b/internal/database/querier.go index 248f6bd..e902dd1 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -23,6 +23,7 @@ type Querier interface { // Bulk update format group for all media items BulkUpdateFormatGroups(ctx context.Context) error BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdateProgressFromSyncParams) ([]interface{}, error) + CheckForProgressConflicts(ctx context.Context, arg CheckForProgressConflictsParams) (int64, error) // Cleanup expired OPDS tokens CleanupExpiredOpdsTokens(ctx context.Context) error CleanupExpiredRefreshTokens(ctx context.Context) error @@ -275,7 +276,9 @@ type Querier interface { IsBookOnKoboShelf(ctx context.Context, arg IsBookOnKoboShelfParams) (bool, error) // Link unlinked book to media item LinkUnlinkedBook(ctx context.Context, arg LinkUnlinkedBookParams) (UnlinkedBooks, error) + ListAllConflictsByUserAndStatus(ctx context.Context, arg ListAllConflictsByUserAndStatusParams) ([]ListAllConflictsByUserAndStatusRow, error) ListAllSyncQueueItems(ctx context.Context, arg ListAllSyncQueueItemsParams) ([]ListAllSyncQueueItemsRow, error) + ListConflictsByUser(ctx context.Context, userID pgtype.UUID) ([]ListConflictsByUserRow, error) ListDevicesByType(ctx context.Context, deviceType string) ([]Devices, error) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 3467be9..5274924 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -153,6 +153,28 @@ func (q *Queries) BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdate return items, nil } +const CheckForProgressConflicts = `-- name: CheckForProgressConflicts :one +SELECT COUNT(*) as conflict_count +FROM reading_progress +WHERE media_item_id = $1 + AND user_id = $2 + AND last_sync_timestamp > NOW() - INTERVAL '5 minutes' + AND last_sync_source != $3 +` + +type CheckForProgressConflictsParams struct { + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` +} + +func (q *Queries) CheckForProgressConflicts(ctx context.Context, arg CheckForProgressConflictsParams) (int64, error) { + row := q.db.QueryRow(ctx, CheckForProgressConflicts, arg.MediaItemID, arg.UserID, arg.LastSyncSource) + var conflict_count int64 + err := row.Scan(&conflict_count) + return conflict_count, err +} + const CleanupExpiredOpdsTokens = `-- name: CleanupExpiredOpdsTokens :exec DELETE FROM opds_tokens WHERE expires_at < NOW() ` @@ -6322,6 +6344,67 @@ func (q *Queries) LinkUnlinkedBook(ctx context.Context, arg LinkUnlinkedBookPara return i, err } +const ListAllConflictsByUserAndStatus = `-- name: ListAllConflictsByUserAndStatus :many +SELECT sc.id, sc.media_item_id, sc.user_id, sc.conflict_type, sc.conflict_data, sc.resolution_status, sc.resolution_data, sc.resolved_by, sc.resolved_at, sc.created_at, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 AND sc.resolution_status = $2 +ORDER BY sc.created_at DESC +` + +type ListAllConflictsByUserAndStatusParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + ResolutionStatus pgtype.Text `db:"resolution_status" json:"resolution_status"` +} + +type ListAllConflictsByUserAndStatusRow struct { + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + ConflictType string `db:"conflict_type" json:"conflict_type"` + ConflictData []byte `db:"conflict_data" json:"conflict_data"` + ResolutionStatus pgtype.Text `db:"resolution_status" json:"resolution_status"` + ResolutionData []byte `db:"resolution_data" json:"resolution_data"` + ResolvedBy pgtype.UUID `db:"resolved_by" json:"resolved_by"` + ResolvedAt pgtype.Timestamptz `db:"resolved_at" json:"resolved_at"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` +} + +func (q *Queries) ListAllConflictsByUserAndStatus(ctx context.Context, arg ListAllConflictsByUserAndStatusParams) ([]ListAllConflictsByUserAndStatusRow, error) { + rows, err := q.db.Query(ctx, ListAllConflictsByUserAndStatus, arg.UserID, arg.ResolutionStatus) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListAllConflictsByUserAndStatusRow{} + for rows.Next() { + var i ListAllConflictsByUserAndStatusRow + if err := rows.Scan( + &i.ID, + &i.MediaItemID, + &i.UserID, + &i.ConflictType, + &i.ConflictData, + &i.ResolutionStatus, + &i.ResolutionData, + &i.ResolvedBy, + &i.ResolvedAt, + &i.CreatedAt, + &i.Title, + &i.Author, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListAllSyncQueueItems = `-- name: ListAllSyncQueueItems :many SELECT sq.id, sq.device_id, sq.media_item_id, sq.sync_type, sq.sync_data, sq.priority, sq.attempts, sq.max_attempts, sq.status, sq.error_message, sq.created_at, sq.processed_at, @@ -6398,6 +6481,62 @@ func (q *Queries) ListAllSyncQueueItems(ctx context.Context, arg ListAllSyncQueu return items, nil } +const ListConflictsByUser = `-- name: ListConflictsByUser :many +SELECT sc.id, sc.media_item_id, sc.user_id, sc.conflict_type, sc.conflict_data, sc.resolution_status, sc.resolution_data, sc.resolved_by, sc.resolved_at, sc.created_at, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 +ORDER BY sc.created_at DESC +` + +type ListConflictsByUserRow struct { + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + ConflictType string `db:"conflict_type" json:"conflict_type"` + ConflictData []byte `db:"conflict_data" json:"conflict_data"` + ResolutionStatus pgtype.Text `db:"resolution_status" json:"resolution_status"` + ResolutionData []byte `db:"resolution_data" json:"resolution_data"` + ResolvedBy pgtype.UUID `db:"resolved_by" json:"resolved_by"` + ResolvedAt pgtype.Timestamptz `db:"resolved_at" json:"resolved_at"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` +} + +func (q *Queries) ListConflictsByUser(ctx context.Context, userID pgtype.UUID) ([]ListConflictsByUserRow, error) { + rows, err := q.db.Query(ctx, ListConflictsByUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListConflictsByUserRow{} + for rows.Next() { + var i ListConflictsByUserRow + if err := rows.Scan( + &i.ID, + &i.MediaItemID, + &i.UserID, + &i.ConflictType, + &i.ConflictData, + &i.ResolutionStatus, + &i.ResolutionData, + &i.ResolvedBy, + &i.ResolvedAt, + &i.CreatedAt, + &i.Title, + &i.Author, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListDevicesByType = `-- name: ListDevicesByType :many SELECT id, user_id, device_name, device_type, device_identifier, auth_token, last_sync, last_seen, sync_enabled, auto_sync, sync_frequency_minutes, device_metadata, created_at, updated_at FROM devices WHERE device_type = $1 ORDER BY created_at DESC ` diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 70bdfbb..f1b8131 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -1099,19 +1099,19 @@ RETURNING *; -- name: DeleteSyncConflict :exec DELETE FROM sync_conflicts WHERE id = $1; - -- name: ListAllConflictsByUserAndStatus :many - SELECT sc.*, mi.title, mi.author - FROM sync_conflicts sc - JOIN media_items mi ON sc.media_item_id = mi.id - WHERE sc.user_id = $1 AND sc.resolution_status = $2 - ORDER BY sc.created_at DESC; +-- name: ListAllConflictsByUserAndStatus :many +SELECT sc.*, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 AND sc.resolution_status = $2 +ORDER BY sc.created_at DESC; - -- name: ListConflictsByUser :many - SELECT sc.*, mi.title, mi.author - FROM sync_conflicts sc - JOIN media_items mi ON sc.media_item_id = mi.id - WHERE sc.user_id = $1 - ORDER BY sc.created_at DESC; +-- name: ListConflictsByUser :many +SELECT sc.*, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 +ORDER BY sc.created_at DESC; -- ============================================ -- KOREADER SYNC PROTOCOL @@ -1216,7 +1216,7 @@ WHERE lv.user_id = $1 ORDER BY mi.title ASC LIMIT 1000; - -- name: CheckForProgressConflicts :one +-- name: CheckForProgressConflicts :one SELECT COUNT(*) as conflict_count FROM reading_progress WHERE media_item_id = $1 diff --git a/internal/handlers/conflicts.go b/internal/handlers/conflicts.go index 82f253b..a2a2210 100644 --- a/internal/handlers/conflicts.go +++ b/internal/handlers/conflicts.go @@ -401,7 +401,7 @@ func (h *ConflictHandler) DeleteConflict(c *echo.Context) error { func (h *ConflictHandler) DismissAllResolved(c *echo.Context) error { user := c.Get("user").(database.Users) - conflicts, err := h.db.ListSyncConflictsByUser(context.Background(), user.ID) + conflicts, err := h.db.ListConflictsByUser(context.Background(), user.ID) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to list conflicts") }