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(
1161 mut self,
1162 control: Option<&crate::ExecutionControl>,
1163 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
1164 idempotency: Option<IdempotencyRequest>,
1165 ) -> Result<(Epoch, Vec<RowId>)> {
1166 if let Some(message) = self.allocation_error.take() {
1167 self.state.abort(AbortReason::Error(message.clone()));
1168 return Err(MongrelError::Full(message));
1169 }
1170 if let Err(fault) = mongreldb_fault::inject("txn.prepare.before") {
1173 let error = crate::commit_log::fault_as_io(fault);
1174 classify_commit_error(&self.state, &error);
1175 return Err(error);
1176 }
1177 self.state.begin_prepare();
1178 if let Err(fault) = mongreldb_fault::inject("txn.prepare.after") {
1179 let error = crate::commit_log::fault_as_io(fault);
1180 classify_commit_error(&self.state, &error);
1181 return Err(error);
1182 }
1183 let context = TxnCommitContext {
1184 isolation: self.isolation,
1185 read_ts: self.read_ts,
1186 read_set: std::mem::take(&mut self.read_set),
1187 predicate_set: std::mem::take(&mut self.predicate_set),
1188 state: Some(self.state.clone()),
1189 idempotency,
1190 };
1191 let staging = std::mem::take(&mut self.staging);
1192 let external_states = std::mem::take(&mut self.external_states);
1193 let materialized_view_updates = std::mem::take(&mut self.materialized_view_updates);
1194 let principal = self.principal.take();
1195 let result = match (control, before_commit) {
1196 (Some(control), Some(before_commit)) => {
1197 self.db.commit_transaction_with_external_states_controlled(
1198 self.txn_id,
1199 self.read.epoch,
1200 staging,
1201 external_states,
1202 materialized_view_updates,
1203 principal,
1204 self.principal_catalog_bound,
1205 self.external_trigger_bridge,
1206 context,
1207 control,
1208 before_commit,
1209 )
1210 }
1211 _ => self.db.commit_transaction_with_external_states(
1212 self.txn_id,
1213 self.read.epoch,
1214 staging,
1215 external_states,
1216 materialized_view_updates,
1217 principal,
1218 self.principal_catalog_bound,
1219 self.external_trigger_bridge,
1220 context,
1221 ),
1222 };
1223 if let Err(error) = &result {
1224 classify_commit_error(&self.state, error);
1225 }
1226 result
1227 }
1228
1229 pub fn rollback(self) {
1231 self.state.abort(AbortReason::RolledBack);
1232 }
1234}
1235
1236impl Drop for Transaction<'_> {
1237 fn drop(&mut self) {
1238 self.state.abort(AbortReason::RolledBack);
1242 if self.txn_id != crate::wal::SYSTEM_TXN_ID {
1246 self.db.release_txn_locks(self.txn_id);
1247 }
1248 }
1249}
1250
1251fn owned_row_from_cells(cells: &[(u16, Value)]) -> OwnedRow {
1252 let mut columns = cells.to_vec();
1253 columns.sort_by_key(|(id, _)| *id);
1254 OwnedRow { columns }
1255}
1256
1257fn owned_row_from_map(columns: HashMap<u16, Value>) -> OwnedRow {
1258 let mut columns: Vec<(u16, Value)> = columns.into_iter().collect();
1259 columns.sort_by_key(|(id, _)| *id);
1260 OwnedRow { columns }
1261}
1262
1263fn merge_cells(mut base: Vec<(u16, Value)>, updates: Vec<(u16, Value)>) -> Vec<(u16, Value)> {
1264 for (id, value) in updates {
1265 base.retain(|(existing, _)| *existing != id);
1266 base.push((id, value));
1267 }
1268 base.sort_by_key(|(id, _)| *id);
1269 base
1270}
1271
1272fn changed_columns(cells: &[(u16, Value)]) -> Vec<u16> {
1273 let mut columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
1274 columns.sort_unstable();
1275 columns.dedup();
1276 columns
1277}
1278
1279fn columns_equal(a: &[(u16, Value)], b: &[(u16, Value)]) -> bool {
1280 if a.len() != b.len() {
1281 return false;
1282 }
1283 let mut a: Vec<_> = a.iter().collect();
1284 let mut b: Vec<_> = b.iter().collect();
1285 a.sort_by_key(|(id, _)| *id);
1286 b.sort_by_key(|(id, _)| *id);
1287 a.iter()
1288 .zip(b.iter())
1289 .all(|((id_a, v_a), (id_b, v_b))| id_a == id_b && v_a == v_b)
1290}
1291
1292pub(crate) enum StagedOp {
1294 Put(Vec<crate::memtable::Row>),
1295 Delete(Vec<RowId>),
1296 Truncate,
1297}
1298
1299use std::collections::{BTreeMap, HashMap};
1302use std::hash::{Hash, Hasher};
1303
1304#[derive(Clone, Debug)]
1307pub enum WriteKey {
1308 Row { table_id: u64, row_id: u64 },
1310 Unique {
1312 table_id: u64,
1313 index_id: u16,
1314 key_hash: u64,
1315 },
1316 Table { table_id: u64 },
1318}
1319
1320impl Hash for WriteKey {
1321 fn hash<H: Hasher>(&self, state: &mut H) {
1322 match self {
1323 WriteKey::Row { table_id, row_id } => {
1324 0u8.hash(state);
1325 table_id.hash(state);
1326 row_id.hash(state);
1327 }
1328 WriteKey::Unique {
1329 table_id,
1330 index_id,
1331 key_hash,
1332 } => {
1333 1u8.hash(state);
1334 table_id.hash(state);
1335 index_id.hash(state);
1336 key_hash.hash(state);
1337 }
1338 WriteKey::Table { table_id } => {
1339 2u8.hash(state);
1340 table_id.hash(state);
1341 }
1342 }
1343 }
1344}
1345
1346impl PartialEq for WriteKey {
1347 fn eq(&self, other: &Self) -> bool {
1348 match (self, other) {
1349 (
1350 WriteKey::Row {
1351 table_id: a,
1352 row_id: b,
1353 },
1354 WriteKey::Row {
1355 table_id: c,
1356 row_id: d,
1357 },
1358 ) => a == c && b == d,
1359 (
1360 WriteKey::Unique {
1361 table_id: a,
1362 index_id: b,
1363 key_hash: c,
1364 },
1365 WriteKey::Unique {
1366 table_id: d,
1367 index_id: e,
1368 key_hash: f,
1369 },
1370 ) => a == d && b == e && c == f,
1371 (WriteKey::Table { table_id: a }, WriteKey::Table { table_id: b }) => a == b,
1372 _ => false,
1373 }
1374 }
1375}
1376
1377impl Eq for WriteKey {}
1378
1379const CONFLICT_SHARDS: usize = 16;
1380
1381pub struct ConflictIndex {
1385 shards: [parking_lot::Mutex<HashMap<WriteKey, u64>>; CONFLICT_SHARDS],
1386 table_truncate_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
1387 table_write_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
1388 version: std::sync::atomic::AtomicU64,
1392}
1393
1394impl ConflictIndex {
1395 pub fn new() -> Self {
1396 Self {
1397 shards: std::array::from_fn(|_| parking_lot::Mutex::new(HashMap::new())),
1398 table_truncate_epochs: parking_lot::Mutex::new(HashMap::new()),
1399 table_write_epochs: parking_lot::Mutex::new(HashMap::new()),
1400 version: std::sync::atomic::AtomicU64::new(0),
1401 }
1402 }
1403
1404 pub fn version(&self) -> u64 {
1408 self.version.load(std::sync::atomic::Ordering::Acquire)
1409 }
1410
1411 fn shard(&self, key: &WriteKey) -> &parking_lot::Mutex<HashMap<WriteKey, u64>> {
1412 let mut h = std::collections::hash_map::DefaultHasher::new();
1413 key.hash(&mut h);
1414 let idx = (h.finish() as usize) & (CONFLICT_SHARDS - 1);
1415 &self.shards[idx]
1416 }
1417
1418 pub fn conflicts(&self, keys: &[WriteKey], read_epoch: Epoch) -> bool {
1421 for k in keys {
1422 let s = self.shard(k);
1423 if let Some(&ce) = s.lock().get(k) {
1424 if ce > read_epoch.0 {
1425 return true;
1426 }
1427 }
1428 }
1429 let truncates = self.table_truncate_epochs.lock();
1430 let writes = self.table_write_epochs.lock();
1431 for k in keys {
1432 match k {
1433 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
1434 if truncates.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
1435 return true;
1436 }
1437 }
1438 WriteKey::Table { table_id } => {
1439 if writes.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
1440 return true;
1441 }
1442 }
1443 }
1444 }
1445 false
1446 }
1447
1448 pub fn record(&self, keys: &[WriteKey], commit_epoch: Epoch) {
1450 for k in keys {
1451 let s = self.shard(k);
1452 s.lock().insert(k.clone(), commit_epoch.0);
1453 }
1454 let mut truncates = self.table_truncate_epochs.lock();
1455 let mut writes = self.table_write_epochs.lock();
1456 for k in keys {
1457 match k {
1458 WriteKey::Table { table_id } => {
1459 truncates
1460 .entry(*table_id)
1461 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1462 .or_insert(commit_epoch.0);
1463 }
1464 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
1465 writes
1466 .entry(*table_id)
1467 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1468 .or_insert(commit_epoch.0);
1469 }
1470 }
1471 }
1472 self.version
1473 .fetch_add(1, std::sync::atomic::Ordering::Release);
1474 }
1475
1476 pub fn prune_below(&self, min_active: Epoch) {
1479 for s in &self.shards {
1480 s.lock().retain(|_, ce| *ce >= min_active.0);
1481 }
1482 self.table_truncate_epochs
1483 .lock()
1484 .retain(|_, ce| *ce >= min_active.0);
1485 self.table_write_epochs
1486 .lock()
1487 .retain(|_, ce| *ce >= min_active.0);
1488 }
1489}
1490
1491impl Default for ConflictIndex {
1492 fn default() -> Self {
1493 Self::new()
1494 }
1495}
1496
1497pub struct GroupCommit {
1507 inner: PlMutex<GroupState>,
1508 cv: Condvar,
1509 lifecycle: Option<Arc<crate::core::LifecycleController>>,
1513}
1514
1515struct GroupState {
1516 durable_seq: u64,
1517 syncing: bool,
1518 poisoned: bool,
1519}
1520
1521impl GroupCommit {
1522 pub fn new(durable_seq: u64) -> Self {
1523 Self {
1524 inner: PlMutex::new(GroupState {
1525 durable_seq,
1526 syncing: false,
1527 poisoned: false,
1528 }),
1529 cv: Condvar::new(),
1530 lifecycle: None,
1531 }
1532 }
1533
1534 pub fn with_lifecycle(mut self, lifecycle: Arc<crate::core::LifecycleController>) -> Self {
1538 self.lifecycle = Some(lifecycle);
1539 self
1540 }
1541
1542 pub fn await_durable(&self, wal: &PlMutex<SharedWal>, commit_seq: u64) -> Result<()> {
1548 let mut st = self.inner.lock();
1549 loop {
1550 if st.poisoned {
1551 return Err(MongrelError::Other(
1552 "database poisoned by fsync error".into(),
1553 ));
1554 }
1555 if st.durable_seq >= commit_seq {
1556 return Ok(());
1557 }
1558 if st.syncing {
1559 self.cv.wait(&mut st);
1561 continue;
1562 }
1563 st.syncing = true;
1566 drop(st);
1567 std::thread::sleep(std::time::Duration::from_micros(50));
1569 let res = wal.lock().group_sync();
1570 st = self.inner.lock();
1571 st.syncing = false;
1572 match res {
1573 Ok(durable) => {
1574 if durable > st.durable_seq {
1575 st.durable_seq = durable;
1576 }
1577 self.cv.notify_all();
1578 }
1581 Err(e) => {
1582 st.poisoned = true;
1583 if let Some(lifecycle) = &self.lifecycle {
1587 lifecycle.poison();
1588 }
1589 self.cv.notify_all();
1590 return Err(e);
1591 }
1592 }
1593 }
1594 }
1595}
1596
1597pub struct ActiveTxns {
1601 inner: parking_lot::Mutex<BTreeMap<u64, u64>>,
1602}
1603
1604impl ActiveTxns {
1605 pub fn new() -> Self {
1606 Self {
1607 inner: parking_lot::Mutex::new(BTreeMap::new()),
1608 }
1609 }
1610
1611 pub fn register(&self, read_epoch: Epoch) -> ActiveTxnGuard<'_> {
1614 let mut g = self.inner.lock();
1615 *g.entry(read_epoch.0).or_insert(0) += 1;
1616 ActiveTxnGuard {
1617 active: self,
1618 epoch: read_epoch.0,
1619 }
1620 }
1621
1622 pub fn min_read_epoch(&self) -> u64 {
1624 self.inner.lock().keys().next().copied().unwrap_or(u64::MAX)
1625 }
1626}
1627
1628impl Default for ActiveTxns {
1629 fn default() -> Self {
1630 Self::new()
1631 }
1632}
1633
1634pub struct ActiveTxnGuard<'a> {
1636 active: &'a ActiveTxns,
1637 epoch: u64,
1638}
1639
1640impl Drop for ActiveTxnGuard<'_> {
1641 fn drop(&mut self) {
1642 let mut g = self.active.inner.lock();
1643 if let Some(count) = g.get_mut(&self.epoch) {
1644 *count -= 1;
1645 if *count == 0 {
1646 g.remove(&self.epoch);
1647 }
1648 }
1649 }
1650}
1651
1652#[cfg(test)]
1653mod tests {
1654 use super::*;
1655
1656 #[test]
1657 fn shared_transaction_allocator_never_crosses_open_generation() {
1658 let allocator = PlMutex::new((7_u64 << 32) | u32::MAX as u64);
1659 assert_eq!(
1660 allocate_txn_id(&allocator).unwrap(),
1661 (7_u64 << 32) | u32::MAX as u64
1662 );
1663 assert!(matches!(
1664 allocate_txn_id(&allocator),
1665 Err(MongrelError::Full(_))
1666 ));
1667 }
1668
1669 #[test]
1670 fn conflict_index_first_committer_wins_and_prunes_safely() {
1671 let ci = ConflictIndex::new();
1672 let k = vec![WriteKey::Row {
1673 table_id: 1,
1674 row_id: 7,
1675 }];
1676 assert!(!ci.conflicts(&k, Epoch(5)));
1677 ci.record(&k, Epoch(6));
1678 assert!(ci.conflicts(&k, Epoch(5)));
1679 assert!(!ci.conflicts(&k, Epoch(6)));
1680 ci.prune_below(Epoch(7));
1681 assert!(!ci.conflicts(&k, Epoch(5)));
1682 }
1683
1684 #[test]
1685 fn conflict_index_table_scope_conflicts_both_directions() {
1686 let ci = ConflictIndex::new();
1687 ci.record(&[WriteKey::Table { table_id: 1 }], Epoch(6));
1688 assert!(ci.conflicts(
1689 &[WriteKey::Row {
1690 table_id: 1,
1691 row_id: 7,
1692 }],
1693 Epoch(5)
1694 ));
1695 assert!(ci.conflicts(
1696 &[WriteKey::Unique {
1697 table_id: 1,
1698 index_id: 0,
1699 key_hash: 42,
1700 }],
1701 Epoch(5)
1702 ));
1703 assert!(!ci.conflicts(
1704 &[WriteKey::Row {
1705 table_id: 2,
1706 row_id: 7,
1707 }],
1708 Epoch(5)
1709 ));
1710
1711 let ci = ConflictIndex::new();
1712 ci.record(
1713 &[WriteKey::Row {
1714 table_id: 1,
1715 row_id: 7,
1716 }],
1717 Epoch(6),
1718 );
1719 assert!(ci.conflicts(&[WriteKey::Table { table_id: 1 }], Epoch(5)));
1720 assert!(!ci.conflicts(&[WriteKey::Table { table_id: 2 }], Epoch(5)));
1721 }
1722
1723 #[test]
1724 fn writekey_eq_across_variants() {
1725 let r1 = WriteKey::Row {
1726 table_id: 1,
1727 row_id: 2,
1728 };
1729 let r2 = WriteKey::Row {
1730 table_id: 1,
1731 row_id: 2,
1732 };
1733 let r3 = WriteKey::Row {
1734 table_id: 1,
1735 row_id: 3,
1736 };
1737 assert_eq!(r1, r2);
1738 assert_ne!(r1, r3);
1739
1740 let u1 = WriteKey::Unique {
1741 table_id: 1,
1742 index_id: 0,
1743 key_hash: 42,
1744 };
1745 let u2 = WriteKey::Unique {
1746 table_id: 1,
1747 index_id: 0,
1748 key_hash: 42,
1749 };
1750 assert_eq!(u1, u2);
1751 assert_ne!(r1, u1);
1752
1753 let t1 = WriteKey::Table { table_id: 5 };
1754 let t2 = WriteKey::Table { table_id: 5 };
1755 assert_eq!(t1, t2);
1756 assert_ne!(t1, r1);
1757 }
1758
1759 #[test]
1760 fn active_txns_tracks_min_read_epoch() {
1761 let at = ActiveTxns::new();
1762 assert_eq!(at.min_read_epoch(), u64::MAX);
1763 let g1 = at.register(Epoch(5));
1764 assert_eq!(at.min_read_epoch(), 5);
1765 let g2 = at.register(Epoch(3));
1766 assert_eq!(at.min_read_epoch(), 3);
1767 drop(g2);
1768 assert_eq!(at.min_read_epoch(), 5);
1769 drop(g1);
1770 assert_eq!(at.min_read_epoch(), u64::MAX);
1771 }
1772
1773 #[test]
1774 fn active_txns_dedups_same_epoch() {
1775 let at = ActiveTxns::new();
1776 let g1 = at.register(Epoch(7));
1777 let g2 = at.register(Epoch(7));
1778 assert_eq!(at.min_read_epoch(), 7);
1779 drop(g1);
1780 assert_eq!(at.min_read_epoch(), 7);
1781 drop(g2);
1782 assert_eq!(at.min_read_epoch(), u64::MAX);
1783 }
1784
1785 #[test]
1786 fn isolation_level_snapshot_aliases_repeatable_read() {
1787 #[allow(deprecated)]
1788 let snapshot = IsolationLevel::Snapshot;
1789 assert_eq!(snapshot.canonical(), IsolationLevel::RepeatableRead);
1790 assert_eq!(IsolationLevel::default(), IsolationLevel::RepeatableRead);
1791 assert_eq!(
1792 IsolationLevel::ReadCommitted.canonical(),
1793 IsolationLevel::ReadCommitted
1794 );
1795 assert_eq!(
1796 IsolationLevel::Serializable.canonical(),
1797 IsolationLevel::Serializable
1798 );
1799 }
1800
1801 #[test]
1802 fn transaction_state_transitions_are_enforced() {
1803 let handle = TxnStateHandle::new();
1804 assert!(matches!(handle.state(), TransactionState::Active));
1805 assert!(!handle.enter_commit_critical());
1807 assert!(matches!(handle.state(), TransactionState::Active));
1808 let receipt = mongreldb_log::CommitReceipt {
1810 transaction_id: mongreldb_types::ids::TransactionId::from_bytes([0; 16]),
1811 commit_ts: HlcTimestamp::ZERO,
1812 log_position: mongreldb_log::LogPosition::ZERO,
1813 durability: mongreldb_log::DurabilityLevel::GroupCommit,
1814 };
1815 assert!(!handle.committed(receipt.clone()));
1816
1817 assert!(handle.begin_prepare());
1818 assert!(matches!(handle.state(), TransactionState::Preparing));
1819 assert!(handle.enter_commit_critical());
1820 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1821 assert!(!handle.abort(AbortReason::Conflict("late".into())));
1823 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1824 assert!(handle.committed(receipt));
1825 assert!(matches!(handle.state(), TransactionState::Committed(_)));
1826 assert!(!handle.abort(AbortReason::RolledBack));
1828 assert!(!handle.begin_prepare());
1829 }
1830
1831 #[test]
1832 fn abort_from_preparing_is_terminal() {
1833 let handle = TxnStateHandle::new();
1834 assert!(handle.abort(AbortReason::RolledBack));
1835 assert!(matches!(
1836 handle.state(),
1837 TransactionState::Aborted(AbortReason::RolledBack)
1838 ));
1839 assert!(!handle.begin_prepare());
1840
1841 let handle = TxnStateHandle::new();
1842 handle.begin_prepare();
1843 assert!(handle.abort(AbortReason::Validation("bad row".into())));
1844 match handle.state() {
1845 TransactionState::Aborted(AbortReason::Validation(message)) => {
1846 assert_eq!(message, "bad row")
1847 }
1848 other => panic!("expected validation abort, got {other:?}"),
1849 }
1850 }
1851
1852 #[test]
1853 fn classify_commit_error_leaves_post_fence_states_untouched() {
1854 let handle = TxnStateHandle::new();
1855 handle.begin_prepare();
1856 classify_commit_error(&handle, &MongrelError::Conflict("ww".into()));
1857 assert!(matches!(
1858 handle.state(),
1859 TransactionState::Aborted(AbortReason::Conflict(_))
1860 ));
1861
1862 let handle = TxnStateHandle::new();
1863 handle.begin_prepare();
1864 handle.enter_commit_critical();
1865 classify_commit_error(
1866 &handle,
1867 &MongrelError::CommitOutcomeUnknown {
1868 epoch: 7,
1869 message: "fsync".into(),
1870 },
1871 );
1872 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1873 classify_commit_error(
1874 &handle,
1875 &MongrelError::DurableCommit {
1876 epoch: 7,
1877 message: "publish".into(),
1878 },
1879 );
1880 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1881 }
1882
1883 #[test]
1884 fn classify_commit_error_maps_serialization_failure_to_conflict_abort() {
1885 let handle = TxnStateHandle::new();
1889 handle.begin_prepare();
1890 classify_commit_error(
1891 &handle,
1892 &MongrelError::SerializationFailure {
1893 message: "a concurrent commit invalidated this transaction's reads".into(),
1894 },
1895 );
1896 match handle.state() {
1897 TransactionState::Aborted(AbortReason::Conflict(message)) => {
1898 assert_eq!(
1899 message,
1900 "serialization failure: a concurrent commit invalidated this transaction's reads"
1901 );
1902 }
1903 other => panic!("expected conflict abort, got {other:?}"),
1904 }
1905 }
1906
1907 #[test]
1908 fn ssi_validation_keys_cover_rows_and_predicates() {
1909 let mut reads = ReadSet::default();
1910 reads.record_row(3, RowId(9));
1911 reads.record_row(3, RowId(9));
1912 reads.record_row(4, RowId(1));
1913 let mut predicates = PredicateSet::default();
1914 predicates.record_table(5);
1915 let keys = ssi_validation_keys(&reads, &predicates);
1916 assert_eq!(keys.len(), 3);
1917 assert!(keys.iter().any(|key| matches!(
1918 key,
1919 WriteKey::Row {
1920 table_id: 3,
1921 row_id: 9
1922 }
1923 )));
1924 assert!(keys.iter().any(|key| matches!(
1925 key,
1926 WriteKey::Row {
1927 table_id: 4,
1928 row_id: 1
1929 }
1930 )));
1931 assert!(keys
1932 .iter()
1933 .any(|key| matches!(key, WriteKey::Table { table_id: 5 })));
1934 }
1935
1936 fn test_request(key: &str) -> IdempotencyRequest {
1937 IdempotencyRequest {
1938 key: key.to_string(),
1939 owner: "alice".to_string(),
1940 fingerprint: 42,
1941 ttl: None,
1942 }
1943 }
1944
1945 fn test_commit_ts() -> HlcTimestamp {
1946 HlcTimestamp {
1947 physical_micros: 1_700_000_000_000_000,
1948 logical: 3,
1949 node_tiebreaker: 0,
1950 }
1951 }
1952
1953 #[test]
1954 fn idempotency_ledger_replay_conflict_and_restart() {
1955 let dir = tempfile::tempdir().unwrap();
1956 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
1957 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
1958
1959 let request = test_request("k1");
1960 assert!(matches!(
1961 ledger.check_and_reserve(&request).unwrap(),
1962 IdempotencyCheck::Reserved
1963 ));
1964 assert!(matches!(
1966 ledger.check_and_reserve(&request),
1967 Err(MongrelError::Conflict(_))
1968 ));
1969 ledger
1970 .complete(&request, 7, Epoch(11), test_commit_ts())
1971 .unwrap();
1972
1973 let replay = ledger.check_and_reserve(&request).unwrap();
1975 let IdempotencyCheck::Replay(receipt) = replay else {
1976 panic!("expected replay");
1977 };
1978 assert_eq!(receipt.log_position.index, 11);
1979 assert_eq!(receipt.commit_ts, test_commit_ts());
1980 assert_eq!(
1981 receipt.durability,
1982 mongreldb_log::DurabilityLevel::GroupCommit
1983 );
1984
1985 let mut other = test_request("k1");
1987 other.fingerprint = 43;
1988 assert!(matches!(
1989 ledger.check_and_reserve(&other),
1990 Err(MongrelError::Conflict(_))
1991 ));
1992 let mut foreign = test_request("k1");
1994 foreign.owner = "bob".to_string();
1995 assert!(matches!(
1996 ledger.check_and_reserve(&foreign).unwrap(),
1997 IdempotencyCheck::Reserved
1998 ));
1999 drop(ledger);
2000
2001 let reopened = IdempotencyLedger::open(root, None).unwrap();
2003 let replay = reopened.check_and_reserve(&request).unwrap();
2004 let IdempotencyCheck::Replay(receipt) = replay else {
2005 panic!("expected replay after restart");
2006 };
2007 assert_eq!(receipt.log_position.index, 11);
2008 assert_eq!(receipt.commit_ts, test_commit_ts());
2009 assert!(matches!(
2011 reopened.check_and_reserve(&foreign),
2012 Err(MongrelError::Conflict(_))
2013 ));
2014 }
2015
2016 #[test]
2017 fn idempotency_ledger_release_and_expiry() {
2018 let dir = tempfile::tempdir().unwrap();
2019 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2020 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
2021
2022 let request = test_request("k-release");
2024 assert!(matches!(
2025 ledger.check_and_reserve(&request).unwrap(),
2026 IdempotencyCheck::Reserved
2027 ));
2028 ledger.release(&request);
2029 assert!(matches!(
2030 ledger.check_and_reserve(&request).unwrap(),
2031 IdempotencyCheck::Reserved
2032 ));
2033 ledger
2034 .complete(&request, 9, Epoch(12), test_commit_ts())
2035 .unwrap();
2036
2037 let mut expired = test_request("k-expired");
2040 expired.ttl = Some(Duration::from_nanos(1));
2041 assert!(matches!(
2042 ledger.check_and_reserve(&expired).unwrap(),
2043 IdempotencyCheck::Reserved
2044 ));
2045 ledger
2046 .complete(&expired, 10, Epoch(13), test_commit_ts())
2047 .unwrap();
2048 std::thread::sleep(Duration::from_millis(2));
2049 assert!(matches!(
2050 ledger.check_and_reserve(&expired).unwrap(),
2051 IdempotencyCheck::Reserved
2052 ));
2053
2054 drop(ledger);
2056 let reopened = IdempotencyLedger::open(root, None).unwrap();
2057 assert!(matches!(
2058 reopened.check_and_reserve(&expired).unwrap(),
2059 IdempotencyCheck::Reserved
2060 ));
2061 }
2062
2063 #[test]
2064 fn idempotency_ledger_rejects_empty_key_or_owner() {
2065 let dir = tempfile::tempdir().unwrap();
2066 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2067 let ledger = IdempotencyLedger::open(root, None).unwrap();
2068 let mut request = test_request("");
2069 assert!(matches!(
2070 ledger.check_and_reserve(&request),
2071 Err(MongrelError::InvalidArgument(_))
2072 ));
2073 request.key = "k".to_string();
2074 request.owner.clear();
2075 assert!(matches!(
2076 ledger.check_and_reserve(&request),
2077 Err(MongrelError::InvalidArgument(_))
2078 ));
2079 }
2080
2081 #[test]
2082 fn idempotency_ledger_enforces_bounded_size() {
2083 let mut records: Vec<StoredIdempotencyRecord> = (0..MAX_IDEMPOTENCY_RECORDS + 4)
2084 .map(|index| StoredIdempotencyRecord {
2085 owner: "o".to_string(),
2086 key: format!("k{index}"),
2087 fingerprint: 1,
2088 expires_at_micros: None,
2089 outcome: StoredIdempotencyOutcome::Committed {
2090 txn_id: index as u64,
2091 epoch: index as u64,
2092 commit_ts: test_commit_ts(),
2093 },
2094 })
2095 .collect();
2096 enforce_bounds(&mut records).unwrap();
2097 assert_eq!(records.len(), MAX_IDEMPOTENCY_RECORDS);
2098 assert!(records.iter().all(|record| match &record.outcome {
2100 StoredIdempotencyOutcome::Committed { epoch, .. } => *epoch >= 4,
2101 StoredIdempotencyOutcome::Reserved => false,
2102 }));
2103
2104 let mut reserved: Vec<StoredIdempotencyRecord> = (0..MAX_IDEMPOTENCY_RECORDS + 1)
2107 .map(|index| StoredIdempotencyRecord {
2108 owner: "o".to_string(),
2109 key: format!("r{index}"),
2110 fingerprint: 1,
2111 expires_at_micros: None,
2112 outcome: StoredIdempotencyOutcome::Reserved,
2113 })
2114 .collect();
2115 assert!(matches!(
2116 enforce_bounds(&mut reserved),
2117 Err(MongrelError::ResourceLimitExceeded { .. })
2118 ));
2119 }
2120
2121 #[test]
2122 fn idempotency_ledger_tampered_file_fails_closed() {
2123 let dir = tempfile::tempdir().unwrap();
2124 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2125 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
2126 let request = test_request("k1");
2127 ledger.check_and_reserve(&request).unwrap();
2128 drop(ledger);
2129
2130 let path = dir.path().join(IDEMPOTENCY_FILENAME);
2131 let mut bytes = std::fs::read(&path).unwrap();
2132 let last = bytes.len() - 1;
2133 bytes[last] ^= 0xFF;
2134 std::fs::write(&path, bytes).unwrap();
2135 assert!(IdempotencyLedger::open(root, None).is_err());
2136 }
2137}
2138
2139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2159pub enum IsolationLevel {
2160 #[default]
2161 RepeatableRead,
2162 #[deprecated(
2163 note = "renamed to `RepeatableRead` (spec §10.2 S1B-002); identical semantics, kept for compatibility"
2164 )]
2165 Snapshot,
2166 ReadCommitted,
2167 Serializable,
2168}
2169
2170impl IsolationLevel {
2171 pub fn canonical(self) -> Self {
2174 match self {
2175 #[allow(deprecated)]
2176 Self::Snapshot => Self::RepeatableRead,
2177 other => other,
2178 }
2179 }
2180}
2181
2182use std::time::Duration;
2185
2186#[derive(Debug, Clone)]
2189pub struct IdempotencyRequest {
2190 pub key: String,
2192 pub owner: String,
2194 pub fingerprint: u64,
2198 pub ttl: Option<Duration>,
2201}
2202
2203pub const IDEMPOTENCY_FILENAME: &str = "TXN_IDEMPOTENCY";
2207
2208const IDEMPOTENCY_FORMAT_VERSION: u16 = 1;
2209const IDEMPOTENCY_MAGIC: &[u8; 8] = b"MONGRTXI";
2210const MAX_IDEMPOTENCY_RECORDS: usize = 65_536;
2213const MAX_IDEMPOTENCY_BYTES: u64 = 64 * 1024 * 1024;
2215
2216#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2218enum StoredIdempotencyOutcome {
2219 Reserved,
2222 Committed {
2224 txn_id: u64,
2225 epoch: u64,
2226 commit_ts: HlcTimestamp,
2227 },
2228}
2229
2230#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2231struct StoredIdempotencyRecord {
2232 owner: String,
2233 key: String,
2234 fingerprint: u64,
2235 expires_at_micros: Option<u64>,
2236 outcome: StoredIdempotencyOutcome,
2237}
2238
2239#[derive(serde::Serialize, serde::Deserialize)]
2240struct IdempotencyEnvelope {
2241 format_version: u16,
2242 records: Vec<StoredIdempotencyRecord>,
2243}
2244
2245pub(crate) enum IdempotencyCheck {
2247 Reserved,
2250 Replay(mongreldb_log::CommitReceipt),
2253}
2254
2255pub(crate) struct IdempotencyLedger {
2261 root: std::sync::Arc<crate::durable_file::DurableRoot>,
2262 meta_dek: Option<[u8; crate::catalog::META_DEK_LEN]>,
2263 inner: PlMutex<Vec<StoredIdempotencyRecord>>,
2264}
2265
2266impl IdempotencyLedger {
2267 pub(crate) fn open(
2272 root: std::sync::Arc<crate::durable_file::DurableRoot>,
2273 meta_dek: Option<[u8; crate::catalog::META_DEK_LEN]>,
2274 ) -> Result<Self> {
2275 let mut inner = read_idempotency_file(&root, meta_dek.as_ref())?.unwrap_or_default();
2276 sweep_expired(&mut inner, wall_micros());
2277 Ok(Self {
2278 root,
2279 meta_dek,
2280 inner: PlMutex::new(inner),
2281 })
2282 }
2283
2284 pub(crate) fn check_and_reserve(
2289 &self,
2290 request: &IdempotencyRequest,
2291 ) -> Result<IdempotencyCheck> {
2292 validate_request(request)?;
2293 let now = wall_micros();
2294 let mut inner = self.inner.lock();
2295 sweep_expired(&mut inner, now);
2296 if let Some(existing) = inner
2297 .iter()
2298 .find(|record| record.owner == request.owner && record.key == request.key)
2299 {
2300 if existing.fingerprint != request.fingerprint {
2301 return Err(MongrelError::Conflict(format!(
2302 "idempotency key {:?} was already used with a different request",
2303 request.key
2304 )));
2305 }
2306 return match &existing.outcome {
2307 StoredIdempotencyOutcome::Committed {
2308 txn_id,
2309 epoch,
2310 commit_ts,
2311 } => Ok(IdempotencyCheck::Replay(mongreldb_log::CommitReceipt {
2312 transaction_id: crate::commit_log::transaction_id_from_txn(*txn_id),
2313 commit_ts: *commit_ts,
2314 log_position: mongreldb_log::LogPosition {
2315 term: 0,
2316 index: *epoch,
2317 },
2318 durability: mongreldb_log::DurabilityLevel::GroupCommit,
2319 })),
2320 StoredIdempotencyOutcome::Reserved => Err(MongrelError::Conflict(format!(
2321 "idempotency key {:?} has an in-flight or interrupted commit; retry to resolve",
2322 request.key
2323 ))),
2324 };
2325 }
2326 inner.push(StoredIdempotencyRecord {
2327 owner: request.owner.clone(),
2328 key: request.key.clone(),
2329 fingerprint: request.fingerprint,
2330 expires_at_micros: expiry_micros(request.ttl, now),
2331 outcome: StoredIdempotencyOutcome::Reserved,
2332 });
2333 persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner)?;
2334 Ok(IdempotencyCheck::Reserved)
2335 }
2336
2337 pub(crate) fn complete(
2343 &self,
2344 request: &IdempotencyRequest,
2345 txn_id: u64,
2346 epoch: Epoch,
2347 commit_ts: HlcTimestamp,
2348 ) -> Result<()> {
2349 let mut inner = self.inner.lock();
2350 let now = wall_micros();
2351 let Some(record) = inner
2352 .iter_mut()
2353 .find(|record| record.owner == request.owner && record.key == request.key)
2354 else {
2355 return Err(MongrelError::Other(format!(
2356 "idempotency reservation for key {:?} vanished during commit",
2357 request.key
2358 )));
2359 };
2360 record.expires_at_micros = expiry_micros(request.ttl, now);
2361 record.outcome = StoredIdempotencyOutcome::Committed {
2362 txn_id,
2363 epoch: epoch.0,
2364 commit_ts,
2365 };
2366 persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner)
2367 }
2368
2369 pub(crate) fn release(&self, request: &IdempotencyRequest) {
2373 let mut inner = self.inner.lock();
2374 inner.retain(|record| {
2375 !(record.owner == request.owner
2376 && record.key == request.key
2377 && matches!(record.outcome, StoredIdempotencyOutcome::Reserved))
2378 });
2379 let _ = persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner);
2380 }
2381}
2382
2383pub(crate) struct IdempotencyReservationGuard<'a> {
2386 ledger: &'a IdempotencyLedger,
2387 request: IdempotencyRequest,
2388 armed: bool,
2389}
2390
2391impl<'a> IdempotencyReservationGuard<'a> {
2392 pub(crate) fn new(ledger: &'a IdempotencyLedger, request: IdempotencyRequest) -> Self {
2393 Self {
2394 ledger,
2395 request,
2396 armed: true,
2397 }
2398 }
2399
2400 pub(crate) fn disarm(&mut self) {
2401 self.armed = false;
2402 }
2403}
2404
2405impl Drop for IdempotencyReservationGuard<'_> {
2406 fn drop(&mut self) {
2407 if self.armed {
2408 self.ledger.release(&self.request);
2409 }
2410 }
2411}
2412
2413fn validate_request(request: &IdempotencyRequest) -> Result<()> {
2414 if request.key.is_empty() {
2415 return Err(MongrelError::InvalidArgument(
2416 "idempotency key must not be empty".into(),
2417 ));
2418 }
2419 if request.owner.is_empty() {
2420 return Err(MongrelError::InvalidArgument(
2421 "idempotency owner must not be empty".into(),
2422 ));
2423 }
2424 Ok(())
2425}
2426
2427fn expiry_micros(ttl: Option<Duration>, now_micros: u64) -> Option<u64> {
2428 ttl.map(|ttl| now_micros.saturating_add(u64::try_from(ttl.as_micros()).unwrap_or(u64::MAX)))
2429}
2430
2431fn sweep_expired(records: &mut Vec<StoredIdempotencyRecord>, now_micros: u64) {
2432 records.retain(|record| {
2433 record
2434 .expires_at_micros
2435 .is_none_or(|expires_at| expires_at > now_micros)
2436 });
2437}
2438
2439fn enforce_bounds(records: &mut Vec<StoredIdempotencyRecord>) -> Result<()> {
2443 while records.len() > MAX_IDEMPOTENCY_RECORDS {
2444 let Some((oldest, _)) = records
2445 .iter()
2446 .enumerate()
2447 .filter(|(_, record)| {
2448 matches!(record.outcome, StoredIdempotencyOutcome::Committed { .. })
2449 })
2450 .min_by_key(|(_, record)| match &record.outcome {
2451 StoredIdempotencyOutcome::Committed { epoch, .. } => *epoch,
2452 StoredIdempotencyOutcome::Reserved => unreachable!(),
2453 })
2454 else {
2455 return Err(MongrelError::ResourceLimitExceeded {
2456 resource: "idempotency records",
2457 requested: records.len(),
2458 limit: MAX_IDEMPOTENCY_RECORDS,
2459 });
2460 };
2461 records.remove(oldest);
2462 }
2463 Ok(())
2464}
2465
2466fn persist_locked(
2467 root: &crate::durable_file::DurableRoot,
2468 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2469 records: &mut Vec<StoredIdempotencyRecord>,
2470) -> Result<()> {
2471 enforce_bounds(records)?;
2472 let body = serde_json::to_vec(&IdempotencyEnvelope {
2473 format_version: IDEMPOTENCY_FORMAT_VERSION,
2474 records: records.clone(),
2475 })
2476 .map_err(|error| MongrelError::Other(format!("idempotency ledger serialize: {error}")))?;
2477 let payload = seal_idempotency(&body, meta_dek)?;
2478 root.write_atomic(IDEMPOTENCY_FILENAME, &payload)?;
2479 Ok(())
2480}
2481
2482fn read_idempotency_file(
2483 root: &crate::durable_file::DurableRoot,
2484 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2485) -> Result<Option<Vec<StoredIdempotencyRecord>>> {
2486 use std::io::Read as _;
2487 let file = match root.open_regular(IDEMPOTENCY_FILENAME) {
2488 Ok(file) => file,
2489 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2490 Err(error) => return Err(error.into()),
2491 };
2492 let length = file.metadata()?.len();
2493 if length > MAX_IDEMPOTENCY_BYTES {
2494 return Err(MongrelError::Other(format!(
2495 "idempotency ledger of {length} bytes exceeds the {MAX_IDEMPOTENCY_BYTES}-byte limit"
2496 )));
2497 }
2498 let mut bytes = Vec::with_capacity(length as usize);
2499 file.take(MAX_IDEMPOTENCY_BYTES + 1)
2500 .read_to_end(&mut bytes)?;
2501 if bytes.len() as u64 != length {
2502 return Err(MongrelError::Other(
2503 "idempotency ledger length changed while reading".into(),
2504 ));
2505 }
2506 open_idempotency_payload(&bytes, meta_dek).map(Some)
2507}
2508
2509fn decode_idempotency(body: &[u8]) -> Result<Vec<StoredIdempotencyRecord>> {
2510 let envelope: IdempotencyEnvelope = serde_json::from_slice(body)
2511 .map_err(|error| MongrelError::Other(format!("idempotency ledger deserialize: {error}")))?;
2512 if envelope.format_version != IDEMPOTENCY_FORMAT_VERSION {
2513 return Err(MongrelError::Other(format!(
2514 "unsupported idempotency ledger format version {}",
2515 envelope.format_version
2516 )));
2517 }
2518 Ok(envelope.records)
2519}
2520
2521fn plaintext_idempotency_frame(body: &[u8]) -> Vec<u8> {
2522 use sha2::Digest as _;
2523 let hash = sha2::Sha256::digest(body);
2524 let mut out = Vec::with_capacity(body.len() + 8 + 32);
2525 out.extend_from_slice(IDEMPOTENCY_MAGIC);
2526 out.extend_from_slice(&hash);
2527 out.extend_from_slice(body);
2528 out
2529}
2530
2531fn parse_idempotency_plaintext(bytes: &[u8]) -> Result<Vec<StoredIdempotencyRecord>> {
2532 use sha2::Digest as _;
2533 if bytes.len() < 8 + 32 || &bytes[..8] != IDEMPOTENCY_MAGIC {
2534 return Err(MongrelError::Other(
2535 "idempotency ledger magic mismatch (corrupt or sealed with a key)".into(),
2536 ));
2537 }
2538 let (tag, body) = bytes[8..].split_at(32);
2539 let calc = sha2::Sha256::digest(body);
2540 if tag != calc.as_slice() {
2541 return Err(MongrelError::Other(
2542 "idempotency ledger checksum mismatch (tampered or torn)".into(),
2543 ));
2544 }
2545 decode_idempotency(body)
2546}
2547
2548fn seal_idempotency(
2549 body: &[u8],
2550 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2551) -> Result<Vec<u8>> {
2552 match meta_dek {
2553 Some(dek) => crate::encryption::encrypt_blob(dek, body),
2554 None => Ok(plaintext_idempotency_frame(body)),
2555 }
2556}
2557
2558fn open_idempotency_payload(
2559 bytes: &[u8],
2560 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2561) -> Result<Vec<StoredIdempotencyRecord>> {
2562 match meta_dek {
2563 Some(dek) => {
2566 let body = crate::encryption::decrypt_blob(dek, bytes).map_err(|_| {
2567 MongrelError::Decryption(
2568 "idempotency ledger authentication failed (wrong key or tampered)".into(),
2569 )
2570 })?;
2571 decode_idempotency(&body)
2572 }
2573 None => parse_idempotency_plaintext(bytes),
2574 }
2575}
2576
2577fn wall_micros() -> u64 {
2579 let micros = std::time::SystemTime::now()
2580 .duration_since(std::time::UNIX_EPOCH)
2581 .map(|duration| duration.as_micros())
2582 .unwrap_or(0);
2583 u64::try_from(micros).unwrap_or(u64::MAX)
2584}