1pub mod db_service;
7pub mod models;
8pub mod schema;
9
10use crate::utils::lock::MutexSafe;
11use std::path::PathBuf;
12use std::sync::{Arc, Mutex};
13
14use log::{debug, error, info};
15use rusqlite::{Connection, Result as SqliteResult};
16
17pub use db_service::{DatabaseService, RusqliteService};
18use schema::MIGRATIONS;
19
20const BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
23
24const READER_CONNECTIONS: usize = 3;
28
29pub struct Database {
35 #[cfg_attr(not(test), allow(dead_code))]
39 conn: Arc<Mutex<Connection>>,
40 service: RusqliteService,
41 path: PathBuf,
42}
43
44impl Database {
45 pub fn open(path: &PathBuf) -> SqliteResult<Self> {
47 if let Some(parent) = path.parent() {
49 std::fs::create_dir_all(parent).ok();
50 }
51
52 let conn = Connection::open(path)?;
53
54 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
56
57 conn.execute_batch("PRAGMA journal_mode = WAL;")?;
59 conn.execute_batch("PRAGMA synchronous = NORMAL;")?;
63 conn.busy_timeout(BUSY_TIMEOUT)?;
64
65 let conn = Arc::new(Mutex::new(conn));
66 Self::migrate_connection(&conn, MIGRATIONS)?;
67
68 if let Err(e) = conn
74 .lock_safe()
75 .execute_batch("PRAGMA analysis_limit = 400; PRAGMA optimize = 0x10002;")
76 {
77 error!("PRAGMA optimize failed: {}", e);
78 }
79
80 let readers = (0..READER_CONNECTIONS)
82 .map(|_| Self::open_reader(path))
83 .collect::<SqliteResult<Vec<_>>>()?;
84
85 Ok(Self {
86 service: RusqliteService::with_readers(Arc::clone(&conn), readers),
87 conn,
88 path: path.clone(),
89 })
90 }
91
92 fn open_reader(path: &PathBuf) -> SqliteResult<Connection> {
95 let conn = Connection::open(path)?;
96 conn.busy_timeout(BUSY_TIMEOUT)?;
97 conn.execute_batch("PRAGMA query_only = ON;")?;
98 Ok(conn)
99 }
100
101 #[cfg(test)]
103 pub fn open_in_memory() -> SqliteResult<Self> {
104 let conn = Connection::open_in_memory()?;
105
106 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
108
109 let conn = Arc::new(Mutex::new(conn));
110 Self::migrate_connection(&conn, MIGRATIONS)?;
111
112 Ok(Self {
115 service: RusqliteService::new(Arc::clone(&conn)),
116 conn,
117 path: PathBuf::from(":memory:"),
118 })
119 }
120
121 #[cfg(test)]
123 pub fn connection(&self) -> Arc<Mutex<Connection>> {
124 Arc::clone(&self.conn)
125 }
126
127 #[cfg(test)]
130 pub fn migrate(&self) -> SqliteResult<()> {
131 self.migrate_with(MIGRATIONS)
132 }
133
134 #[cfg(test)]
136 fn migrate_with(&self, migrations: &[(&str, &str)]) -> SqliteResult<()> {
137 Self::migrate_connection(&self.conn, migrations)
138 }
139
140 fn migrate_connection(
161 conn: &Mutex<Connection>,
162 migrations: &[(&str, &str)],
163 ) -> SqliteResult<()> {
164 info!("Starting database migrations...");
165 let conn = conn.lock_safe();
166
167 debug!("Creating _migrations table if it doesn't exist...");
169 match conn.execute(
170 "CREATE TABLE IF NOT EXISTS _migrations (
171 id INTEGER PRIMARY KEY,
172 name TEXT NOT NULL UNIQUE,
173 applied_at TEXT DEFAULT CURRENT_TIMESTAMP
174 )",
175 [],
176 ) {
177 Ok(_) => debug!("_migrations table ready"),
178 Err(e) => {
179 error!("Failed to create _migrations table: {}", e);
180 return Err(e);
181 }
182 }
183
184 debug!("Querying applied migrations...");
186 let mut stmt = conn.prepare("SELECT name FROM _migrations")?;
187 let applied: Vec<String> = stmt
188 .query_map([], |row: &rusqlite::Row| row.get(0))?
189 .filter_map(|r| r.ok())
190 .collect();
191 debug!("Found {} applied migrations", applied.len());
192
193 for (name, sql) in migrations {
195 if applied.contains(&name.to_string()) {
196 debug!("Skipping already applied migration: {}", name);
197 continue;
198 }
199
200 info!("Applying migration: {}", name);
201
202 let tx = conn.unchecked_transaction()?;
206
207 if let Err(e) = tx.execute_batch(sql) {
208 error!("Failed to apply migration {} (rolled back): {}", name, e);
209 return Err(e);
210 }
211 if let Err(e) = tx.execute("INSERT INTO _migrations (name) VALUES (?1)", [name]) {
212 error!("Failed to record migration {} (rolled back): {}", name, e);
213 return Err(e);
214 }
215 tx.commit()?;
216
217 info!("Successfully applied migration: {}", name);
218 }
219
220 info!("All migrations completed successfully");
221 Ok(())
222 }
223
224 pub fn service(&self) -> RusqliteService {
227 self.service.clone()
228 }
229
230 pub fn path(&self) -> &PathBuf {
232 &self.path
233 }
234
235 pub fn file_size(&self) -> Option<u64> {
237 std::fs::metadata(&self.path).ok().map(|m| m.len())
238 }
239}
240
241#[cfg(test)]
243mod tests {
244 use super::*;
245 use rusqlite::params;
246
247 #[test]
248 fn test_open_in_memory() {
249 let db = Database::open_in_memory().unwrap();
250 assert_eq!(db.path().to_str(), Some(":memory:"));
251 }
252
253 #[test]
264 fn test_failed_migration_rolls_back_and_stays_retryable() {
265 let db = Database::open_in_memory().unwrap();
266
267 let poisoned = &[(
269 "900_partially_failing",
270 "ALTER TABLE downloads ADD COLUMN audit_a TEXT;
271 ALTER TABLE downloads ADD COLUMN audit_b TEXT FROM NOWHERE;
272 ALTER TABLE downloads ADD COLUMN audit_c TEXT;",
273 )][..];
274
275 let first = db.migrate_with(poisoned).unwrap_err();
276
277 assert!(
279 !has_column(&db, "downloads", "audit_a"),
280 "statement 1 of a failed migration was left applied: the batch did not roll back"
281 );
282 assert!(!has_column(&db, "downloads", "audit_c"));
283
284 let recorded: i64 = db
286 .connection()
287 .lock_safe()
288 .query_row(
289 "SELECT COUNT(*) FROM _migrations WHERE name = ?1",
290 ["900_partially_failing"],
291 |r| r.get(0),
292 )
293 .unwrap();
294 assert_eq!(recorded, 0, "a failed migration must not be recorded");
295
296 let second = db.migrate_with(poisoned).unwrap_err();
299 assert!(
300 !second.to_string().contains("duplicate column"),
301 "the retry hit leftovers from the failed run instead of the real error: {second}"
302 );
303 assert_eq!(first.to_string(), second.to_string());
304
305 let fixed = &[(
307 "900_partially_failing",
308 "ALTER TABLE downloads ADD COLUMN audit_a TEXT;
309 ALTER TABLE downloads ADD COLUMN audit_c TEXT;",
310 )][..];
311 db.migrate_with(fixed).unwrap();
312 assert!(has_column(&db, "downloads", "audit_a"));
313 assert!(has_column(&db, "downloads", "audit_c"));
314 }
315
316 #[test]
320 fn test_successful_migration_is_recorded_in_the_same_transaction() {
321 let db = Database::open_in_memory().unwrap();
322 let m = &[(
323 "901_adds_a_column",
324 "ALTER TABLE downloads ADD COLUMN audit_d TEXT;",
325 )][..];
326
327 db.migrate_with(m).unwrap();
328 db.migrate_with(m).unwrap();
330 assert!(has_column(&db, "downloads", "audit_d"));
331 }
332
333 fn has_column(db: &Database, table: &str, column: &str) -> bool {
334 let conn = db.connection();
335 let conn = conn.lock_safe();
336 let mut stmt = conn
337 .prepare(&format!("PRAGMA table_info({table})"))
338 .unwrap();
339 let mut names = stmt
340 .query_map([], |row| row.get::<_, String>(1))
341 .unwrap()
342 .filter_map(|r| r.ok());
343 names.any(|n| n == column)
344 }
345
346 #[test]
347 fn test_migrations_run() {
348 let db = Database::open_in_memory().unwrap();
349 let conn = db.connection();
350 let conn = conn.lock_safe();
351
352 let mut stmt = conn
354 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='items'")
355 .unwrap();
356 let exists: Option<String> = stmt.query_row([], |row: &rusqlite::Row| row.get(0)).ok();
357 assert!(exists.is_some());
358 }
359
360 #[test]
361 fn test_all_tables_created() {
362 let db = Database::open_in_memory().unwrap();
363 let conn = db.connection();
364 let conn = conn.lock_safe();
365
366 let expected_tables = [
367 "servers",
368 "users",
369 "libraries",
370 "items",
371 "media_streams",
372 "user_data",
373 "downloads",
374 "sync_queue",
375 "thumbnails",
376 "playlists",
377 "playlist_items",
378 "genres",
379 ];
380
381 for table in expected_tables {
382 let exists: Option<String> = conn
383 .query_row(
384 "SELECT name FROM sqlite_master WHERE type='table' AND name=?1",
385 [table],
386 |row: &rusqlite::Row| row.get(0),
387 )
388 .ok();
389 assert!(exists.is_some(), "Table '{}' should exist", table);
390 }
391 }
392
393 #[test]
394 fn test_fts_table_created() {
395 let db = Database::open_in_memory().unwrap();
396 let conn = db.connection();
397 let conn = conn.lock_safe();
398
399 let exists: Option<String> = conn
400 .query_row(
401 "SELECT name FROM sqlite_master WHERE type='table' AND name='items_fts'",
402 [],
403 |row: &rusqlite::Row| row.get(0),
404 )
405 .ok();
406 assert!(exists.is_some(), "FTS table 'items_fts' should exist");
407 }
408
409 #[test]
410 fn test_server_crud() {
411 let db = Database::open_in_memory().unwrap();
412 let conn = db.connection();
413 let conn = conn.lock_safe();
414
415 conn.execute(
417 "INSERT INTO servers (id, name, url, version) VALUES (?1, ?2, ?3, ?4)",
418 params!["server1", "My Server", "http://localhost:8096", "10.8.0"],
419 )
420 .unwrap();
421
422 let (name, url): (String, String) = conn
424 .query_row(
425 "SELECT name, url FROM servers WHERE id = ?1",
426 ["server1"],
427 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
428 )
429 .unwrap();
430 assert_eq!(name, "My Server");
431 assert_eq!(url, "http://localhost:8096");
432
433 conn.execute(
435 "UPDATE servers SET name = ?1 WHERE id = ?2",
436 params!["Updated Server", "server1"],
437 )
438 .unwrap();
439
440 let name: String = conn
441 .query_row(
442 "SELECT name FROM servers WHERE id = ?1",
443 ["server1"],
444 |row: &rusqlite::Row| row.get(0),
445 )
446 .unwrap();
447 assert_eq!(name, "Updated Server");
448
449 conn.execute("DELETE FROM servers WHERE id = ?1", ["server1"])
451 .unwrap();
452
453 let count: i32 = conn
454 .query_row("SELECT COUNT(*) FROM servers", [], |row: &rusqlite::Row| {
455 row.get(0)
456 })
457 .unwrap();
458 assert_eq!(count, 0);
459 }
460
461 #[test]
462 fn test_user_crud() {
463 let db = Database::open_in_memory().unwrap();
464 let conn = db.connection();
465 let conn = conn.lock_safe();
466
467 conn.execute(
469 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
470 params!["server1", "Test Server", "http://localhost:8096"],
471 )
472 .unwrap();
473
474 conn.execute(
476 "INSERT INTO users (id, server_id, username, is_active)
477 VALUES (?1, ?2, ?3, ?4)",
478 params!["user1", "server1", "admin", 1],
479 )
480 .unwrap();
481
482 let (username, is_active): (String, i32) = conn
484 .query_row(
485 "SELECT username, is_active FROM users WHERE id = ?1",
486 ["user1"],
487 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
488 )
489 .unwrap();
490 assert_eq!(username, "admin");
491 assert_eq!(is_active, 1);
492
493 conn.execute("UPDATE users SET is_active = 0 WHERE id = ?1", ["user1"])
495 .unwrap();
496
497 let is_active: i32 = conn
498 .query_row(
499 "SELECT is_active FROM users WHERE id = ?1",
500 ["user1"],
501 |row: &rusqlite::Row| row.get(0),
502 )
503 .unwrap();
504 assert_eq!(is_active, 0);
505 }
506
507 #[test]
508 fn test_cascade_delete_server_removes_users() {
509 let db = Database::open_in_memory().unwrap();
510 let conn = db.connection();
511 let conn = conn.lock_safe();
512
513 conn.execute(
515 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
516 params!["server1", "Test Server", "http://localhost:8096"],
517 )
518 .unwrap();
519
520 conn.execute(
521 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
522 params!["user1", "server1", "admin"],
523 )
524 .unwrap();
525
526 let count: i32 = conn
528 .query_row(
529 "SELECT COUNT(*) FROM users WHERE server_id = ?1",
530 ["server1"],
531 |row: &rusqlite::Row| row.get(0),
532 )
533 .unwrap();
534 assert_eq!(count, 1);
535
536 conn.execute("DELETE FROM servers WHERE id = ?1", ["server1"])
538 .unwrap();
539
540 let count: i32 = conn
542 .query_row("SELECT COUNT(*) FROM users", [], |row: &rusqlite::Row| {
543 row.get(0)
544 })
545 .unwrap();
546 assert_eq!(count, 0);
547 }
548
549 #[test]
550 fn test_item_insert_and_fts_search() {
551 let db = Database::open_in_memory().unwrap();
552 let conn = db.connection();
553 let conn = conn.lock_safe();
554
555 conn.execute(
557 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
558 params!["server1", "Test Server", "http://localhost:8096"],
559 )
560 .unwrap();
561
562 conn.execute(
564 "INSERT INTO items (id, server_id, name, item_type, overview, album_name, artists)
565 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
566 params![
567 "item1",
568 "server1",
569 "Bohemian Rhapsody",
570 "Audio",
571 "A legendary rock song",
572 "A Night at the Opera",
573 "[\"Queen\"]"
574 ],
575 )
576 .unwrap();
577
578 let mut stmt = conn
580 .prepare(
581 "SELECT i.name FROM items i
582 JOIN items_fts ON i.rowid = items_fts.rowid
583 WHERE items_fts MATCH ?1",
584 )
585 .unwrap();
586
587 let result: Option<String> = stmt
589 .query_row(["Bohemian"], |row: &rusqlite::Row| row.get(0))
590 .ok();
591 assert_eq!(result, Some("Bohemian Rhapsody".to_string()));
592
593 let result: Option<String> = stmt
595 .query_row(["Opera"], |row: &rusqlite::Row| row.get(0))
596 .ok();
597 assert_eq!(result, Some("Bohemian Rhapsody".to_string()));
598
599 let result: Option<String> = stmt
601 .query_row(["Queen"], |row: &rusqlite::Row| row.get(0))
602 .ok();
603 assert_eq!(result, Some("Bohemian Rhapsody".to_string()));
604 }
605
606 #[test]
607 fn test_user_data_playback_position() {
608 let db = Database::open_in_memory().unwrap();
609 let conn = db.connection();
610 let conn = conn.lock_safe();
611
612 conn.execute(
614 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
615 params!["server1", "Test", "http://localhost"],
616 )
617 .unwrap();
618
619 conn.execute(
620 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
621 params!["user1", "server1", "admin"],
622 )
623 .unwrap();
624
625 conn.execute(
626 "INSERT INTO items (id, server_id, name, item_type) VALUES (?1, ?2, ?3, ?4)",
627 params!["item1", "server1", "Test Movie", "Movie"],
628 )
629 .unwrap();
630
631 conn.execute(
633 "INSERT INTO user_data (user_id, item_id, playback_position_ticks, is_played)
634 VALUES (?1, ?2, ?3, ?4)",
635 params!["user1", "item1", 12345678900_i64, 0],
636 )
637 .unwrap();
638
639 let (position, is_played): (i64, i32) = conn
641 .query_row(
642 "SELECT playback_position_ticks, is_played FROM user_data
643 WHERE user_id = ?1 AND item_id = ?2",
644 ["user1", "item1"],
645 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
646 )
647 .unwrap();
648 assert_eq!(position, 12345678900);
649 assert_eq!(is_played, 0);
650
651 conn.execute(
653 "UPDATE user_data SET is_played = 1, playback_position_ticks = 0
654 WHERE user_id = ?1 AND item_id = ?2",
655 ["user1", "item1"],
656 )
657 .unwrap();
658
659 let is_played: i32 = conn
660 .query_row(
661 "SELECT is_played FROM user_data WHERE user_id = ?1 AND item_id = ?2",
662 ["user1", "item1"],
663 |row: &rusqlite::Row| row.get(0),
664 )
665 .unwrap();
666 assert_eq!(is_played, 1);
667 }
668
669 #[test]
670 fn test_sync_queue_operations() {
671 let db = Database::open_in_memory().unwrap();
672 let conn = db.connection();
673 let conn = conn.lock_safe();
674
675 conn.execute(
677 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
678 params!["server1", "Test", "http://localhost"],
679 )
680 .unwrap();
681
682 conn.execute(
683 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
684 params!["user1", "server1", "admin"],
685 )
686 .unwrap();
687
688 conn.execute(
690 "INSERT INTO sync_queue (user_id, operation, item_id, payload, status)
691 VALUES (?1, ?2, ?3, ?4, ?5)",
692 params![
693 "user1",
694 "mark_favorite",
695 "item123",
696 r#"{"favorite": true}"#,
697 "pending"
698 ],
699 )
700 .unwrap();
701
702 let mut stmt = conn
704 .prepare("SELECT operation, item_id FROM sync_queue WHERE status = 'pending'")
705 .unwrap();
706
707 let ops: Vec<(String, String)> = stmt
708 .query_map([], |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)))
709 .unwrap()
710 .filter_map(|r| r.ok())
711 .collect();
712
713 assert_eq!(ops.len(), 1);
714 assert_eq!(ops[0].0, "mark_favorite");
715 assert_eq!(ops[0].1, "item123");
716
717 conn.execute(
719 "UPDATE sync_queue SET status = 'completed' WHERE item_id = ?1",
720 ["item123"],
721 )
722 .unwrap();
723
724 let pending_count: i32 = conn
725 .query_row(
726 "SELECT COUNT(*) FROM sync_queue WHERE status = 'pending'",
727 [],
728 |row: &rusqlite::Row| row.get(0),
729 )
730 .unwrap();
731 assert_eq!(pending_count, 0);
732 }
733
734 #[test]
735 fn test_downloads_table() {
736 let db = Database::open_in_memory().unwrap();
737 let conn = db.connection();
738 let conn = conn.lock_safe();
739
740 conn.execute(
742 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
743 params!["server1", "Test", "http://localhost"],
744 )
745 .unwrap();
746
747 conn.execute(
748 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
749 params!["user1", "server1", "admin"],
750 )
751 .unwrap();
752
753 conn.execute(
754 "INSERT INTO items (id, server_id, name, item_type) VALUES (?1, ?2, ?3, ?4)",
755 params!["item1", "server1", "Test Song", "Audio"],
756 )
757 .unwrap();
758
759 conn.execute(
761 "INSERT INTO downloads (item_id, user_id, file_path, status, progress)
762 VALUES (?1, ?2, ?3, ?4, ?5)",
763 params!["item1", "user1", "/data/downloads/test.mp3", "pending", 0.0],
764 )
765 .unwrap();
766
767 conn.execute(
769 "UPDATE downloads SET status = 'downloading', progress = 0.5
770 WHERE item_id = ?1",
771 ["item1"],
772 )
773 .unwrap();
774
775 let (status, progress): (String, f64) = conn
776 .query_row(
777 "SELECT status, progress FROM downloads WHERE item_id = ?1",
778 ["item1"],
779 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
780 )
781 .unwrap();
782 assert_eq!(status, "downloading");
783 assert!((progress - 0.5).abs() < 0.001);
784
785 conn.execute(
787 "UPDATE downloads SET status = 'completed', progress = 1.0
788 WHERE item_id = ?1",
789 ["item1"],
790 )
791 .unwrap();
792
793 let status: String = conn
794 .query_row(
795 "SELECT status FROM downloads WHERE item_id = ?1",
796 ["item1"],
797 |row: &rusqlite::Row| row.get(0),
798 )
799 .unwrap();
800 assert_eq!(status, "completed");
801 }
802
803 #[test]
804 fn test_migrations_idempotent() {
805 let db = Database::open_in_memory().unwrap();
806
807 let result = db.migrate();
809 assert!(result.is_ok());
810
811 let conn = db.connection();
813 let conn = conn.lock_safe();
814
815 let count: i32 = conn
816 .query_row(
817 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='items'",
818 [],
819 |row: &rusqlite::Row| row.get(0),
820 )
821 .unwrap();
822 assert_eq!(count, 1);
823 }
824
825 #[test]
826 fn test_global_active_user_deactivation() {
827 let db = Database::open_in_memory().unwrap();
828 let conn = db.connection();
829 let conn = conn.lock_safe();
830
831 conn.execute(
833 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
834 params!["server1", "Server 1", "http://server1.com"],
835 )
836 .unwrap();
837
838 conn.execute(
839 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
840 params!["server2", "Server 2", "http://server2.com"],
841 )
842 .unwrap();
843
844 conn.execute(
846 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
847 VALUES (?1, ?2, ?3, 1, '2024-01-01 10:00:00')",
848 params!["user1", "server1", "admin"],
849 )
850 .unwrap();
851
852 conn.execute(
853 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
854 VALUES (?1, ?2, ?3, 1, '2024-01-01 11:00:00')",
855 params!["user2", "server2", "admin"],
856 )
857 .unwrap();
858
859 let active_count: i32 = conn
861 .query_row(
862 "SELECT COUNT(*) FROM users WHERE is_active = 1",
863 [],
864 |row: &rusqlite::Row| row.get(0),
865 )
866 .unwrap();
867 assert_eq!(active_count, 2);
868
869 conn.execute("UPDATE users SET is_active = 0", []).unwrap();
871 conn.execute(
872 "UPDATE users SET is_active = 1, last_login_at = CURRENT_TIMESTAMP WHERE id = ?1",
873 ["user1"],
874 )
875 .unwrap();
876
877 let active_count: i32 = conn
879 .query_row(
880 "SELECT COUNT(*) FROM users WHERE is_active = 1",
881 [],
882 |row: &rusqlite::Row| row.get(0),
883 )
884 .unwrap();
885 assert_eq!(active_count, 1);
886
887 let active_user: String = conn
889 .query_row(
890 "SELECT id FROM users WHERE is_active = 1",
891 [],
892 |row: &rusqlite::Row| row.get(0),
893 )
894 .unwrap();
895 assert_eq!(active_user, "user1");
896 }
897
898 #[test]
899 fn test_active_session_query_ordering() {
900 let db = Database::open_in_memory().unwrap();
901 let conn = db.connection();
902 let conn = conn.lock_safe();
903
904 conn.execute(
906 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
907 params!["server1", "Test Server", "http://localhost:8096"],
908 )
909 .unwrap();
910
911 conn.execute(
913 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
914 VALUES (?1, ?2, ?3, 0, '2024-01-01 10:00:00')",
915 params!["user1", "server1", "old_user"],
916 )
917 .unwrap();
918
919 conn.execute(
920 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
921 VALUES (?1, ?2, ?3, 1, '2024-01-01 12:00:00')",
922 params!["user2", "server1", "recent_user"],
923 )
924 .unwrap();
925
926 let (user_id, username): (String, String) = conn
928 .query_row(
929 "SELECT u.id, u.username FROM users u
930 JOIN servers s ON u.server_id = s.id
931 WHERE u.is_active = 1
932 ORDER BY u.last_login_at DESC
933 LIMIT 1",
934 [],
935 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
936 )
937 .unwrap();
938
939 assert_eq!(user_id, "user2");
940 assert_eq!(username, "recent_user");
941 }
942
943 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
952 async fn reads_do_not_wait_for_an_in_flight_write() {
953 use crate::storage::db_service::{DatabaseService, Query};
954 use std::time::{Duration, Instant};
955
956 let dir = tempfile::tempdir().unwrap();
957 let db = Database::open(&dir.path().join("jellytau.db")).unwrap();
958 let service = db.service();
959
960 let writer = service.clone();
961 let write = tokio::spawn(async move {
962 writer
963 .transaction(|_tx| {
964 std::thread::sleep(Duration::from_millis(600));
965 Ok(())
966 })
967 .await
968 });
969 tokio::time::sleep(Duration::from_millis(100)).await;
971
972 let started = Instant::now();
973 let servers: i64 = service
974 .query_one(Query::new("SELECT COUNT(*) FROM servers"), |r| r.get(0))
975 .await
976 .unwrap();
977 let waited = started.elapsed();
978
979 assert_eq!(servers, 0);
980 assert!(
981 waited < Duration::from_millis(250),
982 "a read waited {waited:?} for an unrelated write to commit"
983 );
984 write.await.unwrap().unwrap();
985 }
986
987 #[test]
994 fn open_configures_wal_for_interactive_use() {
995 let dir = tempfile::tempdir().unwrap();
996 let db = Database::open(&dir.path().join("jellytau.db")).unwrap();
997 let conn = db.connection();
998 let conn = conn.lock_safe();
999
1000 let mode: String = conn
1001 .query_row("PRAGMA journal_mode", [], |r| r.get(0))
1002 .unwrap();
1003 let synchronous: i64 = conn
1004 .query_row("PRAGMA synchronous", [], |r| r.get(0))
1005 .unwrap();
1006 let busy_timeout: i64 = conn
1007 .query_row("PRAGMA busy_timeout", [], |r| r.get(0))
1008 .unwrap();
1009
1010 assert_eq!(mode, "wal");
1011 assert_eq!(synchronous, 1, "expected synchronous = NORMAL");
1012 assert!(busy_timeout > 0, "expected a busy timeout");
1013 }
1014
1015 #[test]
1023 fn open_gives_the_planner_statistics() {
1024 let dir = tempfile::tempdir().unwrap();
1025 let path = dir.path().join("jellytau.db");
1026 {
1027 let db = Database::open(&path).unwrap();
1028 let conn = db.connection();
1029 let conn = conn.lock_safe();
1030 conn.execute_batch(
1031 "INSERT INTO servers (id, name, url) VALUES ('s', 'S', 'http://s');",
1032 )
1033 .unwrap();
1034 for i in 0..2000 {
1035 conn.execute(
1036 "INSERT INTO items (id, server_id, name, item_type) VALUES (?1, 's', 'n', 'Audio')",
1037 [format!("i{i}")],
1038 )
1039 .unwrap();
1040 }
1041 }
1042
1043 let db = Database::open(&path).unwrap();
1044 let conn = db.connection();
1045 let conn = conn.lock_safe();
1046 let analysed: i64 = conn
1047 .query_row(
1048 "SELECT COUNT(*) FROM sqlite_master WHERE name = 'sqlite_stat1'",
1049 [],
1050 |r| r.get(0),
1051 )
1052 .unwrap();
1053 assert_eq!(analysed, 1, "the planner has no statistics");
1054 let items_stats: i64 = conn
1055 .query_row(
1056 "SELECT COUNT(*) FROM sqlite_stat1 WHERE tbl = 'items'",
1057 [],
1058 |r| r.get(0),
1059 )
1060 .unwrap();
1061 assert!(items_stats > 0, "no statistics for items");
1062 }
1063
1064 #[test]
1068 #[ignore]
1069 fn write_bench_database() {
1070 let Ok(path) = std::env::var("JELLYTAU_BENCH_DB") else {
1071 return;
1072 };
1073 let _ = std::fs::remove_file(&path);
1074 let db = Database::open(&PathBuf::from(&path)).unwrap();
1075 let conn = db.connection();
1076 let conn = conn.lock_safe();
1077 conn.execute_batch(
1078 "BEGIN;
1079 INSERT INTO servers (id, name, url) VALUES ('srv', 'S', 'http://s');
1080 INSERT INTO users (id, server_id, username) VALUES ('u', 'srv', 'u');
1081 INSERT INTO libraries (id, server_id, name, collection_type) VALUES ('tv', 'srv', 'TV', 'tvshows');
1082 INSERT INTO libraries (id, server_id, name, collection_type) VALUES ('music', 'srv', 'Music', 'music');",
1083 )
1084 .unwrap();
1085 let now = "2026-09-23T00:00:00Z";
1086 let mut item = conn
1087 .prepare(
1088 "INSERT INTO items (id, server_id, library_id, parent_id, name, sort_name, item_type,
1089 series_id, season_id, album_id, synced_at)
1090 VALUES (?1, 'srv', ?2, ?3, ?4, ?4, ?5, ?6, ?7, ?8, ?9)",
1091 )
1092 .unwrap();
1093 let none: Option<String> = None;
1094 for s in 0..300 {
1095 let series = format!("series-{s}");
1096 item.execute(rusqlite::params![
1097 series,
1098 "tv",
1099 none,
1100 format!("Show {s}"),
1101 "Series",
1102 none,
1103 none,
1104 none,
1105 now
1106 ])
1107 .unwrap();
1108 for n in 0..11 {
1109 let season = format!("{series}-s{n}");
1110 item.execute(rusqlite::params![
1111 season,
1112 "tv",
1113 series,
1114 format!("Season {n}"),
1115 "Season",
1116 series,
1117 none,
1118 none,
1119 now
1120 ])
1121 .unwrap();
1122 for e in 0..24 {
1123 let ep = format!("{season}-e{e}");
1124 item.execute(rusqlite::params![
1125 ep,
1126 "tv",
1127 season,
1128 format!("A Title {e}"),
1129 "Episode",
1130 series,
1131 season,
1132 none,
1133 now
1134 ])
1135 .unwrap();
1136 conn.execute(
1137 "INSERT INTO user_data (user_id, item_id, playback_position_ticks) VALUES ('u', ?1, 5)",
1138 [&ep],
1139 )
1140 .unwrap();
1141 }
1142 }
1143 }
1144 for a in 0..2000 {
1145 let album = format!("album-{a}");
1146 item.execute(rusqlite::params![
1147 album,
1148 "music",
1149 none,
1150 format!("Album {a}"),
1151 "MusicAlbum",
1152 none,
1153 none,
1154 none,
1155 now
1156 ])
1157 .unwrap();
1158 for t in 0..12 {
1159 item.execute(rusqlite::params![
1160 format!("{album}-t{t}"),
1161 "music",
1162 album,
1163 format!("Track {t}"),
1164 "Audio",
1165 none,
1166 none,
1167 album,
1168 now
1169 ])
1170 .unwrap();
1171 }
1172 }
1173 for d in 0..400 {
1174 conn.execute(
1175 "INSERT INTO downloads (item_id, user_id, file_path, status) VALUES (?1, 'u', '/x', 'completed')",
1176 [format!("series-{}-s1-e{}", d % 300, d % 24)],
1177 )
1178 .unwrap();
1179 }
1180 drop(item);
1181 conn.execute_batch("COMMIT;").unwrap();
1183 }
1184}