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}