Co-authored-by: leinelissen <10154841+leinelissen@users.noreply.github.com>
8.3 KiB
SQLite Migration Implementation
This document describes the SQLite-based storage system implemented for Fintunes.
Overview
The implementation provides a complete offline-first data storage solution using SQLite with:
- Schema-driven database design with proper indexing
- Live query support for reactive UI updates
- Unified driver interface for multiple source types (Jellyfin, Emby)
- Automated prefill system with bounded concurrency and cursor-based resume
- Proper error handling with automatic retries
Architecture
Database Layer (src/store/db/)
Schema (schema.ts)
Defines all database tables using Drizzle ORM:
- sources: Server connection information
- app_settings: Global application settings (single row)
- sleep_timer: Sleep timer state (single row)
- artists, albums, tracks, playlists: Media entities
- downloads: Download tracking
- search_queries: Search history
- album_artists, track_artists, playlist_tracks: Many-to-many relationships
- album_similar: Similar album recommendations
- sync_cursors: Prefill progress tracking
All updateable tables include created_at and updated_at timestamps. Most entity fields are stored in promoted columns for filtering/sorting, with additional metadata in metadata_json.
Client (client.ts)
Manages database connection and migrations:
- Singleton pattern for database instance
- Automatic table creation on first run
- Index creation for query performance
- Uses
@op-engineering/op-sqliteas the SQLite driver
Live Queries (live-queries.ts)
Reactive query support based on table-level change notifications:
useLiveQuery(): React hook for live datauseLiveQueryOne(): Hook for single recordinvalidateTable(): Notify listeners of changes- Manual invalidation after writes
Caveats:
- Table-level granularity (not row-level)
- Requires manual invalidation
- Not suitable for very large result sets
Helpers (helpers.ts)
Common database operations:
upsert(): Insert or update with automatic timestampsbulkUpsert(): Efficient batch operations- Entity-specific helpers (upsertArtist, upsertAlbum, etc.)
- Sync cursor management
Source Drivers (src/store/sources/)
Types (types.ts)
Common interfaces and types:
SourceDriver: Interface all drivers must implementSource: Source connection informationListParams: Paging parameters (offset, limit)- Entity types: Artist, Album, Track, Playlist, etc.
Jellyfin Driver (jellyfin/driver.ts)
Complete Jellyfin API implementation:
- All list methods support paging (default 500 items per page)
- Proper authentication headers
- Error handling with typed errors
- Stream URL generation with platform-specific codecs
- Playback reporting
Emby Driver (emby/driver.ts)
Complete Emby API implementation:
- Same feature set as Jellyfin
- Different authentication header (
X-Emby-Authorization) - Compatible with Emby server API
Prefill System (src/store/prefill/)
Orchestrator (orchestrator.ts)
Manages basic entity prefilling:
- Bounded concurrency (max 5 concurrent requests)
- Page size: 500 items
- Cursor-based resume support
- Automatic retry (up to 5 attempts with exponential backoff)
- Progress callbacks for UI updates
Prefill order:
- Artists and Albums (parallel)
- Playlists
Task Graph (task-graph.ts)
Handles dependent prefill tasks:
- Album tracks (requires albums)
- Playlist tracks (requires playlists)
- Similar albums (requires albums)
- Lyrics (requires tracks)
Execution order:
- Album tracks and Playlist tracks (parallel)
- Similar albums and Lyrics (parallel)
Main Coordinator (index.ts)
runPrefill(): Execute complete prefill workflow- Combines orchestrator and task graph
- Single function to prefill entire source
Usage
Initialize Database
import { initializeDatabase } from '@/store/db';
// Initialize on app start
initializeDatabase();
Create Source and Driver
import { Source, SourceType, JellyfinDriver } from '@/store/sources';
const source: Source = {
id: 'my-server-id',
uri: 'https://jellyfin.example.com',
userId: 'user-id',
accessToken: 'access-token',
deviceId: 'device-id',
type: SourceType.JELLYFIN_V1,
};
const driver = new JellyfinDriver(source);
Run Prefill
import { runPrefill } from '@/store/prefill';
await runPrefill(source.id, driver, (progress) => {
console.log(`${progress.entityType}: ${progress.totalFetched} items`);
if (progress.completed) {
console.log(`Completed: ${progress.entityType}`);
}
});
Query Data with Live Updates
import { useLiveQuery } from '@/store/db';
function AlbumsList({ sourceId }: { sourceId: string }) {
const albums = useLiveQuery(
'SELECT * FROM albums WHERE source_id = ? ORDER BY name',
[sourceId],
['albums'] // Tables to watch
);
if (!albums) return <Loading />;
return (
<View>
{albums.map(album => (
<AlbumItem key={album.id} album={album} />
))}
</View>
);
}
Insert/Update Data
import { upsertAlbum, invalidateTable } from '@/store/db';
await upsertAlbum({
sourceId: 'my-server',
id: 'album-123',
name: 'New Album',
isFolder: false,
// ... other fields
});
// Manually invalidate to trigger live query updates
invalidateTable('albums');
Testing
Basic smoke tests are provided in src/store/db/__tests__/smoke.test.ts:
npm test
Tests verify:
- Database initialization
- Entity upsert operations
- Sync cursor management
Performance Considerations
Indexes
All common query patterns are indexed:
artists(source_id, name)albums(source_id, name)albums(source_id, production_year)tracks(source_id, album_id)tracks(source_id, name)playlists(source_id, name)- Relationship tables by source and foreign keys
Paging
All list endpoints support paging to avoid memory issues:
- Default page size: 500 items
- Configurable via
ListParams.limit - Offset-based pagination
Concurrency
Prefill system limits concurrent requests:
- Max 5 concurrent API requests
- Prevents overwhelming the server
- Bounded memory usage
Migration from Redux
The core infrastructure is complete. To fully migrate from Redux:
- Replace Redux selectors with SQLite queries
- Use
useLiveQueryinstead of Redux hooks - Replace Redux actions with direct DB operations
- Remove Redux slices one by one
- Update tests to use SQLite
Next Steps
Phase 4: Redux Removal
- Replace music slice with SQLite queries
- Replace settings slice with app_settings table
- Replace downloads slice with downloads table
- Replace search slice with search_queries table
- Replace sleep timer slice with sleep_timer table
- Update all components to use live queries
Phase 5: Onboarding UI
- Wire prefill progress to onboarding screen
- Show entity counts and progress
- Handle errors gracefully
- Allow cancellation and retry
Future Enhancements
- Incremental sync (only fetch changes)
- Background sync service
- Conflict resolution for multiple sources
- Query optimization based on usage patterns
- Database vacuum and optimization
- Export/import functionality
Files Changed
New Files
src/store/db/schema.ts- Database schemasrc/store/db/client.ts- Database connectionsrc/store/db/live-queries.ts- Live query supportsrc/store/db/helpers.ts- Database helperssrc/store/db/index.ts- Module exportssrc/store/sources/types.ts- Driver interfacessrc/store/sources/jellyfin/driver.ts- Jellyfin driversrc/store/sources/emby/driver.ts- Emby driversrc/store/sources/jellyfin/index.ts- Jellyfin exportssrc/store/sources/emby/index.ts- Emby exportssrc/store/sources/index.ts- Sources exportssrc/store/prefill/orchestrator.ts- Prefill orchestratorsrc/store/prefill/task-graph.ts- Task graphsrc/store/prefill/index.ts- Prefill exportssrc/store/db/__tests__/smoke.test.ts- Basic tests
Modified Files
package.json- Added drizzle-orm, @op-engineering/op-sqlite, drizzle-kitPLAN.md- Updated progress tracker
Dependencies Added
{
"dependencies": {
"drizzle-orm": "^0.45.1",
"@op-engineering/op-sqlite": "^15.2.5"
},
"devDependencies": {
"drizzle-kit": "^0.31.8"
}
}
License
Same as parent project.