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