-- name: CreateUser :one INSERT INTO users (email, username, password_hash, first_name, last_name, theme, role) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, email, username, theme, first_name, last_name, role, created_at, updated_at; -- name: GetUserByEmail :one SELECT id, email, username, theme, first_name, last_name, role, created_at, updated_at FROM users WHERE email = $1; -- name: GetUserByUsername :one SELECT id, email, username, theme, first_name, last_name, role, created_at, updated_at FROM users WHERE username = $1; -- name: GetUserByEmailOrUsername :one SELECT id, email, username, theme, first_name, last_name, role, created_at, updated_at FROM users WHERE email = $1 OR username = $1; -- name: GetUserForLogin :one SELECT id, email, username, password_hash, theme, first_name, last_name, role, created_at, updated_at FROM users WHERE email = $1 OR username = $1; -- name: GetUser :one SELECT id, email, username, theme, first_name, last_name, role, created_at, updated_at FROM users WHERE id = $1; -- name: GetUserPasswordHash :one SELECT password_hash FROM users WHERE id = $1; -- name: ListUsers :many SELECT id, email, username, theme, first_name, last_name, role, created_at, updated_at FROM users ORDER BY created_at DESC; -- Library Types queries -- name: GetLibraryTypes :many SELECT * FROM library_types ORDER BY name; -- name: GetLibraryType :one SELECT * FROM library_types WHERE id = $1; -- name: GetLibraryTypeByName :one SELECT * FROM library_types WHERE name = $1; -- Libraries queries -- name: CreateLibrary :one INSERT INTO libraries (name, description, library_type_id, created_by_admin_id) VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetLibrary :one SELECT l.*, lt.name as type_name, lt.description as type_description FROM libraries l JOIN library_types lt ON l.library_type_id = lt.id WHERE l.id = $1; -- name: ListLibraries :many SELECT l.*, lt.name as type_name, lt.description as type_description FROM libraries l JOIN library_types lt ON l.library_type_id = lt.id ORDER BY l.created_at DESC; -- name: UpdateLibrary :one UPDATE libraries SET name = $2, description = $3, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: DeleteLibrary :exec DELETE FROM libraries WHERE id = $1; -- Library Folders queries -- name: AddLibraryFolder :one INSERT INTO library_folders (library_id, folder_path) VALUES ($1, $2) RETURNING *; -- name: GetLibraryFolders :many SELECT * FROM library_folders WHERE library_id = $1 ORDER BY created_at; -- name: DeleteLibraryFolder :one DELETE FROM library_folders WHERE library_id = $1 AND folder_path = $2 RETURNING *; -- name: GetLibraryByFolder :one SELECT lf.library_id, l.* FROM library_folders lf JOIN libraries l ON lf.library_id = l.id WHERE lf.folder_path = $1; -- Library Visibility queries -- name: SetLibraryVisibility :one INSERT INTO library_visibility (user_id, library_id, is_visible) VALUES ($1, $2, $3) ON CONFLICT (user_id, library_id) DO UPDATE SET is_visible = EXCLUDED.is_visible, updated_at = NOW() RETURNING *; -- name: GetLibraryVisibility :one SELECT * FROM library_visibility WHERE user_id = $1 AND library_id = $2; -- name: GetUserVisibleLibraries :many SELECT l.*, lt.name as type_name, lt.description as type_description, COALESCE(lv.is_visible, true) as is_visible FROM libraries l JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 WHERE COALESCE(lv.is_visible, true) = true ORDER BY l.created_at DESC; -- Media Items queries -- name: CreateMediaItem :one INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25) RETURNING *; -- name: GetMediaItem :one SELECT * FROM media_items WHERE id = $1; -- name: ListMediaItems :many SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2; -- name: ListMediaItemsByLibrary :many SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id WHERE mi.library_id = $1 ORDER BY mi.created_at DESC; -- name: ListMediaItemsSorted :many SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id WHERE mi.library_id = sqlc.narg('library_id') ORDER BY CASE WHEN sqlc.narg('sort') = 'title ASC' THEN mi.title ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'title DESC' THEN mi.title ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'author ASC' THEN COALESCE(mi.author, '') ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'author DESC' THEN COALESCE(mi.author, '') ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'created_at ASC' THEN mi.created_at ELSE '1970-01-01'::timestamp END ASC, CASE WHEN sqlc.narg('sort') = 'created_at DESC' THEN mi.created_at ELSE '1970-01-01'::timestamp END DESC, CASE WHEN sqlc.narg('sort') = 'series ASC' THEN COALESCE(mi.series, '') ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'series DESC' THEN COALESCE(mi.series, '') ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'date_published ASC' THEN COALESCE(mi.date_published::text, '') ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'date_published DESC' THEN COALESCE(mi.date_published::text, '') ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'copyright_year ASC' THEN COALESCE(mi.copyright_year::text, '0') ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'copyright_year DESC' THEN COALESCE(mi.copyright_year::text, '0') ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'page_count ASC' THEN COALESCE(mi.page_count::text, '0') ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'page_count DESC' THEN COALESCE(mi.page_count::text, '0') ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'genre ASC' THEN COALESCE(mi.genre, '') ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'genre DESC' THEN COALESCE(mi.genre, '') ELSE '' END DESC, mi.created_at DESC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: ListMediaItemsFiltered :many SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE mi.library_id = sqlc.narg('library_id') AND COALESCE(lv.is_visible, true) = true AND (sqlc.narg('author_filter') = '' OR mi.author ILIKE sqlc.narg('author_filter')) AND (sqlc.narg('series_filter') = '' OR mi.series ILIKE sqlc.narg('series_filter')) AND (sqlc.narg('genre_filter') = '' OR mi.genre = sqlc.narg('genre_filter')) AND (sqlc.narg('language_filter') = '' OR mi.language = sqlc.narg('language_filter')) AND (sqlc.narg('year_min') = 0 OR mi.copyright_year >= sqlc.narg('year_min')) AND (sqlc.narg('year_max') = 0 OR mi.copyright_year <= sqlc.narg('year_max')) AND (sqlc.narg('has_cover') = false OR mi.cover_image_path IS NOT NULL) ORDER BY CASE WHEN sqlc.narg('sort') = 'title ASC' THEN mi.title ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'title DESC' THEN mi.title ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'author ASC' THEN COALESCE(mi.author, '') ELSE '' END ASC, CASE WHEN sqlc.narg('sort') = 'author DESC' THEN COALESCE(mi.author, '') ELSE '' END DESC, CASE WHEN sqlc.narg('sort') = 'created_at ASC' THEN mi.created_at ELSE '1970-01-01'::timestamp END ASC, CASE WHEN sqlc.narg('sort') = 'created_at DESC' THEN mi.created_at ELSE '1970-01-01'::timestamp END DESC, mi.created_at DESC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: UpdateMediaItem :one UPDATE media_items SET title = $2, author = $3, isbn = $4, description = $5, cover_image_path = $6, series = $7, series_number = $8, tags = $9, asin = $10, date_published = $11, publisher = $12, contributors = $13, language = $14, edition = $15, page_count = $16, genre = $17, copyright_year = $18, goodreads_id = $19, openlibrary_id = $20, google_books_id = $21, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: DeleteMediaItem :exec DELETE FROM media_items WHERE id = $1; -- name: GetMediaItemByFilePath :one SELECT * FROM media_items WHERE file_path = $1; -- name: GetReadingProgress :one SELECT * FROM reading_progress WHERE media_item_id = $1 AND user_id = $2; -- name: UpdateReadingProgress :one INSERT INTO reading_progress (media_item_id, user_id, current_page, total_pages, last_read_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (media_item_id, user_id) DO UPDATE SET current_page = EXCLUDED.current_page, total_pages = EXCLUDED.total_pages, last_read_at = NOW() RETURNING *; -- name: DeleteReadingProgress :exec DELETE FROM reading_progress WHERE media_item_id = $1 AND user_id = $2; -- name: UpdateUserTheme :exec UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1; -- name: UpdateUserProfile :exec UPDATE users SET first_name = $2, last_name = $3, updated_at = NOW() WHERE id = $1; -- name: UpdateUsername :exec UPDATE users SET username = $2, updated_at = NOW() WHERE id = $1; -- name: UpdateEmail :exec UPDATE users SET email = $2, updated_at = NOW() WHERE id = $1; -- name: UpdatePassword :exec UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1; -- name: DeleteUser :exec DELETE FROM users WHERE id = $1; -- name: UpdateScanSettings :exec UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at = NOW() WHERE id = $1; -- name: GetScanSettings :one SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1; -- name: CreateMediaRating :one INSERT INTO media_ratings (media_item_id, user_id, rating) VALUES ($1, $2, $3) ON CONFLICT (media_item_id, user_id) DO UPDATE SET rating = EXCLUDED.rating, updated_at = NOW() RETURNING *; -- name: GetMediaRating :one SELECT * FROM media_ratings WHERE media_item_id = $1 AND user_id = $2; -- name: GetMediaRatings :many SELECT mr.*, u.username FROM media_ratings mr JOIN users u ON mr.user_id = u.id WHERE mr.media_item_id = $1 ORDER BY mr.created_at DESC; -- name: UpdateMediaRating :one UPDATE media_ratings SET rating = $3, updated_at = NOW() WHERE media_item_id = $1 AND user_id = $2 RETURNING *; -- name: DeleteMediaRating :exec DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2; -- Note: User ebook folders replaced by library folders system -- Legacy folder management is now handled through libraries -- Search Media Items queries -- name: SearchMediaItems :many SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true AND ( mi.title ILIKE sqlc.narg('search_pattern') OR mi.author ILIKE sqlc.narg('search_pattern') OR mi.series ILIKE sqlc.narg('search_pattern') OR mi.tags ILIKE sqlc.narg('search_pattern') OR mi.contributors ILIKE sqlc.narg('search_pattern') ) ORDER BY CASE WHEN mi.title ILIKE sqlc.narg('search_pattern') THEN 1 WHEN mi.author ILIKE sqlc.narg('search_pattern') THEN 2 WHEN mi.series ILIKE sqlc.narg('search_pattern') THEN 3 WHEN mi.tags ILIKE sqlc.narg('search_pattern') THEN 4 ELSE 5 END, mi.title ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: SearchMediaItemsFuzzy :many SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true AND ( word_similarity(sqlc.narg('search_query'), mi.title) > 0.3 OR word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3 OR word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3 OR word_similarity(sqlc.narg('search_query'), COALESCE(mi.tags, '')) > 0.3 OR word_similarity(sqlc.narg('search_query'), COALESCE(mi.contributors, '')) > 0.3 ) ORDER BY GREATEST( word_similarity(sqlc.narg('search_query'), mi.title), word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')), word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')), word_similarity(sqlc.narg('search_query'), COALESCE(mi.tags, '')), word_similarity(sqlc.narg('search_query'), COALESCE(mi.contributors, '')) ) DESC, mi.title ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- Media Notes queries -- name: CreateMediaNote :one INSERT INTO media_notes (media_item_id, user_id, content, position) VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetMediaNote :one SELECT * FROM media_notes WHERE id = $1; -- name: GetMediaNotes :many SELECT * FROM media_notes WHERE media_item_id = $1 AND user_id = $2 ORDER BY created_at DESC; -- name: UpdateMediaNote :one UPDATE media_notes SET content = $2, position = $3, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: DeleteMediaNote :exec DELETE FROM media_notes WHERE id = $1; -- Media Highlights queries -- name: CreateMediaHighlight :one INSERT INTO media_highlights (media_item_id, user_id, selection_text, start_position, end_position, color, note_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: GetMediaHighlight :one SELECT * FROM media_highlights WHERE id = $1; -- name: GetMediaHighlights :many SELECT * FROM media_highlights WHERE media_item_id = $1 AND user_id = $2 ORDER BY created_at DESC; -- name: UpdateMediaHighlight :one UPDATE media_highlights SET selection_text = $2, start_position = $3, end_position = $4, color = $5, note_id = $6, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: DeleteMediaHighlight :exec DELETE FROM media_highlights WHERE id = $1; -- Backward compatibility - Ebook Notes queries (using views) -- name: CreateEbookNote :one INSERT INTO media_notes (media_item_id, user_id, content, position) VALUES ($1, $2, $3, $4) RETURNING *; -- name: UpdateEbookNote :one UPDATE media_notes SET content = $2, position = $3, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: DeleteEbookNote :exec DELETE FROM media_notes WHERE id = $1; -- Refresh Tokens queries -- name: CreateRefreshToken :one INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES ($1, $2, $3) RETURNING *; -- name: GetRefreshToken :one SELECT rt.*, u.email, u.username, u.role FROM refresh_tokens rt JOIN users u ON rt.user_id = u.id WHERE rt.token = $1 AND rt.revoked_at IS NULL AND rt.expires_at > NOW(); -- name: RevokeRefreshToken :exec UPDATE refresh_tokens SET revoked_at = NOW() WHERE token = $1; -- name: RevokeAllUserRefreshTokens :exec UPDATE refresh_tokens SET revoked_at = NOW() WHERE user_id = $1 AND revoked_at IS NULL; -- name: CleanupExpiredRefreshTokens :exec DELETE FROM refresh_tokens WHERE expires_at < NOW() OR (revoked_at IS NOT NULL AND revoked_at < NOW() - INTERVAL '7 days'); -- ============================================ -- PHASE 1: FORMAT DETECTION & PROGRESS (Week 2) -- ============================================ -- Update media item format group information -- name: UpdateMediaItemFormatGroup :exec UPDATE media_items SET format_group = $2, format_mimetype = $3, is_reflowable = $4, has_fixed_layout = $5, total_characters = $6, chapter_count = $7, updated_at = NOW() WHERE id = $1; -- Bulk update format group for all media items -- name: BulkUpdateFormatGroups :exec UPDATE media_items m SET format_group = detect_format_group(m.mime_type, m.file_path), format_mimetype = m.mime_type, is_reflowable = (detect_format_group(m.mime_type, m.file_path) = 'reflowable'), has_fixed_layout = (detect_format_group(m.mime_type, m.file_path) IN ('fixed_layout', 'comic_archive')), updated_at = NOW() WHERE m.format_group IS NULL OR m.format_group = 'unknown'; -- Get universal progress for a book -- name: GetUniversalProgress :one SELECT rp.id, rp.media_item_id, rp.user_id, rp.current_page, rp.total_pages, rp.last_read_at, rp.percentage, rp.character_offset, rp.epubcfi, rp.chapter, rp.chapter_progress, rp.viewport_x, rp.viewport_y, rp.zoom_level, rp.scroll_position_x, rp.scroll_position_y, rp.panel_number, rp.reading_mode, rp.last_sync_device, rp.last_sync_source, rp.last_sync_timestamp, rp.conflict_detected, rp.conflict_resolved, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count FROM reading_progress rp JOIN media_items mi ON rp.media_item_id = mi.id WHERE rp.media_item_id = $1 AND rp.user_id = $2; -- Update universal progress -- name: UpdateUniversalProgress :one INSERT INTO reading_progress ( media_item_id, user_id, percentage, character_offset, epubcfi, chapter, chapter_progress, viewport_x, viewport_y, zoom_level, scroll_position_x, scroll_position_y, panel_number, reading_mode, last_sync_device, last_sync_source, last_sync_timestamp, current_page, total_pages, last_read_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, NOW(), $17, $18, NOW() ) ON CONFLICT (media_item_id, user_id) DO UPDATE SET percentage = EXCLUDED.percentage, character_offset = EXCLUDED.character_offset, epubcfi = EXCLUDED.epubcfi, chapter = EXCLUDED.chapter, chapter_progress = EXCLUDED.chapter_progress, viewport_x = EXCLUDED.viewport_x, viewport_y = EXCLUDED.viewport_y, zoom_level = EXCLUDED.zoom_level, scroll_position_x = EXCLUDED.scroll_position_x, scroll_position_y = EXCLUDED.scroll_position_y, panel_number = EXCLUDED.panel_number, reading_mode = EXCLUDED.reading_mode, last_sync_device = EXCLUDED.last_sync_device, last_sync_source = EXCLUDED.last_sync_source, last_sync_timestamp = EXCLUDED.last_sync_timestamp, current_page = EXCLUDED.current_page, total_pages = EXCLUDED.total_pages, last_read_at = NOW() RETURNING *; -- Get reading history for a user and book -- name: GetReadingHistory :many SELECT * FROM reading_history WHERE user_id = $1 AND media_item_id = $2 ORDER BY created_at DESC LIMIT $3; -- Create reading history entry -- name: CreateReadingHistory :one INSERT INTO reading_history ( user_id, media_item_id, device_id, progress_percentage, reading_session_start, reading_session_end, pages_read, time_spent_seconds, device_metadata ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; -- ============================================ -- PHASE 2: DEVICE MANAGEMENT & AUTH (Weeks 5-6) -- ============================================ -- Device Registration & Management -- name: CreateDevice :one INSERT INTO devices (user_id, device_name, device_type, device_identifier, auth_token, sync_enabled, auto_sync, sync_frequency_minutes, device_metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; -- name: GetDevice :one SELECT * FROM devices WHERE id = $1; -- name: GetDeviceByIdentifier :one SELECT * FROM devices WHERE device_identifier = $1; -- name: GetDeviceByAuthToken :one SELECT * FROM devices WHERE auth_token = $1; -- name: ListDevicesByUser :many SELECT * FROM devices WHERE user_id = $1 ORDER BY created_at DESC; -- name: ListDevicesByType :many SELECT * FROM devices WHERE device_type = $1 ORDER BY created_at DESC; -- name: UpdateDevice :one UPDATE devices SET device_name = $2, sync_enabled = $3, auto_sync = $4, sync_frequency_minutes = $5, device_metadata = $6, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: UpdateDeviceLastSync :one UPDATE devices SET last_sync = NOW(), last_seen = NOW(), updated_at = NOW() WHERE id = $1 RETURNING *; -- name: UpdateDeviceLastSeen :one UPDATE devices SET last_seen = NOW(), updated_at = NOW() WHERE id = $1 RETURNING *; -- name: RevokeDevice :exec UPDATE devices SET auth_token = NULL, sync_enabled = false, updated_at = NOW() WHERE id = $1; -- name: DeleteDevice :exec DELETE FROM devices WHERE id = $1; -- name: DeleteDeviceByToken :exec DELETE FROM devices WHERE auth_token = $1; -- Sync Queue Management -- name: CreateSyncQueueItem :one INSERT INTO sync_queue (device_id, media_item_id, sync_type, sync_data, priority, max_attempts, status) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: GetSyncQueueItem :one SELECT * FROM sync_queue WHERE id = $1; -- name: ListPendingSyncQueueItems :many SELECT * FROM sync_queue WHERE device_id = $1 AND status = 'pending' ORDER BY priority ASC, created_at ASC LIMIT $2; -- name: UpdateSyncQueueItemStatus :one UPDATE sync_queue SET status = $2, attempts = attempts + 1, error_message = $3, processed_at = CASE WHEN $2 = 'completed' THEN NOW() ELSE NULL END WHERE id = $1 RETURNING *; -- name: DeleteSyncQueueItem :exec DELETE FROM sync_queue WHERE id = $1; -- name: ClearDeviceSyncQueue :exec DELETE FROM sync_queue WHERE device_id = $1; -- Conflict Resolution -- name: CreateSyncConflict :one INSERT INTO sync_conflicts (media_item_id, user_id, conflict_type, conflict_data) VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetSyncConflict :one SELECT * FROM sync_conflicts WHERE id = $1; -- name: ListSyncConflictsByUser :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 = 'unresolved' ORDER BY sc.created_at DESC; -- name: ListSyncConflictsByMediaItem :many SELECT * FROM sync_conflicts WHERE media_item_id = $1 AND user_id = $2 ORDER BY created_at DESC; -- name: ResolveSyncConflict :one UPDATE sync_conflicts SET resolution_status = $2, resolution_data = $3, resolved_by = $4, resolved_at = NOW() WHERE id = $1 RETURNING *; -- name: DeleteSyncConflict :exec DELETE FROM sync_conflicts WHERE id = $1; -- ============================================ -- PHASE 3: KOREADER SYNC PROTOCOL (Weeks 7-9) -- ============================================ -- name: GetMediaItemByFilePathForSync :one SELECT * FROM media_items WHERE file_path = $1; -- name: GetUserProgressForBooks :many SELECT rp.media_item_id, rp.user_id, rp.percentage, rp.character_offset, rp.epubcfi, rp.chapter, rp.chapter_progress, rp.current_page, rp.total_pages, rp.last_read_at, rp.last_sync_device, rp.last_sync_source, mi.title, mi.author, mi.file_path FROM reading_progress rp JOIN media_items mi ON rp.media_item_id = mi.id WHERE rp.user_id = $1 AND rp.media_item_id = ANY($2::uuid[]) ORDER BY rp.last_read_at DESC; -- name: BulkUpdateProgressFromSync :many SELECT * FROM bulk_update_progress_from_koreader($1::uuid, $2::jsonb); -- name: GetAnnotationsForBook :many SELECT mh.id, mh.selection_text, mh.start_position, mh.end_position, mh.color, mh.created_at, mh.updated_at, 'highlight' as annotation_type, mh.percentage_start, mh.percentage_end, mh.epubcfi_start, mh.epubcfi_end FROM media_highlights mh WHERE mh.media_item_id = $1 AND mh.user_id = $2 UNION ALL SELECT mn.id, mn.content, mn.position, NULL as end_position, NULL as color, mn.created_at, mn.updated_at, 'note' as annotation_type, mn.percentage_location as percentage_start, NULL as percentage_end, mn.epubcfi_location as epubcfi_start, NULL as epubcfi_end FROM media_notes mn WHERE mn.media_item_id = $1 AND mn.user_id = $2 ORDER BY created_at DESC; -- name: UpdateDeviceSyncTimestamp :one UPDATE devices SET last_sync = NOW(), last_seen = NOW(), updated_at = NOW() WHERE id = $1 RETURNING *; -- name: CreateSyncHistoryEntry :one INSERT INTO sync_queue (device_id, media_item_id, sync_type, sync_data, priority, status) VALUES ($1, $2, 'koreader_progress', $3, 5, 'completed') RETURNING *; -- name: GetUserMediaItemsForSync :many SELECT mi.id, mi.title, mi.author, mi.file_path, mi.mime_type, mi.page_count, mi.format_group, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.file_size FROM media_items mi JOIN library_visibility lv ON mi.library_id = lv.library_id WHERE lv.user_id = $1 AND lv.is_visible = true ORDER BY mi.title ASC LIMIT 1000; -- 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; -- ============================================ -- KOBO SHELF MANAGEMENT QUERIES (Phase 4) -- ============================================ -- name: AddBookToKoboShelf :one INSERT INTO kobo_shelves (device_id, media_item_id, shelf_name, shelf_position) VALUES ($1, $2, $3, $4) ON CONFLICT (device_id, media_item_id) DO UPDATE SET shelf_name = EXCLUDED.shelf_name, shelf_position = EXCLUDED.shelf_position, last_synced_at = NOW() RETURNING *; -- name: RemoveBookFromKoboShelf :exec DELETE FROM kobo_shelves WHERE device_id = $1 AND media_item_id = $2; -- name: GetKoboShelfBooks :many SELECT ks.*, mi.title, mi.author, mi.file_path, mi.mime_type, mi.entitlement_id, mi.kobo_content_id, mi.revision_number FROM kobo_shelves ks JOIN media_items mi ON ks.media_item_id = mi.id WHERE ks.device_id = $1 ORDER BY ks.shelf_position ASC, ks.added_at ASC; -- name: GetKoboShelfBooksByShelfName :many SELECT ks.*, mi.title, mi.author, mi.file_path, mi.mime_type, mi.entitlement_id, mi.kobo_content_id, mi.revision_number FROM kobo_shelves ks JOIN media_items mi ON ks.media_item_id = mi.id WHERE ks.device_id = $1 AND ks.shelf_name = $2 ORDER BY ks.shelf_position ASC, ks.added_at ASC; -- name: UpdateKoboShelfBookPosition :exec UPDATE kobo_shelves SET shelf_position = $3, last_synced_at = NOW() WHERE device_id = $1 AND media_item_id = $2; -- name: ClearKoboShelf :exec DELETE FROM kobo_shelves WHERE device_id = $1; -- name: ClearKoboShelfByName :exec DELETE FROM kobo_shelves WHERE device_id = $1 AND shelf_name = $2; -- name: IsBookOnKoboShelf :one SELECT EXISTS( SELECT 1 FROM kobo_shelves WHERE device_id = $1 AND media_item_id = $2 ) as on_shelf; -- name: GetKoboShelfBookCount :one SELECT COUNT(*) as book_count FROM kobo_shelves WHERE device_id = $1; -- ============================================ -- KOBO ENTITLEMENT QUERIES (Phase 4) -- ============================================ -- name: CreateOrUpdateKoboEntitlement :one INSERT INTO kobo_entitlements (device_id, media_item_id, entitlement_id, content_id, revision_number, purchase_date, kobo_metadata) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (device_id, entitlement_id) DO UPDATE SET content_id = EXCLUDED.content_id, revision_number = EXCLUDED.revision_number, purchase_date = COALESCE(EXCLUDED.purchase_date, kobo_entitlements.purchase_date), book_status = 'installed', sync_status = 'synced', kobo_metadata = EXCLUDED.kobo_metadata, updated_at = NOW() RETURNING *; -- name: GetKoboEntitlementsForDevice :many SELECT ke.*, mi.title, mi.author, mi.file_path FROM kobo_entitlements ke JOIN media_items mi ON ke.media_item_id = mi.id WHERE ke.device_id = $1 ORDER BY ke.accession_date DESC; -- name: GetKoboEntitlementByContentId :one SELECT ke.*, mi.title, mi.author, mi.file_path FROM kobo_entitlements ke JOIN media_items mi ON ke.media_item_id = mi.id WHERE ke.device_id = $1 AND ke.content_id = $2; -- name: GetKoboEntitlementByEntitlementId :one SELECT ke.*, mi.title, mi.author, mi.file_path FROM kobo_entitlements ke JOIN media_items mi ON ke.media_item_id = mi.id WHERE ke.device_id = $1 AND ke.entitlement_id = $2; -- name: UpdateKoboEntitlementStatus :exec UPDATE kobo_entitlements SET book_status = $3, sync_status = 'synced', updated_at = NOW() WHERE device_id = $1 AND entitlement_id = $2; -- name: UpdateKoboEntitlementRevision :exec UPDATE kobo_entitlements SET revision_number = $3, updated_at = NOW() WHERE device_id = $1 AND entitlement_id = $2; -- name: DeleteKoboEntitlement :exec DELETE FROM kobo_entitlements WHERE device_id = $1 AND entitlement_id = $2; -- name: GenerateKoboEntitlementId :one SELECT 'kobo_' || uuid_generate_v4()::TEXT as entitlement_id; -- name: UpdateMediaItemKoboMetadata :one UPDATE media_items SET entitlement_id = $2, kobo_content_id = $3, revision_number = COALESCE($4, revision_number) + 1, kobo_metadata = $5, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: GetMediaItemByKoboContentId :one SELECT * FROM media_items WHERE kobo_content_id = $1; -- Media Items Admin Operations