1- //! Tihs crate implements a SQLite storage backend for the TaskChampion sync server.
1+ //! This crate implements a SQLite storage backend for the TaskChampion sync server.
2+ //!
3+ //! Use the [`SqliteStorage`] type as an implementation of the [`Storage`] trait.
4+ //!
5+ //! This crate is intended for small deployments of a sync server, supporting one or a small number
6+ //! of users. The schema for the database is considered an implementation detail. For more robust
7+ //! database support, consider `taskchampion-sync-server-storage-postgres`.
8+
29use anyhow:: Context ;
310use chrono:: { TimeZone , Utc } ;
411use rusqlite:: types:: { FromSql , ToSql } ;
@@ -43,7 +50,7 @@ impl SqliteStorage {
4350 /// Create a new instance using a database at the given directory.
4451 ///
4552 /// The database will be stored in a file named `taskchampion-sync-server.sqlite3` in the given
46- /// directory.
53+ /// directory. The database will be created if it does not exist.
4754 pub fn new < P : AsRef < Path > > ( directory : P ) -> anyhow:: Result < SqliteStorage > {
4855 std:: fs:: create_dir_all ( & directory)
4956 . with_context ( || format ! ( "Failed to create `{}`." , directory. as_ref( ) . display( ) ) ) ?;
@@ -176,7 +183,7 @@ impl StorageTxn for Txn {
176183 async fn new_client ( & mut self , latest_version_id : Uuid ) -> anyhow:: Result < ( ) > {
177184 self . con
178185 . execute (
179- "INSERT OR REPLACE INTO clients (client_id, latest_version_id) VALUES (?, ?)" ,
186+ "INSERT INTO clients (client_id, latest_version_id) VALUES (?, ?)" ,
180187 params ! [ & StoredUuid ( self . client_id) , & StoredUuid ( latest_version_id) ] ,
181188 )
182189 . context ( "Error creating/updating client" ) ?;
@@ -231,7 +238,6 @@ impl StorageTxn for Txn {
231238
232239 async fn get_version_by_parent (
233240 & mut self ,
234-
235241 parent_version_id : Uuid ,
236242 ) -> anyhow:: Result < Option < Version > > {
237243 self . get_version_impl (
@@ -249,7 +255,6 @@ impl StorageTxn for Txn {
249255
250256 async fn add_version (
251257 & mut self ,
252-
253258 version_id : Uuid ,
254259 parent_version_id : Uuid ,
255260 history_segment : Vec < u8 > ,
@@ -264,17 +269,26 @@ impl StorageTxn for Txn {
264269 ]
265270 )
266271 . context ( "Error adding version" ) ?;
267- self . con
272+ let rows_changed = self
273+ . con
268274 . execute (
269275 "UPDATE clients
270276 SET
271277 latest_version_id = ?,
272278 versions_since_snapshot = versions_since_snapshot + 1
273- WHERE client_id = ?" ,
274- params ! [ StoredUuid ( version_id) , StoredUuid ( self . client_id) , ] ,
279+ WHERE client_id = ? and latest_version_id = ?" ,
280+ params ! [
281+ StoredUuid ( version_id) ,
282+ StoredUuid ( self . client_id) ,
283+ StoredUuid ( parent_version_id)
284+ ] ,
275285 )
276286 . context ( "Error updating client for new version" ) ?;
277287
288+ if rows_changed == 0 {
289+ anyhow:: bail!( "clients.latest_version_id does not match parent_version_id" ) ;
290+ }
291+
278292 Ok ( ( ) )
279293 }
280294
@@ -328,12 +342,12 @@ mod test {
328342 assert_eq ! ( client. latest_version_id, latest_version_id) ;
329343 assert ! ( client. snapshot. is_none( ) ) ;
330344
331- let latest_version_id = Uuid :: new_v4 ( ) ;
332- txn. add_version ( latest_version_id , Uuid :: new_v4 ( ) , vec ! [ 1 , 1 ] )
345+ let new_version_id = Uuid :: new_v4 ( ) ;
346+ txn. add_version ( new_version_id , latest_version_id , vec ! [ 1 , 1 ] )
333347 . await ?;
334348
335349 let client = txn. get_client ( ) . await ?. unwrap ( ) ;
336- assert_eq ! ( client. latest_version_id, latest_version_id ) ;
350+ assert_eq ! ( client. latest_version_id, new_version_id ) ;
337351 assert ! ( client. snapshot. is_none( ) ) ;
338352
339353 let snap = Snapshot {
@@ -344,7 +358,7 @@ mod test {
344358 txn. set_snapshot ( snap. clone ( ) , vec ! [ 1 , 2 , 3 ] ) . await ?;
345359
346360 let client = txn. get_client ( ) . await ?. unwrap ( ) ;
347- assert_eq ! ( client. latest_version_id, latest_version_id ) ;
361+ assert_eq ! ( client. latest_version_id, new_version_id ) ;
348362 assert_eq ! ( client. snapshot. unwrap( ) , snap) ;
349363
350364 Ok ( ( ) )
@@ -368,8 +382,10 @@ mod test {
368382 let client_id = Uuid :: new_v4 ( ) ;
369383 let mut txn = storage. txn ( client_id) . await ?;
370384
371- let version_id = Uuid :: new_v4 ( ) ;
372385 let parent_version_id = Uuid :: new_v4 ( ) ;
386+ txn. new_client ( parent_version_id) . await ?;
387+
388+ let version_id = Uuid :: new_v4 ( ) ;
373389 let history_segment = b"abc" . to_vec ( ) ;
374390 txn. add_version ( version_id, parent_version_id, history_segment. clone ( ) )
375391 . await ?;
@@ -396,11 +412,35 @@ mod test {
396412 let client_id = Uuid :: new_v4 ( ) ;
397413 let mut txn = storage. txn ( client_id) . await ?;
398414
399- let version_id = Uuid :: new_v4 ( ) ;
400415 let parent_version_id = Uuid :: new_v4 ( ) ;
416+ txn. new_client ( parent_version_id) . await ?;
417+
418+ let version_id = Uuid :: new_v4 ( ) ;
401419 let history_segment = b"abc" . to_vec ( ) ;
402420 txn. add_version ( version_id, parent_version_id, history_segment. clone ( ) )
403421 . await ?;
422+ // Fails because the version already exists.
423+ assert ! ( txn
424+ . add_version( version_id, parent_version_id, history_segment. clone( ) )
425+ . await
426+ . is_err( ) ) ;
427+ Ok ( ( ) )
428+ }
429+
430+ #[ tokio:: test]
431+ async fn test_add_version_mismatch ( ) -> anyhow:: Result < ( ) > {
432+ let tmp_dir = TempDir :: new ( ) ?;
433+ let storage = SqliteStorage :: new ( tmp_dir. path ( ) ) ?;
434+ let client_id = Uuid :: new_v4 ( ) ;
435+ let mut txn = storage. txn ( client_id) . await ?;
436+
437+ let latest_version_id = Uuid :: new_v4 ( ) ;
438+ txn. new_client ( latest_version_id) . await ?;
439+
440+ let version_id = Uuid :: new_v4 ( ) ;
441+ let parent_version_id = Uuid :: new_v4 ( ) ; // != latest_version_id
442+ let history_segment = b"abc" . to_vec ( ) ;
443+ // Fails because the latest_version_id is not parent_version_id.
404444 assert ! ( txn
405445 . add_version( version_id, parent_version_id, history_segment. clone( ) )
406446 . await
0 commit comments