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 row.named = Some(normalized_fields.into_iter().collect());
752 }
753 }
754 crate::storage::EntityData::Node(node) => {
755 let mut field_ops = Vec::new();
756 let mut metadata_ops = Vec::new();
757 let mut node_type_ops = Vec::new();
758
759 for mut op in operations {
760 let Some(root) = op.path.first().map(String::as_str) else {
761 return Err(crate::RedDBError::Query(
762 "patch path cannot be empty".to_string(),
763 ));
764 };
765
766 match root {
767 "fields" | "properties" => {
768 if op.path.len() < 2 {
769 return Err(crate::RedDBError::Query(
770 "patch path 'fields' requires a nested key".to_string(),
771 ));
772 }
773 op.path.remove(0);
774 field_ops.push(op);
775 }
776 "metadata" => {
777 if op.path.len() < 2 {
778 return Err(crate::RedDBError::Query(
779 "patch path 'metadata' requires a nested key".to_string(),
780 ));
781 }
782 op.path.remove(0);
783 metadata_ops.push(op);
784 }
785 "node_type" => {
786 if op.path.len() != 1 {
787 return Err(crate::RedDBError::Query(
788 "patch path 'node_type' does not allow nested keys".to_string(),
789 ));
790 }
791 op.path.clear();
792 node_type_ops.push(op);
793 }
794 _ => {
795 return Err(crate::RedDBError::Query(format!(
796 "unsupported patch target '{root}' for graph nodes. Use fields/*, properties/*, node_type, or metadata/*"
797 )));
798 }
799 }
800 }
801
802 for op in node_type_ops {
803 context_index_dirty = true;
804 let value = op.value.ok_or_else(|| {
805 crate::RedDBError::Query("node_type operations require a value".to_string())
806 })?;
807
808 match op.op {
809 PatchEntityOperationType::Unset => {
810 return Err(crate::RedDBError::Query(
811 "node_type cannot be unset through patch operations".to_string(),
812 ));
813 }
814 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
815 let Some(node_type) = value.as_str() else {
816 return Err(crate::RedDBError::Query(
817 "node_type operation requires a text value".to_string(),
818 ));
819 };
820 graph_node_type = Some(node_type.to_string());
821 modified_columns.push("node_type".to_string());
822 }
823 }
824 }
825
826 if !field_ops.is_empty() {
827 context_index_dirty = true;
828 apply_patch_operations_to_storage_map(&mut node.properties, &field_ops)?;
829 }
830
831 if let Some(fields) = payload
832 .get("fields")
833 .and_then(crate::json::Value::as_object)
834 {
835 context_index_dirty = true;
836 for (key, value) in fields {
837 node.properties
838 .insert(key.clone(), json_to_storage_value(value)?);
839 }
840 }
841
842 if !metadata_ops.is_empty() {
843 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
844 let metadata = patch_metadata.get_or_insert_with(|| {
845 store.get_metadata(&collection, id).unwrap_or_default()
846 });
847 let mut metadata_json = metadata_to_json(metadata);
848 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
849 .map_err(crate::RedDBError::Query)?;
850 *metadata = metadata_from_json(&metadata_json)?;
851 metadata_changed = true;
852 }
853 }
854 crate::storage::EntityData::Edge(edge) => {
855 let mut field_ops = Vec::new();
856 let mut metadata_ops = Vec::new();
857 let mut weight_ops = Vec::new();
858
859 for mut op in operations {
860 let Some(root) = op.path.first().map(String::as_str) else {
861 return Err(crate::RedDBError::Query(
862 "patch path cannot be empty".to_string(),
863 ));
864 };
865
866 match root {
867 "fields" | "properties" => {
868 if op.path.len() < 2 {
869 return Err(crate::RedDBError::Query(
870 "patch path 'fields' requires a nested key".to_string(),
871 ));
872 }
873 op.path.remove(0);
874 field_ops.push(op);
875 }
876 "weight" => {
877 if op.path.len() != 1 {
878 return Err(crate::RedDBError::Query(
879 "patch path 'weight' does not allow nested keys".to_string(),
880 ));
881 }
882 op.path.clear();
883 weight_ops.push(op);
884 }
885 "metadata" => {
886 if op.path.len() < 2 {
887 return Err(crate::RedDBError::Query(
888 "patch path 'metadata' requires a nested key".to_string(),
889 ));
890 }
891 op.path.remove(0);
892 metadata_ops.push(op);
893 }
894 _ => {
895 return Err(crate::RedDBError::Query(format!(
896 "unsupported patch target '{root}' for graph edges. Use fields/*, weight, metadata/*"
897 )));
898 }
899 }
900 }
901
902 if !field_ops.is_empty() {
903 context_index_dirty = true;
904 apply_patch_operations_to_storage_map(&mut edge.properties, &field_ops)?;
905 }
906
907 for op in weight_ops {
908 context_index_dirty = true;
909 let value = op.value.ok_or_else(|| {
910 crate::RedDBError::Query("weight operations require a value".to_string())
911 })?;
912
913 match op.op {
914 PatchEntityOperationType::Unset => {
915 return Err(crate::RedDBError::Query(
916 "weight cannot be unset through patch operations".to_string(),
917 ));
918 }
919 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
920 let Some(weight) = value.as_f64() else {
921 return Err(crate::RedDBError::Query(
922 "weight operation requires a numeric value".to_string(),
923 ));
924 };
925 graph_edge_weight = Some(weight as f32);
926 edge.weight = weight as f32;
927 modified_columns.push("weight".to_string());
928 }
929 }
930 }
931
932 if let Some(fields) = payload
933 .get("fields")
934 .and_then(crate::json::Value::as_object)
935 {
936 context_index_dirty = true;
937 for (key, value) in fields {
938 edge.properties
939 .insert(key.clone(), json_to_storage_value(value)?);
940 }
941 }
942
943 if !metadata_ops.is_empty() {
944 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
945 let metadata = patch_metadata.get_or_insert_with(|| {
946 store.get_metadata(&collection, id).unwrap_or_default()
947 });
948 let mut metadata_json = metadata_to_json(metadata);
949 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
950 .map_err(crate::RedDBError::Query)?;
951 *metadata = metadata_from_json(&metadata_json)?;
952 metadata_changed = true;
953 }
954 }
955 crate::storage::EntityData::Vector(vector) => {
956 let mut field_ops = Vec::new();
957 let mut metadata_ops = Vec::new();
958
959 for mut op in operations {
960 let Some(root) = op.path.first().map(String::as_str) else {
961 return Err(crate::RedDBError::Query(
962 "patch path cannot be empty".to_string(),
963 ));
964 };
965
966 match root {
967 "fields" => {
968 if op.path.len() < 2 {
969 return Err(crate::RedDBError::Query(
970 "patch path 'fields' requires a nested key".to_string(),
971 ));
972 }
973 op.path.remove(0);
974 let Some(target) = op.path.first().map(String::as_str) else {
975 return Err(crate::RedDBError::Query(
976 "patch path requires a target under fields".to_string(),
977 ));
978 };
979 if !matches!(target, "dense" | "content" | "sparse") {
980 return Err(crate::RedDBError::Query(format!(
981 "unsupported vector patch target '{target}'"
982 )));
983 }
984 field_ops.push(op);
985 }
986 "metadata" => {
987 if op.path.len() < 2 {
988 return Err(crate::RedDBError::Query(
989 "patch path 'metadata' requires a nested key".to_string(),
990 ));
991 }
992 op.path.remove(0);
993 metadata_ops.push(op);
994 }
995 _ => {
996 return Err(crate::RedDBError::Query(format!(
997 "unsupported patch target '{root}' for vectors. Use fields/* or metadata/*"
998 )));
999 }
1000 }
1001 }
1002
1003 if !field_ops.is_empty() {
1004 context_index_dirty = true;
1005 apply_patch_operations_to_vector_fields(vector, &field_ops)?;
1006 }
1007
1008 if let Some(fields) = payload
1009 .get("fields")
1010 .and_then(crate::json::Value::as_object)
1011 {
1012 context_index_dirty = true;
1013 if let Some(content) =
1014 fields.get("content").and_then(crate::json::Value::as_str)
1015 {
1016 vector.content = Some(content.to_string());
1017 }
1018 if let Some(dense) = fields.get("dense") {
1019 vector.dense = dense
1020 .as_array()
1021 .ok_or_else(|| {
1022 crate::RedDBError::Query(
1023 "field 'dense' must be an array".to_string(),
1024 )
1025 })?
1026 .iter()
1027 .map(|value| {
1028 value.as_f64().map(|value| value as f32).ok_or_else(|| {
1029 crate::RedDBError::Query(
1030 "field 'dense' must contain only numbers".to_string(),
1031 )
1032 })
1033 })
1034 .collect::<Result<Vec<_>, _>>()?;
1035 }
1036 }
1037
1038 if !metadata_ops.is_empty() {
1039 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1040 let metadata = patch_metadata.get_or_insert_with(|| {
1041 store.get_metadata(&collection, id).unwrap_or_default()
1042 });
1043 let mut metadata_json = metadata_to_json(metadata);
1044 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1045 .map_err(crate::RedDBError::Query)?;
1046 *metadata = metadata_from_json(&metadata_json)?;
1047 metadata_changed = true;
1048 }
1049 }
1050 crate::storage::EntityData::TimeSeries(_)
1051 | crate::storage::EntityData::QueueMessage(_) => {
1052 return Err(crate::RedDBError::Query(
1053 "patch operations are not supported for TimeSeries or QueueMessage entities"
1054 .to_string(),
1055 ));
1056 }
1057 }
1058
1059 if let Some(node_type) = graph_node_type {
1060 if let crate::storage::EntityKind::GraphNode(node) = &mut entity.kind {
1061 node.node_type = node_type;
1062 }
1063 }
1064 if let Some(weight) = graph_edge_weight {
1065 if let crate::storage::EntityKind::GraphEdge(edge) = &mut entity.kind {
1066 edge.weight = (weight * 1000.0) as u32;
1067 }
1068 }
1069
1070 if let Some(metadata) = payload
1071 .get("metadata")
1072 .and_then(crate::json::Value::as_object)
1073 {
1074 let patch_metadata = patch_metadata
1075 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1076 for (key, value) in metadata {
1077 ensure_non_tree_reserved_metadata_key(key)?;
1078 patch_metadata.set(key.clone(), json_to_metadata_value(value)?);
1079 }
1080 metadata_changed = true;
1081 }
1082
1083 for (key, value) in parse_top_level_ttl_metadata_entries(&payload)? {
1084 let patch_metadata = patch_metadata
1085 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1086 if matches!(value, crate::storage::unified::MetadataValue::Null) {
1087 patch_metadata.remove(&key);
1088 } else {
1089 patch_metadata.set(key, value);
1090 }
1091 metadata_changed = true;
1092 }
1093
1094 entity.updated_at = std::time::SystemTime::now()
1095 .duration_since(std::time::UNIX_EPOCH)
1096 .unwrap_or_default()
1097 .as_secs();
1098
1099 if let Some(xid) = versioned_update_xid {
1105 let logical_id = entity.logical_id();
1106 entity.id = store.next_entity_id();
1107 entity.set_logical_id(logical_id);
1108 entity.set_xmin(xid);
1109 entity.set_xmax(0);
1110 if let Some(old) = replaced_entity.as_mut() {
1111 old.set_xmax(xid);
1112 }
1113 }
1114
1115 modified_columns = dedupe_modified_columns(modified_columns);
1116
1117 Ok(AppliedEntityMutation {
1118 id: entity.id,
1119 collection,
1120 entity,
1121 metadata: patch_metadata,
1122 modified_columns,
1123 persist_metadata: metadata_changed,
1124 context_index_dirty,
1125 replaced_entity,
1126 replaced_entity_previous_xmax: previous_xmax,
1127 pre_mutation_fields,
1128 })
1129 }
1130
1131 pub(crate) fn apply_loaded_sql_update_row_core(
1132 &self,
1133 collection: String,
1134 mut entity: crate::storage::UnifiedEntity,
1135 static_field_assignments: &[(String, Value)],
1136 dynamic_field_assignments: Vec<(String, Value)>,
1137 static_metadata_assignments: &[(String, MetadataValue)],
1138 dynamic_metadata_assignments: Vec<(String, MetadataValue)>,
1139 row_contract_plan: Option<&RowUpdateContractPlan>,
1140 row_modified_columns_template: &[String],
1141 row_touches_unique_columns: bool,
1142 ) -> RedDBResult<AppliedEntityMutation> {
1143 let id = entity.id;
1144 let previous_xmax = entity.xmax;
1145 let db = self.db();
1146 let store = db.store();
1147 let Some(_) = store.get_collection(&collection) else {
1148 return Err(crate::RedDBError::NotFound(format!(
1149 "collection not found: {collection}"
1150 )));
1151 };
1152
1153 let versioned_update_xid = match self.current_xid() {
1154 Some(xid) => Some(xid),
1155 None => {
1156 let snapshot_manager = self.snapshot_manager();
1157 let xid = snapshot_manager.begin();
1158 snapshot_manager.commit(xid);
1159 Some(xid)
1160 }
1161 };
1162 let mut replaced_entity = versioned_update_xid.map(|xid| {
1163 let mut old = entity.clone();
1164 old.set_xmax(xid);
1165 old
1166 });
1167
1168 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
1169 let row_contract_timestamps = row_contract_plan
1170 .map(|plan| plan.timestamps_enabled)
1171 .unwrap_or(false);
1172 let mut metadata_changed = false;
1173 let mut modified_columns = row_modified_columns_template.to_vec();
1174 let mut context_index_dirty = !modified_columns.is_empty();
1175
1176 let pre_mutation_fields = entity_row_fields_snapshot(&entity);
1180
1181 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1182 return Err(crate::RedDBError::Query(
1183 "SQL row update fast path requires a row entity".to_string(),
1184 ));
1185 };
1186
1187 let _ = row_contract_plan;
1188
1189 let is_document_collection = db
1195 .collection_contract(&collection)
1196 .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Document)
1197 .unwrap_or(false);
1198 let kv_key = if row.get_field("value").is_some() || row.columns.len() >= 2 {
1199 row.get_field("key")
1200 .cloned()
1201 .or_else(|| row.columns.first().cloned())
1202 } else {
1203 None
1204 };
1205 if is_document_collection {
1206 let body_assignment = static_field_assignments
1210 .iter()
1211 .chain(dynamic_field_assignments.iter())
1212 .rev()
1213 .find(|(column, _)| column == "body")
1214 .cloned();
1215
1216 match body_assignment {
1217 Some((_, value)) => {
1218 let body = document_body_from_assignment(&value)?;
1219 replace_document_row_body(
1220 row,
1221 body,
1222 &collection,
1223 self.binary_document_body_enabled(),
1224 &mut modified_columns,
1225 )?;
1226 context_index_dirty = true;
1227 }
1228 None => {
1229 let mut static_row_assignments = Vec::new();
1232 let mut dynamic_row_assignments = Vec::new();
1233 let mut document_body_ops = Vec::new();
1234 for (column, value) in static_field_assignments.iter().cloned() {
1235 if column == "body" {
1236 static_row_assignments.push((column, value));
1237 } else {
1238 document_body_ops.push(document_body_set_operation(&column, value));
1239 }
1240 }
1241 for (column, value) in dynamic_field_assignments {
1242 if column == "body" {
1243 dynamic_row_assignments.push((column, value));
1244 } else {
1245 document_body_ops.push(document_body_set_operation(&column, value));
1246 }
1247 }
1248 apply_row_field_assignments_raw(row, static_row_assignments);
1249 apply_row_field_assignments_raw(row, dynamic_row_assignments);
1250 if !document_body_ops.is_empty() {
1251 let mut body = document_body_from_named(
1252 row.named.get_or_insert_with(Default::default),
1253 )?;
1254 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
1255 replace_document_row_body(
1256 row,
1257 body,
1258 &collection,
1259 self.binary_document_body_enabled(),
1260 &mut modified_columns,
1261 )?;
1262 context_index_dirty = true;
1263 }
1264 }
1265 }
1266 } else {
1267 let mut all_assignments: Vec<(String, Value)> = static_field_assignments.to_vec();
1269 all_assignments.extend(dynamic_field_assignments);
1270 apply_row_field_assignments_raw(row, all_assignments);
1271 if let Some(key) = kv_key {
1272 set_row_field(row, "key", key);
1273 }
1274 }
1275
1276 for (key, value) in static_metadata_assignments
1277 .iter()
1278 .cloned()
1279 .chain(dynamic_metadata_assignments)
1280 {
1281 ensure_non_tree_reserved_metadata_key(&key)?;
1282 patch_metadata
1283 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default())
1284 .set(key, value);
1285 metadata_changed = true;
1286 }
1287
1288 if !modified_columns.is_empty() || row_contract_timestamps {
1289 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1290 if row_contract_timestamps {
1291 context_index_dirty = true;
1292 set_row_field(row, "updated_at", contract.managed_timestamp_value());
1293 modified_columns.push("updated_at".to_string());
1294 }
1295 let expression_contract = if row_contract_plan.is_some_and(|plan| plan.has_expressions)
1296 {
1297 db.collection_contract_arc(&collection)
1298 } else {
1299 None
1300 };
1301 if let Some(schema) = &expression_contract {
1302 let normalized = contract.normalize_update_fields(collect_row_fields(row))?;
1303 for column in schema
1304 .declared_columns
1305 .iter()
1306 .filter(|column| column.generated.is_some())
1307 {
1308 modified_columns.push(column.name.clone());
1309 }
1310 for (name, value) in normalized {
1311 set_row_field(row, &name, value);
1312 }
1313 context_index_dirty = true;
1314 }
1315 if row_touches_unique_columns || expression_contract.is_some() {
1316 let current_fields = collect_row_fields(row);
1317 contract.enforce_row_uniqueness(¤t_fields, Some(id))?;
1318 }
1319 }
1320
1321 entity.updated_at = std::time::SystemTime::now()
1322 .duration_since(std::time::UNIX_EPOCH)
1323 .unwrap_or_default()
1324 .as_secs();
1325
1326 if let Some(xid) = versioned_update_xid {
1327 let logical_id = entity.logical_id();
1328 entity.id = store.next_entity_id();
1329 entity.set_logical_id(logical_id);
1330 entity.set_xmin(xid);
1331 entity.set_xmax(0);
1332 if let Some(old) = replaced_entity.as_mut() {
1333 old.set_xmax(xid);
1334 }
1335 }
1336
1337 modified_columns = dedupe_modified_columns(modified_columns);
1338
1339 Ok(AppliedEntityMutation {
1340 id: entity.id,
1341 collection,
1342 entity,
1343 metadata: patch_metadata,
1344 modified_columns,
1345 persist_metadata: metadata_changed,
1346 context_index_dirty,
1347 replaced_entity,
1348 replaced_entity_previous_xmax: previous_xmax,
1349 pre_mutation_fields,
1350 })
1351 }
1352
1353 pub(crate) fn persist_applied_entity_mutations(
1354 &self,
1355 applied: &[AppliedEntityMutation],
1356 ) -> RedDBResult<()> {
1357 if applied.is_empty() {
1358 return Ok(());
1359 }
1360
1361 let store = self.db().store();
1362 let collection = &applied[0].collection;
1363 let Some(manager) = store.get_collection(collection) else {
1364 return Err(crate::RedDBError::NotFound(format!(
1365 "collection not found: {collection}"
1366 )));
1367 };
1368
1369 let mut ordinary = Vec::with_capacity(applied.len());
1370 for item in applied {
1371 if let Some(old_version) = item.replaced_entity.as_ref() {
1372 store
1373 .install_versioned_table_row_update(
1374 collection,
1375 old_version.clone(),
1376 item.entity.clone(),
1377 if item.persist_metadata {
1378 item.metadata.as_ref()
1379 } else {
1380 None
1381 },
1382 )
1383 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
1384 if self.current_xid().is_some() {
1385 self.record_pending_versioned_update(
1386 crate::runtime::impl_core::current_connection_id(),
1387 collection,
1388 old_version.id,
1389 item.entity.id,
1390 old_version.xmax,
1391 item.replaced_entity_previous_xmax,
1392 );
1393 }
1394 } else {
1395 ordinary.push(item);
1396 }
1397 }
1398 if ordinary.is_empty() {
1399 return Ok(());
1400 }
1401
1402 manager
1403 .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
1404 (
1405 &item.entity,
1406 item.modified_columns.as_slice(),
1407 if item.persist_metadata {
1408 item.metadata.as_ref()
1409 } else {
1410 None
1411 },
1412 )
1413 }))
1414 .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
1415
1416 let indexed_cols = self
1425 .index_store_ref()
1426 .indexed_columns_set(collection.as_str());
1427 let all_hot = !indexed_cols.is_empty()
1428 && ordinary.iter().all(|item| {
1429 !item.persist_metadata
1430 && !item
1431 .modified_columns
1432 .iter()
1433 .any(|c| indexed_cols.contains(c))
1434 })
1435 || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
1436
1437 let entity_refs: Vec<&crate::storage::UnifiedEntity> =
1441 ordinary.iter().map(|item| &item.entity).collect();
1442 let persist_fn = if all_hot {
1443 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
1444 } else {
1445 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
1446 };
1447 persist_fn(store.as_ref(), collection, &entity_refs)
1448 .map_err(|err| crate::RedDBError::Internal(err.to_string()))
1449 }
1450
1451 pub(crate) fn flush_applied_entity_mutation(
1452 &self,
1453 applied: &AppliedEntityMutation,
1454 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1455 ) -> RedDBResult<()> {
1456 let store = self.db().store();
1457 if applied.context_index_dirty {
1458 store
1459 .context_index()
1460 .index_entity(&applied.collection, &applied.entity);
1461 }
1462 let mut changed_columns: Option<Vec<String>> = None;
1470 if !applied.pre_mutation_fields.is_empty() {
1471 let post = entity_row_fields_snapshot(&applied.entity);
1472 if !post.is_empty() {
1473 let damage = crate::application::entity::row_damage_vector(
1474 &applied.pre_mutation_fields,
1475 &post,
1476 );
1477 if !damage.is_empty() {
1478 changed_columns = Some(
1479 damage
1480 .touched_columns()
1481 .into_iter()
1482 .map(str::to_string)
1483 .collect(),
1484 );
1485 }
1486
1487 let indexed_cols: std::collections::HashSet<String> = self
1499 .index_store_ref()
1500 .indexed_columns_set_with_parents(applied.collection.as_str());
1501 let pre_index_fields = self
1505 .index_store_ref()
1506 .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
1507 let post_index_fields = self
1508 .index_store_ref()
1509 .augment_body_derived_index_fields(&post, &indexed_cols);
1510 let modified_cols: std::collections::HashSet<String> =
1511 crate::application::entity::row_damage_vector(
1512 &pre_index_fields,
1513 &post_index_fields,
1514 )
1515 .touched_columns()
1516 .into_iter()
1517 .map(str::to_string)
1518 .collect();
1519 if let Some(old_version) = applied.replaced_entity.as_ref() {
1520 let old_index_fields: Vec<(String, Value)> = pre_index_fields
1521 .iter()
1522 .filter(|(col, _)| indexed_cols.contains(col))
1523 .cloned()
1524 .collect();
1525 let new_index_fields: Vec<(String, Value)> = post_index_fields
1526 .iter()
1527 .filter(|(col, _)| indexed_cols.contains(col))
1528 .cloned()
1529 .collect();
1530 if !old_index_fields.is_empty() {
1531 self.index_store_ref()
1532 .index_entity_delete(
1533 &applied.collection,
1534 old_version.id,
1535 &old_index_fields,
1536 )
1537 .map_err(crate::RedDBError::Internal)?;
1538 }
1539 if !new_index_fields.is_empty() {
1540 self.index_store_ref()
1541 .index_entity_insert(
1542 &applied.collection,
1543 applied.entity.id,
1544 &new_index_fields,
1545 )
1546 .map_err(crate::RedDBError::Internal)?;
1547 }
1548 } else {
1549 let decision = crate::storage::engine::hot_update::decide(
1550 &crate::storage::engine::hot_update::HotUpdateInputs {
1551 collection: applied.collection.as_str(),
1552 indexed_columns: &indexed_cols,
1553 modified_columns: &modified_cols,
1554 new_tuple_size: 0,
1558 page_free_space: usize::MAX,
1559 },
1560 );
1561 if !decision.can_hot {
1562 self.index_store_ref()
1563 .index_entity_update(
1564 &applied.collection,
1565 applied.id,
1566 &pre_index_fields,
1567 &post_index_fields,
1568 )
1569 .map_err(crate::RedDBError::Internal)?;
1570 } else {
1571 tracing::debug!(
1575 collection = %reddb_wire::audit_safe_log_field(&applied.collection),
1576 "hot_update fast-path: skipped index_entity_update"
1577 );
1578 }
1579 }
1580 }
1581 }
1582 drop(topology_guard);
1583 #[cfg(test)]
1584 self.index_store_ref().mutation_test_hook(
1585 &applied.collection,
1586 crate::runtime::MutationTestPhase::BeforeEvents,
1587 );
1588 self.cdc_emit_prebuilt_with_columns(
1589 crate::replication::cdc::ChangeOperation::Update,
1590 &applied.collection,
1591 &applied.entity,
1592 "entity",
1593 applied.metadata.as_ref(),
1594 true,
1595 changed_columns,
1596 );
1597 Ok(())
1598 }
1599
1600 pub(crate) fn apply_loaded_patch_entity(
1601 &self,
1602 collection: String,
1603 entity: crate::storage::UnifiedEntity,
1604 payload: JsonValue,
1605 operations: Vec<PatchEntityOperation>,
1606 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1607 ) -> RedDBResult<CreateEntityOutput> {
1608 let applied =
1609 self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
1610 let _reservation = self.admit_entity_mutation(&applied)?;
1611 self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
1612 #[cfg(test)]
1613 self.index_store_ref().mutation_test_hook(
1614 &applied.collection,
1615 crate::runtime::MutationTestPhase::StoragePublished,
1616 );
1617 self.flush_applied_entity_mutation(&applied, topology_guard)?;
1618 Ok(CreateEntityOutput {
1619 id: applied.id,
1620 entity: Some(public_document_entity(applied.entity)),
1621 })
1622 }
1623}
1624
1625fn public_document_entity(
1634 mut entity: crate::storage::UnifiedEntity,
1635) -> crate::storage::UnifiedEntity {
1636 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1637 return entity;
1638 };
1639 let Some(named) = row.named.as_mut() else {
1640 return entity;
1641 };
1642 let Some(Value::Json(bytes)) = named.get("body") else {
1643 return entity;
1644 };
1645 let bytes = bytes.clone();
1646 if let Some(fields) = crate::document_body::body_fields(&bytes) {
1647 for (name, value) in fields {
1648 named.entry(name).or_insert(value);
1649 }
1650 }
1651 if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
1652 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
1653 named.insert("body".to_string(), Value::Json(json_bytes));
1654 }
1655 }
1656 entity
1657}
1658
1659fn ensure_non_tree_reserved_metadata_patch_paths(
1660 operations: &[PatchEntityOperation],
1661) -> RedDBResult<()> {
1662 for operation in operations {
1663 let Some(key) = operation.path.first().map(String::as_str) else {
1664 continue;
1665 };
1666 ensure_non_tree_reserved_metadata_key(key)?;
1667 }
1668 Ok(())
1669}
1670
1671fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
1672 if key.starts_with(TREE_METADATA_PREFIX) {
1673 return Err(crate::RedDBError::Query(format!(
1674 "metadata key '{}' is reserved for managed trees",
1675 key
1676 )));
1677 }
1678 Ok(())
1679}
1680
1681fn ensure_non_tree_reserved_metadata_entries(
1682 metadata: &[(String, MetadataValue)],
1683) -> RedDBResult<()> {
1684 for (key, _) in metadata {
1685 ensure_non_tree_reserved_metadata_key(key)?;
1686 }
1687 Ok(())
1688}
1689
1690fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
1691 if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
1692 return Err(crate::RedDBError::Query(format!(
1693 "edge label '{}' is reserved for managed trees",
1694 TREE_CHILD_EDGE_LABEL
1695 )));
1696 }
1697 Ok(())
1698}
1699
1700impl RedDBRuntime {
1701 pub(crate) fn create_node_unchecked(
1702 &self,
1703 input: CreateNodeInput,
1704 ) -> RedDBResult<CreateEntityOutput> {
1705 let db = self.db();
1706 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1707 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1708 let mut metadata = input.metadata;
1709 contract.apply_default_ttl(&mut metadata);
1710 let mut builder = db.node(&input.collection, &input.label);
1711
1712 if let Some(node_type) = input.node_type {
1713 builder = builder.node_type(node_type);
1714 }
1715
1716 for (key, value) in input.properties {
1717 builder = builder.property(key, value);
1718 }
1719
1720 for (key, value) in metadata {
1721 builder = builder.metadata(key, value);
1722 }
1723
1724 for embedding in input.embeddings {
1725 if let Some(model) = embedding.model {
1726 builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
1727 } else {
1728 builder = builder.embedding(embedding.name, embedding.vector);
1729 }
1730 }
1731
1732 for link in input.table_links {
1733 builder = builder.link_to_table(link.key, link.table);
1734 }
1735
1736 for link in input.node_links {
1737 builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
1738 }
1739
1740 let id = builder.save()?;
1741 self.stamp_xmin_if_in_txn(&input.collection, id);
1744 refresh_context_index(&db, &input.collection, id)?;
1745 self.cdc_emit(
1746 crate::replication::cdc::ChangeOperation::Insert,
1747 &input.collection,
1748 id.raw(),
1749 "graph_node",
1750 );
1751 Ok(CreateEntityOutput {
1752 id,
1753 entity: db.store().get(&input.collection, id),
1754 })
1755 }
1756
1757 pub(crate) fn create_edge_unchecked(
1758 &self,
1759 input: CreateEdgeInput,
1760 ) -> RedDBResult<CreateEntityOutput> {
1761 let db = self.db();
1762 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1763 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1764 let mut metadata = input.metadata;
1765 contract.apply_default_ttl(&mut metadata);
1766 let mut builder = db
1767 .edge(&input.collection, &input.label)
1768 .from(input.from)
1769 .to(input.to);
1770
1771 if let Some(weight) = input.weight {
1772 builder = builder.weight(weight);
1773 }
1774
1775 for (key, value) in input.properties {
1776 builder = builder.property(key, value);
1777 }
1778
1779 for (key, value) in metadata {
1780 builder = builder.metadata(key, value);
1781 }
1782
1783 let id = builder.save()?;
1784 self.stamp_xmin_if_in_txn(&input.collection, id);
1787 refresh_context_index(&db, &input.collection, id)?;
1788 self.cdc_emit(
1789 crate::replication::cdc::ChangeOperation::Insert,
1790 &input.collection,
1791 id.raw(),
1792 "graph_edge",
1793 );
1794 Ok(CreateEntityOutput {
1795 id,
1796 entity: db.store().get(&input.collection, id),
1797 })
1798 }
1799}
1800
1801fn create_rows_batch_prevalidated_columnar_with_outputs(
1802 runtime: &RedDBRuntime,
1803 collection: String,
1804 column_names: std::sync::Arc<Vec<String>>,
1805 rows: Vec<Vec<reddb_types::Value>>,
1806) -> RedDBResult<Vec<CreateEntityOutput>> {
1807 use crate::storage::{
1808 unified::{EntityData, EntityKind, RowData},
1809 EntityId, UnifiedEntity,
1810 };
1811 use std::sync::Arc;
1812
1813 if rows.is_empty() {
1814 return Ok(Vec::new());
1815 }
1816 if runtime.index_store_ref().has_unique_hash_index(&collection) {
1819 return runtime.create_rows_batch_columnar_with_outputs(collection, column_names, rows);
1820 }
1821 runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1822 runtime.check_batch_size(rows.len())?;
1823 runtime.check_db_size()?;
1824
1825 let db = runtime.db();
1826 let contract = CollectionContractWriteEnforcer::new(runtime, &db, &collection);
1827 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1828
1829 let store = db.store();
1830 let table_arc: Arc<str> = Arc::from(collection.as_str());
1831
1832 let indexed_cols = runtime
1833 .index_store_ref()
1834 .indexed_columns_set(collection.as_str());
1835 let has_secondary_indexes = !indexed_cols.is_empty();
1836 let mut field_snapshots: Vec<Vec<(String, reddb_types::Value)>> = if has_secondary_indexes {
1837 Vec::with_capacity(rows.len())
1838 } else {
1839 Vec::new()
1840 };
1841
1842 let entities: Vec<UnifiedEntity> = rows
1843 .into_iter()
1844 .map(|values| {
1845 if has_secondary_indexes {
1846 let mut snap: Vec<(String, reddb_types::Value)> =
1847 Vec::with_capacity(indexed_cols.len());
1848 for (name, val) in column_names.iter().zip(values.iter()) {
1849 if indexed_cols.contains(name) {
1850 snap.push((name.clone(), val.clone()));
1851 }
1852 }
1853 field_snapshots.push(snap);
1854 }
1855 let mut row = RowData::new(values);
1856 row.schema = Some(Arc::clone(&column_names));
1857 UnifiedEntity::new(
1858 EntityId::new(0),
1859 EntityKind::TableRow {
1860 table: Arc::clone(&table_arc),
1861 row_id: 0,
1862 },
1863 EntityData::Row(row),
1864 )
1865 })
1866 .collect();
1867
1868 let ids = store
1869 .bulk_insert(&collection, entities)
1870 .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
1871
1872 if has_secondary_indexes {
1873 let index_rows: Vec<(EntityId, Vec<(String, reddb_types::Value)>)> = ids
1874 .iter()
1875 .zip(field_snapshots)
1876 .map(|(id, fields)| (*id, fields))
1877 .collect();
1878 runtime
1879 .index_store_ref()
1880 .index_entity_insert_batch(&collection, &index_rows)
1881 .map_err(crate::RedDBError::Internal)?;
1882 }
1883
1884 runtime.invalidate_result_cache();
1885 runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
1886
1887 Ok(ids
1888 .into_iter()
1889 .map(|id| CreateEntityOutput { id, entity: None })
1890 .collect())
1891}
1892
1893impl RuntimeEntityPort for RedDBRuntime {
1894 fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
1895 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1896 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
1897 self,
1898 &input.collection,
1899 );
1900 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
1901 let db = self.db();
1902 let CreateRowInput {
1903 collection,
1904 fields,
1905 metadata: input_metadata,
1906 node_links,
1907 vector_links,
1908 } = input;
1909 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1910 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1911 let mut metadata = input_metadata;
1912 contract.apply_default_ttl(&mut metadata);
1913 let fields = contract.normalize_insert_fields(fields)?;
1914 contract.enforce_row_uniqueness(&fields, None)?;
1915 let engine = self.mutation_engine();
1917 let result = engine.apply(
1918 collection.clone(),
1919 vec![crate::runtime::mutation::MutationRow {
1920 fields,
1921 metadata,
1922 node_links,
1923 vector_links,
1924 }],
1925 )?;
1926 let id = result.ids[0];
1927 Ok(CreateEntityOutput {
1932 id,
1933 entity: db.store().get(&collection, id),
1934 })
1935 }
1936
1937 fn create_rows_batch(
1938 &self,
1939 input: CreateRowsBatchInput,
1940 ) -> RedDBResult<Vec<CreateEntityOutput>> {
1941 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
1942 self,
1943 &input.collection,
1944 );
1945 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
1946 if input.rows.is_empty() {
1947 return Ok(Vec::new());
1948 }
1949 self.check_batch_size(input.rows.len())?;
1950 self.check_db_size()?;
1951
1952 let db = self.db();
1953 let collection = input.collection;
1954 let suppress_events = input.suppress_events;
1955 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1956 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1957
1958 let mut prepared_rows = Vec::with_capacity(input.rows.len());
1959 let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
1960 for row in input.rows {
1961 if row.collection != collection {
1962 return Err(crate::RedDBError::Query(format!(
1963 "batch row collection mismatch: expected '{}', got '{}'",
1964 collection, row.collection
1965 )));
1966 }
1967
1968 let mut metadata = row.metadata;
1969 contract.apply_default_ttl(&mut metadata);
1970 let fields = contract.normalize_insert_fields(row.fields)?;
1971 contract.enforce_row_uniqueness(&fields, None)?;
1972 uniqueness_rows.push(fields.clone());
1973 prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
1974 }
1975
1976 contract.enforce_batch_uniqueness(&uniqueness_rows)?;
1977
1978 let engine = {
1981 let e = self.mutation_engine();
1982 if suppress_events {
1983 e.with_suppress_events()
1984 } else {
1985 e
1986 }
1987 };
1988 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
1989 .into_iter()
1990 .map(|(fields, metadata, node_links, vector_links)| {
1991 crate::runtime::mutation::MutationRow {
1992 fields,
1993 metadata,
1994 node_links,
1995 vector_links,
1996 }
1997 })
1998 .collect();
1999
2000 let result = engine
2001 .apply(collection.clone(), mutation_rows)
2002 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
2003
2004 let store = db.store();
2005 Ok(result
2006 .ids
2007 .into_iter()
2008 .map(|id| CreateEntityOutput {
2009 id,
2010 entity: store.get(&collection, id),
2011 })
2012 .collect())
2013 }
2014
2015 fn create_rows_batch_prevalidated_columnar(
2016 &self,
2017 collection: String,
2018 column_names: std::sync::Arc<Vec<String>>,
2019 rows: Vec<Vec<reddb_types::Value>>,
2020 ) -> RedDBResult<usize> {
2021 create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2022 .map(|outputs| outputs.len())
2023 }
2024
2025 fn create_rows_batch_columnar(
2026 &self,
2027 collection: String,
2028 column_names: std::sync::Arc<Vec<String>>,
2029 rows: Vec<Vec<reddb_types::Value>>,
2030 ) -> RedDBResult<usize> {
2031 self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2032 .map(|outputs| outputs.len())
2033 }
2034
2035 fn create_rows_batch_columnar_with_outputs(
2036 &self,
2037 collection: String,
2038 column_names: std::sync::Arc<Vec<String>>,
2039 rows: Vec<Vec<reddb_types::Value>>,
2040 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2041 if rows.is_empty() {
2042 return Ok(Vec::new());
2043 }
2044 self.check_batch_size(rows.len())?;
2045 self.check_db_size()?;
2046
2047 let db = self.db();
2048 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2049 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2050
2051 let needs_normalisation = self.index_store_ref().has_unique_hash_index(&collection)
2061 || match db.collection_contract(&collection) {
2062 Some(c) => {
2063 c.declared_model == crate::catalog::CollectionModel::Table
2064 && (!c.declared_columns.is_empty()
2065 || c.table_def
2066 .as_ref()
2067 .map(|t| !t.columns.is_empty())
2068 .unwrap_or(false))
2069 }
2070 None => false,
2071 };
2072 if !needs_normalisation {
2073 return create_rows_batch_prevalidated_columnar_with_outputs(
2074 self,
2075 collection,
2076 column_names,
2077 rows,
2078 );
2079 }
2080
2081 let ncols = column_names.len();
2088 let tuple_rows: Vec<CreateRowInput> = rows
2089 .into_iter()
2090 .map(|values| {
2091 let mut fields: Vec<(String, reddb_types::Value)> = Vec::with_capacity(ncols);
2092 for (name, value) in column_names.iter().zip(values) {
2093 fields.push((name.clone(), value));
2094 }
2095 CreateRowInput {
2096 collection: collection.clone(),
2097 fields,
2098 metadata: Vec::new(),
2099 node_links: Vec::new(),
2100 vector_links: Vec::new(),
2101 }
2102 })
2103 .collect();
2104 self.create_rows_batch(CreateRowsBatchInput {
2105 collection,
2106 rows: tuple_rows,
2107 suppress_events: false,
2108 })
2109 }
2110
2111 fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
2112 self.create_rows_batch_prevalidated_with_outputs(input)
2113 .map(|outputs| outputs.len())
2114 }
2115
2116 fn create_rows_batch_prevalidated_with_outputs(
2117 &self,
2118 input: CreateRowsBatchInput,
2119 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2120 if input.rows.is_empty() {
2121 return Ok(Vec::new());
2122 }
2123 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2124 self.check_batch_size(input.rows.len())?;
2125 self.check_db_size()?;
2126
2127 let db = self.db();
2128 let collection = input.collection;
2129 let suppress_events = input.suppress_events;
2130 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2135 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2136
2137 let default_ttl_plan = contract.default_ttl_plan();
2143
2144 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
2145 Vec::with_capacity(input.rows.len());
2146 let mut mutation_rows = mutation_rows;
2147 for row in input.rows {
2148 if row.collection != collection {
2149 return Err(crate::RedDBError::Query(format!(
2150 "batch row collection mismatch: expected '{}', got '{}'",
2151 collection, row.collection
2152 )));
2153 }
2154 let mut metadata = row.metadata;
2155 default_ttl_plan.apply(&mut metadata);
2156 mutation_rows.push(crate::runtime::mutation::MutationRow {
2157 fields: row.fields,
2158 metadata,
2159 node_links: row.node_links,
2160 vector_links: row.vector_links,
2161 });
2162 }
2163
2164 let engine = {
2165 let engine = self.mutation_engine();
2166 if suppress_events {
2167 engine.with_suppress_events()
2168 } else {
2169 engine
2170 }
2171 };
2172 let result = engine
2173 .apply(collection.clone(), mutation_rows)
2174 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
2175 let store = db.store();
2176 Ok(result
2177 .ids
2178 .into_iter()
2179 .map(|id| CreateEntityOutput {
2180 id,
2181 entity: store.get(&collection, id),
2182 })
2183 .collect())
2184 }
2185
2186 fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
2187 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2188 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2189 input.properties.iter().map(|(key, _)| key.as_str()),
2190 &format!("node '{}'", input.collection),
2191 )?;
2192 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2193 self.create_node_unchecked(input)
2194 }
2195
2196 fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
2197 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2198 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2199 input.properties.iter().map(|(key, _)| key.as_str()),
2200 &format!("edge '{}'", input.collection),
2201 )?;
2202 ensure_non_tree_structural_edge_label(&input.label)?;
2203 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2204 self.create_edge_unchecked(input)
2205 }
2206
2207 fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
2208 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2209 let db = self.db();
2210 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2211 contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
2212 contract.ensure_vector_dimension(input.dense.len())?;
2213 let mut metadata = input.metadata;
2214 contract.apply_default_ttl(&mut metadata);
2215 let mut builder = db.vector(&input.collection).dense(input.dense);
2216
2217 if let Some(content) = input.content {
2218 builder = builder.content(content);
2219 }
2220
2221 for (key, value) in metadata {
2222 builder = builder.metadata(key, value);
2223 }
2224
2225 if let Some(link_row) = input.link_row {
2226 builder = builder.link_to_table(link_row);
2227 }
2228
2229 if let Some(link_node) = input.link_node {
2230 builder = builder.link_to_node(link_node);
2231 }
2232
2233 let id = builder.save_with_xmin(self.current_xid())?;
2236 let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
2237 Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
2238 _ => None,
2239 };
2240 refresh_context_index(&db, &input.collection, id)?;
2241 if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
2247 state.ensure_populated(&db.store(), &input.collection);
2251 use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
2253 fire(InjectionPoint::BeforeWalFsync);
2254 let _ = db
2255 .store()
2256 .append_vector_insert_record(&input.collection, id.raw(), &dense);
2257 fire(InjectionPoint::BeforeIndexCommit);
2258 let mut index = state.index.lock();
2259 index.insert(id, dense.clone());
2260 fire(InjectionPoint::BeforeExtentFsync);
2267 if let Some(extent) = state.extent.lock().as_mut() {
2268 let packed = index.encode_for_extent(&dense);
2269 let _ = extent.append(&packed);
2270 }
2271 }
2272 self.cdc_emit(
2273 crate::replication::cdc::ChangeOperation::Insert,
2274 &input.collection,
2275 id.raw(),
2276 "vector",
2277 );
2278 Ok(CreateEntityOutput {
2279 id,
2280 entity: db.store().get(&input.collection, id),
2281 })
2282 }
2283
2284 fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
2285 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2286 let db = self.db();
2287 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2288 contract.ensure_model(crate::catalog::CollectionModel::Document)?;
2289
2290 if let JsonValue::Object(ref map) = input.body {
2291 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2292 map.keys().map(String::as_str),
2293 &format!("document '{}'", input.collection),
2294 )?;
2295 }
2296
2297 let binary_body = self.binary_document_body_enabled();
2301 let body_bytes = crate::document_body::serialize_document_body(&input.body, binary_body)?;
2302 let fields: Vec<(String, reddb_types::Value)> =
2303 vec![("body".to_string(), reddb_types::Value::Json(body_bytes))];
2304
2305 let mut metadata = input.metadata;
2306 contract.apply_default_ttl(&mut metadata);
2307 let columns: Vec<(&str, reddb_types::Value)> = fields
2308 .iter()
2309 .map(|(key, value)| (key.as_str(), value.clone()))
2310 .collect();
2311 let mut builder = db.row(&input.collection, columns);
2312
2313 for (key, value) in metadata {
2314 builder = builder.metadata(key, value);
2315 }
2316
2317 for node in input.node_links {
2318 builder = builder.link_to_node(node);
2319 }
2320
2321 for vector in input.vector_links {
2322 builder = builder.link_to_vector(vector);
2323 }
2324
2325 let id = builder.save()?;
2326 self.stamp_xmin_if_in_txn(&input.collection, id);
2328 self.index_store_ref()
2329 .index_entity_insert(&input.collection, id, &fields)
2330 .map_err(crate::RedDBError::Internal)?;
2331 refresh_context_index(&db, &input.collection, id)?;
2332 self.cdc_emit(
2333 crate::replication::cdc::ChangeOperation::Insert,
2334 &input.collection,
2335 id.raw(),
2336 "document",
2337 );
2338 Ok(CreateEntityOutput {
2339 id,
2340 entity: db.store().get(&input.collection, id),
2341 })
2342 }
2343
2344 fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
2345 let db = self.db();
2346 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2347 let declared_model = db
2348 .collection_contract(&input.collection)
2349 .map(|contract| contract.declared_model);
2350 let value = if declared_model == Some(crate::catalog::CollectionModel::Vault) {
2351 contract.ensure_model(crate::catalog::CollectionModel::Vault)?;
2352 self.seal_vault_value(&input.collection, input.value)?
2353 } else {
2354 contract.ensure_model(crate::catalog::CollectionModel::Kv)?;
2355 input.value
2356 };
2357 let fields = vec![
2358 ("key".to_string(), reddb_types::Value::text(input.key)),
2359 ("value".to_string(), value),
2360 ];
2361 let collection = input.collection;
2362 let result = self.mutation_engine().apply(
2363 collection.clone(),
2364 vec![crate::runtime::mutation::MutationRow {
2365 fields,
2366 metadata: input.metadata,
2367 node_links: Vec::new(),
2368 vector_links: Vec::new(),
2369 }],
2370 )?;
2371 let id = result.ids[0];
2372 Ok(CreateEntityOutput {
2373 id,
2374 entity: db.store().get(&collection, id),
2375 })
2376 }
2377
2378 fn create_timeseries_point(
2379 &self,
2380 input: CreateTimeSeriesPointInput,
2381 ) -> RedDBResult<CreateEntityOutput> {
2382 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2383 let db = self.db();
2384 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2385 contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
2386
2387 let mut fields = vec![
2388 ("metric".to_string(), reddb_types::Value::text(input.metric)),
2389 ("value".to_string(), reddb_types::Value::Float(input.value)),
2390 ];
2391
2392 if let Some(timestamp_ns) = input.timestamp_ns {
2393 fields.push((
2394 "timestamp".to_string(),
2395 reddb_types::Value::UnsignedInteger(timestamp_ns),
2396 ));
2397 }
2398
2399 if !input.tags.is_empty() {
2400 let tags_json = JsonValue::Object(
2401 input
2402 .tags
2403 .into_iter()
2404 .map(|(key, value)| (key, JsonValue::String(value)))
2405 .collect(),
2406 );
2407 let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
2408 crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
2409 })?;
2410 fields.push(("tags".to_string(), reddb_types::Value::Json(tags_bytes)));
2411 }
2412
2413 let collection = input.collection;
2414 let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
2415 self.stamp_xmin_if_in_txn(&collection, id);
2418 refresh_context_index(&db, &collection, id)?;
2419
2420 Ok(CreateEntityOutput {
2421 id,
2422 entity: db.store().get(&collection, id),
2423 })
2424 }
2425
2426 fn get_kv(
2427 &self,
2428 collection: &str,
2429 key: &str,
2430 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
2431 let db = self.db();
2432 crate::application::collection_contract_enforcer::ensure_collection_model_read(
2433 &db,
2434 collection,
2435 crate::catalog::CollectionModel::Kv,
2436 )?;
2437 let store = db.store();
2438 let Some(manager) = store.get_collection(collection) else {
2439 return Ok(None);
2440 };
2441 let entities = manager.query_all(|_| true);
2442 for entity in entities {
2443 if let crate::storage::EntityData::Row(ref row) = entity.data {
2444 if let Some(ref named) = row.named {
2445 if let Some(reddb_types::Value::Text(ref k)) = named.get("key") {
2446 if &**k == key {
2447 let value = named
2448 .get("value")
2449 .cloned()
2450 .unwrap_or(reddb_types::Value::Null);
2451 return Ok(Some((value, entity.id)));
2452 }
2453 }
2454 }
2455 }
2456 }
2457 Ok(None)
2458 }
2459
2460 fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
2461 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2462 let found = self.get_kv(collection, key)?;
2463 if let Some((_, id)) = found {
2464 let db = self.db();
2465 let store = db.store();
2466 let deleted = store
2467 .delete(collection, id)
2468 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2469 if deleted {
2470 store.context_index().remove_entity(id);
2471 }
2472 Ok(deleted)
2473 } else {
2474 Ok(false)
2475 }
2476 }
2477
2478 fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
2479 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2480 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2481 self,
2482 &input.collection,
2483 );
2484 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2485 let topology_lock = self
2486 .index_store_ref()
2487 .collection_topology_lock(&input.collection);
2488 let topology_guard = topology_lock.read();
2489 let PatchEntityInput {
2490 collection,
2491 id,
2492 payload,
2493 operations,
2494 } = input;
2495 let db = self.db();
2496 let store = db.store();
2497 let Some(manager) = store.get_collection(&collection) else {
2498 return Err(crate::RedDBError::NotFound(format!(
2499 "collection not found: {collection}"
2500 )));
2501 };
2502 let Some(entity) = manager.get(id) else {
2503 return Err(crate::RedDBError::NotFound(format!(
2504 "entity not found: {}",
2505 id.raw()
2506 )));
2507 };
2508 self.apply_loaded_patch_entity(collection, entity, payload, operations, topology_guard)
2509 }
2510
2511 fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
2512 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2513 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2514 self,
2515 &input.collection,
2516 );
2517 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2518 let topology_lock = self
2519 .index_store_ref()
2520 .collection_topology_lock(&input.collection);
2521 let topology_guard = topology_lock.read();
2522 let store = self.db().store();
2523 let pre_delete_fields = store
2527 .get(&input.collection, input.id)
2528 .as_ref()
2529 .map(entity_row_fields_snapshot)
2530 .unwrap_or_default();
2531 let deleted = store
2535 .delete(&input.collection, input.id)
2536 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2537 if deleted {
2538 #[cfg(test)]
2539 self.index_store_ref().mutation_test_hook(
2540 &input.collection,
2541 crate::runtime::MutationTestPhase::StoragePublished,
2542 );
2543 store.context_index().remove_entity(input.id);
2544 if !pre_delete_fields.is_empty() {
2547 self.index_store_ref()
2548 .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
2549 .map_err(crate::RedDBError::Internal)?;
2550 }
2551 drop(topology_guard);
2552 #[cfg(test)]
2553 self.index_store_ref().mutation_test_hook(
2554 &input.collection,
2555 crate::runtime::MutationTestPhase::BeforeEvents,
2556 );
2557 self.cdc_emit(
2558 crate::replication::cdc::ChangeOperation::Delete,
2559 &input.collection,
2560 input.id.raw(),
2561 "entity",
2562 );
2563 }
2564 Ok(DeleteEntityOutput {
2565 deleted,
2566 id: input.id,
2567 })
2568 }
2569}