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