Skip to main content

linkrs_api/
session_variables.rs

1//! Transaction-aware session variable store shared by the server and
2//! embedded session implementations.
3
4use parking_lot::RwLock;
5use std::collections::HashMap;
6
7use linkrs_core::Value;
8
9/// A session-variable operation recorded while an explicit transaction is
10/// active. The overlay guarantees that ROLLBACK (and ROLLBACK TO SAVEPOINT)
11/// restores variables to their pre-statement values, matching transaction
12/// semantics. `Set` records both the previous value (for restore) and the
13/// new value (for snapshot merge); `Savepoint` marks a rollback boundary.
14#[derive(Debug, Clone)]
15enum VariableOp {
16    Set {
17        name: String,
18        prev: Option<Value>,
19        value: Value,
20    },
21    Savepoint {
22        name: String,
23    },
24}
25
26/// Session-scoped user variables (`$name`).
27///
28/// A base store plus a transaction overlay: assignments made while an
29/// explicit transaction is active are recorded as operations instead of
30/// mutating the base store directly. COMMIT merges the overlay into the
31/// base store; ROLLBACK (and ROLLBACK TO SAVEPOINT) restores the base to
32/// its pre-transaction values.
33#[derive(Debug, Default)]
34pub struct SessionVariables {
35    base: RwLock<HashMap<String, Value>>,
36    ops: RwLock<Vec<VariableOp>>,
37}
38
39impl SessionVariables {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Assign a session variable. When `in_transaction` is true, the
45    /// assignment is recorded on the overlay instead of the base store so
46    /// ROLLBACK / ROLLBACK TO SAVEPOINT restore the previous value.
47    pub fn set_variable(&self, name: String, value: Value, in_transaction: bool) {
48        if in_transaction {
49            let prev = self.variable_value(&name);
50            self.ops.write().push(VariableOp::Set { name, prev, value });
51        } else {
52            self.base.write().insert(name, value);
53        }
54    }
55
56    /// Effective value of a session variable: transaction overlay first,
57    /// then the base store.
58    pub fn variable_value(&self, name: &str) -> Option<Value> {
59        let ops = self.ops.read();
60        for op in ops.iter().rev() {
61            if let VariableOp::Set {
62                name: op_name,
63                value,
64                ..
65            } = op
66            {
67                if op_name == name {
68                    return Some(value.clone());
69                }
70            }
71        }
72        self.base.read().get(name).cloned()
73    }
74
75    /// Snapshot of all session variables (base + overlay) for injection as
76    /// query inputs.
77    pub fn variables_snapshot(&self) -> HashMap<String, Value> {
78        let mut merged = self.base.read().clone();
79        for op in self.ops.read().iter() {
80            if let VariableOp::Set { name, value, .. } = op {
81                merged.insert(name.clone(), value.clone());
82            }
83        }
84        merged
85    }
86
87    /// COMMIT: apply overlay operations to the base store and clear the
88    /// overlay (the last assignment of each variable wins).
89    pub fn commit_variables(&self) {
90        let mut base = self.base.write();
91        let ops = std::mem::take(&mut *self.ops.write());
92        for op in ops {
93            if let VariableOp::Set { name, value, .. } = op {
94                base.insert(name, value);
95            }
96        }
97    }
98
99    /// Full ROLLBACK: restore pre-transaction values and clear the overlay.
100    pub fn rollback_variables(&self) {
101        self.restore_overlay_after(None);
102    }
103
104    /// ROLLBACK TO SAVEPOINT: restore values assigned after the named
105    /// savepoint and truncate the overlay at the savepoint marker.
106    pub fn rollback_variables_to(&self, savepoint_name: &str) -> bool {
107        self.restore_overlay_after(Some(savepoint_name))
108    }
109
110    /// RELEASE SAVEPOINT: drop the marker (operations stay part of the
111    /// transaction; they are no longer individually rollback-able).
112    pub fn release_variable_savepoint(&self, savepoint_name: &str) {
113        let mut ops = self.ops.write();
114        ops.retain(|op| !matches!(op, VariableOp::Savepoint { name } if name == savepoint_name));
115    }
116
117    /// SAVEPOINT: record a variable-overlay boundary so ROLLBACK TO can
118    /// restore assignments made after the savepoint.
119    pub fn push_variable_savepoint(&self, savepoint_name: &str) {
120        self.ops.write().push(VariableOp::Savepoint {
121            name: savepoint_name.to_string(),
122        });
123    }
124
125    /// Restore variable values for operations at or after `savepoint_name`
126    /// (or the whole overlay when `None`), then truncate those operations.
127    /// Returns whether a matching savepoint marker was found.
128    fn restore_overlay_after(&self, savepoint_name: Option<&str>) -> bool {
129        let mut base = self.base.write();
130        let mut ops = self.ops.write();
131        let removed: Vec<VariableOp> = match savepoint_name {
132            Some(name) => {
133                let mut found = None;
134                for (idx, op) in ops.iter().enumerate() {
135                    if matches!(op, VariableOp::Savepoint { name: n } if n == name) {
136                        found = Some(idx + 1);
137                    }
138                }
139                match found {
140                    Some(idx) => ops.drain(idx..).collect(),
141                    None => return false,
142                }
143            }
144            None => std::mem::take(&mut *ops),
145        };
146        for op in removed.iter().rev() {
147            if let VariableOp::Set { name, prev, .. } = op {
148                match prev {
149                    Some(value) => {
150                        base.insert(name.clone(), value.clone());
151                    }
152                    None => {
153                        base.remove(name);
154                    }
155                }
156            }
157        }
158        true
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn store() -> SessionVariables {
167        SessionVariables::new()
168    }
169
170    #[test]
171    fn set_outside_transaction_mutates_base() {
172        let s = store();
173        s.set_variable("x".into(), Value::Int(1), false);
174        assert_eq!(s.variable_value("x"), Some(Value::Int(1)));
175        assert_eq!(s.variables_snapshot().len(), 1);
176    }
177
178    #[test]
179    fn rollback_restores_pre_transaction_value() {
180        let s = store();
181        s.set_variable("x".into(), Value::Int(1), false);
182        s.set_variable("x".into(), Value::Int(2), true);
183        s.set_variable("y".into(), Value::string("txn"), true);
184        assert_eq!(s.variable_value("x"), Some(Value::Int(2)));
185        assert_eq!(s.variable_value("y"), Some(Value::string("txn")));
186        s.rollback_variables();
187        assert_eq!(s.variable_value("x"), Some(Value::Int(1)));
188        assert_eq!(s.variable_value("y"), None);
189    }
190
191    #[test]
192    fn commit_merges_overlay() {
193        let s = store();
194        s.set_variable("a".into(), Value::Int(1), false);
195        s.set_variable("a".into(), Value::Int(5), true);
196        s.commit_variables();
197        assert_eq!(s.variable_value("a"), Some(Value::Int(5)));
198        assert_eq!(s.variables_snapshot().len(), 1);
199    }
200
201    #[test]
202    fn rollback_to_savepoint_restores_after_marker() {
203        let s = store();
204        s.set_variable("a".into(), Value::Int(1), false);
205        s.push_variable_savepoint("sp1");
206        s.set_variable("a".into(), Value::Int(2), true);
207        s.set_variable("b".into(), Value::Int(3), true);
208        assert!(s.rollback_variables_to("sp1"));
209        assert_eq!(s.variable_value("a"), Some(Value::Int(1)));
210        assert_eq!(s.variable_value("b"), None);
211        s.set_variable("b".into(), Value::Int(4), true);
212        assert!(!s.rollback_variables_to("missing"));
213        assert_eq!(s.variable_value("b"), Some(Value::Int(4)));
214    }
215
216    #[test]
217    fn release_savepoint_drops_marker() {
218        let s = store();
219        s.push_variable_savepoint("sp2");
220        s.release_variable_savepoint("sp2");
221        assert!(!s.rollback_variables_to("sp2"));
222    }
223
224    #[test]
225    fn rollback_of_new_variable_removes_it() {
226        let s = store();
227        s.set_variable("new".into(), Value::Int(42), true);
228        s.rollback_variables();
229        assert_eq!(s.variable_value("new"), None);
230    }
231}