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 // Version 12: inbox reconciliation and change tracking.
360 //
361 // `gone_at` marks issues that stopped appearing in the Jira poll
362 // (closed, reassigned) so the list stops showing them instead of
363 // freezing on the first sync. `last_change`/`changed_at` record the
364 // most recent visible change (status, priority, score) for badges
365 // and toasts.
366 self.add_migration(12, "jira_inbox_gone_and_change_tracking", |tx| {
367 tx.execute("ALTER TABLE jira_inbox ADD COLUMN gone_at TIMESTAMP", [])?;
368 tx.execute("ALTER TABLE jira_inbox ADD COLUMN last_change TEXT", [])?;
369 tx.execute("ALTER TABLE jira_inbox ADD COLUMN changed_at TIMESTAMP", [])?;
370 Ok(())
371 });
372 }
373
374 /// Registers a single migration in the migration system.
375 ///
376 /// This helper method adds a migration to the internal registry with
377 /// proper version ordering and validation. It ensures that migrations
378 /// are stored in a consistent format for later execution.
379 ///
380 /// # Arguments
381 ///
382 /// * `version` - Unique version number for this migration
383 /// * `name` - Descriptive name for the migration's purpose
384 /// * `up` - Function that performs the actual schema transformation
385 ///
386 /// # Panics
387 ///
388 /// Panics if a migration with the same version number is already registered.
389 fn add_migration(&mut self, version: u32, name: &'static str, up: fn(&Transaction) -> Result<()>) {
390 self.migrations.push(Migration { version, name, up });
391 }
392
393 /// Executes all pending migrations in the correct order.
394 ///
395 /// This method performs the complete migration process:
396 /// 1. Creates the migrations tracking table if needed
397 /// 2. Determines current database version
398 /// 3. Identifies pending migrations
399 /// 4. Applies each migration within a transaction
400 /// 5. Records successful migrations in the tracking table
401 ///
402 /// ## Transaction Safety
403 ///
404 /// Each migration runs in its own transaction, ensuring that partial
405 /// failures don't leave the database in an inconsistent state. If any
406 /// migration fails, all changes are rolled back automatically.
407 ///
408 /// # Arguments
409 ///
410 /// * `conn` - Mutable database connection for applying migrations
411 ///
412 /// # Returns
413 ///
414 /// Returns `Ok(())` if all migrations succeed, or an error if any
415 /// migration fails during application.
416 ///
417 /// # Example
418 ///
419 /// ```rust
420 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
421 /// use kasl::db::migrations::MigrationManager;
422 /// use rusqlite::Connection;
423 ///
424 /// let manager = MigrationManager::new();
425 /// let mut conn = Connection::open(":memory:")?;
426 /// manager.run_migrations(&mut conn)?;
427 /// # Ok(())
428 /// # }
429 /// ```
430 pub fn run_migrations(&self, conn: &mut Connection) -> Result<()> {
431 // Initialize the migrations tracking table
432 conn.execute(MIGRATIONS_TABLE, [])?;
433
434 // Determine the current schema version
435 let current_version = self.get_current_version(conn)?;
436
437 // Find all migrations that haven't been applied yet
438 let pending: Vec<&Migration> = self.migrations.iter().filter(|m| m.version > current_version).collect();
439
440 // Exit early if no migrations are needed
441 if pending.is_empty() {
442 msg_debug!("Database is up to date");
443 return Ok(());
444 }
445
446 // Notify user about pending migrations
447 msg_info!(Message::MigrationsFound(pending.len()));
448
449 // Execute all pending migrations within a single transaction
450 let tx = conn.transaction()?;
451
452 for migration in pending {
453 msg_info!(Message::RunningMigration(migration.version, migration.name.to_string()));
454
455 match (migration.up)(&tx) {
456 Ok(()) => {
457 // Record successful migration in tracking table
458 tx.execute(
459 "INSERT INTO migrations (version, name) VALUES (?1, ?2)",
460 params![migration.version, migration.name],
461 )?;
462 msg_success!(Message::MigrationCompleted(migration.version));
463 }
464 Err(e) => {
465 // Log migration failure and propagate error
466 msg_error!(Message::MigrationFailed(migration.version, e.to_string()));
467 return Err(e);
468 }
469 }
470 }
471
472 // Commit all successful migrations
473 tx.commit()?;
474 msg_success!(Message::AllMigrationsCompleted);
475
476 Ok(())
477 }
478
479 /// Retrieves the current database schema version.
480 ///
481 /// This method queries the migrations table to determine the highest
482 /// version number that has been successfully applied. It handles the
483 /// case where no migrations have been applied yet (version 0).
484 ///
485 /// # Arguments
486 ///
487 /// * `conn` - Database connection for querying migration status
488 ///
489 /// # Returns
490 ///
491 /// Returns the current schema version number, or 0 if no migrations
492 /// have been applied yet.
493 fn get_current_version(&self, conn: &Connection) -> Result<u32> {
494 let version: Option<u32> = conn.query_row("SELECT MAX(version) FROM migrations", [], |row| row.get(0)).unwrap_or(Some(0));
495
496 Ok(version.unwrap_or(0))
497 }
498
499 /// Checks if a specific migration version has been applied.
500 ///
501 /// This utility method allows callers to verify whether a particular
502 /// migration has been successfully applied to the database. Useful
503 /// for conditional logic based on schema capabilities.
504 ///
505 /// # Arguments
506 ///
507 /// * `conn` - Database connection for querying migration status
508 /// * `version` - Migration version number to check
509 ///
510 /// # Returns
511 ///
512 /// Returns `true` if the migration has been applied, `false` otherwise.
513 ///
514 /// # Example
515 ///
516 /// ```rust
517 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
518 /// use kasl::db::migrations::MigrationManager;
519 /// use rusqlite::Connection;
520 ///
521 /// let manager = MigrationManager::new();
522 /// let mut conn = Connection::open(":memory:")?;
523 /// manager.run_migrations(&mut conn)?;
524 /// if manager.is_migration_applied(&conn, 3)? {
525 /// // Tags system is available
526 /// }
527 /// # Ok(())
528 /// # }
529 /// ```
530 pub fn is_migration_applied(&self, conn: &Connection, version: u32) -> Result<bool> {
531 let count: i32 = conn.query_row("SELECT COUNT(*) FROM migrations WHERE version = ?1", params![version], |row| row.get(0))?;
532
533 Ok(count > 0)
534 }
535
536 /// Retrieves the complete migration history with timestamps.
537 ///
538 /// This method returns a chronological list of all applied migrations,
539 /// including their version numbers, names, and application timestamps.
540 /// Useful for auditing and debugging schema evolution.
541 ///
542 /// # Arguments
543 ///
544 /// * `conn` - Database connection for querying migration history
545 ///
546 /// # Returns
547 ///
548 /// Returns a vector of tuples containing (version, name, applied_at)
549 /// for each applied migration, ordered by version number.
550 ///
551 /// # Example
552 ///
553 /// ```rust
554 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
555 /// use kasl::db::migrations::MigrationManager;
556 /// use rusqlite::Connection;
557 ///
558 /// let manager = MigrationManager::new();
559 /// let mut conn = Connection::open(":memory:")?;
560 /// manager.run_migrations(&mut conn)?;
561 /// let history = manager.get_migration_history(&conn)?;
562 /// for (version, name, applied_at) in history {
563 /// println!("v{}: {} ({})", version, name, applied_at);
564 /// }
565 /// # Ok(())
566 /// # }
567 /// ```
568 pub fn get_migration_history(&self, conn: &Connection) -> Result<Vec<(u32, String, String)>> {
569 let mut stmt = conn.prepare("SELECT version, name, applied_at FROM migrations ORDER BY version")?;
570
571 let history = stmt
572 .query_map([], |row| Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?)))?
573 .collect::<Result<Vec<_>, _>>()?;
574
575 Ok(history)
576 }
577
578 /// Rolls back migrations to a specific target version (debug builds only).
579 ///
580 /// This development utility allows rolling back migrations to a previous
581 /// schema version by removing migration records from the tracking table.
582 ///
583 /// ## ⚠️ Important Notes
584 ///
585 /// - Only available in debug builds for safety
586 /// - This is a simplified rollback that removes migration records
587 /// - Does not actually reverse schema changes (no down() functions)
588 /// - Primarily useful for development and testing scenarios
589 ///
590 /// # Arguments
591 ///
592 /// * `conn` - Mutable database connection for rollback operations
593 /// * `target_version` - Target version to roll back to
594 ///
595 /// # Returns
596 ///
597 /// Returns `Ok(())` if rollback succeeds, or an error if the operation fails.
598 ///
599 /// # Example
600 ///
601 /// ```rust
602 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
603 /// use kasl::db::migrations::MigrationManager;
604 /// use rusqlite::Connection;
605 ///
606 /// #[cfg(debug_assertions)]
607 /// {
608 /// let manager = MigrationManager::new();
609 /// let mut conn = Connection::open(":memory:")?;
610 /// manager.run_migrations(&mut conn)?;
611 /// manager.rollback_to(&mut conn, 2)?; // Roll back to version 2
612 /// }
613 /// # Ok(())
614 /// # }
615 /// ```
616 #[cfg(debug_assertions)]
617 pub fn rollback_to(&self, conn: &mut Connection, target_version: u32) -> Result<()> {
618 let current_version = self.get_current_version(conn)?;
619
620 if target_version >= current_version {
621 msg_info!(Message::NothingToRollback);
622 return Ok(());
623 }
624
625 msg_info!(Message::RollingBack(current_version, target_version));
626
627 // Remove migration records beyond the target version
628 // Note: This is a simplified rollback that doesn't actually reverse schema changes
629 conn.execute("DELETE FROM migrations WHERE version > ?1", params![target_version])?;
630
631 msg_success!(Message::RollbackCompleted(target_version));
632 Ok(())
633 }
634}
635
636/// Initializes a database connection with all pending migrations applied.
637///
638/// This convenience function creates a migration manager and applies all
639/// pending migrations to the provided connection. It's the recommended
640/// way to ensure a database is up to date with the latest schema.
641///
642/// # Arguments
643///
644/// * `conn` - Mutable database connection to initialize
645///
646/// # Returns
647///
648/// Returns `Ok(())` if initialization succeeds, or an error if migration fails.
649///
650/// # Example
651///
652/// ```rust,no_run
653/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
654/// use kasl::db::migrations::init_with_migrations;
655/// use rusqlite::Connection;
656///
657/// let mut conn = Connection::open("kasl.db")?;
658/// init_with_migrations(&mut conn)?;
659/// # Ok(())
660/// # }
661/// ```
662pub fn init_with_migrations(conn: &mut Connection) -> Result<()> {
663 let manager = MigrationManager::new();
664 manager.run_migrations(conn)?;
665 Ok(())
666}
667
668/// Retrieves the current database schema version.
669///
670/// This utility function provides a simple way to check the current
671/// schema version without creating a full migration manager instance.
672///
673/// # Arguments
674///
675/// * `conn` - Database connection to query
676///
677/// # Returns
678///
679/// Returns the current schema version number.
680///
681/// # Example
682///
683/// ```rust
684/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
685/// use kasl::db::migrations::get_db_version;
686/// use rusqlite::Connection;
687///
688/// let conn = Connection::open(":memory:")?;
689/// let version = get_db_version(&conn)?;
690/// println!("Current schema version: {}", version);
691/// # Ok(())
692/// # }
693/// ```
694pub fn get_db_version(conn: &Connection) -> Result<u32> {
695 let manager = MigrationManager::new();
696 manager.get_current_version(conn)
697}
698
699/// Checks if the database requires migration to the latest schema version.
700///
701/// This utility function compares the current database version with the
702/// latest available migration version to determine if updates are needed.
703///
704/// # Arguments
705///
706/// * `conn` - Database connection to check
707///
708/// # Returns
709///
710/// Returns `true` if migrations are needed, `false` if up to date.
711///
712/// # Example
713///
714/// ```rust
715/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
716/// use kasl::db::migrations::needs_migration;
717/// use rusqlite::Connection;
718///
719/// let conn = Connection::open(":memory:")?;
720/// if needs_migration(&conn)? {
721/// println!("Database needs migration!");
722/// }
723/// # Ok(())
724/// # }
725/// ```
726pub fn needs_migration(conn: &Connection) -> Result<bool> {
727 let manager = MigrationManager::new();
728 let current = manager.get_current_version(conn)?;
729 let latest = manager.migrations.last().map(|m| m.version).unwrap_or(0);
730 Ok(current < latest)
731}