""" Database migrations using SQLite's user_version pragma. Migrations run automatically on startup. The user_version pragma tracks which migrations have been applied (defaults to 0 for existing databases). This approach is safe for existing users - their databases have user_version=0, so all migrations run in order on first startup after upgrade. """ import logging import aiosqlite logger = logging.getLogger(__name__) async def get_version(conn: aiosqlite.Connection) -> int: """Get current schema version from SQLite user_version pragma.""" cursor = await conn.execute("PRAGMA user_version") row = await cursor.fetchone() return row[0] if row else 0 async def set_version(conn: aiosqlite.Connection, version: int) -> None: """Set schema version using SQLite user_version pragma.""" await conn.execute(f"PRAGMA user_version = {version}") async def run_migrations(conn: aiosqlite.Connection) -> int: """ Run all pending migrations. Returns the number of migrations applied. """ version = await get_version(conn) applied = 0 # Migration 1: Add last_read_at columns for server-side read tracking if version < 1: logger.info("Applying migration 1: add last_read_at columns") await _migrate_001_add_last_read_at(conn) await set_version(conn, 1) applied += 1 # Future migrations go here: # if version < 2: # await _migrate_002_something(conn) # await set_version(conn, 2) # applied += 1 if applied > 0: logger.info("Applied %d migration(s), schema now at version %d", applied, await get_version(conn)) else: logger.debug("Schema up to date at version %d", version) return applied async def _migrate_001_add_last_read_at(conn: aiosqlite.Connection) -> None: """ Add last_read_at column to contacts and channels tables. This enables server-side read state tracking, replacing the localStorage approach for consistent read state across devices. ALTER TABLE ADD COLUMN is safe - it preserves existing data and handles the "column already exists" case gracefully. """ # Add to contacts table try: await conn.execute("ALTER TABLE contacts ADD COLUMN last_read_at INTEGER") logger.debug("Added last_read_at to contacts table") except aiosqlite.OperationalError as e: if "duplicate column name" in str(e).lower(): logger.debug("contacts.last_read_at already exists, skipping") else: raise # Add to channels table try: await conn.execute("ALTER TABLE channels ADD COLUMN last_read_at INTEGER") logger.debug("Added last_read_at to channels table") except aiosqlite.OperationalError as e: if "duplicate column name" in str(e).lower(): logger.debug("channels.last_read_at already exists, skipping") else: raise await conn.commit()