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