Skip to main content

reddb_server/application/
ports_impls_entity.rs

1use std::collections::HashMap;
2
3use crate::application::collection_contract_enforcer::{
4    normalize_row_update_assignment_with_plan, CollectionContractWriteEnforcer,
5};
6use crate::application::entity::{
7    AppliedEntityMutation, CreateDocumentInput, CreateKvInput, CreateTimeSeriesPointInput,
8    RowUpdateColumnRule, RowUpdateContractPlan,
9};
10use crate::application::ttl_payload::{
11    normalize_ttl_patch_operations, parse_top_level_ttl_metadata_entries,
12};
13use crate::json::{to_vec as json_to_vec, Value as JsonValue};
14use crate::storage::unified::MetadataValue;
15use reddb_types::Value;
16
17use super::*;
18
19const TREE_METADATA_PREFIX: &str = "red.tree.";
20const TREE_CHILD_EDGE_LABEL: &str = "TREE_CHILD";
21
22fn refresh_context_index(
23    db: &crate::storage::unified::devx::RedDB,
24    collection: &str,
25    id: crate::storage::EntityId,
26) -> RedDBResult<()> {
27    let store = db.store();
28    let Some(entity) = store.get(collection, id) else {
29        return Ok(());
30    };
31
32    store.context_index().index_entity(collection, &entity);
33    Ok(())
34}
35
36fn ensure_insert_entity_rls(
37    runtime: &RedDBRuntime,
38    collection: &str,
39    entity: &crate::storage::UnifiedEntity,
40) -> RedDBResult<()> {
41    if crate::runtime::rls_injection::entity_passes_rls_action(
42        runtime,
43        collection,
44        reddb_rql::ast::PolicyAction::Insert,
45        entity,
46    ) {
47        Ok(())
48    } else {
49        Err(crate::RedDBError::Query(format!(
50            "insert into collection '{collection}' denied by row-level security"
51        )))
52    }
53}
54
55/// Pull `(name, value)` pairs for every indexable field on an entity.
56/// Returns empty if field names are not recoverable here, so secondary-index
57/// maintenance has nothing to act on. Used by the delete + update paths.
58pub(crate) fn entity_row_fields_snapshot(
59    entity: &crate::storage::UnifiedEntity,
60) -> Vec<(String, Value)> {
61    let mut fields = match &entity.data {
62        crate::storage::EntityData::Row(row) => {
63            if let Some(named) = &row.named {
64                named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
65            } else if let Some(schema) = &row.schema {
66                schema
67                    .iter()
68                    .zip(row.columns.iter())
69                    .map(|(name, value)| (name.clone(), value.clone()))
70                    .collect()
71            } else {
72                Vec::new()
73            }
74        }
75        crate::storage::EntityData::Node(node) => node
76            .properties
77            .iter()
78            .map(|(k, v)| (k.clone(), v.clone()))
79            .collect(),
80        crate::storage::EntityData::Edge(edge) => edge
81            .properties
82            .iter()
83            .map(|(k, v)| (k.clone(), v.clone()))
84            .collect(),
85        _ => Vec::new(),
86    };
87    expand_binary_document_body_snapshot(&mut fields);
88    fields
89}
90
91fn expand_binary_document_body_snapshot(fields: &mut Vec<(String, Value)>) {
92    let Some(body_idx) = fields.iter().position(|(name, _)| name == "body") else {
93        return;
94    };
95    let Value::Json(bytes) = &fields[body_idx].1 else {
96        return;
97    };
98    let body_fields = crate::document_body::body_fields(bytes);
99    if let Some(body_json) = crate::document_body::decode_container_to_json(bytes) {
100        if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
101            fields[body_idx].1 = Value::Json(json_bytes);
102        }
103    }
104    let Some(body_fields) = body_fields else {
105        return;
106    };
107    for (name, value) in body_fields {
108        if fields.iter().all(|(existing, _)| existing != &name) {
109            fields.push((name, value));
110        }
111    }
112}
113
114fn set_row_field(row: &mut crate::storage::unified::entity::RowData, name: &str, value: Value) {
115    if let Some(named) = row.named.as_mut() {
116        named.insert(name.to_string(), value);
117        return;
118    }
119
120    if let Some(schema) = row.schema.as_ref() {
121        if let Some(index) = schema.iter().position(|column| column == name) {
122            if let Some(slot) = row.columns.get_mut(index) {
123                *slot = value;
124                return;
125            }
126        }
127
128        let mut named = HashMap::with_capacity(schema.len().saturating_add(1));
129        for (column, current) in schema.iter().zip(row.columns.iter()) {
130            named.insert(column.clone(), current.clone());
131        }
132        named.insert(name.to_string(), value);
133        row.named = Some(named);
134        return;
135    }
136
137    let mut named = HashMap::with_capacity(1);
138    named.insert(name.to_string(), value);
139    row.named = Some(named);
140}
141
142fn collect_row_fields(row: &crate::storage::unified::entity::RowData) -> Vec<(String, Value)> {
143    if let Some(named) = row.named.as_ref() {
144        named
145            .iter()
146            .map(|(key, value)| (key.clone(), value.clone()))
147            .collect()
148    } else if let Some(schema) = row.schema.as_ref() {
149        schema
150            .iter()
151            .cloned()
152            .zip(row.columns.iter().cloned())
153            .collect()
154    } else {
155        Vec::new()
156    }
157}
158
159fn apply_row_field_assignments_raw<I>(
160    row: &mut crate::storage::unified::entity::RowData,
161    field_assignments: I,
162) where
163    I: IntoIterator<Item = (String, Value)>,
164{
165    for (column, value) in field_assignments {
166        set_row_field(row, &column, value);
167    }
168}
169
170fn apply_row_field_assignments_incremental<I>(
171    collection: &str,
172    row: &mut crate::storage::unified::entity::RowData,
173    field_assignments: I,
174    row_contract_plan: Option<&RowUpdateContractPlan>,
175) -> RedDBResult<()>
176where
177    I: IntoIterator<Item = (String, Value)>,
178{
179    for (column, value) in field_assignments {
180        let value = normalize_row_update_assignment_with_plan(
181            collection,
182            &column,
183            value,
184            row_contract_plan,
185        )?;
186
187        set_row_field(row, &column, value);
188    }
189
190    Ok(())
191}
192
193fn dedupe_modified_columns(mut modified_columns: Vec<String>) -> Vec<String> {
194    if modified_columns.is_empty() {
195        return modified_columns;
196    }
197
198    let mut unique = Vec::with_capacity(modified_columns.len());
199    for column in modified_columns.drain(..) {
200        if !unique
201            .iter()
202            .any(|existing: &String| existing.eq_ignore_ascii_case(&column))
203        {
204            unique.push(column);
205        }
206    }
207    unique
208}
209
210fn reject_document_array_position_path(path: &[String]) -> RedDBResult<()> {
211    if path.iter().any(|segment| segment.parse::<usize>().is_ok()) {
212        return Err(crate::RedDBError::Query(
213            "array positional document patch paths are unsupported; replace the array or full document body instead"
214                .to_string(),
215        ));
216    }
217    Ok(())
218}
219
220/// Reject a document body `set` whose path would traverse *through* an existing
221/// non-object value (e.g. `SET a.b = …` where `a` is already a scalar). This
222/// mirrors the HTTP JSON-patch contract: intermediate objects are created only
223/// where a slot is absent, never by silently clobbering a scalar. `body` is the
224/// pre-merge document body.
225fn reject_document_patch_path_through_scalar(body: &JsonValue, path: &[String]) -> RedDBResult<()> {
226    if path.len() < 2 {
227        return Ok(());
228    }
229    let scalar_error = |prefix: &str| {
230        crate::RedDBError::Query(format!(
231            "cannot set nested path '{}': '{}' is not an object; setting a path through a non-object value is unsupported — replace that value or the full document body instead",
232            path.join("."),
233            prefix
234        ))
235    };
236    let mut current = body;
237    // Walk the parent path (every segment but the leaf). Each container we index
238    // into must be an object; an absent slot is fine (the merge creates it).
239    for (idx, segment) in path[..path.len() - 1].iter().enumerate() {
240        let JsonValue::Object(map) = current else {
241            return Err(scalar_error(&path[..idx].join(".")));
242        };
243        match map.get(segment) {
244            Some(next) => current = next,
245            None => return Ok(()),
246        }
247    }
248    // `current` is now the leaf's parent container; it must be an object.
249    if !matches!(current, JsonValue::Object(_)) {
250        return Err(scalar_error(&path[..path.len() - 1].join(".")));
251    }
252    Ok(())
253}
254
255/// Apply document body `set`/`unset` operations, enforcing the shared document
256/// patch contract first: array-positional paths and paths that traverse through
257/// a scalar are rejected with clear errors before any mutation touches `body`.
258/// Used by both the SQL nested-`SET` executor and the HTTP JSON-patch operation
259/// path so the two surfaces stay consistent (ADR 0067).
260fn apply_document_body_patch_operations(
261    body: &mut JsonValue,
262    operations: &[PatchEntityOperation],
263) -> RedDBResult<()> {
264    for op in operations {
265        reject_document_array_position_path(&op.path)?;
266        if matches!(op.op, PatchEntityOperationType::Set) {
267            reject_document_patch_path_through_scalar(body, &op.path)?;
268        }
269    }
270    apply_patch_operations_to_json(body, operations).map_err(crate::RedDBError::Query)
271}
272
273fn document_body_from_row(
274    row: &crate::storage::unified::entity::RowData,
275) -> RedDBResult<JsonValue> {
276    match row.get_field("body") {
277        Some(Value::Json(bytes)) => {
278            // The stored body may be the native binary container (PRD-1398) or
279            // plain JSON; decode either form back to JSON.
280            if let Some(body) = crate::document_body::decode_container_to_json(bytes) {
281                Ok(body)
282            } else {
283                crate::json::from_slice(bytes).map_err(|err| {
284                    crate::RedDBError::Query(format!("failed to decode document body: {err}"))
285                })
286            }
287        }
288        Some(_) => Err(crate::RedDBError::Query(
289            "document body field must contain JSON".to_string(),
290        )),
291        None => Ok(JsonValue::Object(Default::default())),
292    }
293}
294
295fn document_body_from_assignment(value: &Value) -> RedDBResult<JsonValue> {
296    match value {
297        Value::Json(bytes) | Value::Blob(bytes) => crate::json::from_slice(bytes)
298            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
299        Value::Text(text) => crate::json::from_str(text.as_ref())
300            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
301        Value::Integer(value) => crate::json::from_str(&value.to_string())
302            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
303        Value::UnsignedInteger(value) => crate::json::from_str(&value.to_string())
304            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
305        Value::Float(value) => crate::json::from_str(&value.to_string())
306            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
307        other => Err(crate::RedDBError::Query(format!(
308            "column 'body' expected JSON body, got {other:?}"
309        ))),
310    }
311}
312
313fn document_body_set_operation(column: &str, value: Value) -> PatchEntityOperation {
314    PatchEntityOperation {
315        op: PatchEntityOperationType::Set,
316        path: column.split('.').map(str::to_string).collect(),
317        value: Some(value.to_json()),
318    }
319}
320
321fn replace_document_fields_body(
322    fields: &mut HashMap<String, Value>,
323    body: JsonValue,
324    binary: bool,
325    modified_columns: &mut Vec<String>,
326) -> RedDBResult<()> {
327    modified_columns.push("body".to_string());
328
329    let old_keys: Vec<String> = fields
330        .keys()
331        .filter(|key| key.as_str() != "body")
332        .cloned()
333        .collect();
334    for key in old_keys {
335        fields.remove(&key);
336        modified_columns.push(key);
337    }
338
339    let body_bytes = crate::document_body::serialize_document_body(&body, binary)?;
340    fields.insert("body".to_string(), Value::Json(body_bytes));
341
342    Ok(())
343}
344
345fn ensure_no_reserved_document_body_fields(body: &JsonValue, collection: &str) -> RedDBResult<()> {
346    if let JsonValue::Object(map) = body {
347        crate::reserved_fields::ensure_no_reserved_public_item_fields(
348            map.keys().map(String::as_str),
349            &format!("document '{collection}'"),
350        )?;
351    }
352    Ok(())
353}
354
355fn replace_document_row_body(
356    row: &mut crate::storage::unified::entity::RowData,
357    body: JsonValue,
358    collection: &str,
359    binary: bool,
360    modified_columns: &mut Vec<String>,
361) -> RedDBResult<()> {
362    ensure_no_reserved_document_body_fields(&body, collection)?;
363    let fields = row.named.get_or_insert_with(Default::default);
364    replace_document_fields_body(fields, body, binary, modified_columns)?;
365
366    // Full body replacement rebuilds the document from the canonical `body`.
367    // Stale positional/schema values would otherwise leak back through JSON
368    // presentation even after the named map was pruned.
369    row.columns.clear();
370    row.schema = None;
371    Ok(())
372}
373
374fn document_merge_patch_payload(payload: &JsonValue) -> &JsonValue {
375    if let JsonValue::Object(object) = payload {
376        if object.len() == 1 {
377            if let Some(body) = object.get("body") {
378                return body;
379            }
380        }
381    }
382    payload
383}
384
385fn apply_document_json_merge_patch(body: &mut JsonValue, patch: &JsonValue) -> RedDBResult<()> {
386    if !matches!(patch, JsonValue::Object(_)) {
387        *body = patch.clone();
388        return Ok(());
389    }
390
391    if matches!(patch, JsonValue::Object(object) if object.is_empty()) {
392        if !matches!(body, JsonValue::Object(_)) {
393            *body = JsonValue::Object(Default::default());
394        }
395        return Ok(());
396    }
397
398    let mut operations = Vec::new();
399    collect_document_json_merge_patch_operations(body, Vec::new(), patch, &mut operations);
400    apply_patch_operations_to_json(body, &operations).map_err(crate::RedDBError::Query)
401}
402
403fn collect_document_json_merge_patch_operations(
404    target: &JsonValue,
405    path: Vec<String>,
406    patch: &JsonValue,
407    operations: &mut Vec<PatchEntityOperation>,
408) {
409    let JsonValue::Object(patch_object) = patch else {
410        operations.push(PatchEntityOperation {
411            op: PatchEntityOperationType::Set,
412            path,
413            value: Some(patch.clone()),
414        });
415        return;
416    };
417
418    for (key, value) in patch_object {
419        let mut child_path = path.clone();
420        child_path.push(key.clone());
421        match value {
422            JsonValue::Null => operations.push(PatchEntityOperation {
423                op: PatchEntityOperationType::Unset,
424                path: child_path,
425                value: None,
426            }),
427            JsonValue::Object(object) if object.is_empty() => {
428                let child_target = match target {
429                    JsonValue::Object(target_object) => target_object.get(key),
430                    _ => None,
431                };
432                if !matches!(child_target, Some(JsonValue::Object(_))) {
433                    operations.push(PatchEntityOperation {
434                        op: PatchEntityOperationType::Set,
435                        path: child_path,
436                        value: Some(JsonValue::Object(Default::default())),
437                    });
438                }
439            }
440            JsonValue::Object(_) => {
441                let child_target = match target {
442                    JsonValue::Object(target_object) => target_object.get(key),
443                    _ => None,
444                }
445                .unwrap_or(&JsonValue::Null);
446                collect_document_json_merge_patch_operations(
447                    child_target,
448                    child_path,
449                    value,
450                    operations,
451                );
452            }
453            _ => operations.push(PatchEntityOperation {
454                op: PatchEntityOperationType::Set,
455                path: child_path,
456                value: Some(value.clone()),
457            }),
458        }
459    }
460}
461
462impl RedDBRuntime {
463    pub(crate) fn apply_loaded_patch_entity_core(
464        &self,
465        collection: String,
466        mut entity: crate::storage::UnifiedEntity,
467        payload: JsonValue,
468        operations: Vec<PatchEntityOperation>,
469    ) -> RedDBResult<AppliedEntityMutation> {
470        let id = entity.id;
471        let previous_xmax = entity.xmax;
472        let operations = normalize_ttl_patch_operations(operations)?;
473        // Snapshot pre-patch row fields for the secondary-index hook —
474        // empty for non-row entities, which is the desired no-op.
475        let pre_mutation_fields = entity_row_fields_snapshot(&entity);
476
477        // Versioned collections retain MVCC history: a PATCH/UPDATE must
478        // produce a NEW full version (the merged document/row/node) with
479        // the prior version tombstoned, mirroring the SQL UPDATE row
480        // path. Gated STRICTLY on the collection `versioned` flag —
481        // non-versioned collections keep last-writer-wins in-place
482        // mutation. Row entities (documents / table rows / KV) carry
483        // logical-id MVCC history (Phase 1/2); graph nodes & edges join
484        // in Phase 3. The post-image re-stamp (new physical id, same
485        // logical_id, fresh xmin) at the tail of this function is fully
486        // kind-agnostic, so the same machinery installs a versioned
487        // graph-node update.
488        let patch_versions = matches!(
489            entity.data,
490            crate::storage::EntityData::Row(_)
491                | crate::storage::EntityData::Node(_)
492                | crate::storage::EntityData::Edge(_)
493        ) && self.vcs_is_versioned(&collection).unwrap_or(false);
494        let versioned_update_xid = if patch_versions {
495            match self.current_xid() {
496                Some(xid) => Some(xid),
497                None => {
498                    let snapshot_manager = self.snapshot_manager();
499                    let xid = snapshot_manager.begin();
500                    snapshot_manager.commit(xid);
501                    Some(xid)
502                }
503            }
504        } else {
505            None
506        };
507        let mut replaced_entity = versioned_update_xid.map(|xid| {
508            let mut old = entity.clone();
509            old.set_xmax(xid);
510            old
511        });
512
513        let db = self.db();
514        let store = db.store();
515        let Some(manager) = store.get_collection(&collection) else {
516            return Err(crate::RedDBError::NotFound(format!(
517                "collection not found: {collection}"
518            )));
519        };
520
521        let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
522        let mut metadata_changed = false;
523        let mut modified_columns: Vec<String> = Vec::new();
524        let mut context_index_dirty = false;
525        let mut graph_node_type: Option<String> = None;
526        let mut graph_edge_weight: Option<f32> = None;
527
528        let row_contract_timestamps = db
529            .collection_contract(&collection)
530            .map(|c| c.timestamps_enabled)
531            .unwrap_or(false);
532
533        match &mut entity.data {
534            crate::storage::EntityData::Row(row) => {
535                // PATCH mutates a named map. A recovered row may instead own
536                // its values under a shared schema; move those values into
537                // the map before applying only the requested field changes.
538                if row.named.is_none() {
539                    if let Some(schema) = row.schema.as_ref() {
540                        let fields = if schema.len() == row.columns.len() {
541                            schema
542                                .iter()
543                                .cloned()
544                                .zip(std::mem::take(&mut row.columns))
545                                .collect()
546                        } else {
547                            collect_row_fields(row).into_iter().collect()
548                        };
549                        row.named = Some(fields);
550                    }
551                }
552                let is_document_collection = db
553                    .collection_contract(&collection)
554                    .map(|contract| {
555                        contract.declared_model == crate::catalog::CollectionModel::Document
556                    })
557                    .unwrap_or(false);
558                let binary_body = self.binary_document_body_enabled();
559                let mut field_ops = Vec::new();
560                let mut metadata_ops = Vec::new();
561                let mut document_body_ops = Vec::new();
562                let mut document_body_replace: Option<JsonValue> = None;
563                let has_patch_operations = !operations.is_empty();
564
565                for mut op in operations {
566                    let Some(root) = op.path.first().map(String::as_str) else {
567                        return Err(crate::RedDBError::Query(
568                            "patch path cannot be empty".to_string(),
569                        ));
570                    };
571
572                    match root {
573                        "body" if is_document_collection => {
574                            if op.path.len() == 1 {
575                                match op.op {
576                                    PatchEntityOperationType::Set
577                                    | PatchEntityOperationType::Replace => {
578                                        let Some(value) = op.value.take() else {
579                                            return Err(crate::RedDBError::Query(
580                                                "document body replacement requires a value"
581                                                    .to_string(),
582                                            ));
583                                        };
584                                        document_body_replace = Some(value);
585                                    }
586                                    PatchEntityOperationType::Unset => {
587                                        return Err(crate::RedDBError::Query(
588                                            "document body cannot be unset; replace it instead"
589                                                .to_string(),
590                                        ));
591                                    }
592                                }
593                                continue;
594                            }
595                            if op.path.len() < 2 {
596                                return Err(crate::RedDBError::Query(
597                                    "document body patch paths require a nested key; use payload.body for full replacement"
598                                        .to_string(),
599                                ));
600                            }
601                            op.path.remove(0);
602                            reject_document_array_position_path(&op.path)?;
603                            document_body_ops.push(op);
604                        }
605                        "fields" | "named" => {
606                            if op.path.len() < 2 {
607                                return Err(crate::RedDBError::Query(
608                                    "patch path 'fields' requires a nested key".to_string(),
609                                ));
610                            }
611                            if row_contract_timestamps {
612                                let leaf = op.path.get(1).map(String::as_str);
613                                if matches!(leaf, Some("created_at") | Some("updated_at")) {
614                                    return Err(crate::RedDBError::Query(format!(
615                                        "collection '{}' manages '{}' automatically — do not set it in UPDATE",
616                                        collection,
617                                        leaf.unwrap_or("")
618                                    )));
619                                }
620                            }
621                            op.path.remove(0);
622                            field_ops.push(op);
623                        }
624                        "metadata" => {
625                            if op.path.len() < 2 {
626                                return Err(crate::RedDBError::Query(
627                                    "patch path 'metadata' requires a nested key".to_string(),
628                                ));
629                            }
630                            op.path.remove(0);
631                            metadata_ops.push(op);
632                        }
633                        _ => {
634                            return Err(crate::RedDBError::Query(format!(
635                                "unsupported patch target '{root}' for table rows. Use fields/*, metadata/*, or weight"
636                            )));
637                        }
638                    }
639                }
640
641                if document_body_replace.is_some() || !document_body_ops.is_empty() {
642                    context_index_dirty = true;
643                    let mut body = match document_body_replace {
644                        Some(body) => body,
645                        None => document_body_from_row(row)?,
646                    };
647                    apply_document_body_patch_operations(&mut body, &document_body_ops)?;
648                    replace_document_row_body(
649                        row,
650                        body,
651                        &collection,
652                        binary_body,
653                        &mut modified_columns,
654                    )?;
655                }
656
657                if is_document_collection && !has_patch_operations {
658                    let mut body = document_body_from_row(row)?;
659                    let previous_body = body.clone();
660                    apply_document_json_merge_patch(
661                        &mut body,
662                        document_merge_patch_payload(&payload),
663                    )?;
664                    if body != previous_body {
665                        context_index_dirty = true;
666                        replace_document_row_body(
667                            row,
668                            body,
669                            &collection,
670                            binary_body,
671                            &mut modified_columns,
672                        )?;
673                    }
674                }
675
676                if !field_ops.is_empty() {
677                    context_index_dirty = true;
678                    if is_document_collection {
679                        // Document fields are projections of the canonical
680                        // `body` JSON. Apply the assignment to the body so the
681                        // stored document remains single-source.
682                        let mut body = document_body_from_row(row)?;
683                        apply_patch_operations_to_json(&mut body, &field_ops)
684                            .map_err(crate::RedDBError::Query)?;
685                        replace_document_row_body(
686                            row,
687                            body,
688                            &collection,
689                            binary_body,
690                            &mut modified_columns,
691                        )?;
692                    } else {
693                        let named = row.named.get_or_insert_with(Default::default);
694                        for op in &field_ops {
695                            if let Some(col) = op.path.first() {
696                                modified_columns.push(col.clone());
697                            }
698                        }
699                        apply_patch_operations_to_storage_map(named, &field_ops)?;
700                    }
701                }
702
703                if let Some(fields) = payload
704                    .get("fields")
705                    .and_then(crate::json::Value::as_object)
706                {
707                    if row_contract_timestamps {
708                        for key in fields.keys() {
709                            if key == "created_at" || key == "updated_at" {
710                                return Err(crate::RedDBError::Query(format!(
711                                    "collection '{}' manages '{}' automatically — do not set it in UPDATE",
712                                    collection, key
713                                )));
714                            }
715                        }
716                    }
717                    context_index_dirty = true;
718                    if is_document_collection {
719                        // Mirror the `field_ops` path above: keep the canonical
720                        // `body` JSON as the single stored representation.
721                        let mut body = document_body_from_row(row)?;
722                        if let JsonValue::Object(map) = &mut body {
723                            for (key, value) in fields {
724                                map.insert(key.clone(), value.clone());
725                            }
726                        }
727                        replace_document_row_body(
728                            row,
729                            body,
730                            &collection,
731                            binary_body,
732                            &mut modified_columns,
733                        )?;
734                    } else {
735                        let named = row.named.get_or_insert_with(Default::default);
736                        for (key, value) in fields {
737                            modified_columns.push(key.clone());
738                            named.insert(key.clone(), json_to_storage_value(value)?);
739                        }
740                    }
741                }
742
743                if !metadata_ops.is_empty() {
744                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
745                    let metadata = patch_metadata.get_or_insert_with(|| {
746                        store.get_metadata(&collection, id).unwrap_or_default()
747                    });
748                    let mut metadata_json = metadata_to_json(metadata);
749                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
750                        .map_err(crate::RedDBError::Query)?;
751                    *metadata = metadata_from_json(&metadata_json)?;
752                    metadata_changed = true;
753                }
754
755                if !modified_columns.is_empty() || row_contract_timestamps {
756                    let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
757                    let current_fields = if let Some(named) = row.named.take() {
758                        named.into_iter().collect::<Vec<_>>()
759                    } else if let Some(schema) = row.schema.as_ref() {
760                        schema
761                            .iter()
762                            .cloned()
763                            .zip(row.columns.iter().cloned())
764                            .collect::<Vec<_>>()
765                    } else {
766                        Vec::new()
767                    };
768                    let normalized_fields = contract.normalize_update_fields(current_fields)?;
769                    if let Some(schema) = db.collection_contract_arc(&collection) {
770                        for column in schema
771                            .declared_columns
772                            .iter()
773                            .filter(|column| column.generated.is_some())
774                        {
775                            modified_columns.push(column.name.clone());
776                            context_index_dirty = true;
777                        }
778                    }
779                    if row_contract_timestamps {
780                        modified_columns.push("updated_at".to_string());
781                        context_index_dirty = true;
782                    }
783                    if contract.requires_uniqueness_check(&modified_columns) {
784                        contract.enforce_row_uniqueness(&normalized_fields, Some(id))?;
785                    }
786                    if self
787                        .index_store_ref()
788                        .touches_unique_index(&collection, &modified_columns)
789                    {
790                        self.enforce_unique_updates(
791                            &collection,
792                            &[(id, normalized_fields.clone())],
793                        )?;
794                    }
795                    row.named = Some(normalized_fields.into_iter().collect());
796                }
797            }
798            crate::storage::EntityData::Node(node) => {
799                let mut field_ops = Vec::new();
800                let mut metadata_ops = Vec::new();
801                let mut node_type_ops = Vec::new();
802
803                for mut op in operations {
804                    let Some(root) = op.path.first().map(String::as_str) else {
805                        return Err(crate::RedDBError::Query(
806                            "patch path cannot be empty".to_string(),
807                        ));
808                    };
809
810                    match root {
811                        "fields" | "properties" => {
812                            if op.path.len() < 2 {
813                                return Err(crate::RedDBError::Query(
814                                    "patch path 'fields' requires a nested key".to_string(),
815                                ));
816                            }
817                            op.path.remove(0);
818                            field_ops.push(op);
819                        }
820                        "metadata" => {
821                            if op.path.len() < 2 {
822                                return Err(crate::RedDBError::Query(
823                                    "patch path 'metadata' requires a nested key".to_string(),
824                                ));
825                            }
826                            op.path.remove(0);
827                            metadata_ops.push(op);
828                        }
829                        "node_type" => {
830                            if op.path.len() != 1 {
831                                return Err(crate::RedDBError::Query(
832                                    "patch path 'node_type' does not allow nested keys".to_string(),
833                                ));
834                            }
835                            op.path.clear();
836                            node_type_ops.push(op);
837                        }
838                        _ => {
839                            return Err(crate::RedDBError::Query(format!(
840                                "unsupported patch target '{root}' for graph nodes. Use fields/*, properties/*, node_type, or metadata/*"
841                            )));
842                        }
843                    }
844                }
845
846                for op in node_type_ops {
847                    context_index_dirty = true;
848                    let value = op.value.ok_or_else(|| {
849                        crate::RedDBError::Query("node_type operations require a value".to_string())
850                    })?;
851
852                    match op.op {
853                        PatchEntityOperationType::Unset => {
854                            return Err(crate::RedDBError::Query(
855                                "node_type cannot be unset through patch operations".to_string(),
856                            ));
857                        }
858                        PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
859                            let Some(node_type) = value.as_str() else {
860                                return Err(crate::RedDBError::Query(
861                                    "node_type operation requires a text value".to_string(),
862                                ));
863                            };
864                            graph_node_type = Some(node_type.to_string());
865                            modified_columns.push("node_type".to_string());
866                        }
867                    }
868                }
869
870                if !field_ops.is_empty() {
871                    context_index_dirty = true;
872                    apply_patch_operations_to_storage_map(&mut node.properties, &field_ops)?;
873                }
874
875                if let Some(fields) = payload
876                    .get("fields")
877                    .and_then(crate::json::Value::as_object)
878                {
879                    context_index_dirty = true;
880                    for (key, value) in fields {
881                        node.properties
882                            .insert(key.clone(), json_to_storage_value(value)?);
883                    }
884                }
885
886                if !metadata_ops.is_empty() {
887                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
888                    let metadata = patch_metadata.get_or_insert_with(|| {
889                        store.get_metadata(&collection, id).unwrap_or_default()
890                    });
891                    let mut metadata_json = metadata_to_json(metadata);
892                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
893                        .map_err(crate::RedDBError::Query)?;
894                    *metadata = metadata_from_json(&metadata_json)?;
895                    metadata_changed = true;
896                }
897            }
898            crate::storage::EntityData::Edge(edge) => {
899                let mut field_ops = Vec::new();
900                let mut metadata_ops = Vec::new();
901                let mut weight_ops = Vec::new();
902
903                for mut op in operations {
904                    let Some(root) = op.path.first().map(String::as_str) else {
905                        return Err(crate::RedDBError::Query(
906                            "patch path cannot be empty".to_string(),
907                        ));
908                    };
909
910                    match root {
911                        "fields" | "properties" => {
912                            if op.path.len() < 2 {
913                                return Err(crate::RedDBError::Query(
914                                    "patch path 'fields' requires a nested key".to_string(),
915                                ));
916                            }
917                            op.path.remove(0);
918                            field_ops.push(op);
919                        }
920                        "weight" => {
921                            if op.path.len() != 1 {
922                                return Err(crate::RedDBError::Query(
923                                    "patch path 'weight' does not allow nested keys".to_string(),
924                                ));
925                            }
926                            op.path.clear();
927                            weight_ops.push(op);
928                        }
929                        "metadata" => {
930                            if op.path.len() < 2 {
931                                return Err(crate::RedDBError::Query(
932                                    "patch path 'metadata' requires a nested key".to_string(),
933                                ));
934                            }
935                            op.path.remove(0);
936                            metadata_ops.push(op);
937                        }
938                        _ => {
939                            return Err(crate::RedDBError::Query(format!(
940                                "unsupported patch target '{root}' for graph edges. Use fields/*, weight, metadata/*"
941                            )));
942                        }
943                    }
944                }
945
946                if !field_ops.is_empty() {
947                    context_index_dirty = true;
948                    apply_patch_operations_to_storage_map(&mut edge.properties, &field_ops)?;
949                }
950
951                for op in weight_ops {
952                    context_index_dirty = true;
953                    let value = op.value.ok_or_else(|| {
954                        crate::RedDBError::Query("weight operations require a value".to_string())
955                    })?;
956
957                    match op.op {
958                        PatchEntityOperationType::Unset => {
959                            return Err(crate::RedDBError::Query(
960                                "weight cannot be unset through patch operations".to_string(),
961                            ));
962                        }
963                        PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
964                            let Some(weight) = value.as_f64() else {
965                                return Err(crate::RedDBError::Query(
966                                    "weight operation requires a numeric value".to_string(),
967                                ));
968                            };
969                            graph_edge_weight = Some(weight as f32);
970                            edge.weight = weight as f32;
971                            modified_columns.push("weight".to_string());
972                        }
973                    }
974                }
975
976                if let Some(fields) = payload
977                    .get("fields")
978                    .and_then(crate::json::Value::as_object)
979                {
980                    context_index_dirty = true;
981                    for (key, value) in fields {
982                        edge.properties
983                            .insert(key.clone(), json_to_storage_value(value)?);
984                    }
985                }
986
987                if !metadata_ops.is_empty() {
988                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
989                    let metadata = patch_metadata.get_or_insert_with(|| {
990                        store.get_metadata(&collection, id).unwrap_or_default()
991                    });
992                    let mut metadata_json = metadata_to_json(metadata);
993                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
994                        .map_err(crate::RedDBError::Query)?;
995                    *metadata = metadata_from_json(&metadata_json)?;
996                    metadata_changed = true;
997                }
998            }
999            crate::storage::EntityData::Vector(vector) => {
1000                let mut field_ops = Vec::new();
1001                let mut metadata_ops = Vec::new();
1002
1003                for mut op in operations {
1004                    let Some(root) = op.path.first().map(String::as_str) else {
1005                        return Err(crate::RedDBError::Query(
1006                            "patch path cannot be empty".to_string(),
1007                        ));
1008                    };
1009
1010                    match root {
1011                        "fields" => {
1012                            if op.path.len() < 2 {
1013                                return Err(crate::RedDBError::Query(
1014                                    "patch path 'fields' requires a nested key".to_string(),
1015                                ));
1016                            }
1017                            op.path.remove(0);
1018                            let Some(target) = op.path.first().map(String::as_str) else {
1019                                return Err(crate::RedDBError::Query(
1020                                    "patch path requires a target under fields".to_string(),
1021                                ));
1022                            };
1023                            if !matches!(target, "dense" | "content" | "sparse") {
1024                                return Err(crate::RedDBError::Query(format!(
1025                                    "unsupported vector patch target '{target}'"
1026                                )));
1027                            }
1028                            field_ops.push(op);
1029                        }
1030                        "metadata" => {
1031                            if op.path.len() < 2 {
1032                                return Err(crate::RedDBError::Query(
1033                                    "patch path 'metadata' requires a nested key".to_string(),
1034                                ));
1035                            }
1036                            op.path.remove(0);
1037                            metadata_ops.push(op);
1038                        }
1039                        _ => {
1040                            return Err(crate::RedDBError::Query(format!(
1041                                "unsupported patch target '{root}' for vectors. Use fields/* or metadata/*"
1042                            )));
1043                        }
1044                    }
1045                }
1046
1047                if !field_ops.is_empty() {
1048                    context_index_dirty = true;
1049                    apply_patch_operations_to_vector_fields(vector, &field_ops)?;
1050                }
1051
1052                if let Some(fields) = payload
1053                    .get("fields")
1054                    .and_then(crate::json::Value::as_object)
1055                {
1056                    context_index_dirty = true;
1057                    if let Some(content) =
1058                        fields.get("content").and_then(crate::json::Value::as_str)
1059                    {
1060                        vector.content = Some(content.to_string());
1061                    }
1062                    if let Some(dense) = fields.get("dense") {
1063                        vector.dense = dense
1064                            .as_array()
1065                            .ok_or_else(|| {
1066                                crate::RedDBError::Query(
1067                                    "field 'dense' must be an array".to_string(),
1068                                )
1069                            })?
1070                            .iter()
1071                            .map(|value| {
1072                                value.as_f64().map(|value| value as f32).ok_or_else(|| {
1073                                    crate::RedDBError::Query(
1074                                        "field 'dense' must contain only numbers".to_string(),
1075                                    )
1076                                })
1077                            })
1078                            .collect::<Result<Vec<_>, _>>()?;
1079                    }
1080                }
1081
1082                if !metadata_ops.is_empty() {
1083                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1084                    let metadata = patch_metadata.get_or_insert_with(|| {
1085                        store.get_metadata(&collection, id).unwrap_or_default()
1086                    });
1087                    let mut metadata_json = metadata_to_json(metadata);
1088                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1089                        .map_err(crate::RedDBError::Query)?;
1090                    *metadata = metadata_from_json(&metadata_json)?;
1091                    metadata_changed = true;
1092                }
1093            }
1094            crate::storage::EntityData::TimeSeries(_)
1095            | crate::storage::EntityData::QueueMessage(_) => {
1096                return Err(crate::RedDBError::Query(
1097                    "patch operations are not supported for TimeSeries or QueueMessage entities"
1098                        .to_string(),
1099                ));
1100            }
1101        }
1102
1103        if let Some(node_type) = graph_node_type {
1104            if let crate::storage::EntityKind::GraphNode(node) = &mut entity.kind {
1105                node.node_type = node_type;
1106            }
1107        }
1108        if let Some(weight) = graph_edge_weight {
1109            if let crate::storage::EntityKind::GraphEdge(edge) = &mut entity.kind {
1110                edge.weight = (weight * 1000.0) as u32;
1111            }
1112        }
1113
1114        if let Some(metadata) = payload
1115            .get("metadata")
1116            .and_then(crate::json::Value::as_object)
1117        {
1118            let patch_metadata = patch_metadata
1119                .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1120            for (key, value) in metadata {
1121                ensure_non_tree_reserved_metadata_key(key)?;
1122                patch_metadata.set(key.clone(), json_to_metadata_value(value)?);
1123            }
1124            metadata_changed = true;
1125        }
1126
1127        for (key, value) in parse_top_level_ttl_metadata_entries(&payload)? {
1128            let patch_metadata = patch_metadata
1129                .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1130            if matches!(value, crate::storage::unified::MetadataValue::Null) {
1131                patch_metadata.remove(&key);
1132            } else {
1133                patch_metadata.set(key, value);
1134            }
1135            metadata_changed = true;
1136        }
1137
1138        entity.updated_at = std::time::SystemTime::now()
1139            .duration_since(std::time::UNIX_EPOCH)
1140            .unwrap_or_default()
1141            .as_secs();
1142
1143        // Versioned PATCH: re-stamp the merged post-image as a fresh
1144        // physical version keyed on the same logical_id, and close the
1145        // prior version's xmax at the same xid. `install_versioned_*`
1146        // in persist installs the new row and tombstones the old one;
1147        // `record_pending_versioned_update` arms first-committer-wins.
1148        if let Some(xid) = versioned_update_xid {
1149            let logical_id = entity.logical_id();
1150            entity.id = store.next_entity_id();
1151            entity.set_logical_id(logical_id);
1152            entity.set_xmin(xid);
1153            entity.set_xmax(0);
1154            if let Some(old) = replaced_entity.as_mut() {
1155                old.set_xmax(xid);
1156            }
1157        }
1158
1159        modified_columns = dedupe_modified_columns(modified_columns);
1160
1161        Ok(AppliedEntityMutation {
1162            id: entity.id,
1163            collection,
1164            entity,
1165            metadata: patch_metadata,
1166            modified_columns,
1167            persist_metadata: metadata_changed,
1168            context_index_dirty,
1169            replaced_entity,
1170            replaced_entity_previous_xmax: previous_xmax,
1171            pre_mutation_fields,
1172        })
1173    }
1174
1175    pub(crate) fn apply_loaded_sql_update_row_core(
1176        &self,
1177        collection: String,
1178        mut entity: crate::storage::UnifiedEntity,
1179        static_field_assignments: &[(String, Value)],
1180        dynamic_field_assignments: Vec<(String, Value)>,
1181        static_metadata_assignments: &[(String, MetadataValue)],
1182        dynamic_metadata_assignments: Vec<(String, MetadataValue)>,
1183        row_contract_plan: Option<&RowUpdateContractPlan>,
1184        row_modified_columns_template: &[String],
1185        row_touches_unique_columns: bool,
1186    ) -> RedDBResult<AppliedEntityMutation> {
1187        let id = entity.id;
1188        let previous_xmax = entity.xmax;
1189        let db = self.db();
1190        let store = db.store();
1191        let Some(_) = store.get_collection(&collection) else {
1192            return Err(crate::RedDBError::NotFound(format!(
1193                "collection not found: {collection}"
1194            )));
1195        };
1196
1197        let versioned_update_xid = match self.current_xid() {
1198            Some(xid) => Some(xid),
1199            None => {
1200                let snapshot_manager = self.snapshot_manager();
1201                let xid = snapshot_manager.begin();
1202                snapshot_manager.commit(xid);
1203                Some(xid)
1204            }
1205        };
1206        let mut replaced_entity = versioned_update_xid.map(|xid| {
1207            let mut old = entity.clone();
1208            old.set_xmax(xid);
1209            old
1210        });
1211
1212        let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
1213        let row_contract_timestamps = row_contract_plan
1214            .map(|plan| plan.timestamps_enabled)
1215            .unwrap_or(false);
1216        let mut metadata_changed = false;
1217        let mut modified_columns = row_modified_columns_template.to_vec();
1218        let mut context_index_dirty = !modified_columns.is_empty();
1219
1220        // Snapshot OLD field values BEFORE applying the assignments —
1221        // the secondary-index maintenance hook needs both before/after to
1222        // delete-then-insert under changed indexed columns.
1223        let pre_mutation_fields = entity_row_fields_snapshot(&entity);
1224
1225        let crate::storage::EntityData::Row(row) = &mut entity.data else {
1226            return Err(crate::RedDBError::Query(
1227                "SQL row update fast path requires a row entity".to_string(),
1228            ));
1229        };
1230
1231        let _ = row_contract_plan;
1232
1233        // Documents store the full JSON under the `body` column. Legacy rows
1234        // also promote top-level keys for filtering, while binary body mode
1235        // keeps `body` as the single source of truth. Reflect non-body SET
1236        // assignments into `body` as patch operations so dotted paths update
1237        // nested JSON instead of becoming literal top-level keys.
1238        let is_document_collection = db
1239            .collection_contract(&collection)
1240            .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Document)
1241            .unwrap_or(false);
1242        let kv_key = if row.get_field("value").is_some() || row.columns.len() >= 2 {
1243            row.get_field("key")
1244                .cloned()
1245                .or_else(|| row.columns.first().cloned())
1246        } else {
1247            None
1248        };
1249        if is_document_collection {
1250            // #1366: when a SET assigns to `body`, treat it as a FULL REPLACE of
1251            // the stored document — drop other column assignments, the new body
1252            // is the single source of truth.
1253            let body_assignment = static_field_assignments
1254                .iter()
1255                .chain(dynamic_field_assignments.iter())
1256                .rev()
1257                .find(|(column, _)| column == "body")
1258                .cloned();
1259
1260            match body_assignment {
1261                Some((_, value)) => {
1262                    let body = document_body_from_assignment(&value)?;
1263                    replace_document_row_body(
1264                        row,
1265                        body,
1266                        &collection,
1267                        self.binary_document_body_enabled(),
1268                        &mut modified_columns,
1269                    )?;
1270                    context_index_dirty = true;
1271                }
1272                None => {
1273                    // #1365: no `body` assignment — fall back to nested-path
1274                    // partial update via document_body_set_operation.
1275                    let mut static_row_assignments = Vec::new();
1276                    let mut dynamic_row_assignments = Vec::new();
1277                    let mut document_body_ops = Vec::new();
1278                    for (column, value) in static_field_assignments.iter().cloned() {
1279                        if column == "body" {
1280                            static_row_assignments.push((column, value));
1281                        } else {
1282                            document_body_ops.push(document_body_set_operation(&column, value));
1283                        }
1284                    }
1285                    for (column, value) in dynamic_field_assignments {
1286                        if column == "body" {
1287                            dynamic_row_assignments.push((column, value));
1288                        } else {
1289                            document_body_ops.push(document_body_set_operation(&column, value));
1290                        }
1291                    }
1292                    apply_row_field_assignments_raw(row, static_row_assignments);
1293                    apply_row_field_assignments_raw(row, dynamic_row_assignments);
1294                    if !document_body_ops.is_empty() {
1295                        let mut body = document_body_from_row(row)?;
1296                        apply_document_body_patch_operations(&mut body, &document_body_ops)?;
1297                        replace_document_row_body(
1298                            row,
1299                            body,
1300                            &collection,
1301                            self.binary_document_body_enabled(),
1302                            &mut modified_columns,
1303                        )?;
1304                        context_index_dirty = true;
1305                    }
1306                }
1307            }
1308        } else {
1309            // Non-document collection: every assignment is a regular row column.
1310            let mut all_assignments: Vec<(String, Value)> = static_field_assignments.to_vec();
1311            all_assignments.extend(dynamic_field_assignments);
1312            apply_row_field_assignments_raw(row, all_assignments);
1313            if let Some(key) = kv_key {
1314                set_row_field(row, "key", key);
1315            }
1316        }
1317
1318        for (key, value) in static_metadata_assignments
1319            .iter()
1320            .cloned()
1321            .chain(dynamic_metadata_assignments)
1322        {
1323            ensure_non_tree_reserved_metadata_key(&key)?;
1324            patch_metadata
1325                .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default())
1326                .set(key, value);
1327            metadata_changed = true;
1328        }
1329
1330        if !modified_columns.is_empty() || row_contract_timestamps {
1331            let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1332            if row_contract_timestamps {
1333                context_index_dirty = true;
1334                set_row_field(row, "updated_at", contract.managed_timestamp_value());
1335                modified_columns.push("updated_at".to_string());
1336            }
1337            let expression_contract = if row_contract_plan.is_some_and(|plan| plan.has_expressions)
1338            {
1339                db.collection_contract_arc(&collection)
1340            } else {
1341                None
1342            };
1343            if let Some(schema) = &expression_contract {
1344                let normalized = contract.normalize_update_fields(collect_row_fields(row))?;
1345                for column in schema
1346                    .declared_columns
1347                    .iter()
1348                    .filter(|column| column.generated.is_some())
1349                {
1350                    modified_columns.push(column.name.clone());
1351                }
1352                for (name, value) in normalized {
1353                    set_row_field(row, &name, value);
1354                }
1355                context_index_dirty = true;
1356            }
1357            if row_touches_unique_columns || expression_contract.is_some() {
1358                let current_fields = collect_row_fields(row);
1359                contract.enforce_row_uniqueness(&current_fields, Some(id))?;
1360            }
1361        }
1362
1363        entity.updated_at = std::time::SystemTime::now()
1364            .duration_since(std::time::UNIX_EPOCH)
1365            .unwrap_or_default()
1366            .as_secs();
1367
1368        if let Some(xid) = versioned_update_xid {
1369            let logical_id = entity.logical_id();
1370            entity.id = store.next_entity_id();
1371            entity.set_logical_id(logical_id);
1372            entity.set_xmin(xid);
1373            entity.set_xmax(0);
1374            if let Some(old) = replaced_entity.as_mut() {
1375                old.set_xmax(xid);
1376            }
1377        }
1378
1379        modified_columns = dedupe_modified_columns(modified_columns);
1380
1381        Ok(AppliedEntityMutation {
1382            id: entity.id,
1383            collection,
1384            entity,
1385            metadata: patch_metadata,
1386            modified_columns,
1387            persist_metadata: metadata_changed,
1388            context_index_dirty,
1389            replaced_entity,
1390            replaced_entity_previous_xmax: previous_xmax,
1391            pre_mutation_fields,
1392        })
1393    }
1394
1395    pub(crate) fn persist_applied_entity_mutations(
1396        &self,
1397        applied: &[AppliedEntityMutation],
1398    ) -> RedDBResult<()> {
1399        if applied.is_empty() {
1400            return Ok(());
1401        }
1402
1403        let store = self.db().store();
1404        // Fence the in-memory change and its WAL append together. Otherwise a
1405        // checkpoint can copy a new transaction's rows or truncate a concurrent
1406        // autocommit's WAL before those rows belong to its frozen generation.
1407        let _publication = store.vault_publication_lock.lock();
1408        let collection = &applied[0].collection;
1409        let Some(manager) = store.get_collection(collection) else {
1410            return Err(crate::RedDBError::NotFound(format!(
1411                "collection not found: {collection}"
1412            )));
1413        };
1414
1415        let mut ordinary = Vec::with_capacity(applied.len());
1416        for item in applied {
1417            if let Some(old_version) = item.replaced_entity.as_ref() {
1418                store
1419                    .install_versioned_table_row_update(
1420                        collection,
1421                        old_version.clone(),
1422                        item.entity.clone(),
1423                        if item.persist_metadata {
1424                            item.metadata.as_ref()
1425                        } else {
1426                            None
1427                        },
1428                    )
1429                    .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
1430                if self.current_xid().is_some() {
1431                    self.record_pending_versioned_update(
1432                        crate::runtime::impl_core::current_connection_id(),
1433                        collection,
1434                        old_version.id,
1435                        item.entity.id,
1436                        old_version.xmax,
1437                        item.replaced_entity_previous_xmax,
1438                    );
1439                }
1440            } else {
1441                ordinary.push(item);
1442            }
1443        }
1444        if ordinary.is_empty() {
1445            return Ok(());
1446        }
1447
1448        manager
1449            .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
1450                (
1451                    &item.entity,
1452                    item.modified_columns.as_slice(),
1453                    if item.persist_metadata {
1454                        item.metadata.as_ref()
1455                    } else {
1456                        None
1457                    },
1458                )
1459            }))
1460            .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
1461
1462        // PG-HOT-like fast path: segment in-place is done; when no
1463        // mutation touches a secondary-indexed column AND no metadata
1464        // payload needs to be folded into a B-tree record, skip the
1465        // in-line B-tree upsert. The WAL still records the update
1466        // (durability preserved; recovery replay rebuilds the B-tree),
1467        // and `manager.get()` prefers the live segment over the
1468        // B-tree for reads — so the short-circuit is invisible to
1469        // callers. See `persist_entities_to_pager_wal_only`.
1470        let indexed_cols = self
1471            .index_store_ref()
1472            .indexed_columns_set(collection.as_str());
1473        let all_hot = !indexed_cols.is_empty()
1474            && ordinary.iter().all(|item| {
1475                !item.persist_metadata
1476                    && !item
1477                        .modified_columns
1478                        .iter()
1479                        .any(|c| indexed_cols.contains(c))
1480            })
1481            || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
1482
1483        // Pass `&[&UnifiedEntity]` — no per-entity clone. The SQL UPDATE
1484        // inner loop hands us `applied` which already owns the post-image
1485        // entity; all we need for the persist path is a read borrow.
1486        let entity_refs: Vec<&crate::storage::UnifiedEntity> =
1487            ordinary.iter().map(|item| &item.entity).collect();
1488        let persist_fn = if all_hot {
1489            crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
1490        } else {
1491            crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
1492        };
1493        persist_fn(store.as_ref(), collection, &entity_refs)
1494            .map_err(|err| crate::RedDBError::Internal(err.to_string()))
1495    }
1496
1497    pub(crate) fn flush_applied_entity_mutation(
1498        &self,
1499        applied: &AppliedEntityMutation,
1500        topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1501    ) -> RedDBResult<()> {
1502        let store = self.db().store();
1503        if applied.context_index_dirty {
1504            store
1505                .context_index()
1506                .index_entity(&applied.collection, &applied.entity);
1507        }
1508        // Secondary-index maintenance for SQL UPDATE / JSON-Patch flows.
1509        // Skip when pre_mutation_fields is empty (entity wasn't a row, or
1510        // didn't carry recoverable column names) — there's nothing to
1511        // delete-then-insert in that case.
1512        //
1513        // Also build the CDC damage vector here so downstream consumers
1514        // see which columns changed without re-diffing.
1515        let mut changed_columns: Option<Vec<String>> = None;
1516        if !applied.pre_mutation_fields.is_empty() {
1517            let post = entity_row_fields_snapshot(&applied.entity);
1518            if !post.is_empty() {
1519                let damage = crate::application::entity::row_damage_vector(
1520                    &applied.pre_mutation_fields,
1521                    &post,
1522                );
1523                if !damage.is_empty() {
1524                    changed_columns = Some(
1525                        damage
1526                            .touched_columns()
1527                            .into_iter()
1528                            .map(str::to_string)
1529                            .collect(),
1530                    );
1531                }
1532
1533                // HOT-like fast path (P3.T2/T3): when no modified
1534                // column is covered by a secondary index, skip the
1535                // `index_entity_update` call entirely. The function
1536                // would short-circuit internally, but the call still
1537                // reads the registry lock + walks the damage vector
1538                // — avoiding it saves a few microseconds per UPDATE.
1539                // Page-local replace + t_ctid chain support (true
1540                // HOT) lives in a follow-up storage spec.
1541                // Use the parent-expanded set so that dot-path indexes
1542                // (e.g. "body.service.tier") are triggered when the root
1543                // field ("body") appears in modified_columns.
1544                let indexed_cols: std::collections::HashSet<String> = self
1545                    .index_store_ref()
1546                    .indexed_columns_set_with_parents(applied.collection.as_str());
1547                // Single-source documents keep promoted index columns in the
1548                // body — surface them so the index sees the value move (a no-op
1549                // for ordinary stored columns).
1550                let pre_index_fields = self
1551                    .index_store_ref()
1552                    .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
1553                let post_index_fields = self
1554                    .index_store_ref()
1555                    .augment_body_derived_index_fields(&post, &indexed_cols);
1556                let modified_cols: std::collections::HashSet<String> =
1557                    crate::application::entity::row_damage_vector(
1558                        &pre_index_fields,
1559                        &post_index_fields,
1560                    )
1561                    .touched_columns()
1562                    .into_iter()
1563                    .map(str::to_string)
1564                    .collect();
1565                if let Some(old_version) = applied.replaced_entity.as_ref() {
1566                    let old_index_fields: Vec<(String, Value)> = pre_index_fields
1567                        .iter()
1568                        .filter(|(col, _)| indexed_cols.contains(col))
1569                        .cloned()
1570                        .collect();
1571                    let new_index_fields: Vec<(String, Value)> = post_index_fields
1572                        .iter()
1573                        .filter(|(col, _)| indexed_cols.contains(col))
1574                        .cloned()
1575                        .collect();
1576                    if !old_index_fields.is_empty() {
1577                        self.index_store_ref()
1578                            .index_entity_delete(
1579                                &applied.collection,
1580                                old_version.id,
1581                                &old_index_fields,
1582                            )
1583                            .map_err(crate::RedDBError::Internal)?;
1584                    }
1585                    if !new_index_fields.is_empty() {
1586                        self.index_store_ref()
1587                            .index_entity_insert(
1588                                &applied.collection,
1589                                applied.entity.id,
1590                                &new_index_fields,
1591                            )
1592                            .map_err(crate::RedDBError::Internal)?;
1593                    }
1594                } else {
1595                    let decision = crate::storage::engine::hot_update::decide(
1596                        &crate::storage::engine::hot_update::HotUpdateInputs {
1597                            collection: applied.collection.as_str(),
1598                            indexed_columns: &indexed_cols,
1599                            modified_columns: &modified_cols,
1600                            // The storage layer currently handles fit via
1601                            // the segment abstraction; we bypass the
1602                            // page-size check here.
1603                            new_tuple_size: 0,
1604                            page_free_space: usize::MAX,
1605                        },
1606                    );
1607                    if !decision.can_hot {
1608                        self.index_store_ref()
1609                            .index_entity_update(
1610                                &applied.collection,
1611                                applied.id,
1612                                &pre_index_fields,
1613                                &post_index_fields,
1614                            )
1615                            .map_err(crate::RedDBError::Internal)?;
1616                    } else {
1617                        // F-04: `applied.collection` is tenant-supplied;
1618                        // strip CR/LF/control bytes via the LogField
1619                        // escaper (ADR 0010).
1620                        tracing::debug!(
1621                            collection = %reddb_wire::audit_safe_log_field(&applied.collection),
1622                            "hot_update fast-path: skipped index_entity_update"
1623                        );
1624                    }
1625                }
1626            }
1627        }
1628        drop(topology_guard);
1629        #[cfg(test)]
1630        self.index_store_ref().mutation_test_hook(
1631            &applied.collection,
1632            crate::runtime::MutationTestPhase::BeforeEvents,
1633        );
1634        self.cdc_emit_prebuilt_with_columns(
1635            crate::replication::cdc::ChangeOperation::Update,
1636            &applied.collection,
1637            &applied.entity,
1638            "entity",
1639            applied.metadata.as_ref(),
1640            true,
1641            changed_columns,
1642        );
1643        Ok(())
1644    }
1645
1646    pub(crate) fn apply_loaded_patch_entity(
1647        &self,
1648        collection: String,
1649        entity: crate::storage::UnifiedEntity,
1650        payload: JsonValue,
1651        operations: Vec<PatchEntityOperation>,
1652        topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1653    ) -> RedDBResult<CreateEntityOutput> {
1654        let applied =
1655            self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
1656        if !crate::runtime::rls_injection::entity_passes_rls_action(
1657            self,
1658            &applied.collection,
1659            reddb_rql::ast::PolicyAction::Update,
1660            &applied.entity,
1661        ) {
1662            return Err(crate::RedDBError::NotFound("entity not found".to_string()));
1663        }
1664        let _reservation = self.admit_entity_mutation(&applied)?;
1665        self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
1666        #[cfg(test)]
1667        self.index_store_ref().mutation_test_hook(
1668            &applied.collection,
1669            crate::runtime::MutationTestPhase::StoragePublished,
1670        );
1671        self.flush_applied_entity_mutation(&applied, topology_guard)?;
1672        Ok(CreateEntityOutput {
1673            id: applied.id,
1674            entity: Some(public_document_entity(applied.entity)),
1675        })
1676    }
1677}
1678
1679/// Shape the canonical binary-body document into the public response entity.
1680/// The stored `body` is the single source of truth (RDOC binary container),
1681/// but callers read the returned entity two ways, so the response satisfies
1682/// both: top-level body fields are surfaced into `named` (mirroring the GET
1683/// presentation derive in `named_fields_json`), and `body` itself is decoded
1684/// back to plain JSON. Derivation runs against the binary container first,
1685/// then `body` is replaced with its JSON form. This only reshapes the
1686/// returned copy — persistence already happened against the binary body.
1687fn public_document_entity(
1688    mut entity: crate::storage::UnifiedEntity,
1689) -> crate::storage::UnifiedEntity {
1690    let crate::storage::EntityData::Row(row) = &mut entity.data else {
1691        return entity;
1692    };
1693    let Some(named) = row.named.as_mut() else {
1694        return entity;
1695    };
1696    let Some(Value::Json(bytes)) = named.get("body") else {
1697        return entity;
1698    };
1699    let bytes = bytes.clone();
1700    if let Some(fields) = crate::document_body::body_fields(&bytes) {
1701        for (name, value) in fields {
1702            named.entry(name).or_insert(value);
1703        }
1704    }
1705    if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
1706        if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
1707            named.insert("body".to_string(), Value::Json(json_bytes));
1708        }
1709    }
1710    entity
1711}
1712
1713fn ensure_non_tree_reserved_metadata_patch_paths(
1714    operations: &[PatchEntityOperation],
1715) -> RedDBResult<()> {
1716    for operation in operations {
1717        let Some(key) = operation.path.first().map(String::as_str) else {
1718            continue;
1719        };
1720        ensure_non_tree_reserved_metadata_key(key)?;
1721    }
1722    Ok(())
1723}
1724
1725fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
1726    if key.starts_with(TREE_METADATA_PREFIX) {
1727        return Err(crate::RedDBError::Query(format!(
1728            "metadata key '{}' is reserved for managed trees",
1729            key
1730        )));
1731    }
1732    Ok(())
1733}
1734
1735fn ensure_non_tree_reserved_metadata_entries(
1736    metadata: &[(String, MetadataValue)],
1737) -> RedDBResult<()> {
1738    for (key, _) in metadata {
1739        ensure_non_tree_reserved_metadata_key(key)?;
1740    }
1741    Ok(())
1742}
1743
1744fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
1745    if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
1746        return Err(crate::RedDBError::Query(format!(
1747            "edge label '{}' is reserved for managed trees",
1748            TREE_CHILD_EDGE_LABEL
1749        )));
1750    }
1751    Ok(())
1752}
1753
1754impl RedDBRuntime {
1755    pub(crate) fn create_node_unchecked(
1756        &self,
1757        mut input: CreateNodeInput,
1758    ) -> RedDBResult<CreateEntityOutput> {
1759        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
1760        crate::runtime::mutation::enforce_tenant_fields(
1761            self,
1762            &input.collection,
1763            &mut input.properties,
1764        )?;
1765        let node_type = input
1766            .node_type
1767            .clone()
1768            .unwrap_or_else(|| input.label.clone());
1769        let candidate = crate::storage::UnifiedEntity::new(
1770            crate::storage::EntityId::new(0),
1771            crate::storage::EntityKind::GraphNode(Box::new(
1772                crate::storage::unified::GraphNodeKind {
1773                    label: input.label.clone(),
1774                    node_type,
1775                },
1776            )),
1777            crate::storage::EntityData::Node(crate::storage::NodeData::with_properties(
1778                input.properties.iter().cloned().collect(),
1779            )),
1780        );
1781        ensure_insert_entity_rls(self, &input.collection, &candidate)?;
1782        let db = self.db();
1783        let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1784        contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1785        let mut metadata = input.metadata;
1786        contract.apply_default_ttl(&mut metadata);
1787        let mut builder = db.node(&input.collection, &input.label);
1788
1789        if let Some(node_type) = input.node_type {
1790            builder = builder.node_type(node_type);
1791        }
1792
1793        for (key, value) in input.properties {
1794            builder = builder.property(key, value);
1795        }
1796
1797        for (key, value) in metadata {
1798            builder = builder.metadata(key, value);
1799        }
1800
1801        for embedding in input.embeddings {
1802            if let Some(model) = embedding.model {
1803                builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
1804            } else {
1805                builder = builder.embedding(embedding.name, embedding.vector);
1806            }
1807        }
1808
1809        for link in input.table_links {
1810            builder = builder.link_to_table(link.key, link.table);
1811        }
1812
1813        for link in input.node_links {
1814            builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
1815        }
1816
1817        let id = builder.save()?;
1818        // Phase 1.1 MVCC universal: stamp xmin so concurrent snapshots
1819        // don't see this node until the transaction commits.
1820        self.stamp_xmin_if_in_txn(&input.collection, id);
1821        refresh_context_index(&db, &input.collection, id)?;
1822        self.cdc_emit(
1823            crate::replication::cdc::ChangeOperation::Insert,
1824            &input.collection,
1825            id.raw(),
1826            "graph_node",
1827        );
1828        Ok(CreateEntityOutput {
1829            id,
1830            entity: db.store().get(&input.collection, id),
1831        })
1832    }
1833
1834    pub(crate) fn create_edge_unchecked(
1835        &self,
1836        mut input: CreateEdgeInput,
1837    ) -> RedDBResult<CreateEntityOutput> {
1838        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
1839        crate::runtime::mutation::enforce_tenant_fields(
1840            self,
1841            &input.collection,
1842            &mut input.properties,
1843        )?;
1844        let weight = input.weight.unwrap_or(1.0);
1845        let candidate = crate::storage::UnifiedEntity::new(
1846            crate::storage::EntityId::new(0),
1847            crate::storage::EntityKind::GraphEdge(Box::new(
1848                crate::storage::unified::GraphEdgeKind {
1849                    label: input.label.clone(),
1850                    from_node: input.from.raw().to_string(),
1851                    to_node: input.to.raw().to_string(),
1852                    weight: (weight * 1000.0) as u32,
1853                },
1854            )),
1855            crate::storage::EntityData::Edge(crate::storage::EdgeData::with_properties(
1856                weight,
1857                input.properties.iter().cloned().collect(),
1858            )),
1859        );
1860        ensure_insert_entity_rls(self, &input.collection, &candidate)?;
1861        let db = self.db();
1862        let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1863        contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1864        let mut metadata = input.metadata;
1865        contract.apply_default_ttl(&mut metadata);
1866        let mut builder = db
1867            .edge(&input.collection, &input.label)
1868            .from(input.from)
1869            .to(input.to);
1870
1871        if let Some(weight) = input.weight {
1872            builder = builder.weight(weight);
1873        }
1874
1875        for (key, value) in input.properties {
1876            builder = builder.property(key, value);
1877        }
1878
1879        for (key, value) in metadata {
1880            builder = builder.metadata(key, value);
1881        }
1882
1883        let id = builder.save()?;
1884        // Phase 1.1 MVCC universal: stamp xmin on the edge so other
1885        // sessions don't follow it until COMMIT.
1886        self.stamp_xmin_if_in_txn(&input.collection, id);
1887        refresh_context_index(&db, &input.collection, id)?;
1888        self.cdc_emit(
1889            crate::replication::cdc::ChangeOperation::Insert,
1890            &input.collection,
1891            id.raw(),
1892            "graph_edge",
1893        );
1894        Ok(CreateEntityOutput {
1895            id,
1896            entity: db.store().get(&input.collection, id),
1897        })
1898    }
1899}
1900
1901fn create_rows_batch_prevalidated_columnar_with_outputs(
1902    runtime: &RedDBRuntime,
1903    collection: String,
1904    column_names: std::sync::Arc<Vec<String>>,
1905    rows: Vec<Vec<reddb_types::Value>>,
1906) -> RedDBResult<Vec<CreateEntityOutput>> {
1907    use crate::storage::{
1908        unified::{EntityData, EntityKind, RowData},
1909        EntityId, UnifiedEntity,
1910    };
1911    use std::sync::Arc;
1912
1913    if rows.is_empty() {
1914        return Ok(Vec::new());
1915    }
1916    if crate::runtime::execution_context::current_auth_identity().is_some() {
1917        let inputs = rows
1918            .into_iter()
1919            .map(|values| CreateRowInput {
1920                collection: collection.clone(),
1921                fields: column_names.iter().cloned().zip(values).collect(),
1922                metadata: Vec::new(),
1923                node_links: Vec::new(),
1924                vector_links: Vec::new(),
1925            })
1926            .collect();
1927        return runtime.create_rows_batch_prevalidated_with_outputs(CreateRowsBatchInput {
1928            collection,
1929            rows: inputs,
1930            suppress_events: false,
1931        });
1932    }
1933    // Schema prevalidation does not admit standalone index keys. Route these
1934    // collections through the guarded row kernel before installing any row.
1935    if runtime.index_store_ref().has_unique_index(&collection) {
1936        return runtime.create_rows_batch_columnar_with_outputs(collection, column_names, rows);
1937    }
1938    runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1939    runtime.check_batch_size(rows.len())?;
1940    runtime.check_db_size()?;
1941
1942    let db = runtime.db();
1943    let contract = CollectionContractWriteEnforcer::new(runtime, &db, &collection);
1944    contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1945
1946    let store = db.store();
1947    let table_arc: Arc<str> = Arc::from(collection.as_str());
1948
1949    let indexed_cols = runtime
1950        .index_store_ref()
1951        .indexed_columns_set(collection.as_str());
1952    let has_secondary_indexes = !indexed_cols.is_empty();
1953    let mut field_snapshots: Vec<Vec<(String, reddb_types::Value)>> = if has_secondary_indexes {
1954        Vec::with_capacity(rows.len())
1955    } else {
1956        Vec::new()
1957    };
1958
1959    let entities: Vec<UnifiedEntity> = rows
1960        .into_iter()
1961        .map(|values| {
1962            if has_secondary_indexes {
1963                let mut snap: Vec<(String, reddb_types::Value)> =
1964                    Vec::with_capacity(indexed_cols.len());
1965                for (name, val) in column_names.iter().zip(values.iter()) {
1966                    if indexed_cols.contains(name) {
1967                        snap.push((name.clone(), val.clone()));
1968                    }
1969                }
1970                field_snapshots.push(snap);
1971            }
1972            let mut row = RowData::new(values);
1973            row.schema = Some(Arc::clone(&column_names));
1974            UnifiedEntity::new(
1975                EntityId::new(0),
1976                EntityKind::TableRow {
1977                    table: Arc::clone(&table_arc),
1978                    row_id: 0,
1979                },
1980                EntityData::Row(row),
1981            )
1982        })
1983        .collect();
1984
1985    let ids = store
1986        .bulk_insert(&collection, entities)
1987        .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
1988
1989    if has_secondary_indexes {
1990        let index_rows: Vec<(EntityId, Vec<(String, reddb_types::Value)>)> = ids
1991            .iter()
1992            .zip(field_snapshots)
1993            .map(|(id, fields)| (*id, fields))
1994            .collect();
1995        runtime
1996            .index_store_ref()
1997            .index_entity_insert_batch(&collection, &index_rows)
1998            .map_err(crate::RedDBError::Internal)?;
1999    }
2000
2001    runtime.invalidate_result_cache();
2002    runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
2003
2004    Ok(ids
2005        .into_iter()
2006        .map(|id| CreateEntityOutput { id, entity: None })
2007        .collect())
2008}
2009
2010impl RuntimeEntityPort for RedDBRuntime {
2011    fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
2012        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2013        let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2014            self,
2015            &input.collection,
2016        );
2017        let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2018        let db = self.db();
2019        let CreateRowInput {
2020            collection,
2021            fields,
2022            metadata: input_metadata,
2023            node_links,
2024            vector_links,
2025        } = input;
2026        let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2027        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2028        let mut metadata = input_metadata;
2029        contract.apply_default_ttl(&mut metadata);
2030        let fields = contract.normalize_insert_fields(fields)?;
2031        contract.enforce_row_uniqueness(&fields, None)?;
2032        // Route through MutationEngine for unified hot path.
2033        let engine = self.mutation_engine();
2034        let result = engine.apply(
2035            collection.clone(),
2036            vec![crate::runtime::mutation::MutationRow {
2037                fields,
2038                metadata,
2039                node_links,
2040                vector_links,
2041            }],
2042        )?;
2043        let id = result.ids[0];
2044        // Perf: `db.get(id)` does a *cross-collection* scan (get_any)
2045        // that also takes a write lock on the entity cache. We know
2046        // the collection — hit the manager directly. Cuts
2047        // create_row() p50 roughly in half on the hot path.
2048        Ok(CreateEntityOutput {
2049            id,
2050            entity: db.store().get(&collection, id),
2051        })
2052    }
2053
2054    fn create_rows_batch(
2055        &self,
2056        input: CreateRowsBatchInput,
2057    ) -> RedDBResult<Vec<CreateEntityOutput>> {
2058        let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2059            self,
2060            &input.collection,
2061        );
2062        let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2063        if input.rows.is_empty() {
2064            return Ok(Vec::new());
2065        }
2066        self.check_batch_size(input.rows.len())?;
2067        self.check_db_size()?;
2068
2069        let db = self.db();
2070        let collection = input.collection;
2071        let suppress_events = input.suppress_events;
2072        let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2073        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2074
2075        let mut prepared_rows = Vec::with_capacity(input.rows.len());
2076        let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
2077        for row in input.rows {
2078            if row.collection != collection {
2079                return Err(crate::RedDBError::Query(format!(
2080                    "batch row collection mismatch: expected '{}', got '{}'",
2081                    collection, row.collection
2082                )));
2083            }
2084
2085            let mut metadata = row.metadata;
2086            contract.apply_default_ttl(&mut metadata);
2087            let fields = contract.normalize_insert_fields(row.fields)?;
2088            contract.enforce_row_uniqueness(&fields, None)?;
2089            uniqueness_rows.push(fields.clone());
2090            prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
2091        }
2092
2093        contract.enforce_batch_uniqueness(&uniqueness_rows)?;
2094
2095        // Route through MutationEngine: single bulk_insert + one CDC batch
2096        // instead of N separate cdc_emit() calls (each acquires a write lock).
2097        let engine = {
2098            let e = self.mutation_engine();
2099            if suppress_events {
2100                e.with_suppress_events()
2101            } else {
2102                e
2103            }
2104        };
2105        let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
2106            .into_iter()
2107            .map(|(fields, metadata, node_links, vector_links)| {
2108                crate::runtime::mutation::MutationRow {
2109                    fields,
2110                    metadata,
2111                    node_links,
2112                    vector_links,
2113                }
2114            })
2115            .collect();
2116
2117        let result = engine.apply(collection.clone(), mutation_rows)?;
2118
2119        let store = db.store();
2120        Ok(result
2121            .ids
2122            .into_iter()
2123            .map(|id| CreateEntityOutput {
2124                id,
2125                entity: store.get(&collection, id),
2126            })
2127            .collect())
2128    }
2129
2130    fn create_rows_batch_prevalidated_columnar(
2131        &self,
2132        collection: String,
2133        column_names: std::sync::Arc<Vec<String>>,
2134        rows: Vec<Vec<reddb_types::Value>>,
2135    ) -> RedDBResult<usize> {
2136        create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2137            .map(|outputs| outputs.len())
2138    }
2139
2140    fn create_rows_batch_columnar(
2141        &self,
2142        collection: String,
2143        column_names: std::sync::Arc<Vec<String>>,
2144        rows: Vec<Vec<reddb_types::Value>>,
2145    ) -> RedDBResult<usize> {
2146        self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2147            .map(|outputs| outputs.len())
2148    }
2149
2150    fn create_rows_batch_columnar_with_outputs(
2151        &self,
2152        collection: String,
2153        column_names: std::sync::Arc<Vec<String>>,
2154        rows: Vec<Vec<reddb_types::Value>>,
2155    ) -> RedDBResult<Vec<CreateEntityOutput>> {
2156        if rows.is_empty() {
2157            return Ok(Vec::new());
2158        }
2159        self.check_batch_size(rows.len())?;
2160        self.check_db_size()?;
2161
2162        let db = self.db();
2163        let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2164        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2165
2166        // Fast path: when the collection carries no contract (or the
2167        // contract has no declared columns) `normalize_insert_fields`
2168        // is a no-op and `enforce_row_uniqueness` finds no unique
2169        // columns to check. Skip the per-row `(String, Value)` tuple
2170        // materialisation entirely and route through the prevalidated
2171        // columnar kernel — same write, CDC, and index path, just
2172        // without the wasted (String, Value) clones. This is the
2173        // bench `bench_users` shape (no contract declared by the
2174        // adapter's `setup_schema`).
2175        let needs_normalisation = self.index_store_ref().has_unique_index(&collection)
2176            || match db.collection_contract(&collection) {
2177                Some(c) => {
2178                    c.declared_model == crate::catalog::CollectionModel::Table
2179                        && (!c.declared_columns.is_empty()
2180                            || c.table_def
2181                                .as_ref()
2182                                .map(|t| !t.columns.is_empty())
2183                                .unwrap_or(false))
2184                }
2185                None => false,
2186            };
2187        if !needs_normalisation {
2188            return create_rows_batch_prevalidated_columnar_with_outputs(
2189                self,
2190                collection,
2191                column_names,
2192                rows,
2193            );
2194        }
2195
2196        // Slow path: contract requires per-row normalisation /
2197        // uniqueness checks. Materialise `Vec<(String, Value)>` from
2198        // the columnar layout (this still pays N×ncols `String::clone`
2199        // — but it's deferred out of the wire decoder hot loop and
2200        // happens behind a runtime API, not the per-row decode loop)
2201        // and fall through to the existing `create_rows_batch` path.
2202        let ncols = column_names.len();
2203        let tuple_rows: Vec<CreateRowInput> = rows
2204            .into_iter()
2205            .map(|values| {
2206                let mut fields: Vec<(String, reddb_types::Value)> = Vec::with_capacity(ncols);
2207                for (name, value) in column_names.iter().zip(values) {
2208                    fields.push((name.clone(), value));
2209                }
2210                CreateRowInput {
2211                    collection: collection.clone(),
2212                    fields,
2213                    metadata: Vec::new(),
2214                    node_links: Vec::new(),
2215                    vector_links: Vec::new(),
2216                }
2217            })
2218            .collect();
2219        self.create_rows_batch(CreateRowsBatchInput {
2220            collection,
2221            rows: tuple_rows,
2222            suppress_events: false,
2223        })
2224    }
2225
2226    fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
2227        self.create_rows_batch_prevalidated_with_outputs(input)
2228            .map(|outputs| outputs.len())
2229    }
2230
2231    fn create_rows_batch_prevalidated_with_outputs(
2232        &self,
2233        input: CreateRowsBatchInput,
2234    ) -> RedDBResult<Vec<CreateEntityOutput>> {
2235        if input.rows.is_empty() {
2236            return Ok(Vec::new());
2237        }
2238        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2239        self.check_batch_size(input.rows.len())?;
2240        self.check_db_size()?;
2241
2242        let db = self.db();
2243        let collection = input.collection;
2244        let suppress_events = input.suppress_events;
2245        // Still verify the collection's declared model before we blast
2246        // rows at it — this one-off check is O(1), independent of
2247        // ncols, and catches schema-kind mismatches that the client
2248        // can't always see.
2249        let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2250        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2251
2252        // Hoist the per-collection default TTL lookup out of the
2253        // per-row loop — it only depends on the collection, not on
2254        // individual rows, and the old path did one HashMap read per
2255        // row (25k reads for a 25k typed_insert bulk). Fetch it once,
2256        // apply to any row whose metadata doesn't already carry a TTL.
2257        let default_ttl_plan = contract.default_ttl_plan();
2258
2259        let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
2260            Vec::with_capacity(input.rows.len());
2261        let mut mutation_rows = mutation_rows;
2262        for row in input.rows {
2263            if row.collection != collection {
2264                return Err(crate::RedDBError::Query(format!(
2265                    "batch row collection mismatch: expected '{}', got '{}'",
2266                    collection, row.collection
2267                )));
2268            }
2269            let mut metadata = row.metadata;
2270            default_ttl_plan.apply(&mut metadata);
2271            mutation_rows.push(crate::runtime::mutation::MutationRow {
2272                fields: row.fields,
2273                metadata,
2274                node_links: row.node_links,
2275                vector_links: row.vector_links,
2276            });
2277        }
2278
2279        let engine = {
2280            let engine = self.mutation_engine();
2281            if suppress_events {
2282                engine.with_suppress_events()
2283            } else {
2284                engine
2285            }
2286        };
2287        let result = engine.apply(collection.clone(), mutation_rows)?;
2288        let store = db.store();
2289        Ok(result
2290            .ids
2291            .into_iter()
2292            .map(|id| CreateEntityOutput {
2293                id,
2294                entity: store.get(&collection, id),
2295            })
2296            .collect())
2297    }
2298
2299    fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
2300        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2301        crate::reserved_fields::ensure_no_reserved_public_item_fields(
2302            input.properties.iter().map(|(key, _)| key.as_str()),
2303            &format!("node '{}'", input.collection),
2304        )?;
2305        ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2306        self.create_node_unchecked(input)
2307    }
2308
2309    fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
2310        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2311        crate::reserved_fields::ensure_no_reserved_public_item_fields(
2312            input.properties.iter().map(|(key, _)| key.as_str()),
2313            &format!("edge '{}'", input.collection),
2314        )?;
2315        ensure_non_tree_structural_edge_label(&input.label)?;
2316        ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2317        self.create_edge_unchecked(input)
2318    }
2319
2320    fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
2321        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2322        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2323        let mut vector_data = crate::storage::VectorData::new(input.dense.clone());
2324        vector_data.content = input.content.clone();
2325        let candidate = crate::storage::UnifiedEntity::new(
2326            crate::storage::EntityId::new(0),
2327            crate::storage::EntityKind::Vector {
2328                collection: input.collection.clone(),
2329            },
2330            crate::storage::EntityData::Vector(vector_data),
2331        );
2332        ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2333        let db = self.db();
2334        let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2335        contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
2336        contract.ensure_vector_dimension(input.dense.len())?;
2337        let mut metadata = input.metadata;
2338        contract.apply_default_ttl(&mut metadata);
2339        let mut builder = db.vector(&input.collection).dense(input.dense);
2340
2341        if let Some(content) = input.content {
2342            builder = builder.content(content);
2343        }
2344
2345        for (key, value) in metadata {
2346            builder = builder.metadata(key, value);
2347        }
2348
2349        if let Some(link_row) = input.link_row {
2350            builder = builder.link_to_table(link_row);
2351        }
2352
2353        if let Some(link_node) = input.link_node {
2354            builder = builder.link_to_node(link_node);
2355        }
2356
2357        // The transaction stamp must be present on the first published version.
2358        // Stamping after save exposed an xmin=0 vector to concurrent snapshots.
2359        let id = builder.save_with_xmin(self.current_xid())?;
2360        let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
2361            Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
2362            _ => None,
2363        };
2364        refresh_context_index(&db, &input.collection, id)?;
2365        // Issue #693 — vector.turbo write path. Order matters and
2366        // matches the write sequence the next slice (#673) will
2367        // attach kill-points to: WAL append → in-memory index update
2368        // → TurboExtent append. The legacy `vector` path is the
2369        // `None` branch and is untouched.
2370        if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
2371            // Lazy populate so an INSERT after restart sees the
2372            // pre-restart vectors in the index before appending its
2373            // own.
2374            state.ensure_populated(&db.store(), &input.collection);
2375            // Issue #694 — named crash boundaries for #673.
2376            use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
2377            fire(InjectionPoint::BeforeWalFsync);
2378            let _ = db
2379                .store()
2380                .append_vector_insert_record(&input.collection, id.raw(), &dense);
2381            fire(InjectionPoint::BeforeIndexCommit);
2382            let mut index = state.index.lock();
2383            index.insert(id, dense.clone());
2384            // Mirror the encoded codes into the per-collection
2385            // TurboExtent when one is available. The bytes are the
2386            // 4-bit-packed code groups (`n_byte_groups`) plus the
2387            // little-endian L2 norm — same shape `BlockedCodeStorage`
2388            // appends in-memory. Failures are non-fatal in this
2389            // slice; durability of the extent is owned by #673.
2390            fire(InjectionPoint::BeforeExtentFsync);
2391            if let Some(extent) = state.extent.lock().as_mut() {
2392                let packed = index.encode_for_extent(&dense);
2393                let _ = extent.append(&packed);
2394            }
2395        }
2396        self.cdc_emit(
2397            crate::replication::cdc::ChangeOperation::Insert,
2398            &input.collection,
2399            id.raw(),
2400            "vector",
2401        );
2402        Ok(CreateEntityOutput {
2403            id,
2404            entity: db.store().get(&input.collection, id),
2405        })
2406    }
2407
2408    fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
2409        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2410        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2411        let db = self.db();
2412        let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2413        contract.ensure_model(crate::catalog::CollectionModel::Document)?;
2414
2415        if let JsonValue::Object(ref map) = input.body {
2416            crate::reserved_fields::ensure_no_reserved_public_item_fields(
2417                map.keys().map(String::as_str),
2418                &format!("document '{}'", input.collection),
2419            )?;
2420        }
2421
2422        // Serialize the full body for the "body" field. Behind the
2423        // `storage.binary_document_body` flag (PRD-1398) this is the native binary
2424        // container; otherwise plain JSON. Reads decode either form to JSON.
2425        let mut fields: Vec<(String, reddb_types::Value)> = vec![(
2426            "body".to_string(),
2427            reddb_types::Value::Json(
2428                crate::json::to_vec(&input.body)
2429                    .map_err(|err| crate::RedDBError::Query(err.to_string()))?,
2430            ),
2431        )];
2432        crate::runtime::mutation::enforce_tenant_fields(self, &input.collection, &mut fields)?;
2433        let (field_names, field_values) =
2434            crate::runtime::impl_dml_support::pairwise_columns_values(&fields);
2435        let scoped_body =
2436            crate::runtime::impl_dml_support::find_document_body_json(&field_names, &field_values)?;
2437        let body_bytes = crate::document_body::serialize_document_body(
2438            &scoped_body,
2439            self.binary_document_body_enabled(),
2440        )?;
2441        if let Some((_, body)) = fields.iter_mut().find(|(name, _)| name == "body") {
2442            *body = reddb_types::Value::Json(body_bytes);
2443        }
2444        let mut row = crate::storage::RowData::new(Vec::new());
2445        row.named = Some(fields.iter().cloned().collect());
2446        let candidate = crate::storage::UnifiedEntity::new(
2447            crate::storage::EntityId::new(0),
2448            crate::storage::EntityKind::TableRow {
2449                table: std::sync::Arc::from(input.collection.as_str()),
2450                row_id: 0,
2451            },
2452            crate::storage::EntityData::Row(row),
2453        );
2454        ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2455
2456        let mut metadata = input.metadata;
2457        contract.apply_default_ttl(&mut metadata);
2458        let columns: Vec<(&str, reddb_types::Value)> = fields
2459            .iter()
2460            .map(|(key, value)| (key.as_str(), value.clone()))
2461            .collect();
2462        let mut builder = db.row(&input.collection, columns);
2463
2464        for (key, value) in metadata {
2465            builder = builder.metadata(key, value);
2466        }
2467
2468        for node in input.node_links {
2469            builder = builder.link_to_node(node);
2470        }
2471
2472        for vector in input.vector_links {
2473            builder = builder.link_to_vector(vector);
2474        }
2475
2476        let id = builder.save()?;
2477        // Phase 1.1 MVCC universal: stamp xmin on the document.
2478        self.stamp_xmin_if_in_txn(&input.collection, id);
2479        self.index_store_ref()
2480            .index_entity_insert(&input.collection, id, &fields)
2481            .map_err(crate::RedDBError::Internal)?;
2482        refresh_context_index(&db, &input.collection, id)?;
2483        self.cdc_emit(
2484            crate::replication::cdc::ChangeOperation::Insert,
2485            &input.collection,
2486            id.raw(),
2487            "document",
2488        );
2489        Ok(CreateEntityOutput {
2490            id,
2491            entity: db.store().get(&input.collection, id),
2492        })
2493    }
2494
2495    fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
2496        crate::runtime::kv_write::create(self, input, None)
2497    }
2498
2499    fn create_timeseries_point(
2500        &self,
2501        input: CreateTimeSeriesPointInput,
2502    ) -> RedDBResult<CreateEntityOutput> {
2503        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2504        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2505        let db = self.db();
2506        let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2507        contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
2508
2509        let mut fields = vec![
2510            ("metric".to_string(), reddb_types::Value::text(input.metric)),
2511            ("value".to_string(), reddb_types::Value::Float(input.value)),
2512        ];
2513
2514        if let Some(timestamp_ns) = input.timestamp_ns {
2515            fields.push((
2516                "timestamp".to_string(),
2517                reddb_types::Value::UnsignedInteger(timestamp_ns),
2518            ));
2519        }
2520
2521        if !input.tags.is_empty() {
2522            let tags_json = JsonValue::Object(
2523                input
2524                    .tags
2525                    .into_iter()
2526                    .map(|(key, value)| (key, JsonValue::String(value)))
2527                    .collect(),
2528            );
2529            let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
2530                crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
2531            })?;
2532            fields.push(("tags".to_string(), reddb_types::Value::Json(tags_bytes)));
2533        }
2534
2535        let collection = input.collection;
2536        let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
2537        // Phase 1.1 MVCC universal: stamp xmin on the point so
2538        // concurrent range scans hide it until COMMIT.
2539        self.stamp_xmin_if_in_txn(&collection, id);
2540        refresh_context_index(&db, &collection, id)?;
2541
2542        Ok(CreateEntityOutput {
2543            id,
2544            entity: db.store().get(&collection, id),
2545        })
2546    }
2547
2548    fn get_kv(
2549        &self,
2550        collection: &str,
2551        key: &str,
2552    ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
2553        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Select, collection)?;
2554        let resolver = crate::runtime::kv_read::KvReadResolver::for_rest_kv(self, collection)?;
2555        let Some(record) = resolver.record(key)? else {
2556            return Ok(None);
2557        };
2558        let visible = self
2559            .db()
2560            .store()
2561            .get(collection, record.id)
2562            .is_some_and(|entity| {
2563                crate::runtime::rls_injection::entity_passes_rls_action(
2564                    self,
2565                    collection,
2566                    reddb_rql::ast::PolicyAction::Select,
2567                    &entity,
2568                )
2569            });
2570        Ok(visible.then_some((record.value.unwrap_or(Value::Null), record.id)))
2571    }
2572
2573    fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
2574        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2575        crate::runtime::impl_kv::KvAtomicOps::new(self).delete(
2576            crate::catalog::CollectionModel::Kv,
2577            collection,
2578            key,
2579        )
2580    }
2581
2582    fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
2583        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2584        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Update, &input.collection)?;
2585        let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2586            self,
2587            &input.collection,
2588        );
2589        let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2590        let topology_lock = self
2591            .index_store_ref()
2592            .collection_topology_lock(&input.collection);
2593        let topology_guard = topology_lock.read();
2594        let PatchEntityInput {
2595            collection,
2596            id,
2597            payload,
2598            operations,
2599        } = input;
2600        let db = self.db();
2601        let store = db.store();
2602        let Some(manager) = store.get_collection(&collection) else {
2603            return Err(crate::RedDBError::NotFound(format!(
2604                "collection not found: {collection}"
2605            )));
2606        };
2607        let Some(entity) = manager.get(id) else {
2608            return Err(crate::RedDBError::NotFound(format!(
2609                "entity not found: {}",
2610                id.raw()
2611            )));
2612        };
2613        if !crate::runtime::rls_injection::entity_passes_rls_action(
2614            self,
2615            &collection,
2616            reddb_rql::ast::PolicyAction::Update,
2617            &entity,
2618        ) {
2619            return Err(crate::RedDBError::NotFound(format!(
2620                "entity not found: {}",
2621                id.raw()
2622            )));
2623        }
2624        self.apply_loaded_patch_entity(collection, entity, payload, operations, topology_guard)
2625    }
2626
2627    fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
2628        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2629        self.check_collection_privilege(reddb_rql::ast::PolicyAction::Delete, &input.collection)?;
2630        let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2631            self,
2632            &input.collection,
2633        );
2634        let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2635        let topology_lock = self
2636            .index_store_ref()
2637            .collection_topology_lock(&input.collection);
2638        let topology_guard = topology_lock.read();
2639        let store = self.db().store();
2640        // Snapshot row fields before delete so we can mirror the removal
2641        // into every secondary index. The fetch is best-effort: if the
2642        // entity is already gone, the delete below is a no-op anyway.
2643        let Some(entity) = store.get(&input.collection, input.id) else {
2644            return Ok(DeleteEntityOutput {
2645                deleted: false,
2646                id: input.id,
2647            });
2648        };
2649        if !crate::runtime::rls_injection::entity_passes_rls_action(
2650            self,
2651            &input.collection,
2652            reddb_rql::ast::PolicyAction::Delete,
2653            &entity,
2654        ) {
2655            return Ok(DeleteEntityOutput {
2656                deleted: false,
2657                id: input.id,
2658            });
2659        }
2660        let pre_delete_fields = entity_row_fields_snapshot(&entity);
2661        // Store delete first (source of truth). Crash between here and index removal
2662        // leaves the entity invisible to most queries but recoverable; the reverse
2663        // (remove index first, then crash) leaves the entity permanently orphaned.
2664        let deleted = store
2665            .delete(&input.collection, input.id)
2666            .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2667        if deleted {
2668            #[cfg(test)]
2669            self.index_store_ref().mutation_test_hook(
2670                &input.collection,
2671                crate::runtime::MutationTestPhase::StoragePublished,
2672            );
2673            store.context_index().remove_entity(input.id);
2674            // Secondary index maintenance — surface only registry-shape
2675            // errors; missing-index removals are tolerated inside the call.
2676            if !pre_delete_fields.is_empty() {
2677                self.index_store_ref()
2678                    .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
2679                    .map_err(crate::RedDBError::Internal)?;
2680            }
2681            drop(topology_guard);
2682            #[cfg(test)]
2683            self.index_store_ref().mutation_test_hook(
2684                &input.collection,
2685                crate::runtime::MutationTestPhase::BeforeEvents,
2686            );
2687            self.cdc_emit(
2688                crate::replication::cdc::ChangeOperation::Delete,
2689                &input.collection,
2690                input.id.raw(),
2691                "entity",
2692            );
2693        }
2694        Ok(DeleteEntityOutput {
2695            deleted,
2696            id: input.id,
2697        })
2698    }
2699}