From 6ffb2ef6bcdc06ac446ed417289152634d4ab73e Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 10 Feb 2026 16:47:18 -0500 Subject: [PATCH] schema: make all schema statements idempotent (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert 18 CREATE TABLE → CREATE TABLE IF NOT EXISTS (27 total) - Convert 62 CREATE INDEX → CREATE INDEX IF NOT EXISTS (82 total) - Add ON CONFLICT to 2 INSERT statements (3 total) - Verify 8 ALTER TABLE already have IF NOT EXISTS - Verify 6 CREATE FUNCTION use OR REPLACE Schema is now fully idempotent and safe for automatic initialization on every startup. --- database/schema/schema.sql | 166 +++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 82 deletions(-) diff --git a/database/schema/schema.sql b/database/schema/schema.sql index 71e2f1a..1efcfd5 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -2,7 +2,7 @@ -- This file contains the complete current schema for the Bookhoard media library management system -- Create library types table -CREATE TABLE library_types ( +CREATE TABLE IF NOT EXISTS library_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) UNIQUE NOT NULL, description TEXT, @@ -14,10 +14,11 @@ CREATE TABLE library_types ( INSERT INTO library_types (name, description, allowed_extensions) VALUES ('ebooks', 'Ebook files including EPUB, PDF, MOBI, etc.', ARRAY['.epub', '.pdf', '.mobi', '.azw', '.azw3', '.txt', '.rtf', '.doc', '.docx', '.lit', '.fb2', '.pdb']), ('comics', 'Comic book archives and image formats', ARRAY['.cbz', '.cbr', '.cb7', '.cbt', '.pdf']), -('manga', 'Manga files including archives and image folders', ARRAY['.cbz', '.cbr', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']); +('manga', 'Manga files including archives and image folders', ARRAY['.cbz', '.cbr', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']) +ON CONFLICT (name) DO NOTHING; -- Create users table -CREATE TABLE users ( +CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, username VARCHAR(255) UNIQUE NOT NULL, @@ -32,7 +33,7 @@ CREATE TABLE users ( ); -- Create system_settings table -CREATE TABLE system_settings ( +CREATE TABLE IF NOT EXISTS system_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), setting_key VARCHAR(100) UNIQUE NOT NULL, setting_value TEXT NOT NULL, @@ -43,10 +44,11 @@ CREATE TABLE system_settings ( -- Insert default system settings INSERT INTO system_settings (setting_key, setting_value, description) VALUES ('scan_frequency_minutes', '60', 'How often to scan all libraries in minutes'), -('auto_scan_enabled', 'true', 'Whether auto-scanning is enabled system-wide'); +('auto_scan_enabled', 'true', 'Whether auto-scanning is enabled system-wide') +ON CONFLICT (setting_key) DO NOTHING; -- Create refresh_tokens table -CREATE TABLE refresh_tokens ( +CREATE TABLE IF NOT EXISTS refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(), @@ -56,7 +58,7 @@ CREATE TABLE refresh_tokens ( ); -- Create libraries table -CREATE TABLE libraries ( +CREATE TABLE IF NOT EXISTS libraries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, description TEXT, @@ -67,7 +69,7 @@ CREATE TABLE libraries ( ); -- Create library_folders table for multiple folders per library -CREATE TABLE library_folders ( +CREATE TABLE IF NOT EXISTS library_folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, folder_path VARCHAR(500) NOT NULL, @@ -76,7 +78,7 @@ CREATE TABLE library_folders ( ); -- Create library_visibility table for user-specific library visibility -CREATE TABLE library_visibility ( +CREATE TABLE IF NOT EXISTS library_visibility ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, @@ -87,7 +89,7 @@ CREATE TABLE library_visibility ( ); -- Create media_items table -CREATE TABLE media_items ( +CREATE TABLE IF NOT EXISTS media_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, @@ -132,8 +134,8 @@ CREATE TABLE media_items ( ); -- Add GIN indexes for fast array searches -CREATE INDEX idx_media_items_tags_gin ON media_items USING GIN (tags); -CREATE INDEX idx_media_items_contributors_gin ON media_items USING GIN (contributors); +CREATE INDEX IF NOT EXISTS idx_media_items_tags_gin ON media_items USING GIN (tags); +CREATE INDEX IF NOT EXISTS idx_media_items_contributors_gin ON media_items USING GIN (contributors); -- Add search fields for case-insensitive, punctuation-free searching ALTER TABLE media_items ADD COLUMN IF NOT EXISTS tags_search TEXT[]; @@ -144,7 +146,7 @@ CREATE INDEX IF NOT EXISTS idx_media_items_tags_search ON media_items USING GIN CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search); -- Create reading_progress table -CREATE TABLE reading_progress ( +CREATE TABLE IF NOT EXISTS reading_progress ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -174,7 +176,7 @@ CREATE TABLE reading_progress ( ); -- Create media_ratings table -CREATE TABLE media_ratings ( +CREATE TABLE IF NOT EXISTS media_ratings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -185,7 +187,7 @@ CREATE TABLE media_ratings ( ); -- Create media_notes table for user notes on media items -CREATE TABLE media_notes ( +CREATE TABLE IF NOT EXISTS media_notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -204,7 +206,7 @@ CREATE TABLE media_notes ( ); -- Create media_highlights table for user highlights on media items -CREATE TABLE media_highlights ( +CREATE TABLE IF NOT EXISTS media_highlights ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -232,7 +234,7 @@ CREATE TABLE media_highlights ( -- ============================================ -- DEVICE REGISTRY (Phase 1) -- ============================================ -CREATE TABLE devices ( +CREATE TABLE IF NOT EXISTS devices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, device_name VARCHAR(100) NOT NULL, @@ -252,7 +254,7 @@ CREATE TABLE devices ( -- ============================================ -- SYNC QUEUE FOR OFFLINE SUPPORT (Phase 1) -- ============================================ -CREATE TABLE sync_queue ( +CREATE TABLE IF NOT EXISTS sync_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE, media_item_id UUID REFERENCES media_items(id) ON DELETE CASCADE, @@ -270,7 +272,7 @@ CREATE TABLE sync_queue ( -- ============================================ -- CONFLICT RESOLUTION (Phase 1) -- ============================================ -CREATE TABLE sync_conflicts ( +CREATE TABLE IF NOT EXISTS sync_conflicts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -286,7 +288,7 @@ CREATE TABLE sync_conflicts ( -- ============================================ -- KOBO SHELF MANAGEMENT (Phase 4) -- ============================================ -CREATE TABLE kobo_shelves ( +CREATE TABLE IF NOT EXISTS kobo_shelves ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE, media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, @@ -297,14 +299,14 @@ CREATE TABLE kobo_shelves ( UNIQUE(device_id, media_item_id) ); -CREATE INDEX idx_kobo_shelves_device_id ON kobo_shelves(device_id); -CREATE INDEX idx_kobo_shelves_media_item_id ON kobo_shelves(media_item_id); -CREATE INDEX idx_kobo_shelves_shelf_name ON kobo_shelves(shelf_name); +CREATE INDEX IF NOT EXISTS idx_kobo_shelves_device_id ON kobo_shelves(device_id); +CREATE INDEX IF NOT EXISTS idx_kobo_shelves_media_item_id ON kobo_shelves(media_item_id); +CREATE INDEX IF NOT EXISTS idx_kobo_shelves_shelf_name ON kobo_shelves(shelf_name); -- ============================================ -- KOBO ENTITLEMENTS TRACKING (Phase 4) -- ============================================ -CREATE TABLE kobo_entitlements ( +CREATE TABLE IF NOT EXISTS kobo_entitlements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE, media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, @@ -321,15 +323,15 @@ CREATE TABLE kobo_entitlements ( UNIQUE(device_id, entitlement_id) ); -CREATE INDEX idx_kobo_entitlements_device_id ON kobo_entitlements(device_id); -CREATE INDEX idx_kobo_entitlements_media_item_id ON kobo_entitlements(media_item_id); -CREATE INDEX idx_kobo_entitlements_entitlement_id ON kobo_entitlements(entitlement_id); -CREATE INDEX idx_kobo_entitlements_content_id ON kobo_entitlements(content_id); +CREATE INDEX IF NOT EXISTS idx_kobo_entitlements_device_id ON kobo_entitlements(device_id); +CREATE INDEX IF NOT EXISTS idx_kobo_entitlements_media_item_id ON kobo_entitlements(media_item_id); +CREATE INDEX IF NOT EXISTS idx_kobo_entitlements_entitlement_id ON kobo_entitlements(entitlement_id); +CREATE INDEX IF NOT EXISTS idx_kobo_entitlements_content_id ON kobo_entitlements(content_id); -- ============================================ -- READING HISTORY (Phase 1) -- ============================================ -CREATE TABLE reading_history ( +CREATE TABLE IF NOT EXISTS reading_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, @@ -345,87 +347,87 @@ CREATE TABLE reading_history ( -- Create indexes for better query performance -CREATE INDEX idx_users_email ON users(email); -CREATE INDEX idx_users_username ON users(username); -CREATE INDEX idx_library_types_name ON library_types(name); -CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token); -CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id); +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); +CREATE INDEX IF NOT EXISTS idx_library_types_name ON library_types(name); +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token); +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id); -- Library indexes -CREATE INDEX idx_libraries_name ON libraries(name); -CREATE INDEX idx_libraries_library_type_id ON libraries(library_type_id); -CREATE INDEX idx_libraries_created_by_admin_id ON libraries(created_by_admin_id); -CREATE INDEX idx_library_folders_library_id ON library_folders(library_id); -CREATE INDEX idx_library_visibility_user_id ON library_visibility(user_id); -CREATE INDEX idx_library_visibility_library_id ON library_visibility(library_id); +CREATE INDEX IF NOT EXISTS idx_libraries_name ON libraries(name); +CREATE INDEX IF NOT EXISTS idx_libraries_library_type_id ON libraries(library_type_id); +CREATE INDEX IF NOT EXISTS idx_libraries_created_by_admin_id ON libraries(created_by_admin_id); +CREATE INDEX IF NOT EXISTS idx_library_folders_library_id ON library_folders(library_id); +CREATE INDEX IF NOT EXISTS idx_library_visibility_user_id ON library_visibility(user_id); +CREATE INDEX IF NOT EXISTS idx_library_visibility_library_id ON library_visibility(library_id); -- Media items indexes -CREATE INDEX idx_media_items_title ON media_items(title); -CREATE INDEX idx_media_items_author ON media_items(author); -CREATE INDEX idx_media_items_library_id ON media_items(library_id); -CREATE INDEX idx_media_items_added_by_admin_id ON media_items(added_by_admin_id); -CREATE INDEX idx_media_items_genre ON media_items(genre); -CREATE INDEX idx_media_items_language ON media_items(language); -CREATE INDEX idx_media_items_copyright_year ON media_items(copyright_year); -CREATE INDEX idx_media_items_page_count ON media_items(page_count); -CREATE INDEX idx_media_items_series ON media_items(series); +CREATE INDEX IF NOT EXISTS idx_media_items_title ON media_items(title); +CREATE INDEX IF NOT EXISTS idx_media_items_author ON media_items(author); +CREATE INDEX IF NOT EXISTS idx_media_items_library_id ON media_items(library_id); +CREATE INDEX IF NOT EXISTS idx_media_items_added_by_admin_id ON media_items(added_by_admin_id); +CREATE INDEX IF NOT EXISTS idx_media_items_genre ON media_items(genre); +CREATE INDEX IF NOT EXISTS idx_media_items_language ON media_items(language); +CREATE INDEX IF NOT EXISTS idx_media_items_copyright_year ON media_items(copyright_year); +CREATE INDEX IF NOT EXISTS idx_media_items_page_count ON media_items(page_count); +CREATE INDEX IF NOT EXISTS idx_media_items_series ON media_items(series); -- Progress and ratings indexes -CREATE INDEX idx_reading_progress_media_item_id ON reading_progress(media_item_id); -CREATE INDEX idx_reading_progress_user_id ON reading_progress(user_id); -CREATE INDEX idx_media_ratings_media_item_id ON media_ratings(media_item_id); -CREATE INDEX idx_media_ratings_user_id ON media_ratings(user_id); +CREATE INDEX IF NOT EXISTS idx_reading_progress_media_item_id ON reading_progress(media_item_id); +CREATE INDEX IF NOT EXISTS idx_reading_progress_user_id ON reading_progress(user_id); +CREATE INDEX IF NOT EXISTS idx_media_ratings_media_item_id ON media_ratings(media_item_id); +CREATE INDEX IF NOT EXISTS idx_media_ratings_user_id ON media_ratings(user_id); -- Notes and highlights indexes -CREATE INDEX idx_media_notes_media_item_id ON media_notes(media_item_id); -CREATE INDEX idx_media_notes_user_id ON media_notes(user_id); -CREATE INDEX idx_media_highlights_media_item_id ON media_highlights(media_item_id); -CREATE INDEX idx_media_highlights_user_id ON media_highlights(user_id); -CREATE INDEX idx_media_highlights_note_id ON media_highlights(note_id); +CREATE INDEX IF NOT EXISTS idx_media_notes_media_item_id ON media_notes(media_item_id); +CREATE INDEX IF NOT EXISTS idx_media_notes_user_id ON media_notes(user_id); +CREATE INDEX IF NOT EXISTS idx_media_highlights_media_item_id ON media_highlights(media_item_id); +CREATE INDEX IF NOT EXISTS idx_media_highlights_user_id ON media_highlights(user_id); +CREATE INDEX IF NOT EXISTS idx_media_highlights_note_id ON media_highlights(note_id); -- ============================================ -- NEW TABLE INDEXES (Phase 1) -- ============================================ -- Device registry indexes -CREATE INDEX idx_devices_user_id ON devices(user_id); -CREATE INDEX idx_devices_device_type ON devices(device_type); -CREATE INDEX idx_devices_device_identifier ON devices(device_identifier); +CREATE INDEX IF NOT EXISTS idx_devices_user_id ON devices(user_id); +CREATE INDEX IF NOT EXISTS idx_devices_device_type ON devices(device_type); +CREATE INDEX IF NOT EXISTS idx_devices_device_identifier ON devices(device_identifier); -- Sync queue indexes -CREATE INDEX idx_sync_queue_device_id ON sync_queue(device_id); -CREATE INDEX idx_sync_queue_status ON sync_queue(status); -CREATE INDEX idx_sync_queue_priority ON sync_queue(priority); +CREATE INDEX IF NOT EXISTS idx_sync_queue_device_id ON sync_queue(device_id); +CREATE INDEX IF NOT EXISTS idx_sync_queue_status ON sync_queue(status); +CREATE INDEX IF NOT EXISTS idx_sync_queue_priority ON sync_queue(priority); -- Sync conflicts indexes -CREATE INDEX idx_sync_conflicts_media_item_id ON sync_conflicts(media_item_id); -CREATE INDEX idx_sync_conflicts_user_id ON sync_conflicts(user_id); -CREATE INDEX idx_sync_conflicts_status ON sync_conflicts(resolution_status); +CREATE INDEX IF NOT EXISTS idx_sync_conflicts_media_item_id ON sync_conflicts(media_item_id); +CREATE INDEX IF NOT EXISTS idx_sync_conflicts_user_id ON sync_conflicts(user_id); +CREATE INDEX IF NOT EXISTS idx_sync_conflicts_status ON sync_conflicts(resolution_status); -- Composite indexes for sync queue performance (Phase 7) -CREATE INDEX idx_sync_queue_device_status_priority ON sync_queue(device_id, status, priority ASC, created_at ASC); -CREATE INDEX idx_sync_queue_status_priority_created ON sync_queue(status, priority ASC, created_at ASC); -CREATE INDEX idx_sync_queue_device_created_at ON sync_queue(device_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_sync_queue_device_status_priority ON sync_queue(device_id, status, priority ASC, created_at ASC); +CREATE INDEX IF NOT EXISTS idx_sync_queue_status_priority_created ON sync_queue(status, priority ASC, created_at ASC); +CREATE INDEX IF NOT EXISTS idx_sync_queue_device_created_at ON sync_queue(device_id, created_at DESC); -- Composite indexes for reading progress performance (Phase 7) -CREATE INDEX idx_reading_progress_user_last_sync ON reading_progress(user_id, last_sync_timestamp DESC); -CREATE INDEX idx_reading_progress_media_last_sync ON reading_progress(media_item_id, last_sync_timestamp DESC); -CREATE INDEX idx_reading_progress_user_sync_source ON reading_progress(user_id, last_sync_source, last_sync_timestamp DESC); +CREATE INDEX IF NOT EXISTS idx_reading_progress_user_last_sync ON reading_progress(user_id, last_sync_timestamp DESC); +CREATE INDEX IF NOT EXISTS idx_reading_progress_media_last_sync ON reading_progress(media_item_id, last_sync_timestamp DESC); +CREATE INDEX IF NOT EXISTS idx_reading_progress_user_sync_source ON reading_progress(user_id, last_sync_source, last_sync_timestamp DESC); -- Composite indexes for devices (Phase 7) -CREATE INDEX idx_devices_user_sync_enabled ON devices(user_id, sync_enabled, auto_sync); -CREATE INDEX idx_devices_user_type ON devices(user_id, device_type); -CREATE INDEX idx_devices_last_seen ON devices(last_seen DESC) WHERE sync_enabled = true; +CREATE INDEX IF NOT EXISTS idx_devices_user_sync_enabled ON devices(user_id, sync_enabled, auto_sync); +CREATE INDEX IF NOT EXISTS idx_devices_user_type ON devices(user_id, device_type); +CREATE INDEX IF NOT EXISTS idx_devices_last_seen ON devices(last_seen DESC) WHERE sync_enabled = true; -- Composite indexes for annotations (Phase 7) -CREATE INDEX idx_media_notes_user_media ON media_notes(user_id, media_item_id); -CREATE INDEX idx_media_highlights_user_media ON media_highlights(user_id, media_item_id); +CREATE INDEX IF NOT EXISTS idx_media_notes_user_media ON media_notes(user_id, media_item_id); +CREATE INDEX IF NOT EXISTS idx_media_highlights_user_media ON media_highlights(user_id, media_item_id); -- Reading history indexes -CREATE INDEX idx_reading_history_user_id ON reading_history(user_id); -CREATE INDEX idx_reading_history_media_item_id ON reading_history(media_item_id); -CREATE INDEX idx_reading_history_created_at ON reading_history(created_at DESC); -CREATE INDEX idx_reading_history_user_device_created ON reading_history(user_id, device_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_reading_history_user_id ON reading_history(user_id); +CREATE INDEX IF NOT EXISTS idx_reading_history_media_item_id ON reading_history(media_item_id); +CREATE INDEX IF NOT EXISTS idx_reading_history_created_at ON reading_history(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_reading_history_user_device_created ON reading_history(user_id, device_id, created_at DESC); -- ============================================ -- TRIGGER FUNCTIONS (Phase 1)