1use std::collections::HashMap;
2
3use crate::application::collection_contract_enforcer::{
4 normalize_row_update_assignment_with_plan, CollectionContractWriteEnforcer,
5};
6use crate::application::entity::{
7 AppliedEntityMutation, CreateDocumentInput, CreateKvInput, CreateTimeSeriesPointInput,
8 RowUpdateColumnRule, RowUpdateContractPlan,
9};
10use crate::application::ttl_payload::{
11 normalize_ttl_patch_operations, parse_top_level_ttl_metadata_entries,
12};
13use crate::json::{to_vec as json_to_vec, Value as JsonValue};
14use crate::storage::unified::MetadataValue;
15use reddb_types::Value;
16
17use super::*;
18
19const TREE_METADATA_PREFIX: &str = "red.tree.";
20const TREE_CHILD_EDGE_LABEL: &str = "TREE_CHILD";
21
22fn refresh_context_index(
23 db: &crate::storage::unified::devx::RedDB,
24 collection: &str,
25 id: crate::storage::EntityId,
26) -> RedDBResult<()> {
27 let store = db.store();
28 let Some(entity) = store.get(collection, id) else {
29 return Ok(());
30 };
31
32 store.context_index().index_entity(collection, &entity);
33 Ok(())
34}
35
36pub(crate) fn entity_row_fields_snapshot(
40 entity: &crate::storage::UnifiedEntity,
41) -> Vec<(String, Value)> {
42 let mut fields = match &entity.data {
43 crate::storage::EntityData::Row(row) => {
44 if let Some(named) = &row.named {
45 named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
46 } else if let Some(schema) = &row.schema {
47 schema
48 .iter()
49 .zip(row.columns.iter())
50 .map(|(name, value)| (name.clone(), value.clone()))
51 .collect()
52 } else {
53 Vec::new()
54 }
55 }
56 crate::storage::EntityData::Node(node) => node
57 .properties
58 .iter()
59 .map(|(k, v)| (k.clone(), v.clone()))
60 .collect(),
61 crate::storage::EntityData::Edge(edge) => edge
62 .properties
63 .iter()
64 .map(|(k, v)| (k.clone(), v.clone()))
65 .collect(),
66 _ => Vec::new(),
67 };
68 expand_binary_document_body_snapshot(&mut fields);
69 fields
70}
71
72fn expand_binary_document_body_snapshot(fields: &mut Vec<(String, Value)>) {
73 let Some(body_idx) = fields.iter().position(|(name, _)| name == "body") else {
74 return;
75 };
76 let Value::Json(bytes) = &fields[body_idx].1 else {
77 return;
78 };
79 let body_fields = crate::document_body::body_fields(bytes);
80 if let Some(body_json) = crate::document_body::decode_container_to_json(bytes) {
81 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
82 fields[body_idx].1 = Value::Json(json_bytes);
83 }
84 }
85 let Some(body_fields) = body_fields else {
86 return;
87 };
88 for (name, value) in body_fields {
89 if fields.iter().all(|(existing, _)| existing != &name) {
90 fields.push((name, value));
91 }
92 }
93}
94
95fn set_row_field(row: &mut crate::storage::unified::entity::RowData, name: &str, value: Value) {
96 if let Some(named) = row.named.as_mut() {
97 named.insert(name.to_string(), value);
98 return;
99 }
100
101 if let Some(schema) = row.schema.as_ref() {
102 if let Some(index) = schema.iter().position(|column| column == name) {
103 if let Some(slot) = row.columns.get_mut(index) {
104 *slot = value;
105 return;
106 }
107 }
108
109 let mut named = HashMap::with_capacity(schema.len().saturating_add(1));
110 for (column, current) in schema.iter().zip(row.columns.iter()) {
111 named.insert(column.clone(), current.clone());
112 }
113 named.insert(name.to_string(), value);
114 row.named = Some(named);
115 return;
116 }
117
118 let mut named = HashMap::with_capacity(1);
119 named.insert(name.to_string(), value);
120 row.named = Some(named);
121}
122
123fn collect_row_fields(row: &crate::storage::unified::entity::RowData) -> Vec<(String, Value)> {
124 if let Some(named) = row.named.as_ref() {
125 named
126 .iter()
127 .map(|(key, value)| (key.clone(), value.clone()))
128 .collect()
129 } else if let Some(schema) = row.schema.as_ref() {
130 schema
131 .iter()
132 .cloned()
133 .zip(row.columns.iter().cloned())
134 .collect()
135 } else {
136 Vec::new()
137 }
138}
139
140fn apply_row_field_assignments_raw<I>(
141 row: &mut crate::storage::unified::entity::RowData,
142 field_assignments: I,
143) where
144 I: IntoIterator<Item = (String, Value)>,
145{
146 for (column, value) in field_assignments {
147 set_row_field(row, &column, value);
148 }
149}
150
151fn apply_row_field_assignments_incremental<I>(
152 collection: &str,
153 row: &mut crate::storage::unified::entity::RowData,
154 field_assignments: I,
155 row_contract_plan: Option<&RowUpdateContractPlan>,
156) -> RedDBResult<()>
157where
158 I: IntoIterator<Item = (String, Value)>,
159{
160 for (column, value) in field_assignments {
161 let value = normalize_row_update_assignment_with_plan(
162 collection,
163 &column,
164 value,
165 row_contract_plan,
166 )?;
167
168 set_row_field(row, &column, value);
169 }
170
171 Ok(())
172}
173
174fn dedupe_modified_columns(mut modified_columns: Vec<String>) -> Vec<String> {
175 if modified_columns.is_empty() {
176 return modified_columns;
177 }
178
179 let mut unique = Vec::with_capacity(modified_columns.len());
180 for column in modified_columns.drain(..) {
181 if !unique
182 .iter()
183 .any(|existing: &String| existing.eq_ignore_ascii_case(&column))
184 {
185 unique.push(column);
186 }
187 }
188 unique
189}
190
191fn reject_document_array_position_path(path: &[String]) -> RedDBResult<()> {
192 if path.iter().any(|segment| segment.parse::<usize>().is_ok()) {
193 return Err(crate::RedDBError::Query(
194 "array positional document patch paths are unsupported; replace the array or full document body instead"
195 .to_string(),
196 ));
197 }
198 Ok(())
199}
200
201fn reject_document_patch_path_through_scalar(body: &JsonValue, path: &[String]) -> RedDBResult<()> {
207 if path.len() < 2 {
208 return Ok(());
209 }
210 let scalar_error = |prefix: &str| {
211 crate::RedDBError::Query(format!(
212 "cannot set nested path '{}': '{}' is not an object; setting a path through a non-object value is unsupported — replace that value or the full document body instead",
213 path.join("."),
214 prefix
215 ))
216 };
217 let mut current = body;
218 for (idx, segment) in path[..path.len() - 1].iter().enumerate() {
221 let JsonValue::Object(map) = current else {
222 return Err(scalar_error(&path[..idx].join(".")));
223 };
224 match map.get(segment) {
225 Some(next) => current = next,
226 None => return Ok(()),
227 }
228 }
229 if !matches!(current, JsonValue::Object(_)) {
231 return Err(scalar_error(&path[..path.len() - 1].join(".")));
232 }
233 Ok(())
234}
235
236fn apply_document_body_patch_operations(
242 body: &mut JsonValue,
243 operations: &[PatchEntityOperation],
244) -> RedDBResult<()> {
245 for op in operations {
246 reject_document_array_position_path(&op.path)?;
247 if matches!(op.op, PatchEntityOperationType::Set) {
248 reject_document_patch_path_through_scalar(body, &op.path)?;
249 }
250 }
251 apply_patch_operations_to_json(body, operations).map_err(crate::RedDBError::Query)
252}
253
254fn document_body_from_row(
255 row: &crate::storage::unified::entity::RowData,
256) -> RedDBResult<JsonValue> {
257 match row.get_field("body") {
258 Some(Value::Json(bytes)) => {
259 if let Some(body) = crate::document_body::decode_container_to_json(bytes) {
262 Ok(body)
263 } else {
264 crate::json::from_slice(bytes).map_err(|err| {
265 crate::RedDBError::Query(format!("failed to decode document body: {err}"))
266 })
267 }
268 }
269 Some(_) => Err(crate::RedDBError::Query(
270 "document body field must contain JSON".to_string(),
271 )),
272 None => Ok(JsonValue::Object(Default::default())),
273 }
274}
275
276fn document_body_from_assignment(value: &Value) -> RedDBResult<JsonValue> {
277 match value {
278 Value::Json(bytes) | Value::Blob(bytes) => crate::json::from_slice(bytes)
279 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
280 Value::Text(text) => crate::json::from_str(text.as_ref())
281 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
282 Value::Integer(value) => crate::json::from_str(&value.to_string())
283 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
284 Value::UnsignedInteger(value) => crate::json::from_str(&value.to_string())
285 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
286 Value::Float(value) => crate::json::from_str(&value.to_string())
287 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
288 other => Err(crate::RedDBError::Query(format!(
289 "column 'body' expected JSON body, got {other:?}"
290 ))),
291 }
292}
293
294fn document_body_set_operation(column: &str, value: Value) -> PatchEntityOperation {
295 PatchEntityOperation {
296 op: PatchEntityOperationType::Set,
297 path: column.split('.').map(str::to_string).collect(),
298 value: Some(value.to_json()),
299 }
300}
301
302fn replace_document_fields_body(
303 fields: &mut HashMap<String, Value>,
304 body: JsonValue,
305 binary: bool,
306 modified_columns: &mut Vec<String>,
307) -> RedDBResult<()> {
308 modified_columns.push("body".to_string());
309
310 let old_keys: Vec<String> = fields
311 .keys()
312 .filter(|key| key.as_str() != "body")
313 .cloned()
314 .collect();
315 for key in old_keys {
316 fields.remove(&key);
317 modified_columns.push(key);
318 }
319
320 let body_bytes = crate::document_body::serialize_document_body(&body, binary)?;
321 fields.insert("body".to_string(), Value::Json(body_bytes));
322
323 Ok(())
324}
325
326fn ensure_no_reserved_document_body_fields(body: &JsonValue, collection: &str) -> RedDBResult<()> {
327 if let JsonValue::Object(map) = body {
328 crate::reserved_fields::ensure_no_reserved_public_item_fields(
329 map.keys().map(String::as_str),
330 &format!("document '{collection}'"),
331 )?;
332 }
333 Ok(())
334}
335
336fn replace_document_row_body(
337 row: &mut crate::storage::unified::entity::RowData,
338 body: JsonValue,
339 collection: &str,
340 binary: bool,
341 modified_columns: &mut Vec<String>,
342) -> RedDBResult<()> {
343 ensure_no_reserved_document_body_fields(&body, collection)?;
344 let fields = row.named.get_or_insert_with(Default::default);
345 replace_document_fields_body(fields, body, binary, modified_columns)?;
346
347 row.columns.clear();
351 row.schema = None;
352 Ok(())
353}
354
355fn document_merge_patch_payload(payload: &JsonValue) -> &JsonValue {
356 if let JsonValue::Object(object) = payload {
357 if object.len() == 1 {
358 if let Some(body) = object.get("body") {
359 return body;
360 }
361 }
362 }
363 payload
364}
365
366fn apply_document_json_merge_patch(body: &mut JsonValue, patch: &JsonValue) -> RedDBResult<()> {
367 if !matches!(patch, JsonValue::Object(_)) {
368 *body = patch.clone();
369 return Ok(());
370 }
371
372 if matches!(patch, JsonValue::Object(object) if object.is_empty()) {
373 if !matches!(body, JsonValue::Object(_)) {
374 *body = JsonValue::Object(Default::default());
375 }
376 return Ok(());
377 }
378
379 let mut operations = Vec::new();
380 collect_document_json_merge_patch_operations(body, Vec::new(), patch, &mut operations);
381 apply_patch_operations_to_json(body, &operations).map_err(crate::RedDBError::Query)
382}
383
384fn collect_document_json_merge_patch_operations(
385 target: &JsonValue,
386 path: Vec<String>,
387 patch: &JsonValue,
388 operations: &mut Vec<PatchEntityOperation>,
389) {
390 let JsonValue::Object(patch_object) = patch else {
391 operations.push(PatchEntityOperation {
392 op: PatchEntityOperationType::Set,
393 path,
394 value: Some(patch.clone()),
395 });
396 return;
397 };
398
399 for (key, value) in patch_object {
400 let mut child_path = path.clone();
401 child_path.push(key.clone());
402 match value {
403 JsonValue::Null => operations.push(PatchEntityOperation {
404 op: PatchEntityOperationType::Unset,
405 path: child_path,
406 value: None,
407 }),
408 JsonValue::Object(object) if object.is_empty() => {
409 let child_target = match target {
410 JsonValue::Object(target_object) => target_object.get(key),
411 _ => None,
412 };
413 if !matches!(child_target, Some(JsonValue::Object(_))) {
414 operations.push(PatchEntityOperation {
415 op: PatchEntityOperationType::Set,
416 path: child_path,
417 value: Some(JsonValue::Object(Default::default())),
418 });
419 }
420 }
421 JsonValue::Object(_) => {
422 let child_target = match target {
423 JsonValue::Object(target_object) => target_object.get(key),
424 _ => None,
425 }
426 .unwrap_or(&JsonValue::Null);
427 collect_document_json_merge_patch_operations(
428 child_target,
429 child_path,
430 value,
431 operations,
432 );
433 }
434 _ => operations.push(PatchEntityOperation {
435 op: PatchEntityOperationType::Set,
436 path: child_path,
437 value: Some(value.clone()),
438 }),
439 }
440 }
441}
442
443impl RedDBRuntime {
444 pub(crate) fn apply_loaded_patch_entity_core(
445 &self,
446 collection: String,
447 mut entity: crate::storage::UnifiedEntity,
448 payload: JsonValue,
449 operations: Vec<PatchEntityOperation>,
450 ) -> RedDBResult<AppliedEntityMutation> {
451 let id = entity.id;
452 let previous_xmax = entity.xmax;
453 let operations = normalize_ttl_patch_operations(operations)?;
454 let pre_mutation_fields = entity_row_fields_snapshot(&entity);
457
458 let patch_versions = matches!(
470 entity.data,
471 crate::storage::EntityData::Row(_)
472 | crate::storage::EntityData::Node(_)
473 | crate::storage::EntityData::Edge(_)
474 ) && self.vcs_is_versioned(&collection).unwrap_or(false);
475 let versioned_update_xid = if patch_versions {
476 match self.current_xid() {
477 Some(xid) => Some(xid),
478 None => {
479 let snapshot_manager = self.snapshot_manager();
480 let xid = snapshot_manager.begin();
481 snapshot_manager.commit(xid);
482 Some(xid)
483 }
484 }
485 } else {
486 None
487 };
488 let mut replaced_entity = versioned_update_xid.map(|xid| {
489 let mut old = entity.clone();
490 old.set_xmax(xid);
491 old
492 });
493
494 let db = self.db();
495 let store = db.store();
496 let Some(manager) = store.get_collection(&collection) else {
497 return Err(crate::RedDBError::NotFound(format!(
498 "collection not found: {collection}"
499 )));
500 };
501
502 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
503 let mut metadata_changed = false;
504 let mut modified_columns: Vec<String> = Vec::new();
505 let mut context_index_dirty = false;
506 let mut graph_node_type: Option<String> = None;
507 let mut graph_edge_weight: Option<f32> = None;
508
509 let row_contract_timestamps = db
510 .collection_contract(&collection)
511 .map(|c| c.timestamps_enabled)
512 .unwrap_or(false);
513
514 match &mut entity.data {
515 crate::storage::EntityData::Row(row) => {
516 if row.named.is_none() {
520 if let Some(schema) = row.schema.as_ref() {
521 let fields = if schema.len() == row.columns.len() {
522 schema
523 .iter()
524 .cloned()
525 .zip(std::mem::take(&mut row.columns))
526 .collect()
527 } else {
528 collect_row_fields(row).into_iter().collect()
529 };
530 row.named = Some(fields);
531 }
532 }
533 let is_document_collection = db
534 .collection_contract(&collection)
535 .map(|contract| {
536 contract.declared_model == crate::catalog::CollectionModel::Document
537 })
538 .unwrap_or(false);
539 let binary_body = self.binary_document_body_enabled();
540 let mut field_ops = Vec::new();
541 let mut metadata_ops = Vec::new();
542 let mut document_body_ops = Vec::new();
543 let mut document_body_replace: Option<JsonValue> = None;
544 let has_patch_operations = !operations.is_empty();
545
546 for mut op in operations {
547 let Some(root) = op.path.first().map(String::as_str) else {
548 return Err(crate::RedDBError::Query(
549 "patch path cannot be empty".to_string(),
550 ));
551 };
552
553 match root {
554 "body" if is_document_collection => {
555 if op.path.len() == 1 {
556 match op.op {
557 PatchEntityOperationType::Set
558 | PatchEntityOperationType::Replace => {
559 let Some(value) = op.value.take() else {
560 return Err(crate::RedDBError::Query(
561 "document body replacement requires a value"
562 .to_string(),
563 ));
564 };
565 document_body_replace = Some(value);
566 }
567 PatchEntityOperationType::Unset => {
568 return Err(crate::RedDBError::Query(
569 "document body cannot be unset; replace it instead"
570 .to_string(),
571 ));
572 }
573 }
574 continue;
575 }
576 if op.path.len() < 2 {
577 return Err(crate::RedDBError::Query(
578 "document body patch paths require a nested key; use payload.body for full replacement"
579 .to_string(),
580 ));
581 }
582 op.path.remove(0);
583 reject_document_array_position_path(&op.path)?;
584 document_body_ops.push(op);
585 }
586 "fields" | "named" => {
587 if op.path.len() < 2 {
588 return Err(crate::RedDBError::Query(
589 "patch path 'fields' requires a nested key".to_string(),
590 ));
591 }
592 if row_contract_timestamps {
593 let leaf = op.path.get(1).map(String::as_str);
594 if matches!(leaf, Some("created_at") | Some("updated_at")) {
595 return Err(crate::RedDBError::Query(format!(
596 "collection '{}' manages '{}' automatically — do not set it in UPDATE",
597 collection,
598 leaf.unwrap_or("")
599 )));
600 }
601 }
602 op.path.remove(0);
603 field_ops.push(op);
604 }
605 "metadata" => {
606 if op.path.len() < 2 {
607 return Err(crate::RedDBError::Query(
608 "patch path 'metadata' requires a nested key".to_string(),
609 ));
610 }
611 op.path.remove(0);
612 metadata_ops.push(op);
613 }
614 _ => {
615 return Err(crate::RedDBError::Query(format!(
616 "unsupported patch target '{root}' for table rows. Use fields/*, metadata/*, or weight"
617 )));
618 }
619 }
620 }
621
622 if document_body_replace.is_some() || !document_body_ops.is_empty() {
623 context_index_dirty = true;
624 let mut body = match document_body_replace {
625 Some(body) => body,
626 None => document_body_from_row(row)?,
627 };
628 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
629 replace_document_row_body(
630 row,
631 body,
632 &collection,
633 binary_body,
634 &mut modified_columns,
635 )?;
636 }
637
638 if is_document_collection && !has_patch_operations {
639 let mut body = document_body_from_row(row)?;
640 let previous_body = body.clone();
641 apply_document_json_merge_patch(
642 &mut body,
643 document_merge_patch_payload(&payload),
644 )?;
645 if body != previous_body {
646 context_index_dirty = true;
647 replace_document_row_body(
648 row,
649 body,
650 &collection,
651 binary_body,
652 &mut modified_columns,
653 )?;
654 }
655 }
656
657 if !field_ops.is_empty() {
658 context_index_dirty = true;
659 if is_document_collection {
660 let mut body = document_body_from_row(row)?;
664 apply_patch_operations_to_json(&mut body, &field_ops)
665 .map_err(crate::RedDBError::Query)?;
666 replace_document_row_body(
667 row,
668 body,
669 &collection,
670 binary_body,
671 &mut modified_columns,
672 )?;
673 } else {
674 let named = row.named.get_or_insert_with(Default::default);
675 for op in &field_ops {
676 if let Some(col) = op.path.first() {
677 modified_columns.push(col.clone());
678 }
679 }
680 apply_patch_operations_to_storage_map(named, &field_ops)?;
681 }
682 }
683
684 if let Some(fields) = payload
685 .get("fields")
686 .and_then(crate::json::Value::as_object)
687 {
688 if row_contract_timestamps {
689 for key in fields.keys() {
690 if key == "created_at" || key == "updated_at" {
691 return Err(crate::RedDBError::Query(format!(
692 "collection '{}' manages '{}' automatically — do not set it in UPDATE",
693 collection, key
694 )));
695 }
696 }
697 }
698 context_index_dirty = true;
699 if is_document_collection {
700 let mut body = document_body_from_row(row)?;
703 if let JsonValue::Object(map) = &mut body {
704 for (key, value) in fields {
705 map.insert(key.clone(), value.clone());
706 }
707 }
708 replace_document_row_body(
709 row,
710 body,
711 &collection,
712 binary_body,
713 &mut modified_columns,
714 )?;
715 } else {
716 let named = row.named.get_or_insert_with(Default::default);
717 for (key, value) in fields {
718 modified_columns.push(key.clone());
719 named.insert(key.clone(), json_to_storage_value(value)?);
720 }
721 }
722 }
723
724 if !metadata_ops.is_empty() {
725 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
726 let metadata = patch_metadata.get_or_insert_with(|| {
727 store.get_metadata(&collection, id).unwrap_or_default()
728 });
729 let mut metadata_json = metadata_to_json(metadata);
730 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
731 .map_err(crate::RedDBError::Query)?;
732 *metadata = metadata_from_json(&metadata_json)?;
733 metadata_changed = true;
734 }
735
736 if !modified_columns.is_empty() || row_contract_timestamps {
737 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
738 let current_fields = if let Some(named) = row.named.take() {
739 named.into_iter().collect::<Vec<_>>()
740 } else if let Some(schema) = row.schema.as_ref() {
741 schema
742 .iter()
743 .cloned()
744 .zip(row.columns.iter().cloned())
745 .collect::<Vec<_>>()
746 } else {
747 Vec::new()
748 };
749 let normalized_fields = contract.normalize_update_fields(current_fields)?;
750 if let Some(schema) = db.collection_contract_arc(&collection) {
751 for column in schema
752 .declared_columns
753 .iter()
754 .filter(|column| column.generated.is_some())
755 {
756 modified_columns.push(column.name.clone());
757 context_index_dirty = true;
758 }
759 }
760 if row_contract_timestamps {
761 modified_columns.push("updated_at".to_string());
762 context_index_dirty = true;
763 }
764 if contract.requires_uniqueness_check(&modified_columns) {
765 contract.enforce_row_uniqueness(&normalized_fields, Some(id))?;
766 }
767 if self
768 .index_store_ref()
769 .touches_unique_index(&collection, &modified_columns)
770 {
771 self.enforce_unique_updates(
772 &collection,
773 &[(id, normalized_fields.clone())],
774 )?;
775 }
776 row.named = Some(normalized_fields.into_iter().collect());
777 }
778 }
779 crate::storage::EntityData::Node(node) => {
780 let mut field_ops = Vec::new();
781 let mut metadata_ops = Vec::new();
782 let mut node_type_ops = Vec::new();
783
784 for mut op in operations {
785 let Some(root) = op.path.first().map(String::as_str) else {
786 return Err(crate::RedDBError::Query(
787 "patch path cannot be empty".to_string(),
788 ));
789 };
790
791 match root {
792 "fields" | "properties" => {
793 if op.path.len() < 2 {
794 return Err(crate::RedDBError::Query(
795 "patch path 'fields' requires a nested key".to_string(),
796 ));
797 }
798 op.path.remove(0);
799 field_ops.push(op);
800 }
801 "metadata" => {
802 if op.path.len() < 2 {
803 return Err(crate::RedDBError::Query(
804 "patch path 'metadata' requires a nested key".to_string(),
805 ));
806 }
807 op.path.remove(0);
808 metadata_ops.push(op);
809 }
810 "node_type" => {
811 if op.path.len() != 1 {
812 return Err(crate::RedDBError::Query(
813 "patch path 'node_type' does not allow nested keys".to_string(),
814 ));
815 }
816 op.path.clear();
817 node_type_ops.push(op);
818 }
819 _ => {
820 return Err(crate::RedDBError::Query(format!(
821 "unsupported patch target '{root}' for graph nodes. Use fields/*, properties/*, node_type, or metadata/*"
822 )));
823 }
824 }
825 }
826
827 for op in node_type_ops {
828 context_index_dirty = true;
829 let value = op.value.ok_or_else(|| {
830 crate::RedDBError::Query("node_type operations require a value".to_string())
831 })?;
832
833 match op.op {
834 PatchEntityOperationType::Unset => {
835 return Err(crate::RedDBError::Query(
836 "node_type cannot be unset through patch operations".to_string(),
837 ));
838 }
839 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
840 let Some(node_type) = value.as_str() else {
841 return Err(crate::RedDBError::Query(
842 "node_type operation requires a text value".to_string(),
843 ));
844 };
845 graph_node_type = Some(node_type.to_string());
846 modified_columns.push("node_type".to_string());
847 }
848 }
849 }
850
851 if !field_ops.is_empty() {
852 context_index_dirty = true;
853 apply_patch_operations_to_storage_map(&mut node.properties, &field_ops)?;
854 }
855
856 if let Some(fields) = payload
857 .get("fields")
858 .and_then(crate::json::Value::as_object)
859 {
860 context_index_dirty = true;
861 for (key, value) in fields {
862 node.properties
863 .insert(key.clone(), json_to_storage_value(value)?);
864 }
865 }
866
867 if !metadata_ops.is_empty() {
868 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
869 let metadata = patch_metadata.get_or_insert_with(|| {
870 store.get_metadata(&collection, id).unwrap_or_default()
871 });
872 let mut metadata_json = metadata_to_json(metadata);
873 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
874 .map_err(crate::RedDBError::Query)?;
875 *metadata = metadata_from_json(&metadata_json)?;
876 metadata_changed = true;
877 }
878 }
879 crate::storage::EntityData::Edge(edge) => {
880 let mut field_ops = Vec::new();
881 let mut metadata_ops = Vec::new();
882 let mut weight_ops = Vec::new();
883
884 for mut op in operations {
885 let Some(root) = op.path.first().map(String::as_str) else {
886 return Err(crate::RedDBError::Query(
887 "patch path cannot be empty".to_string(),
888 ));
889 };
890
891 match root {
892 "fields" | "properties" => {
893 if op.path.len() < 2 {
894 return Err(crate::RedDBError::Query(
895 "patch path 'fields' requires a nested key".to_string(),
896 ));
897 }
898 op.path.remove(0);
899 field_ops.push(op);
900 }
901 "weight" => {
902 if op.path.len() != 1 {
903 return Err(crate::RedDBError::Query(
904 "patch path 'weight' does not allow nested keys".to_string(),
905 ));
906 }
907 op.path.clear();
908 weight_ops.push(op);
909 }
910 "metadata" => {
911 if op.path.len() < 2 {
912 return Err(crate::RedDBError::Query(
913 "patch path 'metadata' requires a nested key".to_string(),
914 ));
915 }
916 op.path.remove(0);
917 metadata_ops.push(op);
918 }
919 _ => {
920 return Err(crate::RedDBError::Query(format!(
921 "unsupported patch target '{root}' for graph edges. Use fields/*, weight, metadata/*"
922 )));
923 }
924 }
925 }
926
927 if !field_ops.is_empty() {
928 context_index_dirty = true;
929 apply_patch_operations_to_storage_map(&mut edge.properties, &field_ops)?;
930 }
931
932 for op in weight_ops {
933 context_index_dirty = true;
934 let value = op.value.ok_or_else(|| {
935 crate::RedDBError::Query("weight operations require a value".to_string())
936 })?;
937
938 match op.op {
939 PatchEntityOperationType::Unset => {
940 return Err(crate::RedDBError::Query(
941 "weight cannot be unset through patch operations".to_string(),
942 ));
943 }
944 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
945 let Some(weight) = value.as_f64() else {
946 return Err(crate::RedDBError::Query(
947 "weight operation requires a numeric value".to_string(),
948 ));
949 };
950 graph_edge_weight = Some(weight as f32);
951 edge.weight = weight as f32;
952 modified_columns.push("weight".to_string());
953 }
954 }
955 }
956
957 if let Some(fields) = payload
958 .get("fields")
959 .and_then(crate::json::Value::as_object)
960 {
961 context_index_dirty = true;
962 for (key, value) in fields {
963 edge.properties
964 .insert(key.clone(), json_to_storage_value(value)?);
965 }
966 }
967
968 if !metadata_ops.is_empty() {
969 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
970 let metadata = patch_metadata.get_or_insert_with(|| {
971 store.get_metadata(&collection, id).unwrap_or_default()
972 });
973 let mut metadata_json = metadata_to_json(metadata);
974 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
975 .map_err(crate::RedDBError::Query)?;
976 *metadata = metadata_from_json(&metadata_json)?;
977 metadata_changed = true;
978 }
979 }
980 crate::storage::EntityData::Vector(vector) => {
981 let mut field_ops = Vec::new();
982 let mut metadata_ops = Vec::new();
983
984 for mut op in operations {
985 let Some(root) = op.path.first().map(String::as_str) else {
986 return Err(crate::RedDBError::Query(
987 "patch path cannot be empty".to_string(),
988 ));
989 };
990
991 match root {
992 "fields" => {
993 if op.path.len() < 2 {
994 return Err(crate::RedDBError::Query(
995 "patch path 'fields' requires a nested key".to_string(),
996 ));
997 }
998 op.path.remove(0);
999 let Some(target) = op.path.first().map(String::as_str) else {
1000 return Err(crate::RedDBError::Query(
1001 "patch path requires a target under fields".to_string(),
1002 ));
1003 };
1004 if !matches!(target, "dense" | "content" | "sparse") {
1005 return Err(crate::RedDBError::Query(format!(
1006 "unsupported vector patch target '{target}'"
1007 )));
1008 }
1009 field_ops.push(op);
1010 }
1011 "metadata" => {
1012 if op.path.len() < 2 {
1013 return Err(crate::RedDBError::Query(
1014 "patch path 'metadata' requires a nested key".to_string(),
1015 ));
1016 }
1017 op.path.remove(0);
1018 metadata_ops.push(op);
1019 }
1020 _ => {
1021 return Err(crate::RedDBError::Query(format!(
1022 "unsupported patch target '{root}' for vectors. Use fields/* or metadata/*"
1023 )));
1024 }
1025 }
1026 }
1027
1028 if !field_ops.is_empty() {
1029 context_index_dirty = true;
1030 apply_patch_operations_to_vector_fields(vector, &field_ops)?;
1031 }
1032
1033 if let Some(fields) = payload
1034 .get("fields")
1035 .and_then(crate::json::Value::as_object)
1036 {
1037 context_index_dirty = true;
1038 if let Some(content) =
1039 fields.get("content").and_then(crate::json::Value::as_str)
1040 {
1041 vector.content = Some(content.to_string());
1042 }
1043 if let Some(dense) = fields.get("dense") {
1044 vector.dense = dense
1045 .as_array()
1046 .ok_or_else(|| {
1047 crate::RedDBError::Query(
1048 "field 'dense' must be an array".to_string(),
1049 )
1050 })?
1051 .iter()
1052 .map(|value| {
1053 value.as_f64().map(|value| value as f32).ok_or_else(|| {
1054 crate::RedDBError::Query(
1055 "field 'dense' must contain only numbers".to_string(),
1056 )
1057 })
1058 })
1059 .collect::<Result<Vec<_>, _>>()?;
1060 }
1061 }
1062
1063 if !metadata_ops.is_empty() {
1064 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1065 let metadata = patch_metadata.get_or_insert_with(|| {
1066 store.get_metadata(&collection, id).unwrap_or_default()
1067 });
1068 let mut metadata_json = metadata_to_json(metadata);
1069 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1070 .map_err(crate::RedDBError::Query)?;
1071 *metadata = metadata_from_json(&metadata_json)?;
1072 metadata_changed = true;
1073 }
1074 }
1075 crate::storage::EntityData::TimeSeries(_)
1076 | crate::storage::EntityData::QueueMessage(_) => {
1077 return Err(crate::RedDBError::Query(
1078 "patch operations are not supported for TimeSeries or QueueMessage entities"
1079 .to_string(),
1080 ));
1081 }
1082 }
1083
1084 if let Some(node_type) = graph_node_type {
1085 if let crate::storage::EntityKind::GraphNode(node) = &mut entity.kind {
1086 node.node_type = node_type;
1087 }
1088 }
1089 if let Some(weight) = graph_edge_weight {
1090 if let crate::storage::EntityKind::GraphEdge(edge) = &mut entity.kind {
1091 edge.weight = (weight * 1000.0) as u32;
1092 }
1093 }
1094
1095 if let Some(metadata) = payload
1096 .get("metadata")
1097 .and_then(crate::json::Value::as_object)
1098 {
1099 let patch_metadata = patch_metadata
1100 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1101 for (key, value) in metadata {
1102 ensure_non_tree_reserved_metadata_key(key)?;
1103 patch_metadata.set(key.clone(), json_to_metadata_value(value)?);
1104 }
1105 metadata_changed = true;
1106 }
1107
1108 for (key, value) in parse_top_level_ttl_metadata_entries(&payload)? {
1109 let patch_metadata = patch_metadata
1110 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1111 if matches!(value, crate::storage::unified::MetadataValue::Null) {
1112 patch_metadata.remove(&key);
1113 } else {
1114 patch_metadata.set(key, value);
1115 }
1116 metadata_changed = true;
1117 }
1118
1119 entity.updated_at = std::time::SystemTime::now()
1120 .duration_since(std::time::UNIX_EPOCH)
1121 .unwrap_or_default()
1122 .as_secs();
1123
1124 if let Some(xid) = versioned_update_xid {
1130 let logical_id = entity.logical_id();
1131 entity.id = store.next_entity_id();
1132 entity.set_logical_id(logical_id);
1133 entity.set_xmin(xid);
1134 entity.set_xmax(0);
1135 if let Some(old) = replaced_entity.as_mut() {
1136 old.set_xmax(xid);
1137 }
1138 }
1139
1140 modified_columns = dedupe_modified_columns(modified_columns);
1141
1142 Ok(AppliedEntityMutation {
1143 id: entity.id,
1144 collection,
1145 entity,
1146 metadata: patch_metadata,
1147 modified_columns,
1148 persist_metadata: metadata_changed,
1149 context_index_dirty,
1150 replaced_entity,
1151 replaced_entity_previous_xmax: previous_xmax,
1152 pre_mutation_fields,
1153 })
1154 }
1155
1156 pub(crate) fn apply_loaded_sql_update_row_core(
1157 &self,
1158 collection: String,
1159 mut entity: crate::storage::UnifiedEntity,
1160 static_field_assignments: &[(String, Value)],
1161 dynamic_field_assignments: Vec<(String, Value)>,
1162 static_metadata_assignments: &[(String, MetadataValue)],
1163 dynamic_metadata_assignments: Vec<(String, MetadataValue)>,
1164 row_contract_plan: Option<&RowUpdateContractPlan>,
1165 row_modified_columns_template: &[String],
1166 row_touches_unique_columns: bool,
1167 ) -> RedDBResult<AppliedEntityMutation> {
1168 let id = entity.id;
1169 let previous_xmax = entity.xmax;
1170 let db = self.db();
1171 let store = db.store();
1172 let Some(_) = store.get_collection(&collection) else {
1173 return Err(crate::RedDBError::NotFound(format!(
1174 "collection not found: {collection}"
1175 )));
1176 };
1177
1178 let versioned_update_xid = match self.current_xid() {
1179 Some(xid) => Some(xid),
1180 None => {
1181 let snapshot_manager = self.snapshot_manager();
1182 let xid = snapshot_manager.begin();
1183 snapshot_manager.commit(xid);
1184 Some(xid)
1185 }
1186 };
1187 let mut replaced_entity = versioned_update_xid.map(|xid| {
1188 let mut old = entity.clone();
1189 old.set_xmax(xid);
1190 old
1191 });
1192
1193 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
1194 let row_contract_timestamps = row_contract_plan
1195 .map(|plan| plan.timestamps_enabled)
1196 .unwrap_or(false);
1197 let mut metadata_changed = false;
1198 let mut modified_columns = row_modified_columns_template.to_vec();
1199 let mut context_index_dirty = !modified_columns.is_empty();
1200
1201 let pre_mutation_fields = entity_row_fields_snapshot(&entity);
1205
1206 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1207 return Err(crate::RedDBError::Query(
1208 "SQL row update fast path requires a row entity".to_string(),
1209 ));
1210 };
1211
1212 let _ = row_contract_plan;
1213
1214 let is_document_collection = db
1220 .collection_contract(&collection)
1221 .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Document)
1222 .unwrap_or(false);
1223 let kv_key = if row.get_field("value").is_some() || row.columns.len() >= 2 {
1224 row.get_field("key")
1225 .cloned()
1226 .or_else(|| row.columns.first().cloned())
1227 } else {
1228 None
1229 };
1230 if is_document_collection {
1231 let body_assignment = static_field_assignments
1235 .iter()
1236 .chain(dynamic_field_assignments.iter())
1237 .rev()
1238 .find(|(column, _)| column == "body")
1239 .cloned();
1240
1241 match body_assignment {
1242 Some((_, value)) => {
1243 let body = document_body_from_assignment(&value)?;
1244 replace_document_row_body(
1245 row,
1246 body,
1247 &collection,
1248 self.binary_document_body_enabled(),
1249 &mut modified_columns,
1250 )?;
1251 context_index_dirty = true;
1252 }
1253 None => {
1254 let mut static_row_assignments = Vec::new();
1257 let mut dynamic_row_assignments = Vec::new();
1258 let mut document_body_ops = Vec::new();
1259 for (column, value) in static_field_assignments.iter().cloned() {
1260 if column == "body" {
1261 static_row_assignments.push((column, value));
1262 } else {
1263 document_body_ops.push(document_body_set_operation(&column, value));
1264 }
1265 }
1266 for (column, value) in dynamic_field_assignments {
1267 if column == "body" {
1268 dynamic_row_assignments.push((column, value));
1269 } else {
1270 document_body_ops.push(document_body_set_operation(&column, value));
1271 }
1272 }
1273 apply_row_field_assignments_raw(row, static_row_assignments);
1274 apply_row_field_assignments_raw(row, dynamic_row_assignments);
1275 if !document_body_ops.is_empty() {
1276 let mut body = document_body_from_row(row)?;
1277 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
1278 replace_document_row_body(
1279 row,
1280 body,
1281 &collection,
1282 self.binary_document_body_enabled(),
1283 &mut modified_columns,
1284 )?;
1285 context_index_dirty = true;
1286 }
1287 }
1288 }
1289 } else {
1290 let mut all_assignments: Vec<(String, Value)> = static_field_assignments.to_vec();
1292 all_assignments.extend(dynamic_field_assignments);
1293 apply_row_field_assignments_raw(row, all_assignments);
1294 if let Some(key) = kv_key {
1295 set_row_field(row, "key", key);
1296 }
1297 }
1298
1299 for (key, value) in static_metadata_assignments
1300 .iter()
1301 .cloned()
1302 .chain(dynamic_metadata_assignments)
1303 {
1304 ensure_non_tree_reserved_metadata_key(&key)?;
1305 patch_metadata
1306 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default())
1307 .set(key, value);
1308 metadata_changed = true;
1309 }
1310
1311 if !modified_columns.is_empty() || row_contract_timestamps {
1312 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1313 if row_contract_timestamps {
1314 context_index_dirty = true;
1315 set_row_field(row, "updated_at", contract.managed_timestamp_value());
1316 modified_columns.push("updated_at".to_string());
1317 }
1318 let expression_contract = if row_contract_plan.is_some_and(|plan| plan.has_expressions)
1319 {
1320 db.collection_contract_arc(&collection)
1321 } else {
1322 None
1323 };
1324 if let Some(schema) = &expression_contract {
1325 let normalized = contract.normalize_update_fields(collect_row_fields(row))?;
1326 for column in schema
1327 .declared_columns
1328 .iter()
1329 .filter(|column| column.generated.is_some())
1330 {
1331 modified_columns.push(column.name.clone());
1332 }
1333 for (name, value) in normalized {
1334 set_row_field(row, &name, value);
1335 }
1336 context_index_dirty = true;
1337 }
1338 if row_touches_unique_columns || expression_contract.is_some() {
1339 let current_fields = collect_row_fields(row);
1340 contract.enforce_row_uniqueness(¤t_fields, Some(id))?;
1341 }
1342 }
1343
1344 entity.updated_at = std::time::SystemTime::now()
1345 .duration_since(std::time::UNIX_EPOCH)
1346 .unwrap_or_default()
1347 .as_secs();
1348
1349 if let Some(xid) = versioned_update_xid {
1350 let logical_id = entity.logical_id();
1351 entity.id = store.next_entity_id();
1352 entity.set_logical_id(logical_id);
1353 entity.set_xmin(xid);
1354 entity.set_xmax(0);
1355 if let Some(old) = replaced_entity.as_mut() {
1356 old.set_xmax(xid);
1357 }
1358 }
1359
1360 modified_columns = dedupe_modified_columns(modified_columns);
1361
1362 Ok(AppliedEntityMutation {
1363 id: entity.id,
1364 collection,
1365 entity,
1366 metadata: patch_metadata,
1367 modified_columns,
1368 persist_metadata: metadata_changed,
1369 context_index_dirty,
1370 replaced_entity,
1371 replaced_entity_previous_xmax: previous_xmax,
1372 pre_mutation_fields,
1373 })
1374 }
1375
1376 pub(crate) fn persist_applied_entity_mutations(
1377 &self,
1378 applied: &[AppliedEntityMutation],
1379 ) -> RedDBResult<()> {
1380 if applied.is_empty() {
1381 return Ok(());
1382 }
1383
1384 let store = self.db().store();
1385 let _publication = store.vault_publication_lock.lock();
1389 let collection = &applied[0].collection;
1390 let Some(manager) = store.get_collection(collection) else {
1391 return Err(crate::RedDBError::NotFound(format!(
1392 "collection not found: {collection}"
1393 )));
1394 };
1395
1396 let mut ordinary = Vec::with_capacity(applied.len());
1397 for item in applied {
1398 if let Some(old_version) = item.replaced_entity.as_ref() {
1399 store
1400 .install_versioned_table_row_update(
1401 collection,
1402 old_version.clone(),
1403 item.entity.clone(),
1404 if item.persist_metadata {
1405 item.metadata.as_ref()
1406 } else {
1407 None
1408 },
1409 )
1410 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
1411 if self.current_xid().is_some() {
1412 self.record_pending_versioned_update(
1413 crate::runtime::impl_core::current_connection_id(),
1414 collection,
1415 old_version.id,
1416 item.entity.id,
1417 old_version.xmax,
1418 item.replaced_entity_previous_xmax,
1419 );
1420 }
1421 } else {
1422 ordinary.push(item);
1423 }
1424 }
1425 if ordinary.is_empty() {
1426 return Ok(());
1427 }
1428
1429 manager
1430 .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
1431 (
1432 &item.entity,
1433 item.modified_columns.as_slice(),
1434 if item.persist_metadata {
1435 item.metadata.as_ref()
1436 } else {
1437 None
1438 },
1439 )
1440 }))
1441 .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
1442
1443 let indexed_cols = self
1452 .index_store_ref()
1453 .indexed_columns_set(collection.as_str());
1454 let all_hot = !indexed_cols.is_empty()
1455 && ordinary.iter().all(|item| {
1456 !item.persist_metadata
1457 && !item
1458 .modified_columns
1459 .iter()
1460 .any(|c| indexed_cols.contains(c))
1461 })
1462 || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
1463
1464 let entity_refs: Vec<&crate::storage::UnifiedEntity> =
1468 ordinary.iter().map(|item| &item.entity).collect();
1469 let persist_fn = if all_hot {
1470 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
1471 } else {
1472 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
1473 };
1474 persist_fn(store.as_ref(), collection, &entity_refs)
1475 .map_err(|err| crate::RedDBError::Internal(err.to_string()))
1476 }
1477
1478 pub(crate) fn flush_applied_entity_mutation(
1479 &self,
1480 applied: &AppliedEntityMutation,
1481 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1482 ) -> RedDBResult<()> {
1483 let store = self.db().store();
1484 if applied.context_index_dirty {
1485 store
1486 .context_index()
1487 .index_entity(&applied.collection, &applied.entity);
1488 }
1489 let mut changed_columns: Option<Vec<String>> = None;
1497 if !applied.pre_mutation_fields.is_empty() {
1498 let post = entity_row_fields_snapshot(&applied.entity);
1499 if !post.is_empty() {
1500 let damage = crate::application::entity::row_damage_vector(
1501 &applied.pre_mutation_fields,
1502 &post,
1503 );
1504 if !damage.is_empty() {
1505 changed_columns = Some(
1506 damage
1507 .touched_columns()
1508 .into_iter()
1509 .map(str::to_string)
1510 .collect(),
1511 );
1512 }
1513
1514 let indexed_cols: std::collections::HashSet<String> = self
1526 .index_store_ref()
1527 .indexed_columns_set_with_parents(applied.collection.as_str());
1528 let pre_index_fields = self
1532 .index_store_ref()
1533 .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
1534 let post_index_fields = self
1535 .index_store_ref()
1536 .augment_body_derived_index_fields(&post, &indexed_cols);
1537 let modified_cols: std::collections::HashSet<String> =
1538 crate::application::entity::row_damage_vector(
1539 &pre_index_fields,
1540 &post_index_fields,
1541 )
1542 .touched_columns()
1543 .into_iter()
1544 .map(str::to_string)
1545 .collect();
1546 if let Some(old_version) = applied.replaced_entity.as_ref() {
1547 let old_index_fields: Vec<(String, Value)> = pre_index_fields
1548 .iter()
1549 .filter(|(col, _)| indexed_cols.contains(col))
1550 .cloned()
1551 .collect();
1552 let new_index_fields: Vec<(String, Value)> = post_index_fields
1553 .iter()
1554 .filter(|(col, _)| indexed_cols.contains(col))
1555 .cloned()
1556 .collect();
1557 if !old_index_fields.is_empty() {
1558 self.index_store_ref()
1559 .index_entity_delete(
1560 &applied.collection,
1561 old_version.id,
1562 &old_index_fields,
1563 )
1564 .map_err(crate::RedDBError::Internal)?;
1565 }
1566 if !new_index_fields.is_empty() {
1567 self.index_store_ref()
1568 .index_entity_insert(
1569 &applied.collection,
1570 applied.entity.id,
1571 &new_index_fields,
1572 )
1573 .map_err(crate::RedDBError::Internal)?;
1574 }
1575 } else {
1576 let decision = crate::storage::engine::hot_update::decide(
1577 &crate::storage::engine::hot_update::HotUpdateInputs {
1578 collection: applied.collection.as_str(),
1579 indexed_columns: &indexed_cols,
1580 modified_columns: &modified_cols,
1581 new_tuple_size: 0,
1585 page_free_space: usize::MAX,
1586 },
1587 );
1588 if !decision.can_hot {
1589 self.index_store_ref()
1590 .index_entity_update(
1591 &applied.collection,
1592 applied.id,
1593 &pre_index_fields,
1594 &post_index_fields,
1595 )
1596 .map_err(crate::RedDBError::Internal)?;
1597 } else {
1598 tracing::debug!(
1602 collection = %reddb_wire::audit_safe_log_field(&applied.collection),
1603 "hot_update fast-path: skipped index_entity_update"
1604 );
1605 }
1606 }
1607 }
1608 }
1609 drop(topology_guard);
1610 #[cfg(test)]
1611 self.index_store_ref().mutation_test_hook(
1612 &applied.collection,
1613 crate::runtime::MutationTestPhase::BeforeEvents,
1614 );
1615 self.cdc_emit_prebuilt_with_columns(
1616 crate::replication::cdc::ChangeOperation::Update,
1617 &applied.collection,
1618 &applied.entity,
1619 "entity",
1620 applied.metadata.as_ref(),
1621 true,
1622 changed_columns,
1623 );
1624 Ok(())
1625 }
1626
1627 pub(crate) fn apply_loaded_patch_entity(
1628 &self,
1629 collection: String,
1630 entity: crate::storage::UnifiedEntity,
1631 payload: JsonValue,
1632 operations: Vec<PatchEntityOperation>,
1633 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1634 ) -> RedDBResult<CreateEntityOutput> {
1635 let applied =
1636 self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
1637 let _reservation = self.admit_entity_mutation(&applied)?;
1638 self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
1639 #[cfg(test)]
1640 self.index_store_ref().mutation_test_hook(
1641 &applied.collection,
1642 crate::runtime::MutationTestPhase::StoragePublished,
1643 );
1644 self.flush_applied_entity_mutation(&applied, topology_guard)?;
1645 Ok(CreateEntityOutput {
1646 id: applied.id,
1647 entity: Some(public_document_entity(applied.entity)),
1648 })
1649 }
1650}
1651
1652fn public_document_entity(
1661 mut entity: crate::storage::UnifiedEntity,
1662) -> crate::storage::UnifiedEntity {
1663 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1664 return entity;
1665 };
1666 let Some(named) = row.named.as_mut() else {
1667 return entity;
1668 };
1669 let Some(Value::Json(bytes)) = named.get("body") else {
1670 return entity;
1671 };
1672 let bytes = bytes.clone();
1673 if let Some(fields) = crate::document_body::body_fields(&bytes) {
1674 for (name, value) in fields {
1675 named.entry(name).or_insert(value);
1676 }
1677 }
1678 if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
1679 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
1680 named.insert("body".to_string(), Value::Json(json_bytes));
1681 }
1682 }
1683 entity
1684}
1685
1686fn ensure_non_tree_reserved_metadata_patch_paths(
1687 operations: &[PatchEntityOperation],
1688) -> RedDBResult<()> {
1689 for operation in operations {
1690 let Some(key) = operation.path.first().map(String::as_str) else {
1691 continue;
1692 };
1693 ensure_non_tree_reserved_metadata_key(key)?;
1694 }
1695 Ok(())
1696}
1697
1698fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
1699 if key.starts_with(TREE_METADATA_PREFIX) {
1700 return Err(crate::RedDBError::Query(format!(
1701 "metadata key '{}' is reserved for managed trees",
1702 key
1703 )));
1704 }
1705 Ok(())
1706}
1707
1708fn ensure_non_tree_reserved_metadata_entries(
1709 metadata: &[(String, MetadataValue)],
1710) -> RedDBResult<()> {
1711 for (key, _) in metadata {
1712 ensure_non_tree_reserved_metadata_key(key)?;
1713 }
1714 Ok(())
1715}
1716
1717fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
1718 if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
1719 return Err(crate::RedDBError::Query(format!(
1720 "edge label '{}' is reserved for managed trees",
1721 TREE_CHILD_EDGE_LABEL
1722 )));
1723 }
1724 Ok(())
1725}
1726
1727impl RedDBRuntime {
1728 pub(crate) fn create_node_unchecked(
1729 &self,
1730 input: CreateNodeInput,
1731 ) -> RedDBResult<CreateEntityOutput> {
1732 let db = self.db();
1733 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1734 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1735 let mut metadata = input.metadata;
1736 contract.apply_default_ttl(&mut metadata);
1737 let mut builder = db.node(&input.collection, &input.label);
1738
1739 if let Some(node_type) = input.node_type {
1740 builder = builder.node_type(node_type);
1741 }
1742
1743 for (key, value) in input.properties {
1744 builder = builder.property(key, value);
1745 }
1746
1747 for (key, value) in metadata {
1748 builder = builder.metadata(key, value);
1749 }
1750
1751 for embedding in input.embeddings {
1752 if let Some(model) = embedding.model {
1753 builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
1754 } else {
1755 builder = builder.embedding(embedding.name, embedding.vector);
1756 }
1757 }
1758
1759 for link in input.table_links {
1760 builder = builder.link_to_table(link.key, link.table);
1761 }
1762
1763 for link in input.node_links {
1764 builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
1765 }
1766
1767 let id = builder.save()?;
1768 self.stamp_xmin_if_in_txn(&input.collection, id);
1771 refresh_context_index(&db, &input.collection, id)?;
1772 self.cdc_emit(
1773 crate::replication::cdc::ChangeOperation::Insert,
1774 &input.collection,
1775 id.raw(),
1776 "graph_node",
1777 );
1778 Ok(CreateEntityOutput {
1779 id,
1780 entity: db.store().get(&input.collection, id),
1781 })
1782 }
1783
1784 pub(crate) fn create_edge_unchecked(
1785 &self,
1786 input: CreateEdgeInput,
1787 ) -> RedDBResult<CreateEntityOutput> {
1788 let db = self.db();
1789 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1790 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1791 let mut metadata = input.metadata;
1792 contract.apply_default_ttl(&mut metadata);
1793 let mut builder = db
1794 .edge(&input.collection, &input.label)
1795 .from(input.from)
1796 .to(input.to);
1797
1798 if let Some(weight) = input.weight {
1799 builder = builder.weight(weight);
1800 }
1801
1802 for (key, value) in input.properties {
1803 builder = builder.property(key, value);
1804 }
1805
1806 for (key, value) in metadata {
1807 builder = builder.metadata(key, value);
1808 }
1809
1810 let id = builder.save()?;
1811 self.stamp_xmin_if_in_txn(&input.collection, id);
1814 refresh_context_index(&db, &input.collection, id)?;
1815 self.cdc_emit(
1816 crate::replication::cdc::ChangeOperation::Insert,
1817 &input.collection,
1818 id.raw(),
1819 "graph_edge",
1820 );
1821 Ok(CreateEntityOutput {
1822 id,
1823 entity: db.store().get(&input.collection, id),
1824 })
1825 }
1826}
1827
1828fn create_rows_batch_prevalidated_columnar_with_outputs(
1829 runtime: &RedDBRuntime,
1830 collection: String,
1831 column_names: std::sync::Arc<Vec<String>>,
1832 rows: Vec<Vec<reddb_types::Value>>,
1833) -> RedDBResult<Vec<CreateEntityOutput>> {
1834 use crate::storage::{
1835 unified::{EntityData, EntityKind, RowData},
1836 EntityId, UnifiedEntity,
1837 };
1838 use std::sync::Arc;
1839
1840 if rows.is_empty() {
1841 return Ok(Vec::new());
1842 }
1843 if runtime.index_store_ref().has_unique_index(&collection) {
1846 return runtime.create_rows_batch_columnar_with_outputs(collection, column_names, rows);
1847 }
1848 runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1849 runtime.check_batch_size(rows.len())?;
1850 runtime.check_db_size()?;
1851
1852 let db = runtime.db();
1853 let contract = CollectionContractWriteEnforcer::new(runtime, &db, &collection);
1854 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1855
1856 let store = db.store();
1857 let table_arc: Arc<str> = Arc::from(collection.as_str());
1858
1859 let indexed_cols = runtime
1860 .index_store_ref()
1861 .indexed_columns_set(collection.as_str());
1862 let has_secondary_indexes = !indexed_cols.is_empty();
1863 let mut field_snapshots: Vec<Vec<(String, reddb_types::Value)>> = if has_secondary_indexes {
1864 Vec::with_capacity(rows.len())
1865 } else {
1866 Vec::new()
1867 };
1868
1869 let entities: Vec<UnifiedEntity> = rows
1870 .into_iter()
1871 .map(|values| {
1872 if has_secondary_indexes {
1873 let mut snap: Vec<(String, reddb_types::Value)> =
1874 Vec::with_capacity(indexed_cols.len());
1875 for (name, val) in column_names.iter().zip(values.iter()) {
1876 if indexed_cols.contains(name) {
1877 snap.push((name.clone(), val.clone()));
1878 }
1879 }
1880 field_snapshots.push(snap);
1881 }
1882 let mut row = RowData::new(values);
1883 row.schema = Some(Arc::clone(&column_names));
1884 UnifiedEntity::new(
1885 EntityId::new(0),
1886 EntityKind::TableRow {
1887 table: Arc::clone(&table_arc),
1888 row_id: 0,
1889 },
1890 EntityData::Row(row),
1891 )
1892 })
1893 .collect();
1894
1895 let ids = store
1896 .bulk_insert(&collection, entities)
1897 .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
1898
1899 if has_secondary_indexes {
1900 let index_rows: Vec<(EntityId, Vec<(String, reddb_types::Value)>)> = ids
1901 .iter()
1902 .zip(field_snapshots)
1903 .map(|(id, fields)| (*id, fields))
1904 .collect();
1905 runtime
1906 .index_store_ref()
1907 .index_entity_insert_batch(&collection, &index_rows)
1908 .map_err(crate::RedDBError::Internal)?;
1909 }
1910
1911 runtime.invalidate_result_cache();
1912 runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
1913
1914 Ok(ids
1915 .into_iter()
1916 .map(|id| CreateEntityOutput { id, entity: None })
1917 .collect())
1918}
1919
1920impl RuntimeEntityPort for RedDBRuntime {
1921 fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
1922 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1923 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
1924 self,
1925 &input.collection,
1926 );
1927 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
1928 let db = self.db();
1929 let CreateRowInput {
1930 collection,
1931 fields,
1932 metadata: input_metadata,
1933 node_links,
1934 vector_links,
1935 } = input;
1936 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1937 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1938 let mut metadata = input_metadata;
1939 contract.apply_default_ttl(&mut metadata);
1940 let fields = contract.normalize_insert_fields(fields)?;
1941 contract.enforce_row_uniqueness(&fields, None)?;
1942 let engine = self.mutation_engine();
1944 let result = engine.apply(
1945 collection.clone(),
1946 vec![crate::runtime::mutation::MutationRow {
1947 fields,
1948 metadata,
1949 node_links,
1950 vector_links,
1951 }],
1952 )?;
1953 let id = result.ids[0];
1954 Ok(CreateEntityOutput {
1959 id,
1960 entity: db.store().get(&collection, id),
1961 })
1962 }
1963
1964 fn create_rows_batch(
1965 &self,
1966 input: CreateRowsBatchInput,
1967 ) -> RedDBResult<Vec<CreateEntityOutput>> {
1968 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
1969 self,
1970 &input.collection,
1971 );
1972 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
1973 if input.rows.is_empty() {
1974 return Ok(Vec::new());
1975 }
1976 self.check_batch_size(input.rows.len())?;
1977 self.check_db_size()?;
1978
1979 let db = self.db();
1980 let collection = input.collection;
1981 let suppress_events = input.suppress_events;
1982 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1983 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1984
1985 let mut prepared_rows = Vec::with_capacity(input.rows.len());
1986 let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
1987 for row in input.rows {
1988 if row.collection != collection {
1989 return Err(crate::RedDBError::Query(format!(
1990 "batch row collection mismatch: expected '{}', got '{}'",
1991 collection, row.collection
1992 )));
1993 }
1994
1995 let mut metadata = row.metadata;
1996 contract.apply_default_ttl(&mut metadata);
1997 let fields = contract.normalize_insert_fields(row.fields)?;
1998 contract.enforce_row_uniqueness(&fields, None)?;
1999 uniqueness_rows.push(fields.clone());
2000 prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
2001 }
2002
2003 contract.enforce_batch_uniqueness(&uniqueness_rows)?;
2004
2005 let engine = {
2008 let e = self.mutation_engine();
2009 if suppress_events {
2010 e.with_suppress_events()
2011 } else {
2012 e
2013 }
2014 };
2015 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
2016 .into_iter()
2017 .map(|(fields, metadata, node_links, vector_links)| {
2018 crate::runtime::mutation::MutationRow {
2019 fields,
2020 metadata,
2021 node_links,
2022 vector_links,
2023 }
2024 })
2025 .collect();
2026
2027 let result = engine
2028 .apply(collection.clone(), mutation_rows)
2029 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
2030
2031 let store = db.store();
2032 Ok(result
2033 .ids
2034 .into_iter()
2035 .map(|id| CreateEntityOutput {
2036 id,
2037 entity: store.get(&collection, id),
2038 })
2039 .collect())
2040 }
2041
2042 fn create_rows_batch_prevalidated_columnar(
2043 &self,
2044 collection: String,
2045 column_names: std::sync::Arc<Vec<String>>,
2046 rows: Vec<Vec<reddb_types::Value>>,
2047 ) -> RedDBResult<usize> {
2048 create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2049 .map(|outputs| outputs.len())
2050 }
2051
2052 fn create_rows_batch_columnar(
2053 &self,
2054 collection: String,
2055 column_names: std::sync::Arc<Vec<String>>,
2056 rows: Vec<Vec<reddb_types::Value>>,
2057 ) -> RedDBResult<usize> {
2058 self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2059 .map(|outputs| outputs.len())
2060 }
2061
2062 fn create_rows_batch_columnar_with_outputs(
2063 &self,
2064 collection: String,
2065 column_names: std::sync::Arc<Vec<String>>,
2066 rows: Vec<Vec<reddb_types::Value>>,
2067 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2068 if rows.is_empty() {
2069 return Ok(Vec::new());
2070 }
2071 self.check_batch_size(rows.len())?;
2072 self.check_db_size()?;
2073
2074 let db = self.db();
2075 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2076 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2077
2078 let needs_normalisation = self.index_store_ref().has_unique_index(&collection)
2088 || match db.collection_contract(&collection) {
2089 Some(c) => {
2090 c.declared_model == crate::catalog::CollectionModel::Table
2091 && (!c.declared_columns.is_empty()
2092 || c.table_def
2093 .as_ref()
2094 .map(|t| !t.columns.is_empty())
2095 .unwrap_or(false))
2096 }
2097 None => false,
2098 };
2099 if !needs_normalisation {
2100 return create_rows_batch_prevalidated_columnar_with_outputs(
2101 self,
2102 collection,
2103 column_names,
2104 rows,
2105 );
2106 }
2107
2108 let ncols = column_names.len();
2115 let tuple_rows: Vec<CreateRowInput> = rows
2116 .into_iter()
2117 .map(|values| {
2118 let mut fields: Vec<(String, reddb_types::Value)> = Vec::with_capacity(ncols);
2119 for (name, value) in column_names.iter().zip(values) {
2120 fields.push((name.clone(), value));
2121 }
2122 CreateRowInput {
2123 collection: collection.clone(),
2124 fields,
2125 metadata: Vec::new(),
2126 node_links: Vec::new(),
2127 vector_links: Vec::new(),
2128 }
2129 })
2130 .collect();
2131 self.create_rows_batch(CreateRowsBatchInput {
2132 collection,
2133 rows: tuple_rows,
2134 suppress_events: false,
2135 })
2136 }
2137
2138 fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
2139 self.create_rows_batch_prevalidated_with_outputs(input)
2140 .map(|outputs| outputs.len())
2141 }
2142
2143 fn create_rows_batch_prevalidated_with_outputs(
2144 &self,
2145 input: CreateRowsBatchInput,
2146 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2147 if input.rows.is_empty() {
2148 return Ok(Vec::new());
2149 }
2150 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2151 self.check_batch_size(input.rows.len())?;
2152 self.check_db_size()?;
2153
2154 let db = self.db();
2155 let collection = input.collection;
2156 let suppress_events = input.suppress_events;
2157 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2162 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2163
2164 let default_ttl_plan = contract.default_ttl_plan();
2170
2171 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
2172 Vec::with_capacity(input.rows.len());
2173 let mut mutation_rows = mutation_rows;
2174 for row in input.rows {
2175 if row.collection != collection {
2176 return Err(crate::RedDBError::Query(format!(
2177 "batch row collection mismatch: expected '{}', got '{}'",
2178 collection, row.collection
2179 )));
2180 }
2181 let mut metadata = row.metadata;
2182 default_ttl_plan.apply(&mut metadata);
2183 mutation_rows.push(crate::runtime::mutation::MutationRow {
2184 fields: row.fields,
2185 metadata,
2186 node_links: row.node_links,
2187 vector_links: row.vector_links,
2188 });
2189 }
2190
2191 let engine = {
2192 let engine = self.mutation_engine();
2193 if suppress_events {
2194 engine.with_suppress_events()
2195 } else {
2196 engine
2197 }
2198 };
2199 let result = engine
2200 .apply(collection.clone(), mutation_rows)
2201 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
2202 let store = db.store();
2203 Ok(result
2204 .ids
2205 .into_iter()
2206 .map(|id| CreateEntityOutput {
2207 id,
2208 entity: store.get(&collection, id),
2209 })
2210 .collect())
2211 }
2212
2213 fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
2214 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2215 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2216 input.properties.iter().map(|(key, _)| key.as_str()),
2217 &format!("node '{}'", input.collection),
2218 )?;
2219 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2220 self.create_node_unchecked(input)
2221 }
2222
2223 fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
2224 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2225 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2226 input.properties.iter().map(|(key, _)| key.as_str()),
2227 &format!("edge '{}'", input.collection),
2228 )?;
2229 ensure_non_tree_structural_edge_label(&input.label)?;
2230 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2231 self.create_edge_unchecked(input)
2232 }
2233
2234 fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
2235 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2236 let db = self.db();
2237 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2238 contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
2239 contract.ensure_vector_dimension(input.dense.len())?;
2240 let mut metadata = input.metadata;
2241 contract.apply_default_ttl(&mut metadata);
2242 let mut builder = db.vector(&input.collection).dense(input.dense);
2243
2244 if let Some(content) = input.content {
2245 builder = builder.content(content);
2246 }
2247
2248 for (key, value) in metadata {
2249 builder = builder.metadata(key, value);
2250 }
2251
2252 if let Some(link_row) = input.link_row {
2253 builder = builder.link_to_table(link_row);
2254 }
2255
2256 if let Some(link_node) = input.link_node {
2257 builder = builder.link_to_node(link_node);
2258 }
2259
2260 let id = builder.save_with_xmin(self.current_xid())?;
2263 let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
2264 Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
2265 _ => None,
2266 };
2267 refresh_context_index(&db, &input.collection, id)?;
2268 if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
2274 state.ensure_populated(&db.store(), &input.collection);
2278 use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
2280 fire(InjectionPoint::BeforeWalFsync);
2281 let _ = db
2282 .store()
2283 .append_vector_insert_record(&input.collection, id.raw(), &dense);
2284 fire(InjectionPoint::BeforeIndexCommit);
2285 let mut index = state.index.lock();
2286 index.insert(id, dense.clone());
2287 fire(InjectionPoint::BeforeExtentFsync);
2294 if let Some(extent) = state.extent.lock().as_mut() {
2295 let packed = index.encode_for_extent(&dense);
2296 let _ = extent.append(&packed);
2297 }
2298 }
2299 self.cdc_emit(
2300 crate::replication::cdc::ChangeOperation::Insert,
2301 &input.collection,
2302 id.raw(),
2303 "vector",
2304 );
2305 Ok(CreateEntityOutput {
2306 id,
2307 entity: db.store().get(&input.collection, id),
2308 })
2309 }
2310
2311 fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
2312 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2313 let db = self.db();
2314 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2315 contract.ensure_model(crate::catalog::CollectionModel::Document)?;
2316
2317 if let JsonValue::Object(ref map) = input.body {
2318 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2319 map.keys().map(String::as_str),
2320 &format!("document '{}'", input.collection),
2321 )?;
2322 }
2323
2324 let binary_body = self.binary_document_body_enabled();
2328 let body_bytes = crate::document_body::serialize_document_body(&input.body, binary_body)?;
2329 let fields: Vec<(String, reddb_types::Value)> =
2330 vec![("body".to_string(), reddb_types::Value::Json(body_bytes))];
2331
2332 let mut metadata = input.metadata;
2333 contract.apply_default_ttl(&mut metadata);
2334 let columns: Vec<(&str, reddb_types::Value)> = fields
2335 .iter()
2336 .map(|(key, value)| (key.as_str(), value.clone()))
2337 .collect();
2338 let mut builder = db.row(&input.collection, columns);
2339
2340 for (key, value) in metadata {
2341 builder = builder.metadata(key, value);
2342 }
2343
2344 for node in input.node_links {
2345 builder = builder.link_to_node(node);
2346 }
2347
2348 for vector in input.vector_links {
2349 builder = builder.link_to_vector(vector);
2350 }
2351
2352 let id = builder.save()?;
2353 self.stamp_xmin_if_in_txn(&input.collection, id);
2355 self.index_store_ref()
2356 .index_entity_insert(&input.collection, id, &fields)
2357 .map_err(crate::RedDBError::Internal)?;
2358 refresh_context_index(&db, &input.collection, id)?;
2359 self.cdc_emit(
2360 crate::replication::cdc::ChangeOperation::Insert,
2361 &input.collection,
2362 id.raw(),
2363 "document",
2364 );
2365 Ok(CreateEntityOutput {
2366 id,
2367 entity: db.store().get(&input.collection, id),
2368 })
2369 }
2370
2371 fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
2372 crate::runtime::kv_write::create(self, input, None)
2373 }
2374
2375 fn create_timeseries_point(
2376 &self,
2377 input: CreateTimeSeriesPointInput,
2378 ) -> RedDBResult<CreateEntityOutput> {
2379 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2380 let db = self.db();
2381 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2382 contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
2383
2384 let mut fields = vec![
2385 ("metric".to_string(), reddb_types::Value::text(input.metric)),
2386 ("value".to_string(), reddb_types::Value::Float(input.value)),
2387 ];
2388
2389 if let Some(timestamp_ns) = input.timestamp_ns {
2390 fields.push((
2391 "timestamp".to_string(),
2392 reddb_types::Value::UnsignedInteger(timestamp_ns),
2393 ));
2394 }
2395
2396 if !input.tags.is_empty() {
2397 let tags_json = JsonValue::Object(
2398 input
2399 .tags
2400 .into_iter()
2401 .map(|(key, value)| (key, JsonValue::String(value)))
2402 .collect(),
2403 );
2404 let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
2405 crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
2406 })?;
2407 fields.push(("tags".to_string(), reddb_types::Value::Json(tags_bytes)));
2408 }
2409
2410 let collection = input.collection;
2411 let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
2412 self.stamp_xmin_if_in_txn(&collection, id);
2415 refresh_context_index(&db, &collection, id)?;
2416
2417 Ok(CreateEntityOutput {
2418 id,
2419 entity: db.store().get(&collection, id),
2420 })
2421 }
2422
2423 fn get_kv(
2424 &self,
2425 collection: &str,
2426 key: &str,
2427 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
2428 let resolver = crate::runtime::kv_read::KvReadResolver::for_rest_kv(self, collection)?;
2429 Ok(resolver
2430 .record(key)
2431 .map(|record| (record.value.unwrap_or(Value::Null), record.id)))
2432 }
2433
2434 fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
2435 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2436 let found = self.get_kv(collection, key)?;
2437 if let Some((_, id)) = found {
2438 let db = self.db();
2439 let store = db.store();
2440 let deleted = store
2441 .delete(collection, id)
2442 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2443 if deleted {
2444 store.context_index().remove_entity(id);
2445 }
2446 Ok(deleted)
2447 } else {
2448 Ok(false)
2449 }
2450 }
2451
2452 fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
2453 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2454 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2455 self,
2456 &input.collection,
2457 );
2458 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2459 let topology_lock = self
2460 .index_store_ref()
2461 .collection_topology_lock(&input.collection);
2462 let topology_guard = topology_lock.read();
2463 let PatchEntityInput {
2464 collection,
2465 id,
2466 payload,
2467 operations,
2468 } = input;
2469 let db = self.db();
2470 let store = db.store();
2471 let Some(manager) = store.get_collection(&collection) else {
2472 return Err(crate::RedDBError::NotFound(format!(
2473 "collection not found: {collection}"
2474 )));
2475 };
2476 let Some(entity) = manager.get(id) else {
2477 return Err(crate::RedDBError::NotFound(format!(
2478 "entity not found: {}",
2479 id.raw()
2480 )));
2481 };
2482 self.apply_loaded_patch_entity(collection, entity, payload, operations, topology_guard)
2483 }
2484
2485 fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
2486 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2487 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2488 self,
2489 &input.collection,
2490 );
2491 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2492 let topology_lock = self
2493 .index_store_ref()
2494 .collection_topology_lock(&input.collection);
2495 let topology_guard = topology_lock.read();
2496 let store = self.db().store();
2497 let pre_delete_fields = store
2501 .get(&input.collection, input.id)
2502 .as_ref()
2503 .map(entity_row_fields_snapshot)
2504 .unwrap_or_default();
2505 let deleted = store
2509 .delete(&input.collection, input.id)
2510 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2511 if deleted {
2512 #[cfg(test)]
2513 self.index_store_ref().mutation_test_hook(
2514 &input.collection,
2515 crate::runtime::MutationTestPhase::StoragePublished,
2516 );
2517 store.context_index().remove_entity(input.id);
2518 if !pre_delete_fields.is_empty() {
2521 self.index_store_ref()
2522 .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
2523 .map_err(crate::RedDBError::Internal)?;
2524 }
2525 drop(topology_guard);
2526 #[cfg(test)]
2527 self.index_store_ref().mutation_test_hook(
2528 &input.collection,
2529 crate::runtime::MutationTestPhase::BeforeEvents,
2530 );
2531 self.cdc_emit(
2532 crate::replication::cdc::ChangeOperation::Delete,
2533 &input.collection,
2534 input.id.raw(),
2535 "entity",
2536 );
2537 }
2538 Ok(DeleteEntityOutput {
2539 deleted,
2540 id: input.id,
2541 })
2542 }
2543}