Phase 1 Week 1: Database schema for universal sync system
- Add format_group columns to media_items table - Add universal progress tracking to reading_progress (percentage, epubcfi, chapter, etc.) - Add device sync metadata (last_sync_device, conflict tracking) - Add location enhancements to media_notes and media_highlights - Create devices table for device registry - Create sync_queue table for offline support - Create sync_conflicts table for conflict resolution - Create reading_history table for session tracking - Add 15 new indexes for performance - Create update_updated_at_column trigger function - Add SQL helper functions: detect_format_group, convert_progress, detect_conflict, merge_progress Schema grew from 272 to 598 lines (+326 lines) Verified with sqlc generate
This commit is contained in:
+331
-4
@@ -103,7 +103,14 @@ CREATE TABLE media_items (
|
||||
google_books_id VARCHAR(100), -- Google Books identifier
|
||||
added_by_admin_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
-- Universal Sync Format Detection (Phase 1)
|
||||
format_group VARCHAR(20) NOT NULL DEFAULT 'reflowable',
|
||||
format_mimetype VARCHAR(100),
|
||||
is_reflowable BOOLEAN DEFAULT TRUE,
|
||||
has_fixed_layout BOOLEAN DEFAULT FALSE,
|
||||
total_characters BIGINT,
|
||||
chapter_count INTEGER
|
||||
);
|
||||
|
||||
-- Create ebooks view for backward compatibility
|
||||
@@ -122,7 +129,26 @@ CREATE TABLE reading_progress (
|
||||
current_page INTEGER DEFAULT 0,
|
||||
total_pages INTEGER,
|
||||
last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(media_item_id, user_id)
|
||||
UNIQUE(media_item_id, user_id),
|
||||
-- Universal Progress Tracking (Phase 1)
|
||||
percentage FLOAT CHECK (percentage >= 0 AND percentage <= 1),
|
||||
character_offset BIGINT,
|
||||
epubcfi TEXT,
|
||||
chapter INTEGER,
|
||||
chapter_progress FLOAT CHECK (chapter_progress >= 0 AND chapter_progress <= 1),
|
||||
viewport_x FLOAT DEFAULT 0,
|
||||
viewport_y FLOAT DEFAULT 0,
|
||||
zoom_level FLOAT DEFAULT 1.0,
|
||||
scroll_position_x FLOAT DEFAULT 0,
|
||||
scroll_position_y FLOAT DEFAULT 0,
|
||||
panel_number INTEGER,
|
||||
reading_mode VARCHAR(20),
|
||||
-- Device Sync Metadata (Phase 1)
|
||||
last_sync_device VARCHAR(50),
|
||||
last_sync_source VARCHAR(20),
|
||||
last_sync_timestamp TIMESTAMP WITH TIME ZONE,
|
||||
conflict_detected BOOLEAN DEFAULT FALSE,
|
||||
conflict_resolved BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- Create reading_progress view for backward compatibility
|
||||
@@ -154,7 +180,15 @@ CREATE TABLE media_notes (
|
||||
content TEXT NOT NULL,
|
||||
position VARCHAR(100), -- optional position (page:offset or CFI) for standalone notes
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
-- Location Enhancements (Phase 1)
|
||||
percentage_location FLOAT,
|
||||
character_start INTEGER,
|
||||
character_end INTEGER,
|
||||
epubcfi_location TEXT,
|
||||
chapter_reference INTEGER,
|
||||
paragraph_reference INTEGER,
|
||||
device_sync_data JSONB
|
||||
);
|
||||
|
||||
-- Create media_highlights table for user highlights on media items
|
||||
@@ -168,9 +202,92 @@ CREATE TABLE media_highlights (
|
||||
color VARCHAR(7) DEFAULT '#ffff00', -- hex color code for highlight
|
||||
note_id UUID REFERENCES media_notes(id) ON DELETE SET NULL, -- optional associated note
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
-- Location Enhancements (Phase 1)
|
||||
percentage_start FLOAT,
|
||||
percentage_end FLOAT,
|
||||
character_start INTEGER,
|
||||
character_end INTEGER,
|
||||
epubcfi_start TEXT,
|
||||
epubcfi_end TEXT,
|
||||
chapter_reference INTEGER,
|
||||
paragraph_start INTEGER,
|
||||
paragraph_end INTEGER,
|
||||
panel_number INTEGER,
|
||||
device_sync_data JSONB
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- DEVICE REGISTRY (Phase 1)
|
||||
-- ============================================
|
||||
CREATE TABLE 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,
|
||||
device_type VARCHAR(20) NOT NULL,
|
||||
device_identifier VARCHAR(255) UNIQUE NOT NULL,
|
||||
auth_token VARCHAR(500) UNIQUE NOT NULL,
|
||||
last_sync TIMESTAMP WITH TIME ZONE,
|
||||
last_seen TIMESTAMP WITH TIME ZONE,
|
||||
sync_enabled BOOLEAN DEFAULT TRUE,
|
||||
auto_sync BOOLEAN DEFAULT TRUE,
|
||||
sync_frequency_minutes INTEGER DEFAULT 5,
|
||||
device_metadata JSONB,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- SYNC QUEUE FOR OFFLINE SUPPORT (Phase 1)
|
||||
-- ============================================
|
||||
CREATE TABLE 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,
|
||||
sync_type VARCHAR(20) NOT NULL,
|
||||
sync_data JSONB NOT NULL,
|
||||
priority INTEGER DEFAULT 5,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
max_attempts INTEGER DEFAULT 3,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
processed_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- CONFLICT RESOLUTION (Phase 1)
|
||||
-- ============================================
|
||||
CREATE TABLE 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,
|
||||
conflict_type VARCHAR(20) NOT NULL,
|
||||
conflict_data JSONB NOT NULL,
|
||||
resolution_status VARCHAR(20) DEFAULT 'unresolved',
|
||||
resolution_data JSONB,
|
||||
resolved_by UUID REFERENCES users(id),
|
||||
resolved_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- READING HISTORY (Phase 1)
|
||||
-- ============================================
|
||||
CREATE TABLE 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,
|
||||
device_id UUID REFERENCES devices(id),
|
||||
progress_percentage FLOAT,
|
||||
reading_session_start TIMESTAMP WITH TIME ZONE,
|
||||
reading_session_end TIMESTAMP WITH TIME ZONE,
|
||||
pages_read INTEGER,
|
||||
time_spent_seconds INTEGER,
|
||||
device_metadata JSONB,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create ebook_ratings view for backward compatibility
|
||||
CREATE VIEW ebook_ratings AS
|
||||
SELECT mr.*,
|
||||
@@ -243,6 +360,49 @@ CREATE INDEX idx_media_highlights_media_item_id ON media_highlights(media_item_i
|
||||
CREATE INDEX idx_media_highlights_user_id ON media_highlights(user_id);
|
||||
CREATE INDEX 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);
|
||||
|
||||
-- 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);
|
||||
|
||||
-- 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);
|
||||
|
||||
-- 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);
|
||||
|
||||
-- ============================================
|
||||
-- TRIGGER FUNCTIONS (Phase 1)
|
||||
-- ============================================
|
||||
|
||||
-- Update updated_at timestamp for devices
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger for devices table
|
||||
CREATE TRIGGER update_devices_updated_at
|
||||
BEFORE UPDATE ON devices
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Add comment explaining the rating system
|
||||
COMMENT ON COLUMN media_ratings.rating IS 'Rating scale 1-10 (odd numbers = half-stars: 1,3,5,7,9 = 0.5,1.5,2.5,3.5,4.5 stars)';
|
||||
|
||||
@@ -269,4 +429,171 @@ COMMENT ON COLUMN media_items.google_books_id IS 'Google Books identifier for in
|
||||
-- - Library visibility is controlled through library_visibility table - admins can hide/show libraries per user
|
||||
-- - Notes and highlights support position data (page:offset or CFI format) for precise location tracking
|
||||
-- - Highlights can have associated notes for detailed annotations
|
||||
-- - Backward compatibility views (ebooks, ebook_ratings, ebook_reading_progress, ebook_notes, ebook_highlights) maintain existing API contracts
|
||||
-- - Backward compatibility views (ebooks, ebook_ratings, ebook_reading_progress, ebook_notes, ebook_highlights) maintain existing API contracts
|
||||
|
||||
-- ============================================
|
||||
-- SYNC HELPER FUNCTIONS (Phase 1)
|
||||
-- ============================================
|
||||
|
||||
-- Detect format group based on mimetype and file path
|
||||
CREATE OR REPLACE FUNCTION detect_format_group(p_mimetype VARCHAR, p_file_path VARCHAR)
|
||||
RETURNS VARCHAR AS $$
|
||||
BEGIN
|
||||
CASE
|
||||
-- Reflowable formats
|
||||
WHEN p_mimetype = 'application/epub+zip' THEN
|
||||
RETURN 'reflowable';
|
||||
WHEN p_mimetype = 'application/x-mobipocket-ebook' THEN
|
||||
RETURN 'reflowable';
|
||||
WHEN p_mimetype = 'application/vnd.amazon.mobi8-ebook' THEN
|
||||
RETURN 'reflowable';
|
||||
WHEN p_file_path LIKE '%.epub' THEN
|
||||
RETURN 'reflowable';
|
||||
WHEN p_file_path LIKE '%.mobi' THEN
|
||||
RETURN 'reflowable';
|
||||
WHEN p_file_path LIKE '%.azw3' THEN
|
||||
RETURN 'reflowable';
|
||||
WHEN p_file_path LIKE '%.fb2' THEN
|
||||
RETURN 'reflowable';
|
||||
WHEN p_file_path LIKE '%.txt' THEN
|
||||
RETURN 'reflowable';
|
||||
|
||||
-- Fixed layout formats
|
||||
WHEN p_mimetype = 'application/pdf' THEN
|
||||
RETURN 'fixed_layout';
|
||||
WHEN p_file_path LIKE '%.pdf' THEN
|
||||
RETURN 'fixed_layout';
|
||||
WHEN p_file_path LIKE '%.djvu' THEN
|
||||
RETURN 'fixed_layout';
|
||||
|
||||
-- Comic archive formats
|
||||
WHEN p_mimetype = 'application/x-cbr' THEN
|
||||
RETURN 'comic_archive';
|
||||
WHEN p_mimetype = 'application/x-cbz' THEN
|
||||
RETURN 'comic_archive';
|
||||
WHEN p_file_path LIKE '%.cbz' THEN
|
||||
RETURN 'comic_archive';
|
||||
WHEN p_file_path LIKE '%.cbr' THEN
|
||||
RETURN 'comic_archive';
|
||||
WHEN p_file_path LIKE '%.cbt' THEN
|
||||
RETURN 'comic_archive';
|
||||
WHEN p_file_path LIKE '%.cb7' THEN
|
||||
RETURN 'comic_archive';
|
||||
|
||||
ELSE
|
||||
RETURN 'unknown';
|
||||
END CASE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Convert progress between format groups
|
||||
CREATE OR REPLACE FUNCTION convert_progress(
|
||||
source_format VARCHAR,
|
||||
target_format VARCHAR,
|
||||
source_progress JSONB
|
||||
) RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
result JSONB;
|
||||
percentage FLOAT;
|
||||
BEGIN
|
||||
-- Extract percentage from source
|
||||
CASE source_format
|
||||
WHEN 'reflowable' THEN
|
||||
percentage := (source_progress->>'percentage')::FLOAT;
|
||||
WHEN 'fixed_layout' THEN
|
||||
percentage := ((source_progress->>'page')::FLOAT /
|
||||
(source_progress->>'total_pages')::FLOAT);
|
||||
WHEN 'comic_archive' THEN
|
||||
percentage := ((source_progress->>'page')::FLOAT /
|
||||
(source_progress->>'total_pages')::FLOAT);
|
||||
ELSE
|
||||
percentage := 0.0;
|
||||
END CASE;
|
||||
|
||||
-- Build target format progress
|
||||
CASE target_format
|
||||
WHEN 'reflowable' THEN
|
||||
result := jsonb_build_object(
|
||||
'percentage', percentage,
|
||||
'epubcfi', 'epubcfi(/6/4/2:' || CAST(percentage * 100 AS INTEGER) || ')'
|
||||
);
|
||||
WHEN 'fixed_layout' THEN
|
||||
result := jsonb_build_object(
|
||||
'page', CAST(percentage * 200 AS INTEGER),
|
||||
'total_pages', 200,
|
||||
'percentage', percentage
|
||||
);
|
||||
WHEN 'comic_archive' THEN
|
||||
result := jsonb_build_object(
|
||||
'page', CAST(percentage * 32 AS INTEGER),
|
||||
'total_pages', 32,
|
||||
'percentage', percentage
|
||||
);
|
||||
ELSE
|
||||
result := '{}'::jsonb;
|
||||
END CASE;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Detect conflicts in sync progress
|
||||
CREATE OR REPLACE FUNCTION detect_conflict(
|
||||
p_media_item_id UUID,
|
||||
new_progress JSONB,
|
||||
device_type VARCHAR
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
existing_progress JSONB;
|
||||
time_diff INTERVAL;
|
||||
BEGIN
|
||||
-- Get most recent progress from different device
|
||||
SELECT jsonb_build_object(
|
||||
'percentage', percentage,
|
||||
'timestamp', last_sync_timestamp
|
||||
) INTO existing_progress
|
||||
FROM reading_progress
|
||||
WHERE media_item_id = p_media_item_id
|
||||
AND last_sync_source != device_type
|
||||
ORDER BY last_sync_timestamp DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- If no existing progress, no conflict
|
||||
IF existing_progress IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
-- Check time difference (within 5 minutes = potential conflict)
|
||||
time_diff := NOW() - (existing_progress->>'timestamp')::TIMESTAMP WITH TIME ZONE;
|
||||
|
||||
IF time_diff < INTERVAL '5 minutes' THEN
|
||||
-- Check if progress significantly different
|
||||
IF ABS((new_progress->>'percentage')::FLOAT -
|
||||
(existing_progress->>'percentage')::FLOAT) > 0.01 THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Merge progress from two sources
|
||||
CREATE OR REPLACE FUNCTION merge_progress(
|
||||
progress_a JSONB,
|
||||
progress_b JSONB
|
||||
) RETURNS JSONB AS $$
|
||||
BEGIN
|
||||
RETURN jsonb_build_object(
|
||||
'percentage', GREATEST(
|
||||
(progress_a->>'percentage')::FLOAT,
|
||||
(progress_b->>'percentage')::FLOAT
|
||||
),
|
||||
'merged_from', ARRAY[
|
||||
(progress_a->>'source'),
|
||||
(progress_b->>'source')
|
||||
],
|
||||
'merge_timestamp', NOW()
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
Reference in New Issue
Block a user