kanban_persistence_sqlite/
lib.rs1pub mod sqlite_store;
2
3pub use sqlite_store::SqliteStore;
4pub use sqlite_store::SUPPORTED_SCHEMA_VERSION;
5
6use kanban_domain::KanbanError;
7use kanban_persistence::{PersistenceError, PersistenceStore, StoreFactory};
8use std::sync::Arc;
9
10#[cfg(feature = "test-helpers")]
24pub async fn write_test_metadata_with_schema_version(
25 path: &std::path::Path,
26 version: u32,
27) -> Result<(), PersistenceError> {
28 use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
29 let pool = SqlitePoolOptions::new()
30 .max_connections(1)
31 .connect_with(
32 SqliteConnectOptions::new()
33 .filename(path)
34 .create_if_missing(true),
35 )
36 .await
37 .map_err(|e| PersistenceError::Database(e.to_string()))?;
38 sqlx::raw_sql(&format!(
39 "CREATE TABLE IF NOT EXISTS metadata (
40 id INTEGER PRIMARY KEY CHECK (id = 1),
41 instance_id TEXT NOT NULL,
42 saved_at TEXT NOT NULL,
43 schema_version INTEGER NOT NULL DEFAULT {SUPPORTED_SCHEMA_VERSION},
44 writer_version TEXT,
45 writer_commit TEXT
46 );
47 INSERT OR REPLACE INTO metadata (id, instance_id, saved_at, schema_version)
48 VALUES (1, '550e8400-e29b-41d4-a716-446655440000', '2030-01-01T00:00:00Z', {version});"
49 ))
50 .execute(&pool)
51 .await
52 .map_err(|e| PersistenceError::Database(e.to_string()))?;
53 pool.close().await;
54 Ok(())
55}
56
57#[cfg(feature = "test-helpers")]
65pub async fn read_test_schema_version(
66 path: &std::path::Path,
67) -> Result<Option<u32>, PersistenceError> {
68 use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
69 let pool = SqlitePoolOptions::new()
70 .max_connections(1)
71 .connect_with(SqliteConnectOptions::new().filename(path))
72 .await
73 .map_err(|e| PersistenceError::Database(e.to_string()))?;
74 let version: Option<u32> =
75 sqlx::query_scalar("SELECT schema_version FROM metadata WHERE id = 1")
76 .fetch_optional(&pool)
77 .await
78 .map_err(|e| PersistenceError::Database(e.to_string()))?;
79 pool.close().await;
80 Ok(version)
81}
82
83pub struct SqliteStoreFactory;
84
85impl StoreFactory for SqliteStoreFactory {
86 fn name(&self) -> &str {
87 "sqlite"
88 }
89
90 fn matches_content(&self, header: &[u8]) -> bool {
91 header.starts_with(b"SQLite format 3\0")
92 }
93
94 fn create(
95 &self,
96 locator: &str,
97 ) -> Result<Arc<dyn PersistenceStore + Send + Sync>, PersistenceError> {
98 let handle = tokio::runtime::Handle::current();
99 if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::CurrentThread {
100 return Err(PersistenceError::Database(
101 "SqliteStoreFactory::create requires a multi-thread Tokio runtime; \
102 block_in_place is unavailable on a current_thread runtime. \
103 Use #[tokio::test(flavor = \"multi_thread\")] in tests."
104 .to_string(),
105 ));
106 }
107 let store = tokio::task::block_in_place(|| handle.block_on(SqliteStore::open(locator)))
108 .map_err(|e| match e {
114 KanbanError::UnsupportedFutureVersion {
115 file_version,
116 binary_max,
117 } => PersistenceError::UnsupportedFutureVersion {
118 file_version,
119 binary_max,
120 },
121 other => PersistenceError::Database(other.to_string()),
122 })?;
123 Ok(Arc::new(store))
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use kanban_persistence::StoreFactory;
131
132 #[test]
133 fn test_sqlite_factory_matches_content_sqlite_magic_bytes() {
134 let header = b"SQLite format 3\0extra";
135 assert!(SqliteStoreFactory.matches_content(header));
136 }
137
138 #[test]
139 fn test_sqlite_factory_matches_content_rejects_json() {
140 let header = b"{\"boards\": []}";
141 assert!(!SqliteStoreFactory.matches_content(header));
142 }
143
144 #[test]
145 fn test_sqlite_factory_matches_content_rejects_empty() {
146 assert!(!SqliteStoreFactory.matches_content(b""));
147 }
148
149 #[test]
150 fn test_sqlite_factory_name_is_sqlite() {
151 assert_eq!(SqliteStoreFactory.name(), "sqlite");
152 }
153
154 #[tokio::test(flavor = "multi_thread")]
155 async fn test_sqlite_factory_create_returns_persistence_store() {
156 let dir = tempfile::tempdir().unwrap();
157 let path = dir.path().join("test.db");
158 let store = SqliteStoreFactory.create(path.to_str().unwrap()).unwrap();
159 assert!(store.exists().await);
160 }
161}