]> uap-core.de Git - note.git/commitdiff
add version number to notes and check version when updating a note
authorOlaf Wintermann <olaf.wintermann@gmail.com>
Sun, 14 Jun 2026 15:25:11 +0000 (17:25 +0200)
committerOlaf Wintermann <olaf.wintermann@gmail.com>
Sun, 14 Jun 2026 15:25:11 +0000 (17:25 +0200)
application/src/backend.rs
application/src/lockmanager.rs
application/src/note.rs
entity/src/note.rs
migration/src/m20260502_184134_create_settings.rs

index 169c382fe5bf39fcfeb344762d5fd17645cf7113..ce601ecfb194885bf6df1a8b7383f117ef1633e9 100644 (file)
 
 use std::future::Future;
 use std::pin::Pin;
-use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbErr, EntityTrait, QueryFilter, ColumnTrait, Set, QueryOrder};
+use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, Set, QueryOrder, DbErr, ExprTrait};
 use tokio::runtime::Runtime;
 use std::sync::{Arc};
 use std::thread::JoinHandle;
 use tokio::sync::{broadcast, mpsc};
 use tokio::sync::broadcast::error::SendError;
-use migration::{Migrator, MigratorTrait};
+use migration::{Expr, Migrator, MigratorTrait};
 use ui_rs::ui;
 
 use entity::{collection, note, notecontent, profile};
 use entity::profile::Entity as Profile;
 use entity::collection::{create_notebook_hierarchy, CollectionType, Entity as Collection, Node};
-use entity::note::{Entity as Note};
+use entity::note::{Column, Entity as Note};
 use entity::notecontent::{Entity as NoteContent};
