Skip to main content

kasl/db/
db.rs

1//! Core database connection management and initialization infrastructure.
2//!
3//! Provides foundational database functionality including connection management,
4//! schema initialization, and migration orchestration.
5//!
6//! ## Features
7//!
8//! - **Connection Management**: Establishing and configuring SQLite connections
9//! - **Schema Initialization**: Ensuring database structure is properly set up
10//! - **Migration Orchestration**: Coordinating automatic schema updates
11//! - **Configuration Enforcement**: Applying consistent database settings
12//! - **Error Handling**: Providing robust error management for database operations
13//!
14//! ## Usage
15//!
16//! ```rust,no_run
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! use kasl::db::db::Db;
19//!
20//! let db = Db::new()?;
21//! let count: i32 = db.conn.query_row(
22//!     "SELECT COUNT(*) FROM tasks",
23//!     [],
24//!     |row| row.get(0)
25//! )?;
26//! # Ok(())
27//! # }
28//! ```
29
30use crate::db::migrations;
31use crate::libs::data_storage::DataStorage;
32use anyhow::Result;
33use rusqlite::Connection;
34
35/// Standard filename for the SQLite database file.
36///
37/// This constant ensures consistency across the application when referencing
38/// the database file. The name is designed to be:
39/// - **Descriptive**: Clearly identifies the application and purpose
40/// - **Platform Safe**: Compatible with all target operating systems
41/// - **Version Neutral**: Suitable for use across application versions
42pub const DB_FILE_NAME: &str = "kasl.db";
43
44/// Core database manager providing connection and initialization services.
45///
46/// The `Db` struct serves as the primary interface for database access throughout
47/// the kasl application. It encapsulates a SQLite connection with all necessary
48/// configuration applied and provides methods for both standard operations and
49/// specialized scenarios like migration management.
50///
51/// ## Design Philosophy
52///
53/// The struct follows the principle of "initialization with validation" - when
54/// a `Db` instance is created, callers can be confident that:
55/// - Database file is accessible and writable
56/// - Schema is current and properly migrated
57/// - Foreign key constraints are active
58/// - Connection is ready for immediate use
59///
60/// ## Thread Safety Considerations
61///
62/// SQLite connections are not thread-safe by default. Each `Db` instance
63/// should be used within a single thread, or appropriate synchronization
64/// mechanisms should be employed when sharing connections across threads.
65///
66/// ## Connection Configuration
67///
68/// All connections are configured with:
69/// - Foreign key constraint enforcement enabled
70/// - Local timezone handling for timestamp operations
71/// - Appropriate pragma settings for desktop application usage
72/// - Transaction isolation levels suitable for single-user scenarios
73pub struct Db {
74    /// The configured SQLite database connection.
75    ///
76    /// This connection has been fully initialized with:
77    /// - Foreign key constraints enabled for referential integrity
78    /// - All pending database migrations applied automatically
79    /// - Appropriate configuration for kasl's usage patterns
80    /// - UTF-8 encoding configured for international text support
81    ///
82    /// The connection can be used directly for custom queries or passed
83    /// to specialized database modules for specific operations.
84    pub conn: Connection,
85}
86
87impl Db {
88    /// Creates a new database instance with complete initialization and migration.
89    ///
90    /// This is the primary constructor for database access in the kasl application.
91    /// It performs the complete database setup process including file location
92    /// resolution, connection establishment, configuration application, and
93    /// automatic schema migration to the latest version.
94    ///
95    /// ## Initialization Process
96    ///
97    /// 1. **File Path Resolution**: Determines the appropriate database file location
98    ///    using platform-specific application data directories
99    /// 2. **Directory Creation**: Ensures all parent directories in the path exist
100    /// 3. **Connection Establishment**: Opens SQLite connection to the database file
101    /// 4. **Foreign Key Activation**: Enables referential integrity enforcement
102    /// 5. **Migration Execution**: Automatically applies any pending schema updates
103    /// 6. **Validation**: Confirms the database is ready for application use
104    ///
105    /// ## Migration Behavior
106    ///
107    /// The method automatically applies all pending database migrations during
108    /// initialization. This ensures that:
109    /// - Schema is always current with the application version
110    /// - Data migrations preserve existing information
111    /// - New features have required database structures available
112    /// - Rollback scenarios are handled appropriately
113    ///
114    /// # Returns
115    ///
116    /// Returns a fully initialized `Db` instance ready for immediate use,
117    /// or an error if any step of the initialization process fails.
118    ///
119    /// # Example
120    ///
121    /// ```rust,no_run
122    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
123    /// use kasl::db::db::Db;
124    ///
125    /// // Standard database initialization
126    /// let db = Db::new()?;
127    ///
128    /// // Database is ready for queries
129    /// let task_count: i32 = db.conn.query_row(
130    ///     "SELECT COUNT(*) FROM tasks",
131    ///     [],
132    ///     |row| row.get(0)
133    /// )?;
134    ///
135    /// println!("Database contains {} tasks", task_count);
136    /// # Ok(())
137    /// # }
138    /// ```
139    ///
140    /// # Error Scenarios
141    ///
142    /// This method can fail in several scenarios:
143    /// - **File System**: Cannot create database directories or files
144    /// - **Permissions**: Insufficient permissions for database file access
145    /// - **Corruption**: Database file exists but is corrupted or incompatible
146    /// - **Migration**: Schema migration fails due to data incompatibility
147    /// - **Configuration**: SQLite configuration cannot be applied
148    ///
149    /// # Performance Notes
150    ///
151    /// The initialization process includes file system operations and potential
152    /// database migrations, which may take time on first run or after updates.
153    /// Subsequent initializations with an existing, current database are much faster.
154    pub fn new() -> Result<Self> {
155        // Resolve the platform-appropriate database file path
156        let db_file_path = DataStorage::new().get_path(DB_FILE_NAME)?;
157
158        // Establish connection to the SQLite database
159        let mut conn = Connection::open(db_file_path)?;
160
161        // Enable foreign key constraint enforcement for referential integrity
162        conn.execute("PRAGMA foreign_keys = ON", [])?;
163
164        // Apply all pending database migrations to ensure current schema
165        migrations::init_with_migrations(&mut conn)?;
166
167        Ok(Self { conn })
168    }
169
170    /// Creates a database connection without automatic migration application.
171    ///
172    /// This specialized constructor provides access to the database without
173    /// triggering automatic schema migrations. It's designed for use cases
174    /// that require precise control over migration timing or need to inspect
175    /// database state before migration.
176    ///
177    /// ## Use Cases
178    ///
179    /// - **Migration Tools**: Utilities that manage migrations manually
180    /// - **Database Inspection**: Tools that examine schema state and version
181    /// - **Testing Scenarios**: Tests that require specific migration states
182    /// - **Recovery Operations**: Procedures that work with partially migrated databases
183    /// - **Backup/Export**: Operations that need database access before migration
184    ///
185    /// ## Limited Functionality
186    ///
187    /// Connections created with this method may have limited functionality
188    /// if the database schema is not current. Application code should generally
189    /// use `Db::new()` for standard database access.
190    ///
191    /// ## Configuration Applied
192    ///
193    /// Even without migrations, this method still applies essential configuration:
194    /// - Foreign key constraint enforcement
195    /// - Basic SQLite pragma settings
196    /// - UTF-8 encoding configuration
197    ///
198    /// # Returns
199    ///
200    /// Returns a raw SQLite connection with basic configuration applied,
201    /// or an error if the database file cannot be accessed or the connection fails.
202    ///
203    /// # Example
204    ///
205    /// ```rust,no_run
206    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
207    /// use kasl::db::db::Db;
208    /// use kasl::db::migrations::{get_db_version, needs_migration};
209    ///
210    /// // Get connection without automatic migrations
211    /// let conn = Db::new_without_migrations()?;
212    ///
213    /// // Check current migration status
214    /// let current_version = get_db_version(&conn)?;
215    /// let needs_update = needs_migration(&conn)?;
216    ///
217    /// println!("Database version: {}", current_version);
218    /// if needs_update {
219    ///     println!("Database needs migration");
220    ///     // Apply migrations manually if needed
221    /// }
222    /// # Ok(())
223    /// # }
224    /// ```
225    ///
226    /// # Safety Considerations
227    ///
228    /// Using this method requires careful consideration of database schema
229    /// compatibility. Operations on databases with outdated schemas may:
230    /// - Fail due to missing tables or columns
231    /// - Produce incorrect results due to schema changes
232    /// - Cause data corruption if schema assumptions are violated
233    ///
234    /// # When to Use
235    ///
236    /// This method should be used only when:
237    /// - Building migration management tools
238    /// - Implementing database diagnostic utilities
239    /// - Creating testing scenarios that require specific schema states
240    /// - Performing database recovery or maintenance operations
241    ///
242    /// For all standard application database access, use `Db::new()` instead.
243    pub fn new_without_migrations() -> Result<Connection> {
244        // Resolve the database file path using the same logic as the main constructor
245        let db_file_path = DataStorage::new().get_path(DB_FILE_NAME)?;
246
247        // Create a basic SQLite connection without additional setup
248        let conn = Connection::open(db_file_path)?;
249
250        // Enable foreign keys for consistency, even without migrations
251        // This ensures referential integrity regardless of migration state
252        conn.execute("PRAGMA foreign_keys = ON", [])?;
253
254        Ok(conn)
255    }
256}