fix(conflicts): use ListConflictsByUser in DismissAllResolved so resolved conflicts are found

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.
This commit is contained in:
2026-04-21 21:15:34 -04:00
parent 855ef161d8
commit ad27902790
4 changed files with 156 additions and 14 deletions
+3
View File
@@ -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)
+139
View File
@@ -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
`
+13 -13
View File
@@ -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
+1 -1
View File
@@ -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")
}