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}