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            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_kit_only(&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_kit_only(&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_kit_only(&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    ///
884    /// For multi-retriever fusion, scores, and optional exact rerank, use
885    /// [`Self::search`] instead.
886    pub fn ann_search(
887        &self,
888        table: &str,
889        column: &str,
890        query: Vec<f32>,
891        k: usize,
892    ) -> Result<Vec<Row>> {
893        let t = self.require_table(table)?;
894        let col = t
895            .column(column)
896            .ok_or_else(|| KitError::Validation(format!("unknown column \"{column}\"")))?;
897        let cond = Condition::Ann {
898            column_id: col.id as u16,
899            query,
900            k,
901        };
902        self.snapshot_rows_pushed(table, &[cond])
903    }
904
905    /// Hybrid scored search: one or more named retrievers, reciprocal-rank
906    /// fusion, optional exact-vector rerank, hard `must` filters.
907    ///
908    /// Calls the engine's authorized search path
909    /// ([`mongreldb_core::Database::search_for_current_principal`]) — the same
910    /// pipeline as the C ABI `mongreldb_table_search`, NAPI `table.search`, and
911    /// daemon `POST /kit/search`. Does **not** include uncommitted staged rows;
912    /// commit first if you need to search writes from this transaction.
913    pub fn search(
914        &self,
915        table: &str,
916        spec: crate::search::SearchSpec,
917    ) -> Result<Vec<crate::search::SearchHit>> {
918        if self.has_staged_for(table) {
919            return Err(KitError::Validation(
920                "search does not include uncommitted staged rows; commit first".into(),
921            ));
922        }
923        let t = self.require_table(table)?;
924        let request = crate::search::build_core_request(t, &spec)?;
925        let hits = self
926            .db
927            .core_db()
928            .search_for_current_principal(table, &request)
929            .map_err(KitError::from)?;
930        hits.into_iter()
931            .map(|hit| crate::search::core_hit_to_kit(hit, t))
932            .collect()
933    }
934
935    /// Learned-sparse (SPLADE-style) retrieval: return the `k` rows whose
936    /// `Sparse` column best matches the weighted query tokens `query`
937    /// (`(token_id, weight)` pairs), resolved by the column's sparse index.
938    /// Requires a `Sparse` index on `column`.
939    pub fn sparse_match(
940        &self,
941        table: &str,
942        column: &str,
943        query: Vec<(u32, f32)>,
944        k: usize,
945    ) -> Result<Vec<Row>> {
946        let t = self.require_table(table)?;
947        let col = t
948            .column(column)
949            .ok_or_else(|| KitError::Validation(format!("unknown column \"{column}\"")))?;
950        let cond = Condition::SparseMatch {
951            column_id: col.id as u16,
952            query,
953            k,
954        };
955        self.snapshot_rows_pushed(table, &[cond])
956    }
957
958    pub fn execute(&mut self, query: &Query) -> Result<Vec<Value>> {
959        match query {
960            Query::Select(_) => Err(KitError::Validation("use select() for SELECT".into())),
961            Query::Insert(insert) => Ok(vec![self.insert_returning(
962                &insert.table,
963                insert.values.clone(),
964                insert.returning.clone(),
965            )?]),
966            Query::Upsert(upsert) => Ok(vec![self.upsert(
967                &upsert.table,
968                upsert.values.clone(),
969                upsert.on_conflict.clone(),
970                upsert.returning.clone(),
971            )?]),
972            Query::Update(update) => {
973                if update.pk.is_some() && update.filter.is_some() {
974                    return Err(KitError::Validation(
975                        "update cannot specify both pk and filter".into(),
976                    ));
977                }
978                if let Some(pk) = &update.pk {
979                    let row = self.update(&update.table, pk, update.set.clone())?;
980                    Ok(vec![project_returning(&row, &update.returning)?])
981                } else if let Some(filter) = &update.filter {
982                    self.update_where(
983                        &update.table,
984                        Some(filter.clone()),
985                        update.set.clone(),
986                        update.returning.clone(),
987                    )
988                } else {
989                    Err(KitError::Validation(
990                        "update requires either a pk or a filter".into(),
991                    ))
992                }
993            }
994            Query::Delete(delete) => {
995                if delete.pk.is_some() && delete.filter.is_some() {
996                    return Err(KitError::Validation(
997                        "delete cannot specify both pk and filter".into(),
998                    ));
999                }
1000                if let Some(pk) = &delete.pk {
1001                    let t = self.require_table(&delete.table)?;
1002                    let pk_map = pk_to_map(pk, t)?;
1003                    let projected = match self.get_by_pk_internal(&delete.table, &pk_map)? {
1004                        Some(row) => project_returning(&row, &delete.returning)?,
1005                        None => {
1006                            // Propagate the NotFound error from the canonical delete path.
1007                            self.delete(&delete.table, pk)?;
1008                            unreachable!("delete returned Ok for a missing row")
1009                        }
1010                    };
1011                    self.delete(&delete.table, pk)?;
1012                    Ok(vec![projected])
1013                } else if let Some(filter) = &delete.filter {
1014                    self.delete_where(
1015                        &delete.table,
1016                        Some(filter.clone()),
1017                        delete.returning.clone(),
1018                    )
1019                } else {
1020                    Err(KitError::Validation(
1021                        "delete requires either a pk or a filter".into(),
1022                    ))
1023                }
1024            }
1025            Query::Aggregate(_) | Query::Join(_) => Err(KitError::Validation(
1026                "aggregate/join are not mutating statements".into(),
1027            )),
1028        }
1029    }
1030
1031    /// Build an execution context whose table fetcher reads visible rows at this
1032    /// transaction's read snapshot. When conditions are provided, the fetcher
1033    /// resolves them via native indexes (Kit Priority 1 pushdown).
1034    fn exec_ctx(&self) -> ExecCtx<'_> {
1035        ExecCtx::new(
1036            Some(&self.db.schema),
1037            |name: &str, conds: Option<&[Condition]>| match conds {
1038                Some(c) if !c.is_empty() => self.snapshot_rows_pushed(name, c),
1039                _ => self.snapshot_rows(name),
1040            },
1041        )
1042    }
1043
1044    /// Commit the transaction.
1045    ///
1046    /// A transaction that staged a large batch on some table (typically via
1047    /// [`Self::insert_many`]) flushes that table afterward. Without this, a
1048    /// committed-but-unflushed batch exists only as WAL records: every
1049    /// subsequent `Database::open` (there is no warm/daemon mode for the CLI,
1050    /// and any short-lived process pays this on every invocation) must fully
1051    /// replay and re-index the whole batch just to open the table, which is
1052    /// far more expensive than the one-time flush. Below the threshold this
1053    /// is a no-op cost (the loop below runs over `self.staged`, already in
1054    /// memory) so ordinary interactive transactions are unaffected.
1055    pub fn commit(self) -> Result<()> {
1056        const FLUSH_AFTER_ROWS: usize = 1000;
1057        let mut counts: HashMap<&str, usize> = HashMap::new();
1058        for op in &self.staged {
1059            let table = match op {
1060                StagedOp::Insert { table, .. }
1061                | StagedOp::Update { table, .. }
1062                | StagedOp::Delete { table, .. }
1063                | StagedOp::Truncate { table } => table.as_str(),
1064            };
1065            *counts.entry(table).or_default() += 1;
1066        }
1067        let tables_to_flush: Vec<&str> = counts
1068            .into_iter()
1069            .filter(|&(_, n)| n >= FLUSH_AFTER_ROWS)
1070            .map(|(table, _)| table)
1071            .collect();
1072        let db = self.db;
1073        self.core.commit().map_err(KitError::from)?;
1074        for table in tables_to_flush {
1075            let _ = db.flush_table(table);
1076        }
1077        Ok(())
1078    }
1079
1080    /// Roll back the transaction.
1081    pub fn rollback(self) {
1082        self.core.rollback();
1083    }
1084
1085    // ── internals ──────────────────────────────────────────────────────────
1086
1087    fn alloc_temp_id(&mut self) -> u64 {
1088        let id = self.next_temp_id;
1089        self.next_temp_id += 1;
1090        id
1091    }
1092
1093    fn require_table(&self, name: &str) -> Result<&KitTable> {
1094        self.db
1095            .table(name)
1096            .ok_or_else(|| KitError::Integrity(format!("table {name} not found")))
1097    }
1098
1099    /// All rows of `table` visible to this transaction (staged writes included).
1100    pub fn all_rows(&self, table: &str) -> Result<Vec<Row>> {
1101        self.snapshot_rows(table)
1102    }
1103
1104    fn snapshot_rows(&self, table: &str) -> Result<Vec<Row>> {
1105        let t = self.require_table(table)?;
1106        let core_rows = self
1107            .db
1108            .visible_core_rows_at(table, self.core.read_snapshot())?;
1109        let mut rows = core_rows
1110            .into_iter()
1111            .map(|r| core_row_to_json(&r, t))
1112            .collect::<Result<Vec<_>>>()?;
1113        self.replay_staged_rows(t, &mut rows);
1114        Ok(rows)
1115    }
1116
1117    /// Fetch rows for `table` with native `conditions` resolved by the engine
1118    /// (Kit Priority 1 pushdown). Avoids the full scan that `snapshot_rows`
1119    /// does — the engine resolves conditions via HOT/bitmap/range indexes.
1120    fn snapshot_rows_pushed(&self, table: &str, conditions: &[Condition]) -> Result<Vec<Row>> {
1121        let t = self.require_table(table)?;
1122        if self.has_staged_for(table) {
1123            return Ok(self
1124                .snapshot_rows(table)?
1125                .into_iter()
1126                .filter(|r| row_matches_conditions(t, r, conditions))
1127                .collect());
1128        }
1129        let core_rows = self
1130            .db
1131            .query_core_rows_at(table, conditions, self.core.read_snapshot())?;
1132        core_rows
1133            .into_iter()
1134            .map(|r| core_row_to_json(&r, t))
1135            .collect()
1136    }
1137
1138    fn get_by_pk_internal(&self, table: &str, pk_map: &Map<String, Value>) -> Result<Option<Row>> {
1139        let t = self.require_table(table)?;
1140        if self.has_staged_for(table) {
1141            let rows = self.snapshot_rows(table)?;
1142            return Ok(rows.into_iter().find(|r| pk_matches(&r.values, pk_map, t)));
1143        }
1144        // Kit Priority 1 pushdown: use native PK index (O(1) HOT probe) instead
1145        // of O(N) full scan. Falls back to scan if conditions can't be built.
1146        if let Some(conditions) = crate::pushdown::pk_conditions(t, pk_map) {
1147            let core_rows =
1148                self.db
1149                    .query_core_rows_at(table, &conditions, self.core.read_snapshot())?;
1150            return Ok(core_rows
1151                .into_iter()
1152                .filter_map(|r| core_row_to_json(&r, t).ok())
1153                .find(|r| pk_matches(&r.values, pk_map, t)));
1154        }
1155        let rows = self.snapshot_rows(table)?;
1156        Ok(rows.into_iter().find(|r| pk_matches(&r.values, pk_map, t)))
1157    }
1158
1159    fn internal_rows(&self, table: &str) -> Result<Vec<CoreRow>> {
1160        self.db
1161            .visible_core_rows_at(table, self.core.read_snapshot())
1162    }
1163
1164    // ── unique constraints ─────────────────────────────────────────────────
1165
1166    /// Reject the row if any of its unique keys is already owned by a different
1167    /// row, either in committed state or in this transaction's staging.
1168    fn check_unique_constraints(
1169        &self,
1170        table: &KitTable,
1171        values: &Map<String, Value>,
1172        old_values: Option<&Map<String, Value>>,
1173        preloaded_guards: Option<&[CoreRow]>,
1174    ) -> Result<()> {
1175        if table.unique_constraints.is_empty() {
1176            return Ok(());
1177        }
1178        let owner_pk = encoded_pk_for(table, values);
1179        // P6: reuse the batch-preloaded committed guards instead of re-scanning
1180        // the `__kit_unique_keys` table on every row.
1181        let owned;
1182        let committed: &[CoreRow] = match preloaded_guards {
1183            Some(g) => g,
1184            None => {
1185                owned = self.internal_rows(UNIQUE_KEYS)?;
1186                &owned
1187            }
1188        };
1189        for uq in &table.unique_constraints {
1190            let Some(key) = unique_key(table, uq, values) else {
1191                continue;
1192            };
1193            // Unchanged keys (update where the value did not move) are fine.
1194            if let Some(old) = old_values {
1195                if unique_key(table, uq, old).as_deref() == Some(key.as_str()) {
1196                    continue;
1197                }
1198            }
1199            self.assert_key_free(committed, table, uq, &key, &owner_pk)?;
1200        }
1201        Ok(())
1202    }
1203
1204    fn assert_key_free(
1205        &self,
1206        committed: &[CoreRow],
1207        table: &KitTable,
1208        uq: &UniqueConstraint,
1209        key: &str,
1210        owner_pk: &str,
1211    ) -> Result<()> {
1212        for guard in committed {
1213            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(key) {
1214                let g_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
1215                let g_pk = internal_bytes(guard, cols::UQ_OWNER_PK).unwrap_or_default();
1216                if g_table != table.name || g_pk != owner_pk {
1217                    return Err(KitError::Duplicate(format!(
1218                        "unique constraint {} on {}",
1219                        uq.name, table.name
1220                    )));
1221                }
1222            }
1223        }
1224        for staged in &self.staged_unique {
1225            if staged.encoded_key == key
1226                && (staged.owner_table != table.name || staged.owner_pk != owner_pk)
1227            {
1228                return Err(KitError::Duplicate(format!(
1229                    "unique constraint {} on {}",
1230                    uq.name, table.name
1231                )));
1232            }
1233        }
1234        Ok(())
1235    }
1236
1237    /// Reserve new unique guard rows for `values`, deleting any stale guards that
1238    /// belonged to the previous version of the row (on update).
1239    fn reserve_unique_guards(
1240        &mut self,
1241        table: &KitTable,
1242        values: &Map<String, Value>,
1243        old_values: Option<&Map<String, Value>>,
1244    ) -> Result<()> {
1245        let owner_pk = encoded_pk_for(table, values);
1246        for uq in &table.unique_constraints {
1247            let new_key = unique_key(table, uq, values);
1248            let old_key = old_values.and_then(|old| unique_key(table, uq, old));
1249            if new_key == old_key {
1250                continue;
1251            }
1252            if let Some(key) = &new_key {
1253                self.put_unique_guard(&uq.name, key, &table.name, &owner_pk)?;
1254            }
1255            if let Some(key) = &old_key {
1256                self.delete_unique_guard(key)?;
1257            }
1258        }
1259        Ok(())
1260    }
1261
1262    fn put_unique_guard(
1263        &mut self,
1264        constraint: &str,
1265        encoded_key: &str,
1266        owner_table: &str,
1267        owner_pk: &str,
1268    ) -> Result<()> {
1269        let now = iso_now();
1270        self.core
1271            .put(
1272                UNIQUE_KEYS,
1273                vec![
1274                    (cols::UQ_ENCODED, CoreValue::Bytes(encoded_key.into())),
1275                    (cols::UQ_CONSTRAINT, CoreValue::Bytes(constraint.into())),
1276                    (cols::UQ_OWNER_TABLE, CoreValue::Bytes(owner_table.into())),
1277                    (cols::UQ_OWNER_PK, CoreValue::Bytes(owner_pk.into())),
1278                    (cols::UQ_CREATED, CoreValue::Bytes(now.into_bytes())),
1279                ],
1280            )
1281            .map_err(KitError::from)?;
1282        self.staged_unique.push(StagedUnique {
1283            encoded_key: encoded_key.to_string(),
1284            owner_table: owner_table.to_string(),
1285            owner_pk: owner_pk.to_string(),
1286        });
1287        Ok(())
1288    }
1289
1290    fn delete_unique_guard(&mut self, encoded_key: &str) -> Result<()> {
1291        let committed = self.internal_rows(UNIQUE_KEYS)?;
1292        for guard in &committed {
1293            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(encoded_key) {
1294                self.core
1295                    .delete(UNIQUE_KEYS, guard.row_id)
1296                    .map_err(KitError::from)?;
1297            }
1298        }
1299        self.staged_unique.retain(|s| s.encoded_key != encoded_key);
1300        Ok(())
1301    }
1302
1303    /// Delete every unique guard owned by the given row (used on row delete).
1304    fn delete_unique_guards_for_owner(&mut self, table: &KitTable, owner_pk: &str) -> Result<()> {
1305        let constraint_names: HashSet<&str> = table
1306            .unique_constraints
1307            .iter()
1308            .map(|u| u.name.as_str())
1309            .collect();
1310        let committed = self.internal_rows(UNIQUE_KEYS)?;
1311        for guard in &committed {
1312            let g_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
1313            let g_pk = internal_bytes(guard, cols::UQ_OWNER_PK).unwrap_or_default();
1314            let g_constraint = internal_bytes(guard, cols::UQ_CONSTRAINT).unwrap_or_default();
1315            if g_table == table.name
1316                && g_pk == owner_pk
1317                && (constraint_names.contains(g_constraint.as_str())
1318                    || g_constraint == pk_guard_constraint(table))
1319            {
1320                self.core
1321                    .delete(UNIQUE_KEYS, guard.row_id)
1322                    .map_err(KitError::from)?;
1323            }
1324        }
1325        let owner_pk = owner_pk.to_string();
1326        let name = table.name.clone();
1327        self.staged_unique
1328            .retain(|s| !(s.owner_table == name && s.owner_pk == owner_pk));
1329        Ok(())
1330    }
1331
1332    // ── primary-key handling ───────────────────────────────────────────────
1333    //
1334    // Matches the TypeScript kit: an auto-assigned (sequence) primary key is
1335    // guaranteed unique and skipped; an explicit single-column primary key is
1336    // checked directly against the visible rows (no guard row); only an explicit
1337    // composite primary key reserves a `__pk_<table>` guard in `__kit_unique_keys`
1338    // (it has no single native key to probe), making the duplicate insert throw
1339    // and stay conflict-safe.
1340
1341    fn check_pk(
1342        &self,
1343        table: &KitTable,
1344        values: &Map<String, Value>,
1345        pk_explicit: bool,
1346        pk_seen: Option<&mut HashSet<String>>,
1347    ) -> Result<()> {
1348        // An auto-assigned primary key is unique by construction; nothing to do.
1349        if table.primary_key.is_empty() || !pk_explicit {
1350            return Ok(());
1351        }
1352
1353        if table.primary_key.len() == 1 {
1354            // A single-column explicit PK has a native key, so check whether a row
1355            // with that PK already exists. A batch passes a pre-loaded set so the
1356            // check stays O(1) per row; a single insert checks the visible rows
1357            // (committed plus this transaction's in-flight staging) directly.
1358            let duplicate = match pk_seen {
1359                Some(seen) => {
1360                    let key = encoded_pk_for(table, values);
1361                    if seen.contains(&key) {
1362                        true
1363                    } else {
1364                        seen.insert(key);
1365                        false
1366                    }
1367                }
1368                None => {
1369                    self.parent_exists(&table.name, values)?
1370                        || self.staged_row_exists(table, values)
1371                }
1372            };
1373            if duplicate {
1374                return Err(KitError::Duplicate(format!(
1375                    "primary key {} on {}",
1376                    encoded_pk_for(table, values),
1377                    table.name
1378                )));
1379            }
1380            return Ok(());
1381        }
1382
1383        // A composite explicit PK uses a guard row (conflict-safe), like the
1384        // unique-constraint machinery.
1385        let key = pk_guard_key(table, values);
1386        let owner_pk = encoded_pk_for(table, values);
1387        let committed = self.internal_rows(UNIQUE_KEYS)?;
1388        for guard in &committed {
1389            if internal_bytes(guard, cols::UQ_ENCODED).as_deref() == Some(key.as_str()) {
1390                return Err(KitError::Duplicate(format!(
1391                    "primary key {} on {}",
1392                    owner_pk, table.name
1393                )));
1394            }
1395        }
1396        for staged in &self.staged_unique {
1397            if staged.encoded_key == key {
1398                return Err(KitError::Duplicate(format!(
1399                    "primary key {} on {}",
1400                    owner_pk, table.name
1401                )));
1402            }
1403        }
1404        Ok(())
1405    }
1406
1407    fn reserve_pk_guard(
1408        &mut self,
1409        table: &KitTable,
1410        values: &Map<String, Value>,
1411        pk_explicit: bool,
1412    ) -> Result<()> {
1413        // Only an explicit composite primary key needs a guard row. A single-column
1414        // PK is checked directly, and an auto-assigned PK is guaranteed unique.
1415        if table.primary_key.len() < 2 || !pk_explicit {
1416            return Ok(());
1417        }
1418        let key = pk_guard_key(table, values);
1419        let owner_pk = encoded_pk_for(table, values);
1420        let constraint = pk_guard_constraint(table);
1421        self.put_unique_guard(&constraint, &key, &table.name, &owner_pk)
1422    }
1423
1424    // ── foreign keys ───────────────────────────────────────────────────────
1425
1426    fn check_foreign_keys(
1427        &self,
1428        table: &KitTable,
1429        values: &Map<String, Value>,
1430        mut fk_cache: Option<&mut HashMap<String, bool>>,
1431    ) -> Result<()> {
1432        for fk in &table.foreign_keys {
1433            if fk_values_null(fk, values) {
1434                continue;
1435            }
1436            let parent = self.require_table(&fk.references_table)?;
1437            let parent_pk = parent_pk_value(values, fk, parent)?;
1438            let parent_pk_map = pk_to_map(&parent_pk, parent)?;
1439            // P6: cache parent-existence per `"table\x00encoded_pk"` so repeated
1440            // parent ids in a bulk batch probe the engine once.
1441            let cache_key = format!(
1442                "{}\x00{}",
1443                fk.references_table,
1444                encode_pk(&pk_components(parent, &parent_pk_map))
1445            );
1446            let exists = if let Some(cache) = fk_cache.as_mut() {
1447                if let Some(&hit) = cache.get(&cache_key) {
1448                    hit
1449                } else {
1450                    let hit = self.parent_exists(&fk.references_table, &parent_pk_map)?
1451                        || self.staged_row_exists(parent, &parent_pk_map);
1452                    cache.insert(cache_key, hit);
1453                    hit
1454                }
1455            } else {
1456                self.parent_exists(&fk.references_table, &parent_pk_map)?
1457                    || self.staged_row_exists(parent, &parent_pk_map)
1458            };
1459            if exists {
1460                continue;
1461            }
1462            return Err(KitError::ForeignKey(format!(
1463                "{} references {}({})",
1464                fk.name,
1465                fk.references_table,
1466                fk.references_columns.join(",")
1467            )));
1468        }
1469        Ok(())
1470    }
1471
1472    fn touch_foreign_key_guards(
1473        &mut self,
1474        table: &KitTable,
1475        values: &Map<String, Value>,
1476    ) -> Result<()> {
1477        for fk in table.foreign_keys.clone() {
1478            if fk_values_null(&fk, values) {
1479                continue;
1480            }
1481            let parent = self.require_table(&fk.references_table)?.clone();
1482            let components = parent_pk_components(values, &fk, &parent);
1483            self.touch_row_guard(&parent.name, &components)?;
1484        }
1485        Ok(())
1486    }
1487
1488    fn parent_exists(&self, table: &str, pk_map: &Map<String, Value>) -> Result<bool> {
1489        let t = self.require_table(table)?;
1490        if self.has_staged_for(table) {
1491            let rows = self.snapshot_rows(table)?;
1492            return Ok(rows.iter().any(|r| pk_matches(&r.values, pk_map, t)));
1493        }
1494        // Kit Priority 1 pushdown: O(1) HOT probe instead of O(N) full scan for
1495        // FK parent existence checks.
1496        if let Some(conditions) = crate::pushdown::pk_conditions(t, pk_map) {
1497            let core_rows =
1498                self.db
1499                    .query_core_rows_at(table, &conditions, self.core.read_snapshot())?;
1500            return Ok(!core_rows.is_empty());
1501        }
1502        let rows = self.snapshot_rows(table)?;
1503        Ok(rows.iter().any(|r| pk_matches(&r.values, pk_map, t)))
1504    }
1505
1506    /// Whether a row identified by `pk_map` exists in `table`'s in-flight staging
1507    /// for this transaction. Replays the staging in order (table-scoped) so a row
1508    /// inserted and then deleted within the same transaction is not treated as
1509    /// present. Used both for foreign-key parent checks and for the single-column
1510    /// primary-key duplicate check.
1511    fn staged_row_exists(&self, table: &KitTable, pk_map: &Map<String, Value>) -> bool {
1512        let target = encode_pk(&pk_components(table, pk_map));
1513        let mut exists = false;
1514        for staged in &self.staged {
1515            match staged {
1516                StagedOp::Insert { table: t, values }
1517                | StagedOp::Update {
1518                    table: t, values, ..
1519                } => {
1520                    if t == &table.name && pk_matches(values, pk_map, table) {
1521                        exists = true;
1522                    }
1523                }
1524                StagedOp::Delete { table: t, pk } => {
1525                    if t == &table.name && *pk == target {
1526                        exists = false;
1527                    }
1528                }
1529                StagedOp::Truncate { table: t } => {
1530                    if t == &table.name {
1531                        exists = false;
1532                    }
1533                }
1534            }
1535        }
1536        exists
1537    }
1538
1539    // ── row guards ─────────────────────────────────────────────────────────
1540
1541    fn touch_row_guard(&mut self, table: &str, pk_components: &[KeyComponent]) -> Result<()> {
1542        let encoded_pk = encode_pk(pk_components);
1543        let guard_key = encode_row_guard_key(table, &encoded_pk);
1544        if !self.touched_guards.insert(guard_key.clone()) {
1545            return Ok(());
1546        }
1547        // Replace any existing committed guard, bumping the version. `RG_ENCODED`
1548        // is `ROW_GUARDS`' primary key, so an indexed point lookup (matching at
1549        // most one row) replaces the full-table scan `internal_rows` would do.
1550        let mut version = 1i64;
1551        let committed = self.db.query_core_rows_at(
1552            ROW_GUARDS,
1553            &[Condition::Pk(guard_key.as_bytes().to_vec())],
1554            self.core.read_snapshot(),
1555        )?;
1556        for guard in &committed {
1557            if internal_bytes(guard, cols::RG_ENCODED).as_deref() == Some(guard_key.as_str()) {
1558                if let Some(CoreValue::Int64(v)) = guard.columns.get(&cols::RG_VERSION) {
1559                    version = v + 1;
1560                }
1561                self.core
1562                    .delete(ROW_GUARDS, guard.row_id)
1563                    .map_err(KitError::from)?;
1564            }
1565        }
1566        let now = iso_now();
1567        self.core
1568            .put(
1569                ROW_GUARDS,
1570                vec![
1571                    (cols::RG_ENCODED, CoreValue::Bytes(guard_key.into_bytes())),
1572                    (cols::RG_TABLE, CoreValue::Bytes(table.into())),
1573                    (cols::RG_PK, CoreValue::Bytes(encoded_pk.into_bytes())),
1574                    (cols::RG_VERSION, CoreValue::Int64(version)),
1575                    (cols::RG_UPDATED, CoreValue::Bytes(now.into_bytes())),
1576                ],
1577            )
1578            .map_err(KitError::from)?;
1579        Ok(())
1580    }
1581
1582    fn has_staged_for(&self, table: &str) -> bool {
1583        self.staged.iter().any(|op| match op {
1584            StagedOp::Insert { table: t, .. }
1585            | StagedOp::Update { table: t, .. }
1586            | StagedOp::Delete { table: t, .. }
1587            | StagedOp::Truncate { table: t } => t == table,
1588        })
1589    }
1590
1591    fn replay_staged_rows(&self, table: &KitTable, rows: &mut Vec<Row>) {
1592        for staged in &self.staged {
1593            match staged {
1594                StagedOp::Insert { table: t, values } if t == &table.name => {
1595                    let pk = encoded_pk_for(table, values);
1596                    rows.retain(|r| encoded_pk_for(table, &r.values) != pk);
1597                    rows.push(Row {
1598                        row_id: 0,
1599                        values: values.clone(),
1600                    });
1601                }
1602                StagedOp::Update {
1603                    table: t,
1604                    old_pk,
1605                    row_id,
1606                    values,
1607                } if t == &table.name => {
1608                    let new_pk = encoded_pk_for(table, values);
1609                    rows.retain(|r| {
1610                        let pk = encoded_pk_for(table, &r.values);
1611                        pk != *old_pk && pk != new_pk
1612                    });
1613                    rows.push(Row {
1614                        row_id: *row_id,
1615                        values: values.clone(),
1616                    });
1617                }
1618                StagedOp::Delete { table: t, pk } if t == &table.name => {
1619                    rows.retain(|r| encoded_pk_for(table, &r.values) != *pk);
1620                }
1621                StagedOp::Truncate { table: t } if t == &table.name => {
1622                    rows.clear();
1623                }
1624                _ => {}
1625            }
1626        }
1627    }
1628
1629    /// Remove unique + pk guards for a row that is being deleted.
1630    fn delete_guards_for(&mut self, table: &KitTable, values: &Map<String, Value>) -> Result<()> {
1631        let owner_pk = encoded_pk_for(table, values);
1632        self.delete_unique_guards_for_owner(table, &owner_pk)
1633    }
1634
1635    fn delete_all_guards_for_table(&mut self, table: &KitTable) -> Result<()> {
1636        for guard in self.internal_rows(UNIQUE_KEYS)? {
1637            let owner_table = internal_bytes(&guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
1638            if owner_table == table.name {
1639                self.core
1640                    .delete(UNIQUE_KEYS, guard.row_id)
1641                    .map_err(KitError::from)?;
1642            }
1643        }
1644        for guard in self.internal_rows(ROW_GUARDS)? {
1645            let owner_table = internal_bytes(&guard, cols::RG_TABLE).unwrap_or_default();
1646            if owner_table == table.name {
1647                self.core
1648                    .delete(ROW_GUARDS, guard.row_id)
1649                    .map_err(KitError::from)?;
1650            }
1651        }
1652        self.staged_unique.retain(|s| s.owner_table != table.name);
1653        self.touched_guards
1654            .retain(|key| !key.starts_with(&format!("{}:", table.name)));
1655        Ok(())
1656    }
1657
1658    // ── delete planning ────────────────────────────────────────────────────
1659
1660    fn plan_delete(&self, table: &str, row: &Row) -> Result<(DeletePlan, HashMap<String, Row>)> {
1661        let schema = &self.db.schema;
1662        let t = self.require_table(table)?;
1663        let pk_str = encoded_pk_for(t, &row.values);
1664        // Cache every child row discovered while planning, keyed by
1665        // "<table>:<encoded_pk>", so the apply phase can reuse it instead of
1666        // re-reading each row by PK — which would make a bulk cascade / set-null
1667        // delete O(n^2) (one full table scan per affected child row).
1668        let row_cache: RefCell<HashMap<String, Row>> = RefCell::new(HashMap::new());
1669        let find_children =
1670            |child_table: &KitTable, fk: &ForeignKey, parent_pk: &str| -> Vec<(String, String)> {
1671                let parent_pk_value = match pk_string_to_value(parent_pk, t) {
1672                    Ok(v) => v,
1673                    Err(_) => return Vec::new(),
1674                };
1675                let parent_pk_map = match pk_to_map(&parent_pk_value, t) {
1676                    Ok(m) => m,
1677                    Err(_) => return Vec::new(),
1678                };
1679                let child_rows = match self.snapshot_rows(&child_table.name) {
1680                    Ok(r) => r,
1681                    Err(_) => return Vec::new(),
1682                };
1683                let mut out = Vec::new();
1684                for child_row in child_rows {
1685                    if fk_matches(&child_row.values, fk, &parent_pk_map, t) {
1686                        let child_pk = encoded_pk_for(child_table, &child_row.values);
1687                        row_cache
1688                            .borrow_mut()
1689                            .insert(format!("{}:{}", child_table.name, child_pk), child_row);
1690                        out.push((child_pk, parent_pk.to_string()));
1691                    }
1692                }
1693                out
1694            };
1695        let plan = plan_delete(schema, table, &pk_str, find_children).map_err(KitError::from)?;
1696        Ok((plan, row_cache.into_inner()))
1697    }
1698
1699    fn apply_set_null(
1700        &mut self,
1701        set_null: &mongreldb_kit_core::planner::SetNullUpdate,
1702        child_row: &Row,
1703    ) -> Result<()> {
1704        let child_table = self.require_table(&set_null.table)?.clone();
1705        let mut values = child_row.values.clone();
1706        for col in &set_null.columns {
1707            let col_def = child_table
1708                .column(col)
1709                .ok_or_else(|| KitError::Integrity(format!("set-null column {col} not found")))?;
1710            if !col_def.nullable {
1711                return Err(KitError::Restrict(format!(
1712                    "set-null on non-nullable column {col}"
1713                )));
1714            }
1715            values.insert(col.clone(), Value::Null);
1716        }
1717        // Re-run validation (including checks) on the patched child row.
1718        mongreldb_kit_core::validation::validate_row_kit_only(&child_table, &values)?;
1719        // Recompute unique guards for the patched row. The row itself survives a
1720        // set-null (only its FK columns change), so its primary-key guard is
1721        // re-reserved after `delete_guards_for` clears it.
1722        self.delete_guards_for(&child_table, &child_row.values)?;
1723        let cells = row_to_core_cells(&values, &child_table)?;
1724        self.core
1725            .delete(&set_null.table, RowId(child_row.row_id))
1726            .map_err(KitError::from)?;
1727        self.core
1728            .put(&set_null.table, cells)
1729            .map_err(KitError::from)?;
1730        self.reserve_unique_guards(&child_table, &values, None)?;
1731        // The row keeps its full primary key (only FK columns changed), so it is
1732        // "explicit"; this re-reserves a composite PK guard and is a no-op for a
1733        // single-column PK.
1734        self.reserve_pk_guard(&child_table, &values, true)?;
1735        self.staged.push(StagedOp::Update {
1736            table: set_null.table.clone(),
1737            old_pk: encoded_pk_for(&child_table, &child_row.values),
1738            row_id: child_row.row_id,
1739            values: values.clone(),
1740        });
1741        Ok(())
1742    }
1743
1744    // ── defaults ───────────────────────────────────────────────────────────
1745
1746    fn apply_defaults(&self, row: &mut Map<String, Value>, table: &KitTable) -> Result<()> {
1747        for col in &table.columns {
1748            if row.contains_key(&col.name) && row.get(&col.name) != Some(&Value::Null) {
1749                continue;
1750            }
1751            let Some(default) = &col.default else {
1752                continue;
1753            };
1754            let value = match default {
1755                DefaultKind::Static(v) => v.clone(),
1756                DefaultKind::Now => {
1757                    let now = iso_now();
1758                    if col.storage_type == ColumnType::Date {
1759                        Value::String(now[..10].to_string())
1760                    } else {
1761                        Value::String(now)
1762                    }
1763                }
1764                DefaultKind::Uuid => Value::String(uuid::Uuid::new_v4().to_string()),
1765                DefaultKind::Sequence(name) => {
1766                    let start = self.db.allocate_sequence(name, 1)?;
1767                    Value::Number(start.into())
1768                }
1769                DefaultKind::CustomName(name) => {
1770                    let provider = self.db.default_providers.get(name).ok_or_else(|| {
1771                        KitError::Validation(format!("custom default \"{name}\" is not registered"))
1772                    })?;
1773                    provider()
1774                }
1775            };
1776            row.insert(col.name.clone(), value);
1777        }
1778        Ok(())
1779    }
1780
1781    /// Refresh write-managed `now` columns on update.
1782    ///
1783    /// Only a `generated` column whose default is `now` (e.g. `updatedAt`) is a
1784    /// write-managed timestamp that refreshes on every update. A plain
1785    /// `default: now` column (e.g. `createdAt`) is an insert-time value and must
1786    /// NOT change on update. A column already present in the caller's patch is
1787    /// left as supplied. Mirrors the TypeScript kit's `applyUpdateDefaults`.
1788    fn apply_update_defaults(
1789        &self,
1790        merged: &mut Map<String, Value>,
1791        patch_keys: &HashSet<String>,
1792        table: &KitTable,
1793    ) {
1794        let mut now: Option<String> = None;
1795        for col in &table.columns {
1796            if patch_keys.contains(&col.name) {
1797                continue;
1798            }
1799            if col.generated && matches!(col.default, Some(DefaultKind::Now)) {
1800                let stamp = now.get_or_insert_with(iso_now);
1801                let value = if col.storage_type == ColumnType::Date {
1802                    Value::String(stamp[..10].to_string())
1803                } else {
1804                    Value::String(stamp.clone())
1805                };
1806                merged.insert(col.name.clone(), value);
1807            }
1808        }
1809    }
1810}
1811
1812// ── free helpers ───────────────────────────────────────────────────────────
1813
1814fn project_returning(row: &Row, columns: &[String]) -> Result<Value> {
1815    let mut out = Map::new();
1816    for c in columns {
1817        out.insert(c.clone(), row.values.get(c).cloned().unwrap_or(Value::Null));
1818    }
1819    Ok(Value::Object(out))
1820}
1821
1822fn pk_values_map(table: &KitTable, values: &Map<String, Value>) -> Map<String, Value> {
1823    let mut out = Map::new();
1824    for name in &table.primary_key {
1825        out.insert(
1826            name.clone(),
1827            values.get(name).cloned().unwrap_or(Value::Null),
1828        );
1829    }
1830    out
1831}
1832
1833/// Convert only the columns present in `patch` to core cells.
1834/// Missing columns are intentionally omitted so an upsert DO UPDATE patch does
1835/// not overwrite unchanged cells with NULL.
1836fn patch_to_core_cells(
1837    patch: &Map<String, Value>,
1838    table: &KitTable,
1839) -> Result<Vec<(u16, CoreValue)>> {
1840    let mut cells = Vec::new();
1841    for (name, value) in patch {
1842        let Some(col) = table.column(name) else {
1843            continue;
1844        };
1845        cells.push((col.id as u16, json_to_core(value, col.storage_type)?));
1846    }
1847    Ok(cells)
1848}
1849
1850fn select_all(table: &str, filter: Option<Expr>) -> Select {
1851    Select {
1852        table: table.to_string(),
1853        columns: vec![],
1854        filter,
1855        order_by: vec![],
1856        limit: None,
1857        offset: None,
1858    }
1859}
1860
1861fn pk_matches(values: &Map<String, Value>, pk_map: &Map<String, Value>, table: &KitTable) -> bool {
1862    for name in &table.primary_key {
1863        if values.get(name) != pk_map.get(name) {
1864            return false;
1865        }
1866    }
1867    true
1868}
1869
1870/// Whether the caller supplied every primary-key column (non-null) in `row`.
1871///
1872/// Mirrors the TypeScript kit's `pkExplicit` flag: a primary key whose columns
1873/// all came from a sequence default (i.e. were not supplied) is auto-assigned and
1874/// guaranteed unique, so it is neither checked nor guarded.
1875fn pk_is_explicit(table: &KitTable, row: &Map<String, Value>) -> bool {
1876    !table.primary_key.is_empty()
1877        && table
1878            .primary_key
1879            .iter()
1880            .all(|name| matches!(row.get(name), Some(v) if !v.is_null()))
1881}
1882
1883fn pk_guard_constraint(table: &KitTable) -> String {
1884    format!("__pk_{}", table.name)
1885}
1886
1887fn pk_guard_key(table: &KitTable, values: &Map<String, Value>) -> String {
1888    encode_unique_key(
1889        KIT_KEY_VERSION,
1890        &pk_guard_constraint(table),
1891        &pk_components(table, values),
1892    )
1893}
1894
1895/// Build the typed key component for a column value.
1896pub(crate) fn key_component(col: &Column, value: Option<&Value>) -> KeyComponent {
1897    match value {
1898        None | Some(Value::Null) => KeyComponent::Null,
1899        Some(v) => match col.storage_type {
1900            ColumnType::Int8
1901            | ColumnType::Int16
1902            | ColumnType::Int32
1903            | ColumnType::Int64
1904            | ColumnType::TimestampNanos => KeyComponent::Int(v.as_i64().unwrap_or(0)),
1905            _ => KeyComponent::Text(value_to_text(v)),
1906        },
1907    }
1908}
1909
1910fn value_to_text(value: &Value) -> String {
1911    match value {
1912        Value::String(s) => s.clone(),
1913        other => other.to_string(),
1914    }
1915}
1916
1917fn pk_components(table: &KitTable, values: &Map<String, Value>) -> Vec<KeyComponent> {
1918    table
1919        .primary_key
1920        .iter()
1921        .map(|name| {
1922            let col = table.column(name);
1923            match col {
1924                Some(c) => key_component(c, values.get(name)),
1925                None => KeyComponent::Null,
1926            }
1927        })
1928        .collect()
1929}
1930
1931pub(crate) fn encoded_pk_for(table: &KitTable, values: &Map<String, Value>) -> String {
1932    encode_pk(&pk_components(table, values))
1933}
1934
1935pub(crate) fn unique_key(
1936    table: &KitTable,
1937    uq: &UniqueConstraint,
1938    values: &Map<String, Value>,
1939) -> Option<String> {
1940    let mut components = Vec::with_capacity(uq.columns.len());
1941    for name in &uq.columns {
1942        let col = table.column(name)?;
1943        let component = key_component(col, values.get(name));
1944        if component == KeyComponent::Null {
1945            return None; // nullable-unique: nulls never collide
1946        }
1947        components.push(component);
1948    }
1949    Some(encode_unique_key(KIT_KEY_VERSION, &uq.name, &components))
1950}
1951
1952pub(crate) fn fk_values_null(fk: &ForeignKey, values: &Map<String, Value>) -> bool {
1953    fk.columns
1954        .iter()
1955        .any(|c| values.get(c).map(|v| v.is_null()).unwrap_or(true))
1956}
1957
1958pub(crate) fn parent_pk_components(
1959    values: &Map<String, Value>,
1960    fk: &ForeignKey,
1961    parent: &KitTable,
1962) -> Vec<KeyComponent> {
1963    fk.columns
1964        .iter()
1965        .zip(&parent.primary_key)
1966        .map(|(child_col, parent_col)| {
1967            let col = parent.column(parent_col);
1968            match col {
1969                Some(c) => key_component(c, values.get(child_col)),
1970                None => KeyComponent::Null,
1971            }
1972        })
1973        .collect()
1974}
1975
1976fn parent_pk_value(
1977    values: &Map<String, Value>,
1978    fk: &ForeignKey,
1979    parent: &KitTable,
1980) -> Result<Value> {
1981    if parent.primary_key.len() == 1 {
1982        let child_col = fk
1983            .columns
1984            .first()
1985            .ok_or_else(|| KitError::Integrity("fk has no columns".into()))?;
1986        Ok(values.get(child_col).cloned().unwrap_or(Value::Null))
1987    } else {
1988        let mut obj = Map::new();
1989        for (child_col, parent_col) in fk.columns.iter().zip(&parent.primary_key) {
1990            obj.insert(
1991                parent_col.clone(),
1992                values.get(child_col).cloned().unwrap_or(Value::Null),
1993            );
1994        }
1995        Ok(Value::Object(obj))
1996    }
1997}
1998
1999fn fk_matches(
2000    child_values: &Map<String, Value>,
2001    fk: &ForeignKey,
2002    parent_pk_map: &Map<String, Value>,
2003    parent_table: &KitTable,
2004) -> bool {
2005    for (child_col, parent_col) in fk.columns.iter().zip(&parent_table.primary_key) {
2006        if child_values.get(child_col) != parent_pk_map.get(parent_col) {
2007            return false;
2008        }
2009    }
2010    true
2011}
2012
2013/// Decode an encoded primary key string back into a JSON value.
2014///
2015/// Single-column keys return the scalar value; composite keys return an object
2016/// keyed by primary-key column name.
2017fn pk_string_to_value(encoded: &str, table: &KitTable) -> Result<Value> {
2018    let components = decode_pk(encoded);
2019    if components.len() != table.primary_key.len() {
2020        return Err(KitError::Validation(format!(
2021            "encoded pk \"{encoded}\" has {} components, expected {}",
2022            components.len(),
2023            table.primary_key.len()
2024        )));
2025    }
2026    if table.primary_key.len() == 1 {
2027        return Ok(component_to_value(&components[0]));
2028    }
2029    let mut obj = Map::new();
2030    for (name, component) in table.primary_key.iter().zip(&components) {
2031        obj.insert(name.clone(), component_to_value(component));
2032    }
2033    Ok(Value::Object(obj))
2034}
2035
2036fn component_to_value(component: &KeyComponent) -> Value {
2037    match component {
2038        KeyComponent::Null => Value::Null,
2039        KeyComponent::Int(i) => Value::Number((*i).into()),
2040        KeyComponent::Text(s) => Value::String(s.clone()),
2041    }
2042}
2043
2044fn row_matches_conditions(table: &KitTable, row: &Row, conditions: &[Condition]) -> bool {
2045    conditions
2046        .iter()
2047        .all(|condition| row_matches_condition(table, row, condition))
2048}
2049
2050fn row_matches_condition(table: &KitTable, row: &Row, condition: &Condition) -> bool {
2051    match condition {
2052        Condition::Pk(key) => {
2053            let Some(pk_name) = table.primary_key.first() else {
2054                return false;
2055            };
2056            let Some(col) = table.column(pk_name) else {
2057                return false;
2058            };
2059            value_index_key(col, &row.values).as_deref() == Some(key.as_slice())
2060        }
2061        Condition::BitmapEq { column_id, value } => {
2062            let Some(col) = column_by_id(table, *column_id) else {
2063                return false;
2064            };
2065            value_index_key(col, &row.values).as_deref() == Some(value.as_slice())
2066        }
2067        Condition::BitmapIn { column_id, values } => {
2068            let Some(col) = column_by_id(table, *column_id) else {
2069                return false;
2070            };
2071            let Some(key) = value_index_key(col, &row.values) else {
2072                return false;
2073            };
2074            values.iter().any(|value| value == &key)
2075        }
2076        Condition::Range { column_id, lo, hi } => {
2077            let Some(col) = column_by_id(table, *column_id) else {
2078                return false;
2079            };
2080            matches!(
2081                json_to_core(
2082                    row.values.get(&col.name).unwrap_or(&Value::Null),
2083                    col.storage_type
2084                ),
2085                Ok(CoreValue::Int64(v)) if v >= *lo && v <= *hi
2086            )
2087        }
2088        Condition::RangeF64 {
2089            column_id,
2090            lo,
2091            lo_inclusive,
2092            hi,
2093            hi_inclusive,
2094        } => {
2095            let Some(col) = column_by_id(table, *column_id) else {
2096                return false;
2097            };
2098            let Ok(CoreValue::Float64(v)) = json_to_core(
2099                row.values.get(&col.name).unwrap_or(&Value::Null),
2100                col.storage_type,
2101            ) else {
2102                return false;
2103            };
2104            let ge_lo = if *lo_inclusive { v >= *lo } else { v > *lo };
2105            let le_hi = if *hi_inclusive { v <= *hi } else { v < *hi };
2106            ge_lo && le_hi
2107        }
2108        Condition::FmContains { column_id, pattern } => {
2109            let Some(col) = column_by_id(table, *column_id) else {
2110                return false;
2111            };
2112            if pattern.is_empty() {
2113                return true;
2114            }
2115            match json_to_core(
2116                row.values.get(&col.name).unwrap_or(&Value::Null),
2117                col.storage_type,
2118            ) {
2119                Ok(CoreValue::Bytes(bytes)) => bytes
2120                    .windows(pattern.len())
2121                    .any(|window| window == pattern.as_slice()),
2122                _ => false,
2123            }
2124        }
2125        Condition::IsNull { column_id } => {
2126            let Some(col) = column_by_id(table, *column_id) else {
2127                return false;
2128            };
2129            matches!(row.values.get(&col.name), None | Some(Value::Null))
2130        }
2131        Condition::IsNotNull { column_id } => {
2132            let Some(col) = column_by_id(table, *column_id) else {
2133                return false;
2134            };
2135            !matches!(row.values.get(&col.name), None | Some(Value::Null))
2136        }
2137        // Conditions the Kit never emits (Ann, SparseMatch, FmContainsAll)
2138        // — assume the index already resolved them.
2139        _ => true,
2140    }
2141}
2142
2143fn column_by_id(table: &KitTable, column_id: u16) -> Option<&Column> {
2144    table.columns.iter().find(|col| col.id as u16 == column_id)
2145}
2146
2147fn value_index_key(col: &Column, values: &Map<String, Value>) -> Option<Vec<u8>> {
2148    let value = values.get(&col.name).unwrap_or(&Value::Null);
2149    if value.is_null() {
2150        return None;
2151    }
2152    json_to_core(value, col.storage_type)
2153        .ok()
2154        .map(|value| value.encode_key())
2155}