+use migration::prelude::Utc;
 use crate::lockmanager::LockManager;
 
 pub struct Backend {
@@ -381,19 +382,62 @@ impl BackendHandle {
     }
     
     pub fn save_note<F>(&self, id: NoteId, note: note::ActiveModel, content: Option<notecontent::ActiveModel>, callback: F)
-    where F: FnOnce(Result<(note::Model, i32), DbErr>) + Send + 'static {
+    where F: FnOnce(SaveNoteResult) + Send + 'static {
         let bhandle = self.clone();
         let cmd = Box::pin(async move {
-            let result = if note.note_id.is_set() {
-                note.update(&bhandle.backend.db).await
+            let result = if let Set(note_id) = note.note_id {
+                let mut update = Note::update_many();
+                if let Set(kind) = note.kind {
+                    update = update.col_expr(Column::Kind, Expr::value(kind));
+                }
+                if let Set(title) = note.title {
+                    update = update.col_expr(Column::Title, Expr::value(title));
+                }
+
+                let version = if let Set(version) = note.version {
+                    version
+                } else {
+                    // not setting version when calling save_note is a mistake and will probably
+                    // lead to a VersionConflict
+                    0
+                };
+
+                let uresult = update
+                    .col_expr(Column::Lastmodified, Expr::value(Utc::now()))
+                    .col_expr(Column::Version, Expr::col(Column::Version).add(1))
+                    .filter(Column::NoteId.eq(note_id))
+                    .filter(Column::Version.eq(version))
+                    .exec(&bhandle.backend.db).await;
+
+                match uresult {
+                    Ok(updated) => {
+                        if updated.rows_affected != 1 {
+                            Err(SaveNoteResult::VersionConflict)
+                        } else {
+                            let result = Note::find_by_id(note_id).one(&bhandle.backend.db).await;
+                            match result {
+                                Ok(Some(note)) => Ok(note),
+                                Ok(None) => Err(SaveNoteResult::Error(DbErr::Custom("note not found".into()))),
+                                Err(e) => Err(SaveNoteResult::Error(e)),
+                            }
+                        }
+                    },
+                    Err(e) => {
+                        Err(SaveNoteResult::Error(e))
+                    }
+                }
             } else {
-                note.insert(&bhandle.backend.db).await
+                let result = note.insert(&bhandle.backend.db).await;
+                match result {
+                    Ok(note) => Ok(note),
+                    Err(e) => Err(SaveNoteResult::Error(e)),
+                }
             };
 
             match result {
                 Ok(note) => {
                     let note_id = note.note_id;
-                    let result: Result<(note::Model, i32), DbErr>;
+                    let result: SaveNoteResult;
                     if let Some(mut content) = content {
                         let result2 = if content.id.is_set() {
                             content.update(&bhandle.backend.db).await
@@ -404,14 +448,14 @@ impl BackendHandle {
 
                         match result2 {
                             Ok(ctn) => {
-                                result = Ok((note.clone(), ctn.id));
+                                result = SaveNoteResult::Ok((note.clone(), ctn.id));
                             }
                             Err(e) => {
-                                result = Err(e);
+                                result = SaveNoteResult::Error(e);
                             }
                         }
                     } else {
-                        result = Ok((note.clone(), 0));
+                        result = SaveNoteResult::Ok((note.clone(), 0));
                     }
 
                     if result.is_ok() {
@@ -425,10 +469,29 @@ impl BackendHandle {
                     callback(result);
                 },
                 Err(e) => {
-                    callback(Err(e));
+                    callback(e);
                 }
             }
         });
         let _ = self.tx.send(cmd);
     }
 }
+
+
+pub enum SaveNoteResult {
+    Ok((entity::note::Model, i32)),
+    VersionConflict,
+    Error(DbErr)
+}
+
+impl SaveNoteResult {
+    pub fn is_ok(&self) -> bool {
+        matches!(self, SaveNoteResult::Ok(_))
+    }
+
+    /*
+    pub fn is_err(&self) -> bool {
+        matches!(self, SaveNoteResult::Error(_))
+    }
+    */
+}
\ No newline at end of file
index 1f0d9a03ff07c0cee9e22abbdec01c19a8eee530..7afb7365f6417848870e22714f32f3a029282166 100644 (file)
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  * POSSIBILITY OF SUCH DAMAGE.
  */
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::sync::Arc;
 
 pub struct LockManager {
-    locks: Arc<std::sync::Mutex<HashMap<i32, bool>>>,
+    locks: Arc<std::sync::Mutex<HashSet<i32>>>,
+    revisions: Arc<std::sync::Mutex<HashMap<i32, u64>>>,
 }
 
 impl LockManager {
     pub fn new() -> Self {
         LockManager {
-            locks: Arc::new(std::sync::Mutex::new(HashMap::new())),
+            locks: Arc::new(std::sync::Mutex::new(HashSet::new())),
+            revisions: Arc::new(std::sync::Mutex::new(HashMap::new())),
         }
     }
 
-    fn locks(&self) -> std::sync::MutexGuard<'_, HashMap<i32, bool>> {
+    fn locks(&self) -> std::sync::MutexGuard<'_, HashSet<i32>> {
         self.locks.lock().unwrap_or_else(|e| {
             // Very unlikely that the mutex gets poisoned (probably only by OOM), and if it does, who cares
-            println!("Error: LockManager poisoned");
+            println!("Error: LockManager locks poisoned");
             self.locks.clear_poison();
             e.into_inner()
         })
     }
 
+    fn revisions(&self) -> std::sync::MutexGuard<'_, HashMap<i32, u64>> {
+        self.revisions.lock().unwrap_or_else(|e| {
+            // Very unlikely that the mutex gets poisoned (probably only by OOM), and if it does, who cares
+            println!("Error: LockManager revisions poisoned");
+            self.revisions.clear_poison();
+            e.into_inner()
+        })
+    }
+
     pub fn lock_note(&self, note_id: i32) -> Option<NoteLock> {
         let mut locks = self.locks();
-        if locks.contains_key(&note_id) {
+        if locks.contains(&note_id) {
             println!("note {} already locked", note_id);
             return None
         }
         println!("lock note {}", note_id);
-        locks.insert(note_id, true);
-        
+        locks.insert(note_id);
+
         let note_lock = NoteLock {
             locks: self.locks.clone(),
             note_id: note_id
         };
         Some(note_lock)
     }
+
+
 }
 
 pub struct NoteLock {
-    locks: Arc<std::sync::Mutex<HashMap<i32, bool>>>,
-    note_id: i32
+    locks: Arc<std::sync::Mutex<HashSet<i32>>>,
+    pub note_id: i32
 }
 
 impl Drop for NoteLock {
@@ -76,4 +89,10 @@ impl Drop for NoteLock {
         let mut locks = self.locks.lock().unwrap_or_else(|e|{ e.into_inner()});
         locks.remove(&self.note_id);
     }
-}
\ No newline at end of file
+}
+
+pub struct NoteRevision {
+    revisions: Arc<std::sync::Mutex<HashMap<i32, u64>>>,
+    pub note_id: i32,
+    pub revision: u64
+}
index 76907571ac734edac7173d51d8ef0c332a4e8bc2..9dc7743bb71bace41f64da75c4762128bd58d6c9 100644 (file)
@@ -33,7 +33,7 @@ use sea_orm::{NotSet, Set};
 use entity::note::NoteType;
 use ui_rs::{action, ui_actions, UiModel};
 use ui_rs::ui::*;
-use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteTitleUpdate};
+use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteTitleUpdate, SaveNoteResult};
 use crate::lockmanager::NoteLock;
 use crate::window::NoteTypeTabView;
 
