From f4c08bd04816cd1ffad0d3fda517cf51ad4111f5 Mon Sep 17 00:00:00 2001 From: kaichaosun Date: Fri, 27 Feb 2026 14:23:13 +0800 Subject: [PATCH] feat: run migrations from sql files --- conversations/src/storage/db.rs | 38 +++------------- conversations/src/storage/migrations.rs | 44 +++++++++++++++++++ .../storage/migrations/001_initial_schema.sql | 27 ++++++++++++ conversations/src/storage/mod.rs | 1 + storage/src/lib.rs | 2 +- 5 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 conversations/src/storage/migrations.rs create mode 100644 conversations/src/storage/migrations/001_initial_schema.sql diff --git a/conversations/src/storage/db.rs b/conversations/src/storage/db.rs index 84ced02..807347b 100644 --- a/conversations/src/storage/db.rs +++ b/conversations/src/storage/db.rs @@ -3,38 +3,10 @@ use storage::{RusqliteError, SqliteDb, StorageConfig, StorageError, params}; use x25519_dalek::StaticSecret; +use super::migrations; use super::types::{ChatRecord, IdentityRecord}; use crate::identity::Identity; -/// Schema for chat storage tables. -/// Note: Ratchet state is stored by double_ratchets::RatchetStorage separately. -const CHAT_SCHEMA: &str = " - -- Identity table (single row) - CREATE TABLE IF NOT EXISTS identity ( - id INTEGER PRIMARY KEY CHECK (id = 1), - name TEXT NOT NULL, - secret_key BLOB NOT NULL - ); - - -- Inbox ephemeral keys for handshakes - CREATE TABLE IF NOT EXISTS inbox_keys ( - public_key_hex TEXT PRIMARY KEY, - secret_key BLOB NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) - ); - - -- Chat metadata - CREATE TABLE IF NOT EXISTS chats ( - chat_id TEXT PRIMARY KEY, - chat_type TEXT NOT NULL, - remote_public_key BLOB, - remote_address TEXT NOT NULL, - created_at INTEGER NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_chats_type ON chats(chat_type); -"; - /// Chat-specific storage operations. /// /// This struct wraps a SqliteDb and provides domain-specific @@ -49,12 +21,12 @@ impl ChatStorage { /// Creates a new ChatStorage with the given configuration. pub fn new(config: StorageConfig) -> Result { let db = SqliteDb::new(config)?; - Self::run_migration(db) + Self::run_migrations(db) } - /// Creates a new chat storage with the given database. - fn run_migration(db: SqliteDb) -> Result { - db.connection().execute_batch(CHAT_SCHEMA)?; + /// Applies all migrations and returns the storage instance. + fn run_migrations(db: SqliteDb) -> Result { + migrations::apply_migrations(db.connection())?; Ok(Self { db }) } diff --git a/conversations/src/storage/migrations.rs b/conversations/src/storage/migrations.rs new file mode 100644 index 0000000..41b3cb4 --- /dev/null +++ b/conversations/src/storage/migrations.rs @@ -0,0 +1,44 @@ +//! Database migrations module. +//! +//! SQL migrations are embedded at compile time and applied in order. + +use storage::{Connection, StorageError}; + +/// Embeds and returns all migration SQL files in order. +pub fn get_migrations() -> Vec<(&'static str, &'static str)> { + vec![( + "001_initial_schema", + include_str!("migrations/001_initial_schema.sql"), + )] +} + +/// Applies all migrations to the database. +/// Uses a simple version tracking table to avoid re-running migrations. +pub fn apply_migrations(conn: &Connection) -> Result<(), StorageError> { + // Create migrations tracking table if it doesn't exist + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS _migrations ( + name TEXT PRIMARY KEY, + applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + );", + )?; + + for (name, sql) in get_migrations() { + // Check if migration already applied + let already_applied: bool = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM _migrations WHERE name = ?1)", + [name], + |row| row.get(0), + )?; + + if !already_applied { + // Apply migration + conn.execute_batch(sql)?; + + // Record migration + conn.execute("INSERT INTO _migrations (name) VALUES (?1)", [name])?; + } + } + + Ok(()) +} diff --git a/conversations/src/storage/migrations/001_initial_schema.sql b/conversations/src/storage/migrations/001_initial_schema.sql new file mode 100644 index 0000000..70b5359 --- /dev/null +++ b/conversations/src/storage/migrations/001_initial_schema.sql @@ -0,0 +1,27 @@ +-- Initial schema for chat storage +-- Migration: 001_initial_schema + +-- Identity table (single row) +CREATE TABLE IF NOT EXISTS identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + name TEXT NOT NULL, + secret_key BLOB NOT NULL +); + +-- Inbox ephemeral keys for handshakes +CREATE TABLE IF NOT EXISTS inbox_keys ( + public_key_hex TEXT PRIMARY KEY, + secret_key BLOB NOT NULL, + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); + +-- Chat metadata +CREATE TABLE IF NOT EXISTS chats ( + chat_id TEXT PRIMARY KEY, + chat_type TEXT NOT NULL, + remote_public_key BLOB, + remote_address TEXT NOT NULL, + created_at INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_chats_type ON chats(chat_type); diff --git a/conversations/src/storage/mod.rs b/conversations/src/storage/mod.rs index 5153dbf..5d33d87 100644 --- a/conversations/src/storage/mod.rs +++ b/conversations/src/storage/mod.rs @@ -7,6 +7,7 @@ //! handles all storage operations automatically. mod db; +mod migrations; pub(crate) mod types; pub(crate) use db::ChatStorage; diff --git a/storage/src/lib.rs b/storage/src/lib.rs index bacc9b6..9240dc2 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -12,4 +12,4 @@ pub use errors::StorageError; pub use sqlite::{SqliteDb, StorageConfig}; // Re-export rusqlite types that domain crates will need -pub use rusqlite::{Error as RusqliteError, Transaction, params}; +pub use rusqlite::{Connection, Error as RusqliteError, Transaction, params};