wip: limited schema migration capability

This commit is contained in:
Jeremy Wall 2025-07-03 20:25:28 -05:00
parent dcfa8bd313
commit a6e501f3e5
2 changed files with 132 additions and 8 deletions

View File

@ -172,4 +172,22 @@ async fn test_reference_without_content_address() {
assert!(matches!(result, Err(StoreError::NoSuchContentAddress))); 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);
}

View File

@ -1,9 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use std::collections::{BTreeMap, HashMap};
use thiserror::Error; use thiserror::Error;
use offline_web_model::Reference; use offline_web_model::Reference;
use sqlx::{Pool, Row, Sqlite, SqlitePool}; use sqlx::{Pool, Row, Sqlite, SqlitePool};
// Schema version constants
const CURRENT_SCHEMA_VERSION: i32 = 1;
const INITIAL_SCHEMA_VERSION: i32 = 0;
pub struct SqliteReferenceStore { pub struct SqliteReferenceStore {
pool: Pool<Sqlite>, pool: Pool<Sqlite>,
} }
@ -33,7 +38,95 @@ impl SqliteReferenceStore {
.await .await
.map_err(|e| StoreError::StorageError(Box::new(e)))?; .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<i32, StoreError> {
// 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( sqlx::query(
r#" r#"
CREATE TABLE IF NOT EXISTS ref_entries ( CREATE TABLE IF NOT EXISTS ref_entries (
@ -43,7 +136,7 @@ impl SqliteReferenceStore {
) )
"#, "#,
) )
.execute(&pool) .execute(&mut **tx)
.await .await
.map_err(|e| StoreError::StorageError(Box::new(e)))?; .map_err(|e| StoreError::StorageError(Box::new(e)))?;
@ -58,7 +151,7 @@ impl SqliteReferenceStore {
) )
"#, "#,
) )
.execute(&pool) .execute(&mut **tx)
.await .await
.map_err(|e| StoreError::StorageError(Box::new(e)))?; .map_err(|e| StoreError::StorageError(Box::new(e)))?;
@ -70,11 +163,24 @@ impl SqliteReferenceStore {
) )
"#, "#,
) )
.execute(&pool) .execute(&mut **tx)
.await .await
.map_err(|e| StoreError::StorageError(Box::new(e)))?; .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> { pub async fn store_reference(&self, reference: &Reference) -> Result<(), StoreError> {
@ -299,8 +405,8 @@ impl SqliteReferenceStore {
.map_err(|e| StoreError::StorageError(Box::new(e)))?; .map_err(|e| StoreError::StorageError(Box::new(e)))?;
// Build the dependency tree iteratively // Build the dependency tree iteratively
let mut reference_map: std::collections::HashMap<String, Reference> = std::collections::HashMap::new(); let mut reference_map: HashMap<String, Reference> = HashMap::new();
let mut children_map: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new(); let mut children_map: HashMap<String, Vec<String>> = HashMap::new();
// First pass: create all references and build the children map // First pass: create all references and build the children map
for row in &rows { for row in &rows {
@ -321,7 +427,7 @@ impl SqliteReferenceStore {
} }
// Second pass: build the dependency tree from bottom up (highest depth first) // Second pass: build the dependency tree from bottom up (highest depth first)
let mut depth_groups: std::collections::BTreeMap<i32, Vec<String>> = std::collections::BTreeMap::new(); let mut depth_groups: BTreeMap<i32, Vec<String>> = BTreeMap::new();
for row in &rows { for row in &rows {
let id: String = row.get("id"); let id: String = row.get("id");
let depth: i32 = row.get("depth"); let depth: i32 = row.get("depth");