@@ -49,6 +49,7 @@ pub struct Note {
 
     pub collection_id: i32,
     pub content_id: i32,
+    pub version: i64,
 
     pub kind: NoteType,
 
@@ -76,6 +77,7 @@ impl Note {
             id: id,
             lock: None,
             content_id: 0,
+            version: 0,
             kind: NoteType::PlainTextNote,
             extract_title: false,
             title_start: -1,
@@ -122,6 +124,7 @@ impl Note {
 
     pub fn init_from_model(&mut self, model: &entity::note::Model) {
         self.id = NoteId::Id(model.note_id);
+        self.version = model.version;
 
         let tab = match model.kind {
             NoteType::PlainTextNote => NoteTypeTabView::TextArea,
@@ -218,7 +221,8 @@ impl Note {
             kind: Set(self.kind.clone()),
             title: Set(title),
             lastmodified: Set(Utc::now().into()),
-            created
+            created: created,
+            version: Set(self.version)
         };
 
         let notecontent = entity::notecontent::ActiveModel {
@@ -231,13 +235,17 @@ impl Note {
         self.backend.save_note(self.id.clone(), note, Some(notecontent), |result|{
             proxy.call_mainthread(move |_doc, note|{
                 match result {
-                    Ok((notemodel, content_id)) => {
+                    SaveNoteResult::Ok((notemodel, content_id)) => {
                         note.id = NoteId::Id(notemodel.note_id);
                         note.content_id = content_id;
-                        println!("Note saved: note_id: {}, content_id: {}", notemodel.note_id, content_id);
+                        note.version = notemodel.version;
+                        println!("Note saved: note_id: {}, content_id: {}, version: {}", notemodel.note_id, content_id, note.version);
                     },
-                    Err(error) => {
-                        println!("Failed to save note: {}", error);
+                    SaveNoteResult::VersionConflict => {
+                        println!("Failed to save note: version conflict");
+                    },
+                    SaveNoteResult::Error(e) => {
+                        println!("Failed to save note: {}", e);
                     }
                 }
             });
index c7d51ca9c3ad6871081f95dc711471ae3d121443..82831da581eea15f3e56541dca0c8bce1ce61c27 100644 (file)
@@ -16,7 +16,9 @@ pub struct Model {
     pub created: DateTimeWithTimeZone,
 
     #[sea_orm(has_one)]
-    pub content: HasOne<crate::notecontent::Entity>
+    pub content: HasOne<crate::notecontent::Entity>,
+
+    pub version: i64
 }
 
 #[derive(EnumIter, DeriveActiveEnum, Clone, Debug, PartialEq, Default)]
@@ -37,7 +39,8 @@ impl Model {
             kind: Default::default(),
             title: Default::default(),
             lastmodified: Default::default(),
-            created: Default::default()
+            created: Default::default(),
+            version: 0
         }
     }
 }
index 1001304db6a935fefba0de14191dd1069a69b0f1..3bdc1cbc932a437577616b974fec2c30ce223a6b 100644 (file)
@@ -52,6 +52,7 @@ impl MigrationTrait for Migration {
                     .col(string("title"))
                     .col(timestamp_with_time_zone("lastmodified"))
                     .col(timestamp_with_time_zone("created"))
+                    .col(big_integer("version"))
                     .to_owned()
             ).await?;