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 versioned_update_xid = if patch_versions {
495 match self.current_xid() {
496 Some(xid) => Some(xid),
497 None => {
498 let snapshot_manager = self.snapshot_manager();
499 let xid = snapshot_manager.begin();
500 snapshot_manager.commit(xid);
501 Some(xid)
502 }
503 }
504 } else {
505 None
506 };
507 let mut replaced_entity = versioned_update_xid.map(|xid| {
508 let mut old = entity.clone();
509 old.set_xmax(xid);
510 old
511 });
512
513 let db = self.db();
514 let store = db.store();
515 let Some(manager) = store.get_collection(&collection) else {
516 return Err(crate::RedDBError::NotFound(format!(
517 "collection not found: {collection}"
518 )));
519 };
520
521 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
522 let mut metadata_changed = false;
523 let mut modified_columns: Vec<String> = Vec::new();
524 let mut context_index_dirty = false;
525 let mut graph_node_type: Option<String> = None;
526 let mut graph_edge_weight: Option<f32> = None;
527
528 let row_contract_timestamps = db
529 .collection_contract(&collection)
530 .map(|c| c.timestamps_enabled)
531 .unwrap_or(false);
532
533 match &mut entity.data {
534 crate::storage::EntityData::Row(row) => {
535 if row.named.is_none() {
539 if let Some(schema) = row.schema.as_ref() {
540 let fields = if schema.len() == row.columns.len() {
541 schema
542 .iter()
543 .cloned()
544 .zip(std::mem::take(&mut row.columns))
545 .collect()
546 } else {
547 collect_row_fields(row).into_iter().collect()
548 };
549 row.named = Some(fields);
550 }
551 }
552 let is_document_collection = db
553 .collection_contract(&collection)
554 .map(|contract| {
555 contract.declared_model == crate::catalog::CollectionModel::Document
556 })
557 .unwrap_or(false);
558 let binary_body = self.binary_document_body_enabled();
559 let mut field_ops = Vec::new();
560 let mut metadata_ops = Vec::new();
561 let mut document_body_ops = Vec::new();
562 let mut document_body_replace: Option<JsonValue> = None;
563 let has_patch_operations = !operations.is_empty();
564
565 for mut op in operations {
566 let Some(root) = op.path.first().map(String::as_str) else {
567 return Err(crate::RedDBError::Query(
568 "patch path cannot be empty".to_string(),
569 ));
570 };
571
572 match root {
573 "body" if is_document_collection => {
574 if op.path.len() == 1 {
575 match op.op {
576 PatchEntityOperationType::Set
577 | PatchEntityOperationType::Replace => {
578 let Some(value) = op.value.take() else {
579 return Err(crate::RedDBError::Query(
580 "document body replacement requires a value"
581 .to_string(),
582 ));
583 };
584 document_body_replace = Some(value);
585 }
586 PatchEntityOperationType::Unset => {
587 return Err(crate::RedDBError::Query(
588 "document body cannot be unset; replace it instead"
589 .to_string(),
590 ));
591 }
592 }
593 continue;
594 }
595 if op.path.len() < 2 {
596 return Err(crate::RedDBError::Query(
597 "document body patch paths require a nested key; use payload.body for full replacement"
598 .to_string(),
599 ));
600 }
601 op.path.remove(0);
602 reject_document_array_position_path(&op.path)?;
603 document_body_ops.push(op);
604 }
605 "fields" | "named" => {
606 if op.path.len() < 2 {
607 return Err(crate::RedDBError::Query(
608 "patch path 'fields' requires a nested key".to_string(),
609 ));
610 }
611 if row_contract_timestamps {
612 let leaf = op.path.get(1).map(String::as_str);
613 if matches!(leaf, Some("created_at") | Some("updated_at")) {
614 return Err(crate::RedDBError::Query(format!(
615 "collection '{}' manages '{}' automatically — do not set it in UPDATE",
616 collection,
617 leaf.unwrap_or("")
618 )));
619 }
620 }
621 op.path.remove(0);
622 field_ops.push(op);
623 }
624 "metadata" => {
625 if op.path.len() < 2 {
626 return Err(crate::RedDBError::Query(
627 "patch path 'metadata' requires a nested key".to_string(),
628 ));
629 }
630 op.path.remove(0);
631 metadata_ops.push(op);
632 }
633 _ => {
634 return Err(crate::RedDBError::Query(format!(
635 "unsupported patch target '{root}' for table rows. Use fields/*, metadata/*, or weight"
636 )));
637 }
638 }
639 }
640
641 if document_body_replace.is_some() || !document_body_ops.is_empty() {
642 context_index_dirty = true;
643 let mut body = match document_body_replace {
644 Some(body) => body,
645 None => document_body_from_row(row)?,
646 };
647 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
648 replace_document_row_body(
649 row,
650 body,
651 &collection,
652 binary_body,
653 &mut modified_columns,
654 )?;
655 }
656
657 if is_document_collection && !has_patch_operations {
658 let mut body = document_body_from_row(row)?;
659 let previous_body = body.clone();
660 apply_document_json_merge_patch(
661 &mut body,
662 document_merge_patch_payload(&payload),
663 )?;
664 if body != previous_body {
665 context_index_dirty = true;
666 replace_document_row_body(
667 row,
668 body,
669 &collection,
670 binary_body,
671 &mut modified_columns,
672 )?;
673 }
674 }
675
676 if !field_ops.is_empty() {
677 context_index_dirty = true;
678 if is_document_collection {
679 let mut body = document_body_from_row(row)?;
683 apply_patch_operations_to_json(&mut body, &field_ops)
684 .map_err(crate::RedDBError::Query)?;
685 replace_document_row_body(
686 row,
687 body,
688 &collection,
689 binary_body,
690 &mut modified_columns,
691 )?;
692 } else {
693 let named = row.named.get_or_insert_with(Default::default);
694 for op in &field_ops {
695 if let Some(col) = op.path.first() {
696 modified_columns.push(col.clone());
697 }
698 }
699 apply_patch_operations_to_storage_map(named, &field_ops)?;
700 }
701 }
702
703 if let Some(fields) = payload
704 .get("fields")
705 .and_then(crate::json::Value::as_object)
706 {
707 if row_contract_timestamps {
708 for key in fields.keys() {
709 if key == "created_at" || key == "updated_at" {
710 return Err(crate::RedDBError::Query(format!(
711 "collection '{}' manages '{}' automatically — do not set it in UPDATE",
712 collection, key
713 )));
714 }
715 }
716 }
717 context_index_dirty = true;
718 if is_document_collection {
719 let mut body = document_body_from_row(row)?;
722 if let JsonValue::Object(map) = &mut body {
723 for (key, value) in fields {
724 map.insert(key.clone(), value.clone());
725 }
726 }
727 replace_document_row_body(
728 row,
729 body,
730 &collection,
731 binary_body,
732 &mut modified_columns,
733 )?;
734 } else {
735 let named = row.named.get_or_insert_with(Default::default);
736 for (key, value) in fields {
737 modified_columns.push(key.clone());
738 named.insert(key.clone(), json_to_storage_value(value)?);
739 }
740 }
741 }
742
743 if !metadata_ops.is_empty() {
744 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
745 let metadata = patch_metadata.get_or_insert_with(|| {
746 store.get_metadata(&collection, id).unwrap_or_default()
747 });
748 let mut metadata_json = metadata_to_json(metadata);
749 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
750 .map_err(crate::RedDBError::Query)?;
751 *metadata = metadata_from_json(&metadata_json)?;
752 metadata_changed = true;
753 }
754
755 if !modified_columns.is_empty() || row_contract_timestamps {
756 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
757 let current_fields = if let Some(named) = row.named.take() {
758 named.into_iter().collect::<Vec<_>>()
759 } else if let Some(schema) = row.schema.as_ref() {
760 schema
761 .iter()
762 .cloned()
763 .zip(row.columns.iter().cloned())
764 .collect::<Vec<_>>()
765 } else {
766 Vec::new()
767 };
768 let normalized_fields = contract.normalize_update_fields(current_fields)?;
769 if let Some(schema) = db.collection_contract_arc(&collection) {
770 for column in schema
771 .declared_columns
772 .iter()
773 .filter(|column| column.generated.is_some())
774 {
775 modified_columns.push(column.name.clone());
776 context_index_dirty = true;
777 }
778 }
779 if row_contract_timestamps {
780 modified_columns.push("updated_at".to_string());
781 context_index_dirty = true;
782 }
783 if contract.requires_uniqueness_check(&modified_columns) {
784 contract.enforce_row_uniqueness(&normalized_fields, Some(id))?;
785 }
786 if self
787 .index_store_ref()
788 .touches_unique_index(&collection, &modified_columns)
789 {
790 self.enforce_unique_updates(
791 &collection,
792 &[(id, normalized_fields.clone())],
793 )?;
794 }
795 row.named = Some(normalized_fields.into_iter().collect());
796 }
797 }
798 crate::storage::EntityData::Node(node) => {
799 let mut field_ops = Vec::new();
800 let mut metadata_ops = Vec::new();
801 let mut node_type_ops = Vec::new();
802
803 for mut op in operations {
804 let Some(root) = op.path.first().map(String::as_str) else {
805 return Err(crate::RedDBError::Query(
806 "patch path cannot be empty".to_string(),
807 ));
808 };
809
810 match root {
811 "fields" | "properties" => {
812 if op.path.len() < 2 {
813 return Err(crate::RedDBError::Query(
814 "patch path 'fields' requires a nested key".to_string(),
815 ));
816 }
817 op.path.remove(0);
818 field_ops.push(op);
819 }
820 "metadata" => {
821 if op.path.len() < 2 {
822 return Err(crate::RedDBError::Query(
823 "patch path 'metadata' requires a nested key".to_string(),
824 ));
825 }
826 op.path.remove(0);
827 metadata_ops.push(op);
828 }
829 "node_type" => {
830 if op.path.len() != 1 {
831 return Err(crate::RedDBError::Query(
832 "patch path 'node_type' does not allow nested keys".to_string(),
833 ));
834 }
835 op.path.clear();
836 node_type_ops.push(op);
837 }
838 _ => {
839 return Err(crate::RedDBError::Query(format!(
840 "unsupported patch target '{root}' for graph nodes. Use fields/*, properties/*, node_type, or metadata/*"
841 )));
842 }
843 }
844 }
845
846 for op in node_type_ops {
847 context_index_dirty = true;
848 let value = op.value.ok_or_else(|| {
849 crate::RedDBError::Query("node_type operations require a value".to_string())
850 })?;
851
852 match op.op {
853 PatchEntityOperationType::Unset => {
854 return Err(crate::RedDBError::Query(
855 "node_type cannot be unset through patch operations".to_string(),
856 ));
857 }
858 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
859 let Some(node_type) = value.as_str() else {
860 return Err(crate::RedDBError::Query(
861 "node_type operation requires a text value".to_string(),
862 ));
863 };
864 graph_node_type = Some(node_type.to_string());
865 modified_columns.push("node_type".to_string());
866 }
867 }
868 }
869
870 if !field_ops.is_empty() {
871 context_index_dirty = true;
872 apply_patch_operations_to_storage_map(&mut node.properties, &field_ops)?;
873 }
874
875 if let Some(fields) = payload
876 .get("fields")
877 .and_then(crate::json::Value::as_object)
878 {
879 context_index_dirty = true;
880 for (key, value) in fields {
881 node.properties
882 .insert(key.clone(), json_to_storage_value(value)?);
883 }
884 }
885
886 if !metadata_ops.is_empty() {
887 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
888 let metadata = patch_metadata.get_or_insert_with(|| {
889 store.get_metadata(&collection, id).unwrap_or_default()
890 });
891 let mut metadata_json = metadata_to_json(metadata);
892 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
893 .map_err(crate::RedDBError::Query)?;
894 *metadata = metadata_from_json(&metadata_json)?;
895 metadata_changed = true;
896 }
897 }
898 crate::storage::EntityData::Edge(edge) => {
899 let mut field_ops = Vec::new();
900 let mut metadata_ops = Vec::new();
901 let mut weight_ops = Vec::new();
902
903 for mut op in operations {
904 let Some(root) = op.path.first().map(String::as_str) else {
905 return Err(crate::RedDBError::Query(
906 "patch path cannot be empty".to_string(),
907 ));
908 };
909
910 match root {
911 "fields" | "properties" => {
912 if op.path.len() < 2 {
913 return Err(crate::RedDBError::Query(
914 "patch path 'fields' requires a nested key".to_string(),
915 ));
916 }
917 op.path.remove(0);
918 field_ops.push(op);
919 }
920 "weight" => {
921 if op.path.len() != 1 {
922 return Err(crate::RedDBError::Query(
923 "patch path 'weight' does not allow nested keys".to_string(),
924 ));
925 }
926 op.path.clear();
927 weight_ops.push(op);
928 }
929 "metadata" => {
930 if op.path.len() < 2 {
931 return Err(crate::RedDBError::Query(
932 "patch path 'metadata' requires a nested key".to_string(),
933 ));
934 }
935 op.path.remove(0);
936 metadata_ops.push(op);
937 }
938 _ => {
939 return Err(crate::RedDBError::Query(format!(
940 "unsupported patch target '{root}' for graph edges. Use fields/*, weight, metadata/*"
941 )));
942 }
943 }
944 }
945
946 if !field_ops.is_empty() {
947 context_index_dirty = true;
948 apply_patch_operations_to_storage_map(&mut edge.properties, &field_ops)?;
949 }
950
951 for op in weight_ops {
952 context_index_dirty = true;
953 let value = op.value.ok_or_else(|| {
954 crate::RedDBError::Query("weight operations require a value".to_string())
955 })?;
956
957 match op.op {
958 PatchEntityOperationType::Unset => {
959 return Err(crate::RedDBError::Query(
960 "weight cannot be unset through patch operations".to_string(),
961 ));
962 }
963 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
964 let Some(weight) = value.as_f64() else {
965 return Err(crate::RedDBError::Query(
966 "weight operation requires a numeric value".to_string(),
967 ));
968 };
969 graph_edge_weight = Some(weight as f32);
970 edge.weight = weight as f32;
971 modified_columns.push("weight".to_string());
972 }
973 }
974 }
975
976 if let Some(fields) = payload
977 .get("fields")
978 .and_then(crate::json::Value::as_object)
979 {
980 context_index_dirty = true;
981 for (key, value) in fields {
982 edge.properties
983 .insert(key.clone(), json_to_storage_value(value)?);
984 }
985 }
986
987 if !metadata_ops.is_empty() {
988 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
989 let metadata = patch_metadata.get_or_insert_with(|| {
990 store.get_metadata(&collection, id).unwrap_or_default()
991 });
992 let mut metadata_json = metadata_to_json(metadata);
993 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
994 .map_err(crate::RedDBError::Query)?;
995 *metadata = metadata_from_json(&metadata_json)?;
996 metadata_changed = true;
997 }
998 }
999 crate::storage::EntityData::Vector(vector) => {
1000 let mut field_ops = Vec::new();
1001 let mut metadata_ops = Vec::new();
1002
1003 for mut op in operations {
1004 let Some(root) = op.path.first().map(String::as_str) else {
1005 return Err(crate::RedDBError::Query(
1006 "patch path cannot be empty".to_string(),
1007 ));
1008 };
1009
1010 match root {
1011 "fields" => {
1012 if op.path.len() < 2 {
1013 return Err(crate::RedDBError::Query(
1014 "patch path 'fields' requires a nested key".to_string(),
1015 ));
1016 }
1017 op.path.remove(0);
1018 let Some(target) = op.path.first().map(String::as_str) else {
1019 return Err(crate::RedDBError::Query(
1020 "patch path requires a target under fields".to_string(),
1021 ));
1022 };
1023 if !matches!(target, "dense" | "content" | "sparse") {
1024 return Err(crate::RedDBError::Query(format!(
1025 "unsupported vector patch target '{target}'"
1026 )));
1027 }
1028 field_ops.push(op);
1029 }
1030 "metadata" => {
1031 if op.path.len() < 2 {
1032 return Err(crate::RedDBError::Query(
1033 "patch path 'metadata' requires a nested key".to_string(),
1034 ));
1035 }
1036 op.path.remove(0);
1037 metadata_ops.push(op);
1038 }
1039 _ => {
1040 return Err(crate::RedDBError::Query(format!(
1041 "unsupported patch target '{root}' for vectors. Use fields/* or metadata/*"
1042 )));
1043 }
1044 }
1045 }
1046
1047 if !field_ops.is_empty() {
1048 context_index_dirty = true;
1049 apply_patch_operations_to_vector_fields(vector, &field_ops)?;
1050 }
1051
1052 if let Some(fields) = payload
1053 .get("fields")
1054 .and_then(crate::json::Value::as_object)
1055 {
1056 context_index_dirty = true;
1057 if let Some(content) =
1058 fields.get("content").and_then(crate::json::Value::as_str)
1059 {
1060 vector.content = Some(content.to_string());
1061 }
1062 if let Some(dense) = fields.get("dense") {
1063 vector.dense = dense
1064 .as_array()
1065 .ok_or_else(|| {
1066 crate::RedDBError::Query(
1067 "field 'dense' must be an array".to_string(),
1068 )
1069 })?
1070 .iter()
1071 .map(|value| {
1072 value.as_f64().map(|value| value as f32).ok_or_else(|| {
1073 crate::RedDBError::Query(
1074 "field 'dense' must contain only numbers".to_string(),
1075 )
1076 })
1077 })
1078 .collect::<Result<Vec<_>, _>>()?;
1079 }
1080 }
1081
1082 if !metadata_ops.is_empty() {
1083 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1084 let metadata = patch_metadata.get_or_insert_with(|| {
1085 store.get_metadata(&collection, id).unwrap_or_default()
1086 });
1087 let mut metadata_json = metadata_to_json(metadata);
1088 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1089 .map_err(crate::RedDBError::Query)?;
1090 *metadata = metadata_from_json(&metadata_json)?;
1091 metadata_changed = true;
1092 }
1093 }
1094 crate::storage::EntityData::TimeSeries(_)
1095 | crate::storage::EntityData::QueueMessage(_) => {
1096 return Err(crate::RedDBError::Query(
1097 "patch operations are not supported for TimeSeries or QueueMessage entities"
1098 .to_string(),
1099 ));
1100 }
1101 }
1102
1103 if let Some(node_type) = graph_node_type {
1104 if let crate::storage::EntityKind::GraphNode(node) = &mut entity.kind {
1105 node.node_type = node_type;
1106 }
1107 }
1108 if let Some(weight) = graph_edge_weight {
1109 if let crate::storage::EntityKind::GraphEdge(edge) = &mut entity.kind {
1110 edge.weight = (weight * 1000.0) as u32;
1111 }
1112 }
1113
1114 if let Some(metadata) = payload
1115 .get("metadata")
1116 .and_then(crate::json::Value::as_object)
1117 {
1118 let patch_metadata = patch_metadata
1119 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1120 for (key, value) in metadata {
1121 ensure_non_tree_reserved_metadata_key(key)?;
1122 patch_metadata.set(key.clone(), json_to_metadata_value(value)?);
1123 }
1124 metadata_changed = true;
1125 }
1126
1127 for (key, value) in parse_top_level_ttl_metadata_entries(&payload)? {
1128 let patch_metadata = patch_metadata
1129 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
1130 if matches!(value, crate::storage::unified::MetadataValue::Null) {
1131 patch_metadata.remove(&key);
1132 } else {
1133 patch_metadata.set(key, value);
1134 }
1135 metadata_changed = true;
1136 }
1137
1138 entity.updated_at = std::time::SystemTime::now()
1139 .duration_since(std::time::UNIX_EPOCH)
1140 .unwrap_or_default()
1141 .as_secs();
1142
1143 if let Some(xid) = versioned_update_xid {
1149 let logical_id = entity.logical_id();
1150 entity.id = store.next_entity_id();
1151 entity.set_logical_id(logical_id);
1152 entity.set_xmin(xid);
1153 entity.set_xmax(0);
1154 if let Some(old) = replaced_entity.as_mut() {
1155 old.set_xmax(xid);
1156 }
1157 }
1158
1159 modified_columns = dedupe_modified_columns(modified_columns);
1160
1161 Ok(AppliedEntityMutation {
1162 id: entity.id,
1163 collection,
1164 entity,
1165 metadata: patch_metadata,
1166 modified_columns,
1167 persist_metadata: metadata_changed,
1168 context_index_dirty,
1169 replaced_entity,
1170 replaced_entity_previous_xmax: previous_xmax,
1171 pre_mutation_fields,
1172 })
1173 }
1174
1175 pub(crate) fn apply_loaded_sql_update_row_core(
1176 &self,
1177 collection: String,
1178 mut entity: crate::storage::UnifiedEntity,
1179 static_field_assignments: &[(String, Value)],
1180 dynamic_field_assignments: Vec<(String, Value)>,
1181 static_metadata_assignments: &[(String, MetadataValue)],
1182 dynamic_metadata_assignments: Vec<(String, MetadataValue)>,
1183 row_contract_plan: Option<&RowUpdateContractPlan>,
1184 row_modified_columns_template: &[String],
1185 row_touches_unique_columns: bool,
1186 ) -> RedDBResult<AppliedEntityMutation> {
1187 let id = entity.id;
1188 let previous_xmax = entity.xmax;
1189 let db = self.db();
1190 let store = db.store();
1191 let Some(_) = store.get_collection(&collection) else {
1192 return Err(crate::RedDBError::NotFound(format!(
1193 "collection not found: {collection}"
1194 )));
1195 };
1196
1197 let versioned_update_xid = match self.current_xid() {
1198 Some(xid) => Some(xid),
1199 None => {
1200 let snapshot_manager = self.snapshot_manager();
1201 let xid = snapshot_manager.begin();
1202 snapshot_manager.commit(xid);
1203 Some(xid)
1204 }
1205 };
1206 let mut replaced_entity = versioned_update_xid.map(|xid| {
1207 let mut old = entity.clone();
1208 old.set_xmax(xid);
1209 old
1210 });
1211
1212 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
1213 let row_contract_timestamps = row_contract_plan
1214 .map(|plan| plan.timestamps_enabled)
1215 .unwrap_or(false);
1216 let mut metadata_changed = false;
1217 let mut modified_columns = row_modified_columns_template.to_vec();
1218 let mut context_index_dirty = !modified_columns.is_empty();
1219
1220 let pre_mutation_fields = entity_row_fields_snapshot(&entity);
1224
1225 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1226 return Err(crate::RedDBError::Query(
1227 "SQL row update fast path requires a row entity".to_string(),
1228 ));
1229 };
1230
1231 let _ = row_contract_plan;
1232
1233 let is_document_collection = db
1239 .collection_contract(&collection)
1240 .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Document)
1241 .unwrap_or(false);
1242 let kv_key = if row.get_field("value").is_some() || row.columns.len() >= 2 {
1243 row.get_field("key")
1244 .cloned()
1245 .or_else(|| row.columns.first().cloned())
1246 } else {
1247 None
1248 };
1249 if is_document_collection {
1250 let body_assignment = static_field_assignments
1254 .iter()
1255 .chain(dynamic_field_assignments.iter())
1256 .rev()
1257 .find(|(column, _)| column == "body")
1258 .cloned();
1259
1260 match body_assignment {
1261 Some((_, value)) => {
1262 let body = document_body_from_assignment(&value)?;
1263 replace_document_row_body(
1264 row,
1265 body,
1266 &collection,
1267 self.binary_document_body_enabled(),
1268 &mut modified_columns,
1269 )?;
1270 context_index_dirty = true;
1271 }
1272 None => {
1273 let mut static_row_assignments = Vec::new();
1276 let mut dynamic_row_assignments = Vec::new();
1277 let mut document_body_ops = Vec::new();
1278 for (column, value) in static_field_assignments.iter().cloned() {
1279 if column == "body" {
1280 static_row_assignments.push((column, value));
1281 } else {
1282 document_body_ops.push(document_body_set_operation(&column, value));
1283 }
1284 }
1285 for (column, value) in dynamic_field_assignments {
1286 if column == "body" {
1287 dynamic_row_assignments.push((column, value));
1288 } else {
1289 document_body_ops.push(document_body_set_operation(&column, value));
1290 }
1291 }
1292 apply_row_field_assignments_raw(row, static_row_assignments);
1293 apply_row_field_assignments_raw(row, dynamic_row_assignments);
1294 if !document_body_ops.is_empty() {
1295 let mut body = document_body_from_row(row)?;
1296 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
1297 replace_document_row_body(
1298 row,
1299 body,
1300 &collection,
1301 self.binary_document_body_enabled(),
1302 &mut modified_columns,
1303 )?;
1304 context_index_dirty = true;
1305 }
1306 }
1307 }
1308 } else {
1309 let mut all_assignments: Vec<(String, Value)> = static_field_assignments.to_vec();
1311 all_assignments.extend(dynamic_field_assignments);
1312 apply_row_field_assignments_raw(row, all_assignments);
1313 if let Some(key) = kv_key {
1314 set_row_field(row, "key", key);
1315 }
1316 }
1317
1318 for (key, value) in static_metadata_assignments
1319 .iter()
1320 .cloned()
1321 .chain(dynamic_metadata_assignments)
1322 {
1323 ensure_non_tree_reserved_metadata_key(&key)?;
1324 patch_metadata
1325 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default())
1326 .set(key, value);
1327 metadata_changed = true;
1328 }
1329
1330 if !modified_columns.is_empty() || row_contract_timestamps {
1331 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1332 if row_contract_timestamps {
1333 context_index_dirty = true;
1334 set_row_field(row, "updated_at", contract.managed_timestamp_value());
1335 modified_columns.push("updated_at".to_string());
1336 }
1337 let expression_contract = if row_contract_plan.is_some_and(|plan| plan.has_expressions)
1338 {
1339 db.collection_contract_arc(&collection)
1340 } else {
1341 None
1342 };
1343 if let Some(schema) = &expression_contract {
1344 let normalized = contract.normalize_update_fields(collect_row_fields(row))?;
1345 for column in schema
1346 .declared_columns
1347 .iter()
1348 .filter(|column| column.generated.is_some())
1349 {
1350 modified_columns.push(column.name.clone());
1351 }
1352 for (name, value) in normalized {
1353 set_row_field(row, &name, value);
1354 }
1355 context_index_dirty = true;
1356 }
1357 if row_touches_unique_columns || expression_contract.is_some() {
1358 let current_fields = collect_row_fields(row);
1359 contract.enforce_row_uniqueness(¤t_fields, Some(id))?;
1360 }
1361 }
1362
1363 entity.updated_at = std::time::SystemTime::now()
1364 .duration_since(std::time::UNIX_EPOCH)
1365 .unwrap_or_default()
1366 .as_secs();
1367
1368 if let Some(xid) = versioned_update_xid {
1369 let logical_id = entity.logical_id();
1370 entity.id = store.next_entity_id();
1371 entity.set_logical_id(logical_id);
1372 entity.set_xmin(xid);
1373 entity.set_xmax(0);
1374 if let Some(old) = replaced_entity.as_mut() {
1375 old.set_xmax(xid);
1376 }
1377 }
1378
1379 modified_columns = dedupe_modified_columns(modified_columns);
1380
1381 Ok(AppliedEntityMutation {
1382 id: entity.id,
1383 collection,
1384 entity,
1385 metadata: patch_metadata,
1386 modified_columns,
1387 persist_metadata: metadata_changed,
1388 context_index_dirty,
1389 replaced_entity,
1390 replaced_entity_previous_xmax: previous_xmax,
1391 pre_mutation_fields,
1392 })
1393 }
1394
1395 pub(crate) fn persist_applied_entity_mutations(
1396 &self,
1397 applied: &[AppliedEntityMutation],
1398 ) -> RedDBResult<()> {
1399 if applied.is_empty() {
1400 return Ok(());
1401 }
1402
1403 let store = self.db().store();
1404 let _publication = store.vault_publication_lock.lock();
1408 let collection = &applied[0].collection;
1409 let Some(manager) = store.get_collection(collection) else {
1410 return Err(crate::RedDBError::NotFound(format!(
1411 "collection not found: {collection}"
1412 )));
1413 };
1414
1415 let mut ordinary = Vec::with_capacity(applied.len());
1416 for item in applied {
1417 if let Some(old_version) = item.replaced_entity.as_ref() {
1418 store
1419 .install_versioned_table_row_update(
1420 collection,
1421 old_version.clone(),
1422 item.entity.clone(),
1423 if item.persist_metadata {
1424 item.metadata.as_ref()
1425 } else {
1426 None
1427 },
1428 )
1429 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
1430 if self.current_xid().is_some() {
1431 self.record_pending_versioned_update(
1432 crate::runtime::impl_core::current_connection_id(),
1433 collection,
1434 old_version.id,
1435 item.entity.id,
1436 old_version.xmax,
1437 item.replaced_entity_previous_xmax,
1438 );
1439 }
1440 } else {
1441 ordinary.push(item);
1442 }
1443 }
1444 if ordinary.is_empty() {
1445 return Ok(());
1446 }
1447
1448 manager
1449 .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
1450 (
1451 &item.entity,
1452 item.modified_columns.as_slice(),
1453 if item.persist_metadata {
1454 item.metadata.as_ref()
1455 } else {
1456 None
1457 },
1458 )
1459 }))
1460 .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
1461
1462 let indexed_cols = self
1471 .index_store_ref()
1472 .indexed_columns_set(collection.as_str());
1473 let all_hot = !indexed_cols.is_empty()
1474 && ordinary.iter().all(|item| {
1475 !item.persist_metadata
1476 && !item
1477 .modified_columns
1478 .iter()
1479 .any(|c| indexed_cols.contains(c))
1480 })
1481 || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
1482
1483 let entity_refs: Vec<&crate::storage::UnifiedEntity> =
1487 ordinary.iter().map(|item| &item.entity).collect();
1488 let persist_fn = if all_hot {
1489 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
1490 } else {
1491 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
1492 };
1493 persist_fn(store.as_ref(), collection, &entity_refs)
1494 .map_err(|err| crate::RedDBError::Internal(err.to_string()))
1495 }
1496
1497 pub(crate) fn flush_applied_entity_mutation(
1498 &self,
1499 applied: &AppliedEntityMutation,
1500 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1501 ) -> RedDBResult<()> {
1502 let store = self.db().store();
1503 if applied.context_index_dirty {
1504 store
1505 .context_index()
1506 .index_entity(&applied.collection, &applied.entity);
1507 }
1508 let mut changed_columns: Option<Vec<String>> = None;
1516 if !applied.pre_mutation_fields.is_empty() {
1517 let post = entity_row_fields_snapshot(&applied.entity);
1518 if !post.is_empty() {
1519 let damage = crate::application::entity::row_damage_vector(
1520 &applied.pre_mutation_fields,
1521 &post,
1522 );
1523 if !damage.is_empty() {
1524 changed_columns = Some(
1525 damage
1526 .touched_columns()
1527 .into_iter()
1528 .map(str::to_string)
1529 .collect(),
1530 );
1531 }
1532
1533 let indexed_cols: std::collections::HashSet<String> = self
1545 .index_store_ref()
1546 .indexed_columns_set_with_parents(applied.collection.as_str());
1547 let pre_index_fields = self
1551 .index_store_ref()
1552 .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
1553 let post_index_fields = self
1554 .index_store_ref()
1555 .augment_body_derived_index_fields(&post, &indexed_cols);
1556 let modified_cols: std::collections::HashSet<String> =
1557 crate::application::entity::row_damage_vector(
1558 &pre_index_fields,
1559 &post_index_fields,
1560 )
1561 .touched_columns()
1562 .into_iter()
1563 .map(str::to_string)
1564 .collect();
1565 if let Some(old_version) = applied.replaced_entity.as_ref() {
1566 let old_index_fields: Vec<(String, Value)> = pre_index_fields
1567 .iter()
1568 .filter(|(col, _)| indexed_cols.contains(col))
1569 .cloned()
1570 .collect();
1571 let new_index_fields: Vec<(String, Value)> = post_index_fields
1572 .iter()
1573 .filter(|(col, _)| indexed_cols.contains(col))
1574 .cloned()
1575 .collect();
1576 if !old_index_fields.is_empty() {
1577 self.index_store_ref()
1578 .index_entity_delete(
1579 &applied.collection,
1580 old_version.id,
1581 &old_index_fields,
1582 )
1583 .map_err(crate::RedDBError::Internal)?;
1584 }
1585 if !new_index_fields.is_empty() {
1586 self.index_store_ref()
1587 .index_entity_insert(
1588 &applied.collection,
1589 applied.entity.id,
1590 &new_index_fields,
1591 )
1592 .map_err(crate::RedDBError::Internal)?;
1593 }
1594 } else {
1595 let decision = crate::storage::engine::hot_update::decide(
1596 &crate::storage::engine::hot_update::HotUpdateInputs {
1597 collection: applied.collection.as_str(),
1598 indexed_columns: &indexed_cols,
1599 modified_columns: &modified_cols,
1600 new_tuple_size: 0,
1604 page_free_space: usize::MAX,
1605 },
1606 );
1607 if !decision.can_hot {
1608 self.index_store_ref()
1609 .index_entity_update(
1610 &applied.collection,
1611 applied.id,
1612 &pre_index_fields,
1613 &post_index_fields,
1614 )
1615 .map_err(crate::RedDBError::Internal)?;
1616 } else {
1617 tracing::debug!(
1621 collection = %reddb_wire::audit_safe_log_field(&applied.collection),
1622 "hot_update fast-path: skipped index_entity_update"
1623 );
1624 }
1625 }
1626 }
1627 }
1628 drop(topology_guard);
1629 #[cfg(test)]
1630 self.index_store_ref().mutation_test_hook(
1631 &applied.collection,
1632 crate::runtime::MutationTestPhase::BeforeEvents,
1633 );
1634 self.cdc_emit_prebuilt_with_columns(
1635 crate::replication::cdc::ChangeOperation::Update,
1636 &applied.collection,
1637 &applied.entity,
1638 "entity",
1639 applied.metadata.as_ref(),
1640 true,
1641 changed_columns,
1642 );
1643 Ok(())
1644 }
1645
1646 pub(crate) fn apply_loaded_patch_entity(
1647 &self,
1648 collection: String,
1649 entity: crate::storage::UnifiedEntity,
1650 payload: JsonValue,
1651 operations: Vec<PatchEntityOperation>,
1652 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1653 ) -> RedDBResult<CreateEntityOutput> {
1654 let applied =
1655 self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
1656 if !crate::runtime::rls_injection::entity_passes_rls_action(
1657 self,
1658 &applied.collection,
1659 reddb_rql::ast::PolicyAction::Update,
1660 &applied.entity,
1661 ) {
1662 return Err(crate::RedDBError::NotFound("entity not found".to_string()));
1663 }
1664 let _reservation = self.admit_entity_mutation(&applied)?;
1665 self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
1666 #[cfg(test)]
1667 self.index_store_ref().mutation_test_hook(
1668 &applied.collection,
1669 crate::runtime::MutationTestPhase::StoragePublished,
1670 );
1671 self.flush_applied_entity_mutation(&applied, topology_guard)?;
1672 Ok(CreateEntityOutput {
1673 id: applied.id,
1674 entity: Some(public_document_entity(applied.entity)),
1675 })
1676 }
1677}
1678
1679fn public_document_entity(
1688 mut entity: crate::storage::UnifiedEntity,
1689) -> crate::storage::UnifiedEntity {
1690 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1691 return entity;
1692 };
1693 let Some(named) = row.named.as_mut() else {
1694 return entity;
1695 };
1696 let Some(Value::Json(bytes)) = named.get("body") else {
1697 return entity;
1698 };
1699 let bytes = bytes.clone();
1700 if let Some(fields) = crate::document_body::body_fields(&bytes) {
1701 for (name, value) in fields {
1702 named.entry(name).or_insert(value);
1703 }
1704 }
1705 if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
1706 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
1707 named.insert("body".to_string(), Value::Json(json_bytes));
1708 }
1709 }
1710 entity
1711}
1712
1713fn ensure_non_tree_reserved_metadata_patch_paths(
1714 operations: &[PatchEntityOperation],
1715) -> RedDBResult<()> {
1716 for operation in operations {
1717 let Some(key) = operation.path.first().map(String::as_str) else {
1718 continue;
1719 };
1720 ensure_non_tree_reserved_metadata_key(key)?;
1721 }
1722 Ok(())
1723}
1724
1725fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
1726 if key.starts_with(TREE_METADATA_PREFIX) {
1727 return Err(crate::RedDBError::Query(format!(
1728 "metadata key '{}' is reserved for managed trees",
1729 key
1730 )));
1731 }
1732 Ok(())
1733}
1734
1735fn ensure_non_tree_reserved_metadata_entries(
1736 metadata: &[(String, MetadataValue)],
1737) -> RedDBResult<()> {
1738 for (key, _) in metadata {
1739 ensure_non_tree_reserved_metadata_key(key)?;
1740 }
1741 Ok(())
1742}
1743
1744fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
1745 if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
1746 return Err(crate::RedDBError::Query(format!(
1747 "edge label '{}' is reserved for managed trees",
1748 TREE_CHILD_EDGE_LABEL
1749 )));
1750 }
1751 Ok(())
1752}
1753
1754impl RedDBRuntime {
1755 pub(crate) fn create_node_unchecked(
1756 &self,
1757 mut input: CreateNodeInput,
1758 ) -> RedDBResult<CreateEntityOutput> {
1759 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
1760 crate::runtime::mutation::enforce_tenant_fields(
1761 self,
1762 &input.collection,
1763 &mut input.properties,
1764 )?;
1765 let node_type = input
1766 .node_type
1767 .clone()
1768 .unwrap_or_else(|| input.label.clone());
1769 let candidate = crate::storage::UnifiedEntity::new(
1770 crate::storage::EntityId::new(0),
1771 crate::storage::EntityKind::GraphNode(Box::new(
1772 crate::storage::unified::GraphNodeKind {
1773 label: input.label.clone(),
1774 node_type,
1775 },
1776 )),
1777 crate::storage::EntityData::Node(crate::storage::NodeData::with_properties(
1778 input.properties.iter().cloned().collect(),
1779 )),
1780 );
1781 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
1782 let db = self.db();
1783 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1784 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1785 let mut metadata = input.metadata;
1786 contract.apply_default_ttl(&mut metadata);
1787 let mut builder = db.node(&input.collection, &input.label);
1788
1789 if let Some(node_type) = input.node_type {
1790 builder = builder.node_type(node_type);
1791 }
1792
1793 for (key, value) in input.properties {
1794 builder = builder.property(key, value);
1795 }
1796
1797 for (key, value) in metadata {
1798 builder = builder.metadata(key, value);
1799 }
1800
1801 for embedding in input.embeddings {
1802 if let Some(model) = embedding.model {
1803 builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
1804 } else {
1805 builder = builder.embedding(embedding.name, embedding.vector);
1806 }
1807 }
1808
1809 for link in input.table_links {
1810 builder = builder.link_to_table(link.key, link.table);
1811 }
1812
1813 for link in input.node_links {
1814 builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
1815 }
1816
1817 let id = builder.save()?;
1818 self.stamp_xmin_if_in_txn(&input.collection, id);
1821 refresh_context_index(&db, &input.collection, id)?;
1822 self.cdc_emit(
1823 crate::replication::cdc::ChangeOperation::Insert,
1824 &input.collection,
1825 id.raw(),
1826 "graph_node",
1827 );
1828 Ok(CreateEntityOutput {
1829 id,
1830 entity: db.store().get(&input.collection, id),
1831 })
1832 }
1833
1834 pub(crate) fn create_edge_unchecked(
1835 &self,
1836 mut input: CreateEdgeInput,
1837 ) -> RedDBResult<CreateEntityOutput> {
1838 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
1839 crate::runtime::mutation::enforce_tenant_fields(
1840 self,
1841 &input.collection,
1842 &mut input.properties,
1843 )?;
1844 let weight = input.weight.unwrap_or(1.0);
1845 let candidate = crate::storage::UnifiedEntity::new(
1846 crate::storage::EntityId::new(0),
1847 crate::storage::EntityKind::GraphEdge(Box::new(
1848 crate::storage::unified::GraphEdgeKind {
1849 label: input.label.clone(),
1850 from_node: input.from.raw().to_string(),
1851 to_node: input.to.raw().to_string(),
1852 weight: (weight * 1000.0) as u32,
1853 },
1854 )),
1855 crate::storage::EntityData::Edge(crate::storage::EdgeData::with_properties(
1856 weight,
1857 input.properties.iter().cloned().collect(),
1858 )),
1859 );
1860 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
1861 let db = self.db();
1862 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1863 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1864 let mut metadata = input.metadata;
1865 contract.apply_default_ttl(&mut metadata);
1866 let mut builder = db
1867 .edge(&input.collection, &input.label)
1868 .from(input.from)
1869 .to(input.to);
1870
1871 if let Some(weight) = input.weight {
1872 builder = builder.weight(weight);
1873 }
1874
1875 for (key, value) in input.properties {
1876 builder = builder.property(key, value);
1877 }
1878
1879 for (key, value) in metadata {
1880 builder = builder.metadata(key, value);
1881 }
1882
1883 let id = builder.save()?;
1884 self.stamp_xmin_if_in_txn(&input.collection, id);
1887 refresh_context_index(&db, &input.collection, id)?;
1888 self.cdc_emit(
1889 crate::replication::cdc::ChangeOperation::Insert,
1890 &input.collection,
1891 id.raw(),
1892 "graph_edge",
1893 );
1894 Ok(CreateEntityOutput {
1895 id,
1896 entity: db.store().get(&input.collection, id),
1897 })
1898 }
1899}
1900
1901fn create_rows_batch_prevalidated_columnar_with_outputs(
1902 runtime: &RedDBRuntime,
1903 collection: String,
1904 column_names: std::sync::Arc<Vec<String>>,
1905 rows: Vec<Vec<reddb_types::Value>>,
1906) -> RedDBResult<Vec<CreateEntityOutput>> {
1907 use crate::storage::{
1908 unified::{EntityData, EntityKind, RowData},
1909 EntityId, UnifiedEntity,
1910 };
1911 use std::sync::Arc;
1912
1913 if rows.is_empty() {
1914 return Ok(Vec::new());
1915 }
1916 if crate::runtime::execution_context::current_auth_identity().is_some() {
1917 let inputs = rows
1918 .into_iter()
1919 .map(|values| CreateRowInput {
1920 collection: collection.clone(),
1921 fields: column_names.iter().cloned().zip(values).collect(),
1922 metadata: Vec::new(),
1923 node_links: Vec::new(),
1924 vector_links: Vec::new(),
1925 })
1926 .collect();
1927 return runtime.create_rows_batch_prevalidated_with_outputs(CreateRowsBatchInput {
1928 collection,
1929 rows: inputs,
1930 suppress_events: false,
1931 });
1932 }
1933 if runtime.index_store_ref().has_unique_index(&collection) {
1936 return runtime.create_rows_batch_columnar_with_outputs(collection, column_names, rows);
1937 }
1938 runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1939 runtime.check_batch_size(rows.len())?;
1940 runtime.check_db_size()?;
1941
1942 let db = runtime.db();
1943 let contract = CollectionContractWriteEnforcer::new(runtime, &db, &collection);
1944 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1945
1946 let store = db.store();
1947 let table_arc: Arc<str> = Arc::from(collection.as_str());
1948
1949 let indexed_cols = runtime
1950 .index_store_ref()
1951 .indexed_columns_set(collection.as_str());
1952 let has_secondary_indexes = !indexed_cols.is_empty();
1953 let mut field_snapshots: Vec<Vec<(String, reddb_types::Value)>> = if has_secondary_indexes {
1954 Vec::with_capacity(rows.len())
1955 } else {
1956 Vec::new()
1957 };
1958
1959 let entities: Vec<UnifiedEntity> = rows
1960 .into_iter()
1961 .map(|values| {
1962 if has_secondary_indexes {
1963 let mut snap: Vec<(String, reddb_types::Value)> =
1964 Vec::with_capacity(indexed_cols.len());
1965 for (name, val) in column_names.iter().zip(values.iter()) {
1966 if indexed_cols.contains(name) {
1967 snap.push((name.clone(), val.clone()));
1968 }
1969 }
1970 field_snapshots.push(snap);
1971 }
1972 let mut row = RowData::new(values);
1973 row.schema = Some(Arc::clone(&column_names));
1974 UnifiedEntity::new(
1975 EntityId::new(0),
1976 EntityKind::TableRow {
1977 table: Arc::clone(&table_arc),
1978 row_id: 0,
1979 },
1980 EntityData::Row(row),
1981 )
1982 })
1983 .collect();
1984
1985 let ids = store
1986 .bulk_insert(&collection, entities)
1987 .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
1988
1989 if has_secondary_indexes {
1990 let index_rows: Vec<(EntityId, Vec<(String, reddb_types::Value)>)> = ids
1991 .iter()
1992 .zip(field_snapshots)
1993 .map(|(id, fields)| (*id, fields))
1994 .collect();
1995 runtime
1996 .index_store_ref()
1997 .index_entity_insert_batch(&collection, &index_rows)
1998 .map_err(crate::RedDBError::Internal)?;
1999 }
2000
2001 runtime.invalidate_result_cache();
2002 runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
2003
2004 Ok(ids
2005 .into_iter()
2006 .map(|id| CreateEntityOutput { id, entity: None })
2007 .collect())
2008}
2009
2010impl RuntimeEntityPort for RedDBRuntime {
2011 fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
2012 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2013 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2014 self,
2015 &input.collection,
2016 );
2017 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2018 let db = self.db();
2019 let CreateRowInput {
2020 collection,
2021 fields,
2022 metadata: input_metadata,
2023 node_links,
2024 vector_links,
2025 } = input;
2026 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2027 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2028 let mut metadata = input_metadata;
2029 contract.apply_default_ttl(&mut metadata);
2030 let fields = contract.normalize_insert_fields(fields)?;
2031 contract.enforce_row_uniqueness(&fields, None)?;
2032 let engine = self.mutation_engine();
2034 let result = engine.apply(
2035 collection.clone(),
2036 vec![crate::runtime::mutation::MutationRow {
2037 fields,
2038 metadata,
2039 node_links,
2040 vector_links,
2041 }],
2042 )?;
2043 let id = result.ids[0];
2044 Ok(CreateEntityOutput {
2049 id,
2050 entity: db.store().get(&collection, id),
2051 })
2052 }
2053
2054 fn create_rows_batch(
2055 &self,
2056 input: CreateRowsBatchInput,
2057 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2058 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2059 self,
2060 &input.collection,
2061 );
2062 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2063 if input.rows.is_empty() {
2064 return Ok(Vec::new());
2065 }
2066 self.check_batch_size(input.rows.len())?;
2067 self.check_db_size()?;
2068
2069 let db = self.db();
2070 let collection = input.collection;
2071 let suppress_events = input.suppress_events;
2072 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2073 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2074
2075 let mut prepared_rows = Vec::with_capacity(input.rows.len());
2076 let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
2077 for row in input.rows {
2078 if row.collection != collection {
2079 return Err(crate::RedDBError::Query(format!(
2080 "batch row collection mismatch: expected '{}', got '{}'",
2081 collection, row.collection
2082 )));
2083 }
2084
2085 let mut metadata = row.metadata;
2086 contract.apply_default_ttl(&mut metadata);
2087 let fields = contract.normalize_insert_fields(row.fields)?;
2088 contract.enforce_row_uniqueness(&fields, None)?;
2089 uniqueness_rows.push(fields.clone());
2090 prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
2091 }
2092
2093 contract.enforce_batch_uniqueness(&uniqueness_rows)?;
2094
2095 let engine = {
2098 let e = self.mutation_engine();
2099 if suppress_events {
2100 e.with_suppress_events()
2101 } else {
2102 e
2103 }
2104 };
2105 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
2106 .into_iter()
2107 .map(|(fields, metadata, node_links, vector_links)| {
2108 crate::runtime::mutation::MutationRow {
2109 fields,
2110 metadata,
2111 node_links,
2112 vector_links,
2113 }
2114 })
2115 .collect();
2116
2117 let result = engine.apply(collection.clone(), mutation_rows)?;
2118
2119 let store = db.store();
2120 Ok(result
2121 .ids
2122 .into_iter()
2123 .map(|id| CreateEntityOutput {
2124 id,
2125 entity: store.get(&collection, id),
2126 })
2127 .collect())
2128 }
2129
2130 fn create_rows_batch_prevalidated_columnar(
2131 &self,
2132 collection: String,
2133 column_names: std::sync::Arc<Vec<String>>,
2134 rows: Vec<Vec<reddb_types::Value>>,
2135 ) -> RedDBResult<usize> {
2136 create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2137 .map(|outputs| outputs.len())
2138 }
2139
2140 fn create_rows_batch_columnar(
2141 &self,
2142 collection: String,
2143 column_names: std::sync::Arc<Vec<String>>,
2144 rows: Vec<Vec<reddb_types::Value>>,
2145 ) -> RedDBResult<usize> {
2146 self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2147 .map(|outputs| outputs.len())
2148 }
2149
2150 fn create_rows_batch_columnar_with_outputs(
2151 &self,
2152 collection: String,
2153 column_names: std::sync::Arc<Vec<String>>,
2154 rows: Vec<Vec<reddb_types::Value>>,
2155 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2156 if rows.is_empty() {
2157 return Ok(Vec::new());
2158 }
2159 self.check_batch_size(rows.len())?;
2160 self.check_db_size()?;
2161
2162 let db = self.db();
2163 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2164 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2165
2166 let needs_normalisation = self.index_store_ref().has_unique_index(&collection)
2176 || match db.collection_contract(&collection) {
2177 Some(c) => {
2178 c.declared_model == crate::catalog::CollectionModel::Table
2179 && (!c.declared_columns.is_empty()
2180 || c.table_def
2181 .as_ref()
2182 .map(|t| !t.columns.is_empty())
2183 .unwrap_or(false))
2184 }
2185 None => false,
2186 };
2187 if !needs_normalisation {
2188 return create_rows_batch_prevalidated_columnar_with_outputs(
2189 self,
2190 collection,
2191 column_names,
2192 rows,
2193 );
2194 }
2195
2196 let ncols = column_names.len();
2203 let tuple_rows: Vec<CreateRowInput> = rows
2204 .into_iter()
2205 .map(|values| {
2206 let mut fields: Vec<(String, reddb_types::Value)> = Vec::with_capacity(ncols);
2207 for (name, value) in column_names.iter().zip(values) {
2208 fields.push((name.clone(), value));
2209 }
2210 CreateRowInput {
2211 collection: collection.clone(),
2212 fields,
2213 metadata: Vec::new(),
2214 node_links: Vec::new(),
2215 vector_links: Vec::new(),
2216 }
2217 })
2218 .collect();
2219 self.create_rows_batch(CreateRowsBatchInput {
2220 collection,
2221 rows: tuple_rows,
2222 suppress_events: false,
2223 })
2224 }
2225
2226 fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
2227 self.create_rows_batch_prevalidated_with_outputs(input)
2228 .map(|outputs| outputs.len())
2229 }
2230
2231 fn create_rows_batch_prevalidated_with_outputs(
2232 &self,
2233 input: CreateRowsBatchInput,
2234 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2235 if input.rows.is_empty() {
2236 return Ok(Vec::new());
2237 }
2238 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2239 self.check_batch_size(input.rows.len())?;
2240 self.check_db_size()?;
2241
2242 let db = self.db();
2243 let collection = input.collection;
2244 let suppress_events = input.suppress_events;
2245 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2250 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2251
2252 let default_ttl_plan = contract.default_ttl_plan();
2258
2259 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
2260 Vec::with_capacity(input.rows.len());
2261 let mut mutation_rows = mutation_rows;
2262 for row in input.rows {
2263 if row.collection != collection {
2264 return Err(crate::RedDBError::Query(format!(
2265 "batch row collection mismatch: expected '{}', got '{}'",
2266 collection, row.collection
2267 )));
2268 }
2269 let mut metadata = row.metadata;
2270 default_ttl_plan.apply(&mut metadata);
2271 mutation_rows.push(crate::runtime::mutation::MutationRow {
2272 fields: row.fields,
2273 metadata,
2274 node_links: row.node_links,
2275 vector_links: row.vector_links,
2276 });
2277 }
2278
2279 let engine = {
2280 let engine = self.mutation_engine();
2281 if suppress_events {
2282 engine.with_suppress_events()
2283 } else {
2284 engine
2285 }
2286 };
2287 let result = engine.apply(collection.clone(), mutation_rows)?;
2288 let store = db.store();
2289 Ok(result
2290 .ids
2291 .into_iter()
2292 .map(|id| CreateEntityOutput {
2293 id,
2294 entity: store.get(&collection, id),
2295 })
2296 .collect())
2297 }
2298
2299 fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
2300 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2301 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2302 input.properties.iter().map(|(key, _)| key.as_str()),
2303 &format!("node '{}'", input.collection),
2304 )?;
2305 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2306 self.create_node_unchecked(input)
2307 }
2308
2309 fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
2310 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2311 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2312 input.properties.iter().map(|(key, _)| key.as_str()),
2313 &format!("edge '{}'", input.collection),
2314 )?;
2315 ensure_non_tree_structural_edge_label(&input.label)?;
2316 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2317 self.create_edge_unchecked(input)
2318 }
2319
2320 fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
2321 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2322 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2323 let mut vector_data = crate::storage::VectorData::new(input.dense.clone());
2324 vector_data.content = input.content.clone();
2325 let candidate = crate::storage::UnifiedEntity::new(
2326 crate::storage::EntityId::new(0),
2327 crate::storage::EntityKind::Vector {
2328 collection: input.collection.clone(),
2329 },
2330 crate::storage::EntityData::Vector(vector_data),
2331 );
2332 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2333 let db = self.db();
2334 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2335 contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
2336 contract.ensure_vector_dimension(input.dense.len())?;
2337 let mut metadata = input.metadata;
2338 contract.apply_default_ttl(&mut metadata);
2339 let mut builder = db.vector(&input.collection).dense(input.dense);
2340
2341 if let Some(content) = input.content {
2342 builder = builder.content(content);
2343 }
2344
2345 for (key, value) in metadata {
2346 builder = builder.metadata(key, value);
2347 }
2348
2349 if let Some(link_row) = input.link_row {
2350 builder = builder.link_to_table(link_row);
2351 }
2352
2353 if let Some(link_node) = input.link_node {
2354 builder = builder.link_to_node(link_node);
2355 }
2356
2357 let id = builder.save_with_xmin(self.current_xid())?;
2360 let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
2361 Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
2362 _ => None,
2363 };
2364 refresh_context_index(&db, &input.collection, id)?;
2365 if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
2371 state.ensure_populated(&db.store(), &input.collection);
2375 use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
2377 fire(InjectionPoint::BeforeWalFsync);
2378 let _ = db
2379 .store()
2380 .append_vector_insert_record(&input.collection, id.raw(), &dense);
2381 fire(InjectionPoint::BeforeIndexCommit);
2382 let mut index = state.index.lock();
2383 index.insert(id, dense.clone());
2384 fire(InjectionPoint::BeforeExtentFsync);
2391 if let Some(extent) = state.extent.lock().as_mut() {
2392 let packed = index.encode_for_extent(&dense);
2393 let _ = extent.append(&packed);
2394 }
2395 }
2396 self.cdc_emit(
2397 crate::replication::cdc::ChangeOperation::Insert,
2398 &input.collection,
2399 id.raw(),
2400 "vector",
2401 );
2402 Ok(CreateEntityOutput {
2403 id,
2404 entity: db.store().get(&input.collection, id),
2405 })
2406 }
2407
2408 fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
2409 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2410 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2411 let db = self.db();
2412 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2413 contract.ensure_model(crate::catalog::CollectionModel::Document)?;
2414
2415 if let JsonValue::Object(ref map) = input.body {
2416 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2417 map.keys().map(String::as_str),
2418 &format!("document '{}'", input.collection),
2419 )?;
2420 }
2421
2422 let mut fields: Vec<(String, reddb_types::Value)> = vec![(
2426 "body".to_string(),
2427 reddb_types::Value::Json(
2428 crate::json::to_vec(&input.body)
2429 .map_err(|err| crate::RedDBError::Query(err.to_string()))?,
2430 ),
2431 )];
2432 crate::runtime::mutation::enforce_tenant_fields(self, &input.collection, &mut fields)?;
2433 let (field_names, field_values) =
2434 crate::runtime::impl_dml_support::pairwise_columns_values(&fields);
2435 let scoped_body =
2436 crate::runtime::impl_dml_support::find_document_body_json(&field_names, &field_values)?;
2437 let body_bytes = crate::document_body::serialize_document_body(
2438 &scoped_body,
2439 self.binary_document_body_enabled(),
2440 )?;
2441 if let Some((_, body)) = fields.iter_mut().find(|(name, _)| name == "body") {
2442 *body = reddb_types::Value::Json(body_bytes);
2443 }
2444 let mut row = crate::storage::RowData::new(Vec::new());
2445 row.named = Some(fields.iter().cloned().collect());
2446 let candidate = crate::storage::UnifiedEntity::new(
2447 crate::storage::EntityId::new(0),
2448 crate::storage::EntityKind::TableRow {
2449 table: std::sync::Arc::from(input.collection.as_str()),
2450 row_id: 0,
2451 },
2452 crate::storage::EntityData::Row(row),
2453 );
2454 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2455
2456 let mut metadata = input.metadata;
2457 contract.apply_default_ttl(&mut metadata);
2458 let columns: Vec<(&str, reddb_types::Value)> = fields
2459 .iter()
2460 .map(|(key, value)| (key.as_str(), value.clone()))
2461 .collect();
2462 let mut builder = db.row(&input.collection, columns);
2463
2464 for (key, value) in metadata {
2465 builder = builder.metadata(key, value);
2466 }
2467
2468 for node in input.node_links {
2469 builder = builder.link_to_node(node);
2470 }
2471
2472 for vector in input.vector_links {
2473 builder = builder.link_to_vector(vector);
2474 }
2475
2476 let id = builder.save()?;
2477 self.stamp_xmin_if_in_txn(&input.collection, id);
2479 self.index_store_ref()
2480 .index_entity_insert(&input.collection, id, &fields)
2481 .map_err(crate::RedDBError::Internal)?;
2482 refresh_context_index(&db, &input.collection, id)?;
2483 self.cdc_emit(
2484 crate::replication::cdc::ChangeOperation::Insert,
2485 &input.collection,
2486 id.raw(),
2487 "document",
2488 );
2489 Ok(CreateEntityOutput {
2490 id,
2491 entity: db.store().get(&input.collection, id),
2492 })
2493 }
2494
2495 fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
2496 crate::runtime::kv_write::create(self, input, None)
2497 }
2498
2499 fn create_timeseries_point(
2500 &self,
2501 input: CreateTimeSeriesPointInput,
2502 ) -> RedDBResult<CreateEntityOutput> {
2503 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2504 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2505 let db = self.db();
2506 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2507 contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
2508
2509 let mut fields = vec![
2510 ("metric".to_string(), reddb_types::Value::text(input.metric)),
2511 ("value".to_string(), reddb_types::Value::Float(input.value)),
2512 ];
2513
2514 if let Some(timestamp_ns) = input.timestamp_ns {
2515 fields.push((
2516 "timestamp".to_string(),
2517 reddb_types::Value::UnsignedInteger(timestamp_ns),
2518 ));
2519 }
2520
2521 if !input.tags.is_empty() {
2522 let tags_json = JsonValue::Object(
2523 input
2524 .tags
2525 .into_iter()
2526 .map(|(key, value)| (key, JsonValue::String(value)))
2527 .collect(),
2528 );
2529 let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
2530 crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
2531 })?;
2532 fields.push(("tags".to_string(), reddb_types::Value::Json(tags_bytes)));
2533 }
2534
2535 let collection = input.collection;
2536 let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
2537 self.stamp_xmin_if_in_txn(&collection, id);
2540 refresh_context_index(&db, &collection, id)?;
2541
2542 Ok(CreateEntityOutput {
2543 id,
2544 entity: db.store().get(&collection, id),
2545 })
2546 }
2547
2548 fn get_kv(
2549 &self,
2550 collection: &str,
2551 key: &str,
2552 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
2553 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Select, collection)?;
2554 let resolver = crate::runtime::kv_read::KvReadResolver::for_rest_kv(self, collection)?;
2555 let Some(record) = resolver.record(key)? else {
2556 return Ok(None);
2557 };
2558 let visible = self
2559 .db()
2560 .store()
2561 .get(collection, record.id)
2562 .is_some_and(|entity| {
2563 crate::runtime::rls_injection::entity_passes_rls_action(
2564 self,
2565 collection,
2566 reddb_rql::ast::PolicyAction::Select,
2567 &entity,
2568 )
2569 });
2570 Ok(visible.then_some((record.value.unwrap_or(Value::Null), record.id)))
2571 }
2572
2573 fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
2574 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2575 crate::runtime::impl_kv::KvAtomicOps::new(self).delete(
2576 crate::catalog::CollectionModel::Kv,
2577 collection,
2578 key,
2579 )
2580 }
2581
2582 fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
2583 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2584 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Update, &input.collection)?;
2585 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2586 self,
2587 &input.collection,
2588 );
2589 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2590 let topology_lock = self
2591 .index_store_ref()
2592 .collection_topology_lock(&input.collection);
2593 let topology_guard = topology_lock.read();
2594 let PatchEntityInput {
2595 collection,
2596 id,
2597 payload,
2598 operations,
2599 } = input;
2600 let db = self.db();
2601 let store = db.store();
2602 let Some(manager) = store.get_collection(&collection) else {
2603 return Err(crate::RedDBError::NotFound(format!(
2604 "collection not found: {collection}"
2605 )));
2606 };
2607 let Some(entity) = manager.get(id) else {
2608 return Err(crate::RedDBError::NotFound(format!(
2609 "entity not found: {}",
2610 id.raw()
2611 )));
2612 };
2613 if !crate::runtime::rls_injection::entity_passes_rls_action(
2614 self,
2615 &collection,
2616 reddb_rql::ast::PolicyAction::Update,
2617 &entity,
2618 ) {
2619 return Err(crate::RedDBError::NotFound(format!(
2620 "entity not found: {}",
2621 id.raw()
2622 )));
2623 }
2624 self.apply_loaded_patch_entity(collection, entity, payload, operations, topology_guard)
2625 }
2626
2627 fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
2628 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2629 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Delete, &input.collection)?;
2630 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2631 self,
2632 &input.collection,
2633 );
2634 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2635 let topology_lock = self
2636 .index_store_ref()
2637 .collection_topology_lock(&input.collection);
2638 let topology_guard = topology_lock.read();
2639 let store = self.db().store();
2640 let Some(entity) = store.get(&input.collection, input.id) else {
2644 return Ok(DeleteEntityOutput {
2645 deleted: false,
2646 id: input.id,
2647 });
2648 };
2649 if !crate::runtime::rls_injection::entity_passes_rls_action(
2650 self,
2651 &input.collection,
2652 reddb_rql::ast::PolicyAction::Delete,
2653 &entity,
2654 ) {
2655 return Ok(DeleteEntityOutput {
2656 deleted: false,
2657 id: input.id,
2658 });
2659 }
2660 let pre_delete_fields = entity_row_fields_snapshot(&entity);
2661 let deleted = store
2665 .delete(&input.collection, input.id)
2666 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2667 if deleted {
2668 #[cfg(test)]
2669 self.index_store_ref().mutation_test_hook(
2670 &input.collection,
2671 crate::runtime::MutationTestPhase::StoragePublished,
2672 );
2673 store.context_index().remove_entity(input.id);
2674 if !pre_delete_fields.is_empty() {
2677 self.index_store_ref()
2678 .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
2679 .map_err(crate::RedDBError::Internal)?;
2680 }
2681 drop(topology_guard);
2682 #[cfg(test)]
2683 self.index_store_ref().mutation_test_hook(
2684 &input.collection,
2685 crate::runtime::MutationTestPhase::BeforeEvents,
2686 );
2687 self.cdc_emit(
2688 crate::replication::cdc::ChangeOperation::Delete,
2689 &input.collection,
2690 input.id.raw(),
2691 "entity",
2692 );
2693 }
2694 Ok(DeleteEntityOutput {
2695 deleted,
2696 id: input.id,
2697 })
2698 }
2699}