1use crate::database::{Database, ExternalTriggerBridge, TableHandle};
31use crate::epoch::{Epoch, Snapshot};
32use crate::error::{MongrelError, Result};
33use crate::memtable::Value;
34use crate::rowid::RowId;
35use crate::wal::SharedWal;
36use mongreldb_types::hlc::HlcTimestamp;
37use parking_lot::{Condvar, Mutex as PlMutex};
38use std::sync::Arc;
39
40pub(crate) fn allocate_txn_id(allocator: &PlMutex<u64>) -> Result<u64> {
41 let mut next = allocator.lock();
42 let id = *next;
43 if id == crate::wal::SYSTEM_TXN_ID || id & u32::MAX as u64 == 0 {
44 return Err(MongrelError::Full(
45 "per-open transaction id namespace exhausted; reopen the database".into(),
46 ));
47 }
48 *next = id.checked_add(1).ok_or_else(|| {
49 MongrelError::Full(
50 "per-open transaction id namespace exhausted; reopen the database".into(),
51 )
52 })?;
53 Ok(id)
54}
55
56pub(crate) enum Staged {
58 Put(Vec<(u16, Value)>),
59 Delete(RowId),
60 Update {
64 row_id: RowId,
65 new_row: Vec<(u16, Value)>,
66 changed_columns: Vec<u16>,
67 },
68 Truncate,
69}
70
71#[derive(Debug, Clone)]
72pub struct OwnedRow {
73 pub columns: Vec<(u16, Value)>,
74}
75
76#[derive(Debug, Clone)]
77pub struct PutResult {
78 pub auto_inc: Option<i64>,
79 pub row: OwnedRow,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum UpsertActionKind {
84 Inserted,
85 Updated,
86 Unchanged,
87}
88
89#[derive(Debug, Clone)]
90pub enum UpsertAction {
91 DoNothing,
92 DoUpdate(Vec<(u16, Value)>),
93}
94
95#[derive(Debug, Clone)]
96pub struct UpsertResult {
97 pub action: UpsertActionKind,
98 pub row: OwnedRow,
99 pub auto_inc: Option<i64>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum AbortReason {
107 RolledBack,
109 Conflict(String),
112 Validation(String),
115 Cancelled(String),
117 Error(String),
119}
120
121#[derive(Debug, Clone)]
128pub enum TransactionState {
129 Active,
131 Preparing,
134 CommitCritical,
137 Committed(mongreldb_log::CommitReceipt),
139 Aborted(AbortReason),
141}
142
143#[derive(Debug, Clone)]
151pub struct TxnStateHandle {
152 inner: std::sync::Arc<PlMutex<TransactionState>>,
153}
154
155impl TxnStateHandle {
156 fn new() -> Self {
157 Self {
158 inner: std::sync::Arc::new(PlMutex::new(TransactionState::Active)),
159 }
160 }
161
162 pub fn state(&self) -> TransactionState {
164 self.inner.lock().clone()
165 }
166
167 fn transition(&self, next: TransactionState) -> bool {
170 let mut current = self.inner.lock();
171 let legal = matches!(
172 (&*current, &next),
173 (TransactionState::Active, TransactionState::Preparing)
174 | (TransactionState::Active, TransactionState::Aborted(_))
175 | (
176 TransactionState::Preparing,
177 TransactionState::CommitCritical
178 )
179 | (TransactionState::Preparing, TransactionState::Aborted(_))
180 | (TransactionState::Preparing, TransactionState::Committed(_))
183 | (
184 TransactionState::CommitCritical,
185 TransactionState::Committed(_)
186 )
187 );
188 if legal {
189 *current = next;
190 }
191 legal
192 }
193
194 pub(crate) fn begin_prepare(&self) -> bool {
195 self.transition(TransactionState::Preparing)
196 }
197
198 pub(crate) fn enter_commit_critical(&self) -> bool {
199 self.transition(TransactionState::CommitCritical)
200 }
201
202 pub(crate) fn committed(&self, receipt: mongreldb_log::CommitReceipt) -> bool {
203 self.transition(TransactionState::Committed(receipt))
204 }
205
206 pub fn abort(&self, reason: AbortReason) -> bool {
209 self.transition(TransactionState::Aborted(reason))
210 }
211}
212
213pub(crate) fn classify_commit_error(state: &TxnStateHandle, error: &MongrelError) {
216 let reason = match error {
217 MongrelError::Conflict(message) => AbortReason::Conflict(message.clone()),
218 MongrelError::SerializationFailure { message } => {
223 AbortReason::Conflict(format!("serialization failure: {message}"))
224 }
225 MongrelError::Cancelled => AbortReason::Cancelled("cancelled".into()),
226 MongrelError::DeadlineExceeded => AbortReason::Cancelled("deadline exceeded".into()),
227 MongrelError::CommitOutcomeUnknown { .. } | MongrelError::DurableCommit { .. } => return,
229 MongrelError::InvalidArgument(message)
230 | MongrelError::Schema(message)
231 | MongrelError::TriggerValidation(message) => AbortReason::Validation(message.clone()),
232 other => AbortReason::Error(other.to_string()),
233 };
234 state.abort(reason);
235}
236
237#[derive(Debug, Clone, Default)]
243pub struct ReadSet {
244 rows: std::collections::BTreeSet<(u64, u64)>,
245}
246
247impl ReadSet {
248 pub(crate) fn record_row(&mut self, table_id: u64, row_id: RowId) {
249 self.rows.insert((table_id, row_id.0));
250 }
251
252 pub fn rows(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
254 self.rows.iter().copied()
255 }
256
257 pub fn len(&self) -> usize {
258 self.rows.len()
259 }
260
261 pub fn is_empty(&self) -> bool {
262 self.rows.is_empty()
263 }
264}
265
266#[derive(Debug, Clone, Default)]
273pub struct PredicateSet {
274 tables: std::collections::BTreeSet<u64>,
275}
276
277impl PredicateSet {
278 pub(crate) fn record_table(&mut self, table_id: u64) {
279 self.tables.insert(table_id);
280 }
281
282 pub fn tables(&self) -> impl Iterator<Item = u64> + '_ {
284 self.tables.iter().copied()
285 }
286
287 pub fn len(&self) -> usize {
288 self.tables.len()
289 }
290
291 pub fn is_empty(&self) -> bool {
292 self.tables.is_empty()
293 }
294}
295
296pub(crate) fn ssi_validation_keys(
300 read_set: &ReadSet,
301 predicate_set: &PredicateSet,
302) -> Vec<WriteKey> {
303 let mut keys = Vec::with_capacity(read_set.len() + predicate_set.len());
304 for (table_id, row_id) in read_set.rows() {
305 keys.push(WriteKey::Row { table_id, row_id });
306 }
307 for table_id in predicate_set.tables() {
308 keys.push(WriteKey::Table { table_id });
309 }
310 keys
311}
312
313pub(crate) struct TxnCommitContext {
318 pub isolation: IsolationLevel,
320 pub read_ts: Option<HlcTimestamp>,
324 pub read_set: ReadSet,
326 pub predicate_set: PredicateSet,
328 pub state: Option<TxnStateHandle>,
330 pub idempotency: Option<IdempotencyRequest>,
332}
333
334impl TxnCommitContext {
335 pub(crate) fn internal() -> Self {
339 Self {
340 isolation: IsolationLevel::RepeatableRead,
341 read_ts: None,
342 read_set: ReadSet::default(),
343 predicate_set: PredicateSet::default(),
344 state: None,
345 idempotency: None,
346 }
347 }
348}
349
350pub struct Transaction<'db> {
357 db: &'db Database,
358 txn_id: u64,
359 allocation_error: Option<String>,
360 isolation: IsolationLevel,
361 read: Snapshot,
362 read_ts: Option<HlcTimestamp>,
363 read_set: ReadSet,
364 predicate_set: PredicateSet,
365 state: TxnStateHandle,
366 staging: Vec<(u64 , Staged)>,
367 external_states: Vec<(String, Vec<u8>)>,
368 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
369 principal: Option<crate::auth::Principal>,
370 principal_catalog_bound: bool,
371 external_trigger_bridge: Option<&'db dyn ExternalTriggerBridge>,
372 _active: Option<ActiveTxnGuard<'db>>,
373}
374
375impl<'db> Transaction<'db> {
376 pub(crate) fn new(
377 db: &'db Database,
378 txn_id: Result<u64>,
379 read: Snapshot,
380 isolation: IsolationLevel,
381 ) -> Self {
382 let guard = db.register_active(read.epoch);
383 let read_ts = db.hlc_clock().now().ok();
387 let (txn_id, allocation_error) = match txn_id {
388 Ok(txn_id) => (txn_id, None),
389 Err(MongrelError::Full(message)) => (crate::wal::SYSTEM_TXN_ID, Some(message)),
390 Err(error) => (crate::wal::SYSTEM_TXN_ID, Some(error.to_string())),
391 };
392 Self {
393 db,
394 txn_id,
395 allocation_error,
396 isolation,
397 read,
398 read_ts,
399 read_set: ReadSet::default(),
400 predicate_set: PredicateSet::default(),
401 state: TxnStateHandle::new(),
402 staging: Vec::new(),
403 external_states: Vec::new(),
404 materialized_view_updates: Vec::new(),
405 principal: None,
406 principal_catalog_bound: false,
407 external_trigger_bridge: None,
408 _active: Some(guard),
409 }
410 }
411
412 pub(crate) fn with_external_trigger_bridge(
413 mut self,
414 bridge: &'db dyn ExternalTriggerBridge,
415 ) -> Self {
416 self.external_trigger_bridge = Some(bridge);
417 self
418 }
419
420 pub(crate) fn with_principal(
421 mut self,
422 principal: Option<crate::auth::Principal>,
423 catalog_bound: bool,
424 ) -> Self {
425 self.principal = principal;
426 self.principal_catalog_bound = catalog_bound;
427 self
428 }
429
430 pub fn read_snapshot(&self) -> Snapshot {
431 self.read
432 }
433
434 pub fn txn_id(&self) -> u64 {
437 self.txn_id
438 }
439
440 pub fn isolation(&self) -> IsolationLevel {
442 self.isolation
443 }
444
445 pub fn read_ts(&self) -> Option<HlcTimestamp> {
448 self.read_ts
449 }
450
451 pub fn state(&self) -> TransactionState {
453 self.state.state()
454 }
455
456 pub fn state_handle(&self) -> TxnStateHandle {
459 self.state.clone()
460 }
461
462 pub fn read_set(&self) -> &ReadSet {
464 &self.read_set
465 }
466
467 pub fn predicate_set(&self) -> &PredicateSet {
469 &self.predicate_set
470 }
471
472 fn statement_snapshot(&self) -> Snapshot {
476 match self.isolation.canonical() {
477 IsolationLevel::ReadCommitted => Snapshot::at(self.db.visible_epoch()),
478 _ => self.read,
479 }
480 }
481
482 pub fn get(&mut self, table: &str, row_id: RowId) -> Result<Option<OwnedRow>> {
487 let snap = self.statement_snapshot();
488 let id = self.db.table_id(table)?;
489 let handle = self.db.table(table)?;
490 let row = handle.lock().get(row_id, snap);
491 Ok(row.map(|row| {
492 self.read_set.record_row(id, row_id);
493 owned_row_from_map(row.columns)
494 }))
495 }
496
497 pub fn track_predicate_read(&mut self, table: &str) -> Result<()> {
501 let id = self.db.table_id(table)?;
502 self.predicate_set.record_table(id);
503 Ok(())
504 }
505
506 fn lock_auto_inc_barrier(&self, table_id: u64, cells: &[(u16, Value)]) -> Result<()> {
521 if self.db.table_auto_inc_would_allocate(table_id, cells) {
522 self.db.acquire_txn_lock(
523 self.txn_id,
524 crate::locks::LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str()),
525 crate::locks::LockMode::Exclusive,
526 None,
527 )?;
528 }
529 Ok(())
530 }
531
532 pub fn put(&mut self, table: &str, mut cells: Vec<(u16, Value)>) -> Result<Option<i64>> {
533 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
534 let id = self.db.table_id(table)?;
535 self.lock_auto_inc_barrier(id, &cells)?;
536 let handle = self.db.table(table)?;
537 let mut t = handle.lock();
538 let assigned = t.fill_auto_inc(&mut cells)?;
539 t.apply_defaults(&mut cells)?;
540 drop(t);
541 self.staging.push((id, Staged::Put(cells)));
542 Ok(assigned)
543 }
544
545 #[doc(hidden)]
547 pub fn put_building(
548 &mut self,
549 table: &str,
550 mut cells: Vec<(u16, Value)>,
551 ) -> Result<Option<i64>> {
552 self.db
553 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
554 let id = self.db.building_table_id(table)?;
555 self.lock_auto_inc_barrier(id, &cells)?;
556 let handle = self.db.table_by_id(id)?;
557 let mut target = handle.lock();
558 let assigned = target.fill_auto_inc(&mut cells)?;
559 target.apply_defaults(&mut cells)?;
560 let primary_key_column = target
561 .schema()
562 .primary_key()
563 .map(|column| column.id)
564 .ok_or_else(|| MongrelError::Schema("CTAS build table has no primary key".into()))?;
565 let primary_key = cells
566 .iter()
567 .find(|(column, _)| *column == primary_key_column)
568 .map(|(_, value)| value)
569 .ok_or_else(|| MongrelError::InvalidArgument("CTAS primary key is missing".into()))?;
570 if matches!(primary_key, Value::Null) {
571 return Err(MongrelError::InvalidArgument(
572 "CTAS primary key cannot be NULL".into(),
573 ));
574 }
575 let primary_key = primary_key.encode_key();
576 let replacing = self
577 .staging
578 .iter()
579 .any(|(table_id, staged)| *table_id == id && matches!(staged, Staged::Truncate));
580 if !replacing && target.lookup_pk(&primary_key).is_some() {
581 return Err(MongrelError::InvalidArgument(
582 "duplicate CTAS primary key".into(),
583 ));
584 }
585 drop(target);
586 if self.staging.iter().any(|(staged_table, staged)| {
587 if *staged_table != id {
588 return false;
589 }
590 let Staged::Put(staged_cells) = staged else {
591 return false;
592 };
593 staged_cells
594 .iter()
595 .find(|(column, _)| *column == primary_key_column)
596 .is_some_and(|(_, value)| value.encode_key() == primary_key)
597 }) {
598 return Err(MongrelError::InvalidArgument(
599 "duplicate CTAS primary key".into(),
600 ));
601 }
602 self.staging.push((id, Staged::Put(cells)));
603 Ok(assigned)
604 }
605
606 #[doc(hidden)]
608 pub fn truncate_building(&mut self, table: &str) -> Result<()> {
609 self.db
610 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
611 let id = self.db.building_table_id(table)?;
612 if self.staging.iter().any(|(table_id, _)| *table_id == id) {
613 return Err(MongrelError::InvalidArgument(
614 "building-table truncate must be staged before replacement rows".into(),
615 ));
616 }
617 self.staging.push((id, Staged::Truncate));
618 Ok(())
619 }
620
621 pub fn put_returning(
622 &mut self,
623 table: &str,
624 mut cells: Vec<(u16, Value)>,
625 ) -> Result<PutResult> {
626 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
627 let id = self.db.table_id(table)?;
628 self.lock_auto_inc_barrier(id, &cells)?;
629 let handle = self.db.table(table)?;
630 let mut t = handle.lock();
631 let assigned = t.fill_auto_inc(&mut cells)?;
632 t.apply_defaults(&mut cells)?;
633 drop(t);
634 let row = owned_row_from_cells(&cells);
635 self.staging.push((id, Staged::Put(cells)));
636 Ok(PutResult {
637 auto_inc: assigned,
638 row,
639 })
640 }
641
642 #[doc(hidden)]
645 pub fn put_returning_bound(
646 &mut self,
647 table: &str,
648 expected_table_id: u64,
649 expected_schema_id: u64,
650 mut cells: Vec<(u16, Value)>,
651 ) -> Result<PutResult> {
652 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
653 self.lock_auto_inc_barrier(expected_table_id, &cells)?;
654 let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
655 let mut target = handle.lock();
656 let assigned = target.fill_auto_inc(&mut cells)?;
657 target.apply_defaults(&mut cells)?;
658 drop(target);
659 let row = owned_row_from_cells(&cells);
660 self.staging.push((expected_table_id, Staged::Put(cells)));
661 Ok(PutResult {
662 auto_inc: assigned,
663 row,
664 })
665 }
666
667 pub fn put_batch(
673 &mut self,
674 table: &str,
675 rows: Vec<Vec<(u16, Value)>>,
676 ) -> Result<Vec<Option<i64>>> {
677 if !rows.is_empty() {
678 let mut columns = rows
679 .iter()
680 .flat_map(|cells| cells.iter().map(|(column, _)| *column))
681 .collect::<Vec<_>>();
682 columns.sort_unstable();
683 columns.dedup();
684 self.db.require_columns_for(
685 table,
686 crate::auth::ColumnOperation::Insert,
687 &columns,
688 self.principal.as_ref(),
689 )?;
690 }
691 let id = self.db.table_id(table)?;
692 for cells in &rows {
693 self.lock_auto_inc_barrier(id, cells)?;
694 }
695 let handle = self.db.table(table)?;
696 let mut t = handle.lock();
697 let mut assigned = Vec::with_capacity(rows.len());
698 for mut cells in rows {
699 let a = t.fill_auto_inc(&mut cells)?;
700 t.apply_defaults(&mut cells)?;
701 assigned.push(a);
702 self.staging.push((id, Staged::Put(cells)));
703 }
704 drop(t);
705 Ok(assigned)
706 }
707
708 pub fn delete(&mut self, table: &str, row_id: RowId) -> Result<()> {
710 self.delete_batch(table, vec![row_id])
711 }
712
713 #[doc(hidden)]
716 pub fn delete_bound(
717 &mut self,
718 table: &str,
719 expected_table_id: u64,
720 expected_schema_id: u64,
721 row_id: RowId,
722 ) -> Result<()> {
723 self.require_delete(table)?;
724 self.bound_table(table, expected_table_id, expected_schema_id)?;
725 self.reject_after_truncate(expected_table_id)?;
726 self.staging
727 .push((expected_table_id, Staged::Delete(row_id)));
728 Ok(())
729 }
730
731 #[doc(hidden)]
734 pub fn delete_by_pk_bound(
735 &mut self,
736 table: &str,
737 expected_table_id: u64,
738 expected_schema_id: u64,
739 key: &Value,
740 ) -> Result<bool> {
741 self.require_delete(table)?;
742 let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
743 self.reject_after_truncate(expected_table_id)?;
744 let row_id = {
745 let mut target = handle.lock();
746 target.ensure_indexes_complete()?;
747 target.lookup_pk(&key.encode_key())
748 };
749 let Some(row_id) = row_id else {
750 return Ok(false);
751 };
752 self.read_set.record_row(expected_table_id, row_id);
753 self.staging
754 .push((expected_table_id, Staged::Delete(row_id)));
755 Ok(true)
756 }
757
758 pub fn delete_batch(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<()> {
760 self.require_delete(table)?;
761 let id = self.db.table_id(table)?;
762 self.reject_after_truncate(id)?;
763 self.staging.extend(
764 row_ids
765 .into_iter()
766 .map(|row_id| (id, Staged::Delete(row_id))),
767 );
768 Ok(())
769 }
770
771 pub fn put_external_state(&mut self, table: &str, state: Vec<u8>) -> Result<()> {
774 if self.db.external_table(table).is_none() {
775 return Err(MongrelError::NotFound(format!(
776 "external table {table:?} not found"
777 )));
778 }
779 self.external_states.push((table.to_string(), state));
780 Ok(())
781 }
782
783 pub fn set_materialized_view_definition(
787 &mut self,
788 definition: crate::catalog::MaterializedViewEntry,
789 ) -> Result<()> {
790 self.db
791 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
792 if self.db.table_id(&definition.name).is_err() {
793 return Err(MongrelError::NotFound(format!(
794 "materialized view table {:?} not found",
795 definition.name
796 )));
797 }
798 self.materialized_view_updates
799 .retain(|current| current.name != definition.name);
800 self.materialized_view_updates.push(definition);
801 Ok(())
802 }
803
804 pub fn delete_many(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<Vec<OwnedRow>> {
805 self.require_delete(table)?;
806 let id = self.db.table_id(table)?;
807 self.reject_after_truncate(id)?;
808 let snap = self.statement_snapshot();
809 let handle = self.db.table(table)?;
810 let t = handle.lock();
811 let mut pre_images = Vec::with_capacity(row_ids.len());
812 for row_id in &row_ids {
813 if let Some(row) = t.get(*row_id, snap) {
814 pre_images.push(owned_row_from_map(row.columns));
815 self.read_set.record_row(id, *row_id);
816 }
817 }
818 drop(t);
819 for row_id in row_ids {
820 self.staging.push((id, Staged::Delete(row_id)));
821 }
822 Ok(pre_images)
823 }
824
825 pub fn update_many(
826 &mut self,
827 table: &str,
828 updates: Vec<(RowId, Vec<(u16, Value)>)>,
829 ) -> Result<Vec<OwnedRow>> {
830 if !updates.is_empty() {
831 let mut columns = updates
832 .iter()
833 .flat_map(|(_, cells)| cells.iter().map(|(column, _)| *column))
834 .collect::<Vec<_>>();
835 columns.sort_unstable();
836 columns.dedup();
837 self.db.require_columns_for(
838 table,
839 crate::auth::ColumnOperation::Update,
840 &columns,
841 self.principal.as_ref(),
842 )?;
843 }
844 let id = self.db.table_id(table)?;
845 self.reject_after_truncate(id)?;
846 let snap = self.statement_snapshot();
847 let handle = self.db.table(table)?;
848 let t = handle.lock();
849 let mut post_images = Vec::with_capacity(updates.len());
850 let mut staged = Vec::with_capacity(updates.len());
851 for (old_id, new_cells) in updates {
852 let changed_columns = changed_columns(&new_cells);
853 let old_row = t
854 .get(old_id, snap)
855 .ok_or_else(|| MongrelError::NotFound(format!("row {old_id:?} not found")))?;
856 self.read_set.record_row(id, old_id);
857 let merged = merge_cells(old_row.columns.into_iter().collect(), new_cells);
858 post_images.push(owned_row_from_cells(&merged));
859 staged.push((
860 id,
861 Staged::Update {
862 row_id: old_id,
863 new_row: merged,
864 changed_columns,
865 },
866 ));
867 }
868 drop(t);
869 self.staging.extend(staged);
870 Ok(post_images)
871 }
872
873 pub fn upsert(
874 &mut self,
875 table: &str,
876 mut insert_cells: Vec<(u16, Value)>,
877 action: UpsertAction,
878 ) -> Result<UpsertResult> {
879 self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
883 let id = self.db.table_id(table)?;
884 self.lock_auto_inc_barrier(id, &insert_cells)?;
885 self.reject_after_truncate(id)?;
886 match (self.existing_pk_row(table, &insert_cells)?, action) {
887 (None, _) => {
888 let handle = self.db.table(table)?;
889 let mut t = handle.lock();
890 let assigned = t.fill_auto_inc(&mut insert_cells)?;
891 t.apply_defaults(&mut insert_cells)?;
892 drop(t);
893 let row = owned_row_from_cells(&insert_cells);
894 self.staging.push((id, Staged::Put(insert_cells)));
895 Ok(UpsertResult {
896 action: UpsertActionKind::Inserted,
897 row,
898 auto_inc: assigned,
899 })
900 }
901 (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
902 action: UpsertActionKind::Unchanged,
903 row: old_row,
904 auto_inc: None,
905 }),
906 (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
907 self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
909 let changed_columns = changed_columns(&update_cells);
910 let merged = merge_cells(old_row.columns.clone(), update_cells);
911 if columns_equal(&old_row.columns, &merged) {
912 return Ok(UpsertResult {
913 action: UpsertActionKind::Unchanged,
914 row: old_row,
915 auto_inc: None,
916 });
917 }
918 let row = owned_row_from_cells(&merged);
919 self.staging.push((
920 id,
921 Staged::Update {
922 row_id: old_id,
923 new_row: merged,
924 changed_columns,
925 },
926 ));
927 Ok(UpsertResult {
928 action: UpsertActionKind::Updated,
929 row,
930 auto_inc: None,
931 })
932 }
933 }
934 }
935
936 #[doc(hidden)]
939 pub fn upsert_bound(
940 &mut self,
941 table: &str,
942 expected_table_id: u64,
943 expected_schema_id: u64,
944 mut insert_cells: Vec<(u16, Value)>,
945 action: UpsertAction,
946 ) -> Result<UpsertResult> {
947 self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
948 self.lock_auto_inc_barrier(expected_table_id, &insert_cells)?;
949 let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
950 self.reject_after_truncate(expected_table_id)?;
951 match (
952 self.existing_pk_row_in(&handle, &insert_cells, expected_table_id)?,
953 action,
954 ) {
955 (None, _) => {
956 let mut target = handle.lock();
957 let assigned = target.fill_auto_inc(&mut insert_cells)?;
958 target.apply_defaults(&mut insert_cells)?;
959 drop(target);
960 let row = owned_row_from_cells(&insert_cells);
961 self.staging
962 .push((expected_table_id, Staged::Put(insert_cells)));
963 Ok(UpsertResult {
964 action: UpsertActionKind::Inserted,
965 row,
966 auto_inc: assigned,
967 })
968 }
969 (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
970 action: UpsertActionKind::Unchanged,
971 row: old_row,
972 auto_inc: None,
973 }),
974 (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
975 self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
976 let changed_columns = changed_columns(&update_cells);
977 let merged = merge_cells(old_row.columns.clone(), update_cells);
978 if columns_equal(&old_row.columns, &merged) {
979 return Ok(UpsertResult {
980 action: UpsertActionKind::Unchanged,
981 row: old_row,
982 auto_inc: None,
983 });
984 }
985 let row = owned_row_from_cells(&merged);
986 self.staging.push((
987 expected_table_id,
988 Staged::Update {
989 row_id: old_id,
990 new_row: merged,
991 changed_columns,
992 },
993 ));
994 Ok(UpsertResult {
995 action: UpsertActionKind::Updated,
996 row,
997 auto_inc: None,
998 })
999 }
1000 }
1001 }
1002
1003 pub fn truncate(&mut self, table: &str) -> Result<()> {
1004 self.db
1005 .require_for(self.principal.as_ref(), &crate::auth::Permission::Admin)?;
1006 let id = self.db.table_id(table)?;
1007 for (table_id, op) in &self.staging {
1008 if *table_id == id && !matches!(op, Staged::Truncate) {
1009 return Err(MongrelError::InvalidArgument(
1010 "truncate cannot be combined with other writes on the same table".into(),
1011 ));
1012 }
1013 }
1014 self.staging.push((id, Staged::Truncate));
1015 Ok(())
1016 }
1017
1018 fn reject_after_truncate(&self, table_id: u64) -> Result<()> {
1019 if self
1020 .staging
1021 .iter()
1022 .any(|(tid, op)| *tid == table_id && matches!(op, Staged::Truncate))
1023 {
1024 return Err(MongrelError::InvalidArgument(
1025 "truncate cannot be combined with other writes on the same table".into(),
1026 ));
1027 }
1028 Ok(())
1029 }
1030
1031 fn require_columns(
1032 &self,
1033 table: &str,
1034 operation: crate::auth::ColumnOperation,
1035 cells: &[(u16, Value)],
1036 ) -> Result<()> {
1037 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
1038 self.db
1039 .require_columns_for(table, operation, &columns, self.principal.as_ref())
1040 }
1041
1042 fn require_delete(&self, table: &str) -> Result<()> {
1043 self.db.require_for(
1044 self.principal.as_ref(),
1045 &crate::auth::Permission::Delete {
1046 table: table.to_string(),
1047 },
1048 )
1049 }
1050
1051 fn bound_table(
1052 &self,
1053 table: &str,
1054 expected_table_id: u64,
1055 expected_schema_id: u64,
1056 ) -> Result<TableHandle> {
1057 let current = self.db.table_identity(table)?;
1058 if current != (expected_table_id, expected_schema_id) {
1059 return Err(MongrelError::Conflict(format!(
1060 "table {table:?} changed after request authorization"
1061 )));
1062 }
1063 self.db.table_by_id(expected_table_id)
1064 }
1065
1066 fn existing_pk_row(
1067 &mut self,
1068 table: &str,
1069 cells: &[(u16, Value)],
1070 ) -> Result<Option<(RowId, OwnedRow)>> {
1071 let id = self.db.table_id(table)?;
1072 let handle = self.db.table(table)?;
1073 self.existing_pk_row_in(&handle, cells, id)
1074 }
1075
1076 fn existing_pk_row_in(
1077 &mut self,
1078 handle: &TableHandle,
1079 cells: &[(u16, Value)],
1080 table_id: u64,
1081 ) -> Result<Option<(RowId, OwnedRow)>> {
1082 let snap = self.statement_snapshot();
1083 let target = handle.lock();
1084 let Some(pk_col) = target.schema().primary_key() else {
1085 return Ok(None);
1086 };
1087 let Some((_, pk_value)) = cells.iter().find(|(id, _)| *id == pk_col.id) else {
1088 return Ok(None);
1089 };
1090 if matches!(pk_value, Value::Null) {
1091 return Ok(None);
1092 }
1093 let Some(row_id) = target.lookup_pk(&pk_value.encode_key()) else {
1094 return Ok(None);
1095 };
1096 let found = target
1097 .get(row_id, snap)
1098 .map(|row| (row_id, owned_row_from_map(row.columns)));
1099 if found.is_some() {
1100 self.read_set.record_row(table_id, row_id);
1101 }
1102 Ok(found)
1103 }
1104
1105 pub fn commit(self) -> Result<Epoch> {
1107 self.commit_full(None, None, None).map(|(epoch, _)| epoch)
1108 }
1109
1110 pub fn commit_idempotent(self, request: IdempotencyRequest) -> Result<Epoch> {
1115 self.commit_full(None, None, Some(request))
1116 .map(|(epoch, _)| epoch)
1117 }
1118
1119 pub fn commit_with_row_ids(self) -> Result<(Epoch, Vec<RowId>)> {
1120 self.commit_full(None, None, None)
1121 }
1122
1123 pub fn commit_controlled<F>(
1127 self,
1128 control: &crate::ExecutionControl,
1129 mut before_commit: F,
1130 ) -> Result<Epoch>
1131 where
1132 F: FnMut() -> Result<()>,
1133 {
1134 self.commit_full(Some(control), Some(&mut before_commit), None)
1135 .map(|(epoch, _)| epoch)
1136 }
1137
1138 pub fn commit_controlled_with_row_ids<F>(
1139 self,
1140 control: &crate::ExecutionControl,
1141 mut before_commit: F,
1142 ) -> Result<(Epoch, Vec<RowId>)>
1143 where
1144 F: FnMut() -> Result<()>,
1145 {
1146 self.commit_full(Some(control), Some(&mut before_commit), None)
1147 }
1148
1149 fn commit_full(
1156 mut self,
1157 control: Option<&crate::ExecutionControl>,
1158 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
1159 idempotency: Option<IdempotencyRequest>,
1160 ) -> Result<(Epoch, Vec<RowId>)> {
1161 if let Some(message) = self.allocation_error.take() {
1162 self.state.abort(AbortReason::Error(message.clone()));
1163 return Err(MongrelError::Full(message));
1164 }
1165 self.state.begin_prepare();
1166 let context = TxnCommitContext {
1167 isolation: self.isolation,
1168 read_ts: self.read_ts,
1169 read_set: std::mem::take(&mut self.read_set),
1170 predicate_set: std::mem::take(&mut self.predicate_set),
1171 state: Some(self.state.clone()),
1172 idempotency,
1173 };
1174 let staging = std::mem::take(&mut self.staging);
1175 let external_states = std::mem::take(&mut self.external_states);
1176 let materialized_view_updates = std::mem::take(&mut self.materialized_view_updates);
1177 let principal = self.principal.take();
1178 let result = match (control, before_commit) {
1179 (Some(control), Some(before_commit)) => {
1180 self.db.commit_transaction_with_external_states_controlled(
1181 self.txn_id,
1182 self.read.epoch,
1183 staging,
1184 external_states,
1185 materialized_view_updates,
1186 principal,
1187 self.principal_catalog_bound,
1188 self.external_trigger_bridge,
1189 context,
1190 control,
1191 before_commit,
1192 )
1193 }
1194 _ => self.db.commit_transaction_with_external_states(
1195 self.txn_id,
1196 self.read.epoch,
1197 staging,
1198 external_states,
1199 materialized_view_updates,
1200 principal,
1201 self.principal_catalog_bound,
1202 self.external_trigger_bridge,
1203 context,
1204 ),
1205 };
1206 if let Err(error) = &result {
1207 classify_commit_error(&self.state, error);
1208 }
1209 result
1210 }
1211
1212 pub fn rollback(self) {
1214 self.state.abort(AbortReason::RolledBack);
1215 }
1217}
1218
1219impl Drop for Transaction<'_> {
1220 fn drop(&mut self) {
1221 self.state.abort(AbortReason::RolledBack);
1225 if self.txn_id != crate::wal::SYSTEM_TXN_ID {
1229 self.db.release_txn_locks(self.txn_id);
1230 }
1231 }
1232}
1233
1234fn owned_row_from_cells(cells: &[(u16, Value)]) -> OwnedRow {
1235 let mut columns = cells.to_vec();
1236 columns.sort_by_key(|(id, _)| *id);
1237 OwnedRow { columns }
1238}
1239
1240fn owned_row_from_map(columns: HashMap<u16, Value>) -> OwnedRow {
1241 let mut columns: Vec<(u16, Value)> = columns.into_iter().collect();
1242 columns.sort_by_key(|(id, _)| *id);
1243 OwnedRow { columns }
1244}
1245
1246fn merge_cells(mut base: Vec<(u16, Value)>, updates: Vec<(u16, Value)>) -> Vec<(u16, Value)> {
1247 for (id, value) in updates {
1248 base.retain(|(existing, _)| *existing != id);
1249 base.push((id, value));
1250 }
1251 base.sort_by_key(|(id, _)| *id);
1252 base
1253}
1254
1255fn changed_columns(cells: &[(u16, Value)]) -> Vec<u16> {
1256 let mut columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
1257 columns.sort_unstable();
1258 columns.dedup();
1259 columns
1260}
1261
1262fn columns_equal(a: &[(u16, Value)], b: &[(u16, Value)]) -> bool {
1263 if a.len() != b.len() {
1264 return false;
1265 }
1266 let mut a: Vec<_> = a.iter().collect();
1267 let mut b: Vec<_> = b.iter().collect();
1268 a.sort_by_key(|(id, _)| *id);
1269 b.sort_by_key(|(id, _)| *id);
1270 a.iter()
1271 .zip(b.iter())
1272 .all(|((id_a, v_a), (id_b, v_b))| id_a == id_b && v_a == v_b)
1273}
1274
1275pub(crate) enum StagedOp {
1277 Put(Vec<crate::memtable::Row>),
1278 Delete(Vec<RowId>),
1279 Truncate,
1280}
1281
1282use std::collections::{BTreeMap, HashMap};
1285use std::hash::{Hash, Hasher};
1286
1287#[derive(Clone, Debug)]
1290pub enum WriteKey {
1291 Row { table_id: u64, row_id: u64 },
1293 Unique {
1295 table_id: u64,
1296 index_id: u16,
1297 key_hash: u64,
1298 },
1299 Table { table_id: u64 },
1301}
1302
1303impl Hash for WriteKey {
1304 fn hash<H: Hasher>(&self, state: &mut H) {
1305 match self {
1306 WriteKey::Row { table_id, row_id } => {
1307 0u8.hash(state);
1308 table_id.hash(state);
1309 row_id.hash(state);
1310 }
1311 WriteKey::Unique {
1312 table_id,
1313 index_id,
1314 key_hash,
1315 } => {
1316 1u8.hash(state);
1317 table_id.hash(state);
1318 index_id.hash(state);
1319 key_hash.hash(state);
1320 }
1321 WriteKey::Table { table_id } => {
1322 2u8.hash(state);
1323 table_id.hash(state);
1324 }
1325 }
1326 }
1327}
1328
1329impl PartialEq for WriteKey {
1330 fn eq(&self, other: &Self) -> bool {
1331 match (self, other) {
1332 (
1333 WriteKey::Row {
1334 table_id: a,
1335 row_id: b,
1336 },
1337 WriteKey::Row {
1338 table_id: c,
1339 row_id: d,
1340 },
1341 ) => a == c && b == d,
1342 (
1343 WriteKey::Unique {
1344 table_id: a,
1345 index_id: b,
1346 key_hash: c,
1347 },
1348 WriteKey::Unique {
1349 table_id: d,
1350 index_id: e,
1351 key_hash: f,
1352 },
1353 ) => a == d && b == e && c == f,
1354 (WriteKey::Table { table_id: a }, WriteKey::Table { table_id: b }) => a == b,
1355 _ => false,
1356 }
1357 }
1358}
1359
1360impl Eq for WriteKey {}
1361
1362const CONFLICT_SHARDS: usize = 16;
1363
1364pub struct ConflictIndex {
1368 shards: [parking_lot::Mutex<HashMap<WriteKey, u64>>; CONFLICT_SHARDS],
1369 table_truncate_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
1370 table_write_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
1371 version: std::sync::atomic::AtomicU64,
1375}
1376
1377impl ConflictIndex {
1378 pub fn new() -> Self {
1379 Self {
1380 shards: std::array::from_fn(|_| parking_lot::Mutex::new(HashMap::new())),
1381 table_truncate_epochs: parking_lot::Mutex::new(HashMap::new()),
1382 table_write_epochs: parking_lot::Mutex::new(HashMap::new()),
1383 version: std::sync::atomic::AtomicU64::new(0),
1384 }
1385 }
1386
1387 pub fn version(&self) -> u64 {
1391 self.version.load(std::sync::atomic::Ordering::Acquire)
1392 }
1393
1394 fn shard(&self, key: &WriteKey) -> &parking_lot::Mutex<HashMap<WriteKey, u64>> {
1395 let mut h = std::collections::hash_map::DefaultHasher::new();
1396 key.hash(&mut h);
1397 let idx = (h.finish() as usize) & (CONFLICT_SHARDS - 1);
1398 &self.shards[idx]
1399 }
1400
1401 pub fn conflicts(&self, keys: &[WriteKey], read_epoch: Epoch) -> bool {
1404 for k in keys {
1405 let s = self.shard(k);
1406 if let Some(&ce) = s.lock().get(k) {
1407 if ce > read_epoch.0 {
1408 return true;
1409 }
1410 }
1411 }
1412 let truncates = self.table_truncate_epochs.lock();
1413 let writes = self.table_write_epochs.lock();
1414 for k in keys {
1415 match k {
1416 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
1417 if truncates.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
1418 return true;
1419 }
1420 }
1421 WriteKey::Table { table_id } => {
1422 if writes.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
1423 return true;
1424 }
1425 }
1426 }
1427 }
1428 false
1429 }
1430
1431 pub fn record(&self, keys: &[WriteKey], commit_epoch: Epoch) {
1433 for k in keys {
1434 let s = self.shard(k);
1435 s.lock().insert(k.clone(), commit_epoch.0);
1436 }
1437 let mut truncates = self.table_truncate_epochs.lock();
1438 let mut writes = self.table_write_epochs.lock();
1439 for k in keys {
1440 match k {
1441 WriteKey::Table { table_id } => {
1442 truncates
1443 .entry(*table_id)
1444 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1445 .or_insert(commit_epoch.0);
1446 }
1447 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
1448 writes
1449 .entry(*table_id)
1450 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1451 .or_insert(commit_epoch.0);
1452 }
1453 }
1454 }
1455 self.version
1456 .fetch_add(1, std::sync::atomic::Ordering::Release);
1457 }
1458
1459 pub fn prune_below(&self, min_active: Epoch) {
1462 for s in &self.shards {
1463 s.lock().retain(|_, ce| *ce >= min_active.0);
1464 }
1465 self.table_truncate_epochs
1466 .lock()
1467 .retain(|_, ce| *ce >= min_active.0);
1468 self.table_write_epochs
1469 .lock()
1470 .retain(|_, ce| *ce >= min_active.0);
1471 }
1472}
1473
1474impl Default for ConflictIndex {
1475 fn default() -> Self {
1476 Self::new()
1477 }
1478}
1479
1480pub struct GroupCommit {
1490 inner: PlMutex<GroupState>,
1491 cv: Condvar,
1492 lifecycle: Option<Arc<crate::core::LifecycleController>>,
1496}
1497
1498struct GroupState {
1499 durable_seq: u64,
1500 syncing: bool,
1501 poisoned: bool,
1502}
1503
1504impl GroupCommit {
1505 pub fn new(durable_seq: u64) -> Self {
1506 Self {
1507 inner: PlMutex::new(GroupState {
1508 durable_seq,
1509 syncing: false,
1510 poisoned: false,
1511 }),
1512 cv: Condvar::new(),
1513 lifecycle: None,
1514 }
1515 }
1516
1517 pub fn with_lifecycle(mut self, lifecycle: Arc<crate::core::LifecycleController>) -> Self {
1521 self.lifecycle = Some(lifecycle);
1522 self
1523 }
1524
1525 pub fn await_durable(&self, wal: &PlMutex<SharedWal>, commit_seq: u64) -> Result<()> {
1531 let mut st = self.inner.lock();
1532 loop {
1533 if st.poisoned {
1534 return Err(MongrelError::Other(
1535 "database poisoned by fsync error".into(),
1536 ));
1537 }
1538 if st.durable_seq >= commit_seq {
1539 return Ok(());
1540 }
1541 if st.syncing {
1542 self.cv.wait(&mut st);
1544 continue;
1545 }
1546 st.syncing = true;
1549 drop(st);
1550 std::thread::sleep(std::time::Duration::from_micros(50));
1552 let res = wal.lock().group_sync();
1553 st = self.inner.lock();
1554 st.syncing = false;
1555 match res {
1556 Ok(durable) => {
1557 if durable > st.durable_seq {
1558 st.durable_seq = durable;
1559 }
1560 self.cv.notify_all();
1561 }
1564 Err(e) => {
1565 st.poisoned = true;
1566 if let Some(lifecycle) = &self.lifecycle {
1570 lifecycle.poison();
1571 }
1572 self.cv.notify_all();
1573 return Err(e);
1574 }
1575 }
1576 }
1577 }
1578}
1579
1580pub struct ActiveTxns {
1584 inner: parking_lot::Mutex<BTreeMap<u64, u64>>,
1585}
1586
1587impl ActiveTxns {
1588 pub fn new() -> Self {
1589 Self {
1590 inner: parking_lot::Mutex::new(BTreeMap::new()),
1591 }
1592 }
1593
1594 pub fn register(&self, read_epoch: Epoch) -> ActiveTxnGuard<'_> {
1597 let mut g = self.inner.lock();
1598 *g.entry(read_epoch.0).or_insert(0) += 1;
1599 ActiveTxnGuard {
1600 active: self,
1601 epoch: read_epoch.0,
1602 }
1603 }
1604
1605 pub fn min_read_epoch(&self) -> u64 {
1607 self.inner.lock().keys().next().copied().unwrap_or(u64::MAX)
1608 }
1609}
1610
1611impl Default for ActiveTxns {
1612 fn default() -> Self {
1613 Self::new()
1614 }
1615}
1616
1617pub struct ActiveTxnGuard<'a> {
1619 active: &'a ActiveTxns,
1620 epoch: u64,
1621}
1622
1623impl Drop for ActiveTxnGuard<'_> {
1624 fn drop(&mut self) {
1625 let mut g = self.active.inner.lock();
1626 if let Some(count) = g.get_mut(&self.epoch) {
1627 *count -= 1;
1628 if *count == 0 {
1629 g.remove(&self.epoch);
1630 }
1631 }
1632 }
1633}
1634
1635#[cfg(test)]
1636mod tests {
1637 use super::*;
1638
1639 #[test]
1640 fn shared_transaction_allocator_never_crosses_open_generation() {
1641 let allocator = PlMutex::new((7_u64 << 32) | u32::MAX as u64);
1642 assert_eq!(
1643 allocate_txn_id(&allocator).unwrap(),
1644 (7_u64 << 32) | u32::MAX as u64
1645 );
1646 assert!(matches!(
1647 allocate_txn_id(&allocator),
1648 Err(MongrelError::Full(_))
1649 ));
1650 }
1651
1652 #[test]
1653 fn conflict_index_first_committer_wins_and_prunes_safely() {
1654 let ci = ConflictIndex::new();
1655 let k = vec![WriteKey::Row {
1656 table_id: 1,
1657 row_id: 7,
1658 }];
1659 assert!(!ci.conflicts(&k, Epoch(5)));
1660 ci.record(&k, Epoch(6));
1661 assert!(ci.conflicts(&k, Epoch(5)));
1662 assert!(!ci.conflicts(&k, Epoch(6)));
1663 ci.prune_below(Epoch(7));
1664 assert!(!ci.conflicts(&k, Epoch(5)));
1665 }
1666
1667 #[test]
1668 fn conflict_index_table_scope_conflicts_both_directions() {
1669 let ci = ConflictIndex::new();
1670 ci.record(&[WriteKey::Table { table_id: 1 }], Epoch(6));
1671 assert!(ci.conflicts(
1672 &[WriteKey::Row {
1673 table_id: 1,
1674 row_id: 7,
1675 }],
1676 Epoch(5)
1677 ));
1678 assert!(ci.conflicts(
1679 &[WriteKey::Unique {
1680 table_id: 1,
1681 index_id: 0,
1682 key_hash: 42,
1683 }],
1684 Epoch(5)
1685 ));
1686 assert!(!ci.conflicts(
1687 &[WriteKey::Row {
1688 table_id: 2,
1689 row_id: 7,
1690 }],
1691 Epoch(5)
1692 ));
1693
1694 let ci = ConflictIndex::new();
1695 ci.record(
1696 &[WriteKey::Row {
1697 table_id: 1,
1698 row_id: 7,
1699 }],
1700 Epoch(6),
1701 );
1702 assert!(ci.conflicts(&[WriteKey::Table { table_id: 1 }], Epoch(5)));
1703 assert!(!ci.conflicts(&[WriteKey::Table { table_id: 2 }], Epoch(5)));
1704 }
1705
1706 #[test]
1707 fn writekey_eq_across_variants() {
1708 let r1 = WriteKey::Row {
1709 table_id: 1,
1710 row_id: 2,
1711 };
1712 let r2 = WriteKey::Row {
1713 table_id: 1,
1714 row_id: 2,
1715 };
1716 let r3 = WriteKey::Row {
1717 table_id: 1,
1718 row_id: 3,
1719 };
1720 assert_eq!(r1, r2);
1721 assert_ne!(r1, r3);
1722
1723 let u1 = WriteKey::Unique {
1724 table_id: 1,
1725 index_id: 0,
1726 key_hash: 42,
1727 };
1728 let u2 = WriteKey::Unique {
1729 table_id: 1,
1730 index_id: 0,
1731 key_hash: 42,
1732 };
1733 assert_eq!(u1, u2);
1734 assert_ne!(r1, u1);
1735
1736 let t1 = WriteKey::Table { table_id: 5 };
1737 let t2 = WriteKey::Table { table_id: 5 };
1738 assert_eq!(t1, t2);
1739 assert_ne!(t1, r1);
1740 }
1741
1742 #[test]
1743 fn active_txns_tracks_min_read_epoch() {
1744 let at = ActiveTxns::new();
1745 assert_eq!(at.min_read_epoch(), u64::MAX);
1746 let g1 = at.register(Epoch(5));
1747 assert_eq!(at.min_read_epoch(), 5);
1748 let g2 = at.register(Epoch(3));
1749 assert_eq!(at.min_read_epoch(), 3);
1750 drop(g2);
1751 assert_eq!(at.min_read_epoch(), 5);
1752 drop(g1);
1753 assert_eq!(at.min_read_epoch(), u64::MAX);
1754 }
1755
1756 #[test]
1757 fn active_txns_dedups_same_epoch() {
1758 let at = ActiveTxns::new();
1759 let g1 = at.register(Epoch(7));
1760 let g2 = at.register(Epoch(7));
1761 assert_eq!(at.min_read_epoch(), 7);
1762 drop(g1);
1763 assert_eq!(at.min_read_epoch(), 7);
1764 drop(g2);
1765 assert_eq!(at.min_read_epoch(), u64::MAX);
1766 }
1767
1768 #[test]
1769 fn isolation_level_snapshot_aliases_repeatable_read() {
1770 #[allow(deprecated)]
1771 let snapshot = IsolationLevel::Snapshot;
1772 assert_eq!(snapshot.canonical(), IsolationLevel::RepeatableRead);
1773 assert_eq!(IsolationLevel::default(), IsolationLevel::RepeatableRead);
1774 assert_eq!(
1775 IsolationLevel::ReadCommitted.canonical(),
1776 IsolationLevel::ReadCommitted
1777 );
1778 assert_eq!(
1779 IsolationLevel::Serializable.canonical(),
1780 IsolationLevel::Serializable
1781 );
1782 }
1783
1784 #[test]
1785 fn transaction_state_transitions_are_enforced() {
1786 let handle = TxnStateHandle::new();
1787 assert!(matches!(handle.state(), TransactionState::Active));
1788 assert!(!handle.enter_commit_critical());
1790 assert!(matches!(handle.state(), TransactionState::Active));
1791 let receipt = mongreldb_log::CommitReceipt {
1793 transaction_id: mongreldb_types::ids::TransactionId::from_bytes([0; 16]),
1794 commit_ts: HlcTimestamp::ZERO,
1795 log_position: mongreldb_log::LogPosition::ZERO,
1796 durability: mongreldb_log::DurabilityLevel::GroupCommit,
1797 };
1798 assert!(!handle.committed(receipt.clone()));
1799
1800 assert!(handle.begin_prepare());
1801 assert!(matches!(handle.state(), TransactionState::Preparing));
1802 assert!(handle.enter_commit_critical());
1803 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1804 assert!(!handle.abort(AbortReason::Conflict("late".into())));
1806 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1807 assert!(handle.committed(receipt));
1808 assert!(matches!(handle.state(), TransactionState::Committed(_)));
1809 assert!(!handle.abort(AbortReason::RolledBack));
1811 assert!(!handle.begin_prepare());
1812 }
1813
1814 #[test]
1815 fn abort_from_preparing_is_terminal() {
1816 let handle = TxnStateHandle::new();
1817 assert!(handle.abort(AbortReason::RolledBack));
1818 assert!(matches!(
1819 handle.state(),
1820 TransactionState::Aborted(AbortReason::RolledBack)
1821 ));
1822 assert!(!handle.begin_prepare());
1823
1824 let handle = TxnStateHandle::new();
1825 handle.begin_prepare();
1826 assert!(handle.abort(AbortReason::Validation("bad row".into())));
1827 match handle.state() {
1828 TransactionState::Aborted(AbortReason::Validation(message)) => {
1829 assert_eq!(message, "bad row")
1830 }
1831 other => panic!("expected validation abort, got {other:?}"),
1832 }
1833 }
1834
1835 #[test]
1836 fn classify_commit_error_leaves_post_fence_states_untouched() {
1837 let handle = TxnStateHandle::new();
1838 handle.begin_prepare();
1839 classify_commit_error(&handle, &MongrelError::Conflict("ww".into()));
1840 assert!(matches!(
1841 handle.state(),
1842 TransactionState::Aborted(AbortReason::Conflict(_))
1843 ));
1844
1845 let handle = TxnStateHandle::new();
1846 handle.begin_prepare();
1847 handle.enter_commit_critical();
1848 classify_commit_error(
1849 &handle,
1850 &MongrelError::CommitOutcomeUnknown {
1851 epoch: 7,
1852 message: "fsync".into(),
1853 },
1854 );
1855 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1856 classify_commit_error(
1857 &handle,
1858 &MongrelError::DurableCommit {
1859 epoch: 7,
1860 message: "publish".into(),
1861 },
1862 );
1863 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1864 }
1865
1866 #[test]
1867 fn classify_commit_error_maps_serialization_failure_to_conflict_abort() {
1868 let handle = TxnStateHandle::new();
1872 handle.begin_prepare();
1873 classify_commit_error(
1874 &handle,
1875 &MongrelError::SerializationFailure {
1876 message: "a concurrent commit invalidated this transaction's reads".into(),
1877 },
1878 );
1879 match handle.state() {
1880 TransactionState::Aborted(AbortReason::Conflict(message)) => {
1881 assert_eq!(
1882 message,
1883 "serialization failure: a concurrent commit invalidated this transaction's reads"
1884 );
1885 }
1886 other => panic!("expected conflict abort, got {other:?}"),
1887 }
1888 }
1889
1890 #[test]
1891 fn ssi_validation_keys_cover_rows_and_predicates() {
1892 let mut reads = ReadSet::default();
1893 reads.record_row(3, RowId(9));
1894 reads.record_row(3, RowId(9));
1895 reads.record_row(4, RowId(1));
1896 let mut predicates = PredicateSet::default();
1897 predicates.record_table(5);
1898 let keys = ssi_validation_keys(&reads, &predicates);
1899 assert_eq!(keys.len(), 3);
1900 assert!(keys.iter().any(|key| matches!(
1901 key,
1902 WriteKey::Row {
1903 table_id: 3,
1904 row_id: 9
1905 }
1906 )));
1907 assert!(keys.iter().any(|key| matches!(
1908 key,
1909 WriteKey::Row {
1910 table_id: 4,
1911 row_id: 1
1912 }
1913 )));
1914 assert!(keys
1915 .iter()
1916 .any(|key| matches!(key, WriteKey::Table { table_id: 5 })));
1917 }
1918
1919 fn test_request(key: &str) -> IdempotencyRequest {
1920 IdempotencyRequest {
1921 key: key.to_string(),
1922 owner: "alice".to_string(),
1923 fingerprint: 42,
1924 ttl: None,
1925 }
1926 }
1927
1928 fn test_commit_ts() -> HlcTimestamp {
1929 HlcTimestamp {
1930 physical_micros: 1_700_000_000_000_000,
1931 logical: 3,
1932 node_tiebreaker: 0,
1933 }
1934 }
1935
1936 #[test]
1937 fn idempotency_ledger_replay_conflict_and_restart() {
1938 let dir = tempfile::tempdir().unwrap();
1939 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
1940 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
1941
1942 let request = test_request("k1");
1943 assert!(matches!(
1944 ledger.check_and_reserve(&request).unwrap(),
1945 IdempotencyCheck::Reserved
1946 ));
1947 assert!(matches!(
1949 ledger.check_and_reserve(&request),
1950 Err(MongrelError::Conflict(_))
1951 ));
1952 ledger
1953 .complete(&request, 7, Epoch(11), test_commit_ts())
1954 .unwrap();
1955
1956 let replay = ledger.check_and_reserve(&request).unwrap();
1958 let IdempotencyCheck::Replay(receipt) = replay else {
1959 panic!("expected replay");
1960 };
1961 assert_eq!(receipt.log_position.index, 11);
1962 assert_eq!(receipt.commit_ts, test_commit_ts());
1963 assert_eq!(
1964 receipt.durability,
1965 mongreldb_log::DurabilityLevel::GroupCommit
1966 );
1967
1968 let mut other = test_request("k1");
1970 other.fingerprint = 43;
1971 assert!(matches!(
1972 ledger.check_and_reserve(&other),
1973 Err(MongrelError::Conflict(_))
1974 ));
1975 let mut foreign = test_request("k1");
1977 foreign.owner = "bob".to_string();
1978 assert!(matches!(
1979 ledger.check_and_reserve(&foreign).unwrap(),
1980 IdempotencyCheck::Reserved
1981 ));
1982 drop(ledger);
1983
1984 let reopened = IdempotencyLedger::open(root, None).unwrap();
1986 let replay = reopened.check_and_reserve(&request).unwrap();
1987 let IdempotencyCheck::Replay(receipt) = replay else {
1988 panic!("expected replay after restart");
1989 };
1990 assert_eq!(receipt.log_position.index, 11);
1991 assert_eq!(receipt.commit_ts, test_commit_ts());
1992 assert!(matches!(
1994 reopened.check_and_reserve(&foreign),
1995 Err(MongrelError::Conflict(_))
1996 ));
1997 }
1998
1999 #[test]
2000 fn idempotency_ledger_release_and_expiry() {
2001 let dir = tempfile::tempdir().unwrap();
2002 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2003 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
2004
2005 let request = test_request("k-release");
2007 assert!(matches!(
2008 ledger.check_and_reserve(&request).unwrap(),
2009 IdempotencyCheck::Reserved
2010 ));
2011 ledger.release(&request);
2012 assert!(matches!(
2013 ledger.check_and_reserve(&request).unwrap(),
2014 IdempotencyCheck::Reserved
2015 ));
2016 ledger
2017 .complete(&request, 9, Epoch(12), test_commit_ts())
2018 .unwrap();
2019
2020 let mut expired = test_request("k-expired");
2023 expired.ttl = Some(Duration::from_nanos(1));
2024 assert!(matches!(
2025 ledger.check_and_reserve(&expired).unwrap(),
2026 IdempotencyCheck::Reserved
2027 ));
2028 ledger
2029 .complete(&expired, 10, Epoch(13), test_commit_ts())
2030 .unwrap();
2031 std::thread::sleep(Duration::from_millis(2));
2032 assert!(matches!(
2033 ledger.check_and_reserve(&expired).unwrap(),
2034 IdempotencyCheck::Reserved
2035 ));
2036
2037 drop(ledger);
2039 let reopened = IdempotencyLedger::open(root, None).unwrap();
2040 assert!(matches!(
2041 reopened.check_and_reserve(&expired).unwrap(),
2042 IdempotencyCheck::Reserved
2043 ));
2044 }
2045
2046 #[test]
2047 fn idempotency_ledger_rejects_empty_key_or_owner() {
2048 let dir = tempfile::tempdir().unwrap();
2049 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2050 let ledger = IdempotencyLedger::open(root, None).unwrap();
2051 let mut request = test_request("");
2052 assert!(matches!(
2053 ledger.check_and_reserve(&request),
2054 Err(MongrelError::InvalidArgument(_))
2055 ));
2056 request.key = "k".to_string();
2057 request.owner.clear();
2058 assert!(matches!(
2059 ledger.check_and_reserve(&request),
2060 Err(MongrelError::InvalidArgument(_))
2061 ));
2062 }
2063
2064 #[test]
2065 fn idempotency_ledger_enforces_bounded_size() {
2066 let mut records: Vec<StoredIdempotencyRecord> = (0..MAX_IDEMPOTENCY_RECORDS + 4)
2067 .map(|index| StoredIdempotencyRecord {
2068 owner: "o".to_string(),
2069 key: format!("k{index}"),
2070 fingerprint: 1,
2071 expires_at_micros: None,
2072 outcome: StoredIdempotencyOutcome::Committed {
2073 txn_id: index as u64,
2074 epoch: index as u64,
2075 commit_ts: test_commit_ts(),
2076 },
2077 })
2078 .collect();
2079 enforce_bounds(&mut records).unwrap();
2080 assert_eq!(records.len(), MAX_IDEMPOTENCY_RECORDS);
2081 assert!(records.iter().all(|record| match &record.outcome {
2083 StoredIdempotencyOutcome::Committed { epoch, .. } => *epoch >= 4,
2084 StoredIdempotencyOutcome::Reserved => false,
2085 }));
2086
2087 let mut reserved: Vec<StoredIdempotencyRecord> = (0..MAX_IDEMPOTENCY_RECORDS + 1)
2090 .map(|index| StoredIdempotencyRecord {
2091 owner: "o".to_string(),
2092 key: format!("r{index}"),
2093 fingerprint: 1,
2094 expires_at_micros: None,
2095 outcome: StoredIdempotencyOutcome::Reserved,
2096 })
2097 .collect();
2098 assert!(matches!(
2099 enforce_bounds(&mut reserved),
2100 Err(MongrelError::ResourceLimitExceeded { .. })
2101 ));
2102 }
2103
2104 #[test]
2105 fn idempotency_ledger_tampered_file_fails_closed() {
2106 let dir = tempfile::tempdir().unwrap();
2107 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2108 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
2109 let request = test_request("k1");
2110 ledger.check_and_reserve(&request).unwrap();
2111 drop(ledger);
2112
2113 let path = dir.path().join(IDEMPOTENCY_FILENAME);
2114 let mut bytes = std::fs::read(&path).unwrap();
2115 let last = bytes.len() - 1;
2116 bytes[last] ^= 0xFF;
2117 std::fs::write(&path, bytes).unwrap();
2118 assert!(IdempotencyLedger::open(root, None).is_err());
2119 }
2120}
2121
2122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2142pub enum IsolationLevel {
2143 #[default]
2144 RepeatableRead,
2145 #[deprecated(
2146 note = "renamed to `RepeatableRead` (spec §10.2 S1B-002); identical semantics, kept for compatibility"
2147 )]
2148 Snapshot,
2149 ReadCommitted,
2150 Serializable,
2151}
2152
2153impl IsolationLevel {
2154 pub fn canonical(self) -> Self {
2157 match self {
2158 #[allow(deprecated)]
2159 Self::Snapshot => Self::RepeatableRead,
2160 other => other,
2161 }
2162 }
2163}
2164
2165use std::time::Duration;
2168
2169#[derive(Debug, Clone)]
2172pub struct IdempotencyRequest {
2173 pub key: String,
2175 pub owner: String,
2177 pub fingerprint: u64,
2181 pub ttl: Option<Duration>,
2184}
2185
2186pub const IDEMPOTENCY_FILENAME: &str = "TXN_IDEMPOTENCY";
2190
2191const IDEMPOTENCY_FORMAT_VERSION: u16 = 1;
2192const IDEMPOTENCY_MAGIC: &[u8; 8] = b"MONGRTXI";
2193const MAX_IDEMPOTENCY_RECORDS: usize = 65_536;
2196const MAX_IDEMPOTENCY_BYTES: u64 = 64 * 1024 * 1024;
2198
2199#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2201enum StoredIdempotencyOutcome {
2202 Reserved,
2205 Committed {
2207 txn_id: u64,
2208 epoch: u64,
2209 commit_ts: HlcTimestamp,
2210 },
2211}
2212
2213#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2214struct StoredIdempotencyRecord {
2215 owner: String,
2216 key: String,
2217 fingerprint: u64,
2218 expires_at_micros: Option<u64>,
2219 outcome: StoredIdempotencyOutcome,
2220}
2221
2222#[derive(serde::Serialize, serde::Deserialize)]
2223struct IdempotencyEnvelope {
2224 format_version: u16,
2225 records: Vec<StoredIdempotencyRecord>,
2226}
2227
2228pub(crate) enum IdempotencyCheck {
2230 Reserved,
2233 Replay(mongreldb_log::CommitReceipt),
2236}
2237
2238pub(crate) struct IdempotencyLedger {
2244 root: std::sync::Arc<crate::durable_file::DurableRoot>,
2245 meta_dek: Option<[u8; crate::catalog::META_DEK_LEN]>,
2246 inner: PlMutex<Vec<StoredIdempotencyRecord>>,
2247}
2248
2249impl IdempotencyLedger {
2250 pub(crate) fn open(
2255 root: std::sync::Arc<crate::durable_file::DurableRoot>,
2256 meta_dek: Option<[u8; crate::catalog::META_DEK_LEN]>,
2257 ) -> Result<Self> {
2258 let mut inner = read_idempotency_file(&root, meta_dek.as_ref())?.unwrap_or_default();
2259 sweep_expired(&mut inner, wall_micros());
2260 Ok(Self {
2261 root,
2262 meta_dek,
2263 inner: PlMutex::new(inner),
2264 })
2265 }
2266
2267 pub(crate) fn check_and_reserve(
2272 &self,
2273 request: &IdempotencyRequest,
2274 ) -> Result<IdempotencyCheck> {
2275 validate_request(request)?;
2276 let now = wall_micros();
2277 let mut inner = self.inner.lock();
2278 sweep_expired(&mut inner, now);
2279 if let Some(existing) = inner
2280 .iter()
2281 .find(|record| record.owner == request.owner && record.key == request.key)
2282 {
2283 if existing.fingerprint != request.fingerprint {
2284 return Err(MongrelError::Conflict(format!(
2285 "idempotency key {:?} was already used with a different request",
2286 request.key
2287 )));
2288 }
2289 return match &existing.outcome {
2290 StoredIdempotencyOutcome::Committed {
2291 txn_id,
2292 epoch,
2293 commit_ts,
2294 } => Ok(IdempotencyCheck::Replay(mongreldb_log::CommitReceipt {
2295 transaction_id: crate::commit_log::transaction_id_from_txn(*txn_id),
2296 commit_ts: *commit_ts,
2297 log_position: mongreldb_log::LogPosition {
2298 term: 0,
2299 index: *epoch,
2300 },
2301 durability: mongreldb_log::DurabilityLevel::GroupCommit,
2302 })),
2303 StoredIdempotencyOutcome::Reserved => Err(MongrelError::Conflict(format!(
2304 "idempotency key {:?} has an in-flight or interrupted commit; retry to resolve",
2305 request.key
2306 ))),
2307 };
2308 }
2309 inner.push(StoredIdempotencyRecord {
2310 owner: request.owner.clone(),
2311 key: request.key.clone(),
2312 fingerprint: request.fingerprint,
2313 expires_at_micros: expiry_micros(request.ttl, now),
2314 outcome: StoredIdempotencyOutcome::Reserved,
2315 });
2316 persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner)?;
2317 Ok(IdempotencyCheck::Reserved)
2318 }
2319
2320 pub(crate) fn complete(
2326 &self,
2327 request: &IdempotencyRequest,
2328 txn_id: u64,
2329 epoch: Epoch,
2330 commit_ts: HlcTimestamp,
2331 ) -> Result<()> {
2332 let mut inner = self.inner.lock();
2333 let now = wall_micros();
2334 let Some(record) = inner
2335 .iter_mut()
2336 .find(|record| record.owner == request.owner && record.key == request.key)
2337 else {
2338 return Err(MongrelError::Other(format!(
2339 "idempotency reservation for key {:?} vanished during commit",
2340 request.key
2341 )));
2342 };
2343 record.expires_at_micros = expiry_micros(request.ttl, now);
2344 record.outcome = StoredIdempotencyOutcome::Committed {
2345 txn_id,
2346 epoch: epoch.0,
2347 commit_ts,
2348 };
2349 persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner)
2350 }
2351
2352 pub(crate) fn release(&self, request: &IdempotencyRequest) {
2356 let mut inner = self.inner.lock();
2357 inner.retain(|record| {
2358 !(record.owner == request.owner
2359 && record.key == request.key
2360 && matches!(record.outcome, StoredIdempotencyOutcome::Reserved))
2361 });
2362 let _ = persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner);
2363 }
2364}
2365
2366pub(crate) struct IdempotencyReservationGuard<'a> {
2369 ledger: &'a IdempotencyLedger,
2370 request: IdempotencyRequest,
2371 armed: bool,
2372}
2373
2374impl<'a> IdempotencyReservationGuard<'a> {
2375 pub(crate) fn new(ledger: &'a IdempotencyLedger, request: IdempotencyRequest) -> Self {
2376 Self {
2377 ledger,
2378 request,
2379 armed: true,
2380 }
2381 }
2382
2383 pub(crate) fn disarm(&mut self) {
2384 self.armed = false;
2385 }
2386}
2387
2388impl Drop for IdempotencyReservationGuard<'_> {
2389 fn drop(&mut self) {
2390 if self.armed {
2391 self.ledger.release(&self.request);
2392 }
2393 }
2394}
2395
2396fn validate_request(request: &IdempotencyRequest) -> Result<()> {
2397 if request.key.is_empty() {
2398 return Err(MongrelError::InvalidArgument(
2399 "idempotency key must not be empty".into(),
2400 ));
2401 }
2402 if request.owner.is_empty() {
2403 return Err(MongrelError::InvalidArgument(
2404 "idempotency owner must not be empty".into(),
2405 ));
2406 }
2407 Ok(())
2408}
2409
2410fn expiry_micros(ttl: Option<Duration>, now_micros: u64) -> Option<u64> {
2411 ttl.map(|ttl| now_micros.saturating_add(u64::try_from(ttl.as_micros()).unwrap_or(u64::MAX)))
2412}
2413
2414fn sweep_expired(records: &mut Vec<StoredIdempotencyRecord>, now_micros: u64) {
2415 records.retain(|record| {
2416 record
2417 .expires_at_micros
2418 .is_none_or(|expires_at| expires_at > now_micros)
2419 });
2420}
2421
2422fn enforce_bounds(records: &mut Vec<StoredIdempotencyRecord>) -> Result<()> {
2426 while records.len() > MAX_IDEMPOTENCY_RECORDS {
2427 let Some((oldest, _)) = records
2428 .iter()
2429 .enumerate()
2430 .filter(|(_, record)| {
2431 matches!(record.outcome, StoredIdempotencyOutcome::Committed { .. })
2432 })
2433 .min_by_key(|(_, record)| match &record.outcome {
2434 StoredIdempotencyOutcome::Committed { epoch, .. } => *epoch,
2435 StoredIdempotencyOutcome::Reserved => unreachable!(),
2436 })
2437 else {
2438 return Err(MongrelError::ResourceLimitExceeded {
2439 resource: "idempotency records",
2440 requested: records.len(),
2441 limit: MAX_IDEMPOTENCY_RECORDS,
2442 });
2443 };
2444 records.remove(oldest);
2445 }
2446 Ok(())
2447}
2448
2449fn persist_locked(
2450 root: &crate::durable_file::DurableRoot,
2451 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2452 records: &mut Vec<StoredIdempotencyRecord>,
2453) -> Result<()> {
2454 enforce_bounds(records)?;
2455 let body = serde_json::to_vec(&IdempotencyEnvelope {
2456 format_version: IDEMPOTENCY_FORMAT_VERSION,
2457 records: records.clone(),
2458 })
2459 .map_err(|error| MongrelError::Other(format!("idempotency ledger serialize: {error}")))?;
2460 let payload = seal_idempotency(&body, meta_dek)?;
2461 root.write_atomic(IDEMPOTENCY_FILENAME, &payload)?;
2462 Ok(())
2463}
2464
2465fn read_idempotency_file(
2466 root: &crate::durable_file::DurableRoot,
2467 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2468) -> Result<Option<Vec<StoredIdempotencyRecord>>> {
2469 use std::io::Read as _;
2470 let file = match root.open_regular(IDEMPOTENCY_FILENAME) {
2471 Ok(file) => file,
2472 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2473 Err(error) => return Err(error.into()),
2474 };
2475 let length = file.metadata()?.len();
2476 if length > MAX_IDEMPOTENCY_BYTES {
2477 return Err(MongrelError::Other(format!(
2478 "idempotency ledger of {length} bytes exceeds the {MAX_IDEMPOTENCY_BYTES}-byte limit"
2479 )));
2480 }
2481 let mut bytes = Vec::with_capacity(length as usize);
2482 file.take(MAX_IDEMPOTENCY_BYTES + 1)
2483 .read_to_end(&mut bytes)?;
2484 if bytes.len() as u64 != length {
2485 return Err(MongrelError::Other(
2486 "idempotency ledger length changed while reading".into(),
2487 ));
2488 }
2489 open_idempotency_payload(&bytes, meta_dek).map(Some)
2490}
2491
2492fn decode_idempotency(body: &[u8]) -> Result<Vec<StoredIdempotencyRecord>> {
2493 let envelope: IdempotencyEnvelope = serde_json::from_slice(body)
2494 .map_err(|error| MongrelError::Other(format!("idempotency ledger deserialize: {error}")))?;
2495 if envelope.format_version != IDEMPOTENCY_FORMAT_VERSION {
2496 return Err(MongrelError::Other(format!(
2497 "unsupported idempotency ledger format version {}",
2498 envelope.format_version
2499 )));
2500 }
2501 Ok(envelope.records)
2502}
2503
2504fn plaintext_idempotency_frame(body: &[u8]) -> Vec<u8> {
2505 use sha2::Digest as _;
2506 let hash = sha2::Sha256::digest(body);
2507 let mut out = Vec::with_capacity(body.len() + 8 + 32);
2508 out.extend_from_slice(IDEMPOTENCY_MAGIC);
2509 out.extend_from_slice(&hash);
2510 out.extend_from_slice(body);
2511 out
2512}
2513
2514fn parse_idempotency_plaintext(bytes: &[u8]) -> Result<Vec<StoredIdempotencyRecord>> {
2515 use sha2::Digest as _;
2516 if bytes.len() < 8 + 32 || &bytes[..8] != IDEMPOTENCY_MAGIC {
2517 return Err(MongrelError::Other(
2518 "idempotency ledger magic mismatch (corrupt or sealed with a key)".into(),
2519 ));
2520 }
2521 let (tag, body) = bytes[8..].split_at(32);
2522 let calc = sha2::Sha256::digest(body);
2523 if tag != calc.as_slice() {
2524 return Err(MongrelError::Other(
2525 "idempotency ledger checksum mismatch (tampered or torn)".into(),
2526 ));
2527 }
2528 decode_idempotency(body)
2529}
2530
2531#[cfg(feature = "encryption")]
2532fn seal_idempotency(
2533 body: &[u8],
2534 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2535) -> Result<Vec<u8>> {
2536 match meta_dek {
2537 Some(dek) => crate::encryption::encrypt_blob(dek, body),
2538 None => Ok(plaintext_idempotency_frame(body)),
2539 }
2540}
2541
2542#[cfg(not(feature = "encryption"))]
2543fn seal_idempotency(
2544 body: &[u8],
2545 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2546) -> Result<Vec<u8>> {
2547 if meta_dek.is_some() {
2548 return Err(MongrelError::Encryption(
2549 "a metadata key was supplied but the `encryption` feature is disabled".into(),
2550 ));
2551 }
2552 Ok(plaintext_idempotency_frame(body))
2553}
2554
2555#[cfg(feature = "encryption")]
2556fn open_idempotency_payload(
2557 bytes: &[u8],
2558 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2559) -> Result<Vec<StoredIdempotencyRecord>> {
2560 match meta_dek {
2561 Some(dek) => {
2564 let body = crate::encryption::decrypt_blob(dek, bytes).map_err(|_| {
2565 MongrelError::Decryption(
2566 "idempotency ledger authentication failed (wrong key or tampered)".into(),
2567 )
2568 })?;
2569 decode_idempotency(&body)
2570 }
2571 None => parse_idempotency_plaintext(bytes),
2572 }
2573}
2574
2575#[cfg(not(feature = "encryption"))]
2576fn open_idempotency_payload(
2577 bytes: &[u8],
2578 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2579) -> Result<Vec<StoredIdempotencyRecord>> {
2580 if meta_dek.is_some() {
2581 return Err(MongrelError::Encryption(
2582 "a metadata key was supplied but the `encryption` feature is disabled".into(),
2583 ));
2584 }
2585 parse_idempotency_plaintext(bytes)
2586}
2587
2588fn wall_micros() -> u64 {
2590 let micros = std::time::SystemTime::now()
2591 .duration_since(std::time::UNIX_EPOCH)
2592 .map(|duration| duration.as_micros())
2593 .unwrap_or(0);
2594 u64::try_from(micros).unwrap_or(u64::MAX)
2595}