Skip to main content

mongreldb_kit/
txn.rs

1//! Kit transaction wrapper around a MongrelDB core transaction.
2//!
3//! Constraints are enforced with the guard-table architecture shared with the
4//! TypeScript kit:
5//!
6//! * Unique constraints reserve a row in `__kit_unique_keys` keyed by the typed
7//!   encoded unique key. Concurrent inserts of the same value collide on that
8//!   key and one transaction retries.
9//! * Foreign keys verify the parent row exists and then *touch* the parent's
10//!   `__kit_row_guards` row. A concurrent parent delete also writes that guard
11//!   key, forcing a write-write conflict so the unsafe snapshot interleaving is
12//!   impossible.
13//! * Primary keys are handled like the TypeScript kit. An auto-assigned
14//!   (sequence-default) primary key is guaranteed unique and needs no check. An
15//!   explicit single-column primary key is checked directly against the visible
16//!   rows (no guard row). Only an explicit composite primary key reserves a
17//!   `__pk_<table>` guard in `__kit_unique_keys` (it has no single native key to
18//!   probe), so a duplicate-PK insert is rejected instead of silently upserting.
19//!
20//! Reads inside a transaction use the transaction's read snapshot; writes staged
21//! earlier in the same transaction are tracked in memory so read-your-writes
22//! behaves correctly even though the core transaction cannot read its own
23//! staging.
24
25use crate::db::internal_bytes;
26use crate::error::{KitError, Result};
27use crate::internal::{cols, iso_now, ROW_GUARDS, UNIQUE_KEYS};
28use crate::query::{project_distinct, run_aggregate, run_join, run_select, ExecCtx, JoinRow};
29use crate::schema::{core_row_to_json, json_to_core, pk_to_map, row_to_core_cells, Row};
30use mongreldb_core::memtable::{Row as CoreRow, Value as CoreValue};
31use mongreldb_core::query::Condition;
32use mongreldb_core::RowId;
33use mongreldb_core::UpsertAction;
34use mongreldb_core::{NativeAgg, NativeAggResult};
35use mongreldb_kit_core::keys::{
36    decode_pk, encode_pk, encode_row_guard_key, encode_unique_key, KeyComponent, KIT_KEY_VERSION,
37};
38use mongreldb_kit_core::planner::{plan_delete, DeletePlan};
39use mongreldb_kit_core::query::{
40    AggFunc, AggregateQuery, Cte, Expr, JoinQuery, OnConflict, Query, Select,
41};
42use mongreldb_kit_core::schema::{
43    Column, ColumnType, DefaultKind, ForeignKey, Table as KitTable, UniqueConstraint,
44};
45use serde_json::{Map, Value};
46use std::cell::RefCell;
47use std::collections::{HashMap, HashSet};
48
49/// A kit transaction.
50pub struct Transaction<'a> {
51    db: &'a crate::db::Database,
52    core: mongreldb_core::txn::Transaction<'a>,
53    staged: Vec<StagedOp>,
54    /// Unique keys reserved within this (uncommitted) transaction.
55    staged_unique: Vec<StagedUnique>,
56    /// Row-guard keys already touched in this transaction (dedupe).
57    touched_guards: HashSet<String>,
58    next_temp_id: u64,
59}
60
61#[derive(Debug, Clone)]
62enum StagedOp {
63    Insert {
64        table: String,
65        values: Map<String, Value>,
66    },
67    Update {
68        table: String,
69        old_pk: String,
70        row_id: u64,
71        values: Map<String, Value>,
72    },
73    /// A delete staged in this transaction. Tracked so `staged_row_exists`
74    /// can ignore rows removed earlier in the same transaction.
75    Delete {
76        table: String,
77        pk: String,
78    },
79    Truncate {
80        table: String,
81    },
82}
83
84#[derive(Debug, Clone)]
85struct StagedUnique {
86    encoded_key: String,
87    owner_table: String,
88    owner_pk: String,
89}
90
91impl<'a> Transaction<'a> {
92    pub(crate) fn new(
93        db: &'a crate::db::Database,
94        core: mongreldb_core::txn::Transaction<'a>,
95    ) -> Self {
96        Self {
97            db,
98            core,
99            staged: Vec::new(),
100            staged_unique: Vec::new(),
101            touched_guards: HashSet::new(),
102            next_temp_id: 1,
103        }
104    }
105
106    /// Insert a row into `table`.
107    pub fn insert(&mut self, table: &str, row: Map<String, Value>) -> Result<Row> {
108        let t = self.require_table(table)?.clone();
109        self.do_insert(table, &t, row, None)
110    }
111
112    pub fn insert_returning(
113        &mut self,
114        table: &str,
115        row: Map<String, Value>,
116        returning: Vec<String>,
117    ) -> Result<Value> {
118        let inserted = self.insert(table, row)?;
119        project_returning(&inserted, &returning)
120    }
121
122    /// Insert many rows into `table` within this single transaction.
123    ///
124    /// Each row still passes through defaults, validation, and constraint checks,
125    /// but the whole batch is staged in one transaction (the caller commits once)
126    /// — far faster than a row-at-a-time begin/commit loop for bulk loads. For a
127    /// single-column primary key the existing primary keys are loaded once into a
128    /// set so the per-row duplicate check stays O(1) instead of re-scanning the
129    /// table for every row. Mirrors the TypeScript kit's `insertInto().valuesMany`.
130    pub fn insert_many(&mut self, table: &str, rows: Vec<Map<String, Value>>) -> Result<Vec<Row>> {
131        let t = self.require_table(table)?.clone();
132        // Preload the visible single-column primary keys once; explicit-PK rows in
133        // the batch are then checked (and staged) against this in-memory set.
134        let mut pk_seen: Option<HashSet<String>> = if t.primary_key.len() == 1 {
135            let mut set = HashSet::new();
136            for r in self.snapshot_rows(table)? {
137                set.insert(encoded_pk_for(&t, &r.values));
138            }
139            Some(set)
140        } else {
141            None
142        };
143        let mut out = Vec::with_capacity(rows.len());
144        for row in rows {
145            out.push(self.do_insert(table, &t, row, pk_seen.as_mut())?);
146        }
147        Ok(out)
148    }
149
150    /// Core insert path shared by [`insert`](Self::insert) and
151    /// [`insert_many`](Self::insert_many). `pk_seen`, when present, is the batch's
152    /// in-memory set of single-column primary keys used for an O(1) duplicate
153    /// check (and is updated as explicit-PK rows are staged).
154    fn do_insert(
155        &mut self,
156        table: &str,
157        t: &KitTable,
158        mut row: Map<String, Value>,
159        pk_seen: Option<&mut HashSet<String>>,
160    ) -> Result<Row> {
161        // A primary key is "explicit" when the caller supplied all of its columns
162        // in the original input (before defaults are applied); only an explicit PK
163        // can collide. An auto-assigned (sequence) PK is guaranteed unique.
164        let pk_explicit = pk_is_explicit(t, &row);
165
166        self.apply_defaults(&mut row, t)?;
167        // Normalize any column still unset to explicit null, so the stored row and
168        // the returned row agree (an omitted nullable column reads back as null).
169        for col in &t.columns {
170            row.entry(col.name.clone()).or_insert(Value::Null);
171        }
172        mongreldb_kit_core::validation::validate_row(t, &row)?;
173
174        // Validate all constraints before staging any writes.
175        self.check_unique_constraints(t, &row, None)?;
176        self.check_pk(t, &row, pk_explicit, pk_seen)?;
177        self.check_foreign_keys(t, &row)?;
178
179        // Stage guard rows + the application row atomically.
180        self.reserve_unique_guards(t, &row, None)?;
181        self.reserve_pk_guard(t, &row, pk_explicit)?;
182        self.touch_foreign_key_guards(t, &row)?;
183
184        let cells = row_to_core_cells(&row, t)?;
185        self.core.put(table, cells).map_err(KitError::from)?;
186        let temp_id = self.alloc_temp_id();
187        self.staged.push(StagedOp::Insert {
188            table: table.to_string(),
189            values: row.clone(),
190        });
191        Ok(Row {
192            row_id: temp_id,
193            values: row,
194        })
195    }
196
197    /// Update the row in `table` identified by `pk` with `patch`.
198    pub fn update(&mut self, table: &str, pk: &Value, patch: Map<String, Value>) -> Result<Row> {
199        let t = self.require_table(table)?.clone();
200        let pk_map = pk_to_map(pk, &t)?;
201        let old_row = self
202            .get_by_pk_internal(table, &pk_map)?
203            .ok_or_else(|| KitError::Integrity(format!("row not found in {table}")))?;
204
205        let patch_keys: HashSet<String> = patch.keys().cloned().collect();
206        let mut values = old_row.values.clone();
207        for (k, v) in patch {
208            if t.column(&k).is_some() {
209                values.insert(k, v);
210            }
211        }
212        self.apply_update_defaults(&mut values, &patch_keys, &t);
213        mongreldb_kit_core::validation::validate_row(&t, &values)?;
214        self.check_unique_constraints(&t, &values, Some(&old_row.values))?;
215        self.check_foreign_keys(&t, &values)?;
216
217        // Reserve new unique keys and delete stale ones.
218        self.reserve_unique_guards(&t, &values, Some(&old_row.values))?;
219        self.touch_foreign_key_guards(&t, &values)?;
220
221        let cells = row_to_core_cells(&values, &t)?;
222        self.core
223            .delete(table, RowId(old_row.row_id))
224            .map_err(KitError::from)?;
225        self.core.put(table, cells).map_err(KitError::from)?;
226        let temp_id = self.alloc_temp_id();
227        self.staged.push(StagedOp::Update {
228            table: table.to_string(),
229            old_pk: encoded_pk_for(&t, &old_row.values),
230            row_id: old_row.row_id,
231            values: values.clone(),
232        });
233        Ok(Row {
234            row_id: temp_id,
235            values,
236        })
237    }
238
239    /// Prepare a row (and its cascade/set-null children) for deletion. Returns
240    /// the engine row id of the target row; the caller is responsible for the
241    /// final engine delete so that bulk deletes can be batched.
242    fn delete_row(&mut self, table: &str, row: &Row) -> Result<u64> {
243        let t = self.require_table(table)?.clone();
244        let (plan, row_cache) = self.plan_delete(table, row)?;
245        if !plan.restricted.is_empty() {
246            let msg = format!(
247                "delete restricted by {}",
248                plan.restricted
249                    .iter()
250                    .map(|r| format!("{}.{}", r.table, r.constraint))
251                    .collect::<Vec<_>>()
252                    .join(", ")
253            );
254            return Err(KitError::Restrict(msg));
255        }
256
257        // Apply set-null updates first, then cascade deletes, finally the target.
258        // Each affected child row was already fetched during planning, so reuse it
259        // from the cache rather than re-reading it by PK.
260        for set_null in &plan.set_null {
261            let key = format!("{}:{}", set_null.table, set_null.pk);
262            if let Some(child_row) = row_cache.get(&key).cloned() {
263                self.apply_set_null(set_null, &child_row)?;
264            }
265        }
266        for del in &plan.delete {
267            if del.table == table {
268                continue; // deleted by the caller
269            }
270            let child_table = self.require_table(&del.table)?.clone();
271            let key = format!("{}:{}", del.table, del.pk);
272            if let Some(child_row) = row_cache.get(&key) {
273                let row_id = child_row.row_id;
274                let values = child_row.values.clone();
275                self.delete_guards_for(&child_table, &values)?;
276                self.core
277                    .delete(&del.table, RowId(row_id))
278                    .map_err(KitError::from)?;
279                self.staged.push(StagedOp::Delete {
280                    table: del.table.clone(),
281                    pk: del.pk.clone(),
282                });
283            }
284        }
285
286        // Clean the target's guards and force a conflict with any concurrent
287        // child insert by touching its row guard.
288        self.delete_guards_for(&t, &row.values)?;
289        self.touch_row_guard(table, &pk_components(&t, &row.values))?;
290        self.staged.push(StagedOp::Delete {
291            table: table.to_string(),
292            pk: encoded_pk_for(&t, &row.values),
293        });
294        Ok(row.row_id)
295    }
296
297    /// Delete the row in `table` identified by `pk`.
298    pub fn delete(&mut self, table: &str, pk: &Value) -> Result<()> {
299        let t = self.require_table(table)?.clone();
300        let pk_map = pk_to_map(pk, &t)?;
301        let row = self
302            .get_by_pk_internal(table, &pk_map)?
303            .ok_or_else(|| KitError::Integrity(format!("row not found in {table}")))?;
304        let row_id = self.delete_row(table, &row)?;
305        self.core
306            .delete(table, RowId(row_id))
307            .map_err(KitError::from)?;
308        Ok(())
309    }
310
311    pub fn truncate(&mut self, table: &str) -> Result<()> {
312        let t = self.require_table(table)?.clone();
313        // Repeating a truncate is harmless (the engine allows it); only block
314        // data writes that would be lost by the truncation.
315        let has_data_writes = self.staged.iter().any(|op| match op {
316            StagedOp::Insert { table: t, .. }
317            | StagedOp::Update { table: t, .. }
318            | StagedOp::Delete { table: t, .. } => t == table,
319            StagedOp::Truncate { .. } => false,
320        });
321        if has_data_writes {
322            return Err(KitError::Validation(format!(
323                "truncate cannot be combined with prior writes on {table}"
324            )));
325        }
326        if self.db.schema.tables.iter().any(|other| {
327            other.name != t.name
328                && other
329                    .foreign_keys
330                    .iter()
331                    .any(|fk| fk.references_table == t.name)
332        }) {
333            return Err(KitError::Restrict(format!(
334                "table {} is referenced by a foreign key",
335                t.name
336            )));
337        }
338        self.delete_all_guards_for_table(&t)?;
339        self.core.truncate(table).map_err(KitError::from)?;
340        self.staged.push(StagedOp::Truncate { table: t.name });
341        Ok(())
342    }
343
344    pub fn upsert(
345        &mut self,
346        table: &str,
347        row: Map<String, Value>,
348        on_conflict: OnConflict,
349        returning: Vec<String>,
350    ) -> Result<Value> {
351        let t = self.require_table(table)?.clone();
352        let mut values = row;
353        self.apply_defaults(&mut values, &t)?;
354        for col in &t.columns {
355            values.entry(col.name.clone()).or_insert(Value::Null);
356        }
357        mongreldb_kit_core::validation::validate_row(&t, &values)?;
358        let pk_map = pk_values_map(&t, &values);
359        let existing = self.get_by_pk_internal(table, &pk_map)?;
360        match (existing, on_conflict) {
361            (Some(old), OnConflict::DoNothing) => project_returning(&old, &returning),
362            (Some(old), OnConflict::DoUpdate(patch)) => {
363                let mut merged = values.clone();
364                let mut patch_keys = HashSet::new();
365                for (k, v) in patch {
366                    if t.column(&k).is_some() {
367                        merged.insert(k.clone(), v);
368                        patch_keys.insert(k);
369                    }
370                }
371                self.apply_update_defaults(&mut merged, &patch_keys, &t);
372                mongreldb_kit_core::validation::validate_row(&t, &merged)?;
373                self.check_unique_constraints(&t, &merged, Some(&old.values))?;
374                self.check_foreign_keys(&t, &merged)?;
375                self.reserve_unique_guards(&t, &merged, Some(&old.values))?;
376                self.touch_foreign_key_guards(&t, &merged)?;
377
378                let insert_cells = row_to_core_cells(&merged, &t)?;
379                let mut patch_values = Map::new();
380                for k in &patch_keys {
381                    patch_values.insert(k.clone(), merged.get(k).cloned().unwrap_or(Value::Null));
382                }
383                let update_cells = patch_to_core_cells(&patch_values, &t)?;
384
385                self.core
386                    .upsert(table, insert_cells, UpsertAction::DoUpdate(update_cells))
387                    .map_err(KitError::from)?;
388                self.staged.push(StagedOp::Update {
389                    table: table.to_string(),
390                    old_pk: encoded_pk_for(&t, &old.values),
391                    row_id: old.row_id,
392                    values: merged.clone(),
393                });
394                project_returning(
395                    &Row {
396                        row_id: old.row_id,
397                        values: merged,
398                    },
399                    &returning,
400                )
401            }
402            (None, on_conflict) => {
403                self.check_unique_constraints(&t, &values, None)?;
404                self.check_foreign_keys(&t, &values)?;
405                self.reserve_unique_guards(&t, &values, None)?;
406                let pk_explicit = pk_is_explicit(&t, &values);
407                self.reserve_pk_guard(&t, &values, pk_explicit)?;
408                self.touch_foreign_key_guards(&t, &values)?;
409                let cells = row_to_core_cells(&values, &t)?;
410                let action = match on_conflict {
411                    OnConflict::DoNothing => UpsertAction::DoNothing,
412                    OnConflict::DoUpdate(patch) => {
413                        UpsertAction::DoUpdate(patch_to_core_cells(&patch, &t)?)
414                    }
415                };
416                self.core
417                    .upsert(table, cells, action)
418                    .map_err(KitError::from)?;
419                self.staged.push(StagedOp::Insert {
420                    table: table.to_string(),
421                    values: values.clone(),
422                });
423                project_returning(&Row { row_id: 0, values }, &returning)
424            }
425        }
426    }
427
428    pub fn update_where(
429        &mut self,
430        table: &str,
431        filter: Option<Expr>,
432        patch: Map<String, Value>,
433        returning: Vec<String>,
434    ) -> Result<Vec<Value>> {
435        let t = self.require_table(table)?.clone();
436        let rows = self.select(&Query::Select(select_all(table, filter)))?;
437        let mut updates = Vec::with_capacity(rows.len());
438        let mut out = Vec::with_capacity(rows.len());
439        let patch_keys: HashSet<String> = patch.keys().cloned().collect();
440        for row in rows {
441            let mut values = row.values.clone();
442            for (k, v) in &patch {
443                if t.column(k).is_some() {
444                    values.insert(k.clone(), v.clone());
445                }
446            }
447            self.apply_update_defaults(&mut values, &patch_keys, &t);
448            mongreldb_kit_core::validation::validate_row(&t, &values)?;
449            self.check_unique_constraints(&t, &values, Some(&row.values))?;
450            self.check_foreign_keys(&t, &values)?;
451            self.reserve_unique_guards(&t, &values, Some(&row.values))?;
452            self.touch_foreign_key_guards(&t, &values)?;
453
454            let cells = row_to_core_cells(&values, &t)?;
455            updates.push((RowId(row.row_id), cells));
456            self.staged.push(StagedOp::Update {
457                table: table.to_string(),
458                old_pk: encoded_pk_for(&t, &row.values),
459                row_id: row.row_id,
460                values: values.clone(),
461            });
462            out.push(project_returning(
463                &Row {
464                    row_id: row.row_id,
465                    values,
466                },
467                &returning,
468            )?);
469        }
470        if !updates.is_empty() {
471            self.core
472                .update_many(table, updates)
473                .map_err(KitError::from)?;
474        }
475        Ok(out)
476    }
477
478    pub fn delete_where(
479        &mut self,
480        table: &str,
481        filter: Option<Expr>,
482        returning: Vec<String>,
483    ) -> Result<Vec<Value>> {
484        self.require_table(table)?;
485        let rows = self.select(&Query::Select(select_all(table, filter)))?;
486        let mut out = Vec::with_capacity(rows.len());
487        let mut ids = Vec::with_capacity(rows.len());
488        for row in rows {
489            out.push(project_returning(&row, &returning)?);
490            ids.push(self.delete_row(table, &row)?);
491        }
492        if !ids.is_empty() {
493            self.core
494                .delete_many(table, ids.into_iter().map(RowId).collect())
495                .map_err(KitError::from)?;
496        }
497        Ok(out)
498    }
499
500    /// Read a row by primary key.
501    pub fn get_by_pk(&self, table: &str, pk: &Value) -> Result<Option<Row>> {
502        let t = self.require_table(table)?;
503        let pk_map = pk_to_map(pk, t)?;
504        self.get_by_pk_internal(table, &pk_map)
505    }
506
507    /// Execute a `Select` query. Subqueries (`IN (subquery)`, `EXISTS`) and
508    /// `like`/`contains`/`not in` predicates resolve against this transaction's
509    /// read snapshot, so they may reference other tables.
510    pub fn select(&self, query: &Query) -> Result<Vec<Row>> {
511        let select = match query {
512            Query::Select(s) => s,
513            _ => return Err(KitError::Validation("only SELECT supported".into())),
514        };
515        let ctx = self.exec_ctx();
516        run_select(&ctx, select)
517    }
518
519    /// Like [`select`](Self::select) but drops duplicate rows. When the select
520    /// projects columns, duplicates are decided on the projection (true
521    /// `SELECT DISTINCT col, ...`); otherwise on the whole row.
522    pub fn select_distinct(&self, query: &Query) -> Result<Vec<Row>> {
523        let select = match query {
524            Query::Select(s) => s,
525            _ => return Err(KitError::Validation("only SELECT supported".into())),
526        };
527        let ctx = self.exec_ctx();
528        let rows = run_select(&ctx, select)?;
529        Ok(project_distinct(select, rows))
530    }
531
532    /// Materialize each CTE in order (a later CTE may read an earlier one) and
533    /// run `body` with those named results available as virtual tables.
534    pub fn select_with(&self, ctes: &[Cte], body: &Select) -> Result<Vec<Row>> {
535        let mut ctx = self.exec_ctx();
536        for cte in ctes {
537            let rows = run_select(&ctx, &cte.query)?;
538            ctx.add_cte(cte.name.clone(), rows);
539        }
540        run_select(&ctx, body)
541    }
542
543    /// Run an aggregate / group-by / having query. Returns one row per group
544    /// (group-key columns plus the aggregate aliases); with no `group_by` the
545    /// whole filtered table is a single group.
546    pub fn aggregate(&self, query: &AggregateQuery) -> Result<Vec<Row>> {
547        if let Some(rows) = self.try_native_aggregate(query)? {
548            return Ok(rows);
549        }
550        let ctx = self.exec_ctx();
551        run_aggregate(&ctx, query)
552    }
553
554    /// Kit Priority 7: serve a single ungrouped aggregate from the engine
555    /// (survivor cardinality / page stats / vectorized cursor) with no row
556    /// materialization. Covers `COUNT(*)`, `COUNT(col)`, `SUM`, `MIN`, `MAX`,
557    /// `AVG`. Returns `None` (caller scans) unless the result is provably
558    /// identical to the row-scan path: no `GROUP BY`/`HAVING`, a single
559    /// aggregate, no staged writes for the table, and a fully + exactly
560    /// translated (or absent) filter. The engine reach methods additionally
561    /// require the read snapshot to be the latest committed epoch, and fall
562    /// back (`None`) for non-numeric columns or multi-run/overlay layouts.
563    fn try_native_aggregate(&self, query: &AggregateQuery) -> Result<Option<Vec<Row>>> {
564        if !query.group_by.is_empty() || query.having.is_some() || query.aggregates.len() != 1 {
565            return Ok(None);
566        }
567        let agg = &query.aggregates[0];
568        if self.has_staged_for(&query.table) {
569            return Ok(None); // engine aggregate omits this transaction's staged writes
570        }
571
572        // COUNT(DISTINCT col), unfiltered, over a bitmap-indexed column ⇒ the
573        // bitmap's partition cardinality (no scan). `count_distinct_from_bitmap`
574        // is whole-column, so it applies only without a filter; other DISTINCT
575        // shapes (filtered, non-indexed, or DISTINCT SUM/MIN/MAX/AVG) fall back.
576        if agg.distinct {
577            if !matches!(agg.func, AggFunc::Count) || query.filter.is_some() {
578                return Ok(None);
579            }
580            let Some(col_name) = &agg.column else {
581                return Ok(None);
582            };
583            let t = self.require_table(&query.table)?;
584            let Some(col) = t.columns.iter().find(|c| &c.name == col_name) else {
585                return Ok(None);
586            };
587            let Some(n) = self.db.count_distinct_core_at(
588                &query.table,
589                col.id as u16,
590                self.core.read_snapshot(),
591            )?
592            else {
593                return Ok(None);
594            };
595            let mut values = Map::new();
596            values.insert(agg.alias.clone(), Value::Number((n as i64).into()));
597            return Ok(Some(vec![Row { row_id: 0, values }]));
598        }
599
600        let conditions: Vec<Condition> = match &query.filter {
601            None => Vec::new(),
602            Some(filter) => {
603                let t = self.require_table(&query.table)?;
604                match crate::pushdown::translate_predicate(t, filter) {
605                    // A residual / inexact filter would skew the result ⇒ scan.
606                    Some(plan) if plan.can_push() && plan.fully_translated => plan.conditions,
607                    _ => return Ok(None),
608                }
609            }
610        };
611        let snapshot = self.core.read_snapshot();
612
613        let value: Value = match (agg.func, &agg.column) {
614            // COUNT(*): survivor cardinality (filtered) / O(1) live_count.
615            (AggFunc::Count, None) => {
616                let Some(n) = self
617                    .db
618                    .count_core_rows_at(&query.table, &conditions, snapshot)?
619                else {
620                    return Ok(None);
621                };
622                Value::Number((n as i64).into())
623            }
624            // COUNT(col)/SUM/MIN/MAX/AVG over a column: engine page stats /
625            // vectorized cursor. SUM/MIN/MAX/AVG without a column is invalid.
626            (func, Some(col_name)) => {
627                let t = self.require_table(&query.table)?;
628                let Some(col) = t.columns.iter().find(|c| &c.name == col_name) else {
629                    return Ok(None);
630                };
631                let native = match func {
632                    AggFunc::Count => NativeAgg::Count,
633                    AggFunc::Sum => NativeAgg::Sum,
634                    AggFunc::Min => NativeAgg::Min,
635                    AggFunc::Max => NativeAgg::Max,
636                    AggFunc::Avg => NativeAgg::Avg,
637                };
638                let Some(result) = self.db.aggregate_core_at(
639                    &query.table,
640                    Some(col.id as u16),
641                    &conditions,
642                    native,
643                    snapshot,
644                )?
645                else {
646                    return Ok(None);
647                };
648                // Map the engine result to a JSON value matching the in-Rust
649                // path: COUNT/Int → integer, Float → float, no inputs → NULL.
650                match result {
651                    NativeAggResult::Count(n) => Value::Number((n as i64).into()),
652                    NativeAggResult::Int(x) => Value::Number(x.into()),
653                    NativeAggResult::Float(f) => serde_json::Number::from_f64(f)
654                        .map(Value::Number)
655                        .unwrap_or(Value::Null),
656                    NativeAggResult::Null => Value::Null,
657                }
658            }
659            // SUM/MIN/MAX/AVG with no column is not a valid shape here ⇒ scan.
660            _ => return Ok(None),
661        };
662
663        let mut values = Map::new();
664        values.insert(agg.alias.clone(), value);
665        Ok(Some(vec![Row { row_id: 0, values }]))
666    }
667
668    /// Run a nested-loop join. Each result row is a map keyed by table alias; see
669    /// [`JoinQuery`] for the shape. Supports inner, left, and cross joins.
670    pub fn join(&self, query: &JoinQuery) -> Result<Vec<JoinRow>> {
671        let ctx = self.exec_ctx();
672        run_join(&ctx, query)
673    }
674
675    /// Approximate nearest-neighbour search: return the `k` rows whose
676    /// `Embedding` column is closest to `query`, resolved by the column's ANN
677    /// (HNSW) index. Requires an `Ann` index on `column`. Results are the top-`k`
678    /// survivor rows (as a set — distance ranking is not currently surfaced).
679    pub fn ann_search(
680        &self,
681        table: &str,
682        column: &str,
683        query: Vec<f32>,
684        k: usize,
685    ) -> Result<Vec<Row>> {
686        let t = self.require_table(table)?;
687        let col = t
688            .column(column)
689            .ok_or_else(|| KitError::Validation(format!("unknown column \"{column}\"")))?;
690        let cond = Condition::Ann {
691            column_id: col.id as u16,
692            query,
693            k,
694        };
695        self.snapshot_rows_pushed(table, &[cond])
696    }
697
698    /// Learned-sparse (SPLADE-style) retrieval: return the `k` rows whose
699    /// `Sparse` column best matches the weighted query tokens `query`
700    /// (`(token_id, weight)` pairs), resolved by the column's sparse index.
701    /// Requires a `Sparse` index on `column`.
702    pub fn sparse_match(
703        &self,
704        table: &str,
705        column: &str,
706        query: Vec<(u32, f32)>,
707        k: usize,
708    ) -> Result<Vec<Row>> {
709        let t = self.require_table(table)?;
710        let col = t
711            .column(column)
712            .ok_or_else(|| KitError::Validation(format!("unknown column \"{column}\"")))?;
713        let cond = Condition::SparseMatch {
714            column_id: col.id as u16,
715            query,
716            k,
717        };
718        self.snapshot_rows_pushed(table, &[cond])
719    }
720
721    pub fn execute(&mut self, query: &Query) -> Result<Vec<Value>> {
722        match query {
723            Query::Select(_) => Err(KitError::Validation("use select() for SELECT".into())),
724            Query::Insert(insert) => Ok(vec![self.insert_returning(
725                &insert.table,
726                insert.values.clone(),
727                insert.returning.clone(),
728            )?]),
729            Query::Upsert(upsert) => Ok(vec![self.upsert(
730                &upsert.table,
731                upsert.values.clone(),
732                upsert.on_conflict.clone(),
733                upsert.returning.clone(),
734            )?]),
735            Query::Update(update) => {
736                if update.pk.is_some() && update.filter.is_some() {
737                    return Err(KitError::Validation(
738                        "update cannot specify both pk and filter".into(),
739                    ));
740                }
741                if let Some(pk) = &update.pk {
742                    let row = self.update(&update.table, pk, update.set.clone())?;
743                    Ok(vec![project_returning(&row, &update.returning)?])
744                } else if let Some(filter) = &update.filter {
745                    self.update_where(
746                        &update.table,
747                        Some(filter.clone()),
748                        update.set.clone(),
749                        update.returning.clone(),
750                    )
751                } else {
752                    Err(KitError::Validation(
753                        "update requires either a pk or a filter".into(),
754                    ))
755                }
756            }
757            Query::Delete(delete) => {
758                if delete.pk.is_some() && delete.filter.is_some() {
759                    return Err(KitError::Validation(
760                        "delete cannot specify both pk and filter".into(),
761                    ));
762                }
763                if let Some(pk) = &delete.pk {
764                    let t = self.require_table(&delete.table)?;
765                    let pk_map = pk_to_map(pk, t)?;
766                    let projected = match self.get_by_pk_internal(&delete.table, &pk_map)? {
767                        Some(row) => project_returning(&row, &delete.returning)?,
768                        None => {
769                            // Propagate the NotFound error from the canonical delete path.
770                            self.delete(&delete.table, pk)?;
771                            unreachable!("delete returned Ok for a missing row")
772                        }
773                    };
774                    self.delete(&delete.table, pk)?;
775                    Ok(vec![projected])
776                } else if let Some(filter) = &delete.filter {
777                    self.delete_where(
778                        &delete.table,
779                        Some(filter.clone()),
780                        delete.returning.clone(),
781                    )
782                } else {
783                    Err(KitError::Validation(
784                        "delete requires either a pk or a filter".into(),
785                    ))
786                }
787            }
788            Query::Aggregate(_) | Query::Join(_) => Err(KitError::Validation(
789                "aggregate/join are not mutating statements".into(),
790            )),
791        }
792    }
793
794    /// Build an execution context whose table fetcher reads visible rows at this
795    /// transaction's read snapshot. When conditions are provided, the fetcher
796    /// resolves them via native indexes (Kit Priority 1 pushdown).
797    fn exec_ctx(&self) -> ExecCtx<'_> {
798        ExecCtx::new(
799            Some(&self.db.schema),
800            |name: &str, conds: Option<&[Condition]>| match conds {
801                Some(c) if !c.is_empty() => self.snapshot_rows_pushed(name, c),
802                _ => self.snapshot_rows(name),
803            },
804        )
805    }
806
807    /// Commit the transaction.
808    ///
809    /// A transaction that staged a large batch on some table (typically via
810    /// [`Self::insert_many`]) flushes that table afterward. Without this, a
811    /// committed-but-unflushed batch exists only as WAL records: every
812    /// subsequent `Database::open` (there is no warm/daemon mode for the CLI,
813    /// and any short-lived process pays this on every invocation) must fully
814    /// replay and re-index the whole batch just to open the table, which is
815    /// far more expensive than the one-time flush. Below the threshold this
816    /// is a no-op cost (the loop below runs over `self.staged`, already in
817    /// memory) so ordinary interactive transactions are unaffected.
818    pub fn commit(self) -> Result<()> {
819        const FLUSH_AFTER_ROWS: usize = 1000;
820        let mut counts: HashMap<&str, usize> = HashMap::new();
821        for op in &self.staged {
822            let table = match op {
823                StagedOp::Insert { table, .. }
824                | StagedOp::Update { table, .. }
825                | StagedOp::Delete { table, .. }
826                | StagedOp::Truncate { table } => table.as_str(),
827            };
828            *counts.entry(table).or_default() += 1;
829        }
830        let tables_to_flush: Vec<&str> = counts
831            .into_iter()
832            .filter(|&(_, n)| n >= FLUSH_AFTER_ROWS)
833            .map(|(table, _)| table)
834            .collect();
835        let db = self.db;
836        self.core.commit().map_err(KitError::from)?;
837        for table in tables_to_flush {
838            let _ = db.flush_table(table);
839        }
840        Ok(())
841    }
842
843    /// Roll back the transaction.
844    pub fn rollback(self) {
845        self.core.rollback();
846    }
847
848    // ── internals ──────────────────────────────────────────────────────────
849
850    fn alloc_temp_id(&mut self) -> u64 {
851        let id = self.next_temp_id;
852        self.next_temp_id += 1;
853        id
854    }
855
856    fn require_table(&self, name: &str) -> Result<&KitTable> {
857        self.db
858            .table(name)
859            .ok_or_else(|| KitError::Integrity(format!("table {name} not found")))
860    }
861
862    /// All rows of `table` visible to this transaction (staged writes included).
863    pub fn all_rows(&self, table: &str) -> Result<Vec<Row>> {
864        self.snapshot_rows(table)
865    }
866
867    fn snapshot_rows(&self, table: &str) -> Result<Vec<Row>> {
868        let t = self.require_table(table)?;
869        let core_rows = self
870            .db
871            .visible_core_rows_at(table, self.core.read_snapshot())?;
872        let mut rows = core_rows
873            .into_iter()
874            .map(|r| core_row_to_json(&r, t))
875            .collect::<Result<Vec<_>>>()?;
876        self.replay_staged_rows(t, &mut rows);
877        Ok(rows)
878    }
879
880    /// Fetch rows for `table` with native `conditions` resolved by the engine
881    /// (Kit Priority 1 pushdown). Avoids the full scan that `snapshot_rows`
882    /// does — the engine resolves conditions via HOT/bitmap/range indexes.
883    fn snapshot_rows_pushed(&self, table: &str, conditions: &[Condition]) -> Result<Vec<Row>> {
884        let t = self.require_table(table)?;
885        if self.has_staged_for(table) {
886            return Ok(self
887                .snapshot_rows(table)?
888                .into_iter()
889                .filter(|r| row_matches_conditions(t, r, conditions))
890                .collect());
891        }
892        let core_rows = self
893            .db
894            .query_core_rows_at(table, conditions, self.core.read_snapshot())?;
895        core_rows
896            .into_iter()
897            .map(|r| core_row_to_json(&r, t))
898            .collect()
899    }
900
901    fn get_by_pk_internal(&self, table: &str, pk_map: &Map<String, Value>) -> Result<Option<Row>> {
902        let t = self.require_table(table)?;
903        if self.has_staged_for(table) {
904            let rows = self.snapshot_rows(table)?;
905            return Ok(rows.into_iter().find(|r| pk_matches(&r.values, pk_map, t)));
906        }
907        // Kit Priority 1 pushdown: use native PK index (O(1) HOT probe) instead
908        // of O(N) full scan. Falls back to scan if conditions can't be built.
909        if let Some(conditions) = crate::pushdown::pk_conditions(t, pk_map) {
910            let core_rows =
911                self.db
912                    .query_core_rows_at(table, &conditions, self.core.read_snapshot())?;
913            return Ok(core_rows
914                .into_iter()
915                .filter_map(|r| core_row_to_json(&r, t).ok())
916                .find(|r| pk_matches(&r.values, pk_map, t)));
917        }
918        let rows = self.snapshot_rows(table)?;
919        Ok(rows.into_iter().find(|r| pk_matches(&r.values, pk_map, t)))
920    }
921
922    fn internal_rows(&self, table: &str) -> Result<Vec<CoreRow>> {
923        self.db
924            .visible_core_rows_at(table, self.core.read_snapshot())
925    }
926
927    // ── unique constraints ─────────────────────────────────────────────────
928
929    /// Reject the row if any of its unique keys is already owned by a different
930    /// row, either in committed state or in this transaction's staging.
931    fn check_unique_constraints(
932        &self,
933        table: &KitTable,
934        values: &Map<String, Value>,
935        old_values: Option<&Map<String, Value>>,
936    ) -> Result<()> {
937        if table.unique_constraints.is_empty() {
938            return Ok(());
939        }
940        let owner_pk = encoded_pk_for(table, values);
941        let committed = self.internal_rows(UNIQUE_KEYS)?;
942        for uq in &table.unique_constraints {
943            let Some(key) = unique_key(table, uq, values) else {
944                continue;
945            };
946            // Unchanged keys (update where the value did not move) are fine.
947            if let Some(old) = old_values {
948                if unique_key(table, uq, old).as_deref() == Some(key.as_str()) {
949                    continue;
950                }
951            }
952            self.assert_key_free(&committed, table, uq, &key, &owner_pk)?;
953        }
954        Ok(())
955    }
956
957    fn assert_key_free(
958        &self,
959        committed: &[CoreRow],
960        table: &KitTable,
961        uq: &UniqueConstraint,
962        key: &str,
963        owner_pk: &str,
964    ) -> Result<()> {
965        for guard in committed {
966            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(key) {
967                let g_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
968                let g_pk = internal_bytes(guard, cols::UQ_OWNER_PK).unwrap_or_default();
969                if g_table != table.name || g_pk != owner_pk {
970                    return Err(KitError::Duplicate(format!(
971                        "unique constraint {} on {}",
972                        uq.name, table.name
973                    )));
974                }
975            }
976        }
977        for staged in &self.staged_unique {
978            if staged.encoded_key == key
979                && (staged.owner_table != table.name || staged.owner_pk != owner_pk)
980            {
981                return Err(KitError::Duplicate(format!(
982                    "unique constraint {} on {}",
983                    uq.name, table.name
984                )));
985            }
986        }
987        Ok(())
988    }
989
990    /// Reserve new unique guard rows for `values`, deleting any stale guards that
991    /// belonged to the previous version of the row (on update).
992    fn reserve_unique_guards(
993        &mut self,
994        table: &KitTable,
995        values: &Map<String, Value>,
996        old_values: Option<&Map<String, Value>>,
997    ) -> Result<()> {
998        let owner_pk = encoded_pk_for(table, values);
999        for uq in &table.unique_constraints {
1000            let new_key = unique_key(table, uq, values);
1001            let old_key = old_values.and_then(|old| unique_key(table, uq, old));
1002            if new_key == old_key {
1003                continue;
1004            }
1005            if let Some(key) = &new_key {
1006                self.put_unique_guard(&uq.name, key, &table.name, &owner_pk)?;
1007            }
1008            if let Some(key) = &old_key {
1009                self.delete_unique_guard(key)?;
1010            }
1011        }
1012        Ok(())
1013    }
1014
1015    fn put_unique_guard(
1016        &mut self,
1017        constraint: &str,
1018        encoded_key: &str,
1019        owner_table: &str,
1020        owner_pk: &str,
1021    ) -> Result<()> {
1022        let now = iso_now();
1023        self.core
1024            .put(
1025                UNIQUE_KEYS,
1026                vec![
1027                    (cols::UQ_ENCODED, CoreValue::Bytes(encoded_key.into())),
1028                    (cols::UQ_CONSTRAINT, CoreValue::Bytes(constraint.into())),
1029                    (cols::UQ_OWNER_TABLE, CoreValue::Bytes(owner_table.into())),
1030                    (cols::UQ_OWNER_PK, CoreValue::Bytes(owner_pk.into())),
1031                    (cols::UQ_CREATED, CoreValue::Bytes(now.into_bytes())),
1032                ],
1033            )
1034            .map_err(KitError::from)?;
1035        self.staged_unique.push(StagedUnique {
1036            encoded_key: encoded_key.to_string(),
1037            owner_table: owner_table.to_string(),
1038            owner_pk: owner_pk.to_string(),
1039        });
1040        Ok(())
1041    }
1042
1043    fn delete_unique_guard(&mut self, encoded_key: &str) -> Result<()> {
1044        let committed = self.internal_rows(UNIQUE_KEYS)?;
1045        for guard in &committed {
1046            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(encoded_key) {
1047                self.core
1048                    .delete(UNIQUE_KEYS, guard.row_id)
1049                    .map_err(KitError::from)?;
1050            }
1051        }
1052        self.staged_unique.retain(|s| s.encoded_key != encoded_key);
1053        Ok(())
1054    }
1055
1056    /// Delete every unique guard owned by the given row (used on row delete).
1057    fn delete_unique_guards_for_owner(&mut self, table: &KitTable, owner_pk: &str) -> Result<()> {
1058        let constraint_names: HashSet<&str> = table
1059            .unique_constraints
1060            .iter()
1061            .map(|u| u.name.as_str())
1062            .collect();
1063        let committed = self.internal_rows(UNIQUE_KEYS)?;
1064        for guard in &committed {
1065            let g_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
1066            let g_pk = internal_bytes(guard, cols::UQ_OWNER_PK).unwrap_or_default();
1067            let g_constraint = internal_bytes(guard, cols::UQ_CONSTRAINT).unwrap_or_default();
1068            if g_table == table.name
1069                && g_pk == owner_pk
1070                && (constraint_names.contains(g_constraint.as_str())
1071                    || g_constraint == pk_guard_constraint(table))
1072            {
1073                self.core
1074                    .delete(UNIQUE_KEYS, guard.row_id)
1075                    .map_err(KitError::from)?;
1076            }
1077        }
1078        let owner_pk = owner_pk.to_string();
1079        let name = table.name.clone();
1080        self.staged_unique
1081            .retain(|s| !(s.owner_table == name && s.owner_pk == owner_pk));
1082        Ok(())
1083    }
1084
1085    // ── primary-key handling ───────────────────────────────────────────────
1086    //
1087    // Matches the TypeScript kit: an auto-assigned (sequence) primary key is
1088    // guaranteed unique and skipped; an explicit single-column primary key is
1089    // checked directly against the visible rows (no guard row); only an explicit
1090    // composite primary key reserves a `__pk_<table>` guard in `__kit_unique_keys`
1091    // (it has no single native key to probe), making the duplicate insert throw
1092    // and stay conflict-safe.
1093
1094    fn check_pk(
1095        &self,
1096        table: &KitTable,
1097        values: &Map<String, Value>,
1098        pk_explicit: bool,
1099        pk_seen: Option<&mut HashSet<String>>,
1100    ) -> Result<()> {
1101        // An auto-assigned primary key is unique by construction; nothing to do.
1102        if table.primary_key.is_empty() || !pk_explicit {
1103            return Ok(());
1104        }
1105
1106        if table.primary_key.len() == 1 {
1107            // A single-column explicit PK has a native key, so check whether a row
1108            // with that PK already exists. A batch passes a pre-loaded set so the
1109            // check stays O(1) per row; a single insert checks the visible rows
1110            // (committed plus this transaction's in-flight staging) directly.
1111            let duplicate = match pk_seen {
1112                Some(seen) => {
1113                    let key = encoded_pk_for(table, values);
1114                    if seen.contains(&key) {
1115                        true
1116                    } else {
1117                        seen.insert(key);
1118                        false
1119                    }
1120                }
1121                None => {
1122                    self.parent_exists(&table.name, values)?
1123                        || self.staged_row_exists(table, values)
1124                }
1125            };
1126            if duplicate {
1127                return Err(KitError::Duplicate(format!(
1128                    "primary key {} on {}",
1129                    encoded_pk_for(table, values),
1130                    table.name
1131                )));
1132            }
1133            return Ok(());
1134        }
1135
1136        // A composite explicit PK uses a guard row (conflict-safe), like the
1137        // unique-constraint machinery.
1138        let key = pk_guard_key(table, values);
1139        let owner_pk = encoded_pk_for(table, values);
1140        let committed = self.internal_rows(UNIQUE_KEYS)?;
1141        for guard in &committed {
1142            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(key.as_str()) {
1143                return Err(KitError::Duplicate(format!(
1144                    "primary key {} on {}",
1145                    owner_pk, table.name
1146                )));
1147            }
1148        }
1149        for staged in &self.staged_unique {
1150            if staged.encoded_key == key {
1151                return Err(KitError::Duplicate(format!(
1152                    "primary key {} on {}",
1153                    owner_pk, table.name
1154                )));
1155            }
1156        }
1157        Ok(())
1158    }
1159
1160    fn reserve_pk_guard(
1161        &mut self,
1162        table: &KitTable,
1163        values: &Map<String, Value>,
1164        pk_explicit: bool,
1165    ) -> Result<()> {
1166        // Only an explicit composite primary key needs a guard row. A single-column
1167        // PK is checked directly, and an auto-assigned PK is guaranteed unique.
1168        if table.primary_key.len() < 2 || !pk_explicit {
1169            return Ok(());
1170        }
1171        let key = pk_guard_key(table, values);
1172        let owner_pk = encoded_pk_for(table, values);
1173        let constraint = pk_guard_constraint(table);
1174        self.put_unique_guard(&constraint, &key, &table.name, &owner_pk)
1175    }
1176
1177    // ── foreign keys ───────────────────────────────────────────────────────
1178
1179    fn check_foreign_keys(&self, table: &KitTable, values: &Map<String, Value>) -> Result<()> {
1180        for fk in &table.foreign_keys {
1181            if fk_values_null(fk, values) {
1182                continue;
1183            }
1184            let parent = self.require_table(&fk.references_table)?;
1185            let parent_pk = parent_pk_value(values, fk, parent)?;
1186            let parent_pk_map = pk_to_map(&parent_pk, parent)?;
1187            if self.parent_exists(&fk.references_table, &parent_pk_map)?
1188                || self.staged_row_exists(parent, &parent_pk_map)
1189            {
1190                continue;
1191            }
1192            return Err(KitError::ForeignKey(format!(
1193                "{} references {}({})",
1194                fk.name,
1195                fk.references_table,
1196                fk.references_columns.join(",")
1197            )));
1198        }
1199        Ok(())
1200    }
1201
1202    fn touch_foreign_key_guards(
1203        &mut self,
1204        table: &KitTable,
1205        values: &Map<String, Value>,
1206    ) -> Result<()> {
1207        for fk in table.foreign_keys.clone() {
1208            if fk_values_null(&fk, values) {
1209                continue;
1210            }
1211            let parent = self.require_table(&fk.references_table)?.clone();
1212            let components = parent_pk_components(values, &fk, &parent);
1213            self.touch_row_guard(&parent.name, &components)?;
1214        }
1215        Ok(())
1216    }
1217
1218    fn parent_exists(&self, table: &str, pk_map: &Map<String, Value>) -> Result<bool> {
1219        let t = self.require_table(table)?;
1220        if self.has_staged_for(table) {
1221            let rows = self.snapshot_rows(table)?;
1222            return Ok(rows.iter().any(|r| pk_matches(&r.values, pk_map, t)));
1223        }
1224        // Kit Priority 1 pushdown: O(1) HOT probe instead of O(N) full scan for
1225        // FK parent existence checks.
1226        if let Some(conditions) = crate::pushdown::pk_conditions(t, pk_map) {
1227            let core_rows =
1228                self.db
1229                    .query_core_rows_at(table, &conditions, self.core.read_snapshot())?;
1230            return Ok(!core_rows.is_empty());
1231        }
1232        let rows = self.snapshot_rows(table)?;
1233        Ok(rows.iter().any(|r| pk_matches(&r.values, pk_map, t)))
1234    }
1235
1236    /// Whether a row identified by `pk_map` exists in `table`'s in-flight staging
1237    /// for this transaction. Replays the staging in order (table-scoped) so a row
1238    /// inserted and then deleted within the same transaction is not treated as
1239    /// present. Used both for foreign-key parent checks and for the single-column
1240    /// primary-key duplicate check.
1241    fn staged_row_exists(&self, table: &KitTable, pk_map: &Map<String, Value>) -> bool {
1242        let target = encode_pk(&pk_components(table, pk_map));
1243        let mut exists = false;
1244        for staged in &self.staged {
1245            match staged {
1246                StagedOp::Insert { table: t, values }
1247                | StagedOp::Update {
1248                    table: t, values, ..
1249                } => {
1250                    if t == &table.name && pk_matches(values, pk_map, table) {
1251                        exists = true;
1252                    }
1253                }
1254                StagedOp::Delete { table: t, pk } => {
1255                    if t == &table.name && *pk == target {
1256                        exists = false;
1257                    }
1258                }
1259                StagedOp::Truncate { table: t } => {
1260                    if t == &table.name {
1261                        exists = false;
1262                    }
1263                }
1264            }
1265        }
1266        exists
1267    }
1268
1269    // ── row guards ─────────────────────────────────────────────────────────
1270
1271    fn touch_row_guard(&mut self, table: &str, pk_components: &[KeyComponent]) -> Result<()> {
1272        let encoded_pk = encode_pk(pk_components);
1273        let guard_key = encode_row_guard_key(table, &encoded_pk);
1274        if !self.touched_guards.insert(guard_key.clone()) {
1275            return Ok(());
1276        }
1277        // Replace any existing committed guard, bumping the version. `RG_ENCODED`
1278        // is `ROW_GUARDS`' primary key, so an indexed point lookup (matching at
1279        // most one row) replaces the full-table scan `internal_rows` would do.
1280        let mut version = 1i64;
1281        let committed = self.db.query_core_rows_at(
1282            ROW_GUARDS,
1283            &[Condition::Pk(guard_key.as_bytes().to_vec())],
1284            self.core.read_snapshot(),
1285        )?;
1286        for guard in &committed {
1287            if internal_bytes(guard, cols::RG_ENCODED).as_deref() == Some(guard_key.as_str()) {
1288                if let Some(CoreValue::Int64(v)) = guard.columns.get(&cols::RG_VERSION) {
1289                    version = v + 1;
1290                }
1291                self.core
1292                    .delete(ROW_GUARDS, guard.row_id)
1293                    .map_err(KitError::from)?;
1294            }
1295        }
1296        let now = iso_now();
1297        self.core
1298            .put(
1299                ROW_GUARDS,
1300                vec![
1301                    (cols::RG_ENCODED, CoreValue::Bytes(guard_key.into_bytes())),
1302                    (cols::RG_TABLE, CoreValue::Bytes(table.into())),
1303                    (cols::RG_PK, CoreValue::Bytes(encoded_pk.into_bytes())),
1304                    (cols::RG_VERSION, CoreValue::Int64(version)),
1305                    (cols::RG_UPDATED, CoreValue::Bytes(now.into_bytes())),
1306                ],
1307            )
1308            .map_err(KitError::from)?;
1309        Ok(())
1310    }
1311
1312    fn has_staged_for(&self, table: &str) -> bool {
1313        self.staged.iter().any(|op| match op {
1314            StagedOp::Insert { table: t, .. }
1315            | StagedOp::Update { table: t, .. }
1316            | StagedOp::Delete { table: t, .. }
1317            | StagedOp::Truncate { table: t } => t == table,
1318        })
1319    }
1320
1321    fn replay_staged_rows(&self, table: &KitTable, rows: &mut Vec<Row>) {
1322        for staged in &self.staged {
1323            match staged {
1324                StagedOp::Insert { table: t, values } if t == &table.name => {
1325                    let pk = encoded_pk_for(table, values);
1326                    rows.retain(|r| encoded_pk_for(table, &r.values) != pk);
1327                    rows.push(Row {
1328                        row_id: 0,
1329                        values: values.clone(),
1330                    });
1331                }
1332                StagedOp::Update {
1333                    table: t,
1334                    old_pk,
1335                    row_id,
1336                    values,
1337                } if t == &table.name => {
1338                    let new_pk = encoded_pk_for(table, values);
1339                    rows.retain(|r| {
1340                        let pk = encoded_pk_for(table, &r.values);
1341                        pk != *old_pk && pk != new_pk
1342                    });
1343                    rows.push(Row {
1344                        row_id: *row_id,
1345                        values: values.clone(),
1346                    });
1347                }
1348                StagedOp::Delete { table: t, pk } if t == &table.name => {
1349                    rows.retain(|r| encoded_pk_for(table, &r.values) != *pk);
1350                }
1351                StagedOp::Truncate { table: t } if t == &table.name => {
1352                    rows.clear();
1353                }
1354                _ => {}
1355            }
1356        }
1357    }
1358
1359    /// Remove unique + pk guards for a row that is being deleted.
1360    fn delete_guards_for(&mut self, table: &KitTable, values: &Map<String, Value>) -> Result<()> {
1361        let owner_pk = encoded_pk_for(table, values);
1362        self.delete_unique_guards_for_owner(table, &owner_pk)
1363    }
1364
1365    fn delete_all_guards_for_table(&mut self, table: &KitTable) -> Result<()> {
1366        for guard in self.internal_rows(UNIQUE_KEYS)? {
1367            let owner_table = internal_bytes(&guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
1368            if owner_table == table.name {
1369                self.core
1370                    .delete(UNIQUE_KEYS, guard.row_id)
1371                    .map_err(KitError::from)?;
1372            }
1373        }
1374        for guard in self.internal_rows(ROW_GUARDS)? {
1375            let owner_table = internal_bytes(&guard, cols::RG_TABLE).unwrap_or_default();
1376            if owner_table == table.name {
1377                self.core
1378                    .delete(ROW_GUARDS, guard.row_id)
1379                    .map_err(KitError::from)?;
1380            }
1381        }
1382        self.staged_unique.retain(|s| s.owner_table != table.name);
1383        self.touched_guards
1384            .retain(|key| !key.starts_with(&format!("{}:", table.name)));
1385        Ok(())
1386    }
1387
1388    // ── delete planning ────────────────────────────────────────────────────
1389
1390    fn plan_delete(&self, table: &str, row: &Row) -> Result<(DeletePlan, HashMap<String, Row>)> {
1391        let schema = &self.db.schema;
1392        let t = self.require_table(table)?;
1393        let pk_str = encoded_pk_for(t, &row.values);
1394        // Cache every child row discovered while planning, keyed by
1395        // "<table>:<encoded_pk>", so the apply phase can reuse it instead of
1396        // re-reading each row by PK — which would make a bulk cascade / set-null
1397        // delete O(n^2) (one full table scan per affected child row).
1398        let row_cache: RefCell<HashMap<String, Row>> = RefCell::new(HashMap::new());
1399        let find_children =
1400            |child_table: &KitTable, fk: &ForeignKey, parent_pk: &str| -> Vec<(String, String)> {
1401                let parent_pk_value = match pk_string_to_value(parent_pk, t) {
1402                    Ok(v) => v,
1403                    Err(_) => return Vec::new(),
1404                };
1405                let parent_pk_map = match pk_to_map(&parent_pk_value, t) {
1406                    Ok(m) => m,
1407                    Err(_) => return Vec::new(),
1408                };
1409                let child_rows = match self.snapshot_rows(&child_table.name) {
1410                    Ok(r) => r,
1411                    Err(_) => return Vec::new(),
1412                };
1413                let mut out = Vec::new();
1414                for child_row in child_rows {
1415                    if fk_matches(&child_row.values, fk, &parent_pk_map, t) {
1416                        let child_pk = encoded_pk_for(child_table, &child_row.values);
1417                        row_cache
1418                            .borrow_mut()
1419                            .insert(format!("{}:{}", child_table.name, child_pk), child_row);
1420                        out.push((child_pk, parent_pk.to_string()));
1421                    }
1422                }
1423                out
1424            };
1425        let plan = plan_delete(schema, table, &pk_str, find_children).map_err(KitError::from)?;
1426        Ok((plan, row_cache.into_inner()))
1427    }
1428
1429    fn apply_set_null(
1430        &mut self,
1431        set_null: &mongreldb_kit_core::planner::SetNullUpdate,
1432        child_row: &Row,
1433    ) -> Result<()> {
1434        let child_table = self.require_table(&set_null.table)?.clone();
1435        let mut values = child_row.values.clone();
1436        for col in &set_null.columns {
1437            let col_def = child_table
1438                .column(col)
1439                .ok_or_else(|| KitError::Integrity(format!("set-null column {col} not found")))?;
1440            if !col_def.nullable {
1441                return Err(KitError::Restrict(format!(
1442                    "set-null on non-nullable column {col}"
1443                )));
1444            }
1445            values.insert(col.clone(), Value::Null);
1446        }
1447        // Re-run validation (including checks) on the patched child row.
1448        mongreldb_kit_core::validation::validate_row(&child_table, &values)?;
1449        // Recompute unique guards for the patched row. The row itself survives a
1450        // set-null (only its FK columns change), so its primary-key guard is
1451        // re-reserved after `delete_guards_for` clears it.
1452        self.delete_guards_for(&child_table, &child_row.values)?;
1453        let cells = row_to_core_cells(&values, &child_table)?;
1454        self.core
1455            .delete(&set_null.table, RowId(child_row.row_id))
1456            .map_err(KitError::from)?;
1457        self.core
1458            .put(&set_null.table, cells)
1459            .map_err(KitError::from)?;
1460        self.reserve_unique_guards(&child_table, &values, None)?;
1461        // The row keeps its full primary key (only FK columns changed), so it is
1462        // "explicit"; this re-reserves a composite PK guard and is a no-op for a
1463        // single-column PK.
1464        self.reserve_pk_guard(&child_table, &values, true)?;
1465        self.staged.push(StagedOp::Update {
1466            table: set_null.table.clone(),
1467            old_pk: encoded_pk_for(&child_table, &child_row.values),
1468            row_id: child_row.row_id,
1469            values: values.clone(),
1470        });
1471        Ok(())
1472    }
1473
1474    // ── defaults ───────────────────────────────────────────────────────────
1475
1476    fn apply_defaults(&self, row: &mut Map<String, Value>, table: &KitTable) -> Result<()> {
1477        for col in &table.columns {
1478            if row.contains_key(&col.name) && row.get(&col.name) != Some(&Value::Null) {
1479                continue;
1480            }
1481            let Some(default) = &col.default else {
1482                continue;
1483            };
1484            let value = match default {
1485                DefaultKind::Static(v) => v.clone(),
1486                DefaultKind::Now => {
1487                    let now = iso_now();
1488                    if col.storage_type == ColumnType::Date {
1489                        Value::String(now[..10].to_string())
1490                    } else {
1491                        Value::String(now)
1492                    }
1493                }
1494                DefaultKind::Uuid => Value::String(uuid::Uuid::new_v4().to_string()),
1495                DefaultKind::Sequence(name) => {
1496                    let start = self.db.allocate_sequence(name, 1)?;
1497                    Value::Number(start.into())
1498                }
1499                DefaultKind::CustomName(name) => {
1500                    let provider = self.db.default_providers.get(name).ok_or_else(|| {
1501                        KitError::Validation(format!("custom default \"{name}\" is not registered"))
1502                    })?;
1503                    provider()
1504                }
1505            };
1506            row.insert(col.name.clone(), value);
1507        }
1508        Ok(())
1509    }
1510
1511    /// Refresh write-managed `now` columns on update.
1512    ///
1513    /// Only a `generated` column whose default is `now` (e.g. `updatedAt`) is a
1514    /// write-managed timestamp that refreshes on every update. A plain
1515    /// `default: now` column (e.g. `createdAt`) is an insert-time value and must
1516    /// NOT change on update. A column already present in the caller's patch is
1517    /// left as supplied. Mirrors the TypeScript kit's `applyUpdateDefaults`.
1518    fn apply_update_defaults(
1519        &self,
1520        merged: &mut Map<String, Value>,
1521        patch_keys: &HashSet<String>,
1522        table: &KitTable,
1523    ) {
1524        let mut now: Option<String> = None;
1525        for col in &table.columns {
1526            if patch_keys.contains(&col.name) {
1527                continue;
1528            }
1529            if col.generated && matches!(col.default, Some(DefaultKind::Now)) {
1530                let stamp = now.get_or_insert_with(iso_now);
1531                let value = if col.storage_type == ColumnType::Date {
1532                    Value::String(stamp[..10].to_string())
1533                } else {
1534                    Value::String(stamp.clone())
1535                };
1536                merged.insert(col.name.clone(), value);
1537            }
1538        }
1539    }
1540}
1541
1542// ── free helpers ───────────────────────────────────────────────────────────
1543
1544fn project_returning(row: &Row, columns: &[String]) -> Result<Value> {
1545    let mut out = Map::new();
1546    for c in columns {
1547        out.insert(c.clone(), row.values.get(c).cloned().unwrap_or(Value::Null));
1548    }
1549    Ok(Value::Object(out))
1550}
1551
1552fn pk_values_map(table: &KitTable, values: &Map<String, Value>) -> Map<String, Value> {
1553    let mut out = Map::new();
1554    for name in &table.primary_key {
1555        out.insert(
1556            name.clone(),
1557            values.get(name).cloned().unwrap_or(Value::Null),
1558        );
1559    }
1560    out
1561}
1562
1563/// Convert only the columns present in `patch` to core cells.
1564/// Missing columns are intentionally omitted so an upsert DO UPDATE patch does
1565/// not overwrite unchanged cells with NULL.
1566fn patch_to_core_cells(
1567    patch: &Map<String, Value>,
1568    table: &KitTable,
1569) -> Result<Vec<(u16, CoreValue)>> {
1570    let mut cells = Vec::new();
1571    for (name, value) in patch {
1572        let Some(col) = table.column(name) else {
1573            continue;
1574        };
1575        cells.push((col.id as u16, json_to_core(value, col.storage_type)?));
1576    }
1577    Ok(cells)
1578}
1579
1580fn select_all(table: &str, filter: Option<Expr>) -> Select {
1581    Select {
1582        table: table.to_string(),
1583        columns: vec![],
1584        filter,
1585        order_by: vec![],
1586        limit: None,
1587        offset: None,
1588    }
1589}
1590
1591fn pk_matches(values: &Map<String, Value>, pk_map: &Map<String, Value>, table: &KitTable) -> bool {
1592    for name in &table.primary_key {
1593        if values.get(name) != pk_map.get(name) {
1594            return false;
1595        }
1596    }
1597    true
1598}
1599
1600/// Whether the caller supplied every primary-key column (non-null) in `row`.
1601///
1602/// Mirrors the TypeScript kit's `pkExplicit` flag: a primary key whose columns
1603/// all came from a sequence default (i.e. were not supplied) is auto-assigned and
1604/// guaranteed unique, so it is neither checked nor guarded.
1605fn pk_is_explicit(table: &KitTable, row: &Map<String, Value>) -> bool {
1606    !table.primary_key.is_empty()
1607        && table
1608            .primary_key
1609            .iter()
1610            .all(|name| matches!(row.get(name), Some(v) if !v.is_null()))
1611}
1612
1613fn pk_guard_constraint(table: &KitTable) -> String {
1614    format!("__pk_{}", table.name)
1615}
1616
1617fn pk_guard_key(table: &KitTable, values: &Map<String, Value>) -> String {
1618    encode_unique_key(
1619        KIT_KEY_VERSION,
1620        &pk_guard_constraint(table),
1621        &pk_components(table, values),
1622    )
1623}
1624
1625/// Build the typed key component for a column value.
1626pub(crate) fn key_component(col: &Column, value: Option<&Value>) -> KeyComponent {
1627    match value {
1628        None | Some(Value::Null) => KeyComponent::Null,
1629        Some(v) => match col.storage_type {
1630            ColumnType::Int8
1631            | ColumnType::Int16
1632            | ColumnType::Int32
1633            | ColumnType::Int64
1634            | ColumnType::TimestampNanos => KeyComponent::Int(v.as_i64().unwrap_or(0)),
1635            _ => KeyComponent::Text(value_to_text(v)),
1636        },
1637    }
1638}
1639
1640fn value_to_text(value: &Value) -> String {
1641    match value {
1642        Value::String(s) => s.clone(),
1643        other => other.to_string(),
1644    }
1645}
1646
1647fn pk_components(table: &KitTable, values: &Map<String, Value>) -> Vec<KeyComponent> {
1648    table
1649        .primary_key
1650        .iter()
1651        .map(|name| {
1652            let col = table.column(name);
1653            match col {
1654                Some(c) => key_component(c, values.get(name)),
1655                None => KeyComponent::Null,
1656            }
1657        })
1658        .collect()
1659}
1660
1661pub(crate) fn encoded_pk_for(table: &KitTable, values: &Map<String, Value>) -> String {
1662    encode_pk(&pk_components(table, values))
1663}
1664
1665pub(crate) fn unique_key(
1666    table: &KitTable,
1667    uq: &UniqueConstraint,
1668    values: &Map<String, Value>,
1669) -> Option<String> {
1670    let mut components = Vec::with_capacity(uq.columns.len());
1671    for name in &uq.columns {
1672        let col = table.column(name)?;
1673        let component = key_component(col, values.get(name));
1674        if component == KeyComponent::Null {
1675            return None; // nullable-unique: nulls never collide
1676        }
1677        components.push(component);
1678    }
1679    Some(encode_unique_key(KIT_KEY_VERSION, &uq.name, &components))
1680}
1681
1682pub(crate) fn fk_values_null(fk: &ForeignKey, values: &Map<String, Value>) -> bool {
1683    fk.columns
1684        .iter()
1685        .any(|c| values.get(c).map(|v| v.is_null()).unwrap_or(true))
1686}
1687
1688pub(crate) fn parent_pk_components(
1689    values: &Map<String, Value>,
1690    fk: &ForeignKey,
1691    parent: &KitTable,
1692) -> Vec<KeyComponent> {
1693    fk.columns
1694        .iter()
1695        .zip(&parent.primary_key)
1696        .map(|(child_col, parent_col)| {
1697            let col = parent.column(parent_col);
1698            match col {
1699                Some(c) => key_component(c, values.get(child_col)),
1700                None => KeyComponent::Null,
1701            }
1702        })
1703        .collect()
1704}
1705
1706fn parent_pk_value(
1707    values: &Map<String, Value>,
1708    fk: &ForeignKey,
1709    parent: &KitTable,
1710) -> Result<Value> {
1711    if parent.primary_key.len() == 1 {
1712        let child_col = fk
1713            .columns
1714            .first()
1715            .ok_or_else(|| KitError::Integrity("fk has no columns".into()))?;
1716        Ok(values.get(child_col).cloned().unwrap_or(Value::Null))
1717    } else {
1718        let mut obj = Map::new();
1719        for (child_col, parent_col) in fk.columns.iter().zip(&parent.primary_key) {
1720            obj.insert(
1721                parent_col.clone(),
1722                values.get(child_col).cloned().unwrap_or(Value::Null),
1723            );
1724        }
1725        Ok(Value::Object(obj))
1726    }
1727}
1728
1729fn fk_matches(
1730    child_values: &Map<String, Value>,
1731    fk: &ForeignKey,
1732    parent_pk_map: &Map<String, Value>,
1733    parent_table: &KitTable,
1734) -> bool {
1735    for (child_col, parent_col) in fk.columns.iter().zip(&parent_table.primary_key) {
1736        if child_values.get(child_col) != parent_pk_map.get(parent_col) {
1737            return false;
1738        }
1739    }
1740    true
1741}
1742
1743/// Decode an encoded primary key string back into a JSON value.
1744///
1745/// Single-column keys return the scalar value; composite keys return an object
1746/// keyed by primary-key column name.
1747fn pk_string_to_value(encoded: &str, table: &KitTable) -> Result<Value> {
1748    let components = decode_pk(encoded);
1749    if components.len() != table.primary_key.len() {
1750        return Err(KitError::Validation(format!(
1751            "encoded pk \"{encoded}\" has {} components, expected {}",
1752            components.len(),
1753            table.primary_key.len()
1754        )));
1755    }
1756    if table.primary_key.len() == 1 {
1757        return Ok(component_to_value(&components[0]));
1758    }
1759    let mut obj = Map::new();
1760    for (name, component) in table.primary_key.iter().zip(&components) {
1761        obj.insert(name.clone(), component_to_value(component));
1762    }
1763    Ok(Value::Object(obj))
1764}
1765
1766fn component_to_value(component: &KeyComponent) -> Value {
1767    match component {
1768        KeyComponent::Null => Value::Null,
1769        KeyComponent::Int(i) => Value::Number((*i).into()),
1770        KeyComponent::Text(s) => Value::String(s.clone()),
1771    }
1772}
1773
1774fn row_matches_conditions(table: &KitTable, row: &Row, conditions: &[Condition]) -> bool {
1775    conditions
1776        .iter()
1777        .all(|condition| row_matches_condition(table, row, condition))
1778}
1779
1780fn row_matches_condition(table: &KitTable, row: &Row, condition: &Condition) -> bool {
1781    match condition {
1782        Condition::Pk(key) => {
1783            let Some(pk_name) = table.primary_key.first() else {
1784                return false;
1785            };
1786            let Some(col) = table.column(pk_name) else {
1787                return false;
1788            };
1789            value_index_key(col, &row.values).as_deref() == Some(key.as_slice())
1790        }
1791        Condition::BitmapEq { column_id, value } => {
1792            let Some(col) = column_by_id(table, *column_id) else {
1793                return false;
1794            };
1795            value_index_key(col, &row.values).as_deref() == Some(value.as_slice())
1796        }
1797        Condition::BitmapIn { column_id, values } => {
1798            let Some(col) = column_by_id(table, *column_id) else {
1799                return false;
1800            };
1801            let Some(key) = value_index_key(col, &row.values) else {
1802                return false;
1803            };
1804            values.iter().any(|value| value == &key)
1805        }
1806        Condition::Range { column_id, lo, hi } => {
1807            let Some(col) = column_by_id(table, *column_id) else {
1808                return false;
1809            };
1810            matches!(
1811                json_to_core(
1812                    row.values.get(&col.name).unwrap_or(&Value::Null),
1813                    col.storage_type
1814                ),
1815                Ok(CoreValue::Int64(v)) if v >= *lo && v <= *hi
1816            )
1817        }
1818        Condition::RangeF64 {
1819            column_id,
1820            lo,
1821            lo_inclusive,
1822            hi,
1823            hi_inclusive,
1824        } => {
1825            let Some(col) = column_by_id(table, *column_id) else {
1826                return false;
1827            };
1828            let Ok(CoreValue::Float64(v)) = json_to_core(
1829                row.values.get(&col.name).unwrap_or(&Value::Null),
1830                col.storage_type,
1831            ) else {
1832                return false;
1833            };
1834            let ge_lo = if *lo_inclusive { v >= *lo } else { v > *lo };
1835            let le_hi = if *hi_inclusive { v <= *hi } else { v < *hi };
1836            ge_lo && le_hi
1837        }
1838        Condition::FmContains { column_id, pattern } => {
1839            let Some(col) = column_by_id(table, *column_id) else {
1840                return false;
1841            };
1842            if pattern.is_empty() {
1843                return true;
1844            }
1845            match json_to_core(
1846                row.values.get(&col.name).unwrap_or(&Value::Null),
1847                col.storage_type,
1848            ) {
1849                Ok(CoreValue::Bytes(bytes)) => bytes
1850                    .windows(pattern.len())
1851                    .any(|window| window == pattern.as_slice()),
1852                _ => false,
1853            }
1854        }
1855        Condition::IsNull { column_id } => {
1856            let Some(col) = column_by_id(table, *column_id) else {
1857                return false;
1858            };
1859            matches!(row.values.get(&col.name), None | Some(Value::Null))
1860        }
1861        Condition::IsNotNull { column_id } => {
1862            let Some(col) = column_by_id(table, *column_id) else {
1863                return false;
1864            };
1865            !matches!(row.values.get(&col.name), None | Some(Value::Null))
1866        }
1867        // Conditions the Kit never emits (Ann, SparseMatch, FmContainsAll)
1868        // — assume the index already resolved them.
1869        _ => true,
1870    }
1871}
1872
1873fn column_by_id(table: &KitTable, column_id: u16) -> Option<&Column> {
1874    table.columns.iter().find(|col| col.id as u16 == column_id)
1875}
1876
1877fn value_index_key(col: &Column, values: &Map<String, Value>) -> Option<Vec<u8>> {
1878    let value = values.get(&col.name).unwrap_or(&Value::Null);
1879    if value.is_null() {
1880        return None;
1881    }
1882    json_to_core(value, col.storage_type)
1883        .ok()
1884        .map(|value| value.encode_key())
1885}