Skip to main content

reddb_server/application/
ports_impls_entity.rs

1use std::collections::HashMap;
2
3use crate::application::entity::{
4    AppliedEntityMutation, CreateDocumentInput, CreateKvInput, CreateTimeSeriesPointInput,
5    RowUpdateColumnRule, RowUpdateContractPlan,
6};
7use crate::application::ttl_payload::{
8    has_internal_ttl_metadata, normalize_ttl_patch_operations, parse_top_level_ttl_metadata_entries,
9};
10use crate::json::{to_vec as json_to_vec, Value as JsonValue};
11use crate::storage::query::resolve_declared_data_type;
12use crate::storage::schema::{coerce as coerce_schema_value, DataType, Value};
13use crate::storage::unified::MetadataValue;
14
15use super::*;
16
17const TREE_METADATA_PREFIX: &str = "red.tree.";
18const TREE_CHILD_EDGE_LABEL: &str = "TREE_CHILD";
19
20fn apply_collection_default_ttl(
21    db: &crate::storage::unified::devx::RedDB,
22    collection: &str,
23    metadata: &mut Vec<(String, MetadataValue)>,
24) {
25    if has_internal_ttl_metadata(metadata) {
26        return;
27    }
28
29    let Some(default_ttl_ms) = db.collection_default_ttl_ms(collection) else {
30        return;
31    };
32
33    metadata.push((
34        "_ttl_ms".to_string(),
35        if default_ttl_ms <= i64::MAX as u64 {
36            MetadataValue::Int(default_ttl_ms as i64)
37        } else {
38            MetadataValue::Timestamp(default_ttl_ms)
39        },
40    ));
41}
42
43fn refresh_context_index(
44    db: &crate::storage::unified::devx::RedDB,
45    collection: &str,
46    id: crate::storage::EntityId,
47) -> RedDBResult<()> {
48    let store = db.store();
49    let Some(entity) = store.get(collection, id) else {
50        return Ok(());
51    };
52
53    store.context_index().index_entity(collection, &entity);
54    Ok(())
55}
56
57/// Pull `(name, value)` pairs for every named column on a row entity.
58/// Returns empty if the entity is not a row, or if the row carries
59/// neither a `named` map nor a `schema` Arc — both of those mean the
60/// names aren't recoverable here, so secondary-index maintenance has
61/// nothing to act on. Used by the delete + update paths.
62pub(crate) fn entity_row_fields_snapshot(
63    entity: &crate::storage::UnifiedEntity,
64) -> Vec<(String, Value)> {
65    let crate::storage::EntityData::Row(row) = &entity.data else {
66        return Vec::new();
67    };
68    let mut fields = if let Some(named) = &row.named {
69        named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
70    } else if let Some(schema) = &row.schema {
71        schema
72            .iter()
73            .zip(row.columns.iter())
74            .map(|(name, value)| (name.clone(), value.clone()))
75            .collect()
76    } else {
77        Vec::new()
78    };
79    expand_binary_document_body_snapshot(&mut fields);
80    fields
81}
82
83fn expand_binary_document_body_snapshot(fields: &mut Vec<(String, Value)>) {
84    let Some(body_idx) = fields.iter().position(|(name, _)| name == "body") else {
85        return;
86    };
87    let Value::Json(bytes) = &fields[body_idx].1 else {
88        return;
89    };
90    let body_fields = crate::document_body::body_fields(bytes);
91    if let Some(body_json) = crate::document_body::decode_container_to_json(bytes) {
92        if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
93            fields[body_idx].1 = Value::Json(json_bytes);
94        }
95    }
96    let Some(body_fields) = body_fields else {
97        return;
98    };
99    for (name, value) in body_fields {
100        if fields.iter().all(|(existing, _)| existing != &name) {
101            fields.push((name, value));
102        }
103    }
104}
105
106fn ensure_collection_model_contract(
107    db: &crate::storage::unified::devx::RedDB,
108    collection: &str,
109    requested_model: crate::catalog::CollectionModel,
110) -> RedDBResult<()> {
111    if let Some(contract) = db.collection_contract(collection) {
112        if !contract_enforces_model(&contract) {
113            return Ok(());
114        }
115        if collection_model_allows(contract.declared_model, requested_model) {
116            return Ok(());
117        }
118        // A collection that declares an EMBED policy (#1271/#1272) — or a
119        // VISION policy with an image-embedding output (#1275) — holds its
120        // rows' enrichment vectors in place: the CDC enrichment consumer
121        // attaches vectors into the same collection so `VECTOR SEARCH` over
122        // it surfaces the enriched rows. Permit vector writes on such a
123        // collection even when it is otherwise a strict table.
124        if requested_model == crate::catalog::CollectionModel::Vector
125            && contract
126                .ai_policy
127                .as_ref()
128                .is_some_and(|policy| policy.embed.is_some() || policy.vision.is_some())
129        {
130            return Ok(());
131        }
132        return Err(crate::RedDBError::InvalidOperation(format!(
133            "collection '{}' is declared as '{}' and does not allow '{}' writes",
134            collection,
135            collection_model_name(contract.declared_model),
136            collection_model_name(requested_model)
137        )));
138    }
139
140    let now = implicit_contract_unix_ms();
141    db.save_collection_contract(crate::physical::CollectionContract {
142        name: collection.to_string(),
143        declared_model: requested_model,
144        schema_mode: crate::catalog::SchemaMode::Dynamic,
145        origin: crate::physical::ContractOrigin::Implicit,
146        version: 1,
147        created_at_unix_ms: now,
148        updated_at_unix_ms: now,
149        default_ttl_ms: db.collection_default_ttl_ms(collection),
150        vector_dimension: None,
151        vector_metric: None,
152        context_index_fields: Vec::new(),
153        declared_columns: Vec::new(),
154        table_def: matches!(requested_model, crate::catalog::CollectionModel::Table)
155            .then(|| crate::storage::schema::TableDef::new(collection.to_string())),
156        timestamps_enabled: false,
157        context_index_enabled: false,
158        metrics_raw_retention_ms: None,
159        metrics_rollup_policies: Vec::new(),
160        metrics_tenant_identity: None,
161        metrics_namespace: None,
162        // Implicit contracts are created on first write — mutability
163        // is the default until the operator runs explicit DDL.
164        append_only: false,
165        subscriptions: Vec::new(),
166        analytics_config: Vec::new(),
167        session_key: None,
168        session_gap_ms: None,
169        retention_duration_ms: None,
170        analytical_storage: None,
171
172        ai_policy: None,
173    })
174    .map(|_| ())
175    .map_err(|err| crate::RedDBError::Internal(err.to_string()))
176}
177
178fn implicit_contract_unix_ms() -> u128 {
179    std::time::SystemTime::now()
180        .duration_since(std::time::UNIX_EPOCH)
181        .unwrap_or_default()
182        .as_millis()
183}
184
185fn collection_model_allows(
186    declared_model: crate::catalog::CollectionModel,
187    requested_model: crate::catalog::CollectionModel,
188) -> bool {
189    declared_model == requested_model || declared_model == crate::catalog::CollectionModel::Mixed
190}
191
192fn ensure_vector_dimension_contract(
193    db: &crate::storage::unified::devx::RedDB,
194    collection: &str,
195    actual_dimension: usize,
196) -> RedDBResult<()> {
197    let Some(expected_dimension) = db
198        .collection_contract(collection)
199        .and_then(|contract| contract.vector_dimension)
200    else {
201        return Ok(());
202    };
203    if expected_dimension == actual_dimension {
204        return Ok(());
205    }
206    Err(crate::RedDBError::Query(format!(
207        "vector dimension mismatch for collection '{collection}': expected {expected_dimension}, got {actual_dimension}"
208    )))
209}
210
211fn collection_model_name(model: crate::catalog::CollectionModel) -> &'static str {
212    match model {
213        crate::catalog::CollectionModel::Table => "table",
214        crate::catalog::CollectionModel::Document => "document",
215        crate::catalog::CollectionModel::Graph => "graph",
216        crate::catalog::CollectionModel::Vector => "vector",
217        crate::catalog::CollectionModel::Hll => "hll",
218        crate::catalog::CollectionModel::Sketch => "sketch",
219        crate::catalog::CollectionModel::Filter => "filter",
220        crate::catalog::CollectionModel::Kv => "kv",
221        crate::catalog::CollectionModel::Config => "config",
222        crate::catalog::CollectionModel::Vault => "vault",
223        crate::catalog::CollectionModel::Mixed => "mixed",
224        crate::catalog::CollectionModel::TimeSeries => "timeseries",
225        crate::catalog::CollectionModel::Queue => "queue",
226        crate::catalog::CollectionModel::Metrics => "metrics",
227    }
228}
229
230#[derive(Clone)]
231struct UniquenessRule {
232    name: String,
233    columns: Vec<String>,
234    primary_key: bool,
235}
236
237#[derive(Copy, Clone, PartialEq, Eq)]
238enum NormalizeMode {
239    /// First write for this row. Timestamps auto-filled from now on
240    /// both `created_at` and `updated_at`; user attempts to set
241    /// either column are rejected.
242    Insert,
243    /// Update/patch path. `created_at` is preserved from the existing
244    /// row (immutable after insert); `updated_at` is bumped to now.
245    /// User attempts to set either via the patch are rejected.
246    Update,
247}
248
249mod collection_contract_enforcement {
250    use super::*;
251
252    pub(super) struct CollectionContractWriteEnforcer<'a> {
253        db: &'a crate::storage::unified::devx::RedDB,
254        collection: &'a str,
255    }
256
257    impl<'a> CollectionContractWriteEnforcer<'a> {
258        pub(super) fn new(
259            db: &'a crate::storage::unified::devx::RedDB,
260            collection: &'a str,
261        ) -> Self {
262            Self { db, collection }
263        }
264
265        pub(super) fn ensure_model(
266            &self,
267            requested_model: crate::catalog::CollectionModel,
268        ) -> RedDBResult<()> {
269            ensure_collection_model_contract(self.db, self.collection, requested_model)
270        }
271
272        pub(super) fn normalize_insert_fields(
273            &self,
274            fields: Vec<(String, Value)>,
275        ) -> RedDBResult<Vec<(String, Value)>> {
276            normalize_row_fields_for_contract_with_mode(
277                self.db,
278                self.collection,
279                fields,
280                NormalizeMode::Insert,
281            )
282        }
283
284        pub(super) fn normalize_update_fields(
285            &self,
286            fields: Vec<(String, Value)>,
287        ) -> RedDBResult<Vec<(String, Value)>> {
288            normalize_row_fields_for_contract_with_mode(
289                self.db,
290                self.collection,
291                fields,
292                NormalizeMode::Update,
293            )
294        }
295
296        pub(super) fn enforce_row_uniqueness(
297            &self,
298            fields: &[(String, Value)],
299            exclude_id: Option<crate::storage::EntityId>,
300        ) -> RedDBResult<()> {
301            enforce_row_uniqueness(self.db, self.collection, fields, exclude_id)
302        }
303
304        pub(super) fn enforce_batch_uniqueness(
305            &self,
306            rows: &[Vec<(String, Value)>],
307        ) -> RedDBResult<()> {
308            enforce_row_batch_uniqueness(self.db, self.collection, rows)
309        }
310
311        pub(super) fn requires_uniqueness_check(&self, modified_columns: &[String]) -> bool {
312            row_update_requires_uniqueness_check(self.db, self.collection, modified_columns)
313        }
314    }
315}
316
317use collection_contract_enforcement::CollectionContractWriteEnforcer;
318
319fn normalize_row_fields_for_contract_with_mode(
320    db: &crate::storage::unified::devx::RedDB,
321    collection: &str,
322    fields: Vec<(String, Value)>,
323    mode: NormalizeMode,
324) -> RedDBResult<Vec<(String, Value)>> {
325    let Some(contract) = db.collection_contract(collection) else {
326        return Ok(fields);
327    };
328
329    if contract.declared_model != crate::catalog::CollectionModel::Table
330        || (contract.declared_columns.is_empty()
331            && contract
332                .table_def
333                .as_ref()
334                .map(|table| table.columns.is_empty())
335                .unwrap_or(true))
336    {
337        return Ok(fields);
338    }
339
340    // Capture the pre-normalize value of created_at (if present) so
341    // Update mode can preserve it. Also capture updated_at to detect
342    // user attempts to set it via the patch payload.
343    //
344    // Heuristic for Update mode: if fields ALREADY contains a
345    // `created_at` whose value matches the row's on-disk entity, the
346    // caller is the patch pipeline carrying forward an auto-populated
347    // column — not a user mutation. Allow pass-through in that case,
348    // then restore the original value at the end.
349    let existing_created_at = if contract.timestamps_enabled && mode == NormalizeMode::Update {
350        fields
351            .iter()
352            .find(|(n, _)| n == "created_at")
353            .map(|(_, v)| v.clone())
354    } else {
355        None
356    };
357
358    // Reject user attempts to set runtime-managed timestamp columns.
359    // On Insert we reject any mention; on Update we only reject when
360    // the patch pipeline handed us a NEW value (not the one we
361    // auto-populated during the last insert).
362    if contract.timestamps_enabled && mode == NormalizeMode::Insert {
363        for (name, _) in &fields {
364            if name == "created_at" || name == "updated_at" {
365                return Err(crate::RedDBError::Query(format!(
366                    "collection '{}' manages '{}' automatically — do not set it in INSERT",
367                    collection, name
368                )));
369            }
370        }
371    }
372
373    let mut provided = std::collections::BTreeMap::new();
374    for (name, value) in &fields {
375        provided.insert(name.clone(), value.clone());
376    }
377
378    let resolved_columns = resolved_contract_columns(&contract)?;
379    let declared_names: std::collections::BTreeSet<String> = resolved_columns
380        .iter()
381        .map(|column| column.name.clone())
382        .collect();
383    let unknown_fields: Vec<String> = fields
384        .iter()
385        .filter(|(name, _)| !declared_names.contains(name))
386        .map(|(name, _)| name.clone())
387        .collect();
388    if matches!(contract.schema_mode, crate::catalog::SchemaMode::Strict)
389        && !unknown_fields.is_empty()
390    {
391        return Err(crate::RedDBError::Query(format!(
392            "collection '{}' is strict and does not allow undeclared fields: {}",
393            collection,
394            unknown_fields.join(", ")
395        )));
396    }
397    let mut normalized = Vec::new();
398    let now_ms = current_unix_ms_u64();
399
400    for column in &resolved_columns {
401        match provided.remove(&column.name) {
402            Some(value) => {
403                // Runtime-managed columns on Update: always overwrite
404                // with the runtime's own value (preserved created_at
405                // or fresh updated_at). User mutations are silently
406                // discarded because we reject them earlier.
407                if contract.timestamps_enabled && mode == NormalizeMode::Update {
408                    match column.name.as_str() {
409                        "created_at" => {
410                            normalized.push((
411                                column.name.clone(),
412                                existing_created_at
413                                    .clone()
414                                    .unwrap_or(Value::UnsignedInteger(now_ms)),
415                            ));
416                            continue;
417                        }
418                        "updated_at" => {
419                            normalized.push((column.name.clone(), Value::UnsignedInteger(now_ms)));
420                            continue;
421                        }
422                        _ => {}
423                    }
424                }
425                normalized.push((
426                    column.name.clone(),
427                    normalize_contract_value(collection, column, value)?,
428                ));
429            }
430            None => {
431                // Runtime-managed timestamp columns: auto-fill with now
432                // when the contract opted in. Both get the same value on
433                // first insert so callers can order by either.
434                if contract.timestamps_enabled
435                    && (column.name == "created_at" || column.name == "updated_at")
436                {
437                    normalized.push((column.name.clone(), Value::UnsignedInteger(now_ms)));
438                    continue;
439                }
440                if let Some(default) = &column.default {
441                    normalized.push((
442                        column.name.clone(),
443                        coerce_contract_literal(collection, &column.name, column, default)?,
444                    ));
445                } else if column.not_null {
446                    return Err(crate::RedDBError::Query(format!(
447                        "missing required column '{}' for collection '{}'",
448                        column.name, collection
449                    )));
450                }
451            }
452        }
453    }
454
455    for (name, value) in fields {
456        if !declared_names.contains(&name) {
457            normalized.push((name, value));
458        }
459    }
460
461    Ok(normalized)
462}
463
464fn current_unix_ms_u64() -> u64 {
465    std::time::SystemTime::now()
466        .duration_since(std::time::UNIX_EPOCH)
467        .map(|d| d.as_millis() as u64)
468        .unwrap_or(0)
469}
470
471fn enforce_row_uniqueness(
472    db: &crate::storage::unified::devx::RedDB,
473    collection: &str,
474    fields: &[(String, Value)],
475    exclude_id: Option<crate::storage::EntityId>,
476) -> RedDBResult<()> {
477    let Some(contract) = db.collection_contract(collection) else {
478        return Ok(());
479    };
480    if contract.declared_model != crate::catalog::CollectionModel::Table
481        && contract.declared_model != crate::catalog::CollectionModel::Mixed
482    {
483        return Ok(());
484    }
485
486    let rules = resolved_uniqueness_rules(&contract);
487    if rules.is_empty() {
488        return Ok(());
489    }
490
491    let store = db.store();
492    let Some(manager) = store.get_collection(collection) else {
493        return Ok(());
494    };
495
496    let input_fields: std::collections::BTreeMap<String, Value> = fields.iter().cloned().collect();
497
498    for rule in &rules {
499        let mut expected_signatures = Vec::new();
500        let mut skip_rule = false;
501
502        for column in &rule.columns {
503            match input_fields.get(column) {
504                Some(Value::Null) | None if rule.primary_key => {
505                    return Err(crate::RedDBError::Query(format!(
506                        "primary key '{}' in collection '{}' requires non-null column '{}'",
507                        rule.name, collection, column
508                    )))
509                }
510                Some(Value::Null) | None => {
511                    skip_rule = true;
512                    break;
513                }
514                Some(value) => {
515                    expected_signatures.push((column.clone(), value_signature(value)));
516                }
517            }
518        }
519
520        if skip_rule {
521            continue;
522        }
523
524        for entity in manager.query_all(|_| true) {
525            if exclude_id.map(|id| id == entity.id).unwrap_or(false) {
526                continue;
527            }
528            let Some(existing_fields) = row_fields_from_entity(&entity) else {
529                continue;
530            };
531
532            let duplicate = expected_signatures.iter().all(|(column, expected)| {
533                existing_fields
534                    .get(column)
535                    .map(|value| value_signature(value) == *expected)
536                    .unwrap_or(false)
537            });
538
539            if duplicate {
540                let qualifier = if rule.primary_key {
541                    "primary key"
542                } else {
543                    "unique constraint"
544                };
545                return Err(crate::RedDBError::Query(format!(
546                    "{} '{}' violated on collection '{}' for columns [{}]",
547                    qualifier,
548                    rule.name,
549                    collection,
550                    rule.columns.join(", ")
551                )));
552            }
553        }
554    }
555
556    Ok(())
557}
558
559fn enforce_row_batch_uniqueness(
560    db: &crate::storage::unified::devx::RedDB,
561    collection: &str,
562    rows: &[Vec<(String, Value)>],
563) -> RedDBResult<()> {
564    let Some(contract) = db.collection_contract(collection) else {
565        return Ok(());
566    };
567    if contract.declared_model != crate::catalog::CollectionModel::Table
568        && contract.declared_model != crate::catalog::CollectionModel::Mixed
569    {
570        return Ok(());
571    }
572
573    let rules = resolved_uniqueness_rules(&contract);
574    if rules.is_empty() {
575        return Ok(());
576    }
577
578    for rule in &rules {
579        let mut seen = std::collections::HashMap::<String, usize>::new();
580        for (row_index, fields) in rows.iter().enumerate() {
581            let input_fields: std::collections::BTreeMap<String, Value> =
582                fields.iter().cloned().collect();
583            let mut signatures = Vec::new();
584            let mut skip_rule = false;
585
586            for column in &rule.columns {
587                match input_fields.get(column) {
588                    Some(Value::Null) | None if rule.primary_key => {
589                        return Err(crate::RedDBError::Query(format!(
590                            "primary key '{}' in collection '{}' requires non-null column '{}'",
591                            rule.name, collection, column
592                        )))
593                    }
594                    Some(Value::Null) | None => {
595                        skip_rule = true;
596                        break;
597                    }
598                    Some(value) => signatures.push(format!("{column}={}", value_signature(value))),
599                }
600            }
601
602            if skip_rule {
603                continue;
604            }
605
606            let signature = signatures.join("|");
607            if let Some(previous_index) = seen.insert(signature, row_index) {
608                return Err(crate::RedDBError::Query(format!(
609                    "batch insert violates uniqueness rule '{}' in collection '{}' between rows {} and {}",
610                    rule.name,
611                    collection,
612                    previous_index + 1,
613                    row_index + 1
614                )));
615            }
616        }
617    }
618
619    Ok(())
620}
621
622fn row_update_requires_uniqueness_check(
623    db: &crate::storage::unified::devx::RedDB,
624    collection: &str,
625    modified_columns: &[String],
626) -> bool {
627    if modified_columns.is_empty() {
628        return false;
629    }
630
631    let Some(contract) = db.collection_contract(collection) else {
632        return false;
633    };
634    if contract.declared_model != crate::catalog::CollectionModel::Table
635        && contract.declared_model != crate::catalog::CollectionModel::Mixed
636    {
637        return false;
638    }
639
640    let rules = resolved_uniqueness_rules(&contract);
641    if rules.is_empty() {
642        return false;
643    }
644
645    rules.iter().any(|rule| {
646        rule.columns.iter().any(|column| {
647            modified_columns
648                .iter()
649                .any(|modified| modified.eq_ignore_ascii_case(column))
650        })
651    })
652}
653
654pub(crate) fn build_row_update_contract_plan(
655    db: &crate::storage::unified::devx::RedDB,
656    collection: &str,
657) -> RedDBResult<Option<RowUpdateContractPlan>> {
658    let Some(contract) = db.collection_contract(collection) else {
659        return Ok(None);
660    };
661
662    let declared_rules = if contract.declared_model == crate::catalog::CollectionModel::Table
663        && !(contract.declared_columns.is_empty()
664            && contract
665                .table_def
666                .as_ref()
667                .map(|table| table.columns.is_empty())
668                .unwrap_or(true))
669    {
670        resolved_contract_columns(&contract)?
671            .into_iter()
672            .map(|rule| {
673                (
674                    rule.name.clone(),
675                    RowUpdateColumnRule {
676                        name: rule.name,
677                        data_type: rule.data_type,
678                        data_type_name: rule.data_type_name,
679                        not_null: rule.not_null,
680                        enum_variants: rule.enum_variants,
681                    },
682                )
683            })
684            .collect()
685    } else {
686        HashMap::new()
687    };
688
689    let unique_columns = if matches!(
690        contract.declared_model,
691        crate::catalog::CollectionModel::Table | crate::catalog::CollectionModel::Mixed
692    ) {
693        resolved_uniqueness_rules(&contract)
694            .into_iter()
695            .flat_map(|rule| rule.columns.into_iter())
696            .map(|column| (column, ()))
697            .collect()
698    } else {
699        HashMap::new()
700    };
701
702    Ok(Some(RowUpdateContractPlan {
703        timestamps_enabled: contract.timestamps_enabled,
704        strict_schema: matches!(contract.schema_mode, crate::catalog::SchemaMode::Strict),
705        declared_rules,
706        unique_columns,
707    }))
708}
709
710pub(crate) fn normalize_row_update_assignment_with_plan(
711    collection: &str,
712    column: &str,
713    value: Value,
714    row_contract_plan: Option<&RowUpdateContractPlan>,
715) -> RedDBResult<Value> {
716    let Some(plan) = row_contract_plan else {
717        return Ok(value);
718    };
719
720    if plan.timestamps_enabled && (column == "created_at" || column == "updated_at") {
721        return Err(crate::RedDBError::Query(format!(
722            "collection '{}' manages '{}' automatically — do not set it in UPDATE",
723            collection, column
724        )));
725    }
726
727    if let Some(rule) = plan.declared_rules.get(column) {
728        let rule = ResolvedColumnRule {
729            name: rule.name.clone(),
730            data_type: rule.data_type,
731            data_type_name: rule.data_type_name.clone(),
732            not_null: rule.not_null,
733            default: None,
734            enum_variants: rule.enum_variants.clone(),
735        };
736        normalize_contract_value(collection, &rule, value)
737    } else if plan.strict_schema {
738        Err(crate::RedDBError::Query(format!(
739            "collection '{}' is strict and does not allow undeclared fields: {}",
740            collection, column
741        )))
742    } else {
743        Ok(value)
744    }
745}
746
747pub(crate) fn normalize_row_update_value_for_rule(
748    collection: &str,
749    value: Value,
750    row_rule: Option<&RowUpdateColumnRule>,
751) -> RedDBResult<Value> {
752    let Some(rule) = row_rule else {
753        return Ok(value);
754    };
755
756    let rule = ResolvedColumnRule {
757        name: rule.name.clone(),
758        data_type: rule.data_type,
759        data_type_name: rule.data_type_name.clone(),
760        not_null: rule.not_null,
761        default: None,
762        enum_variants: rule.enum_variants.clone(),
763    };
764    normalize_contract_value(collection, &rule, value)
765}
766
767fn set_row_field(row: &mut crate::storage::unified::entity::RowData, name: &str, value: Value) {
768    if let Some(named) = row.named.as_mut() {
769        named.insert(name.to_string(), value);
770        return;
771    }
772
773    if let Some(schema) = row.schema.as_ref() {
774        if let Some(index) = schema.iter().position(|column| column == name) {
775            if let Some(slot) = row.columns.get_mut(index) {
776                *slot = value;
777                return;
778            }
779        }
780
781        let mut named = HashMap::with_capacity(schema.len().saturating_add(1));
782        for (column, current) in schema.iter().zip(row.columns.iter()) {
783            named.insert(column.clone(), current.clone());
784        }
785        named.insert(name.to_string(), value);
786        row.named = Some(named);
787        return;
788    }
789
790    let mut named = HashMap::with_capacity(1);
791    named.insert(name.to_string(), value);
792    row.named = Some(named);
793}
794
795fn collect_row_fields(row: &crate::storage::unified::entity::RowData) -> Vec<(String, Value)> {
796    if let Some(named) = row.named.as_ref() {
797        named
798            .iter()
799            .map(|(key, value)| (key.clone(), value.clone()))
800            .collect()
801    } else if let Some(schema) = row.schema.as_ref() {
802        schema
803            .iter()
804            .cloned()
805            .zip(row.columns.iter().cloned())
806            .collect()
807    } else {
808        Vec::new()
809    }
810}
811
812fn apply_row_field_assignments_raw<I>(
813    row: &mut crate::storage::unified::entity::RowData,
814    field_assignments: I,
815) where
816    I: IntoIterator<Item = (String, Value)>,
817{
818    for (column, value) in field_assignments {
819        set_row_field(row, &column, value);
820    }
821}
822
823fn apply_row_field_assignments_incremental<I>(
824    collection: &str,
825    row: &mut crate::storage::unified::entity::RowData,
826    field_assignments: I,
827    row_contract_plan: Option<&RowUpdateContractPlan>,
828) -> RedDBResult<()>
829where
830    I: IntoIterator<Item = (String, Value)>,
831{
832    for (column, value) in field_assignments {
833        let value = normalize_row_update_assignment_with_plan(
834            collection,
835            &column,
836            value,
837            row_contract_plan,
838        )?;
839
840        set_row_field(row, &column, value);
841    }
842
843    Ok(())
844}
845
846fn resolved_uniqueness_rules(
847    contract: &crate::physical::CollectionContract,
848) -> Vec<UniquenessRule> {
849    let mut rules = Vec::new();
850
851    if let Some(table_def) = &contract.table_def {
852        if !table_def.primary_key.is_empty() {
853            rules.push(UniquenessRule {
854                name: "primary_key".to_string(),
855                columns: table_def.primary_key.clone(),
856                primary_key: true,
857            });
858        }
859
860        for constraint in &table_def.constraints {
861            if matches!(
862                constraint.constraint_type,
863                crate::storage::schema::ConstraintType::PrimaryKey
864            ) && !constraint.columns.is_empty()
865            {
866                rules.push(UniquenessRule {
867                    name: constraint.name.clone(),
868                    columns: constraint.columns.clone(),
869                    primary_key: true,
870                });
871            } else if matches!(
872                constraint.constraint_type,
873                crate::storage::schema::ConstraintType::Unique
874            ) && !constraint.columns.is_empty()
875            {
876                rules.push(UniquenessRule {
877                    name: constraint.name.clone(),
878                    columns: constraint.columns.clone(),
879                    primary_key: false,
880                });
881            }
882        }
883    } else {
884        for column in &contract.declared_columns {
885            if column.primary_key {
886                rules.push(UniquenessRule {
887                    name: format!("pk_{}", column.name),
888                    columns: vec![column.name.clone()],
889                    primary_key: true,
890                });
891            } else if column.unique {
892                rules.push(UniquenessRule {
893                    name: format!("uniq_{}", column.name),
894                    columns: vec![column.name.clone()],
895                    primary_key: false,
896                });
897            }
898        }
899    }
900
901    let mut dedup = std::collections::BTreeSet::new();
902    rules
903        .into_iter()
904        .filter(|rule| dedup.insert((rule.primary_key, rule.columns.clone())))
905        .collect()
906}
907
908fn row_fields_from_entity(
909    entity: &crate::storage::UnifiedEntity,
910) -> Option<std::collections::BTreeMap<String, Value>> {
911    match &entity.data {
912        crate::storage::EntityData::Row(row) => {
913            if let Some(named) = &row.named {
914                Some(
915                    named
916                        .iter()
917                        .map(|(key, value)| (key.clone(), value.clone()))
918                        .collect(),
919                )
920            } else {
921                row.schema.as_ref().map(|schema| {
922                    schema
923                        .iter()
924                        .cloned()
925                        .zip(row.columns.iter().cloned())
926                        .collect()
927                })
928            }
929        }
930        _ => None,
931    }
932}
933
934fn value_signature(value: &Value) -> String {
935    format!("{value:?}")
936}
937
938fn normalize_contract_value(
939    collection: &str,
940    column: &ResolvedColumnRule,
941    value: Value,
942) -> RedDBResult<Value> {
943    if matches!(value, Value::Null) {
944        if column.not_null {
945            return Err(crate::RedDBError::Query(format!(
946                "column '{}' in collection '{}' cannot be null",
947                column.name, collection
948            )));
949        }
950        return Ok(Value::Null);
951    }
952
953    let target = column.data_type;
954    if value_matches_declared_type(&value, target) {
955        return Ok(value);
956    }
957
958    let Some(raw) = value_to_coercion_input(&value) else {
959        return Err(crate::RedDBError::Query(format!(
960            "column '{}' in collection '{}' requires type '{}' but value cannot be coerced",
961            column.name, collection, column.data_type_name
962        )));
963    };
964
965    coerce_contract_literal(collection, &column.name, column, &raw)
966}
967
968fn coerce_contract_literal(
969    collection: &str,
970    column_name: &str,
971    column: &ResolvedColumnRule,
972    raw: &str,
973) -> RedDBResult<Value> {
974    let target = column.data_type;
975    match target {
976        DataType::Blob => Ok(Value::Blob(raw.as_bytes().to_vec())),
977        DataType::Json => Ok(Value::Json(raw.as_bytes().to_vec())),
978        DataType::Timestamp => raw.parse::<i64>().map(Value::Timestamp).map_err(|err| {
979            crate::RedDBError::Query(format!(
980                "failed to coerce column '{}' in collection '{}' to '{}': {}",
981                column_name, collection, column.data_type_name, err
982            ))
983        }),
984        DataType::Duration => raw.parse::<i64>().map(Value::Duration).map_err(|err| {
985            crate::RedDBError::Query(format!(
986                "failed to coerce column '{}' in collection '{}' to '{}': {}",
987                column_name, collection, column.data_type_name, err
988            ))
989        }),
990        DataType::Vector | DataType::Array => Err(crate::RedDBError::Query(format!(
991            "column '{}' in collection '{}' requires '{}' and only typed values are accepted for this type",
992            column_name, collection, column.data_type_name
993        ))),
994        _ => coerce_schema_value(raw, target, Some(column.enum_variants.as_slice())).map_err(
995            |err| {
996                crate::RedDBError::Query(format!(
997                    "failed to coerce column '{}' in collection '{}' to '{}': {}",
998                    column_name, collection, column.data_type_name, err
999                ))
1000            },
1001        ),
1002    }
1003}
1004
1005struct ResolvedColumnRule {
1006    name: String,
1007    data_type: DataType,
1008    data_type_name: String,
1009    not_null: bool,
1010    default: Option<String>,
1011    enum_variants: Vec<String>,
1012}
1013
1014fn resolved_contract_columns(
1015    contract: &crate::physical::CollectionContract,
1016) -> RedDBResult<Vec<ResolvedColumnRule>> {
1017    if let Some(table_def) = &contract.table_def {
1018        return Ok(table_def
1019            .columns
1020            .iter()
1021            .map(|column| ResolvedColumnRule {
1022                name: column.name.clone(),
1023                data_type: column.data_type,
1024                data_type_name: data_type_name(column.data_type).to_string(),
1025                not_null: !column.nullable,
1026                default: column
1027                    .default
1028                    .as_ref()
1029                    .map(|bytes| String::from_utf8_lossy(bytes).to_string()),
1030                enum_variants: column.enum_variants.clone(),
1031            })
1032            .collect());
1033    }
1034
1035    contract
1036        .declared_columns
1037        .iter()
1038        .map(|column| {
1039            let data_type = column
1040                .sql_type
1041                .as_ref()
1042                .map(crate::storage::query::resolve_sql_type_name)
1043                .transpose()
1044                .map_err(|err| crate::RedDBError::Query(err.to_string()))?
1045                .unwrap_or(parse_declared_data_type(&column.data_type)?);
1046            Ok(ResolvedColumnRule {
1047                name: column.name.clone(),
1048                data_type,
1049                data_type_name: column.data_type.clone(),
1050                not_null: column.not_null,
1051                default: column.default.clone(),
1052                enum_variants: column.enum_variants.clone(),
1053            })
1054        })
1055        .collect()
1056}
1057
1058fn parse_declared_data_type(value: &str) -> RedDBResult<DataType> {
1059    resolve_declared_data_type(value).map_err(|err| crate::RedDBError::Query(err.to_string()))
1060}
1061
1062fn data_type_name(data_type: DataType) -> &'static str {
1063    match data_type {
1064        DataType::Integer => "integer",
1065        DataType::UnsignedInteger => "unsigned_integer",
1066        DataType::Float => "float",
1067        DataType::Text => "text",
1068        DataType::Blob => "blob",
1069        DataType::Boolean => "boolean",
1070        DataType::Timestamp => "timestamp",
1071        DataType::Duration => "duration",
1072        DataType::IpAddr => "ipaddr",
1073        DataType::MacAddr => "macaddr",
1074        DataType::Vector => "vector",
1075        DataType::Nullable => "nullable",
1076        DataType::Unknown => "unknown",
1077        DataType::Json => "json",
1078        DataType::Uuid => "uuid",
1079        DataType::NodeRef => "noderef",
1080        DataType::EdgeRef => "edgeref",
1081        DataType::VectorRef => "vectorref",
1082        DataType::RowRef => "rowref",
1083        DataType::Color => "color",
1084        DataType::Email => "email",
1085        DataType::Url => "url",
1086        DataType::Phone => "phone",
1087        DataType::Semver => "semver",
1088        DataType::Cidr => "cidr",
1089        DataType::Date => "date",
1090        DataType::Time => "time",
1091        DataType::Decimal => "decimal",
1092        DataType::Enum => "enum",
1093        DataType::Array => "array",
1094        DataType::TimestampMs => "timestamp_ms",
1095        DataType::Ipv4 => "ipv4",
1096        DataType::Ipv6 => "ipv6",
1097        DataType::Subnet => "subnet",
1098        DataType::Port => "port",
1099        DataType::Latitude => "latitude",
1100        DataType::Longitude => "longitude",
1101        DataType::GeoPoint => "geopoint",
1102        DataType::Country2 => "country2",
1103        DataType::Country3 => "country3",
1104        DataType::Lang2 => "lang2",
1105        DataType::Lang5 => "lang5",
1106        DataType::Currency => "currency",
1107        DataType::AssetCode => "asset_code",
1108        DataType::Money => "money",
1109        DataType::ColorAlpha => "color_alpha",
1110        DataType::BigInt => "bigint",
1111        DataType::KeyRef => "keyref",
1112        DataType::DocRef => "docref",
1113        DataType::TableRef => "tableref",
1114        DataType::PageRef => "pageref",
1115        DataType::Secret => "secret",
1116        DataType::Password => "password",
1117        DataType::TextZstd => "text",
1118        DataType::BlobZstd => "blob",
1119    }
1120}
1121
1122fn value_matches_declared_type(value: &Value, target: DataType) -> bool {
1123    matches!(
1124        (value, target),
1125        (Value::Null, _)
1126            | (Value::Integer(_), DataType::Integer)
1127            | (Value::UnsignedInteger(_), DataType::UnsignedInteger)
1128            | (Value::Float(_), DataType::Float)
1129            | (Value::Text(_), DataType::Text)
1130            | (Value::Blob(_), DataType::Blob)
1131            | (Value::Boolean(_), DataType::Boolean)
1132            | (Value::Timestamp(_), DataType::Timestamp)
1133            | (Value::Duration(_), DataType::Duration)
1134            | (Value::IpAddr(_), DataType::IpAddr)
1135            | (Value::MacAddr(_), DataType::MacAddr)
1136            | (Value::Vector(_), DataType::Vector)
1137            | (Value::Json(_), DataType::Json)
1138            | (Value::Uuid(_), DataType::Uuid)
1139            | (Value::NodeRef(_), DataType::NodeRef)
1140            | (Value::EdgeRef(_), DataType::EdgeRef)
1141            | (Value::VectorRef(_, _), DataType::VectorRef)
1142            | (Value::RowRef(_, _), DataType::RowRef)
1143            | (Value::Color(_), DataType::Color)
1144            | (Value::Email(_), DataType::Email)
1145            | (Value::Url(_), DataType::Url)
1146            | (Value::Phone(_), DataType::Phone)
1147            | (Value::Semver(_), DataType::Semver)
1148            | (Value::Cidr(_, _), DataType::Cidr)
1149            | (Value::Date(_), DataType::Date)
1150            | (Value::Time(_), DataType::Time)
1151            | (Value::Decimal(_), DataType::Decimal)
1152            | (Value::EnumValue(_), DataType::Enum)
1153            | (Value::Array(_), DataType::Array)
1154            | (Value::TimestampMs(_), DataType::TimestampMs)
1155            | (Value::Ipv4(_), DataType::Ipv4)
1156            | (Value::Ipv6(_), DataType::Ipv6)
1157            | (Value::Subnet(_, _), DataType::Subnet)
1158            | (Value::Port(_), DataType::Port)
1159            | (Value::Latitude(_), DataType::Latitude)
1160            | (Value::Longitude(_), DataType::Longitude)
1161            | (Value::GeoPoint(_, _), DataType::GeoPoint)
1162            | (Value::Country2(_), DataType::Country2)
1163            | (Value::Country3(_), DataType::Country3)
1164            | (Value::Lang2(_), DataType::Lang2)
1165            | (Value::Lang5(_), DataType::Lang5)
1166            | (Value::Currency(_), DataType::Currency)
1167            | (Value::ColorAlpha(_), DataType::ColorAlpha)
1168            | (Value::BigInt(_), DataType::BigInt)
1169            | (Value::KeyRef(_, _), DataType::KeyRef)
1170            | (Value::DocRef(_, _), DataType::DocRef)
1171            | (Value::TableRef(_), DataType::TableRef)
1172            | (Value::PageRef(_), DataType::PageRef)
1173            | (Value::Secret(_), DataType::Secret)
1174            | (Value::Password(_), DataType::Password)
1175    )
1176}
1177
1178fn value_to_coercion_input(value: &Value) -> Option<String> {
1179    match value {
1180        Value::Null => None,
1181        Value::Integer(value) => Some(value.to_string()),
1182        Value::UnsignedInteger(value) => Some(value.to_string()),
1183        Value::Float(value) => Some(value.to_string()),
1184        Value::Text(value) => Some(value.to_string()),
1185        Value::Blob(value) => String::from_utf8(value.clone()).ok(),
1186        Value::Boolean(value) => Some(value.to_string()),
1187        Value::Timestamp(value) => Some(value.to_string()),
1188        Value::Duration(value) => Some(value.to_string()),
1189        Value::IpAddr(value) => Some(value.to_string()),
1190        Value::MacAddr(value) => Some(format!(
1191            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
1192            value[0], value[1], value[2], value[3], value[4], value[5]
1193        )),
1194        Value::Json(value) => Some(String::from_utf8_lossy(value).to_string()),
1195        Value::Email(value) => Some(value.clone()),
1196        Value::Url(value) => Some(value.clone()),
1197        Value::Phone(value) => Some(value.to_string()),
1198        Value::Semver(value) => Some(format!(
1199            "{}.{}.{}",
1200            value / 1_000_000,
1201            (value / 1_000) % 1_000,
1202            value % 1_000
1203        )),
1204        Value::Date(value) => Some(value.to_string()),
1205        Value::Time(value) => Some(value.to_string()),
1206        Value::Decimal(value) => Some(value.to_string()),
1207        Value::TimestampMs(value) => Some(value.to_string()),
1208        Value::Ipv4(value) => Some(format!(
1209            "{}.{}.{}.{}",
1210            (value >> 24) & 0xFF,
1211            (value >> 16) & 0xFF,
1212            (value >> 8) & 0xFF,
1213            value & 0xFF
1214        )),
1215        Value::Port(value) => Some(value.to_string()),
1216        Value::Latitude(value) => Some((*value as f64 / 1_000_000.0).to_string()),
1217        Value::Longitude(value) => Some((*value as f64 / 1_000_000.0).to_string()),
1218        Value::GeoPoint(lat, lon) => Some(format!(
1219            "{},{}",
1220            *lat as f64 / 1_000_000.0,
1221            *lon as f64 / 1_000_000.0
1222        )),
1223        Value::BigInt(value) => Some(value.to_string()),
1224        Value::TableRef(value) => Some(value.clone()),
1225        Value::PageRef(value) => Some(value.to_string()),
1226        Value::Password(value) => Some(value.clone()),
1227        _ => None,
1228    }
1229}
1230
1231fn dedupe_modified_columns(mut modified_columns: Vec<String>) -> Vec<String> {
1232    if modified_columns.is_empty() {
1233        return modified_columns;
1234    }
1235
1236    let mut unique = Vec::with_capacity(modified_columns.len());
1237    for column in modified_columns.drain(..) {
1238        if !unique
1239            .iter()
1240            .any(|existing: &String| existing.eq_ignore_ascii_case(&column))
1241        {
1242            unique.push(column);
1243        }
1244    }
1245    unique
1246}
1247
1248fn reject_document_array_position_path(path: &[String]) -> RedDBResult<()> {
1249    if path.iter().any(|segment| segment.parse::<usize>().is_ok()) {
1250        return Err(crate::RedDBError::Query(
1251            "array positional document patch paths are unsupported; replace the array or full document body instead"
1252                .to_string(),
1253        ));
1254    }
1255    Ok(())
1256}
1257
1258fn document_body_from_named(fields: &HashMap<String, Value>) -> RedDBResult<JsonValue> {
1259    match fields.get("body") {
1260        Some(Value::Json(bytes)) => {
1261            // The stored body may be the native binary container (PRD-1398) or
1262            // plain JSON; decode either form back to JSON.
1263            if let Some(body) = crate::document_body::decode_container_to_json(bytes) {
1264                Ok(body)
1265            } else {
1266                crate::json::from_slice(bytes).map_err(|err| {
1267                    crate::RedDBError::Query(format!("failed to decode document body: {err}"))
1268                })
1269            }
1270        }
1271        Some(_) => Err(crate::RedDBError::Query(
1272            "document body field must contain JSON".to_string(),
1273        )),
1274        None => Ok(JsonValue::Object(Default::default())),
1275    }
1276}
1277
1278fn document_body_from_assignment(value: &Value) -> RedDBResult<JsonValue> {
1279    match value {
1280        Value::Json(bytes) | Value::Blob(bytes) => crate::json::from_slice(bytes)
1281            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1282        Value::Text(text) => crate::json::from_str(text.as_ref())
1283            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1284        Value::Integer(value) => crate::json::from_str(&value.to_string())
1285            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1286        Value::UnsignedInteger(value) => crate::json::from_str(&value.to_string())
1287            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1288        Value::Float(value) => crate::json::from_str(&value.to_string())
1289            .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1290        other => Err(crate::RedDBError::Query(format!(
1291            "column 'body' expected JSON body, got {other:?}"
1292        ))),
1293    }
1294}
1295
1296fn document_body_set_operation(column: &str, value: Value) -> PatchEntityOperation {
1297    PatchEntityOperation {
1298        op: PatchEntityOperationType::Set,
1299        path: column.split('.').map(str::to_string).collect(),
1300        value: Some(crate::presentation::entity_json::storage_value_to_json(
1301            &value,
1302        )),
1303    }
1304}
1305
1306fn replace_document_fields_body(
1307    fields: &mut HashMap<String, Value>,
1308    body: JsonValue,
1309    binary: bool,
1310    modified_columns: &mut Vec<String>,
1311) -> RedDBResult<()> {
1312    modified_columns.push("body".to_string());
1313
1314    let old_keys: Vec<String> = fields
1315        .keys()
1316        .filter(|key| key.as_str() != "body")
1317        .cloned()
1318        .collect();
1319    for key in old_keys {
1320        fields.remove(&key);
1321        modified_columns.push(key);
1322    }
1323
1324    let body_bytes = crate::document_body::serialize_document_body(&body, binary)?;
1325    fields.insert("body".to_string(), Value::Json(body_bytes));
1326
1327    Ok(())
1328}
1329
1330fn ensure_no_reserved_document_body_fields(body: &JsonValue, collection: &str) -> RedDBResult<()> {
1331    if let JsonValue::Object(map) = body {
1332        crate::reserved_fields::ensure_no_reserved_public_item_fields(
1333            map.keys().map(String::as_str),
1334            &format!("document '{collection}'"),
1335        )?;
1336    }
1337    Ok(())
1338}
1339
1340fn replace_document_row_body(
1341    row: &mut crate::storage::unified::entity::RowData,
1342    body: JsonValue,
1343    collection: &str,
1344    binary: bool,
1345    modified_columns: &mut Vec<String>,
1346) -> RedDBResult<()> {
1347    ensure_no_reserved_document_body_fields(&body, collection)?;
1348    let fields = row.named.get_or_insert_with(Default::default);
1349    replace_document_fields_body(fields, body, binary, modified_columns)?;
1350
1351    // Full body replacement rebuilds the document from the canonical `body`.
1352    // Stale positional/schema values would otherwise leak back through JSON
1353    // presentation even after the named map was pruned.
1354    row.columns.clear();
1355    row.schema = None;
1356    Ok(())
1357}
1358
1359fn document_merge_patch_payload(payload: &JsonValue) -> &JsonValue {
1360    if let JsonValue::Object(object) = payload {
1361        if object.len() == 1 {
1362            if let Some(body) = object.get("body") {
1363                return body;
1364            }
1365        }
1366    }
1367    payload
1368}
1369
1370fn apply_document_json_merge_patch(body: &mut JsonValue, patch: &JsonValue) -> RedDBResult<()> {
1371    if !matches!(patch, JsonValue::Object(_)) {
1372        *body = patch.clone();
1373        return Ok(());
1374    }
1375
1376    if matches!(patch, JsonValue::Object(object) if object.is_empty()) {
1377        if !matches!(body, JsonValue::Object(_)) {
1378            *body = JsonValue::Object(Default::default());
1379        }
1380        return Ok(());
1381    }
1382
1383    let mut operations = Vec::new();
1384    collect_document_json_merge_patch_operations(body, Vec::new(), patch, &mut operations);
1385    apply_patch_operations_to_json(body, &operations).map_err(crate::RedDBError::Query)
1386}
1387
1388fn collect_document_json_merge_patch_operations(
1389    target: &JsonValue,
1390    path: Vec<String>,
1391    patch: &JsonValue,
1392    operations: &mut Vec<PatchEntityOperation>,
1393) {
1394    let JsonValue::Object(patch_object) = patch else {
1395        operations.push(PatchEntityOperation {
1396            op: PatchEntityOperationType::Set,
1397            path,
1398            value: Some(patch.clone()),
1399        });
1400        return;
1401    };
1402
1403    for (key, value) in patch_object {
1404        let mut child_path = path.clone();
1405        child_path.push(key.clone());
1406        match value {
1407            JsonValue::Null => operations.push(PatchEntityOperation {
1408                op: PatchEntityOperationType::Unset,
1409                path: child_path,
1410                value: None,
1411            }),
1412            JsonValue::Object(object) if object.is_empty() => {
1413                let child_target = match target {
1414                    JsonValue::Object(target_object) => target_object.get(key),
1415                    _ => None,
1416                };
1417                if !matches!(child_target, Some(JsonValue::Object(_))) {
1418                    operations.push(PatchEntityOperation {
1419                        op: PatchEntityOperationType::Set,
1420                        path: child_path,
1421                        value: Some(JsonValue::Object(Default::default())),
1422                    });
1423                }
1424            }
1425            JsonValue::Object(_) => {
1426                let child_target = match target {
1427                    JsonValue::Object(target_object) => target_object.get(key),
1428                    _ => None,
1429                }
1430                .unwrap_or(&JsonValue::Null);
1431                collect_document_json_merge_patch_operations(
1432                    child_target,
1433                    child_path,
1434                    value,
1435                    operations,
1436                );
1437            }
1438            _ => operations.push(PatchEntityOperation {
1439                op: PatchEntityOperationType::Set,
1440                path: child_path,
1441                value: Some(value.clone()),
1442            }),
1443        }
1444    }
1445}
1446
1447impl RedDBRuntime {
1448    pub(crate) fn apply_loaded_patch_entity_core(
1449        &self,
1450        collection: String,
1451        mut entity: crate::storage::UnifiedEntity,
1452        payload: JsonValue,
1453        operations: Vec<PatchEntityOperation>,
1454    ) -> RedDBResult<AppliedEntityMutation> {
1455        let id = entity.id;
1456        let previous_xmax = entity.xmax;
1457        let operations = normalize_ttl_patch_operations(operations)?;
1458        // Snapshot pre-patch row fields for the secondary-index hook —
1459        // empty for non-row entities, which is the desired no-op.
1460        let pre_mutation_fields = entity_row_fields_snapshot(&entity);
1461
1462        // Versioned collections retain MVCC history: a PATCH/UPDATE must
1463        // produce a NEW full version (the merged document/row/node) with
1464        // the prior version tombstoned, mirroring the SQL UPDATE row
1465        // path. Gated STRICTLY on the collection `versioned` flag —
1466        // non-versioned collections keep last-writer-wins in-place
1467        // mutation. Row entities (documents / table rows / KV) carry
1468        // logical-id MVCC history (Phase 1/2); graph nodes & edges join
1469        // in Phase 3. The post-image re-stamp (new physical id, same
1470        // logical_id, fresh xmin) at the tail of this function is fully
1471        // kind-agnostic, so the same machinery installs a versioned
1472        // graph-node update.
1473        let patch_versions = matches!(
1474            entity.data,
1475            crate::storage::EntityData::Row(_)
1476                | crate::storage::EntityData::Node(_)
1477                | crate::storage::EntityData::Edge(_)
1478        ) && self.vcs_is_versioned(&collection).unwrap_or(false);
1479        let versioned_update_xid = if patch_versions {
1480            match self.current_xid() {
1481                Some(xid) => Some(xid),
1482                None => {
1483                    let snapshot_manager = self.snapshot_manager();
1484                    let xid = snapshot_manager.begin();
1485                    snapshot_manager.commit(xid);
1486                    Some(xid)
1487                }
1488            }
1489        } else {
1490            None
1491        };
1492        let mut replaced_entity = versioned_update_xid.map(|xid| {
1493            let mut old = entity.clone();
1494            old.set_xmax(xid);
1495            old
1496        });
1497
1498        let db = self.db();
1499        let store = db.store();
1500        let Some(manager) = store.get_collection(&collection) else {
1501            return Err(crate::RedDBError::NotFound(format!(
1502                "collection not found: {collection}"
1503            )));
1504        };
1505
1506        let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
1507        let mut metadata_changed = false;
1508        let mut modified_columns: Vec<String> = Vec::new();
1509        let mut context_index_dirty = false;
1510        let mut graph_node_type: Option<String> = None;
1511        let mut graph_edge_weight: Option<f32> = None;
1512
1513        let row_contract_timestamps = db
1514            .collection_contract(&collection)
1515            .map(|c| c.timestamps_enabled)
1516            .unwrap_or(false);
1517
1518        match &mut entity.data {
1519            crate::storage::EntityData::Row(row) => {
1520                let is_document_collection = db
1521                    .collection_contract(&collection)
1522                    .map(|contract| {
1523                        contract.declared_model == crate::catalog::CollectionModel::Document
1524                    })
1525                    .unwrap_or(false);
1526                let binary_body = self.binary_document_body_enabled();
1527                let mut field_ops = Vec::new();
1528                let mut metadata_ops = Vec::new();
1529                let mut document_body_ops = Vec::new();
1530                let mut document_body_replace: Option<JsonValue> = None;
1531                let has_patch_operations = !operations.is_empty();
1532
1533                for mut op in operations {
1534                    let Some(root) = op.path.first().map(String::as_str) else {
1535                        return Err(crate::RedDBError::Query(
1536                            "patch path cannot be empty".to_string(),
1537                        ));
1538                    };
1539
1540                    match root {
1541                        "body" if is_document_collection => {
1542                            if op.path.len() == 1 {
1543                                match op.op {
1544                                    PatchEntityOperationType::Set
1545                                    | PatchEntityOperationType::Replace => {
1546                                        let Some(value) = op.value.take() else {
1547                                            return Err(crate::RedDBError::Query(
1548                                                "document body replacement requires a value"
1549                                                    .to_string(),
1550                                            ));
1551                                        };
1552                                        document_body_replace = Some(value);
1553                                    }
1554                                    PatchEntityOperationType::Unset => {
1555                                        return Err(crate::RedDBError::Query(
1556                                            "document body cannot be unset; replace it instead"
1557                                                .to_string(),
1558                                        ));
1559                                    }
1560                                }
1561                                continue;
1562                            }
1563                            if op.path.len() < 2 {
1564                                return Err(crate::RedDBError::Query(
1565                                    "document body patch paths require a nested key; use payload.body for full replacement"
1566                                        .to_string(),
1567                                ));
1568                            }
1569                            op.path.remove(0);
1570                            reject_document_array_position_path(&op.path)?;
1571                            document_body_ops.push(op);
1572                        }
1573                        "fields" | "named" => {
1574                            if op.path.len() < 2 {
1575                                return Err(crate::RedDBError::Query(
1576                                    "patch path 'fields' requires a nested key".to_string(),
1577                                ));
1578                            }
1579                            if row_contract_timestamps {
1580                                let leaf = op.path.get(1).map(String::as_str);
1581                                if matches!(leaf, Some("created_at") | Some("updated_at")) {
1582                                    return Err(crate::RedDBError::Query(format!(
1583                                        "collection '{}' manages '{}' automatically — do not set it in UPDATE",
1584                                        collection,
1585                                        leaf.unwrap_or("")
1586                                    )));
1587                                }
1588                            }
1589                            op.path.remove(0);
1590                            field_ops.push(op);
1591                        }
1592                        "metadata" => {
1593                            if op.path.len() < 2 {
1594                                return Err(crate::RedDBError::Query(
1595                                    "patch path 'metadata' requires a nested key".to_string(),
1596                                ));
1597                            }
1598                            op.path.remove(0);
1599                            metadata_ops.push(op);
1600                        }
1601                        _ => {
1602                            return Err(crate::RedDBError::Query(format!(
1603                                "unsupported patch target '{root}' for table rows. Use fields/*, metadata/*, or weight"
1604                            )));
1605                        }
1606                    }
1607                }
1608
1609                if document_body_replace.is_some() || !document_body_ops.is_empty() {
1610                    context_index_dirty = true;
1611                    let mut body = match document_body_replace {
1612                        Some(body) => body,
1613                        None => document_body_from_named(
1614                            row.named.get_or_insert_with(Default::default),
1615                        )?,
1616                    };
1617                    apply_patch_operations_to_json(&mut body, &document_body_ops)
1618                        .map_err(crate::RedDBError::Query)?;
1619                    replace_document_row_body(
1620                        row,
1621                        body,
1622                        &collection,
1623                        binary_body,
1624                        &mut modified_columns,
1625                    )?;
1626                }
1627
1628                if is_document_collection && !has_patch_operations {
1629                    let mut body =
1630                        document_body_from_named(row.named.get_or_insert_with(Default::default))?;
1631                    let previous_body = body.clone();
1632                    apply_document_json_merge_patch(
1633                        &mut body,
1634                        document_merge_patch_payload(&payload),
1635                    )?;
1636                    if body != previous_body {
1637                        context_index_dirty = true;
1638                        replace_document_row_body(
1639                            row,
1640                            body,
1641                            &collection,
1642                            binary_body,
1643                            &mut modified_columns,
1644                        )?;
1645                    }
1646                }
1647
1648                if !field_ops.is_empty() {
1649                    context_index_dirty = true;
1650                    let named = row.named.get_or_insert_with(Default::default);
1651                    if is_document_collection {
1652                        // Document fields are projections of the canonical
1653                        // `body` JSON. Apply the assignment to the body so the
1654                        // stored document remains single-source.
1655                        let mut body = document_body_from_named(named)?;
1656                        apply_patch_operations_to_json(&mut body, &field_ops)
1657                            .map_err(crate::RedDBError::Query)?;
1658                        replace_document_row_body(
1659                            row,
1660                            body,
1661                            &collection,
1662                            binary_body,
1663                            &mut modified_columns,
1664                        )?;
1665                    } else {
1666                        for op in &field_ops {
1667                            if let Some(col) = op.path.first() {
1668                                modified_columns.push(col.clone());
1669                            }
1670                        }
1671                        apply_patch_operations_to_storage_map(named, &field_ops)?;
1672                    }
1673                }
1674
1675                if let Some(fields) = payload
1676                    .get("fields")
1677                    .and_then(crate::json::Value::as_object)
1678                {
1679                    if row_contract_timestamps {
1680                        for key in fields.keys() {
1681                            if key == "created_at" || key == "updated_at" {
1682                                return Err(crate::RedDBError::Query(format!(
1683                                    "collection '{}' manages '{}' automatically — do not set it in UPDATE",
1684                                    collection, key
1685                                )));
1686                            }
1687                        }
1688                    }
1689                    context_index_dirty = true;
1690                    let named = row.named.get_or_insert_with(Default::default);
1691                    if is_document_collection {
1692                        // Mirror the `field_ops` path above: keep the canonical
1693                        // `body` JSON as the single stored representation.
1694                        let mut body = document_body_from_named(named)?;
1695                        if let JsonValue::Object(map) = &mut body {
1696                            for (key, value) in fields {
1697                                map.insert(key.clone(), value.clone());
1698                            }
1699                        }
1700                        replace_document_row_body(
1701                            row,
1702                            body,
1703                            &collection,
1704                            binary_body,
1705                            &mut modified_columns,
1706                        )?;
1707                    } else {
1708                        for (key, value) in fields {
1709                            modified_columns.push(key.clone());
1710                            named.insert(key.clone(), json_to_storage_value(value)?);
1711                        }
1712                    }
1713                }
1714
1715                if !metadata_ops.is_empty() {
1716                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1717                    let metadata = patch_metadata.get_or_insert_with(|| {
1718                        store.get_metadata(&collection, id).unwrap_or_default()
1719                    });
1720                    let mut metadata_json = metadata_to_json(metadata);
1721                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1722                        .map_err(crate::RedDBError::Query)?;
1723                    *metadata = metadata_from_json(&metadata_json)?;
1724                    metadata_changed = true;
1725                }
1726
1727                if !modified_columns.is_empty() || row_contract_timestamps {
1728                    let contract = CollectionContractWriteEnforcer::new(&db, &collection);
1729                    let current_fields = if let Some(named) = row.named.take() {
1730                        named.into_iter().collect::<Vec<_>>()
1731                    } else if let Some(schema) = row.schema.as_ref() {
1732                        schema
1733                            .iter()
1734                            .cloned()
1735                            .zip(row.columns.iter().cloned())
1736                            .collect::<Vec<_>>()
1737                    } else {
1738                        Vec::new()
1739                    };
1740                    let normalized_fields = contract.normalize_update_fields(current_fields)?;
1741                    if row_contract_timestamps {
1742                        modified_columns.push("updated_at".to_string());
1743                        context_index_dirty = true;
1744                    }
1745                    if contract.requires_uniqueness_check(&modified_columns) {
1746                        contract.enforce_row_uniqueness(&normalized_fields, Some(id))?;
1747                    }
1748                    row.named = Some(normalized_fields.into_iter().collect());
1749                }
1750            }
1751            crate::storage::EntityData::Node(node) => {
1752                let mut field_ops = Vec::new();
1753                let mut metadata_ops = Vec::new();
1754                let mut node_type_ops = Vec::new();
1755
1756                for mut op in operations {
1757                    let Some(root) = op.path.first().map(String::as_str) else {
1758                        return Err(crate::RedDBError::Query(
1759                            "patch path cannot be empty".to_string(),
1760                        ));
1761                    };
1762
1763                    match root {
1764                        "fields" | "properties" => {
1765                            if op.path.len() < 2 {
1766                                return Err(crate::RedDBError::Query(
1767                                    "patch path 'fields' requires a nested key".to_string(),
1768                                ));
1769                            }
1770                            op.path.remove(0);
1771                            field_ops.push(op);
1772                        }
1773                        "metadata" => {
1774                            if op.path.len() < 2 {
1775                                return Err(crate::RedDBError::Query(
1776                                    "patch path 'metadata' requires a nested key".to_string(),
1777                                ));
1778                            }
1779                            op.path.remove(0);
1780                            metadata_ops.push(op);
1781                        }
1782                        "node_type" => {
1783                            if op.path.len() != 1 {
1784                                return Err(crate::RedDBError::Query(
1785                                    "patch path 'node_type' does not allow nested keys".to_string(),
1786                                ));
1787                            }
1788                            op.path.clear();
1789                            node_type_ops.push(op);
1790                        }
1791                        _ => {
1792                            return Err(crate::RedDBError::Query(format!(
1793                                "unsupported patch target '{root}' for graph nodes. Use fields/*, properties/*, node_type, or metadata/*"
1794                            )));
1795                        }
1796                    }
1797                }
1798
1799                for op in node_type_ops {
1800                    context_index_dirty = true;
1801                    let value = op.value.ok_or_else(|| {
1802                        crate::RedDBError::Query("node_type operations require a value".to_string())
1803                    })?;
1804
1805                    match op.op {
1806                        PatchEntityOperationType::Unset => {
1807                            return Err(crate::RedDBError::Query(
1808                                "node_type cannot be unset through patch operations".to_string(),
1809                            ));
1810                        }
1811                        PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
1812                            let Some(node_type) = value.as_str() else {
1813                                return Err(crate::RedDBError::Query(
1814                                    "node_type operation requires a text value".to_string(),
1815                                ));
1816                            };
1817                            graph_node_type = Some(node_type.to_string());
1818                            modified_columns.push("node_type".to_string());
1819                        }
1820                    }
1821                }
1822
1823                if !field_ops.is_empty() {
1824                    context_index_dirty = true;
1825                    apply_patch_operations_to_storage_map(&mut node.properties, &field_ops)?;
1826                }
1827
1828                if let Some(fields) = payload
1829                    .get("fields")
1830                    .and_then(crate::json::Value::as_object)
1831                {
1832                    context_index_dirty = true;
1833                    for (key, value) in fields {
1834                        node.properties
1835                            .insert(key.clone(), json_to_storage_value(value)?);
1836                    }
1837                }
1838
1839                if !metadata_ops.is_empty() {
1840                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1841                    let metadata = patch_metadata.get_or_insert_with(|| {
1842                        store.get_metadata(&collection, id).unwrap_or_default()
1843                    });
1844                    let mut metadata_json = metadata_to_json(metadata);
1845                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1846                        .map_err(crate::RedDBError::Query)?;
1847                    *metadata = metadata_from_json(&metadata_json)?;
1848                    metadata_changed = true;
1849                }
1850            }
1851            crate::storage::EntityData::Edge(edge) => {
1852                let mut field_ops = Vec::new();
1853                let mut metadata_ops = Vec::new();
1854                let mut weight_ops = Vec::new();
1855
1856                for mut op in operations {
1857                    let Some(root) = op.path.first().map(String::as_str) else {
1858                        return Err(crate::RedDBError::Query(
1859                            "patch path cannot be empty".to_string(),
1860                        ));
1861                    };
1862
1863                    match root {
1864                        "fields" | "properties" => {
1865                            if op.path.len() < 2 {
1866                                return Err(crate::RedDBError::Query(
1867                                    "patch path 'fields' requires a nested key".to_string(),
1868                                ));
1869                            }
1870                            op.path.remove(0);
1871                            field_ops.push(op);
1872                        }
1873                        "weight" => {
1874                            if op.path.len() != 1 {
1875                                return Err(crate::RedDBError::Query(
1876                                    "patch path 'weight' does not allow nested keys".to_string(),
1877                                ));
1878                            }
1879                            op.path.clear();
1880                            weight_ops.push(op);
1881                        }
1882                        "metadata" => {
1883                            if op.path.len() < 2 {
1884                                return Err(crate::RedDBError::Query(
1885                                    "patch path 'metadata' requires a nested key".to_string(),
1886                                ));
1887                            }
1888                            op.path.remove(0);
1889                            metadata_ops.push(op);
1890                        }
1891                        _ => {
1892                            return Err(crate::RedDBError::Query(format!(
1893                                "unsupported patch target '{root}' for graph edges. Use fields/*, weight, metadata/*"
1894                            )));
1895                        }
1896                    }
1897                }
1898
1899                if !field_ops.is_empty() {
1900                    context_index_dirty = true;
1901                    apply_patch_operations_to_storage_map(&mut edge.properties, &field_ops)?;
1902                }
1903
1904                for op in weight_ops {
1905                    context_index_dirty = true;
1906                    let value = op.value.ok_or_else(|| {
1907                        crate::RedDBError::Query("weight operations require a value".to_string())
1908                    })?;
1909
1910                    match op.op {
1911                        PatchEntityOperationType::Unset => {
1912                            return Err(crate::RedDBError::Query(
1913                                "weight cannot be unset through patch operations".to_string(),
1914                            ));
1915                        }
1916                        PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
1917                            let Some(weight) = value.as_f64() else {
1918                                return Err(crate::RedDBError::Query(
1919                                    "weight operation requires a numeric value".to_string(),
1920                                ));
1921                            };
1922                            graph_edge_weight = Some(weight as f32);
1923                            edge.weight = weight as f32;
1924                            modified_columns.push("weight".to_string());
1925                        }
1926                    }
1927                }
1928
1929                if let Some(fields) = payload
1930                    .get("fields")
1931                    .and_then(crate::json::Value::as_object)
1932                {
1933                    context_index_dirty = true;
1934                    for (key, value) in fields {
1935                        edge.properties
1936                            .insert(key.clone(), json_to_storage_value(value)?);
1937                    }
1938                }
1939
1940                if !metadata_ops.is_empty() {
1941                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1942                    let metadata = patch_metadata.get_or_insert_with(|| {
1943                        store.get_metadata(&collection, id).unwrap_or_default()
1944                    });
1945                    let mut metadata_json = metadata_to_json(metadata);
1946                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1947                        .map_err(crate::RedDBError::Query)?;
1948                    *metadata = metadata_from_json(&metadata_json)?;
1949                    metadata_changed = true;
1950                }
1951            }
1952            crate::storage::EntityData::Vector(vector) => {
1953                let mut field_ops = Vec::new();
1954                let mut metadata_ops = Vec::new();
1955
1956                for mut op in operations {
1957                    let Some(root) = op.path.first().map(String::as_str) else {
1958                        return Err(crate::RedDBError::Query(
1959                            "patch path cannot be empty".to_string(),
1960                        ));
1961                    };
1962
1963                    match root {
1964                        "fields" => {
1965                            if op.path.len() < 2 {
1966                                return Err(crate::RedDBError::Query(
1967                                    "patch path 'fields' requires a nested key".to_string(),
1968                                ));
1969                            }
1970                            op.path.remove(0);
1971                            let Some(target) = op.path.first().map(String::as_str) else {
1972                                return Err(crate::RedDBError::Query(
1973                                    "patch path requires a target under fields".to_string(),
1974                                ));
1975                            };
1976                            if !matches!(target, "dense" | "content" | "sparse") {
1977                                return Err(crate::RedDBError::Query(format!(
1978                                    "unsupported vector patch target '{target}'"
1979                                )));
1980                            }
1981                            field_ops.push(op);
1982                        }
1983                        "metadata" => {
1984                            if op.path.len() < 2 {
1985                                return Err(crate::RedDBError::Query(
1986                                    "patch path 'metadata' requires a nested key".to_string(),
1987                                ));
1988                            }
1989                            op.path.remove(0);
1990                            metadata_ops.push(op);
1991                        }
1992                        _ => {
1993                            return Err(crate::RedDBError::Query(format!(
1994                                "unsupported patch target '{root}' for vectors. Use fields/* or metadata/*"
1995                            )));
1996                        }
1997                    }
1998                }
1999
2000                if !field_ops.is_empty() {
2001                    context_index_dirty = true;
2002                    apply_patch_operations_to_vector_fields(vector, &field_ops)?;
2003                }
2004
2005                if let Some(fields) = payload
2006                    .get("fields")
2007                    .and_then(crate::json::Value::as_object)
2008                {
2009                    context_index_dirty = true;
2010                    if let Some(content) =
2011                        fields.get("content").and_then(crate::json::Value::as_str)
2012                    {
2013                        vector.content = Some(content.to_string());
2014                    }
2015                    if let Some(dense) = fields.get("dense") {
2016                        vector.dense = dense
2017                            .as_array()
2018                            .ok_or_else(|| {
2019                                crate::RedDBError::Query(
2020                                    "field 'dense' must be an array".to_string(),
2021                                )
2022                            })?
2023                            .iter()
2024                            .map(|value| {
2025                                value.as_f64().map(|value| value as f32).ok_or_else(|| {
2026                                    crate::RedDBError::Query(
2027                                        "field 'dense' must contain only numbers".to_string(),
2028                                    )
2029                                })
2030                            })
2031                            .collect::<Result<Vec<_>, _>>()?;
2032                    }
2033                }
2034
2035                if !metadata_ops.is_empty() {
2036                    ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
2037                    let metadata = patch_metadata.get_or_insert_with(|| {
2038                        store.get_metadata(&collection, id).unwrap_or_default()
2039                    });
2040                    let mut metadata_json = metadata_to_json(metadata);
2041                    apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
2042                        .map_err(crate::RedDBError::Query)?;
2043                    *metadata = metadata_from_json(&metadata_json)?;
2044                    metadata_changed = true;
2045                }
2046            }
2047            crate::storage::EntityData::TimeSeries(_)
2048            | crate::storage::EntityData::QueueMessage(_) => {
2049                return Err(crate::RedDBError::Query(
2050                    "patch operations are not supported for TimeSeries or QueueMessage entities"
2051                        .to_string(),
2052                ));
2053            }
2054        }
2055
2056        if let Some(node_type) = graph_node_type {
2057            if let crate::storage::EntityKind::GraphNode(node) = &mut entity.kind {
2058                node.node_type = node_type;
2059            }
2060        }
2061        if let Some(weight) = graph_edge_weight {
2062            if let crate::storage::EntityKind::GraphEdge(edge) = &mut entity.kind {
2063                edge.weight = (weight * 1000.0) as u32;
2064            }
2065        }
2066
2067        if let Some(metadata) = payload
2068            .get("metadata")
2069            .and_then(crate::json::Value::as_object)
2070        {
2071            let patch_metadata = patch_metadata
2072                .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
2073            for (key, value) in metadata {
2074                ensure_non_tree_reserved_metadata_key(key)?;
2075                patch_metadata.set(key.clone(), json_to_metadata_value(value)?);
2076            }
2077            metadata_changed = true;
2078        }
2079
2080        for (key, value) in parse_top_level_ttl_metadata_entries(&payload)? {
2081            let patch_metadata = patch_metadata
2082                .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
2083            if matches!(value, crate::storage::unified::MetadataValue::Null) {
2084                patch_metadata.remove(&key);
2085            } else {
2086                patch_metadata.set(key, value);
2087            }
2088            metadata_changed = true;
2089        }
2090
2091        entity.updated_at = std::time::SystemTime::now()
2092            .duration_since(std::time::UNIX_EPOCH)
2093            .unwrap_or_default()
2094            .as_secs();
2095
2096        // Versioned PATCH: re-stamp the merged post-image as a fresh
2097        // physical version keyed on the same logical_id, and close the
2098        // prior version's xmax at the same xid. `install_versioned_*`
2099        // in persist installs the new row and tombstones the old one;
2100        // `record_pending_versioned_update` arms first-committer-wins.
2101        if let Some(xid) = versioned_update_xid {
2102            let logical_id = entity.logical_id();
2103            entity.id = store.next_entity_id();
2104            entity.set_logical_id(logical_id);
2105            entity.set_xmin(xid);
2106            entity.set_xmax(0);
2107            if let Some(old) = replaced_entity.as_mut() {
2108                old.set_xmax(xid);
2109            }
2110        }
2111
2112        modified_columns = dedupe_modified_columns(modified_columns);
2113
2114        Ok(AppliedEntityMutation {
2115            id: entity.id,
2116            collection,
2117            entity,
2118            metadata: patch_metadata,
2119            modified_columns,
2120            persist_metadata: metadata_changed,
2121            context_index_dirty,
2122            replaced_entity,
2123            replaced_entity_previous_xmax: previous_xmax,
2124            pre_mutation_fields,
2125        })
2126    }
2127
2128    pub(crate) fn apply_loaded_sql_update_row_core(
2129        &self,
2130        collection: String,
2131        mut entity: crate::storage::UnifiedEntity,
2132        static_field_assignments: &[(String, Value)],
2133        dynamic_field_assignments: Vec<(String, Value)>,
2134        static_metadata_assignments: &[(String, MetadataValue)],
2135        dynamic_metadata_assignments: Vec<(String, MetadataValue)>,
2136        row_contract_plan: Option<&RowUpdateContractPlan>,
2137        row_modified_columns_template: &[String],
2138        row_touches_unique_columns: bool,
2139    ) -> RedDBResult<AppliedEntityMutation> {
2140        let id = entity.id;
2141        let previous_xmax = entity.xmax;
2142        let db = self.db();
2143        let store = db.store();
2144        let Some(_) = store.get_collection(&collection) else {
2145            return Err(crate::RedDBError::NotFound(format!(
2146                "collection not found: {collection}"
2147            )));
2148        };
2149
2150        let versioned_update_xid = match self.current_xid() {
2151            Some(xid) => Some(xid),
2152            None => {
2153                let snapshot_manager = self.snapshot_manager();
2154                let xid = snapshot_manager.begin();
2155                snapshot_manager.commit(xid);
2156                Some(xid)
2157            }
2158        };
2159        let mut replaced_entity = versioned_update_xid.map(|xid| {
2160            let mut old = entity.clone();
2161            old.set_xmax(xid);
2162            old
2163        });
2164
2165        let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
2166        let row_contract_timestamps = row_contract_plan
2167            .map(|plan| plan.timestamps_enabled)
2168            .unwrap_or(false);
2169        let mut metadata_changed = false;
2170        let mut modified_columns = row_modified_columns_template.to_vec();
2171        let mut context_index_dirty = !modified_columns.is_empty();
2172
2173        // Snapshot OLD field values BEFORE applying the assignments —
2174        // the secondary-index maintenance hook needs both before/after to
2175        // delete-then-insert under changed indexed columns.
2176        let pre_mutation_fields = entity_row_fields_snapshot(&entity);
2177
2178        let crate::storage::EntityData::Row(row) = &mut entity.data else {
2179            return Err(crate::RedDBError::Query(
2180                "SQL row update fast path requires a row entity".to_string(),
2181            ));
2182        };
2183
2184        let _ = row_contract_plan;
2185
2186        // Documents store the full JSON under the `body` column. Legacy rows
2187        // also promote top-level keys for filtering, while binary body mode
2188        // keeps `body` as the single source of truth. Reflect non-body SET
2189        // assignments into `body` as patch operations so dotted paths update
2190        // nested JSON instead of becoming literal top-level keys.
2191        let is_document_collection = db
2192            .collection_contract(&collection)
2193            .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Document)
2194            .unwrap_or(false);
2195        let kv_key = if row.get_field("value").is_some() || row.columns.len() >= 2 {
2196            row.get_field("key")
2197                .cloned()
2198                .or_else(|| row.columns.first().cloned())
2199        } else {
2200            None
2201        };
2202        if is_document_collection {
2203            // #1366: when a SET assigns to `body`, treat it as a FULL REPLACE of
2204            // the stored document — drop other column assignments, the new body
2205            // is the single source of truth.
2206            let body_assignment = static_field_assignments
2207                .iter()
2208                .chain(dynamic_field_assignments.iter())
2209                .rev()
2210                .find(|(column, _)| column == "body")
2211                .cloned();
2212
2213            match body_assignment {
2214                Some((_, value)) => {
2215                    let body = document_body_from_assignment(&value)?;
2216                    replace_document_row_body(
2217                        row,
2218                        body,
2219                        &collection,
2220                        self.binary_document_body_enabled(),
2221                        &mut modified_columns,
2222                    )?;
2223                    context_index_dirty = true;
2224                }
2225                None => {
2226                    // #1365: no `body` assignment — fall back to nested-path
2227                    // partial update via document_body_set_operation.
2228                    let mut static_row_assignments = Vec::new();
2229                    let mut dynamic_row_assignments = Vec::new();
2230                    let mut document_body_ops = Vec::new();
2231                    for (column, value) in static_field_assignments.iter().cloned() {
2232                        if column == "body" {
2233                            static_row_assignments.push((column, value));
2234                        } else {
2235                            document_body_ops.push(document_body_set_operation(&column, value));
2236                        }
2237                    }
2238                    for (column, value) in dynamic_field_assignments {
2239                        if column == "body" {
2240                            dynamic_row_assignments.push((column, value));
2241                        } else {
2242                            document_body_ops.push(document_body_set_operation(&column, value));
2243                        }
2244                    }
2245                    apply_row_field_assignments_raw(row, static_row_assignments);
2246                    apply_row_field_assignments_raw(row, dynamic_row_assignments);
2247                    if !document_body_ops.is_empty() {
2248                        let mut body = document_body_from_named(
2249                            row.named.get_or_insert_with(Default::default),
2250                        )?;
2251                        apply_patch_operations_to_json(&mut body, &document_body_ops)
2252                            .map_err(crate::RedDBError::Query)?;
2253                        replace_document_row_body(
2254                            row,
2255                            body,
2256                            &collection,
2257                            self.binary_document_body_enabled(),
2258                            &mut modified_columns,
2259                        )?;
2260                        context_index_dirty = true;
2261                    }
2262                }
2263            }
2264        } else {
2265            // Non-document collection: every assignment is a regular row column.
2266            let mut all_assignments: Vec<(String, Value)> = static_field_assignments.to_vec();
2267            all_assignments.extend(dynamic_field_assignments);
2268            apply_row_field_assignments_raw(row, all_assignments);
2269            if let Some(key) = kv_key {
2270                set_row_field(row, "key", key);
2271            }
2272        }
2273
2274        for (key, value) in static_metadata_assignments
2275            .iter()
2276            .cloned()
2277            .chain(dynamic_metadata_assignments)
2278        {
2279            ensure_non_tree_reserved_metadata_key(&key)?;
2280            patch_metadata
2281                .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default())
2282                .set(key, value);
2283            metadata_changed = true;
2284        }
2285
2286        if !modified_columns.is_empty() || row_contract_timestamps {
2287            let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2288            if row_contract_timestamps {
2289                context_index_dirty = true;
2290                set_row_field(
2291                    row,
2292                    "updated_at",
2293                    Value::UnsignedInteger(current_unix_ms_u64()),
2294                );
2295                modified_columns.push("updated_at".to_string());
2296            }
2297            if row_touches_unique_columns {
2298                let current_fields = collect_row_fields(row);
2299                contract.enforce_row_uniqueness(&current_fields, Some(id))?;
2300            }
2301        }
2302
2303        entity.updated_at = std::time::SystemTime::now()
2304            .duration_since(std::time::UNIX_EPOCH)
2305            .unwrap_or_default()
2306            .as_secs();
2307
2308        if let Some(xid) = versioned_update_xid {
2309            let logical_id = entity.logical_id();
2310            entity.id = store.next_entity_id();
2311            entity.set_logical_id(logical_id);
2312            entity.set_xmin(xid);
2313            entity.set_xmax(0);
2314            if let Some(old) = replaced_entity.as_mut() {
2315                old.set_xmax(xid);
2316            }
2317        }
2318
2319        modified_columns = dedupe_modified_columns(modified_columns);
2320
2321        Ok(AppliedEntityMutation {
2322            id: entity.id,
2323            collection,
2324            entity,
2325            metadata: patch_metadata,
2326            modified_columns,
2327            persist_metadata: metadata_changed,
2328            context_index_dirty,
2329            replaced_entity,
2330            replaced_entity_previous_xmax: previous_xmax,
2331            pre_mutation_fields,
2332        })
2333    }
2334
2335    pub(crate) fn persist_applied_entity_mutations(
2336        &self,
2337        applied: &[AppliedEntityMutation],
2338    ) -> RedDBResult<()> {
2339        if applied.is_empty() {
2340            return Ok(());
2341        }
2342
2343        let store = self.db().store();
2344        let collection = &applied[0].collection;
2345        let Some(manager) = store.get_collection(collection) else {
2346            return Err(crate::RedDBError::NotFound(format!(
2347                "collection not found: {collection}"
2348            )));
2349        };
2350
2351        let mut ordinary = Vec::with_capacity(applied.len());
2352        for item in applied {
2353            if let Some(old_version) = item.replaced_entity.as_ref() {
2354                store
2355                    .install_versioned_table_row_update(
2356                        collection,
2357                        old_version.clone(),
2358                        item.entity.clone(),
2359                        if item.persist_metadata {
2360                            item.metadata.as_ref()
2361                        } else {
2362                            None
2363                        },
2364                    )
2365                    .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2366                if self.current_xid().is_some() {
2367                    self.record_pending_versioned_update(
2368                        crate::runtime::impl_core::current_connection_id(),
2369                        collection,
2370                        old_version.id,
2371                        item.entity.id,
2372                        old_version.xmax,
2373                        item.replaced_entity_previous_xmax,
2374                    );
2375                }
2376            } else {
2377                ordinary.push(item);
2378            }
2379        }
2380        if ordinary.is_empty() {
2381            return Ok(());
2382        }
2383
2384        manager
2385            .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
2386                (
2387                    &item.entity,
2388                    item.modified_columns.as_slice(),
2389                    if item.persist_metadata {
2390                        item.metadata.as_ref()
2391                    } else {
2392                        None
2393                    },
2394                )
2395            }))
2396            .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
2397
2398        // PG-HOT-like fast path: segment in-place is done; when no
2399        // mutation touches a secondary-indexed column AND no metadata
2400        // payload needs to be folded into a B-tree record, skip the
2401        // in-line B-tree upsert. The WAL still records the update
2402        // (durability preserved; recovery replay rebuilds the B-tree),
2403        // and `manager.get()` prefers the live segment over the
2404        // B-tree for reads — so the short-circuit is invisible to
2405        // callers. See `persist_entities_to_pager_wal_only`.
2406        let indexed_cols = self
2407            .index_store_ref()
2408            .indexed_columns_set(collection.as_str());
2409        let all_hot = !indexed_cols.is_empty()
2410            && ordinary.iter().all(|item| {
2411                !item.persist_metadata
2412                    && !item
2413                        .modified_columns
2414                        .iter()
2415                        .any(|c| indexed_cols.contains(c))
2416            })
2417            || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
2418
2419        // Pass `&[&UnifiedEntity]` — no per-entity clone. The SQL UPDATE
2420        // inner loop hands us `applied` which already owns the post-image
2421        // entity; all we need for the persist path is a read borrow.
2422        let entity_refs: Vec<&crate::storage::UnifiedEntity> =
2423            ordinary.iter().map(|item| &item.entity).collect();
2424        let persist_fn = if all_hot {
2425            crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
2426        } else {
2427            crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
2428        };
2429        persist_fn(store.as_ref(), collection, &entity_refs)
2430            .map_err(|err| crate::RedDBError::Internal(err.to_string()))
2431    }
2432
2433    pub(crate) fn flush_applied_entity_mutation(
2434        &self,
2435        applied: &AppliedEntityMutation,
2436    ) -> RedDBResult<()> {
2437        let store = self.db().store();
2438        if applied.context_index_dirty {
2439            store
2440                .context_index()
2441                .index_entity(&applied.collection, &applied.entity);
2442        }
2443        // Secondary-index maintenance for SQL UPDATE / JSON-Patch flows.
2444        // Skip when pre_mutation_fields is empty (entity wasn't a row, or
2445        // didn't carry recoverable column names) — there's nothing to
2446        // delete-then-insert in that case.
2447        //
2448        // Also build the CDC damage vector here so downstream consumers
2449        // see which columns changed without re-diffing.
2450        let mut changed_columns: Option<Vec<String>> = None;
2451        if !applied.pre_mutation_fields.is_empty() {
2452            let post = entity_row_fields_snapshot(&applied.entity);
2453            if !post.is_empty() {
2454                let damage = crate::application::entity::row_damage_vector(
2455                    &applied.pre_mutation_fields,
2456                    &post,
2457                );
2458                if !damage.is_empty() {
2459                    changed_columns = Some(
2460                        damage
2461                            .touched_columns()
2462                            .into_iter()
2463                            .map(str::to_string)
2464                            .collect(),
2465                    );
2466                }
2467
2468                // HOT-like fast path (P3.T2/T3): when no modified
2469                // column is covered by a secondary index, skip the
2470                // `index_entity_update` call entirely. The function
2471                // would short-circuit internally, but the call still
2472                // reads the registry lock + walks the damage vector
2473                // — avoiding it saves a few microseconds per UPDATE.
2474                // Page-local replace + t_ctid chain support (true
2475                // HOT) lives in a follow-up storage spec.
2476                // Use the parent-expanded set so that dot-path indexes
2477                // (e.g. "body.service.tier") are triggered when the root
2478                // field ("body") appears in modified_columns.
2479                let indexed_cols: std::collections::HashSet<String> = self
2480                    .index_store_ref()
2481                    .indexed_columns_set_with_parents(applied.collection.as_str());
2482                // Single-source documents keep promoted index columns in the
2483                // body — surface them so the index sees the value move (a no-op
2484                // for ordinary stored columns).
2485                let pre_index_fields = self
2486                    .index_store_ref()
2487                    .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
2488                let post_index_fields = self
2489                    .index_store_ref()
2490                    .augment_body_derived_index_fields(&post, &indexed_cols);
2491                let modified_cols: std::collections::HashSet<String> =
2492                    crate::application::entity::row_damage_vector(
2493                        &pre_index_fields,
2494                        &post_index_fields,
2495                    )
2496                    .touched_columns()
2497                    .into_iter()
2498                    .map(str::to_string)
2499                    .collect();
2500                if let Some(old_version) = applied.replaced_entity.as_ref() {
2501                    let old_index_fields: Vec<(String, Value)> = pre_index_fields
2502                        .iter()
2503                        .filter(|(col, _)| indexed_cols.contains(col))
2504                        .cloned()
2505                        .collect();
2506                    let new_index_fields: Vec<(String, Value)> = post_index_fields
2507                        .iter()
2508                        .filter(|(col, _)| indexed_cols.contains(col))
2509                        .cloned()
2510                        .collect();
2511                    if !old_index_fields.is_empty() {
2512                        self.index_store_ref()
2513                            .index_entity_delete(
2514                                &applied.collection,
2515                                old_version.id,
2516                                &old_index_fields,
2517                            )
2518                            .map_err(crate::RedDBError::Internal)?;
2519                    }
2520                    if !new_index_fields.is_empty() {
2521                        self.index_store_ref()
2522                            .index_entity_insert(
2523                                &applied.collection,
2524                                applied.entity.id,
2525                                &new_index_fields,
2526                            )
2527                            .map_err(crate::RedDBError::Internal)?;
2528                    }
2529                } else {
2530                    let decision = crate::storage::engine::hot_update::decide(
2531                        &crate::storage::engine::hot_update::HotUpdateInputs {
2532                            collection: applied.collection.as_str(),
2533                            indexed_columns: &indexed_cols,
2534                            modified_columns: &modified_cols,
2535                            // The storage layer currently handles fit via
2536                            // the segment abstraction; we bypass the
2537                            // page-size check here.
2538                            new_tuple_size: 0,
2539                            page_free_space: usize::MAX,
2540                        },
2541                    );
2542                    if !decision.can_hot {
2543                        self.index_store_ref()
2544                            .index_entity_update(
2545                                &applied.collection,
2546                                applied.id,
2547                                &pre_index_fields,
2548                                &post_index_fields,
2549                            )
2550                            .map_err(crate::RedDBError::Internal)?;
2551                    } else {
2552                        // F-04: `applied.collection` is tenant-supplied;
2553                        // strip CR/LF/control bytes via the LogField
2554                        // escaper (ADR 0010).
2555                        tracing::debug!(
2556                            collection = %reddb_wire::audit_safe_log_field(&applied.collection),
2557                            "hot_update fast-path: skipped index_entity_update"
2558                        );
2559                    }
2560                }
2561            }
2562        }
2563        self.cdc_emit_prebuilt_with_columns(
2564            crate::replication::cdc::ChangeOperation::Update,
2565            &applied.collection,
2566            &applied.entity,
2567            "entity",
2568            applied.metadata.as_ref(),
2569            true,
2570            changed_columns,
2571        );
2572        Ok(())
2573    }
2574
2575    pub(crate) fn apply_loaded_patch_entity(
2576        &self,
2577        collection: String,
2578        entity: crate::storage::UnifiedEntity,
2579        payload: JsonValue,
2580        operations: Vec<PatchEntityOperation>,
2581    ) -> RedDBResult<CreateEntityOutput> {
2582        let applied =
2583            self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
2584        self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
2585        self.flush_applied_entity_mutation(&applied)?;
2586        Ok(CreateEntityOutput {
2587            id: applied.id,
2588            entity: Some(public_document_entity(applied.entity)),
2589        })
2590    }
2591}
2592
2593/// Shape the canonical binary-body document into the public response entity.
2594/// The stored `body` is the single source of truth (RDOC binary container),
2595/// but callers read the returned entity two ways, so the response satisfies
2596/// both: top-level body fields are surfaced into `named` (mirroring the GET
2597/// presentation derive in `named_fields_json`), and `body` itself is decoded
2598/// back to plain JSON. Derivation runs against the binary container first,
2599/// then `body` is replaced with its JSON form. This only reshapes the
2600/// returned copy — persistence already happened against the binary body.
2601fn public_document_entity(
2602    mut entity: crate::storage::UnifiedEntity,
2603) -> crate::storage::UnifiedEntity {
2604    let crate::storage::EntityData::Row(row) = &mut entity.data else {
2605        return entity;
2606    };
2607    let Some(named) = row.named.as_mut() else {
2608        return entity;
2609    };
2610    let Some(Value::Json(bytes)) = named.get("body") else {
2611        return entity;
2612    };
2613    let bytes = bytes.clone();
2614    if let Some(fields) = crate::document_body::body_fields(&bytes) {
2615        for (name, value) in fields {
2616            named.entry(name).or_insert(value);
2617        }
2618    }
2619    if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
2620        if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
2621            named.insert("body".to_string(), Value::Json(json_bytes));
2622        }
2623    }
2624    entity
2625}
2626
2627fn ensure_non_tree_reserved_metadata_patch_paths(
2628    operations: &[PatchEntityOperation],
2629) -> RedDBResult<()> {
2630    for operation in operations {
2631        let Some(key) = operation.path.first().map(String::as_str) else {
2632            continue;
2633        };
2634        ensure_non_tree_reserved_metadata_key(key)?;
2635    }
2636    Ok(())
2637}
2638
2639fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
2640    if key.starts_with(TREE_METADATA_PREFIX) {
2641        return Err(crate::RedDBError::Query(format!(
2642            "metadata key '{}' is reserved for managed trees",
2643            key
2644        )));
2645    }
2646    Ok(())
2647}
2648
2649fn ensure_non_tree_reserved_metadata_entries(
2650    metadata: &[(String, MetadataValue)],
2651) -> RedDBResult<()> {
2652    for (key, _) in metadata {
2653        ensure_non_tree_reserved_metadata_key(key)?;
2654    }
2655    Ok(())
2656}
2657
2658fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
2659    if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
2660        return Err(crate::RedDBError::Query(format!(
2661            "edge label '{}' is reserved for managed trees",
2662            TREE_CHILD_EDGE_LABEL
2663        )));
2664    }
2665    Ok(())
2666}
2667
2668impl RedDBRuntime {
2669    pub(crate) fn create_node_unchecked(
2670        &self,
2671        input: CreateNodeInput,
2672    ) -> RedDBResult<CreateEntityOutput> {
2673        let db = self.db();
2674        let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
2675        contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
2676        let mut metadata = input.metadata;
2677        apply_collection_default_ttl(&db, &input.collection, &mut metadata);
2678        let mut builder = db.node(&input.collection, &input.label);
2679
2680        if let Some(node_type) = input.node_type {
2681            builder = builder.node_type(node_type);
2682        }
2683
2684        for (key, value) in input.properties {
2685            builder = builder.property(key, value);
2686        }
2687
2688        for (key, value) in metadata {
2689            builder = builder.metadata(key, value);
2690        }
2691
2692        for embedding in input.embeddings {
2693            if let Some(model) = embedding.model {
2694                builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
2695            } else {
2696                builder = builder.embedding(embedding.name, embedding.vector);
2697            }
2698        }
2699
2700        for link in input.table_links {
2701            builder = builder.link_to_table(link.key, link.table);
2702        }
2703
2704        for link in input.node_links {
2705            builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
2706        }
2707
2708        let id = builder.save()?;
2709        // Phase 1.1 MVCC universal: stamp xmin so concurrent snapshots
2710        // don't see this node until the transaction commits.
2711        self.stamp_xmin_if_in_txn(&input.collection, id);
2712        refresh_context_index(&db, &input.collection, id)?;
2713        self.cdc_emit(
2714            crate::replication::cdc::ChangeOperation::Insert,
2715            &input.collection,
2716            id.raw(),
2717            "graph_node",
2718        );
2719        Ok(CreateEntityOutput {
2720            id,
2721            entity: db.store().get(&input.collection, id),
2722        })
2723    }
2724
2725    pub(crate) fn create_edge_unchecked(
2726        &self,
2727        input: CreateEdgeInput,
2728    ) -> RedDBResult<CreateEntityOutput> {
2729        let db = self.db();
2730        let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
2731        contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
2732        let mut metadata = input.metadata;
2733        apply_collection_default_ttl(&db, &input.collection, &mut metadata);
2734        let mut builder = db
2735            .edge(&input.collection, &input.label)
2736            .from(input.from)
2737            .to(input.to);
2738
2739        if let Some(weight) = input.weight {
2740            builder = builder.weight(weight);
2741        }
2742
2743        for (key, value) in input.properties {
2744            builder = builder.property(key, value);
2745        }
2746
2747        for (key, value) in metadata {
2748            builder = builder.metadata(key, value);
2749        }
2750
2751        let id = builder.save()?;
2752        // Phase 1.1 MVCC universal: stamp xmin on the edge so other
2753        // sessions don't follow it until COMMIT.
2754        self.stamp_xmin_if_in_txn(&input.collection, id);
2755        refresh_context_index(&db, &input.collection, id)?;
2756        self.cdc_emit(
2757            crate::replication::cdc::ChangeOperation::Insert,
2758            &input.collection,
2759            id.raw(),
2760            "graph_edge",
2761        );
2762        Ok(CreateEntityOutput {
2763            id,
2764            entity: db.store().get(&input.collection, id),
2765        })
2766    }
2767}
2768
2769fn create_rows_batch_prevalidated_columnar_with_outputs(
2770    runtime: &RedDBRuntime,
2771    collection: String,
2772    column_names: std::sync::Arc<Vec<String>>,
2773    rows: Vec<Vec<crate::storage::schema::Value>>,
2774) -> RedDBResult<Vec<CreateEntityOutput>> {
2775    use crate::storage::{
2776        unified::{EntityData, EntityKind, RowData},
2777        EntityId, UnifiedEntity,
2778    };
2779    use std::sync::Arc;
2780
2781    if rows.is_empty() {
2782        return Ok(Vec::new());
2783    }
2784    runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2785    runtime.check_batch_size(rows.len())?;
2786    runtime.check_db_size()?;
2787
2788    let db = runtime.db();
2789    let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2790    contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2791
2792    let store = db.store();
2793    let table_arc: Arc<str> = Arc::from(collection.as_str());
2794
2795    let indexed_cols = runtime
2796        .index_store_ref()
2797        .indexed_columns_set(collection.as_str());
2798    let has_secondary_indexes = !indexed_cols.is_empty();
2799    let mut field_snapshots: Vec<Vec<(String, crate::storage::schema::Value)>> =
2800        if has_secondary_indexes {
2801            Vec::with_capacity(rows.len())
2802        } else {
2803            Vec::new()
2804        };
2805
2806    let entities: Vec<UnifiedEntity> = rows
2807        .into_iter()
2808        .map(|values| {
2809            if has_secondary_indexes {
2810                let mut snap: Vec<(String, crate::storage::schema::Value)> =
2811                    Vec::with_capacity(indexed_cols.len());
2812                for (name, val) in column_names.iter().zip(values.iter()) {
2813                    if indexed_cols.contains(name) {
2814                        snap.push((name.clone(), val.clone()));
2815                    }
2816                }
2817                field_snapshots.push(snap);
2818            }
2819            let mut row = RowData::new(values);
2820            row.schema = Some(Arc::clone(&column_names));
2821            UnifiedEntity::new(
2822                EntityId::new(0),
2823                EntityKind::TableRow {
2824                    table: Arc::clone(&table_arc),
2825                    row_id: 0,
2826                },
2827                EntityData::Row(row),
2828            )
2829        })
2830        .collect();
2831
2832    let ids = store
2833        .bulk_insert(&collection, entities)
2834        .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
2835
2836    if has_secondary_indexes {
2837        let index_rows: Vec<(EntityId, Vec<(String, crate::storage::schema::Value)>)> = ids
2838            .iter()
2839            .zip(field_snapshots)
2840            .map(|(id, fields)| (*id, fields))
2841            .collect();
2842        runtime
2843            .index_store_ref()
2844            .index_entity_insert_batch(&collection, &index_rows)
2845            .map_err(crate::RedDBError::Internal)?;
2846    }
2847
2848    runtime.invalidate_result_cache();
2849    runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
2850
2851    Ok(ids
2852        .into_iter()
2853        .map(|id| CreateEntityOutput { id, entity: None })
2854        .collect())
2855}
2856
2857impl RuntimeEntityPort for RedDBRuntime {
2858    fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
2859        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2860        let db = self.db();
2861        let CreateRowInput {
2862            collection,
2863            fields,
2864            metadata: input_metadata,
2865            node_links,
2866            vector_links,
2867        } = input;
2868        let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2869        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2870        let mut metadata = input_metadata;
2871        apply_collection_default_ttl(&db, &collection, &mut metadata);
2872        let fields = contract.normalize_insert_fields(fields)?;
2873        contract.enforce_row_uniqueness(&fields, None)?;
2874        // Route through MutationEngine for unified hot path.
2875        let engine = self.mutation_engine();
2876        let result = engine.apply(
2877            collection.clone(),
2878            vec![crate::runtime::mutation::MutationRow {
2879                fields,
2880                metadata,
2881                node_links,
2882                vector_links,
2883            }],
2884        )?;
2885        let id = result.ids[0];
2886        // Perf: `db.get(id)` does a *cross-collection* scan (get_any)
2887        // that also takes a write lock on the entity cache. We know
2888        // the collection — hit the manager directly. Cuts
2889        // create_row() p50 roughly in half on the hot path.
2890        Ok(CreateEntityOutput {
2891            id,
2892            entity: db.store().get(&collection, id),
2893        })
2894    }
2895
2896    fn create_rows_batch(
2897        &self,
2898        input: CreateRowsBatchInput,
2899    ) -> RedDBResult<Vec<CreateEntityOutput>> {
2900        if input.rows.is_empty() {
2901            return Ok(Vec::new());
2902        }
2903        self.check_batch_size(input.rows.len())?;
2904        self.check_db_size()?;
2905
2906        let db = self.db();
2907        let collection = input.collection;
2908        let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2909        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2910
2911        let mut prepared_rows = Vec::with_capacity(input.rows.len());
2912        let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
2913        for row in input.rows {
2914            if row.collection != collection {
2915                return Err(crate::RedDBError::Query(format!(
2916                    "batch row collection mismatch: expected '{}', got '{}'",
2917                    collection, row.collection
2918                )));
2919            }
2920
2921            let mut metadata = row.metadata;
2922            apply_collection_default_ttl(&db, &collection, &mut metadata);
2923            let fields = contract.normalize_insert_fields(row.fields)?;
2924            contract.enforce_row_uniqueness(&fields, None)?;
2925            uniqueness_rows.push(fields.clone());
2926            prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
2927        }
2928
2929        contract.enforce_batch_uniqueness(&uniqueness_rows)?;
2930
2931        // Route through MutationEngine: single bulk_insert + one CDC batch
2932        // instead of N separate cdc_emit() calls (each acquires a write lock).
2933        let engine = {
2934            let e = self.mutation_engine();
2935            if input.suppress_events {
2936                e.with_suppress_events()
2937            } else {
2938                e
2939            }
2940        };
2941        let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
2942            .into_iter()
2943            .map(|(fields, metadata, node_links, vector_links)| {
2944                crate::runtime::mutation::MutationRow {
2945                    fields,
2946                    metadata,
2947                    node_links,
2948                    vector_links,
2949                }
2950            })
2951            .collect();
2952
2953        let result = engine
2954            .apply(collection.clone(), mutation_rows)
2955            .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
2956
2957        let store = db.store();
2958        Ok(result
2959            .ids
2960            .into_iter()
2961            .map(|id| CreateEntityOutput {
2962                id,
2963                entity: store.get(&collection, id),
2964            })
2965            .collect())
2966    }
2967
2968    fn create_rows_batch_prevalidated_columnar(
2969        &self,
2970        collection: String,
2971        column_names: std::sync::Arc<Vec<String>>,
2972        rows: Vec<Vec<crate::storage::schema::Value>>,
2973    ) -> RedDBResult<usize> {
2974        create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2975            .map(|outputs| outputs.len())
2976    }
2977
2978    fn create_rows_batch_columnar(
2979        &self,
2980        collection: String,
2981        column_names: std::sync::Arc<Vec<String>>,
2982        rows: Vec<Vec<crate::storage::schema::Value>>,
2983    ) -> RedDBResult<usize> {
2984        self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2985            .map(|outputs| outputs.len())
2986    }
2987
2988    fn create_rows_batch_columnar_with_outputs(
2989        &self,
2990        collection: String,
2991        column_names: std::sync::Arc<Vec<String>>,
2992        rows: Vec<Vec<crate::storage::schema::Value>>,
2993    ) -> RedDBResult<Vec<CreateEntityOutput>> {
2994        if rows.is_empty() {
2995            return Ok(Vec::new());
2996        }
2997        self.check_batch_size(rows.len())?;
2998        self.check_db_size()?;
2999
3000        let db = self.db();
3001        let contract = CollectionContractWriteEnforcer::new(&db, &collection);
3002        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
3003
3004        // Fast path: when the collection carries no contract (or the
3005        // contract has no declared columns) `normalize_insert_fields`
3006        // is a no-op and `enforce_row_uniqueness` finds no unique
3007        // columns to check. Skip the per-row `(String, Value)` tuple
3008        // materialisation entirely and route through the prevalidated
3009        // columnar kernel — same write, CDC, and index path, just
3010        // without the wasted (String, Value) clones. This is the
3011        // bench `bench_users` shape (no contract declared by the
3012        // adapter's `setup_schema`).
3013        let needs_normalisation = match db.collection_contract(&collection) {
3014            Some(c) => {
3015                c.declared_model == crate::catalog::CollectionModel::Table
3016                    && (!c.declared_columns.is_empty()
3017                        || c.table_def
3018                            .as_ref()
3019                            .map(|t| !t.columns.is_empty())
3020                            .unwrap_or(false))
3021            }
3022            None => false,
3023        };
3024        if !needs_normalisation {
3025            return create_rows_batch_prevalidated_columnar_with_outputs(
3026                self,
3027                collection,
3028                column_names,
3029                rows,
3030            );
3031        }
3032
3033        // Slow path: contract requires per-row normalisation /
3034        // uniqueness checks. Materialise `Vec<(String, Value)>` from
3035        // the columnar layout (this still pays N×ncols `String::clone`
3036        // — but it's deferred out of the wire decoder hot loop and
3037        // happens behind a runtime API, not the per-row decode loop)
3038        // and fall through to the existing `create_rows_batch` path.
3039        let ncols = column_names.len();
3040        let tuple_rows: Vec<CreateRowInput> = rows
3041            .into_iter()
3042            .map(|values| {
3043                let mut fields: Vec<(String, crate::storage::schema::Value)> =
3044                    Vec::with_capacity(ncols);
3045                for (name, value) in column_names.iter().zip(values) {
3046                    fields.push((name.clone(), value));
3047                }
3048                CreateRowInput {
3049                    collection: collection.clone(),
3050                    fields,
3051                    metadata: Vec::new(),
3052                    node_links: Vec::new(),
3053                    vector_links: Vec::new(),
3054                }
3055            })
3056            .collect();
3057        self.create_rows_batch(CreateRowsBatchInput {
3058            collection,
3059            rows: tuple_rows,
3060            suppress_events: false,
3061        })
3062    }
3063
3064    fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
3065        if input.rows.is_empty() {
3066            return Ok(0);
3067        }
3068        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3069        self.check_batch_size(input.rows.len())?;
3070        self.check_db_size()?;
3071
3072        let db = self.db();
3073        let collection = input.collection;
3074        // Still verify the collection's declared model before we blast
3075        // rows at it — this one-off check is O(1), independent of
3076        // ncols, and catches schema-kind mismatches that the client
3077        // can't always see.
3078        let contract = CollectionContractWriteEnforcer::new(&db, &collection);
3079        contract.ensure_model(crate::catalog::CollectionModel::Table)?;
3080
3081        // Hoist the per-collection default TTL lookup out of the
3082        // per-row loop — it only depends on the collection, not on
3083        // individual rows, and the old path did one HashMap read per
3084        // row (25k reads for a 25k typed_insert bulk). Fetch it once,
3085        // apply to any row whose metadata doesn't already carry a TTL.
3086        let default_ttl_ms = db.collection_default_ttl_ms(&collection);
3087
3088        let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
3089            Vec::with_capacity(input.rows.len());
3090        let mut mutation_rows = mutation_rows;
3091        for row in input.rows {
3092            if row.collection != collection {
3093                return Err(crate::RedDBError::Query(format!(
3094                    "batch row collection mismatch: expected '{}', got '{}'",
3095                    collection, row.collection
3096                )));
3097            }
3098            let mut metadata = row.metadata;
3099            if let Some(ttl) = default_ttl_ms {
3100                if !has_internal_ttl_metadata(&metadata) {
3101                    metadata.push((
3102                        "_ttl_ms".to_string(),
3103                        if ttl <= i64::MAX as u64 {
3104                            MetadataValue::Int(ttl as i64)
3105                        } else {
3106                            MetadataValue::Timestamp(ttl)
3107                        },
3108                    ));
3109                }
3110            }
3111            mutation_rows.push(crate::runtime::mutation::MutationRow {
3112                fields: row.fields,
3113                metadata,
3114                node_links: row.node_links,
3115                vector_links: row.vector_links,
3116            });
3117        }
3118
3119        let engine = self.mutation_engine();
3120        let result = engine
3121            .apply(collection, mutation_rows)
3122            .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
3123        Ok(result.ids.len())
3124    }
3125
3126    fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
3127        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3128        crate::reserved_fields::ensure_no_reserved_public_item_fields(
3129            input.properties.iter().map(|(key, _)| key.as_str()),
3130            &format!("node '{}'", input.collection),
3131        )?;
3132        ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
3133        self.create_node_unchecked(input)
3134    }
3135
3136    fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
3137        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3138        crate::reserved_fields::ensure_no_reserved_public_item_fields(
3139            input.properties.iter().map(|(key, _)| key.as_str()),
3140            &format!("edge '{}'", input.collection),
3141        )?;
3142        ensure_non_tree_structural_edge_label(&input.label)?;
3143        ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
3144        self.create_edge_unchecked(input)
3145    }
3146
3147    fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
3148        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3149        let db = self.db();
3150        let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3151        contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
3152        ensure_vector_dimension_contract(&db, &input.collection, input.dense.len())?;
3153        let mut metadata = input.metadata;
3154        apply_collection_default_ttl(&db, &input.collection, &mut metadata);
3155        let mut builder = db.vector(&input.collection).dense(input.dense);
3156
3157        if let Some(content) = input.content {
3158            builder = builder.content(content);
3159        }
3160
3161        for (key, value) in metadata {
3162            builder = builder.metadata(key, value);
3163        }
3164
3165        if let Some(link_row) = input.link_row {
3166            builder = builder.link_to_table(link_row);
3167        }
3168
3169        if let Some(link_node) = input.link_node {
3170            builder = builder.link_to_node(link_node);
3171        }
3172
3173        let id = builder.save()?;
3174        let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
3175            Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
3176            _ => None,
3177        };
3178        // Phase 1.1 MVCC universal: stamp xmin on the vector so
3179        // concurrent ANN scans hide it until the transaction commits.
3180        self.stamp_xmin_if_in_txn(&input.collection, id);
3181        refresh_context_index(&db, &input.collection, id)?;
3182        // Issue #693 — vector.turbo write path. Order matters and
3183        // matches the write sequence the next slice (#673) will
3184        // attach kill-points to: WAL append → in-memory index update
3185        // → TurboExtent append. The legacy `vector` path is the
3186        // `None` branch and is untouched.
3187        if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
3188            // Lazy populate so an INSERT after restart sees the
3189            // pre-restart vectors in the index before appending its
3190            // own.
3191            state.ensure_populated(&db.store(), &input.collection);
3192            // Issue #694 — named crash boundaries for #673.
3193            use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
3194            fire(InjectionPoint::BeforeWalFsync);
3195            let _ = db
3196                .store()
3197                .append_vector_insert_record(&input.collection, id.raw(), &dense);
3198            fire(InjectionPoint::BeforeIndexCommit);
3199            let mut index = state.index.lock();
3200            index.insert(id, dense.clone());
3201            // Mirror the encoded codes into the per-collection
3202            // TurboExtent when one is available. The bytes are the
3203            // 4-bit-packed code groups (`n_byte_groups`) plus the
3204            // little-endian L2 norm — same shape `BlockedCodeStorage`
3205            // appends in-memory. Failures are non-fatal in this
3206            // slice; durability of the extent is owned by #673.
3207            fire(InjectionPoint::BeforeExtentFsync);
3208            if let Some(extent) = state.extent.lock().as_mut() {
3209                let packed = index.encode_for_extent(&dense);
3210                let _ = extent.append(&packed);
3211            }
3212        }
3213        self.cdc_emit(
3214            crate::replication::cdc::ChangeOperation::Insert,
3215            &input.collection,
3216            id.raw(),
3217            "vector",
3218        );
3219        Ok(CreateEntityOutput {
3220            id,
3221            entity: db.store().get(&input.collection, id),
3222        })
3223    }
3224
3225    fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
3226        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3227        let db = self.db();
3228        let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3229        contract.ensure_model(crate::catalog::CollectionModel::Document)?;
3230
3231        if let JsonValue::Object(ref map) = input.body {
3232            crate::reserved_fields::ensure_no_reserved_public_item_fields(
3233                map.keys().map(String::as_str),
3234                &format!("document '{}'", input.collection),
3235            )?;
3236        }
3237
3238        // Serialize the full body for the "body" field. Behind the
3239        // `storage.binary_document_body` flag (PRD-1398) this is the native binary
3240        // container; otherwise plain JSON. Reads decode either form to JSON.
3241        let binary_body = self.binary_document_body_enabled();
3242        let body_bytes = crate::document_body::serialize_document_body(&input.body, binary_body)?;
3243        let fields: Vec<(String, crate::storage::schema::Value)> = vec![(
3244            "body".to_string(),
3245            crate::storage::schema::Value::Json(body_bytes),
3246        )];
3247
3248        let mut metadata = input.metadata;
3249        apply_collection_default_ttl(&db, &input.collection, &mut metadata);
3250        let columns: Vec<(&str, crate::storage::schema::Value)> = fields
3251            .iter()
3252            .map(|(key, value)| (key.as_str(), value.clone()))
3253            .collect();
3254        let mut builder = db.row(&input.collection, columns);
3255
3256        for (key, value) in metadata {
3257            builder = builder.metadata(key, value);
3258        }
3259
3260        for node in input.node_links {
3261            builder = builder.link_to_node(node);
3262        }
3263
3264        for vector in input.vector_links {
3265            builder = builder.link_to_vector(vector);
3266        }
3267
3268        let id = builder.save()?;
3269        // Phase 1.1 MVCC universal: stamp xmin on the document.
3270        self.stamp_xmin_if_in_txn(&input.collection, id);
3271        refresh_context_index(&db, &input.collection, id)?;
3272        self.cdc_emit(
3273            crate::replication::cdc::ChangeOperation::Insert,
3274            &input.collection,
3275            id.raw(),
3276            "document",
3277        );
3278        Ok(CreateEntityOutput {
3279            id,
3280            entity: db.store().get(&input.collection, id),
3281        })
3282    }
3283
3284    fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
3285        let db = self.db();
3286        let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3287        let declared_model = db
3288            .collection_contract(&input.collection)
3289            .map(|contract| contract.declared_model);
3290        let value = if declared_model == Some(crate::catalog::CollectionModel::Vault) {
3291            contract.ensure_model(crate::catalog::CollectionModel::Vault)?;
3292            self.seal_vault_value(&input.collection, input.value)?
3293        } else {
3294            contract.ensure_model(crate::catalog::CollectionModel::Kv)?;
3295            input.value
3296        };
3297        let fields = vec![
3298            (
3299                "key".to_string(),
3300                crate::storage::schema::Value::text(input.key),
3301            ),
3302            ("value".to_string(), value),
3303        ];
3304        let collection = input.collection;
3305        let result = self.mutation_engine().apply(
3306            collection.clone(),
3307            vec![crate::runtime::mutation::MutationRow {
3308                fields,
3309                metadata: input.metadata,
3310                node_links: Vec::new(),
3311                vector_links: Vec::new(),
3312            }],
3313        )?;
3314        let id = result.ids[0];
3315        Ok(CreateEntityOutput {
3316            id,
3317            entity: db.store().get(&collection, id),
3318        })
3319    }
3320
3321    fn create_timeseries_point(
3322        &self,
3323        input: CreateTimeSeriesPointInput,
3324    ) -> RedDBResult<CreateEntityOutput> {
3325        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3326        let db = self.db();
3327        let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3328        contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
3329
3330        let mut fields = vec![
3331            (
3332                "metric".to_string(),
3333                crate::storage::schema::Value::text(input.metric),
3334            ),
3335            (
3336                "value".to_string(),
3337                crate::storage::schema::Value::Float(input.value),
3338            ),
3339        ];
3340
3341        if let Some(timestamp_ns) = input.timestamp_ns {
3342            fields.push((
3343                "timestamp".to_string(),
3344                crate::storage::schema::Value::UnsignedInteger(timestamp_ns),
3345            ));
3346        }
3347
3348        if !input.tags.is_empty() {
3349            let tags_json = JsonValue::Object(
3350                input
3351                    .tags
3352                    .into_iter()
3353                    .map(|(key, value)| (key, JsonValue::String(value)))
3354                    .collect(),
3355            );
3356            let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
3357                crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
3358            })?;
3359            fields.push((
3360                "tags".to_string(),
3361                crate::storage::schema::Value::Json(tags_bytes),
3362            ));
3363        }
3364
3365        let collection = input.collection;
3366        let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
3367        // Phase 1.1 MVCC universal: stamp xmin on the point so
3368        // concurrent range scans hide it until COMMIT.
3369        self.stamp_xmin_if_in_txn(&collection, id);
3370        refresh_context_index(&db, &collection, id)?;
3371
3372        Ok(CreateEntityOutput {
3373            id,
3374            entity: db.store().get(&collection, id),
3375        })
3376    }
3377
3378    fn get_kv(
3379        &self,
3380        collection: &str,
3381        key: &str,
3382    ) -> RedDBResult<Option<(crate::storage::schema::Value, crate::storage::EntityId)>> {
3383        let db = self.db();
3384        ensure_collection_model_read(&db, collection, crate::catalog::CollectionModel::Kv)?;
3385        let store = db.store();
3386        let Some(manager) = store.get_collection(collection) else {
3387            return Ok(None);
3388        };
3389        let entities = manager.query_all(|_| true);
3390        for entity in entities {
3391            if let crate::storage::EntityData::Row(ref row) = entity.data {
3392                if let Some(ref named) = row.named {
3393                    if let Some(crate::storage::schema::Value::Text(ref k)) = named.get("key") {
3394                        if &**k == key {
3395                            let value = named
3396                                .get("value")
3397                                .cloned()
3398                                .unwrap_or(crate::storage::schema::Value::Null);
3399                            return Ok(Some((value, entity.id)));
3400                        }
3401                    }
3402                }
3403            }
3404        }
3405        Ok(None)
3406    }
3407
3408    fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
3409        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3410        let found = self.get_kv(collection, key)?;
3411        if let Some((_, id)) = found {
3412            let db = self.db();
3413            let store = db.store();
3414            let deleted = store
3415                .delete(collection, id)
3416                .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
3417            if deleted {
3418                store.context_index().remove_entity(id);
3419            }
3420            Ok(deleted)
3421        } else {
3422            Ok(false)
3423        }
3424    }
3425
3426    fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
3427        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3428        let PatchEntityInput {
3429            collection,
3430            id,
3431            payload,
3432            operations,
3433        } = input;
3434        let db = self.db();
3435        let store = db.store();
3436        let Some(manager) = store.get_collection(&collection) else {
3437            return Err(crate::RedDBError::NotFound(format!(
3438                "collection not found: {collection}"
3439            )));
3440        };
3441        let Some(entity) = manager.get(id) else {
3442            return Err(crate::RedDBError::NotFound(format!(
3443                "entity not found: {}",
3444                id.raw()
3445            )));
3446        };
3447        self.apply_loaded_patch_entity(collection, entity, payload, operations)
3448    }
3449
3450    fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
3451        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3452        let store = self.db().store();
3453        // Snapshot row fields before delete so we can mirror the removal
3454        // into every secondary index. The fetch is best-effort: if the
3455        // entity is already gone, the delete below is a no-op anyway.
3456        let pre_delete_fields = store
3457            .get(&input.collection, input.id)
3458            .as_ref()
3459            .map(entity_row_fields_snapshot)
3460            .unwrap_or_default();
3461        // Store delete first (source of truth). Crash between here and index removal
3462        // leaves the entity invisible to most queries but recoverable; the reverse
3463        // (remove index first, then crash) leaves the entity permanently orphaned.
3464        let deleted = store
3465            .delete(&input.collection, input.id)
3466            .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
3467        if deleted {
3468            store.context_index().remove_entity(input.id);
3469            // Secondary index maintenance — surface only registry-shape
3470            // errors; missing-index removals are tolerated inside the call.
3471            if !pre_delete_fields.is_empty() {
3472                self.index_store_ref()
3473                    .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
3474                    .map_err(crate::RedDBError::Internal)?;
3475            }
3476            self.cdc_emit(
3477                crate::replication::cdc::ChangeOperation::Delete,
3478                &input.collection,
3479                input.id.raw(),
3480                "entity",
3481            );
3482        }
3483        Ok(DeleteEntityOutput {
3484            deleted,
3485            id: input.id,
3486        })
3487    }
3488}