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 collection = &applied[0].collection;
1372 let Some(manager) = store.get_collection(collection) else {
1373 return Err(crate::RedDBError::NotFound(format!(
1374 "collection not found: {collection}"
1375 )));
1376 };
1377
1378 let mut ordinary = Vec::with_capacity(applied.len());
1379 for item in applied {
1380 if let Some(old_version) = item.replaced_entity.as_ref() {
1381 store
1382 .install_versioned_table_row_update(
1383 collection,
1384 old_version.clone(),
1385 item.entity.clone(),
1386 if item.persist_metadata {
1387 item.metadata.as_ref()
1388 } else {
1389 None
1390 },
1391 )
1392 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
1393 if self.current_xid().is_some() {
1394 self.record_pending_versioned_update(
1395 crate::runtime::impl_core::current_connection_id(),
1396 collection,
1397 old_version.id,
1398 item.entity.id,
1399 old_version.xmax,
1400 item.replaced_entity_previous_xmax,
1401 );
1402 }
1403 } else {
1404 ordinary.push(item);
1405 }
1406 }
1407 if ordinary.is_empty() {
1408 return Ok(());
1409 }
1410
1411 manager
1412 .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
1413 (
1414 &item.entity,
1415 item.modified_columns.as_slice(),
1416 if item.persist_metadata {
1417 item.metadata.as_ref()
1418 } else {
1419 None
1420 },
1421 )
1422 }))
1423 .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
1424
1425 let indexed_cols = self
1434 .index_store_ref()
1435 .indexed_columns_set(collection.as_str());
1436 let all_hot = !indexed_cols.is_empty()
1437 && ordinary.iter().all(|item| {
1438 !item.persist_metadata
1439 && !item
1440 .modified_columns
1441 .iter()
1442 .any(|c| indexed_cols.contains(c))
1443 })
1444 || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
1445
1446 let entity_refs: Vec<&crate::storage::UnifiedEntity> =
1450 ordinary.iter().map(|item| &item.entity).collect();
1451 let persist_fn = if all_hot {
1452 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
1453 } else {
1454 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
1455 };
1456 persist_fn(store.as_ref(), collection, &entity_refs)
1457 .map_err(|err| crate::RedDBError::Internal(err.to_string()))
1458 }
1459
1460 pub(crate) fn flush_applied_entity_mutation(
1461 &self,
1462 applied: &AppliedEntityMutation,
1463 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1464 ) -> RedDBResult<()> {
1465 let store = self.db().store();
1466 if applied.context_index_dirty {
1467 store
1468 .context_index()
1469 .index_entity(&applied.collection, &applied.entity);
1470 }
1471 let mut changed_columns: Option<Vec<String>> = None;
1479 if !applied.pre_mutation_fields.is_empty() {
1480 let post = entity_row_fields_snapshot(&applied.entity);
1481 if !post.is_empty() {
1482 let damage = crate::application::entity::row_damage_vector(
1483 &applied.pre_mutation_fields,
1484 &post,
1485 );
1486 if !damage.is_empty() {
1487 changed_columns = Some(
1488 damage
1489 .touched_columns()
1490 .into_iter()
1491 .map(str::to_string)
1492 .collect(),
1493 );
1494 }
1495
1496 let indexed_cols: std::collections::HashSet<String> = self
1508 .index_store_ref()
1509 .indexed_columns_set_with_parents(applied.collection.as_str());
1510 let pre_index_fields = self
1514 .index_store_ref()
1515 .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
1516 let post_index_fields = self
1517 .index_store_ref()
1518 .augment_body_derived_index_fields(&post, &indexed_cols);
1519 let modified_cols: std::collections::HashSet<String> =
1520 crate::application::entity::row_damage_vector(
1521 &pre_index_fields,
1522 &post_index_fields,
1523 )
1524 .touched_columns()
1525 .into_iter()
1526 .map(str::to_string)
1527 .collect();
1528 if let Some(old_version) = applied.replaced_entity.as_ref() {
1529 let old_index_fields: Vec<(String, Value)> = pre_index_fields
1530 .iter()
1531 .filter(|(col, _)| indexed_cols.contains(col))
1532 .cloned()
1533 .collect();
1534 let new_index_fields: Vec<(String, Value)> = post_index_fields
1535 .iter()
1536 .filter(|(col, _)| indexed_cols.contains(col))
1537 .cloned()
1538 .collect();
1539 if !old_index_fields.is_empty() {
1540 self.index_store_ref()
1541 .index_entity_delete(
1542 &applied.collection,
1543 old_version.id,
1544 &old_index_fields,
1545 )
1546 .map_err(crate::RedDBError::Internal)?;
1547 }
1548 if !new_index_fields.is_empty() {
1549 self.index_store_ref()
1550 .index_entity_insert(
1551 &applied.collection,
1552 applied.entity.id,
1553 &new_index_fields,
1554 )
1555 .map_err(crate::RedDBError::Internal)?;
1556 }
1557 } else {
1558 let decision = crate::storage::engine::hot_update::decide(
1559 &crate::storage::engine::hot_update::HotUpdateInputs {
1560 collection: applied.collection.as_str(),
1561 indexed_columns: &indexed_cols,
1562 modified_columns: &modified_cols,
1563 new_tuple_size: 0,
1567 page_free_space: usize::MAX,
1568 },
1569 );
1570 if !decision.can_hot {
1571 self.index_store_ref()
1572 .index_entity_update(
1573 &applied.collection,
1574 applied.id,
1575 &pre_index_fields,
1576 &post_index_fields,
1577 )
1578 .map_err(crate::RedDBError::Internal)?;
1579 } else {
1580 tracing::debug!(
1584 collection = %reddb_wire::audit_safe_log_field(&applied.collection),
1585 "hot_update fast-path: skipped index_entity_update"
1586 );
1587 }
1588 }
1589 }
1590 }
1591 drop(topology_guard);
1592 #[cfg(test)]
1593 self.index_store_ref().mutation_test_hook(
1594 &applied.collection,
1595 crate::runtime::MutationTestPhase::BeforeEvents,
1596 );
1597 self.cdc_emit_prebuilt_with_columns(
1598 crate::replication::cdc::ChangeOperation::Update,
1599 &applied.collection,
1600 &applied.entity,
1601 "entity",
1602 applied.metadata.as_ref(),
1603 true,
1604 changed_columns,
1605 );
1606 Ok(())
1607 }
1608
1609 pub(crate) fn apply_loaded_patch_entity(
1610 &self,
1611 collection: String,
1612 entity: crate::storage::UnifiedEntity,
1613 payload: JsonValue,
1614 operations: Vec<PatchEntityOperation>,
1615 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1616 ) -> RedDBResult<CreateEntityOutput> {
1617 let applied =
1618 self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
1619 let _reservation = self.admit_entity_mutation(&applied)?;
1620 self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
1621 #[cfg(test)]
1622 self.index_store_ref().mutation_test_hook(
1623 &applied.collection,
1624 crate::runtime::MutationTestPhase::StoragePublished,
1625 );
1626 self.flush_applied_entity_mutation(&applied, topology_guard)?;
1627 Ok(CreateEntityOutput {
1628 id: applied.id,
1629 entity: Some(public_document_entity(applied.entity)),
1630 })
1631 }
1632}
1633
1634fn public_document_entity(
1643 mut entity: crate::storage::UnifiedEntity,
1644) -> crate::storage::UnifiedEntity {
1645 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1646 return entity;
1647 };
1648 let Some(named) = row.named.as_mut() else {
1649 return entity;
1650 };
1651 let Some(Value::Json(bytes)) = named.get("body") else {
1652 return entity;
1653 };
1654 let bytes = bytes.clone();
1655 if let Some(fields) = crate::document_body::body_fields(&bytes) {
1656 for (name, value) in fields {
1657 named.entry(name).or_insert(value);
1658 }
1659 }
1660 if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
1661 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
1662 named.insert("body".to_string(), Value::Json(json_bytes));
1663 }
1664 }
1665 entity
1666}
1667
1668fn ensure_non_tree_reserved_metadata_patch_paths(
1669 operations: &[PatchEntityOperation],
1670) -> RedDBResult<()> {
1671 for operation in operations {
1672 let Some(key) = operation.path.first().map(String::as_str) else {
1673 continue;
1674 };
1675 ensure_non_tree_reserved_metadata_key(key)?;
1676 }
1677 Ok(())
1678}
1679
1680fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
1681 if key.starts_with(TREE_METADATA_PREFIX) {
1682 return Err(crate::RedDBError::Query(format!(
1683 "metadata key '{}' is reserved for managed trees",
1684 key
1685 )));
1686 }
1687 Ok(())
1688}
1689
1690fn ensure_non_tree_reserved_metadata_entries(
1691 metadata: &[(String, MetadataValue)],
1692) -> RedDBResult<()> {
1693 for (key, _) in metadata {
1694 ensure_non_tree_reserved_metadata_key(key)?;
1695 }
1696 Ok(())
1697}
1698
1699fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
1700 if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
1701 return Err(crate::RedDBError::Query(format!(
1702 "edge label '{}' is reserved for managed trees",
1703 TREE_CHILD_EDGE_LABEL
1704 )));
1705 }
1706 Ok(())
1707}
1708
1709impl RedDBRuntime {
1710 pub(crate) fn create_node_unchecked(
1711 &self,
1712 input: CreateNodeInput,
1713 ) -> RedDBResult<CreateEntityOutput> {
1714 let db = self.db();
1715 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1716 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1717 let mut metadata = input.metadata;
1718 contract.apply_default_ttl(&mut metadata);
1719 let mut builder = db.node(&input.collection, &input.label);
1720
1721 if let Some(node_type) = input.node_type {
1722 builder = builder.node_type(node_type);
1723 }
1724
1725 for (key, value) in input.properties {
1726 builder = builder.property(key, value);
1727 }
1728
1729 for (key, value) in metadata {
1730 builder = builder.metadata(key, value);
1731 }
1732
1733 for embedding in input.embeddings {
1734 if let Some(model) = embedding.model {
1735 builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
1736 } else {
1737 builder = builder.embedding(embedding.name, embedding.vector);
1738 }
1739 }
1740
1741 for link in input.table_links {
1742 builder = builder.link_to_table(link.key, link.table);
1743 }
1744
1745 for link in input.node_links {
1746 builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
1747 }
1748
1749 let id = builder.save()?;
1750 self.stamp_xmin_if_in_txn(&input.collection, id);
1753 refresh_context_index(&db, &input.collection, id)?;
1754 self.cdc_emit(
1755 crate::replication::cdc::ChangeOperation::Insert,
1756 &input.collection,
1757 id.raw(),
1758 "graph_node",
1759 );
1760 Ok(CreateEntityOutput {
1761 id,
1762 entity: db.store().get(&input.collection, id),
1763 })
1764 }
1765
1766 pub(crate) fn create_edge_unchecked(
1767 &self,
1768 input: CreateEdgeInput,
1769 ) -> RedDBResult<CreateEntityOutput> {
1770 let db = self.db();
1771 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1772 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1773 let mut metadata = input.metadata;
1774 contract.apply_default_ttl(&mut metadata);
1775 let mut builder = db
1776 .edge(&input.collection, &input.label)
1777 .from(input.from)
1778 .to(input.to);
1779
1780 if let Some(weight) = input.weight {
1781 builder = builder.weight(weight);
1782 }
1783
1784 for (key, value) in input.properties {
1785 builder = builder.property(key, value);
1786 }
1787
1788 for (key, value) in metadata {
1789 builder = builder.metadata(key, value);
1790 }
1791
1792 let id = builder.save()?;
1793 self.stamp_xmin_if_in_txn(&input.collection, id);
1796 refresh_context_index(&db, &input.collection, id)?;
1797 self.cdc_emit(
1798 crate::replication::cdc::ChangeOperation::Insert,
1799 &input.collection,
1800 id.raw(),
1801 "graph_edge",
1802 );
1803 Ok(CreateEntityOutput {
1804 id,
1805 entity: db.store().get(&input.collection, id),
1806 })
1807 }
1808}
1809
1810fn create_rows_batch_prevalidated_columnar_with_outputs(
1811 runtime: &RedDBRuntime,
1812 collection: String,
1813 column_names: std::sync::Arc<Vec<String>>,
1814 rows: Vec<Vec<reddb_types::Value>>,
1815) -> RedDBResult<Vec<CreateEntityOutput>> {
1816 use crate::storage::{
1817 unified::{EntityData, EntityKind, RowData},
1818 EntityId, UnifiedEntity,
1819 };
1820 use std::sync::Arc;
1821
1822 if rows.is_empty() {
1823 return Ok(Vec::new());
1824 }
1825 if runtime.index_store_ref().has_unique_index(&collection) {
1828 return runtime.create_rows_batch_columnar_with_outputs(collection, column_names, rows);
1829 }
1830 runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1831 runtime.check_batch_size(rows.len())?;
1832 runtime.check_db_size()?;
1833
1834 let db = runtime.db();
1835 let contract = CollectionContractWriteEnforcer::new(runtime, &db, &collection);
1836 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1837
1838 let store = db.store();
1839 let table_arc: Arc<str> = Arc::from(collection.as_str());
1840
1841 let indexed_cols = runtime
1842 .index_store_ref()
1843 .indexed_columns_set(collection.as_str());
1844 let has_secondary_indexes = !indexed_cols.is_empty();
1845 let mut field_snapshots: Vec<Vec<(String, reddb_types::Value)>> = if has_secondary_indexes {
1846 Vec::with_capacity(rows.len())
1847 } else {
1848 Vec::new()
1849 };
1850
1851 let entities: Vec<UnifiedEntity> = rows
1852 .into_iter()
1853 .map(|values| {
1854 if has_secondary_indexes {
1855 let mut snap: Vec<(String, reddb_types::Value)> =
1856 Vec::with_capacity(indexed_cols.len());
1857 for (name, val) in column_names.iter().zip(values.iter()) {
1858 if indexed_cols.contains(name) {
1859 snap.push((name.clone(), val.clone()));
1860 }
1861 }
1862 field_snapshots.push(snap);
1863 }
1864 let mut row = RowData::new(values);
1865 row.schema = Some(Arc::clone(&column_names));
1866 UnifiedEntity::new(
1867 EntityId::new(0),
1868 EntityKind::TableRow {
1869 table: Arc::clone(&table_arc),
1870 row_id: 0,
1871 },
1872 EntityData::Row(row),
1873 )
1874 })
1875 .collect();
1876
1877 let ids = store
1878 .bulk_insert(&collection, entities)
1879 .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
1880
1881 if has_secondary_indexes {
1882 let index_rows: Vec<(EntityId, Vec<(String, reddb_types::Value)>)> = ids
1883 .iter()
1884 .zip(field_snapshots)
1885 .map(|(id, fields)| (*id, fields))
1886 .collect();
1887 runtime
1888 .index_store_ref()
1889 .index_entity_insert_batch(&collection, &index_rows)
1890 .map_err(crate::RedDBError::Internal)?;
1891 }
1892
1893 runtime.invalidate_result_cache();
1894 runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
1895
1896 Ok(ids
1897 .into_iter()
1898 .map(|id| CreateEntityOutput { id, entity: None })
1899 .collect())
1900}
1901
1902impl RuntimeEntityPort for RedDBRuntime {
1903 fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
1904 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1905 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
1906 self,
1907 &input.collection,
1908 );
1909 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
1910 let db = self.db();
1911 let CreateRowInput {
1912 collection,
1913 fields,
1914 metadata: input_metadata,
1915 node_links,
1916 vector_links,
1917 } = input;
1918 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1919 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1920 let mut metadata = input_metadata;
1921 contract.apply_default_ttl(&mut metadata);
1922 let fields = contract.normalize_insert_fields(fields)?;
1923 contract.enforce_row_uniqueness(&fields, None)?;
1924 let engine = self.mutation_engine();
1926 let result = engine.apply(
1927 collection.clone(),
1928 vec![crate::runtime::mutation::MutationRow {
1929 fields,
1930 metadata,
1931 node_links,
1932 vector_links,
1933 }],
1934 )?;
1935 let id = result.ids[0];
1936 Ok(CreateEntityOutput {
1941 id,
1942 entity: db.store().get(&collection, id),
1943 })
1944 }
1945
1946 fn create_rows_batch(
1947 &self,
1948 input: CreateRowsBatchInput,
1949 ) -> RedDBResult<Vec<CreateEntityOutput>> {
1950 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
1951 self,
1952 &input.collection,
1953 );
1954 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
1955 if input.rows.is_empty() {
1956 return Ok(Vec::new());
1957 }
1958 self.check_batch_size(input.rows.len())?;
1959 self.check_db_size()?;
1960
1961 let db = self.db();
1962 let collection = input.collection;
1963 let suppress_events = input.suppress_events;
1964 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1965 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1966
1967 let mut prepared_rows = Vec::with_capacity(input.rows.len());
1968 let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
1969 for row in input.rows {
1970 if row.collection != collection {
1971 return Err(crate::RedDBError::Query(format!(
1972 "batch row collection mismatch: expected '{}', got '{}'",
1973 collection, row.collection
1974 )));
1975 }
1976
1977 let mut metadata = row.metadata;
1978 contract.apply_default_ttl(&mut metadata);
1979 let fields = contract.normalize_insert_fields(row.fields)?;
1980 contract.enforce_row_uniqueness(&fields, None)?;
1981 uniqueness_rows.push(fields.clone());
1982 prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
1983 }
1984
1985 contract.enforce_batch_uniqueness(&uniqueness_rows)?;
1986
1987 let engine = {
1990 let e = self.mutation_engine();
1991 if suppress_events {
1992 e.with_suppress_events()
1993 } else {
1994 e
1995 }
1996 };
1997 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
1998 .into_iter()
1999 .map(|(fields, metadata, node_links, vector_links)| {
2000 crate::runtime::mutation::MutationRow {
2001 fields,
2002 metadata,
2003 node_links,
2004 vector_links,
2005 }
2006 })
2007 .collect();
2008
2009 let result = engine
2010 .apply(collection.clone(), mutation_rows)
2011 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
2012
2013 let store = db.store();
2014 Ok(result
2015 .ids
2016 .into_iter()
2017 .map(|id| CreateEntityOutput {
2018 id,
2019 entity: store.get(&collection, id),
2020 })
2021 .collect())
2022 }
2023
2024 fn create_rows_batch_prevalidated_columnar(
2025 &self,
2026 collection: String,
2027 column_names: std::sync::Arc<Vec<String>>,
2028 rows: Vec<Vec<reddb_types::Value>>,
2029 ) -> RedDBResult<usize> {
2030 create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2031 .map(|outputs| outputs.len())
2032 }
2033
2034 fn create_rows_batch_columnar(
2035 &self,
2036 collection: String,
2037 column_names: std::sync::Arc<Vec<String>>,
2038 rows: Vec<Vec<reddb_types::Value>>,
2039 ) -> RedDBResult<usize> {
2040 self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2041 .map(|outputs| outputs.len())
2042 }
2043
2044 fn create_rows_batch_columnar_with_outputs(
2045 &self,
2046 collection: String,
2047 column_names: std::sync::Arc<Vec<String>>,
2048 rows: Vec<Vec<reddb_types::Value>>,
2049 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2050 if rows.is_empty() {
2051 return Ok(Vec::new());
2052 }
2053 self.check_batch_size(rows.len())?;
2054 self.check_db_size()?;
2055
2056 let db = self.db();
2057 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2058 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2059
2060 let needs_normalisation = self.index_store_ref().has_unique_index(&collection)
2070 || match db.collection_contract(&collection) {
2071 Some(c) => {
2072 c.declared_model == crate::catalog::CollectionModel::Table
2073 && (!c.declared_columns.is_empty()
2074 || c.table_def
2075 .as_ref()
2076 .map(|t| !t.columns.is_empty())
2077 .unwrap_or(false))
2078 }
2079 None => false,
2080 };
2081 if !needs_normalisation {
2082 return create_rows_batch_prevalidated_columnar_with_outputs(
2083 self,
2084 collection,
2085 column_names,
2086 rows,
2087 );
2088 }
2089
2090 let ncols = column_names.len();
2097 let tuple_rows: Vec<CreateRowInput> = rows
2098 .into_iter()
2099 .map(|values| {
2100 let mut fields: Vec<(String, reddb_types::Value)> = Vec::with_capacity(ncols);
2101 for (name, value) in column_names.iter().zip(values) {
2102 fields.push((name.clone(), value));
2103 }
2104 CreateRowInput {
2105 collection: collection.clone(),
2106 fields,
2107 metadata: Vec::new(),
2108 node_links: Vec::new(),
2109 vector_links: Vec::new(),
2110 }
2111 })
2112 .collect();
2113 self.create_rows_batch(CreateRowsBatchInput {
2114 collection,
2115 rows: tuple_rows,
2116 suppress_events: false,
2117 })
2118 }
2119
2120 fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
2121 self.create_rows_batch_prevalidated_with_outputs(input)
2122 .map(|outputs| outputs.len())
2123 }
2124
2125 fn create_rows_batch_prevalidated_with_outputs(
2126 &self,
2127 input: CreateRowsBatchInput,
2128 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2129 if input.rows.is_empty() {
2130 return Ok(Vec::new());
2131 }
2132 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2133 self.check_batch_size(input.rows.len())?;
2134 self.check_db_size()?;
2135
2136 let db = self.db();
2137 let collection = input.collection;
2138 let suppress_events = input.suppress_events;
2139 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2144 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2145
2146 let default_ttl_plan = contract.default_ttl_plan();
2152
2153 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
2154 Vec::with_capacity(input.rows.len());
2155 let mut mutation_rows = mutation_rows;
2156 for row in input.rows {
2157 if row.collection != collection {
2158 return Err(crate::RedDBError::Query(format!(
2159 "batch row collection mismatch: expected '{}', got '{}'",
2160 collection, row.collection
2161 )));
2162 }
2163 let mut metadata = row.metadata;
2164 default_ttl_plan.apply(&mut metadata);
2165 mutation_rows.push(crate::runtime::mutation::MutationRow {
2166 fields: row.fields,
2167 metadata,
2168 node_links: row.node_links,
2169 vector_links: row.vector_links,
2170 });
2171 }
2172
2173 let engine = {
2174 let engine = self.mutation_engine();
2175 if suppress_events {
2176 engine.with_suppress_events()
2177 } else {
2178 engine
2179 }
2180 };
2181 let result = engine
2182 .apply(collection.clone(), mutation_rows)
2183 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
2184 let store = db.store();
2185 Ok(result
2186 .ids
2187 .into_iter()
2188 .map(|id| CreateEntityOutput {
2189 id,
2190 entity: store.get(&collection, id),
2191 })
2192 .collect())
2193 }
2194
2195 fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
2196 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2197 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2198 input.properties.iter().map(|(key, _)| key.as_str()),
2199 &format!("node '{}'", input.collection),
2200 )?;
2201 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2202 self.create_node_unchecked(input)
2203 }
2204
2205 fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
2206 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2207 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2208 input.properties.iter().map(|(key, _)| key.as_str()),
2209 &format!("edge '{}'", input.collection),
2210 )?;
2211 ensure_non_tree_structural_edge_label(&input.label)?;
2212 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2213 self.create_edge_unchecked(input)
2214 }
2215
2216 fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
2217 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2218 let db = self.db();
2219 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2220 contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
2221 contract.ensure_vector_dimension(input.dense.len())?;
2222 let mut metadata = input.metadata;
2223 contract.apply_default_ttl(&mut metadata);
2224 let mut builder = db.vector(&input.collection).dense(input.dense);
2225
2226 if let Some(content) = input.content {
2227 builder = builder.content(content);
2228 }
2229
2230 for (key, value) in metadata {
2231 builder = builder.metadata(key, value);
2232 }
2233
2234 if let Some(link_row) = input.link_row {
2235 builder = builder.link_to_table(link_row);
2236 }
2237
2238 if let Some(link_node) = input.link_node {
2239 builder = builder.link_to_node(link_node);
2240 }
2241
2242 let id = builder.save_with_xmin(self.current_xid())?;
2245 let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
2246 Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
2247 _ => None,
2248 };
2249 refresh_context_index(&db, &input.collection, id)?;
2250 if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
2256 state.ensure_populated(&db.store(), &input.collection);
2260 use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
2262 fire(InjectionPoint::BeforeWalFsync);
2263 let _ = db
2264 .store()
2265 .append_vector_insert_record(&input.collection, id.raw(), &dense);
2266 fire(InjectionPoint::BeforeIndexCommit);
2267 let mut index = state.index.lock();
2268 index.insert(id, dense.clone());
2269 fire(InjectionPoint::BeforeExtentFsync);
2276 if let Some(extent) = state.extent.lock().as_mut() {
2277 let packed = index.encode_for_extent(&dense);
2278 let _ = extent.append(&packed);
2279 }
2280 }
2281 self.cdc_emit(
2282 crate::replication::cdc::ChangeOperation::Insert,
2283 &input.collection,
2284 id.raw(),
2285 "vector",
2286 );
2287 Ok(CreateEntityOutput {
2288 id,
2289 entity: db.store().get(&input.collection, id),
2290 })
2291 }
2292
2293 fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
2294 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2295 let db = self.db();
2296 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2297 contract.ensure_model(crate::catalog::CollectionModel::Document)?;
2298
2299 if let JsonValue::Object(ref map) = input.body {
2300 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2301 map.keys().map(String::as_str),
2302 &format!("document '{}'", input.collection),
2303 )?;
2304 }
2305
2306 let binary_body = self.binary_document_body_enabled();
2310 let body_bytes = crate::document_body::serialize_document_body(&input.body, binary_body)?;
2311 let fields: Vec<(String, reddb_types::Value)> =
2312 vec![("body".to_string(), reddb_types::Value::Json(body_bytes))];
2313
2314 let mut metadata = input.metadata;
2315 contract.apply_default_ttl(&mut metadata);
2316 let columns: Vec<(&str, reddb_types::Value)> = fields
2317 .iter()
2318 .map(|(key, value)| (key.as_str(), value.clone()))
2319 .collect();
2320 let mut builder = db.row(&input.collection, columns);
2321
2322 for (key, value) in metadata {
2323 builder = builder.metadata(key, value);
2324 }
2325
2326 for node in input.node_links {
2327 builder = builder.link_to_node(node);
2328 }
2329
2330 for vector in input.vector_links {
2331 builder = builder.link_to_vector(vector);
2332 }
2333
2334 let id = builder.save()?;
2335 self.stamp_xmin_if_in_txn(&input.collection, id);
2337 self.index_store_ref()
2338 .index_entity_insert(&input.collection, id, &fields)
2339 .map_err(crate::RedDBError::Internal)?;
2340 refresh_context_index(&db, &input.collection, id)?;
2341 self.cdc_emit(
2342 crate::replication::cdc::ChangeOperation::Insert,
2343 &input.collection,
2344 id.raw(),
2345 "document",
2346 );
2347 Ok(CreateEntityOutput {
2348 id,
2349 entity: db.store().get(&input.collection, id),
2350 })
2351 }
2352
2353 fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
2354 let db = self.db();
2355 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2356 let declared_model = db
2357 .collection_contract(&input.collection)
2358 .map(|contract| contract.declared_model);
2359 let value = if declared_model == Some(crate::catalog::CollectionModel::Vault) {
2360 contract.ensure_model(crate::catalog::CollectionModel::Vault)?;
2361 self.seal_vault_value(&input.collection, input.value)?
2362 } else {
2363 contract.ensure_model(crate::catalog::CollectionModel::Kv)?;
2364 input.value
2365 };
2366 let fields = vec![
2367 ("key".to_string(), reddb_types::Value::text(input.key)),
2368 ("value".to_string(), value),
2369 ];
2370 let collection = input.collection;
2371 let result = self.mutation_engine().apply(
2372 collection.clone(),
2373 vec![crate::runtime::mutation::MutationRow {
2374 fields,
2375 metadata: input.metadata,
2376 node_links: Vec::new(),
2377 vector_links: Vec::new(),
2378 }],
2379 )?;
2380 let id = result.ids[0];
2381 Ok(CreateEntityOutput {
2382 id,
2383 entity: db.store().get(&collection, id),
2384 })
2385 }
2386
2387 fn create_timeseries_point(
2388 &self,
2389 input: CreateTimeSeriesPointInput,
2390 ) -> RedDBResult<CreateEntityOutput> {
2391 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2392 let db = self.db();
2393 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2394 contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
2395
2396 let mut fields = vec![
2397 ("metric".to_string(), reddb_types::Value::text(input.metric)),
2398 ("value".to_string(), reddb_types::Value::Float(input.value)),
2399 ];
2400
2401 if let Some(timestamp_ns) = input.timestamp_ns {
2402 fields.push((
2403 "timestamp".to_string(),
2404 reddb_types::Value::UnsignedInteger(timestamp_ns),
2405 ));
2406 }
2407
2408 if !input.tags.is_empty() {
2409 let tags_json = JsonValue::Object(
2410 input
2411 .tags
2412 .into_iter()
2413 .map(|(key, value)| (key, JsonValue::String(value)))
2414 .collect(),
2415 );
2416 let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
2417 crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
2418 })?;
2419 fields.push(("tags".to_string(), reddb_types::Value::Json(tags_bytes)));
2420 }
2421
2422 let collection = input.collection;
2423 let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
2424 self.stamp_xmin_if_in_txn(&collection, id);
2427 refresh_context_index(&db, &collection, id)?;
2428
2429 Ok(CreateEntityOutput {
2430 id,
2431 entity: db.store().get(&collection, id),
2432 })
2433 }
2434
2435 fn get_kv(
2436 &self,
2437 collection: &str,
2438 key: &str,
2439 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
2440 let db = self.db();
2441 crate::application::collection_contract_enforcer::ensure_collection_model_read(
2442 &db,
2443 collection,
2444 crate::catalog::CollectionModel::Kv,
2445 )?;
2446 let store = db.store();
2447 let Some(manager) = store.get_collection(collection) else {
2448 return Ok(None);
2449 };
2450 let entities = manager.query_all(|_| true);
2451 for entity in entities {
2452 if let crate::storage::EntityData::Row(ref row) = entity.data {
2453 if let Some(ref named) = row.named {
2454 if let Some(reddb_types::Value::Text(ref k)) = named.get("key") {
2455 if &**k == key {
2456 let value = named
2457 .get("value")
2458 .cloned()
2459 .unwrap_or(reddb_types::Value::Null);
2460 return Ok(Some((value, entity.id)));
2461 }
2462 }
2463 }
2464 }
2465 }
2466 Ok(None)
2467 }
2468
2469 fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
2470 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2471 let found = self.get_kv(collection, key)?;
2472 if let Some((_, id)) = found {
2473 let db = self.db();
2474 let store = db.store();
2475 let deleted = store
2476 .delete(collection, id)
2477 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2478 if deleted {
2479 store.context_index().remove_entity(id);
2480 }
2481 Ok(deleted)
2482 } else {
2483 Ok(false)
2484 }
2485 }
2486
2487 fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
2488 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2489 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2490 self,
2491 &input.collection,
2492 );
2493 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2494 let topology_lock = self
2495 .index_store_ref()
2496 .collection_topology_lock(&input.collection);
2497 let topology_guard = topology_lock.read();
2498 let PatchEntityInput {
2499 collection,
2500 id,
2501 payload,
2502 operations,
2503 } = input;
2504 let db = self.db();
2505 let store = db.store();
2506 let Some(manager) = store.get_collection(&collection) else {
2507 return Err(crate::RedDBError::NotFound(format!(
2508 "collection not found: {collection}"
2509 )));
2510 };
2511 let Some(entity) = manager.get(id) else {
2512 return Err(crate::RedDBError::NotFound(format!(
2513 "entity not found: {}",
2514 id.raw()
2515 )));
2516 };
2517 self.apply_loaded_patch_entity(collection, entity, payload, operations, topology_guard)
2518 }
2519
2520 fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
2521 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2522 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2523 self,
2524 &input.collection,
2525 );
2526 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2527 let topology_lock = self
2528 .index_store_ref()
2529 .collection_topology_lock(&input.collection);
2530 let topology_guard = topology_lock.read();
2531 let store = self.db().store();
2532 let pre_delete_fields = store
2536 .get(&input.collection, input.id)
2537 .as_ref()
2538 .map(entity_row_fields_snapshot)
2539 .unwrap_or_default();
2540 let deleted = store
2544 .delete(&input.collection, input.id)
2545 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2546 if deleted {
2547 #[cfg(test)]
2548 self.index_store_ref().mutation_test_hook(
2549 &input.collection,
2550 crate::runtime::MutationTestPhase::StoragePublished,
2551 );
2552 store.context_index().remove_entity(input.id);
2553 if !pre_delete_fields.is_empty() {
2556 self.index_store_ref()
2557 .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
2558 .map_err(crate::RedDBError::Internal)?;
2559 }
2560 drop(topology_guard);
2561 #[cfg(test)]
2562 self.index_store_ref().mutation_test_hook(
2563 &input.collection,
2564 crate::runtime::MutationTestPhase::BeforeEvents,
2565 );
2566 self.cdc_emit(
2567 crate::replication::cdc::ChangeOperation::Delete,
2568 &input.collection,
2569 input.id.raw(),
2570 "entity",
2571 );
2572 }
2573 Ok(DeleteEntityOutput {
2574 deleted,
2575 id: input.id,
2576 })
2577 }
2578}