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    pub fn commit(self) -> Result<()> {
809        self.core.commit().map_err(KitError::from).map(|_| ())
810    }
811
812    /// Roll back the transaction.
813    pub fn rollback(self) {
814        self.core.rollback();
815    }
816
817    // ── internals ──────────────────────────────────────────────────────────
818
819    fn alloc_temp_id(&mut self) -> u64 {
820        let id = self.next_temp_id;
821        self.next_temp_id += 1;
822        id
823    }
824
825    fn require_table(&self, name: &str) -> Result<&KitTable> {
826        self.db
827            .table(name)
828            .ok_or_else(|| KitError::Integrity(format!("table {name} not found")))
829    }
830
831    /// All rows of `table` visible to this transaction (staged writes included).
832    pub fn all_rows(&self, table: &str) -> Result<Vec<Row>> {
833        self.snapshot_rows(table)
834    }
835
836    fn snapshot_rows(&self, table: &str) -> Result<Vec<Row>> {
837        let t = self.require_table(table)?;
838        let core_rows = self
839            .db
840            .visible_core_rows_at(table, self.core.read_snapshot())?;
841        let mut rows = core_rows
842            .into_iter()
843            .map(|r| core_row_to_json(&r, t))
844            .collect::<Result<Vec<_>>>()?;
845        self.replay_staged_rows(t, &mut rows);
846        Ok(rows)
847    }
848
849    /// Fetch rows for `table` with native `conditions` resolved by the engine
850    /// (Kit Priority 1 pushdown). Avoids the full scan that `snapshot_rows`
851    /// does — the engine resolves conditions via HOT/bitmap/range indexes.
852    fn snapshot_rows_pushed(&self, table: &str, conditions: &[Condition]) -> Result<Vec<Row>> {
853        let t = self.require_table(table)?;
854        if self.has_staged_for(table) {
855            return Ok(self
856                .snapshot_rows(table)?
857                .into_iter()
858                .filter(|r| row_matches_conditions(t, r, conditions))
859                .collect());
860        }
861        let core_rows = self
862            .db
863            .query_core_rows_at(table, conditions, self.core.read_snapshot())?;
864        core_rows
865            .into_iter()
866            .map(|r| core_row_to_json(&r, t))
867            .collect()
868    }
869
870    fn get_by_pk_internal(&self, table: &str, pk_map: &Map<String, Value>) -> Result<Option<Row>> {
871        let t = self.require_table(table)?;
872        if self.has_staged_for(table) {
873            let rows = self.snapshot_rows(table)?;
874            return Ok(rows.into_iter().find(|r| pk_matches(&r.values, pk_map, t)));
875        }
876        // Kit Priority 1 pushdown: use native PK index (O(1) HOT probe) instead
877        // of O(N) full scan. Falls back to scan if conditions can't be built.
878        if let Some(conditions) = crate::pushdown::pk_conditions(t, pk_map) {
879            let core_rows =
880                self.db
881                    .query_core_rows_at(table, &conditions, self.core.read_snapshot())?;
882            return Ok(core_rows
883                .into_iter()
884                .filter_map(|r| core_row_to_json(&r, t).ok())
885                .find(|r| pk_matches(&r.values, pk_map, t)));
886        }
887        let rows = self.snapshot_rows(table)?;
888        Ok(rows.into_iter().find(|r| pk_matches(&r.values, pk_map, t)))
889    }
890
891    fn internal_rows(&self, table: &str) -> Result<Vec<CoreRow>> {
892        self.db
893            .visible_core_rows_at(table, self.core.read_snapshot())
894    }
895
896    // ── unique constraints ─────────────────────────────────────────────────
897
898    /// Reject the row if any of its unique keys is already owned by a different
899    /// row, either in committed state or in this transaction's staging.
900    fn check_unique_constraints(
901        &self,
902        table: &KitTable,
903        values: &Map<String, Value>,
904        old_values: Option<&Map<String, Value>>,
905    ) -> Result<()> {
906        let owner_pk = encoded_pk_for(table, values);
907        let committed = self.internal_rows(UNIQUE_KEYS)?;
908        for uq in &table.unique_constraints {
909            let Some(key) = unique_key(table, uq, values) else {
910                continue;
911            };
912            // Unchanged keys (update where the value did not move) are fine.
913            if let Some(old) = old_values {
914                if unique_key(table, uq, old).as_deref() == Some(key.as_str()) {
915                    continue;
916                }
917            }
918            self.assert_key_free(&committed, table, uq, &key, &owner_pk)?;
919        }
920        Ok(())
921    }
922
923    fn assert_key_free(
924        &self,
925        committed: &[CoreRow],
926        table: &KitTable,
927        uq: &UniqueConstraint,
928        key: &str,
929        owner_pk: &str,
930    ) -> Result<()> {
931        for guard in committed {
932            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(key) {
933                let g_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
934                let g_pk = internal_bytes(guard, cols::UQ_OWNER_PK).unwrap_or_default();
935                if g_table != table.name || g_pk != owner_pk {
936                    return Err(KitError::Duplicate(format!(
937                        "unique constraint {} on {}",
938                        uq.name, table.name
939                    )));
940                }
941            }
942        }
943        for staged in &self.staged_unique {
944            if staged.encoded_key == key
945                && (staged.owner_table != table.name || staged.owner_pk != owner_pk)
946            {
947                return Err(KitError::Duplicate(format!(
948                    "unique constraint {} on {}",
949                    uq.name, table.name
950                )));
951            }
952        }
953        Ok(())
954    }
955
956    /// Reserve new unique guard rows for `values`, deleting any stale guards that
957    /// belonged to the previous version of the row (on update).
958    fn reserve_unique_guards(
959        &mut self,
960        table: &KitTable,
961        values: &Map<String, Value>,
962        old_values: Option<&Map<String, Value>>,
963    ) -> Result<()> {
964        let owner_pk = encoded_pk_for(table, values);
965        for uq in &table.unique_constraints {
966            let new_key = unique_key(table, uq, values);
967            let old_key = old_values.and_then(|old| unique_key(table, uq, old));
968            if new_key == old_key {
969                continue;
970            }
971            if let Some(key) = &new_key {
972                self.put_unique_guard(&uq.name, key, &table.name, &owner_pk)?;
973            }
974            if let Some(key) = &old_key {
975                self.delete_unique_guard(key)?;
976            }
977        }
978        Ok(())
979    }
980
981    fn put_unique_guard(
982        &mut self,
983        constraint: &str,
984        encoded_key: &str,
985        owner_table: &str,
986        owner_pk: &str,
987    ) -> Result<()> {
988        let now = iso_now();
989        self.core
990            .put(
991                UNIQUE_KEYS,
992                vec![
993                    (cols::UQ_ENCODED, CoreValue::Bytes(encoded_key.into())),
994                    (cols::UQ_CONSTRAINT, CoreValue::Bytes(constraint.into())),
995                    (cols::UQ_OWNER_TABLE, CoreValue::Bytes(owner_table.into())),
996                    (cols::UQ_OWNER_PK, CoreValue::Bytes(owner_pk.into())),
997                    (cols::UQ_CREATED, CoreValue::Bytes(now.into_bytes())),
998                ],
999            )
1000            .map_err(KitError::from)?;
1001        self.staged_unique.push(StagedUnique {
1002            encoded_key: encoded_key.to_string(),
1003            owner_table: owner_table.to_string(),
1004            owner_pk: owner_pk.to_string(),
1005        });
1006        Ok(())
1007    }
1008
1009    fn delete_unique_guard(&mut self, encoded_key: &str) -> Result<()> {
1010        let committed = self.internal_rows(UNIQUE_KEYS)?;
1011        for guard in &committed {
1012            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(encoded_key) {
1013                self.core
1014                    .delete(UNIQUE_KEYS, guard.row_id)
1015                    .map_err(KitError::from)?;
1016            }
1017        }
1018        self.staged_unique.retain(|s| s.encoded_key != encoded_key);
1019        Ok(())
1020    }
1021
1022    /// Delete every unique guard owned by the given row (used on row delete).
1023    fn delete_unique_guards_for_owner(&mut self, table: &KitTable, owner_pk: &str) -> Result<()> {
1024        let constraint_names: HashSet<&str> = table
1025            .unique_constraints
1026            .iter()
1027            .map(|u| u.name.as_str())
1028            .collect();
1029        let committed = self.internal_rows(UNIQUE_KEYS)?;
1030        for guard in &committed {
1031            let g_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
1032            let g_pk = internal_bytes(guard, cols::UQ_OWNER_PK).unwrap_or_default();
1033            let g_constraint = internal_bytes(guard, cols::UQ_CONSTRAINT).unwrap_or_default();
1034            if g_table == table.name
1035                && g_pk == owner_pk
1036                && (constraint_names.contains(g_constraint.as_str())
1037                    || g_constraint == pk_guard_constraint(table))
1038            {
1039                self.core
1040                    .delete(UNIQUE_KEYS, guard.row_id)
1041                    .map_err(KitError::from)?;
1042            }
1043        }
1044        let owner_pk = owner_pk.to_string();
1045        let name = table.name.clone();
1046        self.staged_unique
1047            .retain(|s| !(s.owner_table == name && s.owner_pk == owner_pk));
1048        Ok(())
1049    }
1050
1051    // ── primary-key handling ───────────────────────────────────────────────
1052    //
1053    // Matches the TypeScript kit: an auto-assigned (sequence) primary key is
1054    // guaranteed unique and skipped; an explicit single-column primary key is
1055    // checked directly against the visible rows (no guard row); only an explicit
1056    // composite primary key reserves a `__pk_<table>` guard in `__kit_unique_keys`
1057    // (it has no single native key to probe), making the duplicate insert throw
1058    // and stay conflict-safe.
1059
1060    fn check_pk(
1061        &self,
1062        table: &KitTable,
1063        values: &Map<String, Value>,
1064        pk_explicit: bool,
1065        pk_seen: Option<&mut HashSet<String>>,
1066    ) -> Result<()> {
1067        // An auto-assigned primary key is unique by construction; nothing to do.
1068        if table.primary_key.is_empty() || !pk_explicit {
1069            return Ok(());
1070        }
1071
1072        if table.primary_key.len() == 1 {
1073            // A single-column explicit PK has a native key, so check whether a row
1074            // with that PK already exists. A batch passes a pre-loaded set so the
1075            // check stays O(1) per row; a single insert checks the visible rows
1076            // (committed plus this transaction's in-flight staging) directly.
1077            let duplicate = match pk_seen {
1078                Some(seen) => {
1079                    let key = encoded_pk_for(table, values);
1080                    if seen.contains(&key) {
1081                        true
1082                    } else {
1083                        seen.insert(key);
1084                        false
1085                    }
1086                }
1087                None => {
1088                    self.parent_exists(&table.name, values)?
1089                        || self.staged_row_exists(table, values)
1090                }
1091            };
1092            if duplicate {
1093                return Err(KitError::Duplicate(format!(
1094                    "primary key {} on {}",
1095                    encoded_pk_for(table, values),
1096                    table.name
1097                )));
1098            }
1099            return Ok(());
1100        }
1101
1102        // A composite explicit PK uses a guard row (conflict-safe), like the
1103        // unique-constraint machinery.
1104        let key = pk_guard_key(table, values);
1105        let owner_pk = encoded_pk_for(table, values);
1106        let committed = self.internal_rows(UNIQUE_KEYS)?;
1107        for guard in &committed {
1108            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(key.as_str()) {
1109                return Err(KitError::Duplicate(format!(
1110                    "primary key {} on {}",
1111                    owner_pk, table.name
1112                )));
1113            }
1114        }
1115        for staged in &self.staged_unique {
1116            if staged.encoded_key == key {
1117                return Err(KitError::Duplicate(format!(
1118                    "primary key {} on {}",
1119                    owner_pk, table.name
1120                )));
1121            }
1122        }
1123        Ok(())
1124    }
1125
1126    fn reserve_pk_guard(
1127        &mut self,
1128        table: &KitTable,
1129        values: &Map<String, Value>,
1130        pk_explicit: bool,
1131    ) -> Result<()> {
1132        // Only an explicit composite primary key needs a guard row. A single-column
1133        // PK is checked directly, and an auto-assigned PK is guaranteed unique.
1134        if table.primary_key.len() < 2 || !pk_explicit {
1135            return Ok(());
1136        }
1137        let key = pk_guard_key(table, values);
1138        let owner_pk = encoded_pk_for(table, values);
1139        let constraint = pk_guard_constraint(table);
1140        self.put_unique_guard(&constraint, &key, &table.name, &owner_pk)
1141    }
1142
1143    // ── foreign keys ───────────────────────────────────────────────────────
1144
1145    fn check_foreign_keys(&self, table: &KitTable, values: &Map<String, Value>) -> Result<()> {
1146        for fk in &table.foreign_keys {
1147            if fk_values_null(fk, values) {
1148                continue;
1149            }
1150            let parent = self.require_table(&fk.references_table)?;
1151            let parent_pk = parent_pk_value(values, fk, parent)?;
1152            let parent_pk_map = pk_to_map(&parent_pk, parent)?;
1153            if self.parent_exists(&fk.references_table, &parent_pk_map)?
1154                || self.staged_row_exists(parent, &parent_pk_map)
1155            {
1156                continue;
1157            }
1158            return Err(KitError::ForeignKey(format!(
1159                "{} references {}({})",
1160                fk.name,
1161                fk.references_table,
1162                fk.references_columns.join(",")
1163            )));
1164        }
1165        Ok(())
1166    }
1167
1168    fn touch_foreign_key_guards(
1169        &mut self,
1170        table: &KitTable,
1171        values: &Map<String, Value>,
1172    ) -> Result<()> {
1173        for fk in table.foreign_keys.clone() {
1174            if fk_values_null(&fk, values) {
1175                continue;
1176            }
1177            let parent = self.require_table(&fk.references_table)?.clone();
1178            let components = parent_pk_components(values, &fk, &parent);
1179            self.touch_row_guard(&parent.name, &components)?;
1180        }
1181        Ok(())
1182    }
1183
1184    fn parent_exists(&self, table: &str, pk_map: &Map<String, Value>) -> Result<bool> {
1185        let t = self.require_table(table)?;
1186        if self.has_staged_for(table) {
1187            let rows = self.snapshot_rows(table)?;
1188            return Ok(rows.iter().any(|r| pk_matches(&r.values, pk_map, t)));
1189        }
1190        // Kit Priority 1 pushdown: O(1) HOT probe instead of O(N) full scan for
1191        // FK parent existence checks.
1192        if let Some(conditions) = crate::pushdown::pk_conditions(t, pk_map) {
1193            let core_rows =
1194                self.db
1195                    .query_core_rows_at(table, &conditions, self.core.read_snapshot())?;
1196            return Ok(!core_rows.is_empty());
1197        }
1198        let rows = self.snapshot_rows(table)?;
1199        Ok(rows.iter().any(|r| pk_matches(&r.values, pk_map, t)))
1200    }
1201
1202    /// Whether a row identified by `pk_map` exists in `table`'s in-flight staging
1203    /// for this transaction. Replays the staging in order (table-scoped) so a row
1204    /// inserted and then deleted within the same transaction is not treated as
1205    /// present. Used both for foreign-key parent checks and for the single-column
1206    /// primary-key duplicate check.
1207    fn staged_row_exists(&self, table: &KitTable, pk_map: &Map<String, Value>) -> bool {
1208        let target = encode_pk(&pk_components(table, pk_map));
1209        let mut exists = false;
1210        for staged in &self.staged {
1211            match staged {
1212                StagedOp::Insert { table: t, values }
1213                | StagedOp::Update {
1214                    table: t, values, ..
1215                } => {
1216                    if t == &table.name && pk_matches(values, pk_map, table) {
1217                        exists = true;
1218                    }
1219                }
1220                StagedOp::Delete { table: t, pk } => {
1221                    if t == &table.name && *pk == target {
1222                        exists = false;
1223                    }
1224                }
1225                StagedOp::Truncate { table: t } => {
1226                    if t == &table.name {
1227                        exists = false;
1228                    }
1229                }
1230            }
1231        }
1232        exists
1233    }
1234
1235    // ── row guards ─────────────────────────────────────────────────────────
1236
1237    fn touch_row_guard(&mut self, table: &str, pk_components: &[KeyComponent]) -> Result<()> {
1238        let encoded_pk = encode_pk(pk_components);
1239        let guard_key = encode_row_guard_key(table, &encoded_pk);
1240        if !self.touched_guards.insert(guard_key.clone()) {
1241            return Ok(());
1242        }
1243        // Replace any existing committed guard, bumping the version.
1244        let mut version = 1i64;
1245        let committed = self.internal_rows(ROW_GUARDS)?;
1246        for guard in &committed {
1247            if internal_bytes(guard, cols::RG_ENCODED).as_deref() == Some(guard_key.as_str()) {
1248                if let Some(CoreValue::Int64(v)) = guard.columns.get(&cols::RG_VERSION) {
1249                    version = v + 1;
1250                }
1251                self.core
1252                    .delete(ROW_GUARDS, guard.row_id)
1253                    .map_err(KitError::from)?;
1254            }
1255        }
1256        let now = iso_now();
1257        self.core
1258            .put(
1259                ROW_GUARDS,
1260                vec![
1261                    (cols::RG_ENCODED, CoreValue::Bytes(guard_key.into_bytes())),
1262                    (cols::RG_TABLE, CoreValue::Bytes(table.into())),
1263                    (cols::RG_PK, CoreValue::Bytes(encoded_pk.into_bytes())),
1264                    (cols::RG_VERSION, CoreValue::Int64(version)),
1265                    (cols::RG_UPDATED, CoreValue::Bytes(now.into_bytes())),
1266                ],
1267            )
1268            .map_err(KitError::from)?;
1269        Ok(())
1270    }
1271
1272    fn has_staged_for(&self, table: &str) -> bool {
1273        self.staged.iter().any(|op| match op {
1274            StagedOp::Insert { table: t, .. }
1275            | StagedOp::Update { table: t, .. }
1276            | StagedOp::Delete { table: t, .. }
1277            | StagedOp::Truncate { table: t } => t == table,
1278        })
1279    }
1280
1281    fn replay_staged_rows(&self, table: &KitTable, rows: &mut Vec<Row>) {
1282        for staged in &self.staged {
1283            match staged {
1284                StagedOp::Insert { table: t, values } if t == &table.name => {
1285                    let pk = encoded_pk_for(table, values);
1286                    rows.retain(|r| encoded_pk_for(table, &r.values) != pk);
1287                    rows.push(Row {
1288                        row_id: 0,
1289                        values: values.clone(),
1290                    });
1291                }
1292                StagedOp::Update {
1293                    table: t,
1294                    old_pk,
1295                    row_id,
1296                    values,
1297                } if t == &table.name => {
1298                    let new_pk = encoded_pk_for(table, values);
1299                    rows.retain(|r| {
1300                        let pk = encoded_pk_for(table, &r.values);
1301                        pk != *old_pk && pk != new_pk
1302                    });
1303                    rows.push(Row {
1304                        row_id: *row_id,
1305                        values: values.clone(),
1306                    });
1307                }
1308                StagedOp::Delete { table: t, pk } if t == &table.name => {
1309                    rows.retain(|r| encoded_pk_for(table, &r.values) != *pk);
1310                }
1311                StagedOp::Truncate { table: t } if t == &table.name => {
1312                    rows.clear();
1313                }
1314                _ => {}
1315            }
1316        }
1317    }
1318
1319    /// Remove unique + pk guards for a row that is being deleted.
1320    fn delete_guards_for(&mut self, table: &KitTable, values: &Map<String, Value>) -> Result<()> {
1321        let owner_pk = encoded_pk_for(table, values);
1322        self.delete_unique_guards_for_owner(table, &owner_pk)
1323    }
1324
1325    fn delete_all_guards_for_table(&mut self, table: &KitTable) -> Result<()> {
1326        for guard in self.internal_rows(UNIQUE_KEYS)? {
1327            let owner_table = internal_bytes(&guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
1328            if owner_table == table.name {
1329                self.core
1330                    .delete(UNIQUE_KEYS, guard.row_id)
1331                    .map_err(KitError::from)?;
1332            }
1333        }
1334        for guard in self.internal_rows(ROW_GUARDS)? {
1335            let owner_table = internal_bytes(&guard, cols::RG_TABLE).unwrap_or_default();
1336            if owner_table == table.name {
1337                self.core
1338                    .delete(ROW_GUARDS, guard.row_id)
1339                    .map_err(KitError::from)?;
1340            }
1341        }
1342        self.staged_unique.retain(|s| s.owner_table != table.name);
1343        self.touched_guards
1344            .retain(|key| !key.starts_with(&format!("{}:", table.name)));
1345        Ok(())
1346    }
1347
1348    // ── delete planning ────────────────────────────────────────────────────
1349
1350    fn plan_delete(&self, table: &str, row: &Row) -> Result<(DeletePlan, HashMap<String, Row>)> {
1351        let schema = &self.db.schema;
1352        let t = self.require_table(table)?;
1353        let pk_str = encoded_pk_for(t, &row.values);
1354        // Cache every child row discovered while planning, keyed by
1355        // "<table>:<encoded_pk>", so the apply phase can reuse it instead of
1356        // re-reading each row by PK — which would make a bulk cascade / set-null
1357        // delete O(n^2) (one full table scan per affected child row).
1358        let row_cache: RefCell<HashMap<String, Row>> = RefCell::new(HashMap::new());
1359        let find_children =
1360            |child_table: &KitTable, fk: &ForeignKey, parent_pk: &str| -> Vec<(String, String)> {
1361                let parent_pk_value = match pk_string_to_value(parent_pk, t) {
1362                    Ok(v) => v,
1363                    Err(_) => return Vec::new(),
1364                };
1365                let parent_pk_map = match pk_to_map(&parent_pk_value, t) {
1366                    Ok(m) => m,
1367                    Err(_) => return Vec::new(),
1368                };
1369                let child_rows = match self.snapshot_rows(&child_table.name) {
1370                    Ok(r) => r,
1371                    Err(_) => return Vec::new(),
1372                };
1373                let mut out = Vec::new();
1374                for child_row in child_rows {
1375                    if fk_matches(&child_row.values, fk, &parent_pk_map, t) {
1376                        let child_pk = encoded_pk_for(child_table, &child_row.values);
1377                        row_cache
1378                            .borrow_mut()
1379                            .insert(format!("{}:{}", child_table.name, child_pk), child_row);
1380                        out.push((child_pk, parent_pk.to_string()));
1381                    }
1382                }
1383                out
1384            };
1385        let plan = plan_delete(schema, table, &pk_str, find_children).map_err(KitError::from)?;
1386        Ok((plan, row_cache.into_inner()))
1387    }
1388
1389    fn apply_set_null(
1390        &mut self,
1391        set_null: &mongreldb_kit_core::planner::SetNullUpdate,
1392        child_row: &Row,
1393    ) -> Result<()> {
1394        let child_table = self.require_table(&set_null.table)?.clone();
1395        let mut values = child_row.values.clone();
1396        for col in &set_null.columns {
1397            let col_def = child_table
1398                .column(col)
1399                .ok_or_else(|| KitError::Integrity(format!("set-null column {col} not found")))?;
1400            if !col_def.nullable {
1401                return Err(KitError::Restrict(format!(
1402                    "set-null on non-nullable column {col}"
1403                )));
1404            }
1405            values.insert(col.clone(), Value::Null);
1406        }
1407        // Re-run validation (including checks) on the patched child row.
1408        mongreldb_kit_core::validation::validate_row(&child_table, &values)?;
1409        // Recompute unique guards for the patched row. The row itself survives a
1410        // set-null (only its FK columns change), so its primary-key guard is
1411        // re-reserved after `delete_guards_for` clears it.
1412        self.delete_guards_for(&child_table, &child_row.values)?;
1413        let cells = row_to_core_cells(&values, &child_table)?;
1414        self.core
1415            .delete(&set_null.table, RowId(child_row.row_id))
1416            .map_err(KitError::from)?;
1417        self.core
1418            .put(&set_null.table, cells)
1419            .map_err(KitError::from)?;
1420        self.reserve_unique_guards(&child_table, &values, None)?;
1421        // The row keeps its full primary key (only FK columns changed), so it is
1422        // "explicit"; this re-reserves a composite PK guard and is a no-op for a
1423        // single-column PK.
1424        self.reserve_pk_guard(&child_table, &values, true)?;
1425        self.staged.push(StagedOp::Update {
1426            table: set_null.table.clone(),
1427            old_pk: encoded_pk_for(&child_table, &child_row.values),
1428            row_id: child_row.row_id,
1429            values: values.clone(),
1430        });
1431        Ok(())
1432    }
1433
1434    // ── defaults ───────────────────────────────────────────────────────────
1435
1436    fn apply_defaults(&self, row: &mut Map<String, Value>, table: &KitTable) -> Result<()> {
1437        for col in &table.columns {
1438            if row.contains_key(&col.name) && row.get(&col.name) != Some(&Value::Null) {
1439                continue;
1440            }
1441            let Some(default) = &col.default else {
1442                continue;
1443            };
1444            let value = match default {
1445                DefaultKind::Static(v) => v.clone(),
1446                DefaultKind::Now => {
1447                    let now = iso_now();
1448                    if col.storage_type == ColumnType::Date {
1449                        Value::String(now[..10].to_string())
1450                    } else {
1451                        Value::String(now)
1452                    }
1453                }
1454                DefaultKind::Uuid => Value::String(uuid::Uuid::new_v4().to_string()),
1455                DefaultKind::Sequence(name) => {
1456                    let start = self.db.allocate_sequence(name, 1)?;
1457                    Value::Number(start.into())
1458                }
1459                DefaultKind::CustomName(name) => {
1460                    let provider = self.db.default_providers.get(name).ok_or_else(|| {
1461                        KitError::Validation(format!("custom default \"{name}\" is not registered"))
1462                    })?;
1463                    provider()
1464                }
1465            };
1466            row.insert(col.name.clone(), value);
1467        }
1468        Ok(())
1469    }
1470
1471    /// Refresh write-managed `now` columns on update.
1472    ///
1473    /// Only a `generated` column whose default is `now` (e.g. `updatedAt`) is a
1474    /// write-managed timestamp that refreshes on every update. A plain
1475    /// `default: now` column (e.g. `createdAt`) is an insert-time value and must
1476    /// NOT change on update. A column already present in the caller's patch is
1477    /// left as supplied. Mirrors the TypeScript kit's `applyUpdateDefaults`.
1478    fn apply_update_defaults(
1479        &self,
1480        merged: &mut Map<String, Value>,
1481        patch_keys: &HashSet<String>,
1482        table: &KitTable,
1483    ) {
1484        let mut now: Option<String> = None;
1485        for col in &table.columns {
1486            if patch_keys.contains(&col.name) {
1487                continue;
1488            }
1489            if col.generated && matches!(col.default, Some(DefaultKind::Now)) {
1490                let stamp = now.get_or_insert_with(iso_now);
1491                let value = if col.storage_type == ColumnType::Date {
1492                    Value::String(stamp[..10].to_string())
1493                } else {
1494                    Value::String(stamp.clone())
1495                };
1496                merged.insert(col.name.clone(), value);
1497            }
1498        }
1499    }
1500}
1501
1502// ── free helpers ───────────────────────────────────────────────────────────
1503
1504fn project_returning(row: &Row, columns: &[String]) -> Result<Value> {
1505    let mut out = Map::new();
1506    for c in columns {
1507        out.insert(c.clone(), row.values.get(c).cloned().unwrap_or(Value::Null));
1508    }
1509    Ok(Value::Object(out))
1510}
1511
1512fn pk_values_map(table: &KitTable, values: &Map<String, Value>) -> Map<String, Value> {
1513    let mut out = Map::new();
1514    for name in &table.primary_key {
1515        out.insert(
1516            name.clone(),
1517            values.get(name).cloned().unwrap_or(Value::Null),
1518        );
1519    }
1520    out
1521}
1522
1523/// Convert only the columns present in `patch` to core cells.
1524/// Missing columns are intentionally omitted so an upsert DO UPDATE patch does
1525/// not overwrite unchanged cells with NULL.
1526fn patch_to_core_cells(
1527    patch: &Map<String, Value>,
1528    table: &KitTable,
1529) -> Result<Vec<(u16, CoreValue)>> {
1530    let mut cells = Vec::new();
1531    for (name, value) in patch {
1532        let Some(col) = table.column(name) else {
1533            continue;
1534        };
1535        cells.push((col.id as u16, json_to_core(value, col.storage_type)?));
1536    }
1537    Ok(cells)
1538}
1539
1540fn select_all(table: &str, filter: Option<Expr>) -> Select {
1541    Select {
1542        table: table.to_string(),
1543        columns: vec![],
1544        filter,
1545        order_by: vec![],
1546        limit: None,
1547        offset: None,
1548    }
1549}
1550
1551fn pk_matches(values: &Map<String, Value>, pk_map: &Map<String, Value>, table: &KitTable) -> bool {
1552    for name in &table.primary_key {
1553        if values.get(name) != pk_map.get(name) {
1554            return false;
1555        }
1556    }
1557    true
1558}
1559
1560/// Whether the caller supplied every primary-key column (non-null) in `row`.
1561///
1562/// Mirrors the TypeScript kit's `pkExplicit` flag: a primary key whose columns
1563/// all came from a sequence default (i.e. were not supplied) is auto-assigned and
1564/// guaranteed unique, so it is neither checked nor guarded.
1565fn pk_is_explicit(table: &KitTable, row: &Map<String, Value>) -> bool {
1566    !table.primary_key.is_empty()
1567        && table
1568            .primary_key
1569            .iter()
1570            .all(|name| matches!(row.get(name), Some(v) if !v.is_null()))
1571}
1572
1573fn pk_guard_constraint(table: &KitTable) -> String {
1574    format!("__pk_{}", table.name)
1575}
1576
1577fn pk_guard_key(table: &KitTable, values: &Map<String, Value>) -> String {
1578    encode_unique_key(
1579        KIT_KEY_VERSION,
1580        &pk_guard_constraint(table),
1581        &pk_components(table, values),
1582    )
1583}
1584
1585/// Build the typed key component for a column value.
1586pub(crate) fn key_component(col: &Column, value: Option<&Value>) -> KeyComponent {
1587    match value {
1588        None | Some(Value::Null) => KeyComponent::Null,
1589        Some(v) => match col.storage_type {
1590            ColumnType::Int8
1591            | ColumnType::Int16
1592            | ColumnType::Int32
1593            | ColumnType::Int64
1594            | ColumnType::TimestampNanos => KeyComponent::Int(v.as_i64().unwrap_or(0)),
1595            _ => KeyComponent::Text(value_to_text(v)),
1596        },
1597    }
1598}
1599
1600fn value_to_text(value: &Value) -> String {
1601    match value {
1602        Value::String(s) => s.clone(),
1603        other => other.to_string(),
1604    }
1605}
1606
1607fn pk_components(table: &KitTable, values: &Map<String, Value>) -> Vec<KeyComponent> {
1608    table
1609        .primary_key
1610        .iter()
1611        .map(|name| {
1612            let col = table.column(name);
1613            match col {
1614                Some(c) => key_component(c, values.get(name)),
1615                None => KeyComponent::Null,
1616            }
1617        })
1618        .collect()
1619}
1620
1621pub(crate) fn encoded_pk_for(table: &KitTable, values: &Map<String, Value>) -> String {
1622    encode_pk(&pk_components(table, values))
1623}
1624
1625pub(crate) fn unique_key(
1626    table: &KitTable,
1627    uq: &UniqueConstraint,
1628    values: &Map<String, Value>,
1629) -> Option<String> {
1630    let mut components = Vec::with_capacity(uq.columns.len());
1631    for name in &uq.columns {
1632        let col = table.column(name)?;
1633        let component = key_component(col, values.get(name));
1634        if component == KeyComponent::Null {
1635            return None; // nullable-unique: nulls never collide
1636        }
1637        components.push(component);
1638    }
1639    Some(encode_unique_key(KIT_KEY_VERSION, &uq.name, &components))
1640}
1641
1642pub(crate) fn fk_values_null(fk: &ForeignKey, values: &Map<String, Value>) -> bool {
1643    fk.columns
1644        .iter()
1645        .any(|c| values.get(c).map(|v| v.is_null()).unwrap_or(true))
1646}
1647
1648pub(crate) fn parent_pk_components(
1649    values: &Map<String, Value>,
1650    fk: &ForeignKey,
1651    parent: &KitTable,
1652) -> Vec<KeyComponent> {
1653    fk.columns
1654        .iter()
1655        .zip(&parent.primary_key)
1656        .map(|(child_col, parent_col)| {
1657            let col = parent.column(parent_col);
1658            match col {
1659                Some(c) => key_component(c, values.get(child_col)),
1660                None => KeyComponent::Null,
1661            }
1662        })
1663        .collect()
1664}
1665
1666fn parent_pk_value(
1667    values: &Map<String, Value>,
1668    fk: &ForeignKey,
1669    parent: &KitTable,
1670) -> Result<Value> {
1671    if parent.primary_key.len() == 1 {
1672        let child_col = fk
1673            .columns
1674            .first()
1675            .ok_or_else(|| KitError::Integrity("fk has no columns".into()))?;
1676        Ok(values.get(child_col).cloned().unwrap_or(Value::Null))
1677    } else {
1678        let mut obj = Map::new();
1679        for (child_col, parent_col) in fk.columns.iter().zip(&parent.primary_key) {
1680            obj.insert(
1681                parent_col.clone(),
1682                values.get(child_col).cloned().unwrap_or(Value::Null),
1683            );
1684        }
1685        Ok(Value::Object(obj))
1686    }
1687}
1688
1689fn fk_matches(
1690    child_values: &Map<String, Value>,
1691    fk: &ForeignKey,
1692    parent_pk_map: &Map<String, Value>,
1693    parent_table: &KitTable,
1694) -> bool {
1695    for (child_col, parent_col) in fk.columns.iter().zip(&parent_table.primary_key) {
1696        if child_values.get(child_col) != parent_pk_map.get(parent_col) {
1697            return false;
1698        }
1699    }
1700    true
1701}
1702
1703/// Decode an encoded primary key string back into a JSON value.
1704///
1705/// Single-column keys return the scalar value; composite keys return an object
1706/// keyed by primary-key column name.
1707fn pk_string_to_value(encoded: &str, table: &KitTable) -> Result<Value> {
1708    let components = decode_pk(encoded);
1709    if components.len() != table.primary_key.len() {
1710        return Err(KitError::Validation(format!(
1711            "encoded pk \"{encoded}\" has {} components, expected {}",
1712            components.len(),
1713            table.primary_key.len()
1714        )));
1715    }
1716    if table.primary_key.len() == 1 {
1717        return Ok(component_to_value(&components[0]));
1718    }
1719    let mut obj = Map::new();
1720    for (name, component) in table.primary_key.iter().zip(&components) {
1721        obj.insert(name.clone(), component_to_value(component));
1722    }
1723    Ok(Value::Object(obj))
1724}
1725
1726fn component_to_value(component: &KeyComponent) -> Value {
1727    match component {
1728        KeyComponent::Null => Value::Null,
1729        KeyComponent::Int(i) => Value::Number((*i).into()),
1730        KeyComponent::Text(s) => Value::String(s.clone()),
1731    }
1732}
1733
1734fn row_matches_conditions(table: &KitTable, row: &Row, conditions: &[Condition]) -> bool {
1735    conditions
1736        .iter()
1737        .all(|condition| row_matches_condition(table, row, condition))
1738}
1739
1740fn row_matches_condition(table: &KitTable, row: &Row, condition: &Condition) -> bool {
1741    match condition {
1742        Condition::Pk(key) => {
1743            let Some(pk_name) = table.primary_key.first() else {
1744                return false;
1745            };
1746            let Some(col) = table.column(pk_name) else {
1747                return false;
1748            };
1749            value_index_key(col, &row.values).as_deref() == Some(key.as_slice())
1750        }
1751        Condition::BitmapEq { column_id, value } => {
1752            let Some(col) = column_by_id(table, *column_id) else {
1753                return false;
1754            };
1755            value_index_key(col, &row.values).as_deref() == Some(value.as_slice())
1756        }
1757        Condition::BitmapIn { column_id, values } => {
1758            let Some(col) = column_by_id(table, *column_id) else {
1759                return false;
1760            };
1761            let Some(key) = value_index_key(col, &row.values) else {
1762                return false;
1763            };
1764            values.iter().any(|value| value == &key)
1765        }
1766        Condition::Range { column_id, lo, hi } => {
1767            let Some(col) = column_by_id(table, *column_id) else {
1768                return false;
1769            };
1770            matches!(
1771                json_to_core(
1772                    row.values.get(&col.name).unwrap_or(&Value::Null),
1773                    col.storage_type
1774                ),
1775                Ok(CoreValue::Int64(v)) if v >= *lo && v <= *hi
1776            )
1777        }
1778        Condition::RangeF64 {
1779            column_id,
1780            lo,
1781            lo_inclusive,
1782            hi,
1783            hi_inclusive,
1784        } => {
1785            let Some(col) = column_by_id(table, *column_id) else {
1786                return false;
1787            };
1788            let Ok(CoreValue::Float64(v)) = json_to_core(
1789                row.values.get(&col.name).unwrap_or(&Value::Null),
1790                col.storage_type,
1791            ) else {
1792                return false;
1793            };
1794            let ge_lo = if *lo_inclusive { v >= *lo } else { v > *lo };
1795            let le_hi = if *hi_inclusive { v <= *hi } else { v < *hi };
1796            ge_lo && le_hi
1797        }
1798        Condition::FmContains { column_id, pattern } => {
1799            let Some(col) = column_by_id(table, *column_id) else {
1800                return false;
1801            };
1802            if pattern.is_empty() {
1803                return true;
1804            }
1805            match json_to_core(
1806                row.values.get(&col.name).unwrap_or(&Value::Null),
1807                col.storage_type,
1808            ) {
1809                Ok(CoreValue::Bytes(bytes)) => bytes
1810                    .windows(pattern.len())
1811                    .any(|window| window == pattern.as_slice()),
1812                _ => false,
1813            }
1814        }
1815        Condition::IsNull { column_id } => {
1816            let Some(col) = column_by_id(table, *column_id) else {
1817                return false;
1818            };
1819            matches!(row.values.get(&col.name), None | Some(Value::Null))
1820        }
1821        Condition::IsNotNull { column_id } => {
1822            let Some(col) = column_by_id(table, *column_id) else {
1823                return false;
1824            };
1825            !matches!(row.values.get(&col.name), None | Some(Value::Null))
1826        }
1827        // Conditions the Kit never emits (Ann, SparseMatch, FmContainsAll)
1828        // — assume the index already resolved them.
1829        _ => true,
1830    }
1831}
1832
1833fn column_by_id(table: &KitTable, column_id: u16) -> Option<&Column> {
1834    table.columns.iter().find(|col| col.id as u16 == column_id)
1835}
1836
1837fn value_index_key(col: &Column, values: &Map<String, Value>) -> Option<Vec<u8>> {
1838    let value = values.get(&col.name).unwrap_or(&Value::Null);
1839    if value.is_null() {
1840        return None;
1841    }
1842    json_to_core(value, col.storage_type)
1843        .ok()
1844        .map(|value| value.encode_key())
1845}