Skip to main content

mongreldb_core/
txn.rs

1//! Cross-table transactions on the shared WAL (spec §8.2, single-applier subset
2//! — parallelism arrives in P3).
3//!
4//! A [`Transaction`] stages puts/deletes keyed by table; [`Transaction::commit`]
5//! reserves a commit epoch from the shared authority, appends the staged data
6//! records + a `TxnCommit` marker to the shared WAL, group-fsyncs, applies the
7//! staging to each table's memtable + indexes at the commit epoch, persists the
8//! per-table manifests, and publishes the visible watermark. Rollback (or a
9//! dropped transaction) discards the staging and appends nothing durable.
10
11use crate::database::{Database, ExternalTriggerBridge, TableHandle};
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) fn allocate_txn_id(allocator: &PlMutex<u64>) -> Result<u64> {
20    let mut next = allocator.lock();
21    let id = *next;
22    if id == crate::wal::SYSTEM_TXN_ID || id & u32::MAX as u64 == 0 {
23        return Err(MongrelError::Full(
24            "per-open transaction id namespace exhausted; reopen the database".into(),
25        ));
26    }
27    *next = id.checked_add(1).ok_or_else(|| {
28        MongrelError::Full(
29            "per-open transaction id namespace exhausted; reopen the database".into(),
30        )
31    })?;
32    Ok(id)
33}
34
35/// One staged mutation against a named table.
36pub(crate) enum Staged {
37    Put(Vec<(u16, Value)>),
38    Delete(RowId),
39    /// Full post-update row image plus the logical columns changed by this
40    /// operation. Authorization uses `changed_columns`; constraints, RLS,
41    /// triggers, WAL publication, and index maintenance use `new_row`.
42    Update {
43        row_id: RowId,
44        new_row: Vec<(u16, Value)>,
45        changed_columns: Vec<u16>,
46    },
47    Truncate,
48}
49
50#[derive(Debug, Clone)]
51pub struct OwnedRow {
52    pub columns: Vec<(u16, Value)>,
53}
54
55#[derive(Debug, Clone)]
56pub struct PutResult {
57    pub auto_inc: Option<i64>,
58    pub row: OwnedRow,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum UpsertActionKind {
63    Inserted,
64    Updated,
65    Unchanged,
66}
67
68#[derive(Debug, Clone)]
69pub enum UpsertAction {
70    DoNothing,
71    DoUpdate(Vec<(u16, Value)>),
72}
73
74#[derive(Debug, Clone)]
75pub struct UpsertResult {
76    pub action: UpsertActionKind,
77    pub row: OwnedRow,
78    pub auto_inc: Option<i64>,
79}
80
81/// An in-flight cross-table transaction. Holds a read snapshot taken at `begin`
82/// and stages writes; nothing is durable or visible until [`Self::commit`].
83pub struct Transaction<'db> {
84    db: &'db Database,
85    txn_id: u64,
86    allocation_error: Option<String>,
87    read: Snapshot,
88    staging: Vec<(u64 /*table_id*/, Staged)>,
89    external_states: Vec<(String, Vec<u8>)>,
90    materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
91    principal: Option<crate::auth::Principal>,
92    principal_catalog_bound: bool,
93    external_trigger_bridge: Option<&'db dyn ExternalTriggerBridge>,
94    _active: Option<ActiveTxnGuard<'db>>,
95}
96
97impl<'db> Transaction<'db> {
98    pub(crate) fn new(db: &'db Database, txn_id: Result<u64>, read: Snapshot) -> Self {
99        let guard = db.register_active(read.epoch);
100        let (txn_id, allocation_error) = match txn_id {
101            Ok(txn_id) => (txn_id, None),
102            Err(MongrelError::Full(message)) => (crate::wal::SYSTEM_TXN_ID, Some(message)),
103            Err(error) => (crate::wal::SYSTEM_TXN_ID, Some(error.to_string())),
104        };
105        Self {
106            db,
107            txn_id,
108            allocation_error,
109            read,
110            staging: Vec::new(),
111            external_states: Vec::new(),
112            materialized_view_updates: Vec::new(),
113            principal: None,
114            principal_catalog_bound: false,
115            external_trigger_bridge: None,
116            _active: Some(guard),
117        }
118    }
119
120    pub(crate) fn with_external_trigger_bridge(
121        mut self,
122        bridge: &'db dyn ExternalTriggerBridge,
123    ) -> Self {
124        self.external_trigger_bridge = Some(bridge);
125        self
126    }
127
128    pub(crate) fn with_principal(
129        mut self,
130        principal: Option<crate::auth::Principal>,
131        catalog_bound: bool,
132    ) -> Self {
133        self.principal = principal;
134        self.principal_catalog_bound = catalog_bound;
135        self
136    }
137
138    pub fn read_snapshot(&self) -> Snapshot {
139        self.read
140    }
141
142    /// The transaction's id (generation-scoped: high 32 bits = open generation,
143    /// low 32 = per-open counter). Mainly diagnostic / test-facing.
144    pub fn txn_id(&self) -> u64 {
145        self.txn_id
146    }
147
148    /// Stage a put on `table`. The row id is allocated at commit so an aborted
149    /// transaction never consumes ids. If the table has an `AUTO_INCREMENT`
150    /// primary key and the column is omitted or null, the engine fills it now
151    /// and returns the assigned value; explicit ids are honored and advance the
152    /// counter. The value is staged in `cells`, so the commit path writes the
153    /// same id into the row.
154    pub fn put(&mut self, table: &str, mut cells: Vec<(u16, Value)>) -> Result<Option<i64>> {
155        self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
156        let id = self.db.table_id(table)?;
157        let handle = self.db.table(table)?;
158        let mut t = handle.lock();
159        let assigned = t.fill_auto_inc(&mut cells)?;
160        t.apply_defaults(&mut cells)?;
161        drop(t);
162        self.staging.push((id, Staged::Put(cells)));
163        Ok(assigned)
164    }
165
166    /// Stage a row in a hidden CTAS build table.
167    #[doc(hidden)]
168    pub fn put_building(
169        &mut self,
170        table: &str,
171        mut cells: Vec<(u16, Value)>,
172    ) -> Result<Option<i64>> {
173        self.db
174            .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
175        let id = self.db.building_table_id(table)?;
176        let handle = self.db.table_by_id(id)?;
177        let mut target = handle.lock();
178        let assigned = target.fill_auto_inc(&mut cells)?;
179        target.apply_defaults(&mut cells)?;
180        let primary_key_column = target
181            .schema()
182            .primary_key()
183            .map(|column| column.id)
184            .ok_or_else(|| MongrelError::Schema("CTAS build table has no primary key".into()))?;
185        let primary_key = cells
186            .iter()
187            .find(|(column, _)| *column == primary_key_column)
188            .map(|(_, value)| value)
189            .ok_or_else(|| MongrelError::InvalidArgument("CTAS primary key is missing".into()))?;
190        if matches!(primary_key, Value::Null) {
191            return Err(MongrelError::InvalidArgument(
192                "CTAS primary key cannot be NULL".into(),
193            ));
194        }
195        let primary_key = primary_key.encode_key();
196        let replacing = self
197            .staging
198            .iter()
199            .any(|(table_id, staged)| *table_id == id && matches!(staged, Staged::Truncate));
200        if !replacing && target.lookup_pk(&primary_key).is_some() {
201            return Err(MongrelError::InvalidArgument(
202                "duplicate CTAS primary key".into(),
203            ));
204        }
205        drop(target);
206        if self.staging.iter().any(|(staged_table, staged)| {
207            if *staged_table != id {
208                return false;
209            }
210            let Staged::Put(staged_cells) = staged else {
211                return false;
212            };
213            staged_cells
214                .iter()
215                .find(|(column, _)| *column == primary_key_column)
216                .is_some_and(|(_, value)| value.encode_key() == primary_key)
217        }) {
218            return Err(MongrelError::InvalidArgument(
219                "duplicate CTAS primary key".into(),
220            ));
221        }
222        self.staging.push((id, Staged::Put(cells)));
223        Ok(assigned)
224    }
225
226    /// Stage a truncate against an unpublished building table.
227    #[doc(hidden)]
228    pub fn truncate_building(&mut self, table: &str) -> Result<()> {
229        self.db
230            .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
231        let id = self.db.building_table_id(table)?;
232        if self.staging.iter().any(|(table_id, _)| *table_id == id) {
233            return Err(MongrelError::InvalidArgument(
234                "building-table truncate must be staged before replacement rows".into(),
235            ));
236        }
237        self.staging.push((id, Staged::Truncate));
238        Ok(())
239    }
240
241    pub fn put_returning(
242        &mut self,
243        table: &str,
244        mut cells: Vec<(u16, Value)>,
245    ) -> Result<PutResult> {
246        self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
247        let id = self.db.table_id(table)?;
248        let handle = self.db.table(table)?;
249        let mut t = handle.lock();
250        let assigned = t.fill_auto_inc(&mut cells)?;
251        t.apply_defaults(&mut cells)?;
252        drop(t);
253        let row = owned_row_from_cells(&cells);
254        self.staging.push((id, Staged::Put(cells)));
255        Ok(PutResult {
256            auto_inc: assigned,
257            row,
258        })
259    }
260
261    /// Stage a returning put only if `table` still names the exact catalog
262    /// resource checked by the caller.
263    #[doc(hidden)]
264    pub fn put_returning_bound(
265        &mut self,
266        table: &str,
267        expected_table_id: u64,
268        expected_schema_id: u64,
269        mut cells: Vec<(u16, Value)>,
270    ) -> Result<PutResult> {
271        self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
272        let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
273        let mut target = handle.lock();
274        let assigned = target.fill_auto_inc(&mut cells)?;
275        target.apply_defaults(&mut cells)?;
276        drop(target);
277        let row = owned_row_from_cells(&cells);
278        self.staging.push((expected_table_id, Staged::Put(cells)));
279        Ok(PutResult {
280            auto_inc: assigned,
281            row,
282        })
283    }
284
285    /// Stage many puts on the same `table` with one table-id lookup + one
286    /// auto-inc lock pass. Each row is staged individually (same as repeated
287    /// `put`); the savings are the amortized lookups/locks for bulk guard-row
288    /// writes and batched application-row inserts. Returns the assigned
289    /// auto-increment values (`Some` only where the engine filled the column).
290    pub fn put_batch(
291        &mut self,
292        table: &str,
293        rows: Vec<Vec<(u16, Value)>>,
294    ) -> Result<Vec<Option<i64>>> {
295        if !rows.is_empty() {
296            let mut columns = rows
297                .iter()
298                .flat_map(|cells| cells.iter().map(|(column, _)| *column))
299                .collect::<Vec<_>>();
300            columns.sort_unstable();
301            columns.dedup();
302            self.db.require_columns_for(
303                table,
304                crate::auth::ColumnOperation::Insert,
305                &columns,
306                self.principal.as_ref(),
307            )?;
308        }
309        let id = self.db.table_id(table)?;
310        let handle = self.db.table(table)?;
311        let mut t = handle.lock();
312        let mut assigned = Vec::with_capacity(rows.len());
313        for mut cells in rows {
314            let a = t.fill_auto_inc(&mut cells)?;
315            t.apply_defaults(&mut cells)?;
316            assigned.push(a);
317            self.staging.push((id, Staged::Put(cells)));
318        }
319        drop(t);
320        Ok(assigned)
321    }
322
323    /// Stage a delete of `row_id` on `table`.
324    pub fn delete(&mut self, table: &str, row_id: RowId) -> Result<()> {
325        self.delete_batch(table, vec![row_id])
326    }
327
328    /// Stage a delete only against the exact table generation checked by the
329    /// caller.
330    #[doc(hidden)]
331    pub fn delete_bound(
332        &mut self,
333        table: &str,
334        expected_table_id: u64,
335        expected_schema_id: u64,
336        row_id: RowId,
337    ) -> Result<()> {
338        self.require_delete(table)?;
339        self.bound_table(table, expected_table_id, expected_schema_id)?;
340        self.reject_after_truncate(expected_table_id)?;
341        self.staging
342            .push((expected_table_id, Staged::Delete(row_id)));
343        Ok(())
344    }
345
346    /// Resolve and delete a primary key only on the exact table generation
347    /// checked by the caller. Returns `false` when the key is absent.
348    #[doc(hidden)]
349    pub fn delete_by_pk_bound(
350        &mut self,
351        table: &str,
352        expected_table_id: u64,
353        expected_schema_id: u64,
354        key: &Value,
355    ) -> Result<bool> {
356        self.require_delete(table)?;
357        let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
358        self.reject_after_truncate(expected_table_id)?;
359        let row_id = {
360            let mut target = handle.lock();
361            target.ensure_indexes_complete()?;
362            target.lookup_pk(&key.encode_key())
363        };
364        let Some(row_id) = row_id else {
365            return Ok(false);
366        };
367        self.staging
368            .push((expected_table_id, Staged::Delete(row_id)));
369        Ok(true)
370    }
371
372    /// Stage deletes without materializing pre-images.
373    pub fn delete_batch(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<()> {
374        self.require_delete(table)?;
375        let id = self.db.table_id(table)?;
376        self.reject_after_truncate(id)?;
377        self.staging.extend(
378            row_ids
379                .into_iter()
380                .map(|row_id| (id, Staged::Delete(row_id))),
381        );
382        Ok(())
383    }
384
385    /// Stage opaque external-table module state. The payload is committed under
386    /// the same WAL `TxnCommit` as ordinary table writes.
387    pub fn put_external_state(&mut self, table: &str, state: Vec<u8>) -> Result<()> {
388        if self.db.external_table(table).is_none() {
389            return Err(MongrelError::NotFound(format!(
390                "external table {table:?} not found"
391            )));
392        }
393        self.external_states.push((table.to_string(), state));
394        Ok(())
395    }
396
397    /// Stage a materialized-view checkpoint in the same durable commit as its
398    /// row deltas. This makes incremental refresh replay idempotent after a
399    /// crash: data and the Last-Event-ID watermark advance together.
400    pub fn set_materialized_view_definition(
401        &mut self,
402        definition: crate::catalog::MaterializedViewEntry,
403    ) -> Result<()> {
404        self.db
405            .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
406        if self.db.table_id(&definition.name).is_err() {
407            return Err(MongrelError::NotFound(format!(
408                "materialized view table {:?} not found",
409                definition.name
410            )));
411        }
412        self.materialized_view_updates
413            .retain(|current| current.name != definition.name);
414        self.materialized_view_updates.push(definition);
415        Ok(())
416    }
417
418    pub fn delete_many(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<Vec<OwnedRow>> {
419        self.require_delete(table)?;
420        let id = self.db.table_id(table)?;
421        self.reject_after_truncate(id)?;
422        let snap = self.read;
423        let handle = self.db.table(table)?;
424        let t = handle.lock();
425        let mut pre_images = Vec::with_capacity(row_ids.len());
426        for row_id in &row_ids {
427            if let Some(row) = t.get(*row_id, snap) {
428                pre_images.push(owned_row_from_map(row.columns));
429            }
430        }
431        drop(t);
432        for row_id in row_ids {
433            self.staging.push((id, Staged::Delete(row_id)));
434        }
435        Ok(pre_images)
436    }
437
438    pub fn update_many(
439        &mut self,
440        table: &str,
441        updates: Vec<(RowId, Vec<(u16, Value)>)>,
442    ) -> Result<Vec<OwnedRow>> {
443        if !updates.is_empty() {
444            let mut columns = updates
445                .iter()
446                .flat_map(|(_, cells)| cells.iter().map(|(column, _)| *column))
447                .collect::<Vec<_>>();
448            columns.sort_unstable();
449            columns.dedup();
450            self.db.require_columns_for(
451                table,
452                crate::auth::ColumnOperation::Update,
453                &columns,
454                self.principal.as_ref(),
455            )?;
456        }
457        let id = self.db.table_id(table)?;
458        self.reject_after_truncate(id)?;
459        let snap = self.read;
460        let handle = self.db.table(table)?;
461        let t = handle.lock();
462        let mut post_images = Vec::with_capacity(updates.len());
463        let mut staged = Vec::with_capacity(updates.len());
464        for (old_id, new_cells) in updates {
465            let changed_columns = changed_columns(&new_cells);
466            let old_row = t
467                .get(old_id, snap)
468                .ok_or_else(|| MongrelError::NotFound(format!("row {old_id:?} not found")))?;
469            let merged = merge_cells(old_row.columns.into_iter().collect(), new_cells);
470            post_images.push(owned_row_from_cells(&merged));
471            staged.push((
472                id,
473                Staged::Update {
474                    row_id: old_id,
475                    new_row: merged,
476                    changed_columns,
477                },
478            ));
479        }
480        drop(t);
481        self.staging.extend(staged);
482        Ok(post_images)
483    }
484
485    pub fn upsert(
486        &mut self,
487        table: &str,
488        mut insert_cells: Vec<(u16, Value)>,
489        action: UpsertAction,
490    ) -> Result<UpsertResult> {
491        // Upsert may insert or update. Check Insert up front (the common
492        // path); the DoUpdate branch additionally checks Update before
493        // mutating an existing row.
494        self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
495        let id = self.db.table_id(table)?;
496        self.reject_after_truncate(id)?;
497        match (self.existing_pk_row(table, &insert_cells)?, action) {
498            (None, _) => {
499                let handle = self.db.table(table)?;
500                let mut t = handle.lock();
501                let assigned = t.fill_auto_inc(&mut insert_cells)?;
502                t.apply_defaults(&mut insert_cells)?;
503                drop(t);
504                let row = owned_row_from_cells(&insert_cells);
505                self.staging.push((id, Staged::Put(insert_cells)));
506                Ok(UpsertResult {
507                    action: UpsertActionKind::Inserted,
508                    row,
509                    auto_inc: assigned,
510                })
511            }
512            (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
513                action: UpsertActionKind::Unchanged,
514                row: old_row,
515                auto_inc: None,
516            }),
517            (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
518                // The update branch requires Update permission.
519                self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
520                let changed_columns = changed_columns(&update_cells);
521                let merged = merge_cells(old_row.columns.clone(), update_cells);
522                if columns_equal(&old_row.columns, &merged) {
523                    return Ok(UpsertResult {
524                        action: UpsertActionKind::Unchanged,
525                        row: old_row,
526                        auto_inc: None,
527                    });
528                }
529                let row = owned_row_from_cells(&merged);
530                self.staging.push((
531                    id,
532                    Staged::Update {
533                        row_id: old_id,
534                        new_row: merged,
535                        changed_columns,
536                    },
537                ));
538                Ok(UpsertResult {
539                    action: UpsertActionKind::Updated,
540                    row,
541                    auto_inc: None,
542                })
543            }
544        }
545    }
546
547    /// Stage an upsert only if `table` still names the exact catalog resource
548    /// checked by the caller.
549    #[doc(hidden)]
550    pub fn upsert_bound(
551        &mut self,
552        table: &str,
553        expected_table_id: u64,
554        expected_schema_id: u64,
555        mut insert_cells: Vec<(u16, Value)>,
556        action: UpsertAction,
557    ) -> Result<UpsertResult> {
558        self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
559        let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
560        self.reject_after_truncate(expected_table_id)?;
561        match (self.existing_pk_row_in(&handle, &insert_cells)?, action) {
562            (None, _) => {
563                let mut target = handle.lock();
564                let assigned = target.fill_auto_inc(&mut insert_cells)?;
565                target.apply_defaults(&mut insert_cells)?;
566                drop(target);
567                let row = owned_row_from_cells(&insert_cells);
568                self.staging
569                    .push((expected_table_id, Staged::Put(insert_cells)));
570                Ok(UpsertResult {
571                    action: UpsertActionKind::Inserted,
572                    row,
573                    auto_inc: assigned,
574                })
575            }
576            (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
577                action: UpsertActionKind::Unchanged,
578                row: old_row,
579                auto_inc: None,
580            }),
581            (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
582                self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
583                let changed_columns = changed_columns(&update_cells);
584                let merged = merge_cells(old_row.columns.clone(), update_cells);
585                if columns_equal(&old_row.columns, &merged) {
586                    return Ok(UpsertResult {
587                        action: UpsertActionKind::Unchanged,
588                        row: old_row,
589                        auto_inc: None,
590                    });
591                }
592                let row = owned_row_from_cells(&merged);
593                self.staging.push((
594                    expected_table_id,
595                    Staged::Update {
596                        row_id: old_id,
597                        new_row: merged,
598                        changed_columns,
599                    },
600                ));
601                Ok(UpsertResult {
602                    action: UpsertActionKind::Updated,
603                    row,
604                    auto_inc: None,
605                })
606            }
607        }
608    }
609
610    pub fn truncate(&mut self, table: &str) -> Result<()> {
611        self.db
612            .require_for(self.principal.as_ref(), &crate::auth::Permission::Admin)?;
613        let id = self.db.table_id(table)?;
614        for (table_id, op) in &self.staging {
615            if *table_id == id && !matches!(op, Staged::Truncate) {
616                return Err(MongrelError::InvalidArgument(
617                    "truncate cannot be combined with other writes on the same table".into(),
618                ));
619            }
620        }
621        self.staging.push((id, Staged::Truncate));
622        Ok(())
623    }
624
625    fn reject_after_truncate(&self, table_id: u64) -> Result<()> {
626        if self
627            .staging
628            .iter()
629            .any(|(tid, op)| *tid == table_id && matches!(op, Staged::Truncate))
630        {
631            return Err(MongrelError::InvalidArgument(
632                "truncate cannot be combined with other writes on the same table".into(),
633            ));
634        }
635        Ok(())
636    }
637
638    fn require_columns(
639        &self,
640        table: &str,
641        operation: crate::auth::ColumnOperation,
642        cells: &[(u16, Value)],
643    ) -> Result<()> {
644        let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
645        self.db
646            .require_columns_for(table, operation, &columns, self.principal.as_ref())
647    }
648
649    fn require_delete(&self, table: &str) -> Result<()> {
650        self.db.require_for(
651            self.principal.as_ref(),
652            &crate::auth::Permission::Delete {
653                table: table.to_string(),
654            },
655        )
656    }
657
658    fn bound_table(
659        &self,
660        table: &str,
661        expected_table_id: u64,
662        expected_schema_id: u64,
663    ) -> Result<TableHandle> {
664        let current = self.db.table_identity(table)?;
665        if current != (expected_table_id, expected_schema_id) {
666            return Err(MongrelError::Conflict(format!(
667                "table {table:?} changed after request authorization"
668            )));
669        }
670        self.db.table_by_id(expected_table_id)
671    }
672
673    fn existing_pk_row(
674        &self,
675        table: &str,
676        cells: &[(u16, Value)],
677    ) -> Result<Option<(RowId, OwnedRow)>> {
678        let handle = self.db.table(table)?;
679        self.existing_pk_row_in(&handle, cells)
680    }
681
682    fn existing_pk_row_in(
683        &self,
684        handle: &TableHandle,
685        cells: &[(u16, Value)],
686    ) -> Result<Option<(RowId, OwnedRow)>> {
687        let target = handle.lock();
688        let Some(pk_col) = target.schema().primary_key() else {
689            return Ok(None);
690        };
691        let Some((_, pk_value)) = cells.iter().find(|(id, _)| *id == pk_col.id) else {
692            return Ok(None);
693        };
694        if matches!(pk_value, Value::Null) {
695            return Ok(None);
696        }
697        let Some(row_id) = target.lookup_pk(&pk_value.encode_key()) else {
698            return Ok(None);
699        };
700        Ok(target
701            .get(row_id, self.read)
702            .map(|row| (row_id, owned_row_from_map(row.columns))))
703    }
704
705    /// Commit: durably seal the staging under one epoch and publish it.
706    pub fn commit(self) -> Result<Epoch> {
707        if let Some(message) = self.allocation_error {
708            return Err(MongrelError::Full(message));
709        }
710        self.db
711            .commit_transaction_with_external_states(
712                self.txn_id,
713                self.read.epoch,
714                self.staging,
715                self.external_states,
716                self.materialized_view_updates,
717                self.principal,
718                self.principal_catalog_bound,
719                self.external_trigger_bridge,
720            )
721            .map(|(epoch, _)| epoch)
722    }
723
724    pub fn commit_with_row_ids(self) -> Result<(Epoch, Vec<RowId>)> {
725        if let Some(message) = self.allocation_error {
726            return Err(MongrelError::Full(message));
727        }
728        self.db.commit_transaction_with_external_states(
729            self.txn_id,
730            self.read.epoch,
731            self.staging,
732            self.external_states,
733            self.materialized_view_updates,
734            self.principal,
735            self.principal_catalog_bound,
736            self.external_trigger_bridge,
737        )
738    }
739
740    /// Cooperatively prepare this transaction, then invoke `before_commit`
741    /// immediately before the first WAL append can occur. If cancellation or
742    /// the callback wins, no commit epoch or WAL record is produced.
743    pub fn commit_controlled<F>(
744        self,
745        control: &crate::ExecutionControl,
746        mut before_commit: F,
747    ) -> Result<Epoch>
748    where
749        F: FnMut() -> Result<()>,
750    {
751        if let Some(message) = self.allocation_error {
752            return Err(MongrelError::Full(message));
753        }
754        self.db
755            .commit_transaction_with_external_states_controlled(
756                self.txn_id,
757                self.read.epoch,
758                self.staging,
759                self.external_states,
760                self.materialized_view_updates,
761                self.principal,
762                self.principal_catalog_bound,
763                self.external_trigger_bridge,
764                control,
765                &mut before_commit,
766            )
767            .map(|(epoch, _)| epoch)
768    }
769
770    pub fn commit_controlled_with_row_ids<F>(
771        self,
772        control: &crate::ExecutionControl,
773        mut before_commit: F,
774    ) -> Result<(Epoch, Vec<RowId>)>
775    where
776        F: FnMut() -> Result<()>,
777    {
778        if let Some(message) = self.allocation_error {
779            return Err(MongrelError::Full(message));
780        }
781        self.db.commit_transaction_with_external_states_controlled(
782            self.txn_id,
783            self.read.epoch,
784            self.staging,
785            self.external_states,
786            self.materialized_view_updates,
787            self.principal,
788            self.principal_catalog_bound,
789            self.external_trigger_bridge,
790            control,
791            &mut before_commit,
792        )
793    }
794
795    /// Rollback: discard staging. Nothing is appended to the WAL.
796    pub fn rollback(self) {
797        // Dropping `self` is enough — staging lives only in memory.
798    }
799}
800
801fn owned_row_from_cells(cells: &[(u16, Value)]) -> OwnedRow {
802    let mut columns = cells.to_vec();
803    columns.sort_by_key(|(id, _)| *id);
804    OwnedRow { columns }
805}
806
807fn owned_row_from_map(columns: HashMap<u16, Value>) -> OwnedRow {
808    let mut columns: Vec<(u16, Value)> = columns.into_iter().collect();
809    columns.sort_by_key(|(id, _)| *id);
810    OwnedRow { columns }
811}
812
813fn merge_cells(mut base: Vec<(u16, Value)>, updates: Vec<(u16, Value)>) -> Vec<(u16, Value)> {
814    for (id, value) in updates {
815        base.retain(|(existing, _)| *existing != id);
816        base.push((id, value));
817    }
818    base.sort_by_key(|(id, _)| *id);
819    base
820}
821
822fn changed_columns(cells: &[(u16, Value)]) -> Vec<u16> {
823    let mut columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
824    columns.sort_unstable();
825    columns.dedup();
826    columns
827}
828
829fn columns_equal(a: &[(u16, Value)], b: &[(u16, Value)]) -> bool {
830    if a.len() != b.len() {
831        return false;
832    }
833    let mut a: Vec<_> = a.iter().collect();
834    let mut b: Vec<_> = b.iter().collect();
835    a.sort_by_key(|(id, _)| *id);
836    b.sort_by_key(|(id, _)| *id);
837    a.iter()
838        .zip(b.iter())
839        .all(|((id_a, v_a), (id_b, v_b))| id_a == id_b && v_a == v_b)
840}
841
842/// Staged operation produced after row-id allocation (internal to commit).
843pub(crate) enum StagedOp {
844    Put(Vec<crate::memtable::Row>),
845    Delete(Vec<RowId>),
846    Truncate,
847}
848
849// ── P3.1: conflict index + active-txn registry (spec §8.3, §9.2) ─────────
850
851use std::collections::{BTreeMap, HashMap};
852use std::hash::{Hash, Hasher};
853
854/// A write-set key broad enough to detect all write–write conflicts under
855/// snapshot isolation (spec §8.3, review fix #13).
856#[derive(Clone, Debug)]
857pub enum WriteKey {
858    /// Row-version key for updates/deletes of existing rows.
859    Row { table_id: u64, row_id: u64 },
860    /// Unique/PK key for inserts/updates touching a UNIQUE column.
861    Unique {
862        table_id: u64,
863        index_id: u16,
864        key_hash: u64,
865    },
866    /// Table-scope key for TRUNCATE/DROP/ALTER and any txn writing that table.
867    Table { table_id: u64 },
868}
869
870impl Hash for WriteKey {
871    fn hash<H: Hasher>(&self, state: &mut H) {
872        match self {
873            WriteKey::Row { table_id, row_id } => {
874                0u8.hash(state);
875                table_id.hash(state);
876                row_id.hash(state);
877            }
878            WriteKey::Unique {
879                table_id,
880                index_id,
881                key_hash,
882            } => {
883                1u8.hash(state);
884                table_id.hash(state);
885                index_id.hash(state);
886                key_hash.hash(state);
887            }
888            WriteKey::Table { table_id } => {
889                2u8.hash(state);
890                table_id.hash(state);
891            }
892        }
893    }
894}
895
896impl PartialEq for WriteKey {
897    fn eq(&self, other: &Self) -> bool {
898        match (self, other) {
899            (
900                WriteKey::Row {
901                    table_id: a,
902                    row_id: b,
903                },
904                WriteKey::Row {
905                    table_id: c,
906                    row_id: d,
907                },
908            ) => a == c && b == d,
909            (
910                WriteKey::Unique {
911                    table_id: a,
912                    index_id: b,
913                    key_hash: c,
914                },
915                WriteKey::Unique {
916                    table_id: d,
917                    index_id: e,
918                    key_hash: f,
919                },
920            ) => a == d && b == e && c == f,
921            (WriteKey::Table { table_id: a }, WriteKey::Table { table_id: b }) => a == b,
922            _ => false,
923        }
924    }
925}
926
927impl Eq for WriteKey {}
928
929const CONFLICT_SHARDS: usize = 16;
930
931/// A sharded concurrent map of `WriteKey → commit_epoch` recording recent
932/// committed writes (spec §9.2). Validation probes per write-set key; pruning
933/// drops entries below `min(active read_epoch)`.
934pub struct ConflictIndex {
935    shards: [parking_lot::Mutex<HashMap<WriteKey, u64>>; CONFLICT_SHARDS],
936    table_truncate_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
937    table_write_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
938    /// Bumped on every `record()` so pre-validation can detect whether new
939    /// commits arrived between the pre-check and the sequencer (spec §8.5,
940    /// review fix #17).
941    version: std::sync::atomic::AtomicU64,
942}
943
944impl ConflictIndex {
945    pub fn new() -> Self {
946        Self {
947            shards: std::array::from_fn(|_| parking_lot::Mutex::new(HashMap::new())),
948            table_truncate_epochs: parking_lot::Mutex::new(HashMap::new()),
949            table_write_epochs: parking_lot::Mutex::new(HashMap::new()),
950            version: std::sync::atomic::AtomicU64::new(0),
951        }
952    }
953
954    /// Current version (incremented on every `record`). Used by the two-phase
955    /// validation: pre-validate + snapshot version → sequencer re-checks only
956    /// if the version advanced.
957    pub fn version(&self) -> u64 {
958        self.version.load(std::sync::atomic::Ordering::Acquire)
959    }
960
961    fn shard(&self, key: &WriteKey) -> &parking_lot::Mutex<HashMap<WriteKey, u64>> {
962        let mut h = std::collections::hash_map::DefaultHasher::new();
963        key.hash(&mut h);
964        let idx = (h.finish() as usize) & (CONFLICT_SHARDS - 1);
965        &self.shards[idx]
966    }
967
968    /// Returns `true` if any key was committed at an epoch strictly greater
969    /// than `read_epoch` (write–write conflict under SI; first-committer-wins).
970    pub fn conflicts(&self, keys: &[WriteKey], read_epoch: Epoch) -> bool {
971        for k in keys {
972            let s = self.shard(k);
973            if let Some(&ce) = s.lock().get(k) {
974                if ce > read_epoch.0 {
975                    return true;
976                }
977            }
978        }
979        let truncates = self.table_truncate_epochs.lock();
980        let writes = self.table_write_epochs.lock();
981        for k in keys {
982            match k {
983                WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
984                    if truncates.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
985                        return true;
986                    }
987                }
988                WriteKey::Table { table_id } => {
989                    if writes.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
990                        return true;
991                    }
992                }
993            }
994        }
995        false
996    }
997
998    /// Record every write-set key at `commit_epoch`.
999    pub fn record(&self, keys: &[WriteKey], commit_epoch: Epoch) {
1000        for k in keys {
1001            let s = self.shard(k);
1002            s.lock().insert(k.clone(), commit_epoch.0);
1003        }
1004        let mut truncates = self.table_truncate_epochs.lock();
1005        let mut writes = self.table_write_epochs.lock();
1006        for k in keys {
1007            match k {
1008                WriteKey::Table { table_id } => {
1009                    truncates
1010                        .entry(*table_id)
1011                        .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1012                        .or_insert(commit_epoch.0);
1013                }
1014                WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
1015                    writes
1016                        .entry(*table_id)
1017                        .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1018                        .or_insert(commit_epoch.0);
1019                }
1020            }
1021        }
1022        self.version
1023            .fetch_add(1, std::sync::atomic::Ordering::Release);
1024    }
1025
1026    /// Drop entries whose `commit_epoch < min_active` (they can never cause a
1027    /// future conflict once no live txn reads below `min_active`).
1028    pub fn prune_below(&self, min_active: Epoch) {
1029        for s in &self.shards {
1030            s.lock().retain(|_, ce| *ce >= min_active.0);
1031        }
1032        self.table_truncate_epochs
1033            .lock()
1034            .retain(|_, ce| *ce >= min_active.0);
1035        self.table_write_epochs
1036            .lock()
1037            .retain(|_, ce| *ce >= min_active.0);
1038    }
1039}
1040
1041impl Default for ConflictIndex {
1042    fn default() -> Self {
1043        Self::new()
1044    }
1045}
1046
1047// ── P3.2: real group commit (spec §9.3) ─────────────────────────────────
1048
1049/// Group-commit coordinator (spec §9.3). The commit sequencer appends a txn's
1050/// records under the WAL mutex but does **not** fsync there; instead each
1051/// committer calls [`Self::await_durable`] with its commit record's WAL seq.
1052/// Exactly one waiter becomes the *leader* and issues a single `group_sync`
1053/// (fsync), which makes durable every record appended up to that point; the
1054/// others are *followers* that simply wait until `durable_seq` reaches their
1055/// commit seq. One fsync therefore covers a whole batch of concurrent commits.
1056pub struct GroupCommit {
1057    inner: PlMutex<GroupState>,
1058    cv: Condvar,
1059}
1060
1061struct GroupState {
1062    durable_seq: u64,
1063    syncing: bool,
1064    poisoned: bool,
1065}
1066
1067impl GroupCommit {
1068    pub fn new(durable_seq: u64) -> Self {
1069        Self {
1070            inner: PlMutex::new(GroupState {
1071                durable_seq,
1072                syncing: false,
1073                poisoned: false,
1074            }),
1075            cv: Condvar::new(),
1076        }
1077    }
1078
1079    /// Block until `commit_seq` is durable. The first eligible caller fsyncs on
1080    /// behalf of the batch; the rest wait on the condvar. On fsync error the
1081    /// coordinator is poisoned and every waiter (current and future) returns
1082    /// `Err` (spec §9.3e). `wal` is the same `SharedWal` the sequencer appended
1083    /// to — locked here only for the brief fsync, never across the wait.
1084    pub fn await_durable(&self, wal: &PlMutex<SharedWal>, commit_seq: u64) -> Result<()> {
1085        let mut st = self.inner.lock();
1086        loop {
1087            if st.poisoned {
1088                return Err(MongrelError::Other(
1089                    "database poisoned by fsync error".into(),
1090                ));
1091            }
1092            if st.durable_seq >= commit_seq {
1093                return Ok(());
1094            }
1095            if st.syncing {
1096                // Another thread is the leader; wait for it to advance durability.
1097                self.cv.wait(&mut st);
1098                continue;
1099            }
1100            // Become the leader: fsync outside the coordinator lock (but under
1101            // the WAL lock) so followers can queue up behind us.
1102            st.syncing = true;
1103            drop(st);
1104            // ponytail: fixed 50 µs batch window; make adaptive if isolated commit latency matters.
1105            std::thread::sleep(std::time::Duration::from_micros(50));
1106            let res = wal.lock().group_sync();
1107            st = self.inner.lock();
1108            st.syncing = false;
1109            match res {
1110                Ok(durable) => {
1111                    if durable > st.durable_seq {
1112                        st.durable_seq = durable;
1113                    }
1114                    self.cv.notify_all();
1115                    // Loop re-checks: our commit_seq <= durable (group_sync makes
1116                    // everything appended-so-far durable), so we return Ok next.
1117                }
1118                Err(e) => {
1119                    st.poisoned = true;
1120                    self.cv.notify_all();
1121                    return Err(e);
1122                }
1123            }
1124        }
1125    }
1126}
1127
1128/// Tracks the `read_epoch` of every in-flight transaction (spec §9.2, review
1129/// fix #12). `begin` registers **before** the first read; `min_read_epoch`
1130/// drives conflict-index pruning.
1131pub struct ActiveTxns {
1132    inner: parking_lot::Mutex<BTreeMap<u64, u64>>,
1133}
1134
1135impl ActiveTxns {
1136    pub fn new() -> Self {
1137        Self {
1138            inner: parking_lot::Mutex::new(BTreeMap::new()),
1139        }
1140    }
1141
1142    /// Register a transaction's read epoch. Returns a guard that deregisters
1143    /// on drop.
1144    pub fn register(&self, read_epoch: Epoch) -> ActiveTxnGuard<'_> {
1145        let mut g = self.inner.lock();
1146        *g.entry(read_epoch.0).or_insert(0) += 1;
1147        ActiveTxnGuard {
1148            active: self,
1149            epoch: read_epoch.0,
1150        }
1151    }
1152
1153    /// The lowest live `read_epoch`, or `u64::MAX` when no txn is active.
1154    pub fn min_read_epoch(&self) -> u64 {
1155        self.inner.lock().keys().next().copied().unwrap_or(u64::MAX)
1156    }
1157}
1158
1159impl Default for ActiveTxns {
1160    fn default() -> Self {
1161        Self::new()
1162    }
1163}
1164
1165/// Guard for an active transaction's read-epoch registration.
1166pub struct ActiveTxnGuard<'a> {
1167    active: &'a ActiveTxns,
1168    epoch: u64,
1169}
1170
1171impl Drop for ActiveTxnGuard<'_> {
1172    fn drop(&mut self) {
1173        let mut g = self.active.inner.lock();
1174        if let Some(count) = g.get_mut(&self.epoch) {
1175            *count -= 1;
1176            if *count == 0 {
1177                g.remove(&self.epoch);
1178            }
1179        }
1180    }
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185    use super::*;
1186
1187    #[test]
1188    fn shared_transaction_allocator_never_crosses_open_generation() {
1189        let allocator = PlMutex::new((7_u64 << 32) | u32::MAX as u64);
1190        assert_eq!(
1191            allocate_txn_id(&allocator).unwrap(),
1192            (7_u64 << 32) | u32::MAX as u64
1193        );
1194        assert!(matches!(
1195            allocate_txn_id(&allocator),
1196            Err(MongrelError::Full(_))
1197        ));
1198    }
1199
1200    #[test]
1201    fn conflict_index_first_committer_wins_and_prunes_safely() {
1202        let ci = ConflictIndex::new();
1203        let k = vec![WriteKey::Row {
1204            table_id: 1,
1205            row_id: 7,
1206        }];
1207        assert!(!ci.conflicts(&k, Epoch(5)));
1208        ci.record(&k, Epoch(6));
1209        assert!(ci.conflicts(&k, Epoch(5)));
1210        assert!(!ci.conflicts(&k, Epoch(6)));
1211        ci.prune_below(Epoch(7));
1212        assert!(!ci.conflicts(&k, Epoch(5)));
1213    }
1214
1215    #[test]
1216    fn conflict_index_table_scope_conflicts_both_directions() {
1217        let ci = ConflictIndex::new();
1218        ci.record(&[WriteKey::Table { table_id: 1 }], Epoch(6));
1219        assert!(ci.conflicts(
1220            &[WriteKey::Row {
1221                table_id: 1,
1222                row_id: 7,
1223            }],
1224            Epoch(5)
1225        ));
1226        assert!(ci.conflicts(
1227            &[WriteKey::Unique {
1228                table_id: 1,
1229                index_id: 0,
1230                key_hash: 42,
1231            }],
1232            Epoch(5)
1233        ));
1234        assert!(!ci.conflicts(
1235            &[WriteKey::Row {
1236                table_id: 2,
1237                row_id: 7,
1238            }],
1239            Epoch(5)
1240        ));
1241
1242        let ci = ConflictIndex::new();
1243        ci.record(
1244            &[WriteKey::Row {
1245                table_id: 1,
1246                row_id: 7,
1247            }],
1248            Epoch(6),
1249        );
1250        assert!(ci.conflicts(&[WriteKey::Table { table_id: 1 }], Epoch(5)));
1251        assert!(!ci.conflicts(&[WriteKey::Table { table_id: 2 }], Epoch(5)));
1252    }
1253
1254    #[test]
1255    fn writekey_eq_across_variants() {
1256        let r1 = WriteKey::Row {
1257            table_id: 1,
1258            row_id: 2,
1259        };
1260        let r2 = WriteKey::Row {
1261            table_id: 1,
1262            row_id: 2,
1263        };
1264        let r3 = WriteKey::Row {
1265            table_id: 1,
1266            row_id: 3,
1267        };
1268        assert_eq!(r1, r2);
1269        assert_ne!(r1, r3);
1270
1271        let u1 = WriteKey::Unique {
1272            table_id: 1,
1273            index_id: 0,
1274            key_hash: 42,
1275        };
1276        let u2 = WriteKey::Unique {
1277            table_id: 1,
1278            index_id: 0,
1279            key_hash: 42,
1280        };
1281        assert_eq!(u1, u2);
1282        assert_ne!(r1, u1);
1283
1284        let t1 = WriteKey::Table { table_id: 5 };
1285        let t2 = WriteKey::Table { table_id: 5 };
1286        assert_eq!(t1, t2);
1287        assert_ne!(t1, r1);
1288    }
1289
1290    #[test]
1291    fn active_txns_tracks_min_read_epoch() {
1292        let at = ActiveTxns::new();
1293        assert_eq!(at.min_read_epoch(), u64::MAX);
1294        let g1 = at.register(Epoch(5));
1295        assert_eq!(at.min_read_epoch(), 5);
1296        let g2 = at.register(Epoch(3));
1297        assert_eq!(at.min_read_epoch(), 3);
1298        drop(g2);
1299        assert_eq!(at.min_read_epoch(), 5);
1300        drop(g1);
1301        assert_eq!(at.min_read_epoch(), u64::MAX);
1302    }
1303
1304    #[test]
1305    fn active_txns_dedups_same_epoch() {
1306        let at = ActiveTxns::new();
1307        let g1 = at.register(Epoch(7));
1308        let g2 = at.register(Epoch(7));
1309        assert_eq!(at.min_read_epoch(), 7);
1310        drop(g1);
1311        assert_eq!(at.min_read_epoch(), 7);
1312        drop(g2);
1313        assert_eq!(at.min_read_epoch(), u64::MAX);
1314    }
1315}
1316
1317/// Transaction isolation level. MongrelDB defaults to `Snapshot` (SI).
1318///
1319/// - `Snapshot`: reads see a consistent snapshot taken at `begin`; writes
1320///   conflict on first-committer-wins for overlapping keys.
1321/// - `ReadCommitted`: each read sees the latest committed epoch (no stale
1322///   reads within a long transaction). Weaker than Snapshot but avoids
1323///   aborts from read-write conflicts.
1324/// - `Serializable`: same as Snapshot under MongrelDB's optimistic model —
1325///   the conflict index already detects write-skew. Explicitly marked so
1326///   callers can request the strongest level without behavioral surprise.
1327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1328pub enum IsolationLevel {
1329    #[default]
1330    Snapshot,
1331    ReadCommitted,
1332    Serializable,
1333}