linkrs_query/executor/streaming/
transaction_scope.rs1use crate::executor::base::ExecutionResult;
18use linkrs_core::error::QueryError;
19
20pub use linkrs_core::types::TransactionId;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum CancelReason {
27 UserKill,
28 Deadline,
29 ClientDisconnect,
30 MemoryLimit,
31 WorkerFailure,
32 Shutdown,
33 TransactionConflict,
37 Internal(String),
38}
39
40impl std::fmt::Display for CancelReason {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Self::UserKill => write!(f, "killed by user"),
44 Self::Deadline => write!(f, "deadline exceeded"),
45 Self::ClientDisconnect => write!(f, "client disconnected"),
46 Self::MemoryLimit => write!(f, "memory limit exceeded"),
47 Self::WorkerFailure => write!(f, "worker failure"),
48 Self::Shutdown => write!(f, "system shutdown"),
49 Self::TransactionConflict => write!(f, "transaction conflict"),
50 Self::Internal(msg) => write!(f, "{}", msg),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
72pub enum TransactionScope {
73 ExplicitBorrowed {
74 transaction_id: TransactionId,
75 read_write: bool,
76 },
77 AutoCommitOwned {
78 transaction_id: TransactionId,
79 rollback_only: bool,
80 },
81 ReadOnlySnapshot {
82 transaction_id: TransactionId,
83 },
84 CommandScope,
85 None,
86}
87
88impl TransactionScope {
89 pub fn explicit(transaction_id: TransactionId, read_write: bool) -> Self {
90 Self::ExplicitBorrowed {
91 transaction_id,
92 read_write,
93 }
94 }
95
96 pub fn auto_commit(transaction_id: TransactionId) -> Self {
97 Self::AutoCommitOwned {
98 transaction_id,
99 rollback_only: false,
100 }
101 }
102
103 pub fn read_only(transaction_id: TransactionId) -> Self {
104 Self::ReadOnlySnapshot { transaction_id }
105 }
106
107 pub fn allows_write(&self) -> bool {
109 match self {
110 Self::ExplicitBorrowed { read_write, .. } => *read_write,
111 Self::AutoCommitOwned { rollback_only, .. } => !*rollback_only,
112 _ => false,
113 }
114 }
115
116 pub fn is_explicit(&self) -> bool {
118 matches!(self, Self::ExplicitBorrowed { .. })
119 }
120
121 pub fn is_auto_commit(&self) -> bool {
123 matches!(self, Self::AutoCommitOwned { .. })
124 }
125
126 pub fn produces_result(&self) -> bool {
128 !matches!(self, Self::CommandScope)
129 }
130
131 pub fn transaction_id(&self) -> Option<TransactionId> {
133 match self {
134 Self::ExplicitBorrowed { transaction_id, .. } => Some(*transaction_id),
135 Self::AutoCommitOwned { transaction_id, .. } => Some(*transaction_id),
136 Self::ReadOnlySnapshot { transaction_id } => Some(*transaction_id),
137 _ => None,
138 }
139 }
140
141 pub fn ensure_can_write(&self) -> Result<(), QueryError> {
144 if self.allows_write() {
145 Ok(())
146 } else {
147 Err(QueryError::execution(
148 "Current transaction scope does not allow writes".to_string(),
149 ))
150 }
151 }
152
153 pub fn mark_rollback_only(&mut self) {
155 if let Self::AutoCommitOwned {
156 ref mut rollback_only,
157 ..
158 } = self
159 {
160 *rollback_only = true;
161 }
162 }
163
164 pub fn is_rollback_only(&self) -> bool {
166 matches!(
167 self,
168 Self::AutoCommitOwned {
169 rollback_only: true,
170 ..
171 }
172 )
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum TransactionState {
181 None,
183 Active,
185 Committing,
187 Committed,
189 RollingBack,
191 RolledBack,
193 RollbackOnly,
195}
196
197impl TransactionState {
198 pub fn can_begin(&self) -> bool {
199 matches!(self, Self::None | Self::Committed | Self::RolledBack)
200 }
201
202 pub fn can_execute(&self) -> bool {
203 matches!(self, Self::Active)
204 }
205
206 pub fn can_commit(&self) -> bool {
207 matches!(self, Self::Active)
208 }
209
210 pub fn can_rollback(&self) -> bool {
211 matches!(self, Self::Active | Self::RollbackOnly)
212 }
213}
214
215#[derive(Debug, Default)]
228pub struct SessionTransactionController {
229 state: parking_lot::RwLock<SessionTransactionState>,
230}
231
232#[derive(Debug, Clone)]
234struct SessionTransactionState {
235 active_transaction: Option<TransactionId>,
236 read_write: bool,
237 auto_commit: bool,
238 state: TransactionState,
239 rollback_only: bool,
240}
241
242impl Default for SessionTransactionState {
243 fn default() -> Self {
244 Self {
245 active_transaction: None,
246 read_write: true,
247 auto_commit: true,
248 state: TransactionState::None,
249 rollback_only: false,
250 }
251 }
252}
253
254impl SessionTransactionController {
255 pub fn new() -> Self {
256 Self {
257 state: parking_lot::RwLock::new(SessionTransactionState::default()),
258 }
259 }
260
261 pub fn state(&self) -> TransactionState {
265 self.state.read().state
266 }
267
268 pub fn is_active(&self) -> bool {
270 let s = self.state.read();
271 s.active_transaction.is_some() && s.state == TransactionState::Active
272 }
273
274 pub fn auto_commit(&self) -> bool {
276 self.state.read().auto_commit
277 }
278
279 pub fn rollback_only(&self) -> bool {
281 self.state.read().rollback_only
282 }
283
284 pub fn current_transaction_id(&self) -> Option<TransactionId> {
286 self.state.read().active_transaction
287 }
288
289 pub fn begin_tracking(
296 &self,
297 transaction_id: TransactionId,
298 read_write: bool,
299 ) -> Result<(), QueryError> {
300 let mut state = self.state.write();
301 if !state.state.can_begin() {
302 return Err(QueryError::execution(format!(
303 "Cannot BEGIN in state {:?}",
304 state.state
305 )));
306 }
307 state.active_transaction = Some(transaction_id);
308 state.read_write = read_write;
309 state.state = TransactionState::Active;
310 state.rollback_only = false;
311 Ok(())
312 }
313
314 pub fn begin_commit(&self) -> Result<TransactionId, QueryError> {
320 let mut state = self.state.write();
321 if !state.state.can_commit() {
322 return Err(QueryError::execution(format!(
323 "Cannot COMMIT in state {:?}",
324 state.state
325 )));
326 }
327 let txn_id = state
328 .active_transaction
329 .ok_or_else(|| QueryError::execution("No active transaction to commit".to_string()))?;
330 state.state = TransactionState::Committing;
331 Ok(txn_id)
332 }
333
334 pub fn commit_finalize(&self) {
336 let mut state = self.state.write();
337 state.active_transaction = None;
338 state.state = TransactionState::Committed;
339 state.rollback_only = false;
340 }
341
342 pub fn fail_commit(&self) {
344 let mut state = self.state.write();
345 state.state = TransactionState::RollbackOnly;
346 state.rollback_only = true;
347 }
348
349 pub fn begin_rollback(&self) -> Result<TransactionId, QueryError> {
351 let mut state = self.state.write();
352 if !state.state.can_rollback() {
353 return Err(QueryError::execution(format!(
354 "Cannot ROLLBACK in state {:?}",
355 state.state
356 )));
357 }
358 let txn_id = state.active_transaction.ok_or_else(|| {
359 QueryError::execution("No active transaction to roll back".to_string())
360 })?;
361 state.state = TransactionState::RollingBack;
362 Ok(txn_id)
363 }
364
365 pub fn rollback_finalize(&self) {
367 let mut state = self.state.write();
368 state.active_transaction = None;
369 state.state = TransactionState::RolledBack;
370 state.rollback_only = false;
371 }
372
373 pub fn mark_rollback_only(&self) {
375 let mut state = self.state.write();
376 if state.state == TransactionState::Active {
377 state.state = TransactionState::RollbackOnly;
378 state.rollback_only = true;
379 }
380 }
381
382 pub fn set_auto_commit(&self, enabled: bool) {
384 self.state.write().auto_commit = enabled;
385 }
386
387 pub fn current_scope(&self) -> TransactionScope {
391 let state = self.state.read();
392 match state.active_transaction {
393 Some(txn_id) if state.state == TransactionState::Active => {
394 TransactionScope::ExplicitBorrowed {
395 transaction_id: txn_id,
396 read_write: state.read_write,
397 }
398 }
399 _ => TransactionScope::None,
400 }
401 }
402
403 pub fn command_scope() -> TransactionScope {
406 TransactionScope::CommandScope
407 }
408}
409
410#[derive(Debug, Clone)]
414pub struct TransactionCommandResult {
415 pub command: &'static str,
416 pub transaction_id: Option<TransactionId>,
417 pub message: String,
418}
419
420impl TransactionCommandResult {
421 pub fn begin(transaction_id: TransactionId) -> Self {
422 Self {
423 command: "BEGIN",
424 transaction_id: Some(transaction_id),
425 message: "Transaction started".to_string(),
426 }
427 }
428
429 pub fn commit(transaction_id: TransactionId) -> Self {
430 Self {
431 command: "COMMIT",
432 transaction_id: Some(transaction_id),
433 message: "Transaction committed".to_string(),
434 }
435 }
436
437 pub fn rollback(transaction_id: TransactionId) -> Self {
438 Self {
439 command: "ROLLBACK",
440 transaction_id: Some(transaction_id),
441 message: "Transaction rolled back".to_string(),
442 }
443 }
444
445 pub fn rollback_to(savepoint_name: String) -> Self {
446 Self {
447 command: "ROLLBACK TO SAVEPOINT",
448 transaction_id: None,
449 message: format!("Rolled back to savepoint '{}'", savepoint_name),
450 }
451 }
452
453 pub fn savepoint(savepoint_name: String) -> Self {
454 Self {
455 command: "SAVEPOINT",
456 transaction_id: None,
457 message: format!("Created savepoint '{}'", savepoint_name),
458 }
459 }
460
461 pub fn release_savepoint(savepoint_name: String) -> Self {
462 Self {
463 command: "RELEASE SAVEPOINT",
464 transaction_id: None,
465 message: format!("Released savepoint '{}'", savepoint_name),
466 }
467 }
468
469 pub fn into_execution_result(self) -> ExecutionResult {
471 let row = vec![
472 linkrs_core::Value::string(self.command),
473 linkrs_core::Value::string(self.message),
474 ];
475 let col_names = vec!["command".to_string(), "result".to_string()];
476 let dataset = linkrs_core::DataSet::from_rows(vec![row], col_names);
477 ExecutionResult::DataSet { data: dataset }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_scope_allows_write() {
487 let scope = TransactionScope::explicit(TransactionId(1), true);
488 assert!(scope.allows_write());
489 assert!(scope.is_explicit());
490 assert_eq!(scope.transaction_id(), Some(TransactionId(1)));
491 }
492
493 #[test]
494 fn test_read_only_scope_does_not_allow_write() {
495 let scope = TransactionScope::read_only(TransactionId(2));
496 assert!(!scope.allows_write());
497 assert!(!scope.is_explicit());
498 assert!(!scope.is_auto_commit());
499 }
500
501 #[test]
502 fn test_auto_commit_scope_allows_write() {
503 let scope = TransactionScope::auto_commit(TransactionId(3));
504 assert!(scope.allows_write());
505 assert!(scope.is_auto_commit());
506 }
507
508 #[test]
509 fn test_rollback_only_prevents_write() {
510 let mut scope = TransactionScope::auto_commit(TransactionId(4));
511 assert!(scope.allows_write());
512 scope.mark_rollback_only();
513 assert!(!scope.allows_write());
514 assert!(scope.is_rollback_only());
515 }
516
517 #[test]
518 fn test_none_scope() {
519 let scope = TransactionScope::None;
520 assert!(!scope.allows_write());
521 assert!(!scope.is_explicit());
522 assert!(!scope.is_auto_commit());
523 assert!(scope.transaction_id().is_none());
524 }
525
526 #[test]
527 fn test_command_scope() {
528 let scope = TransactionScope::CommandScope;
529 assert!(!scope.produces_result());
530 }
531
532 #[test]
533 fn test_ensure_can_write() {
534 let write_scope = TransactionScope::explicit(TransactionId(1), true);
535 assert!(write_scope.ensure_can_write().is_ok());
536
537 let read_scope = TransactionScope::read_only(TransactionId(2));
538 assert!(read_scope.ensure_can_write().is_err());
539 }
540
541 #[test]
542 fn test_session_controller_state() {
543 let ctrl = SessionTransactionController::new();
544 assert_eq!(ctrl.state(), TransactionState::None);
545 assert!(!ctrl.is_active());
546 assert!(ctrl.auto_commit());
547 }
548
549 #[test]
550 fn test_session_controller_tracking() {
551 let ctrl = SessionTransactionController::new();
552 assert!(ctrl.begin_tracking(TransactionId(1), true).is_ok());
553 assert_eq!(ctrl.state(), TransactionState::Active);
554 assert!(ctrl.is_active());
555 assert_eq!(ctrl.current_transaction_id(), Some(TransactionId(1)));
556
557 let txn_id = ctrl.begin_commit().unwrap();
558 assert_eq!(txn_id, TransactionId(1));
559 assert_eq!(ctrl.state(), TransactionState::Committing);
560 ctrl.commit_finalize();
561 assert_eq!(ctrl.state(), TransactionState::Committed);
562 assert!(!ctrl.is_active());
563 }
564
565 #[test]
566 fn test_session_controller_rollback() {
567 let ctrl = SessionTransactionController::new();
568 ctrl.begin_tracking(TransactionId(2), false).unwrap();
569 assert!(ctrl.is_active());
570
571 let txn_id = ctrl.begin_rollback().unwrap();
572 assert_eq!(txn_id, TransactionId(2));
573 ctrl.rollback_finalize();
574 assert_eq!(ctrl.state(), TransactionState::RolledBack);
575 }
576
577 #[test]
578 fn test_session_controller_cannot_double_begin() {
579 let ctrl = SessionTransactionController::new();
580 ctrl.begin_tracking(TransactionId(1), true).unwrap();
581 let result = ctrl.begin_tracking(TransactionId(2), true);
582 assert!(result.is_err());
583 }
584
585 #[test]
586 fn test_session_controller_rollback_only() {
587 let ctrl = SessionTransactionController::new();
588 ctrl.begin_tracking(TransactionId(1), true).unwrap();
589 ctrl.mark_rollback_only();
590 assert!(ctrl.rollback_only());
591 assert_eq!(ctrl.state(), TransactionState::RollbackOnly);
592
593 assert!(ctrl.begin_commit().is_err());
595 assert!(ctrl.begin_rollback().is_ok());
597 }
598
599 #[test]
600 fn test_current_scope() {
601 let ctrl = SessionTransactionController::new();
602 assert!(matches!(ctrl.current_scope(), TransactionScope::None));
603
604 ctrl.begin_tracking(TransactionId(42), true).unwrap();
605 let scope = ctrl.current_scope();
606 assert!(scope.is_explicit());
607 assert!(scope.allows_write());
608 assert_eq!(scope.transaction_id(), Some(TransactionId(42)));
609 }
610
611 #[test]
612 fn test_cancel_reason_display() {
613 assert_eq!(CancelReason::UserKill.to_string(), "killed by user");
614 assert_eq!(CancelReason::Deadline.to_string(), "deadline exceeded");
615 assert_eq!(CancelReason::Shutdown.to_string(), "system shutdown");
616 assert_eq!(
617 CancelReason::Internal("test error".to_string()).to_string(),
618 "test error"
619 );
620 }
621
622 #[test]
623 fn test_transaction_state_transitions() {
624 assert!(TransactionState::None.can_begin());
625 assert!(TransactionState::Committed.can_begin());
626 assert!(TransactionState::RolledBack.can_begin());
627 assert!(!TransactionState::Active.can_begin());
628
629 assert!(TransactionState::Active.can_execute());
630 assert!(!TransactionState::None.can_execute());
631 assert!(!TransactionState::Committed.can_execute());
632
633 assert!(TransactionState::Active.can_commit());
634 assert!(!TransactionState::None.can_commit());
635
636 assert!(TransactionState::Active.can_rollback());
637 assert!(TransactionState::RollbackOnly.can_rollback());
638 assert!(!TransactionState::None.can_rollback());
639 }
640}