1use crate::{
2 error::{Result as ThingsResult, ThingsError},
3 models::{Area, Project, Task, TaskStatus, TaskType},
4};
5use chrono::{DateTime, NaiveDate, Utc};
6use serde::{Deserialize, Serialize};
7use sqlx::{pool::PoolOptions, Row, SqlitePool};
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10use tracing::{debug, error, info, instrument};
11use uuid::Uuid;
12
13fn safe_timestamp_convert(ts_f64: f64) -> i64 {
15 if ts_f64.is_finite() && ts_f64 >= 0.0 {
17 let max_timestamp = 4_102_444_800_f64; if ts_f64 <= max_timestamp {
20 let ts_str = format!("{:.0}", ts_f64.trunc());
22 ts_str.parse::<i64>().unwrap_or(0)
23 } else {
24 0 }
26 } else {
27 0 }
29}
30
31fn things_uuid_to_uuid(things_uuid: &str) -> Uuid {
34 use std::collections::hash_map::DefaultHasher;
37 use std::hash::{Hash, Hasher};
38
39 let mut hasher = DefaultHasher::new();
40 things_uuid.hash(&mut hasher);
41 let hash = hasher.finish();
42
43 let bytes = [
46 ((hash >> 56) & 0xFF) as u8,
47 ((hash >> 48) & 0xFF) as u8,
48 ((hash >> 40) & 0xFF) as u8,
49 ((hash >> 32) & 0xFF) as u8,
50 ((hash >> 24) & 0xFF) as u8,
51 ((hash >> 16) & 0xFF) as u8,
52 ((hash >> 8) & 0xFF) as u8,
53 (hash & 0xFF) as u8,
54 u8::try_from(things_uuid.len().min(255)).unwrap_or(255),
56 things_uuid.chars().next().unwrap_or('0') as u8,
57 things_uuid.chars().nth(1).unwrap_or('0') as u8,
58 things_uuid.chars().nth(2).unwrap_or('0') as u8,
59 things_uuid.chars().nth(3).unwrap_or('0') as u8,
60 things_uuid.chars().nth(4).unwrap_or('0') as u8,
61 things_uuid.chars().nth(5).unwrap_or('0') as u8,
62 things_uuid.chars().nth(6).unwrap_or('0') as u8,
63 ];
64
65 Uuid::from_bytes(bytes)
66}
67
68impl TaskStatus {
69 fn from_i32(value: i32) -> Option<Self> {
70 match value {
71 0 => Some(TaskStatus::Incomplete),
72 1 => Some(TaskStatus::Completed),
73 2 => Some(TaskStatus::Canceled),
74 3 => Some(TaskStatus::Trashed),
75 _ => None,
76 }
77 }
78}
79
80impl TaskType {
81 fn from_i32(value: i32) -> Option<Self> {
82 match value {
83 0 => Some(TaskType::Todo),
84 1 => Some(TaskType::Project),
85 2 => Some(TaskType::Heading),
86 3 => Some(TaskType::Area),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct DatabasePoolConfig {
95 pub max_connections: u32,
97 pub min_connections: u32,
99 pub connect_timeout: Duration,
101 pub idle_timeout: Duration,
103 pub max_lifetime: Duration,
105 pub test_before_acquire: bool,
107 pub sqlite_optimizations: SqliteOptimizations,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct SqliteOptimizations {
114 pub enable_wal_mode: bool,
116 pub synchronous_mode: String,
118 pub cache_size: i32,
120 pub enable_foreign_keys: bool,
122 pub journal_mode: String,
124 pub temp_store: String,
126 pub mmap_size: i64,
128 pub enable_query_planner: bool,
130}
131
132impl Default for DatabasePoolConfig {
133 fn default() -> Self {
134 Self {
135 max_connections: 10,
136 min_connections: 1,
137 connect_timeout: Duration::from_secs(30),
138 idle_timeout: Duration::from_secs(600), max_lifetime: Duration::from_secs(1800), test_before_acquire: true,
141 sqlite_optimizations: SqliteOptimizations::default(),
142 }
143 }
144}
145
146impl Default for SqliteOptimizations {
147 fn default() -> Self {
148 Self {
149 enable_wal_mode: true,
150 synchronous_mode: "NORMAL".to_string(),
151 cache_size: -20000, enable_foreign_keys: true,
153 journal_mode: "WAL".to_string(),
154 temp_store: "MEMORY".to_string(),
155 mmap_size: 268_435_456, enable_query_planner: true,
157 }
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct PoolHealthStatus {
164 pub is_healthy: bool,
165 pub pool_size: u32,
166 pub active_connections: u32,
167 pub idle_connections: u32,
168 pub max_connections: u32,
169 pub min_connections: u32,
170 pub connection_timeout: Duration,
171 pub idle_timeout: Option<Duration>,
172 pub max_lifetime: Option<Duration>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct PoolMetrics {
178 pub pool_size: u32,
179 pub active_connections: u32,
180 pub idle_connections: u32,
181 pub max_connections: u32,
182 pub min_connections: u32,
183 pub utilization_percentage: f64,
184 pub is_healthy: bool,
185 pub response_time_ms: u64,
186 pub connection_timeout: Duration,
187 pub idle_timeout: Option<Duration>,
188 pub max_lifetime: Option<Duration>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ComprehensiveHealthStatus {
194 pub overall_healthy: bool,
195 pub pool_health: PoolHealthStatus,
196 pub pool_metrics: PoolMetrics,
197 pub database_stats: DatabaseStats,
198 pub timestamp: DateTime<Utc>,
199}
200
201#[derive(Debug, Clone)]
204pub struct ThingsDatabase {
205 pool: SqlitePool,
206 config: DatabasePoolConfig,
207}
208
209impl ThingsDatabase {
210 #[instrument]
216 pub async fn new(database_path: &Path) -> ThingsResult<Self> {
217 Self::new_with_config(database_path, DatabasePoolConfig::default()).await
218 }
219
220 #[instrument]
226 pub async fn new_with_config(
227 database_path: &Path,
228 config: DatabasePoolConfig,
229 ) -> ThingsResult<Self> {
230 let database_url = format!("sqlite:{}", database_path.display());
231
232 info!(
233 "Connecting to SQLite database at: {} with optimized pool",
234 database_url
235 );
236
237 let pool = PoolOptions::new()
239 .max_connections(config.max_connections)
240 .min_connections(config.min_connections)
241 .acquire_timeout(config.connect_timeout)
242 .idle_timeout(Some(config.idle_timeout))
243 .max_lifetime(Some(config.max_lifetime))
244 .test_before_acquire(config.test_before_acquire)
245 .connect(&database_url)
246 .await
247 .map_err(|e| ThingsError::unknown(format!("Failed to connect to database: {e}")))?;
248
249 Self::apply_sqlite_optimizations(&pool, &config.sqlite_optimizations).await?;
251
252 info!(
253 "Database connection pool established successfully with {} max connections",
254 config.max_connections
255 );
256
257 Ok(Self { pool, config })
258 }
259
260 async fn apply_sqlite_optimizations(
262 pool: &SqlitePool,
263 optimizations: &SqliteOptimizations,
264 ) -> ThingsResult<()> {
265 sqlx::query(&format!(
267 "PRAGMA journal_mode = {}",
268 optimizations.journal_mode
269 ))
270 .execute(pool)
271 .await
272 .map_err(|e| ThingsError::unknown(format!("Failed to set journal mode: {e}")))?;
273
274 sqlx::query(&format!(
276 "PRAGMA synchronous = {}",
277 optimizations.synchronous_mode
278 ))
279 .execute(pool)
280 .await
281 .map_err(|e| ThingsError::unknown(format!("Failed to set synchronous mode: {e}")))?;
282
283 sqlx::query(&format!("PRAGMA cache_size = {}", optimizations.cache_size))
285 .execute(pool)
286 .await
287 .map_err(|e| ThingsError::unknown(format!("Failed to set cache size: {e}")))?;
288
289 let fk_setting = if optimizations.enable_foreign_keys {
291 "ON"
292 } else {
293 "OFF"
294 };
295 sqlx::query(&format!("PRAGMA foreign_keys = {fk_setting}"))
296 .execute(pool)
297 .await
298 .map_err(|e| ThingsError::unknown(format!("Failed to set foreign keys: {e}")))?;
299
300 sqlx::query(&format!("PRAGMA temp_store = {}", optimizations.temp_store))
302 .execute(pool)
303 .await
304 .map_err(|e| ThingsError::unknown(format!("Failed to set temp store: {e}")))?;
305
306 sqlx::query(&format!("PRAGMA mmap_size = {}", optimizations.mmap_size))
308 .execute(pool)
309 .await
310 .map_err(|e| ThingsError::unknown(format!("Failed to set mmap size: {e}")))?;
311
312 if optimizations.enable_query_planner {
314 sqlx::query("PRAGMA optimize")
315 .execute(pool)
316 .await
317 .map_err(|e| ThingsError::unknown(format!("Failed to optimize database: {e}")))?;
318 }
319
320 debug!(
321 "Applied SQLite optimizations: WAL={}, sync={}, cache={}KB, fk={}, temp={}, mmap={}MB",
322 optimizations.enable_wal_mode,
323 optimizations.synchronous_mode,
324 optimizations.cache_size.abs() / 1024,
325 optimizations.enable_foreign_keys,
326 optimizations.temp_store,
327 optimizations.mmap_size / 1024 / 1024
328 );
329
330 Ok(())
331 }
332
333 #[instrument]
339 pub async fn from_connection_string(database_url: &str) -> ThingsResult<Self> {
340 Self::from_connection_string_with_config(database_url, DatabasePoolConfig::default()).await
341 }
342
343 #[instrument]
349 pub async fn from_connection_string_with_config(
350 database_url: &str,
351 config: DatabasePoolConfig,
352 ) -> ThingsResult<Self> {
353 info!(
354 "Connecting to SQLite database: {} with optimized pool",
355 database_url
356 );
357
358 let pool = PoolOptions::new()
360 .max_connections(config.max_connections)
361 .min_connections(config.min_connections)
362 .acquire_timeout(config.connect_timeout)
363 .idle_timeout(Some(config.idle_timeout))
364 .max_lifetime(Some(config.max_lifetime))
365 .test_before_acquire(config.test_before_acquire)
366 .connect(database_url)
367 .await
368 .map_err(|e| ThingsError::unknown(format!("Failed to connect to database: {e}")))?;
369
370 Self::apply_sqlite_optimizations(&pool, &config.sqlite_optimizations).await?;
372
373 info!(
374 "Database connection pool established successfully with {} max connections",
375 config.max_connections
376 );
377
378 Ok(Self { pool, config })
379 }
380
381 #[must_use]
383 pub fn pool(&self) -> &SqlitePool {
384 &self.pool
385 }
386
387 #[instrument]
389 pub async fn is_connected(&self) -> bool {
390 match sqlx::query("SELECT 1").fetch_one(&self.pool).await {
391 Ok(_) => {
392 debug!("Database connection is healthy");
393 true
394 }
395 Err(e) => {
396 error!("Database connection check failed: {}", e);
397 false
398 }
399 }
400 }
401
402 #[instrument]
408 pub async fn get_pool_health(&self) -> ThingsResult<PoolHealthStatus> {
409 let pool_size = self.pool.size();
410 let idle_connections = self.pool.num_idle();
411 let active_connections = pool_size - u32::try_from(idle_connections).unwrap_or(0);
412
413 let is_healthy = self.is_connected().await;
415
416 Ok(PoolHealthStatus {
417 is_healthy,
418 pool_size,
419 active_connections,
420 idle_connections: u32::try_from(idle_connections).unwrap_or(0),
421 max_connections: self.config.max_connections,
422 min_connections: self.config.min_connections,
423 connection_timeout: self.config.connect_timeout,
424 idle_timeout: Some(self.config.idle_timeout),
425 max_lifetime: Some(self.config.max_lifetime),
426 })
427 }
428
429 #[instrument]
435 pub async fn get_pool_metrics(&self) -> ThingsResult<PoolMetrics> {
436 let pool_size = self.pool.size();
437 let idle_connections = self.pool.num_idle();
438 let active_connections = pool_size - u32::try_from(idle_connections).unwrap_or(0);
439
440 let max_connections = self.config.max_connections;
442 let utilization_percentage = if max_connections > 0 {
443 (f64::from(active_connections) / f64::from(max_connections)) * 100.0
444 } else {
445 0.0
446 };
447
448 let start_time = std::time::Instant::now();
450 let is_connected = self.is_connected().await;
451 let response_time_ms = u64::try_from(start_time.elapsed().as_millis()).unwrap_or(0);
452
453 Ok(PoolMetrics {
454 pool_size,
455 active_connections,
456 idle_connections: u32::try_from(idle_connections).unwrap_or(0),
457 max_connections,
458 min_connections: self.config.min_connections,
459 utilization_percentage,
460 is_healthy: is_connected,
461 response_time_ms,
462 connection_timeout: self.config.connect_timeout,
463 idle_timeout: Some(self.config.idle_timeout),
464 max_lifetime: Some(self.config.max_lifetime),
465 })
466 }
467
468 #[instrument]
474 pub async fn comprehensive_health_check(&self) -> ThingsResult<ComprehensiveHealthStatus> {
475 let pool_health = self.get_pool_health().await?;
476 let pool_metrics = self.get_pool_metrics().await?;
477 let db_stats = self.get_stats().await?;
478
479 let overall_healthy = pool_health.is_healthy && pool_metrics.is_healthy;
480
481 Ok(ComprehensiveHealthStatus {
482 overall_healthy,
483 pool_health,
484 pool_metrics,
485 database_stats: db_stats,
486 timestamp: Utc::now(),
487 })
488 }
489
490 #[instrument]
496 pub async fn get_stats(&self) -> ThingsResult<DatabaseStats> {
497 let task_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMTask")
498 .fetch_one(&self.pool)
499 .await
500 .map_err(|e| ThingsError::unknown(format!("Failed to get task count: {e}")))?;
501
502 let project_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMTask WHERE type = 1")
503 .fetch_one(&self.pool)
504 .await
505 .map_err(|e| ThingsError::unknown(format!("Failed to get project count: {e}")))?;
506
507 let area_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMArea")
508 .fetch_one(&self.pool)
509 .await
510 .map_err(|e| ThingsError::unknown(format!("Failed to get area count: {e}")))?;
511
512 Ok(DatabaseStats {
513 task_count: task_count.try_into().unwrap_or(0),
514 project_count: project_count.try_into().unwrap_or(0),
515 area_count: area_count.try_into().unwrap_or(0),
516 })
517 }
518
519 #[instrument]
525 pub async fn get_all_tasks(&self) -> ThingsResult<Vec<Task>> {
526 let rows = sqlx::query(
527 r"
528 SELECT
529 uuid, title, status, type,
530 start_date, due_date,
531 project_uuid, area_uuid,
532 notes, tags,
533 created, modified
534 FROM TMTask
535 ORDER BY created DESC
536 ",
537 )
538 .fetch_all(&self.pool)
539 .await
540 .map_err(|e| ThingsError::unknown(format!("Failed to fetch tasks: {e}")))?;
541
542 let mut tasks = Vec::new();
543 for row in rows {
544 let task = Task {
545 uuid: Uuid::parse_str(&row.get::<String, _>("uuid"))
546 .map_err(|e| ThingsError::unknown(format!("Invalid task UUID: {e}")))?,
547 title: row.get("title"),
548 status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
549 task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
550 start_date: row
551 .get::<Option<String>, _>("start_date")
552 .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
553 deadline: row
554 .get::<Option<String>, _>("due_date")
555 .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
556 project_uuid: row
557 .get::<Option<String>, _>("project_uuid")
558 .and_then(|s| Uuid::parse_str(&s).ok()),
559 area_uuid: row
560 .get::<Option<String>, _>("area_uuid")
561 .and_then(|s| Uuid::parse_str(&s).ok()),
562 parent_uuid: None, notes: row.get("notes"),
564 tags: row
565 .get::<Option<String>, _>("tags")
566 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
567 .unwrap_or_default(),
568 children: Vec::new(), created: DateTime::parse_from_rfc3339(&row.get::<String, _>("created"))
570 .ok()
571 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
572 modified: DateTime::parse_from_rfc3339(&row.get::<String, _>("modified"))
573 .ok()
574 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
575 };
576 tasks.push(task);
577 }
578
579 debug!("Fetched {} tasks", tasks.len());
580 Ok(tasks)
581 }
582
583 #[instrument]
589 pub async fn get_all_projects(&self) -> ThingsResult<Vec<Project>> {
590 let rows = sqlx::query(
591 r"
592 SELECT
593 uuid, title, status,
594 area, notes,
595 creationDate, userModificationDate,
596 startDate, deadline
597 FROM TMTask
598 WHERE type = 1 AND trashed = 0
599 ORDER BY creationDate DESC
600 ",
601 )
602 .fetch_all(&self.pool)
603 .await
604 .map_err(|e| ThingsError::unknown(format!("Failed to fetch projects: {e}")))?;
605
606 let mut projects = Vec::new();
607 for row in rows {
608 let project = Project {
609 uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
610 title: row.get("title"),
611 status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
612 area_uuid: row
613 .get::<Option<String>, _>("area")
614 .map(|s| things_uuid_to_uuid(&s)),
615 notes: row.get("notes"),
616 deadline: row
617 .get::<Option<i64>, _>("deadline")
618 .and_then(|ts| DateTime::from_timestamp(ts, 0))
619 .map(|dt| dt.date_naive()),
620 start_date: row
621 .get::<Option<i64>, _>("startDate")
622 .and_then(|ts| DateTime::from_timestamp(ts, 0))
623 .map(|dt| dt.date_naive()),
624 tags: Vec::new(), tasks: Vec::new(), created: {
627 let ts_f64 = row.get::<f64, _>("creationDate");
628 let ts = safe_timestamp_convert(ts_f64);
629 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
630 },
631 modified: {
632 let ts_f64 = row.get::<f64, _>("userModificationDate");
633 let ts = safe_timestamp_convert(ts_f64);
634 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
635 },
636 };
637 projects.push(project);
638 }
639
640 debug!("Fetched {} projects", projects.len());
641 Ok(projects)
642 }
643
644 #[instrument]
650 pub async fn get_all_areas(&self) -> ThingsResult<Vec<Area>> {
651 let rows = sqlx::query(
652 r"
653 SELECT
654 uuid, title, visible, `index`
655 FROM TMArea
656 WHERE visible = 1
657 ORDER BY `index` ASC
658 ",
659 )
660 .fetch_all(&self.pool)
661 .await
662 .map_err(|e| ThingsError::unknown(format!("Failed to fetch areas: {e}")))?;
663
664 let mut areas = Vec::new();
665 for row in rows {
666 let area = Area {
667 uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
668 title: row.get("title"),
669 notes: None, projects: Vec::new(), tags: Vec::new(), created: Utc::now(), modified: Utc::now(), };
675 areas.push(area);
676 }
677
678 debug!("Fetched {} areas", areas.len());
679 Ok(areas)
680 }
681
682 #[instrument]
688 pub async fn get_tasks_by_status(&self, status: TaskStatus) -> ThingsResult<Vec<Task>> {
689 let status_value = status as i32;
690 let rows = sqlx::query(
691 r"
692 SELECT
693 uuid, title, status, type,
694 start_date, due_date,
695 project_uuid, area_uuid,
696 notes, tags,
697 created, modified
698 FROM TMTask
699 WHERE status = ?
700 ORDER BY created DESC
701 ",
702 )
703 .bind(status_value)
704 .fetch_all(&self.pool)
705 .await
706 .map_err(|e| ThingsError::unknown(format!("Failed to fetch tasks by status: {e}")))?;
707
708 let mut tasks = Vec::new();
709 for row in rows {
710 let task = Task {
711 uuid: Uuid::parse_str(&row.get::<String, _>("uuid"))
712 .map_err(|e| ThingsError::unknown(format!("Invalid task UUID: {e}")))?,
713 title: row.get("title"),
714 status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
715 task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
716 start_date: row
717 .get::<Option<String>, _>("start_date")
718 .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
719 deadline: row
720 .get::<Option<String>, _>("due_date")
721 .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
722 project_uuid: row
723 .get::<Option<String>, _>("project_uuid")
724 .and_then(|s| Uuid::parse_str(&s).ok()),
725 area_uuid: row
726 .get::<Option<String>, _>("area_uuid")
727 .and_then(|s| Uuid::parse_str(&s).ok()),
728 parent_uuid: None, notes: row.get("notes"),
730 tags: row
731 .get::<Option<String>, _>("tags")
732 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
733 .unwrap_or_default(),
734 children: Vec::new(), created: DateTime::parse_from_rfc3339(&row.get::<String, _>("created"))
736 .ok()
737 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
738 modified: DateTime::parse_from_rfc3339(&row.get::<String, _>("modified"))
739 .ok()
740 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
741 };
742 tasks.push(task);
743 }
744
745 debug!("Fetched {} tasks with status {:?}", tasks.len(), status);
746 Ok(tasks)
747 }
748
749 #[instrument]
755 pub async fn search_tasks(&self, query: &str) -> ThingsResult<Vec<Task>> {
756 let search_pattern = format!("%{query}%");
757 let rows = sqlx::query(
758 r"
759 SELECT
760 uuid, title, status, type,
761 startDate, deadline,
762 project, area,
763 notes, cachedTags,
764 creationDate, userModificationDate
765 FROM TMTask
766 WHERE (title LIKE ? OR notes LIKE ?) AND trashed = 0 AND type = 0
767 ORDER BY creationDate DESC
768 ",
769 )
770 .bind(&search_pattern)
771 .bind(&search_pattern)
772 .fetch_all(&self.pool)
773 .await
774 .map_err(|e| ThingsError::unknown(format!("Failed to search tasks: {e}")))?;
775
776 let mut tasks = Vec::new();
777 for row in rows {
778 let task = Task {
779 uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
780 title: row.get("title"),
781 status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
782 task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
783 start_date: row
784 .get::<Option<i64>, _>("startDate")
785 .and_then(|ts| DateTime::from_timestamp(ts, 0))
786 .map(|dt| dt.date_naive()),
787 deadline: row
788 .get::<Option<i64>, _>("deadline")
789 .and_then(|ts| DateTime::from_timestamp(ts, 0))
790 .map(|dt| dt.date_naive()),
791 project_uuid: row
792 .get::<Option<String>, _>("project")
793 .map(|s| things_uuid_to_uuid(&s)),
794 area_uuid: row
795 .get::<Option<String>, _>("area")
796 .map(|s| things_uuid_to_uuid(&s)),
797 parent_uuid: None, notes: row.get("notes"),
799 tags: row
800 .get::<Option<Vec<u8>>, _>("cachedTags")
801 .map(|_| Vec::new()) .unwrap_or_default(),
803 children: Vec::new(), created: {
805 let ts_f64 = row.get::<f64, _>("creationDate");
806 let ts = safe_timestamp_convert(ts_f64);
807 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
808 },
809 modified: {
810 let ts_f64 = row.get::<f64, _>("userModificationDate");
811 let ts = safe_timestamp_convert(ts_f64);
812 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
813 },
814 };
815 tasks.push(task);
816 }
817
818 debug!("Found {} tasks matching query: {}", tasks.len(), query);
819 Ok(tasks)
820 }
821
822 #[instrument(skip(self))]
828 pub async fn get_inbox(&self, limit: Option<usize>) -> ThingsResult<Vec<Task>> {
829 let query = if let Some(limit) = limit {
830 format!("SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE type = 0 AND status = 0 AND project IS NULL AND trashed = 0 ORDER BY creationDate DESC LIMIT {limit}")
831 } else {
832 "SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE type = 0 AND status = 0 AND project IS NULL AND trashed = 0 ORDER BY creationDate DESC"
833 .to_string()
834 };
835
836 let rows = sqlx::query(&query)
837 .fetch_all(&self.pool)
838 .await
839 .map_err(|e| ThingsError::unknown(format!("Failed to fetch inbox tasks: {e}")))?;
840
841 let tasks = rows
842 .into_iter()
843 .map(|row| {
844 Ok(Task {
845 uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
846 title: row.get("title"),
847 task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
848 status: TaskStatus::from_i32(row.get("status"))
849 .unwrap_or(TaskStatus::Incomplete),
850 notes: row.get("notes"),
851 start_date: row
852 .get::<Option<i64>, _>("startDate")
853 .and_then(|ts| DateTime::from_timestamp(ts, 0))
854 .map(|dt| dt.date_naive()),
855 deadline: row
856 .get::<Option<i64>, _>("deadline")
857 .and_then(|ts| DateTime::from_timestamp(ts, 0))
858 .map(|dt| dt.date_naive()),
859 created: {
860 let ts_f64 = row.get::<f64, _>("creationDate");
861 let ts = safe_timestamp_convert(ts_f64);
862 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
863 },
864 modified: {
865 let ts_f64 = row.get::<f64, _>("userModificationDate");
866 let ts = safe_timestamp_convert(ts_f64);
867 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
868 },
869 project_uuid: row
870 .get::<Option<String>, _>("project")
871 .map(|s| things_uuid_to_uuid(&s)),
872 area_uuid: row
873 .get::<Option<String>, _>("area")
874 .map(|s| things_uuid_to_uuid(&s)),
875 parent_uuid: row
876 .get::<Option<String>, _>("parent")
877 .map(|s| things_uuid_to_uuid(&s)),
878 tags: row
879 .get::<Option<String>, _>("tags")
880 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
881 .unwrap_or_default(),
882 children: Vec::new(),
883 })
884 })
885 .collect::<ThingsResult<Vec<Task>>>()?;
886
887 Ok(tasks)
888 }
889
890 #[instrument(skip(self))]
900 pub async fn get_today(&self, limit: Option<usize>) -> ThingsResult<Vec<Task>> {
901 let today = chrono::Utc::now().date_naive();
902 let start_of_day = today.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
903 let end_of_day = today.and_hms_opt(23, 59, 59).unwrap().and_utc().timestamp();
904
905 let query = if let Some(limit) = limit {
906 format!(
907 "SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE status = 0 AND ((deadline >= ? AND deadline <= ?) OR (startDate >= ? AND startDate <= ?)) AND trashed = 0 ORDER BY creationDate DESC LIMIT {limit}"
908 )
909 } else {
910 "SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE status = 0 AND ((deadline >= ? AND deadline <= ?) OR (startDate >= ? AND startDate <= ?)) AND trashed = 0 ORDER BY creationDate DESC".to_string()
911 };
912
913 let rows = sqlx::query(&query)
914 .bind(start_of_day)
915 .bind(end_of_day)
916 .bind(start_of_day)
917 .bind(end_of_day)
918 .fetch_all(&self.pool)
919 .await
920 .map_err(|e| ThingsError::unknown(format!("Failed to fetch today's tasks: {e}")))?;
921
922 let tasks = rows
923 .into_iter()
924 .map(|row| {
925 Ok(Task {
926 uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
927 title: row.get("title"),
928 task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
929 status: TaskStatus::from_i32(row.get("status"))
930 .unwrap_or(TaskStatus::Incomplete),
931 notes: row.get("notes"),
932 start_date: row
933 .get::<Option<i64>, _>("startDate")
934 .and_then(|ts| DateTime::from_timestamp(ts, 0))
935 .map(|dt| dt.date_naive()),
936 deadline: row
937 .get::<Option<i64>, _>("deadline")
938 .and_then(|ts| DateTime::from_timestamp(ts, 0))
939 .map(|dt| dt.date_naive()),
940 created: {
941 let ts_f64 = row.get::<f64, _>("creationDate");
942 let ts = safe_timestamp_convert(ts_f64);
943 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
944 },
945 modified: {
946 let ts_f64 = row.get::<f64, _>("userModificationDate");
947 let ts = safe_timestamp_convert(ts_f64);
948 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
949 },
950 project_uuid: row
951 .get::<Option<String>, _>("project")
952 .map(|s| things_uuid_to_uuid(&s)),
953 area_uuid: row
954 .get::<Option<String>, _>("area")
955 .map(|s| things_uuid_to_uuid(&s)),
956 parent_uuid: row
957 .get::<Option<String>, _>("parent")
958 .map(|s| things_uuid_to_uuid(&s)),
959 tags: row
960 .get::<Option<String>, _>("tags")
961 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
962 .unwrap_or_default(),
963 children: Vec::new(),
964 })
965 })
966 .collect::<ThingsResult<Vec<Task>>>()?;
967
968 Ok(tasks)
969 }
970
971 #[instrument(skip(self))]
977 pub async fn get_projects(&self, limit: Option<usize>) -> ThingsResult<Vec<Project>> {
978 let _ = limit; self.get_all_projects().await
980 }
981
982 #[instrument(skip(self))]
988 pub async fn get_areas(&self) -> ThingsResult<Vec<Area>> {
989 self.get_all_areas().await
990 }
991}
992
993#[derive(Debug, Clone, Serialize, Deserialize)]
995pub struct DatabaseStats {
996 pub task_count: u64,
997 pub project_count: u64,
998 pub area_count: u64,
999}
1000
1001impl DatabaseStats {
1002 #[must_use]
1003 pub fn total_items(&self) -> u64 {
1004 self.task_count + self.project_count + self.area_count
1005 }
1006}
1007
1008#[must_use]
1020pub fn get_default_database_path() -> PathBuf {
1021 let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
1022 PathBuf::from(format!(
1023 "{home}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/ThingsData-0Z0Z2/Things Database.thingsdatabase/main.sqlite"
1024 ))
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030 use tempfile::{NamedTempFile, TempDir};
1031
1032 #[tokio::test]
1033 async fn test_database_connection() {
1034 let temp_dir = TempDir::new().unwrap();
1035 let db_path = temp_dir.path().join("test.db");
1036
1037 let result = super::ThingsDatabase::new(&db_path).await;
1040 assert!(result.is_err());
1041 }
1042
1043 #[tokio::test]
1044 async fn test_connection_string() {
1045 let result = super::ThingsDatabase::from_connection_string("sqlite::memory:").await;
1046 assert!(result.is_ok());
1047 }
1048
1049 #[test]
1050 fn test_task_status_from_i32() {
1051 assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
1052 assert_eq!(TaskStatus::from_i32(1), Some(TaskStatus::Completed));
1053 assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
1054 assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Trashed));
1055 assert_eq!(TaskStatus::from_i32(4), None);
1056 assert_eq!(TaskStatus::from_i32(-1), None);
1057 }
1058
1059 #[test]
1060 fn test_task_type_from_i32() {
1061 assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
1062 assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
1063 assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
1064 assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
1065 assert_eq!(TaskType::from_i32(4), None);
1066 assert_eq!(TaskType::from_i32(-1), None);
1067 }
1068
1069 #[test]
1070 fn test_database_stats_total_items() {
1071 let stats = DatabaseStats {
1072 task_count: 10,
1073 project_count: 5,
1074 area_count: 3,
1075 };
1076 assert_eq!(stats.total_items(), 18);
1077
1078 let empty_stats = DatabaseStats {
1079 task_count: 0,
1080 project_count: 0,
1081 area_count: 0,
1082 };
1083 assert_eq!(empty_stats.total_items(), 0);
1084 }
1085
1086 #[test]
1087 fn test_database_pool_config_default() {
1088 let config = DatabasePoolConfig::default();
1089 assert_eq!(config.max_connections, 10);
1090 assert_eq!(config.min_connections, 1);
1091 assert_eq!(config.connect_timeout, Duration::from_secs(30));
1092 assert_eq!(config.idle_timeout, Duration::from_secs(600));
1093 assert_eq!(config.max_lifetime, Duration::from_secs(1800));
1094 assert!(config.test_before_acquire);
1095 }
1096
1097 #[test]
1098 fn test_sqlite_optimizations_default() {
1099 let opts = SqliteOptimizations::default();
1100 assert!(opts.enable_wal_mode);
1101 assert_eq!(opts.cache_size, -20000);
1102 assert_eq!(opts.synchronous_mode, "NORMAL".to_string());
1103 assert_eq!(opts.temp_store, "MEMORY".to_string());
1104 assert_eq!(opts.journal_mode, "WAL".to_string());
1105 assert_eq!(opts.mmap_size, 268_435_456);
1106 assert!(opts.enable_foreign_keys);
1107 assert!(opts.enable_query_planner);
1108 }
1109
1110 #[test]
1111 fn test_pool_health_status_creation() {
1112 let status = PoolHealthStatus {
1113 is_healthy: true,
1114 pool_size: 8,
1115 active_connections: 5,
1116 idle_connections: 3,
1117 max_connections: 10,
1118 min_connections: 1,
1119 connection_timeout: Duration::from_secs(30),
1120 idle_timeout: Some(Duration::from_secs(600)),
1121 max_lifetime: Some(Duration::from_secs(1800)),
1122 };
1123 assert!(status.is_healthy);
1124 assert_eq!(status.active_connections, 5);
1125 assert_eq!(status.idle_connections, 3);
1126 assert_eq!(status.pool_size, 8);
1127 }
1128
1129 #[test]
1130 fn test_pool_metrics_creation() {
1131 let metrics = PoolMetrics {
1132 pool_size: 8,
1133 active_connections: 5,
1134 idle_connections: 3,
1135 max_connections: 10,
1136 min_connections: 1,
1137 utilization_percentage: 80.0,
1138 is_healthy: true,
1139 response_time_ms: 50,
1140 connection_timeout: Duration::from_secs(30),
1141 idle_timeout: Some(Duration::from_secs(600)),
1142 max_lifetime: Some(Duration::from_secs(1800)),
1143 };
1144 assert!(metrics.is_healthy);
1145 assert_eq!(metrics.pool_size, 8);
1146 assert_eq!(metrics.active_connections, 5);
1147 assert_eq!(metrics.idle_connections, 3);
1148 assert!((metrics.utilization_percentage - 80.0).abs() < f64::EPSILON);
1149 assert_eq!(metrics.response_time_ms, 50);
1150 }
1151
1152 #[test]
1153 fn test_comprehensive_health_status_creation() {
1154 let pool_health = PoolHealthStatus {
1155 is_healthy: true,
1156 pool_size: 8,
1157 active_connections: 5,
1158 idle_connections: 3,
1159 max_connections: 10,
1160 min_connections: 1,
1161 connection_timeout: Duration::from_secs(30),
1162 idle_timeout: Some(Duration::from_secs(600)),
1163 max_lifetime: Some(Duration::from_secs(1800)),
1164 };
1165
1166 let pool_metrics = PoolMetrics {
1167 pool_size: 8,
1168 active_connections: 5,
1169 idle_connections: 3,
1170 max_connections: 10,
1171 min_connections: 1,
1172 utilization_percentage: 80.0,
1173 is_healthy: true,
1174 response_time_ms: 50,
1175 connection_timeout: Duration::from_secs(30),
1176 idle_timeout: Some(Duration::from_secs(600)),
1177 max_lifetime: Some(Duration::from_secs(1800)),
1178 };
1179
1180 let db_stats = DatabaseStats {
1181 task_count: 50,
1182 project_count: 10,
1183 area_count: 5,
1184 };
1185
1186 let health_status = ComprehensiveHealthStatus {
1187 overall_healthy: true,
1188 pool_health,
1189 pool_metrics,
1190 database_stats: db_stats,
1191 timestamp: Utc::now(),
1192 };
1193
1194 assert!(health_status.overall_healthy);
1195 assert_eq!(health_status.database_stats.total_items(), 65);
1196 }
1197
1198 #[test]
1199 fn test_safe_timestamp_convert_edge_cases() {
1200 assert_eq!(safe_timestamp_convert(1_609_459_200.0), 1_609_459_200); assert_eq!(safe_timestamp_convert(0.0), 0);
1205
1206 assert_eq!(safe_timestamp_convert(-1.0), 0);
1208
1209 assert_eq!(safe_timestamp_convert(f64::INFINITY), 0);
1211
1212 assert_eq!(safe_timestamp_convert(f64::NAN), 0);
1214
1215 assert_eq!(safe_timestamp_convert(5_000_000_000.0), 0);
1217
1218 let max_timestamp = 4_102_444_800_f64; assert_eq!(safe_timestamp_convert(max_timestamp), 4_102_444_800);
1221 }
1222
1223 #[test]
1224 fn test_things_uuid_to_uuid_consistency() {
1225 let things_id = "test-id-123";
1227 let uuid1 = things_uuid_to_uuid(things_id);
1228 let uuid2 = things_uuid_to_uuid(things_id);
1229 assert_eq!(uuid1, uuid2, "UUIDs should be consistent for same input");
1230
1231 let uuid3 = things_uuid_to_uuid("different-id");
1233 assert_ne!(
1234 uuid1, uuid3,
1235 "Different inputs should produce different UUIDs"
1236 );
1237
1238 let uuid_empty = things_uuid_to_uuid("");
1240 assert!(!uuid_empty.to_string().is_empty());
1241
1242 let long_string = "a".repeat(1000);
1244 let uuid_long = things_uuid_to_uuid(&long_string);
1245 assert!(!uuid_long.to_string().is_empty());
1246 }
1247
1248 #[test]
1249 fn test_task_status_from_i32_all_variants() {
1250 assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
1251 assert_eq!(TaskStatus::from_i32(1), Some(TaskStatus::Completed));
1252 assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
1253 assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Trashed));
1254 assert_eq!(TaskStatus::from_i32(999), None);
1255 assert_eq!(TaskStatus::from_i32(-1), None);
1256 }
1257
1258 #[test]
1259 fn test_task_type_from_i32_all_variants() {
1260 assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
1261 assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
1262 assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
1263 assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
1264 assert_eq!(TaskType::from_i32(999), None);
1265 assert_eq!(TaskType::from_i32(-1), None);
1266 }
1267
1268 #[test]
1269 fn test_database_pool_config_default_values() {
1270 let config = DatabasePoolConfig::default();
1271 assert_eq!(config.max_connections, 10);
1272 assert_eq!(config.min_connections, 1);
1273 assert_eq!(config.connect_timeout, Duration::from_secs(30));
1274 assert_eq!(config.idle_timeout, Duration::from_secs(600));
1275 assert_eq!(config.max_lifetime, Duration::from_secs(1800));
1276 assert!(config.test_before_acquire);
1277 }
1278
1279 #[test]
1280 fn test_database_stats_total_items_calculation() {
1281 let stats = DatabaseStats {
1282 task_count: 10,
1283 project_count: 5,
1284 area_count: 3,
1285 };
1286 assert_eq!(stats.total_items(), 18); let empty_stats = DatabaseStats {
1290 task_count: 0,
1291 project_count: 0,
1292 area_count: 0,
1293 };
1294 assert_eq!(empty_stats.total_items(), 0);
1295 }
1296
1297 #[test]
1298 fn test_pool_health_status_creation_comprehensive() {
1299 let status = PoolHealthStatus {
1300 is_healthy: true,
1301 pool_size: 8,
1302 active_connections: 2,
1303 idle_connections: 3,
1304 max_connections: 10,
1305 min_connections: 1,
1306 connection_timeout: Duration::from_secs(30),
1307 idle_timeout: Some(Duration::from_secs(600)),
1308 max_lifetime: Some(Duration::from_secs(1800)),
1309 };
1310 assert!(status.is_healthy);
1311 assert_eq!(status.pool_size, 8);
1312 assert_eq!(status.max_connections, 10);
1313 }
1314
1315 #[test]
1316 fn test_pool_metrics_creation_comprehensive() {
1317 let metrics = PoolMetrics {
1318 pool_size: 8,
1319 active_connections: 5,
1320 idle_connections: 3,
1321 max_connections: 10,
1322 min_connections: 1,
1323 utilization_percentage: 80.0,
1324 is_healthy: true,
1325 response_time_ms: 50,
1326 connection_timeout: Duration::from_secs(30),
1327 idle_timeout: Some(Duration::from_secs(600)),
1328 max_lifetime: Some(Duration::from_secs(1800)),
1329 };
1330 assert_eq!(metrics.pool_size, 8);
1331 assert_eq!(metrics.response_time_ms, 50);
1332 assert!(metrics.is_healthy);
1333 }
1334
1335 #[test]
1336 fn test_comprehensive_health_status_creation_full() {
1337 let pool_health = PoolHealthStatus {
1338 is_healthy: true,
1339 pool_size: 8,
1340 active_connections: 2,
1341 idle_connections: 3,
1342 max_connections: 10,
1343 min_connections: 1,
1344 connection_timeout: Duration::from_secs(30),
1345 idle_timeout: Some(Duration::from_secs(600)),
1346 max_lifetime: Some(Duration::from_secs(1800)),
1347 };
1348
1349 let pool_metrics = PoolMetrics {
1350 pool_size: 8,
1351 active_connections: 5,
1352 idle_connections: 3,
1353 max_connections: 10,
1354 min_connections: 1,
1355 utilization_percentage: 80.0,
1356 is_healthy: true,
1357 response_time_ms: 50,
1358 connection_timeout: Duration::from_secs(30),
1359 idle_timeout: Some(Duration::from_secs(600)),
1360 max_lifetime: Some(Duration::from_secs(1800)),
1361 };
1362
1363 let database_stats = DatabaseStats {
1364 task_count: 100,
1365 project_count: 20,
1366 area_count: 5,
1367 };
1368
1369 let status = ComprehensiveHealthStatus {
1370 overall_healthy: true,
1371 pool_health,
1372 pool_metrics,
1373 database_stats,
1374 timestamp: Utc::now(),
1375 };
1376
1377 assert!(status.overall_healthy);
1378 assert_eq!(status.database_stats.total_items(), 125);
1379 }
1380
1381 #[test]
1382 fn test_sqlite_optimizations_default_values() {
1383 let opts = SqliteOptimizations::default();
1384 assert!(opts.enable_wal_mode);
1385 assert!(opts.enable_foreign_keys);
1386 assert_eq!(opts.cache_size, -20000);
1387 assert_eq!(opts.temp_store, "MEMORY");
1388 assert_eq!(opts.mmap_size, 268_435_456);
1389 assert_eq!(opts.synchronous_mode, "NORMAL");
1390 assert_eq!(opts.journal_mode, "WAL");
1391 }
1392
1393 #[test]
1394 fn test_get_default_database_path_format() {
1395 let path = get_default_database_path();
1396 let path_str = path.to_string_lossy();
1397 assert!(path_str.contains("Things Database.thingsdatabase"));
1398 assert!(path_str.contains("main.sqlite"));
1399 assert!(path_str.contains("Library/Group Containers"));
1400 }
1401
1402 #[tokio::test]
1403 async fn test_database_new_with_config() {
1404 let temp_file = NamedTempFile::new().unwrap();
1405 let db_path = temp_file.path();
1406
1407 crate::test_utils::create_test_database(db_path)
1408 .await
1409 .unwrap();
1410
1411 let config = DatabasePoolConfig {
1412 max_connections: 5,
1413 min_connections: 1,
1414 connect_timeout: Duration::from_secs(10),
1415 idle_timeout: Duration::from_secs(300),
1416 max_lifetime: Duration::from_secs(900),
1417 test_before_acquire: true,
1418 sqlite_optimizations: SqliteOptimizations::default(),
1419 };
1420
1421 let database = ThingsDatabase::new_with_config(db_path, config)
1422 .await
1423 .unwrap();
1424 let pool = database.pool();
1425 assert!(!pool.is_closed());
1426 }
1427
1428 #[tokio::test]
1429 async fn test_database_error_handling_invalid_path() {
1430 let result = ThingsDatabase::new(Path::new("/non/existent/path.db")).await;
1432 assert!(result.is_err(), "Should fail with non-existent path");
1433 }
1434
1435 #[tokio::test]
1436 async fn test_database_get_stats() {
1437 let temp_file = NamedTempFile::new().unwrap();
1438 let db_path = temp_file.path();
1439
1440 crate::test_utils::create_test_database(db_path)
1441 .await
1442 .unwrap();
1443 let database = ThingsDatabase::new(db_path).await.unwrap();
1444
1445 let stats = database.get_stats().await.unwrap();
1446 assert!(stats.task_count > 0, "Should have test tasks");
1447 assert!(stats.area_count > 0, "Should have test areas");
1448 assert!(stats.total_items() > 0, "Should have total items");
1449 }
1450
1451 #[tokio::test]
1452 async fn test_database_comprehensive_health_check() {
1453 let temp_file = NamedTempFile::new().unwrap();
1454 let db_path = temp_file.path();
1455
1456 crate::test_utils::create_test_database(db_path)
1457 .await
1458 .unwrap();
1459 let database = ThingsDatabase::new(db_path).await.unwrap();
1460
1461 let health = database.comprehensive_health_check().await.unwrap();
1462 assert!(health.overall_healthy, "Database should be healthy");
1463 assert!(health.pool_health.is_healthy, "Pool should be healthy");
1464 assert!(
1465 health.pool_metrics.is_healthy,
1466 "Pool metrics should be healthy"
1467 );
1468 }
1469}