kasl/db/migrations.rs
1//! Database schema migration management and versioning system.
2//!
3//! Provides a comprehensive migration framework for evolving the database schema
4//! over time while maintaining data integrity and consistency.
5//!
6//! ## Features
7//!
8//! - **Version Tracking**: Maintains precise records of applied migrations
9//! - **Automatic Application**: Runs pending migrations during database initialization
10//! - **Transaction Safety**: All migrations run within database transactions
11//! - **Rollback Support**: Development-time rollback capabilities (debug builds only)
12//! - **History Tracking**: Complete audit trail of schema changes
13//!
14//! ## Usage
15//!
16//! ```rust,no_run
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! use kasl::db::migrations::{init_with_migrations, get_db_version};
19//! use rusqlite::Connection;
20//!
21//! let mut conn = Connection::open("kasl.db")?;
22//! init_with_migrations(&mut conn)?;
23//! let version = get_db_version(&conn)?;
24//! # Ok(())
25//! # }
26//! ```
27
28use crate::libs::messages::Message;
29use crate::{msg_debug, msg_error, msg_info, msg_success};
30use anyhow::Result;
31use rusqlite::{Connection, Transaction, params};
32
33/// SQL schema for the migrations tracking table.
34///
35/// This table maintains a complete record of all applied migrations,
36/// enabling version tracking and providing an audit trail of schema changes.
37/// Each migration is recorded with its version, name, and application timestamp.
38const MIGRATIONS_TABLE: &str = "
39CREATE TABLE IF NOT EXISTS migrations (
40 id INTEGER PRIMARY KEY,
41 version INTEGER NOT NULL UNIQUE,
42 name TEXT NOT NULL,
43 applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
44)";
45
46/// Represents a single database migration with execution logic.
47///
48/// Each migration contains the information needed to apply a specific
49/// schema change, including version tracking and the transformation function.
50/// Migrations are designed to be immutable and deterministic.
51#[derive(Debug, Clone)]
52struct Migration {
53 /// Unique version number for ordering and tracking
54 version: u32,
55 /// Human-readable name describing the migration's purpose
56 name: &'static str,
57 /// Function that applies the schema changes within a transaction
58 up: fn(&Transaction) -> Result<()>,
59}
60
61/// Central migration system manager that orchestrates schema evolution.
62///
63/// The `MigrationManager` maintains the complete registry of available migrations
64/// and provides the logic for applying them in the correct order. It ensures
65/// that migrations are applied atomically and tracks their completion status.
66///
67/// ## Architecture
68///
69/// - **Migration Registry**: Stores all available migrations in version order
70/// - **Version Control**: Tracks current schema version and pending changes
71/// - **Transaction Management**: Ensures each migration is atomic
72/// - **Error Recovery**: Provides rollback on migration failures
73///
74/// ## Thread Safety
75///
76/// The migration manager is designed for single-threaded use during application
77/// startup. Multiple concurrent migration attempts should be avoided.
78pub struct MigrationManager {
79 /// Ordered list of all available migrations
80 ///
81 /// Migrations are stored in version order to ensure correct application
82 /// sequence. Each migration builds upon the schema state created by
83 /// its predecessors.
84 migrations: Vec<Migration>,
85}
86
87impl Default for MigrationManager {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl MigrationManager {
94 /// Creates a new migration manager with all registered migrations.
95 ///
96 /// This constructor automatically registers all available migrations
97 /// in the correct order. The registration process is deterministic
98 /// and ensures consistent schema evolution across all environments.
99 ///
100 /// # Returns
101 ///
102 /// Returns a fully initialized migration manager ready to apply
103 /// pending schema changes.
104 ///
105 /// # Example
106 ///
107 /// ```rust
108 /// use kasl::db::migrations::MigrationManager;
109 ///
110 /// let manager = MigrationManager::new();
111 /// // Manager is ready to apply migrations
112 /// ```
113 pub fn new() -> Self {
114 let mut manager = Self { migrations: Vec::new() };
115
116 // Register all migrations in chronological order
117 // Each registration adds a migration to the internal registry
118 manager.register_migrations();
119 manager
120 }
121
122 /// Registers all database migrations in chronological order.
123 ///
124 /// This method defines the complete schema evolution history by registering
125 /// each migration version with its transformation logic. Migrations must
126 /// be registered in sequential version order to ensure correct application.
127 ///
128 /// ## Migration Design Principles
129 ///
130 /// - **Incremental**: Each migration makes small, focused changes
131 /// - **Idempotent**: Migrations can be safely re-run if needed
132 /// - **Forward-Only**: No backward compatibility requirements
133 /// - **Atomic**: Each migration succeeds or fails completely
134 fn register_migrations(&mut self) {
135 // Initial schema - version 0 is implicit (empty database)
136 // Base tables are created by individual modules as needed
137
138 // Version 1: Base tables and performance indices
139 // Creates fundamental tables and adds indices for better performance
140 self.add_migration(1, "create_tables_and_indices", |tx| {
141 // First, create base tables that individual modules depend on
142 // This ensures tables exist before any indices are created
143
144 // Create tasks table
145 tx.execute(
146 "CREATE TABLE IF NOT EXISTS tasks (
147 id INTEGER NOT NULL PRIMARY KEY,
148 task_id INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0,
149 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
150 name TEXT NOT NULL,
151 comment TEXT,
152 completeness INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 100,
153 excluded_from_search BOOLEAN NOT NULL ON CONFLICT REPLACE DEFAULT FALSE
154 )",
155 [],
156 )?;
157
158 // Create pauses table
159 tx.execute(
160 "CREATE TABLE IF NOT EXISTS pauses (
161 id INTEGER NOT NULL PRIMARY KEY,
162 start TIMESTAMP NOT NULL,
163 end TIMESTAMP,
164 duration INTEGER
165 )",
166 [],
167 )?;
168
169 // Create workdays table
170 tx.execute(
171 "CREATE TABLE IF NOT EXISTS workdays (
172 id INTEGER PRIMARY KEY,
173 date DATE NOT NULL UNIQUE,
174 start TIMESTAMP NOT NULL,
175 end TIMESTAMP
176 )",
177 [],
178 )?;
179
180 // Now create indices for the tables we just created
181
182 // Index tasks by timestamp for chronological queries
183 tx.execute("CREATE INDEX IF NOT EXISTS idx_tasks_timestamp ON tasks(timestamp)", [])?;
184 // Index tasks by parent task relationship
185 tx.execute("CREATE INDEX IF NOT EXISTS idx_tasks_task_id ON tasks(task_id)", [])?;
186 // Index pauses by start time for temporal queries
187 tx.execute("CREATE INDEX IF NOT EXISTS idx_pauses_start ON pauses(start)", [])?;
188 // Index workdays by date for daily/monthly reporting
189 tx.execute("CREATE INDEX IF NOT EXISTS idx_workdays_date ON workdays(date)", [])?;
190
191 Ok(())
192 });
193
194 // Version 2: Task templates system for reusable task patterns
195 // Introduces the ability to save and reuse common task configurations
196 self.add_migration(2, "add_task_templates", |tx| {
197 tx.execute(
198 "CREATE TABLE IF NOT EXISTS task_templates (
199 id INTEGER PRIMARY KEY,
200 name TEXT NOT NULL UNIQUE,
201 task_name TEXT NOT NULL,
202 comment TEXT,
203 completeness INTEGER DEFAULT 100,
204 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
205 )",
206 [],
207 )?;
208 Ok(())
209 });
210
211 // Version 3: Tags and categorization system for task organization
212 // Adds support for tagging tasks with customizable labels and colors
213 self.add_migration(3, "add_tags_system", |tx| {
214 // Main tags table for storing tag definitions
215 tx.execute(
216 "CREATE TABLE IF NOT EXISTS tags (
217 id INTEGER PRIMARY KEY,
218 name TEXT NOT NULL UNIQUE,
219 color TEXT,
220 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
221 )",
222 [],
223 )?;
224
225 // Junction table for many-to-many task-tag relationships
226 tx.execute(
227 "CREATE TABLE IF NOT EXISTS task_tags (
228 task_id INTEGER NOT NULL,
229 tag_id INTEGER NOT NULL,
230 PRIMARY KEY (task_id, tag_id),
231 FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
232 FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
233 )",
234 [],
235 )?;
236 Ok(())
237 });
238
239 // Version 4: Soft delete functionality for data preservation
240 // Enables logical deletion while maintaining data for auditing
241 self.add_migration(4, "add_soft_delete", |tx| {
242 tx.execute("ALTER TABLE tasks ADD COLUMN deleted_at TIMESTAMP", [])?;
243 tx.execute("CREATE INDEX idx_tasks_deleted_at ON tasks(deleted_at)", [])?;
244 Ok(())
245 });
246
247 // Version 5: Workday notes and annotations for context tracking
248 // Allows users to add contextual notes to their workdays
249 self.add_migration(5, "add_workday_notes", |tx| {
250 tx.execute("ALTER TABLE workdays ADD COLUMN notes TEXT", [])?;
251 Ok(())
252 });
253
254 // Version 6: Manual breaks table for productivity management
255 // Enables users to add manual break periods to improve productivity calculations
256 self.add_migration(6, "add_breaks_table", |tx| {
257 tx.execute(
258 "CREATE TABLE IF NOT EXISTS breaks (
259 id INTEGER PRIMARY KEY,
260 date DATE NOT NULL,
261 start_time DATETIME NOT NULL,
262 end_time DATETIME NOT NULL,
263 duration INTEGER NOT NULL,
264 reason TEXT,
265 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
266 )",
267 [],
268 )?;
269
270 // Create index for efficient daily break lookups
271 tx.execute("CREATE INDEX idx_breaks_date ON breaks(date)", [])?;
272 Ok(())
273 });
274
275 // Version 7: Jira inbox for assigned open issues and toast notifications
276 self.add_migration(7, "add_jira_inbox_table", |tx| {
277 tx.execute(
278 "CREATE TABLE IF NOT EXISTS jira_inbox (
279 issue_key TEXT PRIMARY KEY NOT NULL,
280 issue_id TEXT NOT NULL,
281 summary TEXT NOT NULL,
282 status TEXT NOT NULL,
283 priority TEXT,
284 priority_rank INTEGER NOT NULL DEFAULT 999,
285 url TEXT NOT NULL,
286 first_seen TIMESTAMP NOT NULL,
287 last_seen TIMESTAMP NOT NULL,
288 notified INTEGER NOT NULL DEFAULT 0,
289 pinned INTEGER NOT NULL DEFAULT 0,
290 dismissed INTEGER NOT NULL DEFAULT 0,
291 raw_updated TEXT
292 )",
293 [],
294 )?;
295 tx.execute(
296 "CREATE INDEX IF NOT EXISTS idx_jira_inbox_active
297 ON jira_inbox(dismissed, pinned DESC, priority_rank ASC, last_seen DESC)",
298 [],
299 )?;
300 Ok(())
301 });
302
303 // Version 8: status catalog + scoring/sort_value for inbox ranking
304 self.add_migration(8, "jira_inbox_status_id_and_sort_value", |tx| {
305 tx.execute(
306 "CREATE TABLE IF NOT EXISTS jira_statuses (
307 id TEXT PRIMARY KEY NOT NULL,
308 name TEXT NOT NULL
309 )",
310 [],
311 )?;
312 tx.execute("ALTER TABLE jira_inbox ADD COLUMN status_id TEXT", [])?;
313 tx.execute("ALTER TABLE jira_inbox ADD COLUMN sort_value REAL", [])?;
314 tx.execute(
315 "CREATE INDEX IF NOT EXISTS idx_jira_inbox_sort
316 ON jira_inbox(dismissed, pinned DESC, sort_value DESC, priority_rank ASC)",
317 [],
318 )?;
319 Ok(())
320 });
321
322 // Version 9: wipe legacy status name strings; use status_id + jira_statuses only
323 self.add_migration(9, "clear_jira_inbox_legacy_status_text", |tx| {
324 tx.execute("UPDATE jira_inbox SET status = ''", [])?;
325 Ok(())
326 });
327
328 // Version 10: drop unused legacy status text column (canonical: status_id)
329 self.add_migration(10, "drop_jira_inbox_legacy_status_column", |tx| {
330 tx.execute("ALTER TABLE jira_inbox DROP COLUMN status", [])?;
331 Ok(())
332 });
333
334 // Version 11: fold manual breaks into pauses as protected records.
335 //
336 // The separate `breaks` table held synthetic records whose times were
337 // invented by a placement heuristic; downstream code converted them to
338 // pauses anyway. Manual breaks now live in `pauses` with `protected = 1`,
339 // which exempts them from the duration threshold and from merging with
340 // adjacent pauses. Existing break rows are carried over so historical
341 // reports keep their numbers.
342 self.add_migration(11, "fold_breaks_into_protected_pauses", |tx| {
343 tx.execute("ALTER TABLE pauses ADD COLUMN protected INTEGER NOT NULL DEFAULT 0", [])?;
344 tx.execute("ALTER TABLE pauses ADD COLUMN reason TEXT", [])?;
345
346 // Carry over manual breaks; duration is stored in seconds in `pauses`
347 // but was stored in minutes in `breaks`.
348 tx.execute(
349 "INSERT INTO pauses (start, end, duration, protected, reason)
350 SELECT start_time, end_time, duration * 60, 1, reason FROM breaks",
351 [],
352 )?;
353
354 tx.execute("DROP INDEX IF EXISTS idx_breaks_date", [])?;
355 tx.execute("DROP TABLE IF EXISTS breaks", [])?;
356 Ok(())
357 });
358 }
359
360 /// Registers a single migration in the migration system.
361 ///
362 /// This helper method adds a migration to the internal registry with
363 /// proper version ordering and validation. It ensures that migrations
364 /// are stored in a consistent format for later execution.
365 ///
366 /// # Arguments
367 ///
368 /// * `version` - Unique version number for this migration
369 /// * `name` - Descriptive name for the migration's purpose
370 /// * `up` - Function that performs the actual schema transformation
371 ///
372 /// # Panics
373 ///
374 /// Panics if a migration with the same version number is already registered.
375 fn add_migration(&mut self, version: u32, name: &'static str, up: fn(&Transaction) -> Result<()>) {
376 self.migrations.push(Migration { version, name, up });
377 }
378
379 /// Executes all pending migrations in the correct order.
380 ///
381 /// This method performs the complete migration process:
382 /// 1. Creates the migrations tracking table if needed
383 /// 2. Determines current database version
384 /// 3. Identifies pending migrations
385 /// 4. Applies each migration within a transaction
386 /// 5. Records successful migrations in the tracking table
387 ///
388 /// ## Transaction Safety
389 ///
390 /// Each migration runs in its own transaction, ensuring that partial
391 /// failures don't leave the database in an inconsistent state. If any
392 /// migration fails, all changes are rolled back automatically.
393 ///
394 /// # Arguments
395 ///
396 /// * `conn` - Mutable database connection for applying migrations
397 ///
398 /// # Returns
399 ///
400 /// Returns `Ok(())` if all migrations succeed, or an error if any
401 /// migration fails during application.
402 ///
403 /// # Example
404 ///
405 /// ```rust
406 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
407 /// use kasl::db::migrations::MigrationManager;
408 /// use rusqlite::Connection;
409 ///
410 /// let manager = MigrationManager::new();
411 /// let mut conn = Connection::open(":memory:")?;
412 /// manager.run_migrations(&mut conn)?;
413 /// # Ok(())
414 /// # }
415 /// ```
416 pub fn run_migrations(&self, conn: &mut Connection) -> Result<()> {
417 // Initialize the migrations tracking table
418 conn.execute(MIGRATIONS_TABLE, [])?;
419
420 // Determine the current schema version
421 let current_version = self.get_current_version(conn)?;
422
423 // Find all migrations that haven't been applied yet
424 let pending: Vec<&Migration> = self.migrations.iter().filter(|m| m.version > current_version).collect();
425
426 // Exit early if no migrations are needed
427 if pending.is_empty() {
428 msg_debug!("Database is up to date");
429 return Ok(());
430 }
431
432 // Notify user about pending migrations
433 msg_info!(Message::MigrationsFound(pending.len()));
434
435 // Execute all pending migrations within a single transaction
436 let tx = conn.transaction()?;
437
438 for migration in pending {
439 msg_info!(Message::RunningMigration(migration.version, migration.name.to_string()));
440
441 match (migration.up)(&tx) {
442 Ok(()) => {
443 // Record successful migration in tracking table
444 tx.execute(
445 "INSERT INTO migrations (version, name) VALUES (?1, ?2)",
446 params![migration.version, migration.name],
447 )?;
448 msg_success!(Message::MigrationCompleted(migration.version));
449 }
450 Err(e) => {
451 // Log migration failure and propagate error
452 msg_error!(Message::MigrationFailed(migration.version, e.to_string()));
453 return Err(e);
454 }
455 }
456 }
457
458 // Commit all successful migrations
459 tx.commit()?;
460 msg_success!(Message::AllMigrationsCompleted);
461
462 Ok(())
463 }
464
465 /// Retrieves the current database schema version.
466 ///
467 /// This method queries the migrations table to determine the highest
468 /// version number that has been successfully applied. It handles the
469 /// case where no migrations have been applied yet (version 0).
470 ///
471 /// # Arguments
472 ///
473 /// * `conn` - Database connection for querying migration status
474 ///
475 /// # Returns
476 ///
477 /// Returns the current schema version number, or 0 if no migrations
478 /// have been applied yet.
479 fn get_current_version(&self, conn: &Connection) -> Result<u32> {
480 let version: Option<u32> = conn.query_row("SELECT MAX(version) FROM migrations", [], |row| row.get(0)).unwrap_or(Some(0));
481
482 Ok(version.unwrap_or(0))
483 }
484
485 /// Checks if a specific migration version has been applied.
486 ///
487 /// This utility method allows callers to verify whether a particular
488 /// migration has been successfully applied to the database. Useful
489 /// for conditional logic based on schema capabilities.
490 ///
491 /// # Arguments
492 ///
493 /// * `conn` - Database connection for querying migration status
494 /// * `version` - Migration version number to check
495 ///
496 /// # Returns
497 ///
498 /// Returns `true` if the migration has been applied, `false` otherwise.
499 ///
500 /// # Example
501 ///
502 /// ```rust
503 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
504 /// use kasl::db::migrations::MigrationManager;
505 /// use rusqlite::Connection;
506 ///
507 /// let manager = MigrationManager::new();
508 /// let mut conn = Connection::open(":memory:")?;
509 /// manager.run_migrations(&mut conn)?;
510 /// if manager.is_migration_applied(&conn, 3)? {
511 /// // Tags system is available
512 /// }
513 /// # Ok(())
514 /// # }
515 /// ```
516 pub fn is_migration_applied(&self, conn: &Connection, version: u32) -> Result<bool> {
517 let count: i32 = conn.query_row("SELECT COUNT(*) FROM migrations WHERE version = ?1", params![version], |row| row.get(0))?;
518
519 Ok(count > 0)
520 }
521
522 /// Retrieves the complete migration history with timestamps.
523 ///
524 /// This method returns a chronological list of all applied migrations,
525 /// including their version numbers, names, and application timestamps.
526 /// Useful for auditing and debugging schema evolution.
527 ///
528 /// # Arguments
529 ///
530 /// * `conn` - Database connection for querying migration history
531 ///
532 /// # Returns
533 ///
534 /// Returns a vector of tuples containing (version, name, applied_at)
535 /// for each applied migration, ordered by version number.
536 ///
537 /// # Example
538 ///
539 /// ```rust
540 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
541 /// use kasl::db::migrations::MigrationManager;
542 /// use rusqlite::Connection;
543 ///
544 /// let manager = MigrationManager::new();
545 /// let mut conn = Connection::open(":memory:")?;
546 /// manager.run_migrations(&mut conn)?;
547 /// let history = manager.get_migration_history(&conn)?;
548 /// for (version, name, applied_at) in history {
549 /// println!("v{}: {} ({})", version, name, applied_at);
550 /// }
551 /// # Ok(())
552 /// # }
553 /// ```
554 pub fn get_migration_history(&self, conn: &Connection) -> Result<Vec<(u32, String, String)>> {
555 let mut stmt = conn.prepare("SELECT version, name, applied_at FROM migrations ORDER BY version")?;
556
557 let history = stmt
558 .query_map([], |row| Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?)))?
559 .collect::<Result<Vec<_>, _>>()?;
560
561 Ok(history)
562 }
563
564 /// Rolls back migrations to a specific target version (debug builds only).
565 ///
566 /// This development utility allows rolling back migrations to a previous
567 /// schema version by removing migration records from the tracking table.
568 ///
569 /// ## ⚠️ Important Notes
570 ///
571 /// - Only available in debug builds for safety
572 /// - This is a simplified rollback that removes migration records
573 /// - Does not actually reverse schema changes (no down() functions)
574 /// - Primarily useful for development and testing scenarios
575 ///
576 /// # Arguments
577 ///
578 /// * `conn` - Mutable database connection for rollback operations
579 /// * `target_version` - Target version to roll back to
580 ///
581 /// # Returns
582 ///
583 /// Returns `Ok(())` if rollback succeeds, or an error if the operation fails.
584 ///
585 /// # Example
586 ///
587 /// ```rust
588 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
589 /// use kasl::db::migrations::MigrationManager;
590 /// use rusqlite::Connection;
591 ///
592 /// #[cfg(debug_assertions)]
593 /// {
594 /// let manager = MigrationManager::new();
595 /// let mut conn = Connection::open(":memory:")?;
596 /// manager.run_migrations(&mut conn)?;
597 /// manager.rollback_to(&mut conn, 2)?; // Roll back to version 2
598 /// }
599 /// # Ok(())
600 /// # }
601 /// ```
602 #[cfg(debug_assertions)]
603 pub fn rollback_to(&self, conn: &mut Connection, target_version: u32) -> Result<()> {
604 let current_version = self.get_current_version(conn)?;
605
606 if target_version >= current_version {
607 msg_info!(Message::NothingToRollback);
608 return Ok(());
609 }
610
611 msg_info!(Message::RollingBack(current_version, target_version));
612
613 // Remove migration records beyond the target version
614 // Note: This is a simplified rollback that doesn't actually reverse schema changes
615 conn.execute("DELETE FROM migrations WHERE version > ?1", params![target_version])?;
616
617 msg_success!(Message::RollbackCompleted(target_version));
618 Ok(())
619 }
620}
621
622/// Initializes a database connection with all pending migrations applied.
623///
624/// This convenience function creates a migration manager and applies all
625/// pending migrations to the provided connection. It's the recommended
626/// way to ensure a database is up to date with the latest schema.
627///
628/// # Arguments
629///
630/// * `conn` - Mutable database connection to initialize
631///
632/// # Returns
633///
634/// Returns `Ok(())` if initialization succeeds, or an error if migration fails.
635///
636/// # Example
637///
638/// ```rust,no_run
639/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
640/// use kasl::db::migrations::init_with_migrations;
641/// use rusqlite::Connection;
642///
643/// let mut conn = Connection::open("kasl.db")?;
644/// init_with_migrations(&mut conn)?;
645/// # Ok(())
646/// # }
647/// ```
648pub fn init_with_migrations(conn: &mut Connection) -> Result<()> {
649 let manager = MigrationManager::new();
650 manager.run_migrations(conn)?;
651 Ok(())
652}
653
654/// Retrieves the current database schema version.
655///
656/// This utility function provides a simple way to check the current
657/// schema version without creating a full migration manager instance.
658///
659/// # Arguments
660///
661/// * `conn` - Database connection to query
662///
663/// # Returns
664///
665/// Returns the current schema version number.
666///
667/// # Example
668///
669/// ```rust
670/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
671/// use kasl::db::migrations::get_db_version;
672/// use rusqlite::Connection;
673///
674/// let conn = Connection::open(":memory:")?;
675/// let version = get_db_version(&conn)?;
676/// println!("Current schema version: {}", version);
677/// # Ok(())
678/// # }
679/// ```
680pub fn get_db_version(conn: &Connection) -> Result<u32> {
681 let manager = MigrationManager::new();
682 manager.get_current_version(conn)
683}
684
685/// Checks if the database requires migration to the latest schema version.
686///
687/// This utility function compares the current database version with the
688/// latest available migration version to determine if updates are needed.
689///
690/// # Arguments
691///
692/// * `conn` - Database connection to check
693///
694/// # Returns
695///
696/// Returns `true` if migrations are needed, `false` if up to date.
697///
698/// # Example
699///
700/// ```rust
701/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
702/// use kasl::db::migrations::needs_migration;
703/// use rusqlite::Connection;
704///
705/// let conn = Connection::open(":memory:")?;
706/// if needs_migration(&conn)? {
707/// println!("Database needs migration!");
708/// }
709/// # Ok(())
710/// # }
711/// ```
712pub fn needs_migration(conn: &Connection) -> Result<bool> {
713 let manager = MigrationManager::new();
714 let current = manager.get_current_version(conn)?;
715 let latest = manager.migrations.last().map(|m| m.version).unwrap_or(0);
716 Ok(current < latest)
717}