Skip to main content

radixdb_api/
transaction.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Transaction API
16//!
17//! Provides ACID transaction support with the same ergonomic API as Database.
18//!
19//! # Examples
20//!
21//! ```no_run
22//! use radixdb_api::Database;
23//! # fn main() -> radixdb_core::Result<()> {
24//!
25//! let db = Database::open("memory://")?;
26//! db.execute("CREATE TABLE accounts (id INTEGER, balance INTEGER)", ())?;
27//! db.execute("INSERT INTO accounts VALUES ($1, $2), ($3, $4)", (1, 1000, 2, 500))?;
28//!
29//! // Transfer money atomically
30//! let mut tx = db.begin()?;
31//! tx.execute("UPDATE accounts SET balance = balance - $1 WHERE id = $2", (100, 1))?;
32//! tx.execute("UPDATE accounts SET balance = balance + $1 WHERE id = $2", (100, 2))?;
33//! tx.commit()?;
34//! # Ok(())
35//! # }
36//! ```
37
38use std::sync::Arc;
39
40use crate::params::{NamedParams, ParamVec};
41use radixdb_core::{Error, Result};
42use radixdb_executor::context::{ExecutionContext, ExecutionContextBuilder};
43use radixdb_executor::result::ExecutionResult;
44use radixdb_executor::Executor;
45use radixdb_storage::mvcc::engine::MVCCEngine;
46use radixdb_storage::mvcc::transaction::DdlFenceGuard;
47use radixdb_storage::traits::Transaction as StorageTransaction;
48
49use super::database::{DatabaseInnerHandle, FromValue};
50use super::params::Params;
51use super::rows::Rows;
52use super::statement::Statement;
53
54/// Transaction represents a database transaction
55///
56/// Provides ACID guarantees for a series of database operations.
57/// Must be explicitly committed or rolled back.
58pub struct Transaction {
59    executor: Executor,
60    database_inner: Option<Arc<DatabaseInnerHandle>>,
61    /// Keeps one catalog generation stable for a logical export. Ordinary
62    /// transactions leave this empty and acquire statement fences normally.
63    _logical_export_fence: Option<DdlFenceGuard>,
64    id: i64,
65    committed: bool,
66    rolled_back: bool,
67}
68
69impl Transaction {
70    #[doc(hidden)]
71    pub fn describe_query_output(
72        &self,
73        sql: &str,
74    ) -> Result<Option<Vec<radixdb_executor::QueryOutputColumn>>> {
75        self.check_active()?;
76        self.executor.describe_query_output(sql)
77    }
78
79    /// Create a new transaction wrapper
80    pub(crate) fn new(
81        tx: Box<dyn StorageTransaction>,
82        database_inner: Arc<DatabaseInnerHandle>,
83    ) -> Self {
84        let executor = database_inner.transaction_executor();
85        executor.install_transaction(tx);
86        let id = executor
87            .active_transaction_id()
88            .expect("installed transaction must expose its identity");
89        Self {
90            executor,
91            database_inner: Some(database_inner),
92            _logical_export_fence: None,
93            id,
94            committed: false,
95            rolled_back: false,
96        }
97    }
98
99    pub(crate) fn new_logical_export(
100        tx: Box<dyn StorageTransaction>,
101        engine: Arc<MVCCEngine>,
102        plugin_registry: Arc<radixdb_executor::PluginRegistry>,
103        database_inner: Arc<DatabaseInnerHandle>,
104        fence: DdlFenceGuard,
105    ) -> Self {
106        let executor = Executor::new_with_owned_ddl_fence(engine, plugin_registry);
107        executor.install_transaction(tx);
108        let id = executor
109            .active_transaction_id()
110            .expect("installed logical export transaction must expose its identity");
111        Self {
112            executor,
113            database_inner: Some(database_inner),
114            _logical_export_fence: Some(fence),
115            id,
116            committed: false,
117            rolled_back: false,
118        }
119    }
120
121    #[doc(hidden)]
122    pub fn visit_logical_export_rows(
123        &mut self,
124        table_name: &str,
125        visitor: &mut dyn FnMut(i64, radixdb_core::Row) -> Result<()>,
126    ) -> Result<()> {
127        self.check_active()?;
128        self.executor.visit_logical_export_rows(table_name, visitor)
129    }
130
131    /// Check if the transaction is still active
132    pub(crate) fn check_active(&self) -> Result<()> {
133        if self.committed {
134            return Err(Error::TransactionEnded);
135        }
136        if self.rolled_back {
137            return Err(Error::TransactionEnded);
138        }
139        if !self.executor.has_active_transaction() {
140            return Err(Error::TransactionNotStarted);
141        }
142        Ok(())
143    }
144
145    pub(crate) fn executor(&self) -> &Executor {
146        &self.executor
147    }
148
149    /// Get the transaction ID
150    pub fn id(&self) -> i64 {
151        self.id
152    }
153
154    /// Execute a SQL statement within the transaction
155    ///
156    /// # Parameters
157    ///
158    /// Parameters can be passed using:
159    /// - Empty tuple `()` for no parameters
160    /// - Tuple syntax `(1, "Alice", 30)` for multiple parameters
161    /// - `params!` macro `params![1, "Alice", 30]`
162    ///
163    /// # Examples
164    ///
165    /// ```ignore
166    /// let mut tx = db.begin()?;
167    /// tx.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))?;
168    /// tx.execute("UPDATE accounts SET balance = balance - $1 WHERE user_id = $2", (100, 1))?;
169    /// tx.commit()?;
170    /// ```
171    pub fn execute<P: Params>(&mut self, sql: &str, params: P) -> Result<i64> {
172        self.check_active()?;
173
174        let param_values = params.into_params();
175        let result = self.execute_sql(sql, param_values)?;
176        Ok(result.rows_affected())
177    }
178
179    /// Execute a statement in this transaction with a cancellation deadline.
180    pub fn execute_with_timeout<P: Params>(
181        &mut self,
182        sql: &str,
183        params: P,
184        timeout_ms: u64,
185    ) -> Result<i64> {
186        self.check_active()?;
187        let ctx = ExecutionContextBuilder::new()
188            .params(params.into_params())
189            .timeout_ms(timeout_ms)
190            .build();
191        let result = self.execute_sql_with_ctx(sql, ctx)?;
192        Ok(result.rows_affected())
193    }
194
195    /// Execute a high-level prepared statement with parameters.
196    ///
197    /// Avoids re-parsing SQL on every call — ideal for batch operations
198    /// where the same statement is executed many times with different params.
199    ///
200    pub fn execute_prepared<P: Params>(&mut self, statement: &Statement, params: P) -> Result<i64> {
201        self.check_active()?;
202        let ctx = ExecutionContext::with_params(params.into_params());
203        let result = self.execute_prepared_statement(statement, ctx)?;
204        Ok(result.rows_affected())
205    }
206
207    /// Query using a pre-parsed statement with parameters.
208    ///
209    /// Avoids re-parsing SQL on every call — ideal for batch read operations
210    /// where the same query is executed many times with different params.
211    pub fn query_prepared<P: Params>(&mut self, statement: &Statement, params: P) -> Result<Rows> {
212        self.check_active()?;
213        let ctx = ExecutionContext::with_params(params.into_params());
214        let result = self.execute_prepared_statement(statement, ctx)?;
215        Ok(Rows::new(result))
216    }
217
218    /// Execute a prepared statement for a network session.
219    #[doc(hidden)]
220    pub fn query_prepared_for_server(
221        &mut self,
222        statement: &Statement,
223        context: super::ServerExecutionContext,
224    ) -> Result<Rows> {
225        self.check_active()?;
226        let result = self.execute_prepared_statement(statement, context.into_inner())?;
227        Ok(Rows::new(result))
228    }
229
230    /// Execute a query within the transaction
231    ///
232    /// # Examples
233    ///
234    /// ```ignore
235    /// let mut tx = db.begin()?;
236    /// for row in tx.query("SELECT * FROM users WHERE age > $1", (18,))? {
237    ///     let row = row?;
238    ///     println!("{}", row.get::<String>("name")?);
239    /// }
240    /// tx.commit()?;
241    /// ```
242    pub fn query<P: Params>(&mut self, sql: &str, params: P) -> Result<Rows> {
243        self.check_active()?;
244
245        let param_values = params.into_params();
246        let result = self.execute_sql(sql, param_values)?;
247        Ok(Rows::new(result))
248    }
249
250    /// Query in this transaction with a cancellation deadline.
251    pub fn query_with_timeout<P: Params>(
252        &mut self,
253        sql: &str,
254        params: P,
255        timeout_ms: u64,
256    ) -> Result<Rows> {
257        self.check_active()?;
258        let ctx = ExecutionContextBuilder::new()
259            .params(params.into_params())
260            .timeout_ms(timeout_ms)
261            .build();
262        let result = self.execute_sql_with_ctx(sql, ctx)?;
263        Ok(Rows::new(result))
264    }
265
266    /// Execute a query and return a single value
267    ///
268    /// # Examples
269    ///
270    /// ```ignore
271    /// let mut tx = db.begin()?;
272    /// let count: i64 = tx.query_one("SELECT COUNT(*) FROM users", ())?;
273    /// tx.commit()?;
274    /// ```
275    pub fn query_one<T: FromValue, P: Params>(&mut self, sql: &str, params: P) -> Result<T> {
276        let row = self
277            .query(sql, params)?
278            .next()
279            .ok_or(Error::NoRowsReturned)??;
280        row.get(0)
281    }
282
283    /// Execute a query and return an optional single value
284    ///
285    /// # Examples
286    ///
287    /// ```ignore
288    /// let mut tx = db.begin()?;
289    /// let name: Option<String> = tx.query_opt("SELECT name FROM users WHERE id = $1", (999,))?;
290    /// tx.commit()?;
291    /// ```
292    pub fn query_opt<T: FromValue, P: Params>(
293        &mut self,
294        sql: &str,
295        params: P,
296    ) -> Result<Option<T>> {
297        match self.query(sql, params)?.next() {
298            Some(row) => Ok(Some(row?.get(0)?)),
299            None => Ok(None),
300        }
301    }
302
303    /// Execute a SQL statement with named parameters within the transaction
304    ///
305    /// # Examples
306    ///
307    /// ```ignore
308    /// use radixdb::named_params;
309    ///
310    /// let mut tx = db.begin()?;
311    /// tx.execute_named(
312    ///     "INSERT INTO users VALUES (:id, :name)",
313    ///     named_params!{ id: 1, name: "Alice" }
314    /// )?;
315    /// tx.commit()?;
316    /// ```
317    pub fn execute_named(&mut self, sql: &str, params: NamedParams) -> Result<i64> {
318        self.check_active()?;
319        let ctx = ExecutionContext::with_named_params(params.into_inner());
320        let result = self.execute_sql_with_ctx(sql, ctx)?;
321        Ok(result.rows_affected())
322    }
323
324    /// Execute a query with named parameters within the transaction
325    pub fn query_named(&mut self, sql: &str, params: NamedParams) -> Result<Rows> {
326        self.check_active()?;
327        let ctx = ExecutionContext::with_named_params(params.into_inner());
328        let result = self.execute_sql_with_ctx(sql, ctx)?;
329        Ok(Rows::new(result))
330    }
331
332    /// Execute SQL for a network session.
333    #[doc(hidden)]
334    pub fn query_for_server(
335        &mut self,
336        sql: &str,
337        context: super::ServerExecutionContext,
338    ) -> Result<Rows> {
339        self.check_active()?;
340        let result = self.execute_sql_with_ctx(sql, context.into_inner())?;
341        Ok(Rows::new(result))
342    }
343
344    /// Execute a pre-parsed statement with named parameters.
345    ///
346    /// Combines `execute_prepared` (skip parsing) with `execute_named` (named params).
347    pub fn execute_prepared_named(
348        &mut self,
349        statement: &Statement,
350        params: NamedParams,
351    ) -> Result<i64> {
352        self.check_active()?;
353        let ctx = ExecutionContext::with_named_params(params.into_inner());
354        let result = self.execute_prepared_statement(statement, ctx)?;
355        Ok(result.rows_affected())
356    }
357
358    /// Query using a pre-parsed statement with named parameters.
359    ///
360    /// Combines `query_prepared` (skip parsing) with `query_named` (named params).
361    pub fn query_prepared_named(
362        &mut self,
363        statement: &Statement,
364        params: NamedParams,
365    ) -> Result<Rows> {
366        self.check_active()?;
367        let ctx = ExecutionContext::with_named_params(params.into_inner());
368        let result = self.execute_prepared_statement(statement, ctx)?;
369        Ok(Rows::new(result))
370    }
371
372    /// Internal SQL execution
373    fn execute_sql(&mut self, sql: &str, params: ParamVec) -> Result<ExecutionResult> {
374        let ctx = if params.is_empty() {
375            ExecutionContext::new()
376        } else {
377            ExecutionContext::with_params(params)
378        };
379        self.execute_sql_with_ctx(sql, ctx)
380    }
381
382    /// Internal SQL execution with a pre-built execution context
383    fn execute_sql_with_ctx(
384        &mut self,
385        sql: &str,
386        ctx: ExecutionContext,
387    ) -> Result<ExecutionResult> {
388        self.executor.execute_installed_transaction_sql(sql, &ctx)
389    }
390
391    fn execute_prepared_statement(
392        &mut self,
393        statement: &Statement,
394        ctx: ExecutionContext,
395    ) -> Result<ExecutionResult> {
396        statement.validate_owner(
397            self.database_inner
398                .as_ref()
399                .ok_or(Error::TransactionEnded)?,
400        )?;
401        self.executor.execute_installed_transaction_prepared(
402            statement.prepared_program(),
403            &ctx,
404            statement.sql(),
405        )
406    }
407
408    /// Commit the transaction
409    ///
410    /// All changes made within the transaction become permanent.
411    pub fn commit(&mut self) -> Result<()> {
412        self.check_active()?;
413
414        match self.executor.commit_installed_transaction() {
415            Ok(()) => {
416                self.committed = true;
417                self.database_inner.take();
418            }
419            Err(error) => {
420                if !self.executor.has_active_transaction() {
421                    self.rolled_back = true;
422                    self.database_inner.take();
423                }
424                return Err(error);
425            }
426        }
427
428        Ok(())
429    }
430
431    /// Roll back the transaction
432    ///
433    /// All changes made within the transaction are discarded.
434    pub fn rollback(&mut self) -> Result<()> {
435        if self.committed {
436            return Err(Error::TransactionCommitted);
437        }
438
439        if self.rolled_back {
440            return Ok(()); // Already rolled back
441        }
442
443        let result = self.executor.rollback_installed_transaction();
444        // The executor takes the storage handle before cleanup. Success or
445        // failure is therefore terminal for this public handle.
446        self.rolled_back = true;
447        self.database_inner.take();
448        result
449    }
450
451    /// True when the public handle still represents an active storage
452    /// transaction and can be explicitly committed or rolled back.
453    pub fn is_active(&self) -> bool {
454        !self.committed && !self.rolled_back && self.executor.has_active_transaction()
455    }
456
457    /// Create or replace a transaction savepoint.
458    ///
459    /// Names passed through the Rust API are exact strings. SQL identifier
460    /// folding is applied by the SQL executor before it reaches this facade.
461    pub fn savepoint(&mut self, name: &str) -> Result<()> {
462        self.check_active()?;
463        self.executor.create_active_savepoint(name)
464    }
465
466    /// Roll back all changes made after `name` while retaining the target
467    /// savepoint, so it can be used again or explicitly released.
468    pub fn rollback_to_savepoint(&mut self, name: &str) -> Result<()> {
469        self.check_active()?;
470        self.executor.rollback_active_to_savepoint(name)
471    }
472
473    /// Release a savepoint without rolling back its changes.
474    pub fn release_savepoint(&mut self, name: &str) -> Result<()> {
475        self.check_active()?;
476        self.executor.release_active_savepoint(name)
477    }
478}
479
480impl Drop for Transaction {
481    fn drop(&mut self) {
482        // Auto-rollback if not committed
483        if !self.committed && !self.rolled_back {
484            let _ = self.rollback();
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use crate::Database;
492
493    #[test]
494    fn r3_l02_batch_b_explicit_transaction_retains_one_executor_across_statements() {
495        let db = Database::open_in_memory().expect("open database");
496        db.execute(
497            "CREATE TABLE batch_b_executor (id INTEGER PRIMARY KEY, value INTEGER)",
498            (),
499        )
500        .expect("create table");
501        let before = radixdb_executor::test_executor_construction_count();
502
503        let mut transaction = db.begin().expect("begin transaction");
504        transaction
505            .execute("INSERT INTO batch_b_executor VALUES (1, 10)", ())
506            .expect("first statement");
507        transaction
508            .execute("INSERT INTO batch_b_executor VALUES (2, 20)", ())
509            .expect("second statement");
510
511        assert_eq!(
512            radixdb_executor::test_executor_construction_count() - before,
513            1,
514            "an explicit transaction must construct one retained Executor, not one per statement"
515        );
516        transaction.rollback().expect("rollback transaction");
517    }
518
519    #[test]
520    fn test_transaction_commit() {
521        let db = Database::open_in_memory().unwrap();
522        db.execute(
523            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
524            (),
525        )
526        .unwrap();
527        db.execute("INSERT INTO test VALUES ($1, $2)", (1, 100))
528            .unwrap();
529
530        // Verify data exists
531        let value: i64 = db
532            .query_one("SELECT value FROM test WHERE id = $1", (1,))
533            .unwrap();
534        assert_eq!(value, 100);
535    }
536
537    #[test]
538    fn test_transaction_rollback() {
539        let db = Database::open_in_memory().unwrap();
540        db.execute(
541            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
542            (),
543        )
544        .unwrap();
545        db.execute("INSERT INTO test VALUES ($1, $2)", (1, 100))
546            .unwrap();
547
548        let mut tx = db.begin().unwrap();
549        tx.execute("UPDATE test SET value = $1 WHERE id = $2", (200, 1))
550            .unwrap();
551        tx.rollback().unwrap();
552
553        let value: i64 = db
554            .query_one("SELECT value FROM test WHERE id = $1", (1,))
555            .unwrap();
556        assert_eq!(value, 100);
557    }
558
559    #[test]
560    fn test_transaction_delete_uses_shared_executor_and_respects_outcome() {
561        let db = Database::open_in_memory().unwrap();
562        db.execute(
563            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
564            (),
565        )
566        .unwrap();
567        db.execute("INSERT INTO test VALUES (1, 10), (2, 20), (3, 30)", ())
568            .unwrap();
569
570        let mut rollback_tx = db.begin().unwrap();
571        assert_eq!(
572            rollback_tx
573                .execute("DELETE FROM test WHERE id = $1", (2,))
574                .unwrap(),
575            1
576        );
577        let visible_inside: i64 = rollback_tx
578            .query_one("SELECT COUNT(*) FROM test", ())
579            .unwrap();
580        assert_eq!(visible_inside, 2);
581        rollback_tx.rollback().unwrap();
582
583        let visible_after_rollback: i64 = db.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
584        assert_eq!(visible_after_rollback, 3);
585
586        let mut commit_tx = db.begin().unwrap();
587        assert_eq!(
588            commit_tx
589                .execute("DELETE FROM test WHERE id = $1", (2,))
590                .unwrap(),
591            1
592        );
593        commit_tx.commit().unwrap();
594
595        let visible_after_commit: i64 = db
596            .query_one("SELECT COUNT(*) FROM test WHERE id = 2", ())
597            .unwrap();
598        assert_eq!(visible_after_commit, 0);
599    }
600
601    #[test]
602    fn test_transaction_auto_rollback() {
603        let db = Database::open_in_memory().unwrap();
604        db.execute(
605            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
606            (),
607        )
608        .unwrap();
609        db.execute("INSERT INTO test VALUES ($1, $2)", (1, 100))
610            .unwrap();
611
612        {
613            let mut tx = db.begin().unwrap();
614            tx.execute("UPDATE test SET value = $1 WHERE id = $2", (200, 1))
615                .unwrap();
616            // tx dropped without commit - should auto-rollback
617        }
618
619        let value: i64 = db
620            .query_one("SELECT value FROM test WHERE id = $1", (1,))
621            .unwrap();
622        assert_eq!(value, 100);
623    }
624
625    #[test]
626    fn test_transaction_query() {
627        let db = Database::open_in_memory().unwrap();
628        db.execute(
629            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
630            (),
631        )
632        .unwrap();
633        db.execute("INSERT INTO test VALUES ($1, $2)", (1, 100))
634            .unwrap();
635
636        let mut tx = db.begin().unwrap();
637
638        // New API: query with params
639        for row in tx.query("SELECT * FROM test", ()).unwrap() {
640            let row = row.unwrap();
641            assert_eq!(row.get::<i64>(0).unwrap(), 1);
642            assert_eq!(row.get::<i64>(1).unwrap(), 100);
643        }
644
645        tx.commit().unwrap();
646    }
647
648    #[test]
649    fn test_transaction_query_one() {
650        let db = Database::open_in_memory().unwrap();
651        db.execute(
652            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
653            (),
654        )
655        .unwrap();
656        db.execute("INSERT INTO test VALUES ($1, $2)", (1, 100))
657            .unwrap();
658
659        let mut tx = db.begin().unwrap();
660        let value: i64 = tx
661            .query_one("SELECT value FROM test WHERE id = $1", (1,))
662            .unwrap();
663        assert_eq!(value, 100);
664        tx.commit().unwrap();
665    }
666
667    #[test]
668    fn test_committed_transaction_error() {
669        let db = Database::open_in_memory().unwrap();
670        db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)", ())
671            .unwrap();
672
673        let mut tx = db.begin().unwrap();
674        tx.commit().unwrap();
675
676        // Should error on further operations
677        assert!(tx.execute("INSERT INTO test VALUES ($1)", (1,)).is_err());
678        assert!(tx.commit().is_err());
679    }
680
681    #[test]
682    fn test_transaction_id() {
683        let db = Database::open_in_memory().unwrap();
684        let tx = db.begin().unwrap();
685        assert!(tx.id() > 0);
686    }
687
688    #[test]
689    fn test_execute_prepared_insert() {
690        let db = Database::open_in_memory().unwrap();
691        db.execute(
692            "CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, value FLOAT)",
693            (),
694        )
695        .unwrap();
696
697        let stmt = db.prepare("INSERT INTO test VALUES ($1, $2, $3)").unwrap();
698
699        // Execute multiple times with different params
700        let mut tx = db.begin().unwrap();
701        tx.execute_prepared(&stmt, (1, "Alice", 10.5)).unwrap();
702        tx.execute_prepared(&stmt, (2, "Bob", 20.0)).unwrap();
703        tx.execute_prepared(&stmt, (3, "Charlie", 30.0)).unwrap();
704        tx.commit().unwrap();
705
706        let count: i64 = db.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
707        assert_eq!(count, 3);
708
709        let name: String = db
710            .query_one("SELECT name FROM test WHERE id = $1", (2,))
711            .unwrap();
712        assert_eq!(name, "Bob");
713    }
714
715    #[test]
716    fn test_execute_prepared_no_params() {
717        let db = Database::open_in_memory().unwrap();
718        db.execute(
719            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER DEFAULT 0)",
720            (),
721        )
722        .unwrap();
723        db.execute("INSERT INTO test VALUES (1, 100)", ()).unwrap();
724
725        let stmt = db.prepare("UPDATE test SET value = 999").unwrap();
726
727        let mut tx = db.begin().unwrap();
728        let affected = tx.execute_prepared(&stmt, ()).unwrap();
729        assert_eq!(affected, 1);
730        tx.commit().unwrap();
731
732        let value: i64 = db
733            .query_one("SELECT value FROM test WHERE id = 1", ())
734            .unwrap();
735        assert_eq!(value, 999);
736    }
737
738    #[test]
739    fn test_execute_prepared_on_committed_tx_errors() {
740        let db = Database::open_in_memory().unwrap();
741        db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)", ())
742            .unwrap();
743
744        let stmt = db.prepare("INSERT INTO test VALUES ($1)").unwrap();
745
746        let mut tx = db.begin().unwrap();
747        tx.commit().unwrap();
748        assert!(tx.execute_prepared(&stmt, (1,)).is_err());
749    }
750
751    #[test]
752    fn test_transaction_aggregate_count() {
753        let db = Database::open_in_memory().unwrap();
754        db.execute(
755            "CREATE TABLE items (id INTEGER PRIMARY KEY, category TEXT, price FLOAT)",
756            (),
757        )
758        .unwrap();
759        db.execute("INSERT INTO items VALUES (1, 'A', 10.0)", ())
760            .unwrap();
761        db.execute("INSERT INTO items VALUES (2, 'B', 20.0)", ())
762            .unwrap();
763        db.execute("INSERT INTO items VALUES (3, 'A', 30.0)", ())
764            .unwrap();
765
766        let mut tx = db.begin().unwrap();
767        let count: i64 = tx.query_one("SELECT COUNT(*) FROM items", ()).unwrap();
768        assert_eq!(count, 3);
769
770        let sum: f64 = tx.query_one("SELECT SUM(price) FROM items", ()).unwrap();
771        assert!((sum - 60.0).abs() < f64::EPSILON);
772
773        let avg: f64 = tx.query_one("SELECT AVG(price) FROM items", ()).unwrap();
774        assert!((avg - 20.0).abs() < f64::EPSILON);
775        tx.commit().unwrap();
776    }
777
778    #[test]
779    fn test_transaction_group_by() {
780        let db = Database::open_in_memory().unwrap();
781        db.execute(
782            "CREATE TABLE sales (id INTEGER PRIMARY KEY, category TEXT, amount INTEGER)",
783            (),
784        )
785        .unwrap();
786        db.execute("INSERT INTO sales VALUES (1, 'A', 10)", ())
787            .unwrap();
788        db.execute("INSERT INTO sales VALUES (2, 'B', 20)", ())
789            .unwrap();
790        db.execute("INSERT INTO sales VALUES (3, 'A', 30)", ())
791            .unwrap();
792
793        let mut tx = db.begin().unwrap();
794        let rows: Vec<_> = tx
795            .query(
796                "SELECT category, SUM(amount) as total FROM sales GROUP BY category ORDER BY category",
797                (),
798            )
799            .unwrap()
800            .collect::<std::result::Result<Vec<_>, _>>()
801            .unwrap();
802        assert_eq!(rows.len(), 2);
803        assert_eq!(rows[0].get::<String>(0).unwrap(), "A");
804        assert_eq!(rows[0].get::<i64>(1).unwrap(), 40);
805        assert_eq!(rows[1].get::<String>(0).unwrap(), "B");
806        assert_eq!(rows[1].get::<i64>(1).unwrap(), 20);
807        tx.commit().unwrap();
808    }
809
810    #[test]
811    fn test_transaction_select_after_insert() {
812        let db = Database::open_in_memory().unwrap();
813        db.execute(
814            "CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)",
815            (),
816        )
817        .unwrap();
818
819        let mut tx = db.begin().unwrap();
820        tx.execute("INSERT INTO test VALUES (1, 100)", ()).unwrap();
821        tx.execute("INSERT INTO test VALUES (2, 200)", ()).unwrap();
822
823        // Should see uncommitted inserts within the same transaction
824        let count: i64 = tx.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
825        assert_eq!(count, 2);
826
827        let sum: i64 = tx.query_one("SELECT SUM(value) FROM test", ()).unwrap();
828        assert_eq!(sum, 300);
829
830        // Can still do more DML after SELECT delegation
831        tx.execute("INSERT INTO test VALUES (3, 300)", ()).unwrap();
832        let count2: i64 = tx.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
833        assert_eq!(count2, 3);
834
835        tx.commit().unwrap();
836
837        // Verify committed data
838        let final_count: i64 = db.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
839        assert_eq!(final_count, 3);
840    }
841}