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 :