sqlsrv/
changehook.rs

1//! The change log hook is an abstraction on top of SQLite hooks that only
2//! returns committed changes.
3//!
4//! SQLite hooks are called each time a table is updated within a transaction,
5//! which means that if a rollback occurs, all those updates are reverted.  In
6//! order to only return the committed changes the change log hook will keep
7//! record of all the changes and if a rollback occurs all those changes are
8//! discarded.  If a commit occurs the list of changes is passed to the
9//! application.
10//!
11//! There's a potential issue with this:  If the application performs a massive
12//! amount of updates within a transaction (like deleting 1 billion rows), the
13//! hook's change log may require _a lot_ of memory.
14//!
15//! As a workaround, applications can limit the amount of updates that it
16//! allows within a transaction.  (For instance, removing 1 billion rows could
17//! be done in batches of one thousand rows).
18//!
19//! If tables that may receive a massive amount of updates within a transaction
20//! do not need to be reported in the change log, the application can implement
21//! a `FromStr` that returns an error for that table.  (Returning errors from
22//! the `FromStr` implementation will cause the row event not to be included in
23//! the change log).
24//!
25//! Applications can also use the rawhook, to get closer to the low-level API.
26//! This still means that the same problem exists, but it allows the
27//! application greater freedom in how it chooses to solve it.
28
29use std::{str::FromStr, sync::Arc};
30
31use parking_lot::Mutex;
32
33use rusqlite::Connection;
34
35pub use crate::rawhook::Action;
36
37#[derive(Debug)]
38pub struct Change<D, T> {
39  pub action: Action,
40  pub database: D,
41  pub table: T,
42  pub rowid: i64
43}
44
45/// Application callback used to process committed changes to the database.
46pub trait ChangeLogHook {
47  type Database;
48  type Table;
49
50  /// Changelog callback.
51  ///
52  /// Will be called whenevert a set of actions are being _committed_.
53  /// Return `true` to change the commit to a rollback.
54  fn changelog(
55    &mut self,
56    log: Vec<Change<Self::Database, Self::Table>>
57  ) -> bool;
58}
59
60pub fn hook<D, T>(
61  conn: &Connection,
62  mut cb: Box<dyn ChangeLogHook<Database = D, Table = T> + Send>
63) where
64  D: FromStr + Send + Sized + 'static,
65  T: FromStr + Send + Sized + 'static
66{
67  //
68  // Allocate a shared changelog used to keep track of pending changes.
69  //
70  let changelog = Arc::new(Mutex::new(Vec::new()));
71
72  let chlog = Arc::clone(&changelog);
73  conn.commit_hook(Some(move || {
74    //println!("commit_hook");
75    let mut g = chlog.lock();
76    let log = std::mem::take(&mut *g);
77    drop(g);
78
79    // Pass log to application call-back
80    // Returning true will change commit to a rollback.
81    cb.changelog(log)
82  }));
83
84  let chlog = Arc::clone(&changelog);
85  conn.rollback_hook(Some(move || {
86    //println!("rollback_hook");
87    let mut g = chlog.lock();
88    g.clear();
89    drop(g);
90  }));
91
92  conn.update_hook(Some(move |action, db: &str, tbl: &str, rowid| {
93    let (Ok(database), Ok(table)) = (D::from_str(db), T::from_str(tbl)) else {
94      return;
95    };
96    let Ok(action) = Action::try_from(action) else {
97      // Just ignore unknown actions
98      return;
99    };
100    changelog.lock().push(Change {
101      action,
102      database,
103      table,
104      rowid
105    });
106  }));
107}
108
109// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :