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