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
20pub struct Database {
22 conn: Arc<Mutex<Connection>>,
23 path: PathBuf,
24}
25
26impl Database {
27 pub fn open(path: &PathBuf) -> SqliteResult<Self> {
29 if let Some(parent) = path.parent() {
31 std::fs::create_dir_all(parent).ok();
32 }
33
34 let conn = Connection::open(path)?;
35
36 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
38
39 conn.execute_batch("PRAGMA journal_mode = WAL;")?;
41
42 let db = Self {
43 conn: Arc::new(Mutex::new(conn)),
44 path: path.clone(),
45 };
46
47 db.migrate()?;
49
50 Ok(db)
51 }
52
53 #[cfg(test)]
55 pub fn open_in_memory() -> SqliteResult<Self> {
56 let conn = Connection::open_in_memory()?;
57
58 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
60
61 let db = Self {
62 conn: Arc::new(Mutex::new(conn)),
63 path: PathBuf::from(":memory:"),
64 };
65
66 db.migrate()?;
68
69 Ok(db)
70 }
71
72 #[cfg(test)]
74 pub fn connection(&self) -> Arc<Mutex<Connection>> {
75 Arc::clone(&self.conn)
76 }
77
78 pub fn migrate(&self) -> SqliteResult<()> {
80 info!("Starting database migrations...");
81 let conn = self.conn.lock_safe();
82
83 debug!("Creating _migrations table if it doesn't exist...");
85 match conn.execute(
86 "CREATE TABLE IF NOT EXISTS _migrations (
87 id INTEGER PRIMARY KEY,
88 name TEXT NOT NULL UNIQUE,
89 applied_at TEXT DEFAULT CURRENT_TIMESTAMP
90 )",
91 [],
92 ) {
93 Ok(_) => debug!("_migrations table ready"),
94 Err(e) => {
95 error!("Failed to create _migrations table: {}", e);
96 return Err(e);
97 }
98 }
99
100 debug!("Querying applied migrations...");
102 let mut stmt = conn.prepare("SELECT name FROM _migrations")?;
103 let applied: Vec<String> = stmt
104 .query_map([], |row: &rusqlite::Row| row.get(0))?
105 .filter_map(|r| r.ok())
106 .collect();
107 debug!("Found {} applied migrations", applied.len());
108
109 for (name, sql) in MIGRATIONS {
111 if !applied.contains(&name.to_string()) {
112 info!("Applying migration: {}", name);
113 match conn.execute_batch(sql) {
114 Ok(_) => {
115 info!("Successfully applied migration: {}", name);
116 match conn.execute("INSERT INTO _migrations (name) VALUES (?1)", [name]) {
117 Ok(_) => debug!("Recorded migration: {}", name),
118 Err(e) => {
119 error!("Failed to record migration {}: {}", name, e);
120 return Err(e);
121 }
122 }
123 }
124 Err(e) => {
125 error!("Failed to apply migration {}: {}", name, e);
126 return Err(e);
127 }
128 }
129 } else {
130 debug!("Skipping already applied migration: {}", name);
131 }
132 }
133
134 info!("All migrations completed successfully");
135 Ok(())
136 }
137
138 pub fn service(&self) -> RusqliteService {
143 RusqliteService::new(Arc::clone(&self.conn))
144 }
145
146 pub fn path(&self) -> &PathBuf {
148 &self.path
149 }
150
151 pub fn file_size(&self) -> Option<u64> {
153 std::fs::metadata(&self.path).ok().map(|m| m.len())
154 }
155}
156
157#[cfg(test)]
159mod tests {
160 use super::*;
161 use rusqlite::params;
162
163 #[test]
164 fn test_open_in_memory() {
165 let db = Database::open_in_memory().unwrap();
166 assert_eq!(db.path().to_str(), Some(":memory:"));
167 }
168
169 #[test]
170 fn test_migrations_run() {
171 let db = Database::open_in_memory().unwrap();
172 let conn = db.connection();
173 let conn = conn.lock_safe();
174
175 let mut stmt = conn
177 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='items'")
178 .unwrap();
179 let exists: Option<String> = stmt.query_row([], |row: &rusqlite::Row| row.get(0)).ok();
180 assert!(exists.is_some());
181 }
182
183 #[test]
184 fn test_all_tables_created() {
185 let db = Database::open_in_memory().unwrap();
186 let conn = db.connection();
187 let conn = conn.lock_safe();
188
189 let expected_tables = [
190 "servers",
191 "users",
192 "libraries",
193 "items",
194 "media_streams",
195 "user_data",
196 "downloads",
197 "sync_queue",
198 "thumbnails",
199 "playlists",
200 "playlist_items",
201 "genres",
202 ];
203
204 for table in expected_tables {
205 let exists: Option<String> = conn
206 .query_row(
207 "SELECT name FROM sqlite_master WHERE type='table' AND name=?1",
208 [table],
209 |row: &rusqlite::Row| row.get(0),
210 )
211 .ok();
212 assert!(exists.is_some(), "Table '{}' should exist", table);
213 }
214 }
215
216 #[test]
217 fn test_fts_table_created() {
218 let db = Database::open_in_memory().unwrap();
219 let conn = db.connection();
220 let conn = conn.lock_safe();
221
222 let exists: Option<String> = conn
223 .query_row(
224 "SELECT name FROM sqlite_master WHERE type='table' AND name='items_fts'",
225 [],
226 |row: &rusqlite::Row| row.get(0),
227 )
228 .ok();
229 assert!(exists.is_some(), "FTS table 'items_fts' should exist");
230 }
231
232 #[test]
233 fn test_server_crud() {
234 let db = Database::open_in_memory().unwrap();
235 let conn = db.connection();
236 let conn = conn.lock_safe();
237
238 conn.execute(
240 "INSERT INTO servers (id, name, url, version) VALUES (?1, ?2, ?3, ?4)",
241 params!["server1", "My Server", "http://localhost:8096", "10.8.0"],
242 )
243 .unwrap();
244
245 let (name, url): (String, String) = conn
247 .query_row(
248 "SELECT name, url FROM servers WHERE id = ?1",
249 ["server1"],
250 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
251 )
252 .unwrap();
253 assert_eq!(name, "My Server");
254 assert_eq!(url, "http://localhost:8096");
255
256 conn.execute(
258 "UPDATE servers SET name = ?1 WHERE id = ?2",
259 params!["Updated Server", "server1"],
260 )
261 .unwrap();
262
263 let name: String = conn
264 .query_row(
265 "SELECT name FROM servers WHERE id = ?1",
266 ["server1"],
267 |row: &rusqlite::Row| row.get(0),
268 )
269 .unwrap();
270 assert_eq!(name, "Updated Server");
271
272 conn.execute("DELETE FROM servers WHERE id = ?1", ["server1"])
274 .unwrap();
275
276 let count: i32 = conn
277 .query_row("SELECT COUNT(*) FROM servers", [], |row: &rusqlite::Row| {
278 row.get(0)
279 })
280 .unwrap();
281 assert_eq!(count, 0);
282 }
283
284 #[test]
285 fn test_user_crud() {
286 let db = Database::open_in_memory().unwrap();
287 let conn = db.connection();
288 let conn = conn.lock_safe();
289
290 conn.execute(
292 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
293 params!["server1", "Test Server", "http://localhost:8096"],
294 )
295 .unwrap();
296
297 conn.execute(
299 "INSERT INTO users (id, server_id, username, is_active)
300 VALUES (?1, ?2, ?3, ?4)",
301 params!["user1", "server1", "admin", 1],
302 )
303 .unwrap();
304
305 let (username, is_active): (String, i32) = conn
307 .query_row(
308 "SELECT username, is_active FROM users WHERE id = ?1",
309 ["user1"],
310 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
311 )
312 .unwrap();
313 assert_eq!(username, "admin");
314 assert_eq!(is_active, 1);
315
316 conn.execute("UPDATE users SET is_active = 0 WHERE id = ?1", ["user1"])
318 .unwrap();
319
320 let is_active: i32 = conn
321 .query_row(
322 "SELECT is_active FROM users WHERE id = ?1",
323 ["user1"],
324 |row: &rusqlite::Row| row.get(0),
325 )
326 .unwrap();
327 assert_eq!(is_active, 0);
328 }
329
330 #[test]
331 fn test_cascade_delete_server_removes_users() {
332 let db = Database::open_in_memory().unwrap();
333 let conn = db.connection();
334 let conn = conn.lock_safe();
335
336 conn.execute(
338 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
339 params!["server1", "Test Server", "http://localhost:8096"],
340 )
341 .unwrap();
342
343 conn.execute(
344 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
345 params!["user1", "server1", "admin"],
346 )
347 .unwrap();
348
349 let count: i32 = conn
351 .query_row(
352 "SELECT COUNT(*) FROM users WHERE server_id = ?1",
353 ["server1"],
354 |row: &rusqlite::Row| row.get(0),
355 )
356 .unwrap();
357 assert_eq!(count, 1);
358
359 conn.execute("DELETE FROM servers WHERE id = ?1", ["server1"])
361 .unwrap();
362
363 let count: i32 = conn
365 .query_row("SELECT COUNT(*) FROM users", [], |row: &rusqlite::Row| {
366 row.get(0)
367 })
368 .unwrap();
369 assert_eq!(count, 0);
370 }
371
372 #[test]
373 fn test_item_insert_and_fts_search() {
374 let db = Database::open_in_memory().unwrap();
375 let conn = db.connection();
376 let conn = conn.lock_safe();
377
378 conn.execute(
380 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
381 params!["server1", "Test Server", "http://localhost:8096"],
382 )
383 .unwrap();
384
385 conn.execute(
387 "INSERT INTO items (id, server_id, name, item_type, overview, album_name, artists)
388 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
389 params![
390 "item1",
391 "server1",
392 "Bohemian Rhapsody",
393 "Audio",
394 "A legendary rock song",
395 "A Night at the Opera",
396 "[\"Queen\"]"
397 ],
398 )
399 .unwrap();
400
401 let mut stmt = conn
403 .prepare(
404 "SELECT i.name FROM items i
405 JOIN items_fts ON i.rowid = items_fts.rowid
406 WHERE items_fts MATCH ?1",
407 )
408 .unwrap();
409
410 let result: Option<String> = stmt
412 .query_row(["Bohemian"], |row: &rusqlite::Row| row.get(0))
413 .ok();
414 assert_eq!(result, Some("Bohemian Rhapsody".to_string()));
415
416 let result: Option<String> = stmt
418 .query_row(["Opera"], |row: &rusqlite::Row| row.get(0))
419 .ok();
420 assert_eq!(result, Some("Bohemian Rhapsody".to_string()));
421
422 let result: Option<String> = stmt
424 .query_row(["Queen"], |row: &rusqlite::Row| row.get(0))
425 .ok();
426 assert_eq!(result, Some("Bohemian Rhapsody".to_string()));
427 }
428
429 #[test]
430 fn test_user_data_playback_position() {
431 let db = Database::open_in_memory().unwrap();
432 let conn = db.connection();
433 let conn = conn.lock_safe();
434
435 conn.execute(
437 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
438 params!["server1", "Test", "http://localhost"],
439 )
440 .unwrap();
441
442 conn.execute(
443 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
444 params!["user1", "server1", "admin"],
445 )
446 .unwrap();
447
448 conn.execute(
449 "INSERT INTO items (id, server_id, name, item_type) VALUES (?1, ?2, ?3, ?4)",
450 params!["item1", "server1", "Test Movie", "Movie"],
451 )
452 .unwrap();
453
454 conn.execute(
456 "INSERT INTO user_data (user_id, item_id, playback_position_ticks, is_played)
457 VALUES (?1, ?2, ?3, ?4)",
458 params!["user1", "item1", 12345678900_i64, 0],
459 )
460 .unwrap();
461
462 let (position, is_played): (i64, i32) = conn
464 .query_row(
465 "SELECT playback_position_ticks, is_played FROM user_data
466 WHERE user_id = ?1 AND item_id = ?2",
467 ["user1", "item1"],
468 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
469 )
470 .unwrap();
471 assert_eq!(position, 12345678900);
472 assert_eq!(is_played, 0);
473
474 conn.execute(
476 "UPDATE user_data SET is_played = 1, playback_position_ticks = 0
477 WHERE user_id = ?1 AND item_id = ?2",
478 ["user1", "item1"],
479 )
480 .unwrap();
481
482 let is_played: i32 = conn
483 .query_row(
484 "SELECT is_played FROM user_data WHERE user_id = ?1 AND item_id = ?2",
485 ["user1", "item1"],
486 |row: &rusqlite::Row| row.get(0),
487 )
488 .unwrap();
489 assert_eq!(is_played, 1);
490 }
491
492 #[test]
493 fn test_sync_queue_operations() {
494 let db = Database::open_in_memory().unwrap();
495 let conn = db.connection();
496 let conn = conn.lock_safe();
497
498 conn.execute(
500 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
501 params!["server1", "Test", "http://localhost"],
502 )
503 .unwrap();
504
505 conn.execute(
506 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
507 params!["user1", "server1", "admin"],
508 )
509 .unwrap();
510
511 conn.execute(
513 "INSERT INTO sync_queue (user_id, operation, item_id, payload, status)
514 VALUES (?1, ?2, ?3, ?4, ?5)",
515 params![
516 "user1",
517 "mark_favorite",
518 "item123",
519 r#"{"favorite": true}"#,
520 "pending"
521 ],
522 )
523 .unwrap();
524
525 let mut stmt = conn
527 .prepare("SELECT operation, item_id FROM sync_queue WHERE status = 'pending'")
528 .unwrap();
529
530 let ops: Vec<(String, String)> = stmt
531 .query_map([], |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)))
532 .unwrap()
533 .filter_map(|r| r.ok())
534 .collect();
535
536 assert_eq!(ops.len(), 1);
537 assert_eq!(ops[0].0, "mark_favorite");
538 assert_eq!(ops[0].1, "item123");
539
540 conn.execute(
542 "UPDATE sync_queue SET status = 'completed' WHERE item_id = ?1",
543 ["item123"],
544 )
545 .unwrap();
546
547 let pending_count: i32 = conn
548 .query_row(
549 "SELECT COUNT(*) FROM sync_queue WHERE status = 'pending'",
550 [],
551 |row: &rusqlite::Row| row.get(0),
552 )
553 .unwrap();
554 assert_eq!(pending_count, 0);
555 }
556
557 #[test]
558 fn test_downloads_table() {
559 let db = Database::open_in_memory().unwrap();
560 let conn = db.connection();
561 let conn = conn.lock_safe();
562
563 conn.execute(
565 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
566 params!["server1", "Test", "http://localhost"],
567 )
568 .unwrap();
569
570 conn.execute(
571 "INSERT INTO users (id, server_id, username) VALUES (?1, ?2, ?3)",
572 params!["user1", "server1", "admin"],
573 )
574 .unwrap();
575
576 conn.execute(
577 "INSERT INTO items (id, server_id, name, item_type) VALUES (?1, ?2, ?3, ?4)",
578 params!["item1", "server1", "Test Song", "Audio"],
579 )
580 .unwrap();
581
582 conn.execute(
584 "INSERT INTO downloads (item_id, user_id, file_path, status, progress)
585 VALUES (?1, ?2, ?3, ?4, ?5)",
586 params!["item1", "user1", "/data/downloads/test.mp3", "pending", 0.0],
587 )
588 .unwrap();
589
590 conn.execute(
592 "UPDATE downloads SET status = 'downloading', progress = 0.5
593 WHERE item_id = ?1",
594 ["item1"],
595 )
596 .unwrap();
597
598 let (status, progress): (String, f64) = conn
599 .query_row(
600 "SELECT status, progress FROM downloads WHERE item_id = ?1",
601 ["item1"],
602 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
603 )
604 .unwrap();
605 assert_eq!(status, "downloading");
606 assert!((progress - 0.5).abs() < 0.001);
607
608 conn.execute(
610 "UPDATE downloads SET status = 'completed', progress = 1.0
611 WHERE item_id = ?1",
612 ["item1"],
613 )
614 .unwrap();
615
616 let status: String = conn
617 .query_row(
618 "SELECT status FROM downloads WHERE item_id = ?1",
619 ["item1"],
620 |row: &rusqlite::Row| row.get(0),
621 )
622 .unwrap();
623 assert_eq!(status, "completed");
624 }
625
626 #[test]
627 fn test_migrations_idempotent() {
628 let db = Database::open_in_memory().unwrap();
629
630 let result = db.migrate();
632 assert!(result.is_ok());
633
634 let conn = db.connection();
636 let conn = conn.lock_safe();
637
638 let count: i32 = conn
639 .query_row(
640 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='items'",
641 [],
642 |row: &rusqlite::Row| row.get(0),
643 )
644 .unwrap();
645 assert_eq!(count, 1);
646 }
647
648 #[test]
649 fn test_global_active_user_deactivation() {
650 let db = Database::open_in_memory().unwrap();
651 let conn = db.connection();
652 let conn = conn.lock_safe();
653
654 conn.execute(
656 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
657 params!["server1", "Server 1", "http://server1.com"],
658 )
659 .unwrap();
660
661 conn.execute(
662 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
663 params!["server2", "Server 2", "http://server2.com"],
664 )
665 .unwrap();
666
667 conn.execute(
669 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
670 VALUES (?1, ?2, ?3, 1, '2024-01-01 10:00:00')",
671 params!["user1", "server1", "admin"],
672 )
673 .unwrap();
674
675 conn.execute(
676 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
677 VALUES (?1, ?2, ?3, 1, '2024-01-01 11:00:00')",
678 params!["user2", "server2", "admin"],
679 )
680 .unwrap();
681
682 let active_count: i32 = conn
684 .query_row(
685 "SELECT COUNT(*) FROM users WHERE is_active = 1",
686 [],
687 |row: &rusqlite::Row| row.get(0),
688 )
689 .unwrap();
690 assert_eq!(active_count, 2);
691
692 conn.execute("UPDATE users SET is_active = 0", []).unwrap();
694 conn.execute(
695 "UPDATE users SET is_active = 1, last_login_at = CURRENT_TIMESTAMP WHERE id = ?1",
696 ["user1"],
697 )
698 .unwrap();
699
700 let active_count: i32 = conn
702 .query_row(
703 "SELECT COUNT(*) FROM users WHERE is_active = 1",
704 [],
705 |row: &rusqlite::Row| row.get(0),
706 )
707 .unwrap();
708 assert_eq!(active_count, 1);
709
710 let active_user: String = conn
712 .query_row(
713 "SELECT id FROM users WHERE is_active = 1",
714 [],
715 |row: &rusqlite::Row| row.get(0),
716 )
717 .unwrap();
718 assert_eq!(active_user, "user1");
719 }
720
721 #[test]
722 fn test_active_session_query_ordering() {
723 let db = Database::open_in_memory().unwrap();
724 let conn = db.connection();
725 let conn = conn.lock_safe();
726
727 conn.execute(
729 "INSERT INTO servers (id, name, url) VALUES (?1, ?2, ?3)",
730 params!["server1", "Test Server", "http://localhost:8096"],
731 )
732 .unwrap();
733
734 conn.execute(
736 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
737 VALUES (?1, ?2, ?3, 0, '2024-01-01 10:00:00')",
738 params!["user1", "server1", "old_user"],
739 )
740 .unwrap();
741
742 conn.execute(
743 "INSERT INTO users (id, server_id, username, is_active, last_login_at)
744 VALUES (?1, ?2, ?3, 1, '2024-01-01 12:00:00')",
745 params!["user2", "server1", "recent_user"],
746 )
747 .unwrap();
748
749 let (user_id, username): (String, String) = conn
751 .query_row(
752 "SELECT u.id, u.username FROM users u
753 JOIN servers s ON u.server_id = s.id
754 WHERE u.is_active = 1
755 ORDER BY u.last_login_at DESC
756 LIMIT 1",
757 [],
758 |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?)),
759 )
760 .unwrap();
761
762 assert_eq!(user_id, "user2");
763 assert_eq!(username, "recent_user");
764 }
765}