wip: limited schema migration capability
This commit is contained in:
parent
dcfa8bd313
commit
a6e501f3e5
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user