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 if !rows.is_empty() {
170 let mut columns = rows
171 .iter()
172 .flat_map(|cells| cells.iter().map(|(column, _)| *column))
173 .collect::<Vec<_>>();
174 columns.sort_unstable();
175 columns.dedup();
176 self.db.require_columns_for(
177 table,
178 crate::auth::ColumnOperation::Insert,
179 &columns,
180 self.principal.as_ref(),
181 )?;
182 }
183 let id = self.db.table_id(table)?;
184 let handle = self.db.table(table)?;
185 let mut t = handle.lock();
186 let mut assigned = Vec::with_capacity(rows.len());
187 for mut cells in rows {
188 let a = t.fill_auto_inc(&mut cells)?;
189 t.apply_defaults(&mut cells)?;
190 assigned.push(a);
191 self.staging.push((id, Staged::Put(cells)));
192 }
193 drop(t);
194 Ok(assigned)
195 }
196
197 pub fn delete(&mut self, table: &str, row_id: RowId) -> Result<()> {
199 self.delete_batch(table, vec![row_id])
200 }
201
202 pub fn delete_batch(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<()> {
204 self.require_delete(table)?;
205 let id = self.db.table_id(table)?;
206 self.reject_after_truncate(id)?;
207 self.staging.extend(
208 row_ids
209 .into_iter()
210 .map(|row_id| (id, Staged::Delete(row_id))),
211 );
212 Ok(())
213 }
214
215 pub fn put_external_state(&mut self, table: &str, state: Vec<u8>) -> Result<()> {
218 if self.db.external_table(table).is_none() {
219 return Err(MongrelError::NotFound(format!(
220 "external table {table:?} not found"
221 )));
222 }
223 self.external_states.push((table.to_string(), state));
224 Ok(())
225 }
226
227 pub fn set_materialized_view_definition(
231 &mut self,
232 definition: crate::catalog::MaterializedViewEntry,
233 ) -> Result<()> {
234 self.db
235 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
236 if self.db.table_id(&definition.name).is_err() {
237 return Err(MongrelError::NotFound(format!(
238 "materialized view table {:?} not found",
239 definition.name
240 )));
241 }
242 self.materialized_view_updates
243 .retain(|current| current.name != definition.name);
244 self.materialized_view_updates.push(definition);
245 Ok(())
246 }
247
248 pub fn delete_many(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<Vec<OwnedRow>> {
249 self.require_delete(table)?;
250 let id = self.db.table_id(table)?;
251 self.reject_after_truncate(id)?;
252 let snap = self.read;
253 let handle = self.db.table(table)?;
254 let t = handle.lock();
255 let mut pre_images = Vec::with_capacity(row_ids.len());
256 for row_id in &row_ids {
257 if let Some(row) = t.get(*row_id, snap) {
258 pre_images.push(owned_row_from_map(row.columns));
259 }
260 }
261 drop(t);
262 for row_id in row_ids {
263 self.staging.push((id, Staged::Delete(row_id)));
264 }
265 Ok(pre_images)
266 }
267
268 pub fn update_many(
269 &mut self,
270 table: &str,
271 updates: Vec<(RowId, Vec<(u16, Value)>)>,
272 ) -> Result<Vec<OwnedRow>> {
273 if !updates.is_empty() {
274 let mut columns = updates
275 .iter()
276 .flat_map(|(_, cells)| cells.iter().map(|(column, _)| *column))
277 .collect::<Vec<_>>();
278 columns.sort_unstable();
279 columns.dedup();
280 self.db.require_columns_for(
281 table,
282 crate::auth::ColumnOperation::Update,
283 &columns,
284 self.principal.as_ref(),
285 )?;
286 }
287 let id = self.db.table_id(table)?;
288 self.reject_after_truncate(id)?;
289 let snap = self.read;
290 let handle = self.db.table(table)?;
291 let t = handle.lock();
292 let mut post_images = Vec::with_capacity(updates.len());
293 let mut staged = Vec::with_capacity(updates.len());
294 for (old_id, new_cells) in updates {
295 let old_row = t
296 .get(old_id, snap)
297 .ok_or_else(|| MongrelError::NotFound(format!("row {old_id:?} not found")))?;
298 let merged = merge_cells(old_row.columns.into_iter().collect(), new_cells);
299 post_images.push(owned_row_from_cells(&merged));
300 staged.push((id, Staged::Update(old_id, merged)));
301 }
302 drop(t);
303 self.staging.extend(staged);
304 Ok(post_images)
305 }
306
307 pub fn upsert(
308 &mut self,
309 table: &str,
310 mut insert_cells: Vec<(u16, Value)>,
311 action: UpsertAction,
312 ) -> Result<UpsertResult> {
313 self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
317 let id = self.db.table_id(table)?;
318 self.reject_after_truncate(id)?;
319 match (self.existing_pk_row(table, &insert_cells)?, action) {
320 (None, _) => {
321 let handle = self.db.table(table)?;
322 let mut t = handle.lock();
323 let assigned = t.fill_auto_inc(&mut insert_cells)?;
324 t.apply_defaults(&mut insert_cells)?;
325 drop(t);
326 let row = owned_row_from_cells(&insert_cells);
327 self.staging.push((id, Staged::Put(insert_cells)));
328 Ok(UpsertResult {
329 action: UpsertActionKind::Inserted,
330 row,
331 auto_inc: assigned,
332 })
333 }
334 (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
335 action: UpsertActionKind::Unchanged,
336 row: old_row,
337 auto_inc: None,
338 }),
339 (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
340 self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
342 let merged = merge_cells(old_row.columns.clone(), update_cells);
343 if columns_equal(&old_row.columns, &merged) {
344 return Ok(UpsertResult {
345 action: UpsertActionKind::Unchanged,
346 row: old_row,
347 auto_inc: None,
348 });
349 }
350 let row = owned_row_from_cells(&merged);
351 self.staging.push((id, Staged::Update(old_id, merged)));
352 Ok(UpsertResult {
353 action: UpsertActionKind::Updated,
354 row,
355 auto_inc: None,
356 })
357 }
358 }
359 }
360
361 pub fn truncate(&mut self, table: &str) -> Result<()> {
362 self.db
363 .require_for(self.principal.as_ref(), &crate::auth::Permission::Admin)?;
364 let id = self.db.table_id(table)?;
365 for (table_id, op) in &self.staging {
366 if *table_id == id && !matches!(op, Staged::Truncate) {
367 return Err(MongrelError::InvalidArgument(
368 "truncate cannot be combined with other writes on the same table".into(),
369 ));
370 }
371 }
372 self.staging.push((id, Staged::Truncate));
373 Ok(())
374 }
375
376 fn reject_after_truncate(&self, table_id: u64) -> Result<()> {
377 if self
378 .staging
379 .iter()
380 .any(|(tid, op)| *tid == table_id && matches!(op, Staged::Truncate))
381 {
382 return Err(MongrelError::InvalidArgument(
383 "truncate cannot be combined with other writes on the same table".into(),
384 ));
385 }
386 Ok(())
387 }
388
389 fn require_columns(
390 &self,
391 table: &str,
392 operation: crate::auth::ColumnOperation,
393 cells: &[(u16, Value)],
394 ) -> Result<()> {
395 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
396 self.db
397 .require_columns_for(table, operation, &columns, self.principal.as_ref())
398 }
399
400 fn require_delete(&self, table: &str) -> Result<()> {
401 self.db.require_for(
402 self.principal.as_ref(),
403 &crate::auth::Permission::Delete {
404 table: table.to_string(),
405 },
406 )
407 }
408
409 fn existing_pk_row(
410 &self,
411 table: &str,
412 cells: &[(u16, Value)],
413 ) -> Result<Option<(RowId, OwnedRow)>> {
414 let handle = self.db.table(table)?;
415 let t = handle.lock();
416 let Some(pk_col) = t.schema().primary_key() else {
417 return Ok(None);
418 };
419 let Some((_, pk_value)) = cells.iter().find(|(id, _)| *id == pk_col.id) else {
420 return Ok(None);
421 };
422 if matches!(pk_value, Value::Null) {
423 return Ok(None);
424 }
425 let Some(row_id) = t.lookup_pk(&pk_value.encode_key()) else {
426 return Ok(None);
427 };
428 Ok(t.get(row_id, self.read)
429 .map(|row| (row_id, owned_row_from_map(row.columns))))
430 }
431
432 pub fn commit(self) -> Result<Epoch> {
434 self.db
435 .commit_transaction_with_external_states(
436 self.txn_id,
437 self.read.epoch,
438 self.staging,
439 self.external_states,
440 self.materialized_view_updates,
441 self.principal,
442 self.principal_catalog_bound,
443 self.external_trigger_bridge,
444 )
445 .map(|(epoch, _)| epoch)
446 }
447
448 pub fn commit_with_row_ids(self) -> Result<(Epoch, Vec<RowId>)> {
449 self.db.commit_transaction_with_external_states(
450 self.txn_id,
451 self.read.epoch,
452 self.staging,
453 self.external_states,
454 self.materialized_view_updates,
455 self.principal,
456 self.principal_catalog_bound,
457 self.external_trigger_bridge,
458 )
459 }
460
461 pub fn rollback(self) {
463 }
465}
466
467fn owned_row_from_cells(cells: &[(u16, Value)]) -> OwnedRow {
468 let mut columns = cells.to_vec();
469 columns.sort_by_key(|(id, _)| *id);
470 OwnedRow { columns }
471}
472
473fn owned_row_from_map(columns: HashMap<u16, Value>) -> OwnedRow {
474 let mut columns: Vec<(u16, Value)> = columns.into_iter().collect();
475 columns.sort_by_key(|(id, _)| *id);
476 OwnedRow { columns }
477}
478
479fn merge_cells(mut base: Vec<(u16, Value)>, updates: Vec<(u16, Value)>) -> Vec<(u16, Value)> {
480 for (id, value) in updates {
481 base.retain(|(existing, _)| *existing != id);
482 base.push((id, value));
483 }
484 base.sort_by_key(|(id, _)| *id);
485 base
486}
487
488fn columns_equal(a: &[(u16, Value)], b: &[(u16, Value)]) -> bool {
489 if a.len() != b.len() {
490 return false;
491 }
492 let mut a: Vec<_> = a.iter().collect();
493 let mut b: Vec<_> = b.iter().collect();
494 a.sort_by_key(|(id, _)| *id);
495 b.sort_by_key(|(id, _)| *id);
496 a.iter()
497 .zip(b.iter())
498 .all(|((id_a, v_a), (id_b, v_b))| id_a == id_b && v_a == v_b)
499}
500
501pub(crate) enum StagedOp {
503 Put(Vec<crate::memtable::Row>),
504 Delete(Vec<RowId>),
505 Truncate,
506}
507
508use std::collections::{BTreeMap, HashMap};
511use std::hash::{Hash, Hasher};
512
513#[derive(Clone, Debug)]
516pub enum WriteKey {
517 Row { table_id: u64, row_id: u64 },
519 Unique {
521 table_id: u64,
522 index_id: u16,
523 key_hash: u64,
524 },
525 Table { table_id: u64 },
527}
528
529impl Hash for WriteKey {
530 fn hash<H: Hasher>(&self, state: &mut H) {
531 match self {
532 WriteKey::Row { table_id, row_id } => {
533 0u8.hash(state);
534 table_id.hash(state);
535 row_id.hash(state);
536 }
537 WriteKey::Unique {
538 table_id,
539 index_id,
540 key_hash,
541 } => {
542 1u8.hash(state);
543 table_id.hash(state);
544 index_id.hash(state);
545 key_hash.hash(state);
546 }
547 WriteKey::Table { table_id } => {
548 2u8.hash(state);
549 table_id.hash(state);
550 }
551 }
552 }
553}
554
555impl PartialEq for WriteKey {
556 fn eq(&self, other: &Self) -> bool {
557 match (self, other) {
558 (
559 WriteKey::Row {
560 table_id: a,
561 row_id: b,
562 },
563 WriteKey::Row {
564 table_id: c,
565 row_id: d,
566 },
567 ) => a == c && b == d,
568 (
569 WriteKey::Unique {
570 table_id: a,
571 index_id: b,
572 key_hash: c,
573 },
574 WriteKey::Unique {
575 table_id: d,
576 index_id: e,
577 key_hash: f,
578 },
579 ) => a == d && b == e && c == f,
580 (WriteKey::Table { table_id: a }, WriteKey::Table { table_id: b }) => a == b,
581 _ => false,
582 }
583 }
584}
585
586impl Eq for WriteKey {}
587
588const CONFLICT_SHARDS: usize = 16;
589
590pub struct ConflictIndex {
594 shards: [parking_lot::Mutex<HashMap<WriteKey, u64>>; CONFLICT_SHARDS],
595 table_truncate_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
596 table_write_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
597 version: std::sync::atomic::AtomicU64,
601}
602
603impl ConflictIndex {
604 pub fn new() -> Self {
605 Self {
606 shards: std::array::from_fn(|_| parking_lot::Mutex::new(HashMap::new())),
607 table_truncate_epochs: parking_lot::Mutex::new(HashMap::new()),
608 table_write_epochs: parking_lot::Mutex::new(HashMap::new()),
609 version: std::sync::atomic::AtomicU64::new(0),
610 }
611 }
612
613 pub fn version(&self) -> u64 {
617 self.version.load(std::sync::atomic::Ordering::Acquire)
618 }
619
620 fn shard(&self, key: &WriteKey) -> &parking_lot::Mutex<HashMap<WriteKey, u64>> {
621 let mut h = std::collections::hash_map::DefaultHasher::new();
622 key.hash(&mut h);
623 let idx = (h.finish() as usize) & (CONFLICT_SHARDS - 1);
624 &self.shards[idx]
625 }
626
627 pub fn conflicts(&self, keys: &[WriteKey], read_epoch: Epoch) -> bool {
630 for k in keys {
631 let s = self.shard(k);
632 if let Some(&ce) = s.lock().get(k) {
633 if ce > read_epoch.0 {
634 return true;
635 }
636 }
637 }
638 let truncates = self.table_truncate_epochs.lock();
639 let writes = self.table_write_epochs.lock();
640 for k in keys {
641 match k {
642 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
643 if truncates.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
644 return true;
645 }
646 }
647 WriteKey::Table { table_id } => {
648 if writes.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
649 return true;
650 }
651 }
652 }
653 }
654 false
655 }
656
657 pub fn record(&self, keys: &[WriteKey], commit_epoch: Epoch) {
659 for k in keys {
660 let s = self.shard(k);
661 s.lock().insert(k.clone(), commit_epoch.0);
662 }
663 let mut truncates = self.table_truncate_epochs.lock();
664 let mut writes = self.table_write_epochs.lock();
665 for k in keys {
666 match k {
667 WriteKey::Table { table_id } => {
668 truncates
669 .entry(*table_id)
670 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
671 .or_insert(commit_epoch.0);
672 }
673 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
674 writes
675 .entry(*table_id)
676 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
677 .or_insert(commit_epoch.0);
678 }
679 }
680 }
681 self.version
682 .fetch_add(1, std::sync::atomic::Ordering::Release);
683 }
684
685 pub fn prune_below(&self, min_active: Epoch) {
688 for s in &self.shards {
689 s.lock().retain(|_, ce| *ce >= min_active.0);
690 }
691 self.table_truncate_epochs
692 .lock()
693 .retain(|_, ce| *ce >= min_active.0);
694 self.table_write_epochs
695 .lock()
696 .retain(|_, ce| *ce >= min_active.0);
697 }
698}
699
700impl Default for ConflictIndex {
701 fn default() -> Self {
702 Self::new()
703 }
704}
705
706pub struct GroupCommit {
716 inner: PlMutex<GroupState>,
717 cv: Condvar,
718}
719
720struct GroupState {
721 durable_seq: u64,
722 syncing: bool,
723 poisoned: bool,
724}
725
726impl GroupCommit {
727 pub fn new(durable_seq: u64) -> Self {
728 Self {
729 inner: PlMutex::new(GroupState {
730 durable_seq,
731 syncing: false,
732 poisoned: false,
733 }),
734 cv: Condvar::new(),
735 }
736 }
737
738 pub fn await_durable(&self, wal: &PlMutex<SharedWal>, commit_seq: u64) -> Result<()> {
744 let mut st = self.inner.lock();
745 loop {
746 if st.poisoned {
747 return Err(MongrelError::Other(
748 "database poisoned by fsync error".into(),
749 ));
750 }
751 if st.durable_seq >= commit_seq {
752 return Ok(());
753 }
754 if st.syncing {
755 self.cv.wait(&mut st);
757 continue;
758 }
759 st.syncing = true;
762 drop(st);
763 std::thread::sleep(std::time::Duration::from_micros(50));
765 let res = wal.lock().group_sync();
766 st = self.inner.lock();
767 st.syncing = false;
768 match res {
769 Ok(durable) => {
770 if durable > st.durable_seq {
771 st.durable_seq = durable;
772 }
773 self.cv.notify_all();
774 }
777 Err(e) => {
778 st.poisoned = true;
779 self.cv.notify_all();
780 return Err(e);
781 }
782 }
783 }
784 }
785}
786
787pub struct ActiveTxns {
791 inner: parking_lot::Mutex<BTreeMap<u64, u64>>,
792}
793
794impl ActiveTxns {
795 pub fn new() -> Self {
796 Self {
797 inner: parking_lot::Mutex::new(BTreeMap::new()),
798 }
799 }
800
801 pub fn register(&self, read_epoch: Epoch) -> ActiveTxnGuard<'_> {
804 let mut g = self.inner.lock();
805 *g.entry(read_epoch.0).or_insert(0) += 1;
806 ActiveTxnGuard {
807 active: self,
808 epoch: read_epoch.0,
809 }
810 }
811
812 pub fn min_read_epoch(&self) -> u64 {
814 self.inner.lock().keys().next().copied().unwrap_or(u64::MAX)
815 }
816}
817
818impl Default for ActiveTxns {
819 fn default() -> Self {
820 Self::new()
821 }
822}
823
824pub struct ActiveTxnGuard<'a> {
826 active: &'a ActiveTxns,
827 epoch: u64,
828}
829
830impl Drop for ActiveTxnGuard<'_> {
831 fn drop(&mut self) {
832 let mut g = self.active.inner.lock();
833 if let Some(count) = g.get_mut(&self.epoch) {
834 *count -= 1;
835 if *count == 0 {
836 g.remove(&self.epoch);
837 }
838 }
839 }
840}
841
842#[cfg(test)]
843mod tests {
844 use super::*;
845
846 #[test]
847 fn conflict_index_first_committer_wins_and_prunes_safely() {
848 let ci = ConflictIndex::new();
849 let k = vec![WriteKey::Row {
850 table_id: 1,
851 row_id: 7,
852 }];
853 assert!(!ci.conflicts(&k, Epoch(5)));
854 ci.record(&k, Epoch(6));
855 assert!(ci.conflicts(&k, Epoch(5)));
856 assert!(!ci.conflicts(&k, Epoch(6)));
857 ci.prune_below(Epoch(7));
858 assert!(!ci.conflicts(&k, Epoch(5)));
859 }
860
861 #[test]
862 fn conflict_index_table_scope_conflicts_both_directions() {
863 let ci = ConflictIndex::new();
864 ci.record(&[WriteKey::Table { table_id: 1 }], Epoch(6));
865 assert!(ci.conflicts(
866 &[WriteKey::Row {
867 table_id: 1,
868 row_id: 7,
869 }],
870 Epoch(5)
871 ));
872 assert!(ci.conflicts(
873 &[WriteKey::Unique {
874 table_id: 1,
875 index_id: 0,
876 key_hash: 42,
877 }],
878 Epoch(5)
879 ));
880 assert!(!ci.conflicts(
881 &[WriteKey::Row {
882 table_id: 2,
883 row_id: 7,
884 }],
885 Epoch(5)
886 ));
887
888 let ci = ConflictIndex::new();
889 ci.record(
890 &[WriteKey::Row {
891 table_id: 1,
892 row_id: 7,
893 }],
894 Epoch(6),
895 );
896 assert!(ci.conflicts(&[WriteKey::Table { table_id: 1 }], Epoch(5)));
897 assert!(!ci.conflicts(&[WriteKey::Table { table_id: 2 }], Epoch(5)));
898 }
899
900 #[test]
901 fn writekey_eq_across_variants() {
902 let r1 = WriteKey::Row {
903 table_id: 1,
904 row_id: 2,
905 };
906 let r2 = WriteKey::Row {
907 table_id: 1,
908 row_id: 2,
909 };
910 let r3 = WriteKey::Row {
911 table_id: 1,
912 row_id: 3,
913 };
914 assert_eq!(r1, r2);
915 assert_ne!(r1, r3);
916
917 let u1 = WriteKey::Unique {
918 table_id: 1,
919 index_id: 0,
920 key_hash: 42,
921 };
922 let u2 = WriteKey::Unique {
923 table_id: 1,
924 index_id: 0,
925 key_hash: 42,
926 };
927 assert_eq!(u1, u2);
928 assert_ne!(r1, u1);
929
930 let t1 = WriteKey::Table { table_id: 5 };
931 let t2 = WriteKey::Table { table_id: 5 };
932 assert_eq!(t1, t2);
933 assert_ne!(t1, r1);
934 }
935
936 #[test]
937 fn active_txns_tracks_min_read_epoch() {
938 let at = ActiveTxns::new();
939 assert_eq!(at.min_read_epoch(), u64::MAX);
940 let g1 = at.register(Epoch(5));
941 assert_eq!(at.min_read_epoch(), 5);
942 let g2 = at.register(Epoch(3));
943 assert_eq!(at.min_read_epoch(), 3);
944 drop(g2);
945 assert_eq!(at.min_read_epoch(), 5);
946 drop(g1);
947 assert_eq!(at.min_read_epoch(), u64::MAX);
948 }
949
950 #[test]
951 fn active_txns_dedups_same_epoch() {
952 let at = ActiveTxns::new();
953 let g1 = at.register(Epoch(7));
954 let g2 = at.register(Epoch(7));
955 assert_eq!(at.min_read_epoch(), 7);
956 drop(g1);
957 assert_eq!(at.min_read_epoch(), 7);
958 drop(g2);
959 assert_eq!(at.min_read_epoch(), u64::MAX);
960 }
961}
962
963#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
974pub enum IsolationLevel {
975 #[default]
976 Snapshot,
977 ReadCommitted,
978 Serializable,
979}