feat: run migrations from sql files

This commit is contained in:
kaichaosun 2026-02-27 14:23:13 +08:00
parent e099d5fd15
commit f4c08bd048
No known key found for this signature in database
GPG Key ID: 223E0F992F4F03BF
5 changed files with 78 additions and 34 deletions

View File

@ -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<Self, StorageError> {
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<Self, StorageError> {
db.connection().execute_batch(CHAT_SCHEMA)?;
/// Applies all migrations and returns the storage instance.
fn run_migrations(db: SqliteDb) -> Result<Self, StorageError> {
migrations::apply_migrations(db.connection())?;
Ok(Self { db })
}

View File

@ -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(())
}

View File

@ -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);

View File

@ -7,6 +7,7 @@
//! handles all storage operations automatically.
mod db;
mod migrations;
pub(crate) mod types;
pub(crate) use db::ChatStorage;

View File

@ -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};