From a6e501f3e58d34e7b3c8d8afb651a8dcdca87204 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 3 Jul 2025 20:25:28 -0500 Subject: [PATCH] wip: limited schema migration capability --- offline-web-storage/src/integration_tests.rs | 18 +++ offline-web-storage/src/lib.rs | 122 +++++++++++++++++-- 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/offline-web-storage/src/integration_tests.rs b/offline-web-storage/src/integration_tests.rs index dce679c..d09343c 100644 --- a/offline-web-storage/src/integration_tests.rs +++ b/offline-web-storage/src/integration_tests.rs @@ -172,4 +172,22 @@ async fn test_reference_without_content_address() { assert!(matches!(result, Err(StoreError::NoSuchContentAddress))); } +#[tokio::test] +async fn test_schema_version_management() { + let store = create_test_store().await; + + // Verify the schema version is correctly set + let version = store.get_current_schema_version().await.unwrap(); + assert_eq!(version, 1, "Schema version should be 1"); + + // Verify we can still perform basic operations + let reference = Reference::new( + Some("test_content".to_string()), + "test_schema_version".to_string(), + ); + + store.store_reference(&reference).await.unwrap(); + let retrieved = store.get_reference(&reference.id).await.unwrap(); + assert_eq!(retrieved.name, reference.name); +} diff --git a/offline-web-storage/src/lib.rs b/offline-web-storage/src/lib.rs index fade3d4..68d9bd6 100644 --- a/offline-web-storage/src/lib.rs +++ b/offline-web-storage/src/lib.rs @@ -1,9 +1,14 @@ use std::sync::Arc; +use std::collections::{BTreeMap, HashMap}; use thiserror::Error; use offline_web_model::Reference; use sqlx::{Pool, Row, Sqlite, SqlitePool}; +// Schema version constants +const CURRENT_SCHEMA_VERSION: i32 = 1; +const INITIAL_SCHEMA_VERSION: i32 = 0; + pub struct SqliteReferenceStore { pool: Pool, } @@ -33,7 +38,95 @@ impl SqliteReferenceStore { .await .map_err(|e| StoreError::StorageError(Box::new(e)))?; - // Create tables if they don't exist + let store = Self { pool }; + + // Check current schema version and migrate if necessary + let current_version = store.get_current_schema_version().await?; + if current_version != CURRENT_SCHEMA_VERSION { + store.migrate_schema(current_version, CURRENT_SCHEMA_VERSION).await?; + } + + Ok(store) + } + + async fn get_current_schema_version(&self) -> Result { + // First, ensure the schema_version table exists + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS schema_version ( + version INTEGER PRIMARY KEY, + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, + description TEXT + ) + "#, + ) + .execute(&self.pool) + .await + .map_err(|e| StoreError::StorageError(Box::new(e)))?; + + // Get the current version + let row = sqlx::query( + r#" + SELECT version FROM schema_version ORDER BY version DESC LIMIT 1 + "#, + ) + .fetch_optional(&self.pool) + .await + .map_err(|e| StoreError::StorageError(Box::new(e)))?; + + match row { + Some(row) => { + let version: i32 = row.get("version"); + Ok(version) + } + None => { + // No version found, this is a fresh database + Ok(INITIAL_SCHEMA_VERSION) + } + } + } + + async fn migrate_schema(&self, from_version: i32, to_version: i32) -> Result<(), StoreError> { + if from_version == to_version { + return Ok(()); + } + + if from_version > to_version { + return Err(StoreError::StorageError( + "Downward migrations not currently supported".into() + )); + } + + // Use a transaction for the entire migration process + let mut tx = self.pool.begin().await + .map_err(|e| StoreError::StorageError(Box::new(e)))?; + + // Apply migrations step by step + let mut current_version = from_version; + while current_version < to_version { + match current_version { + 0 => { + // Migration from version 0 to 1: Initial schema setup + self.migrate_to_v1(&mut tx).await?; + current_version = 1; + } + _ => { + return Err(StoreError::StorageError( + format!("Unknown migration path from version {}", current_version).into() + )); + } + } + } + + // Commit all migrations + tx.commit().await + .map_err(|e| StoreError::StorageError(Box::new(e)))?; + + Ok(()) + } + + async fn migrate_to_v1(&self, tx: &mut sqlx::Transaction<'_, Sqlite>) -> Result<(), StoreError> { + // Create the main application tables sqlx::query( r#" CREATE TABLE IF NOT EXISTS ref_entries ( @@ -43,7 +136,7 @@ impl SqliteReferenceStore { ) "#, ) - .execute(&pool) + .execute(&mut **tx) .await .map_err(|e| StoreError::StorageError(Box::new(e)))?; @@ -58,7 +151,7 @@ impl SqliteReferenceStore { ) "#, ) - .execute(&pool) + .execute(&mut **tx) .await .map_err(|e| StoreError::StorageError(Box::new(e)))?; @@ -70,11 +163,24 @@ impl SqliteReferenceStore { ) "#, ) - .execute(&pool) + .execute(&mut **tx) .await .map_err(|e| StoreError::StorageError(Box::new(e)))?; - Ok(Self { pool }) + // Record the schema version + sqlx::query( + r#" + INSERT OR REPLACE INTO schema_version (version, description) + VALUES (?, ?) + "#, + ) + .bind(1) + .bind("Initial schema with ref_entries, ref_dependencies, and content_store tables") + .execute(&mut **tx) + .await + .map_err(|e| StoreError::StorageError(Box::new(e)))?; + + Ok(()) } pub async fn store_reference(&self, reference: &Reference) -> Result<(), StoreError> { @@ -299,8 +405,8 @@ impl SqliteReferenceStore { .map_err(|e| StoreError::StorageError(Box::new(e)))?; // Build the dependency tree iteratively - let mut reference_map: std::collections::HashMap = std::collections::HashMap::new(); - let mut children_map: std::collections::HashMap> = std::collections::HashMap::new(); + let mut reference_map: HashMap = HashMap::new(); + let mut children_map: HashMap> = HashMap::new(); // First pass: create all references and build the children map for row in &rows { @@ -321,7 +427,7 @@ impl SqliteReferenceStore { } // Second pass: build the dependency tree from bottom up (highest depth first) - let mut depth_groups: std::collections::BTreeMap> = std::collections::BTreeMap::new(); + let mut depth_groups: BTreeMap> = BTreeMap::new(); for row in &rows { let id: String = row.get("id"); let depth: i32 = row.get("depth");