1use 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
54pub struct Transaction {
59 executor: Executor,
60 database_inner: Option<Arc<DatabaseInnerHandle>>,
61 _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 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 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 pub fn id(&self) -> i64 {
151 self.id
152 }
153
154 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 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 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 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 #[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 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 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 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 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 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 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 #[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 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 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 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 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 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 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(()); }
442
443 let result = self.executor.rollback_installed_transaction();
444 self.rolled_back = true;
447 self.database_inner.take();
448 result
449 }
450
451 pub fn is_active(&self) -> bool {
454 !self.committed && !self.rolled_back && self.executor.has_active_transaction()
455 }
456
457 pub fn savepoint(&mut self, name: &str) -> Result<()> {
462 self.check_active()?;
463 self.executor.create_active_savepoint(name)
464 }
465
466 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 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 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 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 }
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 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 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 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 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 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 let final_count: i64 = db.query_one("SELECT COUNT(*) FROM test", ()).unwrap();
839 assert_eq!(final_count, 3);
840 }
841}