1use crate::database::{Database, ExternalTriggerBridge};
12use crate::epoch::{Epoch, Snapshot};
13use crate::error::{MongrelError, Result};
14use crate::memtable::Value;
15use crate::rowid::RowId;
16use crate::wal::SharedWal;
17use parking_lot::{Condvar, Mutex as PlMutex};
18
19pub(crate) enum Staged {
21 Put(Vec<(u16, Value)>),
22 Delete(RowId),
23 Update(RowId, Vec<(u16, Value)>),
27 Truncate,
28}
29
30#[derive(Debug, Clone)]
31pub struct OwnedRow {
32 pub columns: Vec<(u16, Value)>,
33}
34
35#[derive(Debug, Clone)]
36pub struct PutResult {
37 pub auto_inc: Option<i64>,
38 pub row: OwnedRow,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum UpsertActionKind {
43 Inserted,
44 Updated,
45 Unchanged,
46}
47
48#[derive(Debug, Clone)]
49pub enum UpsertAction {
50 DoNothing,
51 DoUpdate(Vec<(u16, Value)>),
52}
53
54#[derive(Debug, Clone)]
55pub struct UpsertResult {
56 pub action: UpsertActionKind,
57 pub row: OwnedRow,
58 pub auto_inc: Option<i64>,
59}
60
61pub struct Transaction<'db> {
64 db: &'db Database,
65 txn_id: u64,
66 read: Snapshot,
67 staging: Vec<(u64 , Staged)>,
68 external_states: Vec<(String, Vec<u8>)>,
69 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
70 principal: Option<crate::auth::Principal>,
71 principal_catalog_bound: bool,
72 external_trigger_bridge: Option<&'db dyn ExternalTriggerBridge>,
73 _active: Option<ActiveTxnGuard<'db>>,
74}
75
76impl<'db> Transaction<'db> {
77 pub(crate) fn new(db: &'db Database, txn_id: u64, read: Snapshot) -> Self {
78 let guard = db.register_active(read.epoch);
79 Self {
80 db,
81 txn_id,
82 read,
83 staging: Vec::new(),
84 external_states: Vec::new(),
85 materialized_view_updates: Vec::new(),
86 principal: None,
87 principal_catalog_bound: false,
88 external_trigger_bridge: None,
89 _active: Some(guard),
90 }
91 }
92
93 pub(crate) fn with_external_trigger_bridge(
94 mut self,
95 bridge: &'db dyn ExternalTriggerBridge,
96 ) -> Self {
97 self.external_trigger_bridge = Some(bridge);
98 self
99 }
100
101 pub(crate) fn with_principal(
102 mut self,
103 principal: Option<crate::auth::Principal>,
104 catalog_bound: bool,
105 ) -> Self {
106 self.principal = principal;
107 self.principal_catalog_bound = catalog_bound;
108 self
109 }
110
111 pub fn read_snapshot(&self) -> Snapshot {
112 self.read
113 }
114
115 pub fn txn_id(&self) -> u64 {
118 self.txn_id
119 }
120
121 pub fn put(&mut self, table: &str, mut cells: Vec<(u16, Value)>) -> Result<Option<i64>> {
128 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
129 let id = self.db.table_id(table)?;
130 let handle = self.db.table(table)?;
131 let mut t = handle.lock();
132 let assigned = t.fill_auto_inc(&mut cells)?;
133 t.apply_defaults(&mut cells)?;
134 drop(t);
135 self.staging.push((id, Staged::Put(cells)));
136 Ok(assigned)
137 }
138
139 pub fn put_returning(
140 &mut self,
141 table: &str,
142 mut cells: Vec<(u16, Value)>,
143 ) -> Result<PutResult> {
144 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
145 let id = self.db.table_id(table)?;
146 let handle = self.db.table(table)?;
147 let mut t = handle.lock();
148 let assigned = t.fill_auto_inc(&mut cells)?;
149 t.apply_defaults(&mut cells)?;
150 drop(t);
151 let row = owned_row_from_cells(&cells);
152 self.staging.push((id, Staged::Put(cells)));
153 Ok(PutResult {
154 auto_inc: assigned,
155 row,
156 })
157 }
158
159 pub fn put_batch(
165 &mut self,
166 table: &str,
167 rows: Vec<Vec<(u16, Value)>>,
168 ) -> Result<Vec<Option<i64>>> {
169 for cells in &rows {
170 self.require_columns(table, crate::auth::ColumnOperation::Insert, cells)?;
171 }
172 let id = self.db.table_id(table)?;
173 let handle = self.db.table(table)?;
174 let mut t = handle.lock();
175 let mut assigned = Vec::with_capacity(rows.len());
176 for mut cells in rows {
177 let a = t.fill_auto_inc(&mut cells)?;
178 t.apply_defaults(&mut cells)?;
179 assigned.push(a);
180 self.staging.push((id, Staged::Put(cells)));
181 }
182 drop(t);
183 Ok(assigned)
184 }
185
186 pub fn delete(&mut self, table: &str, row_id: RowId) -> Result<()> {
188 self.require_delete(table)?;
189 let id = self.db.table_id(table)?;
190 self.reject_after_truncate(id)?;
191 self.staging.push((id, Staged::Delete(row_id)));
192 Ok(())
193 }
194
195 pub fn put_external_state(&mut self, table: &str, state: Vec<u8>) -> Result<()> {
198 if self.db.external_table(table).is_none() {
199 return Err(MongrelError::NotFound(format!(
200 "external table {table:?} not found"
201 )));
202 }
203 self.external_states.push((table.to_string(), state));
204 Ok(())
205 }
206
207 pub fn set_materialized_view_definition(
211 &mut self,
212 definition: crate::catalog::MaterializedViewEntry,
213 ) -> Result<()> {
214 self.db
215 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
216 if self.db.table_id(&definition.name).is_err() {
217 return Err(MongrelError::NotFound(format!(
218 "materialized view table {:?} not found",
219 definition.name
220 )));
221 }
222 self.materialized_view_updates
223 .retain(|current| current.name != definition.name);
224 self.materialized_view_updates.push(definition);
225 Ok(())
226 }
227
228 pub fn delete_many(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<Vec<OwnedRow>> {
229 self.require_delete(table)?;
230 let id = self.db.table_id(table)?;
231 self.reject_after_truncate(id)?;
232 let snap = self.read;
233 let handle = self.db.table(table)?;
234 let t = handle.lock();
235 let mut pre_images = Vec::with_capacity(row_ids.len());
236 for row_id in &row_ids {
237 if let Some(row) = t.get(*row_id, snap) {
238 pre_images.push(owned_row_from_map(row.columns));
239 }
240 }
241 drop(t);
242 for row_id in row_ids {
243 self.staging.push((id, Staged::Delete(row_id)));
244 }
245 Ok(pre_images)
246 }
247
248 pub fn update_many(
249 &mut self,
250 table: &str,
251 updates: Vec<(RowId, Vec<(u16, Value)>)>,
252 ) -> Result<Vec<OwnedRow>> {
253 for (_, cells) in &updates {
254 self.require_columns(table, crate::auth::ColumnOperation::Update, cells)?;
255 }
256 let id = self.db.table_id(table)?;
257 self.reject_after_truncate(id)?;
258 let snap = self.read;
259 let handle = self.db.table(table)?;
260 let t = handle.lock();
261 let mut post_images = Vec::with_capacity(updates.len());
262 let mut staged = Vec::with_capacity(updates.len());
263 for (old_id, new_cells) in updates {
264 let old_row = t
265 .get(old_id, snap)
266 .ok_or_else(|| MongrelError::NotFound(format!("row {old_id:?} not found")))?;
267 let merged = merge_cells(old_row.columns.into_iter().collect(), new_cells);
268 post_images.push(owned_row_from_cells(&merged));
269 staged.push((id, Staged::Update(old_id, merged)));
270 }
271 drop(t);
272 self.staging.extend(staged);
273 Ok(post_images)
274 }
275
276 pub fn upsert(
277 &mut self,
278 table: &str,
279 mut insert_cells: Vec<(u16, Value)>,
280 action: UpsertAction,
281 ) -> Result<UpsertResult> {
282 self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
286 let id = self.db.table_id(table)?;
287 self.reject_after_truncate(id)?;
288 match (self.existing_pk_row(table, &insert_cells)?, action) {
289 (None, _) => {
290 let handle = self.db.table(table)?;
291 let mut t = handle.lock();
292 let assigned = t.fill_auto_inc(&mut insert_cells)?;
293 t.apply_defaults(&mut insert_cells)?;
294 drop(t);
295 let row = owned_row_from_cells(&insert_cells);
296 self.staging.push((id, Staged::Put(insert_cells)));
297 Ok(UpsertResult {
298 action: UpsertActionKind::Inserted,
299 row,
300 auto_inc: assigned,
301 })
302 }
303 (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
304 action: UpsertActionKind::Unchanged,
305 row: old_row,
306 auto_inc: None,
307 }),
308 (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
309 self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
311 let merged = merge_cells(old_row.columns.clone(), update_cells);
312 if columns_equal(&old_row.columns, &merged) {
313 return Ok(UpsertResult {
314 action: UpsertActionKind::Unchanged,
315 row: old_row,
316 auto_inc: None,
317 });
318 }
319 let row = owned_row_from_cells(&merged);
320 self.staging.push((id, Staged::Update(old_id, merged)));
321 Ok(UpsertResult {
322 action: UpsertActionKind::Updated,
323 row,
324 auto_inc: None,
325 })
326 }
327 }
328 }
329
330 pub fn truncate(&mut self, table: &str) -> Result<()> {
331 self.db
332 .require_for(self.principal.as_ref(), &crate::auth::Permission::Admin)?;
333 let id = self.db.table_id(table)?;
334 for (table_id, op) in &self.staging {
335 if *table_id == id && !matches!(op, Staged::Truncate) {
336 return Err(MongrelError::InvalidArgument(
337 "truncate cannot be combined with other writes on the same table".into(),
338 ));
339 }
340 }
341 self.staging.push((id, Staged::Truncate));
342 Ok(())
343 }
344
345 fn reject_after_truncate(&self, table_id: u64) -> Result<()> {
346 if self
347 .staging
348 .iter()
349 .any(|(tid, op)| *tid == table_id && matches!(op, Staged::Truncate))
350 {
351 return Err(MongrelError::InvalidArgument(
352 "truncate cannot be combined with other writes on the same table".into(),
353 ));
354 }
355 Ok(())
356 }
357
358 fn require_columns(
359 &self,
360 table: &str,
361 operation: crate::auth::ColumnOperation,
362 cells: &[(u16, Value)],
363 ) -> Result<()> {
364 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
365 self.db
366 .require_columns_for(table, operation, &columns, self.principal.as_ref())
367 }
368
369 fn require_delete(&self, table: &str) -> Result<()> {
370 self.db.require_for(
371 self.principal.as_ref(),
372 &crate::auth::Permission::Delete {
373 table: table.to_string(),
374 },
375 )
376 }
377
378 fn existing_pk_row(
379 &self,
380 table: &str,
381 cells: &[(u16, Value)],
382 ) -> Result<Option<(RowId, OwnedRow)>> {
383 let handle = self.db.table(table)?;
384 let t = handle.lock();
385 let Some(pk_col) = t.schema().primary_key() else {
386 return Ok(None);
387 };
388 let Some((_, pk_value)) = cells.iter().find(|(id, _)| *id == pk_col.id) else {
389 return Ok(None);
390 };
391 if matches!(pk_value, Value::Null) {
392 return Ok(None);
393 }
394 let Some(row_id) = t.lookup_pk(&pk_value.encode_key()) else {
395 return Ok(None);
396 };
397 Ok(t.get(row_id, self.read)
398 .map(|row| (row_id, owned_row_from_map(row.columns))))
399 }
400
401 pub fn commit(self) -> Result<Epoch> {
403 self.db
404 .commit_transaction_with_external_states(
405 self.txn_id,
406 self.read.epoch,
407 self.staging,
408 self.external_states,
409 self.materialized_view_updates,
410 self.principal,
411 self.principal_catalog_bound,
412 self.external_trigger_bridge,
413 )
414 .map(|(epoch, _)| epoch)
415 }
416
417 pub fn commit_with_row_ids(self) -> Result<(Epoch, Vec<RowId>)> {
418 self.db.commit_transaction_with_external_states(
419 self.txn_id,
420 self.read.epoch,
421 self.staging,
422 self.external_states,
423 self.materialized_view_updates,
424 self.principal,
425 self.principal_catalog_bound,
426 self.external_trigger_bridge,
427 )
428 }
429
430 pub fn rollback(self) {
432 }
434}
435
436fn owned_row_from_cells(cells: &[(u16, Value)]) -> OwnedRow {
437 let mut columns = cells.to_vec();
438 columns.sort_by_key(|(id, _)| *id);
439 OwnedRow { columns }
440}
441
442fn owned_row_from_map(columns: HashMap<u16, Value>) -> OwnedRow {
443 let mut columns: Vec<(u16, Value)> = columns.into_iter().collect();
444 columns.sort_by_key(|(id, _)| *id);
445 OwnedRow { columns }
446}
447
448fn merge_cells(mut base: Vec<(u16, Value)>, updates: Vec<(u16, Value)>) -> Vec<(u16, Value)> {
449 for (id, value) in updates {
450 base.retain(|(existing, _)| *existing != id);
451 base.push((id, value));
452 }
453 base.sort_by_key(|(id, _)| *id);
454 base
455}
456
457fn columns_equal(a: &[(u16, Value)], b: &[(u16, Value)]) -> bool {
458 if a.len() != b.len() {
459 return false;
460 }
461 let mut a: Vec<_> = a.iter().collect();
462 let mut b: Vec<_> = b.iter().collect();
463 a.sort_by_key(|(id, _)| *id);
464 b.sort_by_key(|(id, _)| *id);
465 a.iter()
466 .zip(b.iter())
467 .all(|((id_a, v_a), (id_b, v_b))| id_a == id_b && v_a == v_b)
468}
469
470pub(crate) enum StagedOp {
472 Put(crate::memtable::Row),
473 Delete(RowId),
474 Truncate,
475}
476
477use std::collections::{BTreeMap, HashMap};
480use std::hash::{Hash, Hasher};
481
482#[derive(Clone, Debug)]
485pub enum WriteKey {
486 Row { table_id: u64, row_id: u64 },
488 Unique {
490 table_id: u64,
491 index_id: u16,
492 key_hash: u64,
493 },
494 Table { table_id: u64 },
496}
497
498impl Hash for WriteKey {
499 fn hash<H: Hasher>(&self, state: &mut H) {
500 match self {
501 WriteKey::Row { table_id, row_id } => {
502 0u8.hash(state);
503 table_id.hash(state);
504 row_id.hash(state);
505 }
506 WriteKey::Unique {
507 table_id,
508 index_id,
509 key_hash,
510 } => {
511 1u8.hash(state);
512 table_id.hash(state);
513 index_id.hash(state);
514 key_hash.hash(state);
515 }
516 WriteKey::Table { table_id } => {
517 2u8.hash(state);
518 table_id.hash(state);
519 }
520 }
521 }
522}
523
524impl PartialEq for WriteKey {
525 fn eq(&self, other: &Self) -> bool {
526 match (self, other) {
527 (
528 WriteKey::Row {
529 table_id: a,
530 row_id: b,
531 },
532 WriteKey::Row {
533 table_id: c,
534 row_id: d,
535 },
536 ) => a == c && b == d,
537 (
538 WriteKey::Unique {
539 table_id: a,
540 index_id: b,
541 key_hash: c,
542 },
543 WriteKey::Unique {
544 table_id: d,
545 index_id: e,
546 key_hash: f,
547 },
548 ) => a == d && b == e && c == f,
549 (WriteKey::Table { table_id: a }, WriteKey::Table { table_id: b }) => a == b,
550 _ => false,
551 }
552 }
553}
554
555impl Eq for WriteKey {}
556
557const CONFLICT_SHARDS: usize = 16;
558
559pub struct ConflictIndex {
563 shards: [parking_lot::Mutex<HashMap<WriteKey, u64>>; CONFLICT_SHARDS],
564 table_truncate_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
565 table_write_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
566 version: std::sync::atomic::AtomicU64,
570}
571
572impl ConflictIndex {
573 pub fn new() -> Self {
574 Self {
575 shards: std::array::from_fn(|_| parking_lot::Mutex::new(HashMap::new())),
576 table_truncate_epochs: parking_lot::Mutex::new(HashMap::new()),
577 table_write_epochs: parking_lot::Mutex::new(HashMap::new()),
578 version: std::sync::atomic::AtomicU64::new(0),
579 }
580 }
581
582 pub fn version(&self) -> u64 {
586 self.version.load(std::sync::atomic::Ordering::Acquire)
587 }
588
589 fn shard(&self, key: &WriteKey) -> &parking_lot::Mutex<HashMap<WriteKey, u64>> {
590 let mut h = std::collections::hash_map::DefaultHasher::new();
591 key.hash(&mut h);
592 let idx = (h.finish() as usize) & (CONFLICT_SHARDS - 1);
593 &self.shards[idx]
594 }
595
596 pub fn conflicts(&self, keys: &[WriteKey], read_epoch: Epoch) -> bool {
599 for k in keys {
600 let s = self.shard(k);
601 if let Some(&ce) = s.lock().get(k) {
602 if ce > read_epoch.0 {
603 return true;
604 }
605 }
606 }
607 let truncates = self.table_truncate_epochs.lock();
608 let writes = self.table_write_epochs.lock();
609 for k in keys {
610 match k {
611 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
612 if truncates.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
613 return true;
614 }
615 }
616 WriteKey::Table { table_id } => {
617 if writes.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
618 return true;
619 }
620 }
621 }
622 }
623 false
624 }
625
626 pub fn record(&self, keys: &[WriteKey], commit_epoch: Epoch) {
628 for k in keys {
629 let s = self.shard(k);
630 s.lock().insert(k.clone(), commit_epoch.0);
631 }
632 let mut truncates = self.table_truncate_epochs.lock();
633 let mut writes = self.table_write_epochs.lock();
634 for k in keys {
635 match k {
636 WriteKey::Table { table_id } => {
637 truncates
638 .entry(*table_id)
639 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
640 .or_insert(commit_epoch.0);
641 }
642 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
643 writes
644 .entry(*table_id)
645 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
646 .or_insert(commit_epoch.0);
647 }
648 }
649 }
650 self.version
651 .fetch_add(1, std::sync::atomic::Ordering::Release);
652 }
653
654 pub fn prune_below(&self, min_active: Epoch) {
657 for s in &self.shards {
658 s.lock().retain(|_, ce| *ce >= min_active.0);
659 }
660 self.table_truncate_epochs
661 .lock()
662 .retain(|_, ce| *ce >= min_active.0);
663 self.table_write_epochs
664 .lock()
665 .retain(|_, ce| *ce >= min_active.0);
666 }
667}
668
669impl Default for ConflictIndex {
670 fn default() -> Self {
671 Self::new()
672 }
673}
674
675pub struct GroupCommit {
685 inner: PlMutex<GroupState>,
686 cv: Condvar,
687}
688
689struct GroupState {
690 durable_seq: u64,
691 syncing: bool,
692 poisoned: bool,
693}
694
695impl GroupCommit {
696 pub fn new(durable_seq: u64) -> Self {
697 Self {
698 inner: PlMutex::new(GroupState {
699 durable_seq,
700 syncing: false,
701 poisoned: false,
702 }),
703 cv: Condvar::new(),
704 }
705 }
706
707 pub fn await_durable(&self, wal: &PlMutex<SharedWal>, commit_seq: u64) -> Result<()> {
713 let mut st = self.inner.lock();
714 loop {
715 if st.poisoned {
716 return Err(MongrelError::Other(
717 "database poisoned by fsync error".into(),
718 ));
719 }
720 if st.durable_seq >= commit_seq {
721 return Ok(());
722 }
723 if st.syncing {
724 self.cv.wait(&mut st);
726 continue;
727 }
728 st.syncing = true;
731 drop(st);
732 std::thread::sleep(std::time::Duration::from_micros(50));
734 let res = wal.lock().group_sync();
735 st = self.inner.lock();
736 st.syncing = false;
737 match res {
738 Ok(durable) => {
739 if durable > st.durable_seq {
740 st.durable_seq = durable;
741 }
742 self.cv.notify_all();
743 }
746 Err(e) => {
747 st.poisoned = true;
748 self.cv.notify_all();
749 return Err(e);
750 }
751 }
752 }
753 }
754}
755
756pub struct ActiveTxns {
760 inner: parking_lot::Mutex<BTreeMap<u64, u64>>,
761}
762
763impl ActiveTxns {
764 pub fn new() -> Self {
765 Self {
766 inner: parking_lot::Mutex::new(BTreeMap::new()),
767 }
768 }
769
770 pub fn register(&self, read_epoch: Epoch) -> ActiveTxnGuard<'_> {
773 let mut g = self.inner.lock();
774 *g.entry(read_epoch.0).or_insert(0) += 1;
775 ActiveTxnGuard {
776 active: self,
777 epoch: read_epoch.0,
778 }
779 }
780
781 pub fn min_read_epoch(&self) -> u64 {
783 self.inner.lock().keys().next().copied().unwrap_or(u64::MAX)
784 }
785}
786
787impl Default for ActiveTxns {
788 fn default() -> Self {
789 Self::new()
790 }
791}
792
793pub struct ActiveTxnGuard<'a> {
795 active: &'a ActiveTxns,
796 epoch: u64,
797}
798
799impl Drop for ActiveTxnGuard<'_> {
800 fn drop(&mut self) {
801 let mut g = self.active.inner.lock();
802 if let Some(count) = g.get_mut(&self.epoch) {
803 *count -= 1;
804 if *count == 0 {
805 g.remove(&self.epoch);
806 }
807 }
808 }
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814
815 #[test]
816 fn conflict_index_first_committer_wins_and_prunes_safely() {
817 let ci = ConflictIndex::new();
818 let k = vec![WriteKey::Row {
819 table_id: 1,
820 row_id: 7,
821 }];
822 assert!(!ci.conflicts(&k, Epoch(5)));
823 ci.record(&k, Epoch(6));
824 assert!(ci.conflicts(&k, Epoch(5)));
825 assert!(!ci.conflicts(&k, Epoch(6)));
826 ci.prune_below(Epoch(7));
827 assert!(!ci.conflicts(&k, Epoch(5)));
828 }
829
830 #[test]
831 fn conflict_index_table_scope_conflicts_both_directions() {
832 let ci = ConflictIndex::new();
833 ci.record(&[WriteKey::Table { table_id: 1 }], Epoch(6));
834 assert!(ci.conflicts(
835 &[WriteKey::Row {
836 table_id: 1,
837 row_id: 7,
838 }],
839 Epoch(5)
840 ));
841 assert!(ci.conflicts(
842 &[WriteKey::Unique {
843 table_id: 1,
844 index_id: 0,
845 key_hash: 42,
846 }],
847 Epoch(5)
848 ));
849 assert!(!ci.conflicts(
850 &[WriteKey::Row {
851 table_id: 2,
852 row_id: 7,
853 }],
854 Epoch(5)
855 ));
856
857 let ci = ConflictIndex::new();
858 ci.record(
859 &[WriteKey::Row {
860 table_id: 1,
861 row_id: 7,
862 }],
863 Epoch(6),
864 );
865 assert!(ci.conflicts(&[WriteKey::Table { table_id: 1 }], Epoch(5)));
866 assert!(!ci.conflicts(&[WriteKey::Table { table_id: 2 }], Epoch(5)));
867 }
868
869 #[test]
870 fn writekey_eq_across_variants() {
871 let r1 = WriteKey::Row {
872 table_id: 1,
873 row_id: 2,
874 };
875 let r2 = WriteKey::Row {
876 table_id: 1,
877 row_id: 2,
878 };
879 let r3 = WriteKey::Row {
880 table_id: 1,
881 row_id: 3,
882 };
883 assert_eq!(r1, r2);
884 assert_ne!(r1, r3);
885
886 let u1 = WriteKey::Unique {
887 table_id: 1,
888 index_id: 0,
889 key_hash: 42,
890 };
891 let u2 = WriteKey::Unique {
892 table_id: 1,
893 index_id: 0,
894 key_hash: 42,
895 };
896 assert_eq!(u1, u2);
897 assert_ne!(r1, u1);
898
899 let t1 = WriteKey::Table { table_id: 5 };
900 let t2 = WriteKey::Table { table_id: 5 };
901 assert_eq!(t1, t2);
902 assert_ne!(t1, r1);
903 }
904
905 #[test]
906 fn active_txns_tracks_min_read_epoch() {
907 let at = ActiveTxns::new();
908 assert_eq!(at.min_read_epoch(), u64::MAX);
909 let g1 = at.register(Epoch(5));
910 assert_eq!(at.min_read_epoch(), 5);
911 let g2 = at.register(Epoch(3));
912 assert_eq!(at.min_read_epoch(), 3);
913 drop(g2);
914 assert_eq!(at.min_read_epoch(), 5);
915 drop(g1);
916 assert_eq!(at.min_read_epoch(), u64::MAX);
917 }
918
919 #[test]
920 fn active_txns_dedups_same_epoch() {
921 let at = ActiveTxns::new();
922 let g1 = at.register(Epoch(7));
923 let g2 = at.register(Epoch(7));
924 assert_eq!(at.min_read_epoch(), 7);
925 drop(g1);
926 assert_eq!(at.min_read_epoch(), 7);
927 drop(g2);
928 assert_eq!(at.min_read_epoch(), u64::MAX);
929 }
930}
931
932#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
943pub enum IsolationLevel {
944 #[default]
945 Snapshot,
946 ReadCommitted,
947 Serializable,
948}