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