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 if store.get_collection(&collection).is_none() {
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.field_bloom =
1139 crate::storage::unified::entity::compute_entity_field_bloom(&entity.data);
1140 entity.updated_at = std::time::SystemTime::now()
1141 .duration_since(std::time::UNIX_EPOCH)
1142 .unwrap_or_default()
1143 .as_secs();
1144
1145 if let Some(xid) = versioned_update_xid {
1151 let logical_id = entity.logical_id();
1152 entity.id = store.next_entity_id();
1153 entity.set_logical_id(logical_id);
1154 entity.set_xmin(xid);
1155 entity.set_xmax(0);
1156 if let Some(old) = replaced_entity.as_mut() {
1157 old.set_xmax(xid);
1158 }
1159 }
1160
1161 modified_columns = dedupe_modified_columns(modified_columns);
1162
1163 Ok(AppliedEntityMutation {
1164 id: entity.id,
1165 collection,
1166 entity,
1167 metadata: patch_metadata,
1168 modified_columns,
1169 persist_metadata: metadata_changed,
1170 context_index_dirty,
1171 replaced_entity,
1172 replaced_entity_previous_xmax: previous_xmax,
1173 pre_mutation_fields,
1174 })
1175 }
1176
1177 pub(crate) fn apply_loaded_sql_update_row_core(
1178 &self,
1179 collection: String,
1180 mut entity: crate::storage::UnifiedEntity,
1181 static_field_assignments: &[(String, Value)],
1182 dynamic_field_assignments: Vec<(String, Value)>,
1183 static_metadata_assignments: &[(String, MetadataValue)],
1184 dynamic_metadata_assignments: Vec<(String, MetadataValue)>,
1185 row_contract_plan: Option<&RowUpdateContractPlan>,
1186 row_modified_columns_template: &[String],
1187 row_touches_unique_columns: bool,
1188 ) -> RedDBResult<AppliedEntityMutation> {
1189 let id = entity.id;
1190 let previous_xmax = entity.xmax;
1191 let db = self.db();
1192 let store = db.store();
1193 let Some(_) = store.get_collection(&collection) else {
1194 return Err(crate::RedDBError::NotFound(format!(
1195 "collection not found: {collection}"
1196 )));
1197 };
1198
1199 let versioned_update_xid = match self.current_xid() {
1200 Some(xid) => Some(xid),
1201 None => {
1202 let snapshot_manager = self.snapshot_manager();
1203 let xid = snapshot_manager.begin();
1204 snapshot_manager.commit(xid);
1205 Some(xid)
1206 }
1207 };
1208 let mut replaced_entity = versioned_update_xid.map(|xid| {
1209 let mut old = entity.clone();
1210 old.set_xmax(xid);
1211 old
1212 });
1213
1214 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
1215 let row_contract_timestamps = row_contract_plan
1216 .map(|plan| plan.timestamps_enabled)
1217 .unwrap_or(false);
1218 let mut metadata_changed = false;
1219 let mut modified_columns = row_modified_columns_template.to_vec();
1220 let mut context_index_dirty = !modified_columns.is_empty();
1221
1222 let pre_mutation_fields = entity_row_fields_snapshot(&entity);
1226
1227 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1228 return Err(crate::RedDBError::Query(
1229 "SQL row update fast path requires a row entity".to_string(),
1230 ));
1231 };
1232
1233 let _ = row_contract_plan;
1234
1235 let is_document_collection = db
1241 .collection_contract(&collection)
1242 .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Document)
1243 .unwrap_or(false);
1244 let kv_key = if row.get_field("value").is_some() || row.columns.len() >= 2 {
1245 row.get_field("key")
1246 .cloned()
1247 .or_else(|| row.columns.first().cloned())
1248 } else {
1249 None
1250 };
1251 if is_document_collection {
1252 let body_assignment = static_field_assignments
1256 .iter()
1257 .chain(dynamic_field_assignments.iter())
1258 .rev()
1259 .find(|(column, _)| column == "body")
1260 .cloned();
1261
1262 match body_assignment {
1263 Some((_, value)) => {
1264 let body = document_body_from_assignment(&value)?;
1265 replace_document_row_body(
1266 row,
1267 body,
1268 &collection,
1269 self.binary_document_body_enabled(),
1270 &mut modified_columns,
1271 )?;
1272 context_index_dirty = true;
1273 }
1274 None => {
1275 let mut static_row_assignments = Vec::new();
1278 let mut dynamic_row_assignments = Vec::new();
1279 let mut document_body_ops = Vec::new();
1280 for (column, value) in static_field_assignments.iter().cloned() {
1281 if column == "body" {
1282 static_row_assignments.push((column, value));
1283 } else {
1284 document_body_ops.push(document_body_set_operation(&column, value));
1285 }
1286 }
1287 for (column, value) in dynamic_field_assignments {
1288 if column == "body" {
1289 dynamic_row_assignments.push((column, value));
1290 } else {
1291 document_body_ops.push(document_body_set_operation(&column, value));
1292 }
1293 }
1294 apply_row_field_assignments_raw(row, static_row_assignments);
1295 apply_row_field_assignments_raw(row, dynamic_row_assignments);
1296 if !document_body_ops.is_empty() {
1297 let mut body = document_body_from_row(row)?;
1298 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
1299 replace_document_row_body(
1300 row,
1301 body,
1302 &collection,
1303 self.binary_document_body_enabled(),
1304 &mut modified_columns,
1305 )?;
1306 context_index_dirty = true;
1307 }
1308 }
1309 }
1310 } else {
1311 let mut all_assignments: Vec<(String, Value)> = static_field_assignments.to_vec();
1313 all_assignments.extend(dynamic_field_assignments);
1314 apply_row_field_assignments_raw(row, all_assignments);
1315 if let Some(key) = kv_key {
1316 set_row_field(row, "key", key);
1317 }
1318 }
1319
1320 for (key, value) in static_metadata_assignments
1321 .iter()
1322 .cloned()
1323 .chain(dynamic_metadata_assignments)
1324 {
1325 ensure_non_tree_reserved_metadata_key(&key)?;
1326 patch_metadata
1327 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default())
1328 .set(key, value);
1329 metadata_changed = true;
1330 }
1331
1332 if !modified_columns.is_empty() || row_contract_timestamps {
1333 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
1334 if row_contract_timestamps {
1335 context_index_dirty = true;
1336 set_row_field(row, "updated_at", contract.managed_timestamp_value());
1337 modified_columns.push("updated_at".to_string());
1338 }
1339 let expression_contract = if row_contract_plan.is_some_and(|plan| plan.has_expressions)
1340 {
1341 db.collection_contract_arc(&collection)
1342 } else {
1343 None
1344 };
1345 if let Some(schema) = &expression_contract {
1346 let normalized = contract.normalize_update_fields(collect_row_fields(row))?;
1347 for column in schema
1348 .declared_columns
1349 .iter()
1350 .filter(|column| column.generated.is_some())
1351 {
1352 modified_columns.push(column.name.clone());
1353 }
1354 for (name, value) in normalized {
1355 set_row_field(row, &name, value);
1356 }
1357 context_index_dirty = true;
1358 }
1359 if row_touches_unique_columns || expression_contract.is_some() {
1360 let current_fields = collect_row_fields(row);
1361 contract.enforce_row_uniqueness(¤t_fields, Some(id))?;
1362 }
1363 }
1364
1365 entity.field_bloom =
1366 crate::storage::unified::entity::compute_entity_field_bloom(&entity.data);
1367 entity.updated_at = std::time::SystemTime::now()
1368 .duration_since(std::time::UNIX_EPOCH)
1369 .unwrap_or_default()
1370 .as_secs();
1371
1372 if let Some(xid) = versioned_update_xid {
1373 let logical_id = entity.logical_id();
1374 entity.id = store.next_entity_id();
1375 entity.set_logical_id(logical_id);
1376 entity.set_xmin(xid);
1377 entity.set_xmax(0);
1378 if let Some(old) = replaced_entity.as_mut() {
1379 old.set_xmax(xid);
1380 }
1381 }
1382
1383 modified_columns = dedupe_modified_columns(modified_columns);
1384
1385 Ok(AppliedEntityMutation {
1386 id: entity.id,
1387 collection,
1388 entity,
1389 metadata: patch_metadata,
1390 modified_columns,
1391 persist_metadata: metadata_changed,
1392 context_index_dirty,
1393 replaced_entity,
1394 replaced_entity_previous_xmax: previous_xmax,
1395 pre_mutation_fields,
1396 })
1397 }
1398
1399 pub(crate) fn persist_applied_entity_mutations(
1400 &self,
1401 applied: &[AppliedEntityMutation],
1402 ) -> RedDBResult<()> {
1403 if applied.is_empty() {
1404 return Ok(());
1405 }
1406
1407 let store = self.db().store();
1408 let _publication = store.vault_publication_lock.lock();
1412 let collection = &applied[0].collection;
1413 let Some(manager) = store.get_collection(collection) else {
1414 return Err(crate::RedDBError::NotFound(format!(
1415 "collection not found: {collection}"
1416 )));
1417 };
1418
1419 let mut ordinary = Vec::with_capacity(applied.len());
1420 for item in applied {
1421 if let Some(old_version) = item.replaced_entity.as_ref() {
1422 store
1423 .install_versioned_table_row_update(
1424 collection,
1425 old_version.clone(),
1426 item.entity.clone(),
1427 if item.persist_metadata {
1428 item.metadata.as_ref()
1429 } else {
1430 None
1431 },
1432 )
1433 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
1434 if self.current_xid().is_some() {
1435 self.record_pending_versioned_update(
1436 crate::runtime::impl_core::current_connection_id(),
1437 collection,
1438 old_version.id,
1439 item.entity.id,
1440 old_version.xmax,
1441 item.replaced_entity_previous_xmax,
1442 );
1443 }
1444 } else {
1445 ordinary.push(item);
1446 }
1447 }
1448 if ordinary.is_empty() {
1449 return Ok(());
1450 }
1451
1452 manager
1453 .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
1454 (
1455 &item.entity,
1456 item.modified_columns.as_slice(),
1457 if item.persist_metadata {
1458 item.metadata.as_ref()
1459 } else {
1460 None
1461 },
1462 )
1463 }))
1464 .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
1465
1466 let indexed_cols = self
1475 .index_store_ref()
1476 .indexed_columns_set(collection.as_str());
1477 let all_hot = !indexed_cols.is_empty()
1478 && ordinary.iter().all(|item| {
1479 !item.persist_metadata
1480 && !item
1481 .modified_columns
1482 .iter()
1483 .any(|c| indexed_cols.contains(c))
1484 })
1485 || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
1486
1487 let entity_refs: Vec<&crate::storage::UnifiedEntity> =
1491 ordinary.iter().map(|item| &item.entity).collect();
1492 let persist_fn = if all_hot {
1493 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
1494 } else {
1495 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
1496 };
1497 persist_fn(store.as_ref(), collection, &entity_refs)
1498 .map_err(|err| crate::RedDBError::Internal(err.to_string()))
1499 }
1500
1501 pub(crate) fn flush_applied_entity_mutation(
1502 &self,
1503 applied: &AppliedEntityMutation,
1504 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1505 ) -> RedDBResult<()> {
1506 let store = self.db().store();
1507 if applied.context_index_dirty {
1508 store
1509 .context_index()
1510 .index_entity(&applied.collection, &applied.entity);
1511 }
1512 let mut changed_columns: Option<Vec<String>> = None;
1520 if !applied.pre_mutation_fields.is_empty() {
1521 let post = entity_row_fields_snapshot(&applied.entity);
1522 if !post.is_empty() {
1523 let damage = crate::application::entity::row_damage_vector(
1524 &applied.pre_mutation_fields,
1525 &post,
1526 );
1527 if !damage.is_empty() {
1528 changed_columns = Some(
1529 damage
1530 .touched_columns()
1531 .into_iter()
1532 .map(str::to_string)
1533 .collect(),
1534 );
1535 }
1536
1537 let indexed_cols: std::collections::HashSet<String> = self
1549 .index_store_ref()
1550 .indexed_columns_set_with_parents(applied.collection.as_str());
1551 let pre_index_fields = self
1555 .index_store_ref()
1556 .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
1557 let post_index_fields = self
1558 .index_store_ref()
1559 .augment_body_derived_index_fields(&post, &indexed_cols);
1560 let modified_cols: std::collections::HashSet<String> =
1561 crate::application::entity::row_damage_vector(
1562 &pre_index_fields,
1563 &post_index_fields,
1564 )
1565 .touched_columns()
1566 .into_iter()
1567 .map(str::to_string)
1568 .collect();
1569 if let Some(old_version) = applied.replaced_entity.as_ref() {
1570 let old_index_fields: Vec<(String, Value)> = pre_index_fields
1571 .iter()
1572 .filter(|(col, _)| indexed_cols.contains(col))
1573 .cloned()
1574 .collect();
1575 let new_index_fields: Vec<(String, Value)> = post_index_fields
1576 .iter()
1577 .filter(|(col, _)| indexed_cols.contains(col))
1578 .cloned()
1579 .collect();
1580 if !old_index_fields.is_empty() {
1581 self.index_store_ref()
1582 .index_entity_delete(
1583 &applied.collection,
1584 old_version.id,
1585 &old_index_fields,
1586 )
1587 .map_err(crate::RedDBError::Internal)?;
1588 }
1589 if !new_index_fields.is_empty() {
1590 self.index_store_ref()
1591 .index_entity_insert(
1592 &applied.collection,
1593 applied.entity.id,
1594 &new_index_fields,
1595 )
1596 .map_err(crate::RedDBError::Internal)?;
1597 }
1598 } else {
1599 let decision = crate::storage::engine::hot_update::decide(
1600 &crate::storage::engine::hot_update::HotUpdateInputs {
1601 collection: applied.collection.as_str(),
1602 indexed_columns: &indexed_cols,
1603 modified_columns: &modified_cols,
1604 new_tuple_size: 0,
1608 page_free_space: usize::MAX,
1609 },
1610 );
1611 if !decision.can_hot {
1612 self.index_store_ref()
1613 .index_entity_update(
1614 &applied.collection,
1615 applied.id,
1616 &pre_index_fields,
1617 &post_index_fields,
1618 )
1619 .map_err(crate::RedDBError::Internal)?;
1620 } else {
1621 tracing::debug!(
1625 collection = %reddb_wire::audit_safe_log_field(&applied.collection),
1626 "hot_update fast-path: skipped index_entity_update"
1627 );
1628 }
1629 }
1630 }
1631 }
1632 drop(topology_guard);
1633 #[cfg(test)]
1634 self.index_store_ref().mutation_test_hook(
1635 &applied.collection,
1636 crate::runtime::MutationTestPhase::BeforeEvents,
1637 );
1638 self.cdc_emit_prebuilt_with_columns(
1639 crate::replication::cdc::ChangeOperation::Update,
1640 &applied.collection,
1641 &applied.entity,
1642 "entity",
1643 applied.metadata.as_ref(),
1644 true,
1645 changed_columns,
1646 );
1647 Ok(())
1648 }
1649
1650 pub(crate) fn apply_loaded_patch_entity(
1651 &self,
1652 collection: String,
1653 entity: crate::storage::UnifiedEntity,
1654 payload: JsonValue,
1655 operations: Vec<PatchEntityOperation>,
1656 topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
1657 ) -> RedDBResult<CreateEntityOutput> {
1658 let applied =
1659 self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
1660 if !crate::runtime::rls_injection::entity_passes_rls_action(
1661 self,
1662 &applied.collection,
1663 reddb_rql::ast::PolicyAction::Update,
1664 &applied.entity,
1665 ) {
1666 return Err(crate::RedDBError::NotFound("entity not found".to_string()));
1667 }
1668 let _reservation = self.admit_entity_mutation(&applied)?;
1669 self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
1670 #[cfg(test)]
1671 self.index_store_ref().mutation_test_hook(
1672 &applied.collection,
1673 crate::runtime::MutationTestPhase::StoragePublished,
1674 );
1675 self.flush_applied_entity_mutation(&applied, topology_guard)?;
1676 Ok(CreateEntityOutput {
1677 id: applied.id,
1678 entity: Some(public_document_entity(applied.entity)),
1679 })
1680 }
1681}
1682
1683fn public_document_entity(
1692 mut entity: crate::storage::UnifiedEntity,
1693) -> crate::storage::UnifiedEntity {
1694 let crate::storage::EntityData::Row(row) = &mut entity.data else {
1695 return entity;
1696 };
1697 let Some(named) = row.named.as_mut() else {
1698 return entity;
1699 };
1700 let Some(Value::Json(bytes)) = named.get("body") else {
1701 return entity;
1702 };
1703 let bytes = bytes.clone();
1704 if let Some(fields) = crate::document_body::body_fields(&bytes) {
1705 for (name, value) in fields {
1706 named.entry(name).or_insert(value);
1707 }
1708 }
1709 if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
1710 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
1711 named.insert("body".to_string(), Value::Json(json_bytes));
1712 }
1713 }
1714 entity
1715}
1716
1717fn ensure_non_tree_reserved_metadata_patch_paths(
1718 operations: &[PatchEntityOperation],
1719) -> RedDBResult<()> {
1720 for operation in operations {
1721 let Some(key) = operation.path.first().map(String::as_str) else {
1722 continue;
1723 };
1724 ensure_non_tree_reserved_metadata_key(key)?;
1725 }
1726 Ok(())
1727}
1728
1729fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
1730 if key.starts_with(TREE_METADATA_PREFIX) {
1731 return Err(crate::RedDBError::Query(format!(
1732 "metadata key '{}' is reserved for managed trees",
1733 key
1734 )));
1735 }
1736 Ok(())
1737}
1738
1739fn ensure_non_tree_reserved_metadata_entries(
1740 metadata: &[(String, MetadataValue)],
1741) -> RedDBResult<()> {
1742 for (key, _) in metadata {
1743 ensure_non_tree_reserved_metadata_key(key)?;
1744 }
1745 Ok(())
1746}
1747
1748fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
1749 if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
1750 return Err(crate::RedDBError::Query(format!(
1751 "edge label '{}' is reserved for managed trees",
1752 TREE_CHILD_EDGE_LABEL
1753 )));
1754 }
1755 Ok(())
1756}
1757
1758impl RedDBRuntime {
1759 pub(crate) fn create_node_unchecked(
1760 &self,
1761 mut input: CreateNodeInput,
1762 ) -> RedDBResult<CreateEntityOutput> {
1763 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
1764 crate::runtime::mutation::enforce_tenant_fields(
1765 self,
1766 &input.collection,
1767 &mut input.properties,
1768 )?;
1769 let node_type = input
1770 .node_type
1771 .clone()
1772 .unwrap_or_else(|| input.label.clone());
1773 let candidate = crate::storage::UnifiedEntity::new(
1774 crate::storage::EntityId::new(0),
1775 crate::storage::EntityKind::GraphNode(Box::new(
1776 crate::storage::unified::GraphNodeKind {
1777 label: input.label.clone(),
1778 node_type,
1779 },
1780 )),
1781 crate::storage::EntityData::Node(crate::storage::NodeData::with_properties(
1782 input.properties.iter().cloned().collect(),
1783 )),
1784 );
1785 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
1786 let db = self.db();
1787 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1788 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1789 let mut metadata = input.metadata;
1790 contract.apply_default_ttl(&mut metadata);
1791 let mut builder = db.node(&input.collection, &input.label);
1792
1793 if let Some(node_type) = input.node_type {
1794 builder = builder.node_type(node_type);
1795 }
1796
1797 for (key, value) in input.properties {
1798 builder = builder.property(key, value);
1799 }
1800
1801 for (key, value) in metadata {
1802 builder = builder.metadata(key, value);
1803 }
1804
1805 for embedding in input.embeddings {
1806 if let Some(model) = embedding.model {
1807 builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
1808 } else {
1809 builder = builder.embedding(embedding.name, embedding.vector);
1810 }
1811 }
1812
1813 for link in input.table_links {
1814 builder = builder.link_to_table(link.key, link.table);
1815 }
1816
1817 for link in input.node_links {
1818 builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
1819 }
1820
1821 let id = builder.save()?;
1822 self.stamp_xmin_if_in_txn(&input.collection, id);
1825 refresh_context_index(&db, &input.collection, id)?;
1826 self.cdc_emit(
1827 crate::replication::cdc::ChangeOperation::Insert,
1828 &input.collection,
1829 id.raw(),
1830 "graph_node",
1831 );
1832 Ok(CreateEntityOutput {
1833 id,
1834 entity: db.store().get(&input.collection, id),
1835 })
1836 }
1837
1838 pub(crate) fn create_edge_unchecked(
1839 &self,
1840 mut input: CreateEdgeInput,
1841 ) -> RedDBResult<CreateEntityOutput> {
1842 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
1843 crate::runtime::mutation::enforce_tenant_fields(
1844 self,
1845 &input.collection,
1846 &mut input.properties,
1847 )?;
1848 let weight = input.weight.unwrap_or(1.0);
1849 let candidate = crate::storage::UnifiedEntity::new(
1850 crate::storage::EntityId::new(0),
1851 crate::storage::EntityKind::GraphEdge(Box::new(
1852 crate::storage::unified::GraphEdgeKind {
1853 label: input.label.clone(),
1854 from_node: input.from.raw().to_string(),
1855 to_node: input.to.raw().to_string(),
1856 weight: (weight * 1000.0) as u32,
1857 },
1858 )),
1859 crate::storage::EntityData::Edge(crate::storage::EdgeData::with_properties(
1860 weight,
1861 input.properties.iter().cloned().collect(),
1862 )),
1863 );
1864 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
1865 let db = self.db();
1866 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
1867 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
1868 let mut metadata = input.metadata;
1869 contract.apply_default_ttl(&mut metadata);
1870 let mut builder = db
1871 .edge(&input.collection, &input.label)
1872 .from(input.from)
1873 .to(input.to);
1874
1875 if let Some(weight) = input.weight {
1876 builder = builder.weight(weight);
1877 }
1878
1879 for (key, value) in input.properties {
1880 builder = builder.property(key, value);
1881 }
1882
1883 for (key, value) in metadata {
1884 builder = builder.metadata(key, value);
1885 }
1886
1887 let id = builder.save()?;
1888 self.stamp_xmin_if_in_txn(&input.collection, id);
1891 refresh_context_index(&db, &input.collection, id)?;
1892 self.cdc_emit(
1893 crate::replication::cdc::ChangeOperation::Insert,
1894 &input.collection,
1895 id.raw(),
1896 "graph_edge",
1897 );
1898 Ok(CreateEntityOutput {
1899 id,
1900 entity: db.store().get(&input.collection, id),
1901 })
1902 }
1903}
1904
1905fn create_rows_batch_prevalidated_columnar_with_outputs(
1906 runtime: &RedDBRuntime,
1907 collection: String,
1908 column_names: std::sync::Arc<Vec<String>>,
1909 rows: Vec<Vec<reddb_types::Value>>,
1910) -> RedDBResult<Vec<CreateEntityOutput>> {
1911 use crate::storage::{
1912 unified::{EntityData, EntityKind, RowData},
1913 EntityId, UnifiedEntity,
1914 };
1915 use std::sync::Arc;
1916
1917 if rows.is_empty() {
1918 return Ok(Vec::new());
1919 }
1920 if crate::runtime::execution_context::current_auth_identity().is_some() {
1921 let inputs = rows
1922 .into_iter()
1923 .map(|values| CreateRowInput {
1924 collection: collection.clone(),
1925 fields: column_names.iter().cloned().zip(values).collect(),
1926 metadata: Vec::new(),
1927 node_links: Vec::new(),
1928 vector_links: Vec::new(),
1929 })
1930 .collect();
1931 return runtime.create_rows_batch_prevalidated_with_outputs(CreateRowsBatchInput {
1932 collection,
1933 rows: inputs,
1934 suppress_events: false,
1935 });
1936 }
1937 if runtime.index_store_ref().has_unique_index(&collection) {
1940 return runtime.create_rows_batch_columnar_with_outputs(collection, column_names, rows);
1941 }
1942 runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1943 runtime.check_batch_size(rows.len())?;
1944 runtime.check_db_size()?;
1945 let _indexed_mutation = runtime.index_store_ref().begin_indexed_mutation();
1946
1947 let db = runtime.db();
1948 let contract = CollectionContractWriteEnforcer::new(runtime, &db, &collection);
1949 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1950
1951 let store = db.store();
1952 let table_arc: Arc<str> = Arc::from(collection.as_str());
1953
1954 let indexed_cols = runtime
1955 .index_store_ref()
1956 .indexed_columns_set(collection.as_str());
1957 let has_secondary_indexes = !indexed_cols.is_empty();
1958 let mut field_snapshots: Vec<Vec<(String, reddb_types::Value)>> = if has_secondary_indexes {
1959 Vec::with_capacity(rows.len())
1960 } else {
1961 Vec::new()
1962 };
1963
1964 let entities: Vec<UnifiedEntity> = rows
1965 .into_iter()
1966 .map(|values| {
1967 if has_secondary_indexes {
1968 let mut snap: Vec<(String, reddb_types::Value)> =
1969 Vec::with_capacity(indexed_cols.len());
1970 for (name, val) in column_names.iter().zip(values.iter()) {
1971 if indexed_cols.contains(name) {
1972 snap.push((name.clone(), val.clone()));
1973 }
1974 }
1975 field_snapshots.push(snap);
1976 }
1977 let mut row = RowData::new(values);
1978 row.schema = Some(Arc::clone(&column_names));
1979 UnifiedEntity::new(
1980 EntityId::new(0),
1981 EntityKind::TableRow {
1982 table: Arc::clone(&table_arc),
1983 row_id: 0,
1984 },
1985 EntityData::Row(row),
1986 )
1987 })
1988 .collect();
1989
1990 let ids = store
1991 .bulk_insert(&collection, entities)
1992 .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
1993
1994 if has_secondary_indexes {
1995 let index_rows: Vec<(EntityId, Vec<(String, reddb_types::Value)>)> = ids
1996 .iter()
1997 .zip(field_snapshots)
1998 .map(|(id, fields)| (*id, fields))
1999 .collect();
2000 runtime
2001 .index_store_ref()
2002 .index_entity_insert_batch(&collection, &index_rows)
2003 .map_err(crate::RedDBError::Internal)?;
2004 }
2005
2006 runtime.invalidate_result_cache();
2007 runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
2008
2009 Ok(ids
2010 .into_iter()
2011 .map(|id| CreateEntityOutput { id, entity: None })
2012 .collect())
2013}
2014
2015impl RuntimeEntityPort for RedDBRuntime {
2016 fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
2017 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2018 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2019 self,
2020 &input.collection,
2021 );
2022 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2023 let db = self.db();
2024 let CreateRowInput {
2025 collection,
2026 fields,
2027 metadata: input_metadata,
2028 node_links,
2029 vector_links,
2030 } = input;
2031 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2032 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2033 let mut metadata = input_metadata;
2034 contract.apply_default_ttl(&mut metadata);
2035 let fields = contract.normalize_insert_fields(fields)?;
2036 contract.enforce_row_uniqueness(&fields, None)?;
2037 let engine = self.mutation_engine();
2039 let result = engine.apply(
2040 collection.clone(),
2041 vec![crate::runtime::mutation::MutationRow {
2042 fields,
2043 metadata,
2044 node_links,
2045 vector_links,
2046 }],
2047 )?;
2048 let id = result.ids[0];
2049 Ok(CreateEntityOutput {
2054 id,
2055 entity: db.store().get(&collection, id),
2056 })
2057 }
2058
2059 fn create_rows_batch(
2060 &self,
2061 input: CreateRowsBatchInput,
2062 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2063 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2064 self,
2065 &input.collection,
2066 );
2067 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2068 if input.rows.is_empty() {
2069 return Ok(Vec::new());
2070 }
2071 self.check_batch_size(input.rows.len())?;
2072 self.check_db_size()?;
2073
2074 let db = self.db();
2075 let collection = input.collection;
2076 let suppress_events = input.suppress_events;
2077 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2078 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2079
2080 let mut prepared_rows = Vec::with_capacity(input.rows.len());
2081 let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
2082 for row in input.rows {
2083 if row.collection != collection {
2084 return Err(crate::RedDBError::Query(format!(
2085 "batch row collection mismatch: expected '{}', got '{}'",
2086 collection, row.collection
2087 )));
2088 }
2089
2090 let mut metadata = row.metadata;
2091 contract.apply_default_ttl(&mut metadata);
2092 let fields = contract.normalize_insert_fields(row.fields)?;
2093 contract.enforce_row_uniqueness(&fields, None)?;
2094 uniqueness_rows.push(fields.clone());
2095 prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
2096 }
2097
2098 contract.enforce_batch_uniqueness(&uniqueness_rows)?;
2099
2100 let engine = {
2103 let e = self.mutation_engine();
2104 if suppress_events {
2105 e.with_suppress_events()
2106 } else {
2107 e
2108 }
2109 };
2110 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
2111 .into_iter()
2112 .map(|(fields, metadata, node_links, vector_links)| {
2113 crate::runtime::mutation::MutationRow {
2114 fields,
2115 metadata,
2116 node_links,
2117 vector_links,
2118 }
2119 })
2120 .collect();
2121
2122 let result = engine.apply(collection.clone(), mutation_rows)?;
2123
2124 let store = db.store();
2125 Ok(result
2126 .ids
2127 .into_iter()
2128 .map(|id| CreateEntityOutput {
2129 id,
2130 entity: store.get(&collection, id),
2131 })
2132 .collect())
2133 }
2134
2135 fn create_rows_batch_prevalidated_columnar(
2136 &self,
2137 collection: String,
2138 column_names: std::sync::Arc<Vec<String>>,
2139 rows: Vec<Vec<reddb_types::Value>>,
2140 ) -> RedDBResult<usize> {
2141 create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2142 .map(|outputs| outputs.len())
2143 }
2144
2145 fn create_rows_batch_columnar(
2146 &self,
2147 collection: String,
2148 column_names: std::sync::Arc<Vec<String>>,
2149 rows: Vec<Vec<reddb_types::Value>>,
2150 ) -> RedDBResult<usize> {
2151 self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2152 .map(|outputs| outputs.len())
2153 }
2154
2155 fn create_rows_batch_columnar_with_outputs(
2156 &self,
2157 collection: String,
2158 column_names: std::sync::Arc<Vec<String>>,
2159 rows: Vec<Vec<reddb_types::Value>>,
2160 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2161 if rows.is_empty() {
2162 return Ok(Vec::new());
2163 }
2164 self.check_batch_size(rows.len())?;
2165 self.check_db_size()?;
2166
2167 let db = self.db();
2168 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2169 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2170
2171 let needs_normalisation = self.index_store_ref().has_unique_index(&collection)
2181 || match db.collection_contract(&collection) {
2182 Some(c) => {
2183 c.declared_model == crate::catalog::CollectionModel::Table
2184 && (!c.declared_columns.is_empty()
2185 || c.table_def
2186 .as_ref()
2187 .map(|t| !t.columns.is_empty())
2188 .unwrap_or(false))
2189 }
2190 None => false,
2191 };
2192 if !needs_normalisation {
2193 return create_rows_batch_prevalidated_columnar_with_outputs(
2194 self,
2195 collection,
2196 column_names,
2197 rows,
2198 );
2199 }
2200
2201 let ncols = column_names.len();
2208 let tuple_rows: Vec<CreateRowInput> = rows
2209 .into_iter()
2210 .map(|values| {
2211 let mut fields: Vec<(String, reddb_types::Value)> = Vec::with_capacity(ncols);
2212 for (name, value) in column_names.iter().zip(values) {
2213 fields.push((name.clone(), value));
2214 }
2215 CreateRowInput {
2216 collection: collection.clone(),
2217 fields,
2218 metadata: Vec::new(),
2219 node_links: Vec::new(),
2220 vector_links: Vec::new(),
2221 }
2222 })
2223 .collect();
2224 self.create_rows_batch(CreateRowsBatchInput {
2225 collection,
2226 rows: tuple_rows,
2227 suppress_events: false,
2228 })
2229 }
2230
2231 fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
2232 self.create_rows_batch_prevalidated_with_outputs(input)
2233 .map(|outputs| outputs.len())
2234 }
2235
2236 fn create_rows_batch_prevalidated_with_outputs(
2237 &self,
2238 input: CreateRowsBatchInput,
2239 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2240 if input.rows.is_empty() {
2241 return Ok(Vec::new());
2242 }
2243 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2244 self.check_batch_size(input.rows.len())?;
2245 self.check_db_size()?;
2246
2247 let db = self.db();
2248 let collection = input.collection;
2249 let suppress_events = input.suppress_events;
2250 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2255 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2256
2257 let default_ttl_plan = contract.default_ttl_plan();
2263
2264 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
2265 Vec::with_capacity(input.rows.len());
2266 let mut mutation_rows = mutation_rows;
2267 for row in input.rows {
2268 if row.collection != collection {
2269 return Err(crate::RedDBError::Query(format!(
2270 "batch row collection mismatch: expected '{}', got '{}'",
2271 collection, row.collection
2272 )));
2273 }
2274 let mut metadata = row.metadata;
2275 default_ttl_plan.apply(&mut metadata);
2276 mutation_rows.push(crate::runtime::mutation::MutationRow {
2277 fields: row.fields,
2278 metadata,
2279 node_links: row.node_links,
2280 vector_links: row.vector_links,
2281 });
2282 }
2283
2284 let engine = {
2285 let engine = self.mutation_engine();
2286 if suppress_events {
2287 engine.with_suppress_events()
2288 } else {
2289 engine
2290 }
2291 };
2292 let result = engine.apply(collection.clone(), mutation_rows)?;
2293 let store = db.store();
2294 Ok(result
2295 .ids
2296 .into_iter()
2297 .map(|id| CreateEntityOutput {
2298 id,
2299 entity: store.get(&collection, id),
2300 })
2301 .collect())
2302 }
2303
2304 fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
2305 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2306 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2307 input.properties.iter().map(|(key, _)| key.as_str()),
2308 &format!("node '{}'", input.collection),
2309 )?;
2310 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2311 self.create_node_unchecked(input)
2312 }
2313
2314 fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
2315 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2316 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2317 input.properties.iter().map(|(key, _)| key.as_str()),
2318 &format!("edge '{}'", input.collection),
2319 )?;
2320 ensure_non_tree_structural_edge_label(&input.label)?;
2321 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2322 self.create_edge_unchecked(input)
2323 }
2324
2325 fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
2326 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2327 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2328 let mut vector_data = crate::storage::VectorData::new(input.dense.clone());
2329 vector_data.content = input.content.clone();
2330 let candidate = crate::storage::UnifiedEntity::new(
2331 crate::storage::EntityId::new(0),
2332 crate::storage::EntityKind::Vector {
2333 collection: input.collection.clone(),
2334 },
2335 crate::storage::EntityData::Vector(vector_data),
2336 );
2337 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2338 let db = self.db();
2339 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2340 contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
2341 contract.ensure_vector_dimension(input.dense.len())?;
2342 let mut metadata = input.metadata;
2343 contract.apply_default_ttl(&mut metadata);
2344 let mut builder = db.vector(&input.collection).dense(input.dense);
2345
2346 if let Some(content) = input.content {
2347 builder = builder.content(content);
2348 }
2349
2350 for (key, value) in metadata {
2351 builder = builder.metadata(key, value);
2352 }
2353
2354 if let Some(link_row) = input.link_row {
2355 builder = builder.link_to_table(link_row);
2356 }
2357
2358 if let Some(link_node) = input.link_node {
2359 builder = builder.link_to_node(link_node);
2360 }
2361
2362 let id = builder.save_with_xmin(self.current_xid())?;
2365 let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
2366 Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
2367 _ => None,
2368 };
2369 refresh_context_index(&db, &input.collection, id)?;
2370 if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
2376 state.ensure_populated(&db.store(), &input.collection)?;
2380 use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
2382 fire(InjectionPoint::BeforeWalFsync);
2383 let _ = db
2384 .store()
2385 .append_vector_insert_record(&input.collection, id.raw(), &dense);
2386 fire(InjectionPoint::BeforeIndexCommit);
2387 let mut index = state.index.lock();
2388 index.insert(id, dense.clone());
2389 fire(InjectionPoint::BeforeExtentFsync);
2396 if let Some(extent) = state.extent.lock().as_mut() {
2397 let packed = index.encode_for_extent(&dense);
2398 let _ = extent.append(&packed);
2399 }
2400 }
2401 self.cdc_emit(
2402 crate::replication::cdc::ChangeOperation::Insert,
2403 &input.collection,
2404 id.raw(),
2405 "vector",
2406 );
2407 Ok(CreateEntityOutput {
2408 id,
2409 entity: db.store().get(&input.collection, id),
2410 })
2411 }
2412
2413 fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
2414 let _indexed_mutation = self.index_store_ref().begin_indexed_mutation();
2415 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2416 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2417 let db = self.db();
2418 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2419 contract.ensure_model(crate::catalog::CollectionModel::Document)?;
2420
2421 if let JsonValue::Object(ref map) = input.body {
2422 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2423 map.keys().map(String::as_str),
2424 &format!("document '{}'", input.collection),
2425 )?;
2426 }
2427
2428 let mut fields: Vec<(String, reddb_types::Value)> = vec![(
2432 "body".to_string(),
2433 reddb_types::Value::Json(
2434 crate::json::to_vec(&input.body)
2435 .map_err(|err| crate::RedDBError::Query(err.to_string()))?,
2436 ),
2437 )];
2438 crate::runtime::mutation::enforce_tenant_fields(self, &input.collection, &mut fields)?;
2439 let (field_names, field_values) =
2440 crate::runtime::impl_dml_support::pairwise_columns_values(&fields);
2441 let scoped_body =
2442 crate::runtime::impl_dml_support::find_document_body_json(&field_names, &field_values)?;
2443 let body_bytes = crate::document_body::serialize_document_body(
2444 &scoped_body,
2445 self.binary_document_body_enabled(),
2446 )?;
2447 if let Some((_, body)) = fields.iter_mut().find(|(name, _)| name == "body") {
2448 *body = reddb_types::Value::Json(body_bytes);
2449 }
2450 let mut row = crate::storage::RowData::new(Vec::new());
2451 row.named = Some(fields.iter().cloned().collect());
2452 let candidate = crate::storage::UnifiedEntity::new(
2453 crate::storage::EntityId::new(0),
2454 crate::storage::EntityKind::TableRow {
2455 table: std::sync::Arc::from(input.collection.as_str()),
2456 row_id: 0,
2457 },
2458 crate::storage::EntityData::Row(row),
2459 );
2460 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2461
2462 let mut metadata = input.metadata;
2463 contract.apply_default_ttl(&mut metadata);
2464 let columns: Vec<(&str, reddb_types::Value)> = fields
2465 .iter()
2466 .map(|(key, value)| (key.as_str(), value.clone()))
2467 .collect();
2468 let mut builder = db.row(&input.collection, columns);
2469
2470 for (key, value) in metadata {
2471 builder = builder.metadata(key, value);
2472 }
2473
2474 for node in input.node_links {
2475 builder = builder.link_to_node(node);
2476 }
2477
2478 for vector in input.vector_links {
2479 builder = builder.link_to_vector(vector);
2480 }
2481
2482 let id = builder.save()?;
2483 self.stamp_xmin_if_in_txn(&input.collection, id);
2485 self.index_store_ref()
2486 .index_entity_insert(&input.collection, id, &fields)
2487 .map_err(crate::RedDBError::Internal)?;
2488 refresh_context_index(&db, &input.collection, id)?;
2489 self.cdc_emit(
2490 crate::replication::cdc::ChangeOperation::Insert,
2491 &input.collection,
2492 id.raw(),
2493 "document",
2494 );
2495 Ok(CreateEntityOutput {
2496 id,
2497 entity: db.store().get(&input.collection, id),
2498 })
2499 }
2500
2501 fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
2502 crate::runtime::kv_write::create(self, input, None)
2503 }
2504
2505 fn create_timeseries_point(
2506 &self,
2507 input: CreateTimeSeriesPointInput,
2508 ) -> RedDBResult<CreateEntityOutput> {
2509 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2510 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2511 let db = self.db();
2512 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2513 contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
2514
2515 let mut fields = vec![
2516 ("metric".to_string(), reddb_types::Value::text(input.metric)),
2517 ("value".to_string(), reddb_types::Value::Float(input.value)),
2518 ];
2519
2520 if let Some(timestamp_ns) = input.timestamp_ns {
2521 fields.push((
2522 "timestamp".to_string(),
2523 reddb_types::Value::UnsignedInteger(timestamp_ns),
2524 ));
2525 }
2526
2527 if !input.tags.is_empty() {
2528 let tags_json = JsonValue::Object(
2529 input
2530 .tags
2531 .into_iter()
2532 .map(|(key, value)| (key, JsonValue::String(value)))
2533 .collect(),
2534 );
2535 let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
2536 crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
2537 })?;
2538 fields.push(("tags".to_string(), reddb_types::Value::Json(tags_bytes)));
2539 }
2540
2541 let collection = input.collection;
2542 let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
2543 self.stamp_xmin_if_in_txn(&collection, id);
2546 refresh_context_index(&db, &collection, id)?;
2547
2548 Ok(CreateEntityOutput {
2549 id,
2550 entity: db.store().get(&collection, id),
2551 })
2552 }
2553
2554 fn get_kv(
2555 &self,
2556 collection: &str,
2557 key: &str,
2558 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
2559 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Select, collection)?;
2560 let resolver = crate::runtime::kv_read::KvReadResolver::for_rest_kv(self, collection)?;
2561 let Some(record) = resolver.record(key)? else {
2562 return Ok(None);
2563 };
2564 let visible = self
2565 .db()
2566 .store()
2567 .get(collection, record.id)
2568 .is_some_and(|entity| {
2569 crate::runtime::rls_injection::entity_passes_rls_action(
2570 self,
2571 collection,
2572 reddb_rql::ast::PolicyAction::Select,
2573 &entity,
2574 )
2575 });
2576 Ok(visible.then_some((record.value.unwrap_or(Value::Null), record.id)))
2577 }
2578
2579 fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
2580 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2581 crate::runtime::impl_kv::KvAtomicOps::new(self).delete(
2582 crate::catalog::CollectionModel::Kv,
2583 collection,
2584 key,
2585 )
2586 }
2587
2588 fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
2589 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2590 let _indexed_mutation = self.index_store_ref().begin_indexed_mutation();
2591 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Update, &input.collection)?;
2592 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2593 self,
2594 &input.collection,
2595 );
2596 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2597 let topology_lock = self
2598 .index_store_ref()
2599 .collection_topology_lock(&input.collection);
2600 let topology_guard = topology_lock.read();
2601 let PatchEntityInput {
2602 collection,
2603 id,
2604 payload,
2605 operations,
2606 } = input;
2607 let db = self.db();
2608 let store = db.store();
2609 let Some(manager) = store.get_collection(&collection) else {
2610 return Err(crate::RedDBError::NotFound(format!(
2611 "collection not found: {collection}"
2612 )));
2613 };
2614 let Some(entity) = manager.get(id) else {
2615 return Err(crate::RedDBError::NotFound(format!(
2616 "entity not found: {}",
2617 id.raw()
2618 )));
2619 };
2620 if !crate::runtime::rls_injection::entity_passes_rls_action(
2621 self,
2622 &collection,
2623 reddb_rql::ast::PolicyAction::Update,
2624 &entity,
2625 ) {
2626 return Err(crate::RedDBError::NotFound(format!(
2627 "entity not found: {}",
2628 id.raw()
2629 )));
2630 }
2631 self.apply_loaded_patch_entity(collection, entity, payload, operations, topology_guard)
2632 }
2633
2634 fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
2635 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2636 let _indexed_mutation = self.index_store_ref().begin_indexed_mutation();
2637 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Delete, &input.collection)?;
2638 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2639 self,
2640 &input.collection,
2641 );
2642 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2643 let topology_lock = self
2644 .index_store_ref()
2645 .collection_topology_lock(&input.collection);
2646 let topology_guard = topology_lock.read();
2647 let store = self.db().store();
2648 let Some(entity) = store.get(&input.collection, input.id) else {
2652 return Ok(DeleteEntityOutput {
2653 deleted: false,
2654 id: input.id,
2655 });
2656 };
2657 if !crate::runtime::rls_injection::entity_passes_rls_action(
2658 self,
2659 &input.collection,
2660 reddb_rql::ast::PolicyAction::Delete,
2661 &entity,
2662 ) {
2663 return Ok(DeleteEntityOutput {
2664 deleted: false,
2665 id: input.id,
2666 });
2667 }
2668 let pre_delete_fields = entity_row_fields_snapshot(&entity);
2669 let deleted = store
2673 .delete(&input.collection, input.id)
2674 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2675 if deleted {
2676 #[cfg(test)]
2677 self.index_store_ref().mutation_test_hook(
2678 &input.collection,
2679 crate::runtime::MutationTestPhase::StoragePublished,
2680 );
2681 store.context_index().remove_entity(input.id);
2682 if !pre_delete_fields.is_empty() {
2685 self.index_store_ref()
2686 .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
2687 .map_err(crate::RedDBError::Internal)?;
2688 }
2689 drop(topology_guard);
2690 #[cfg(test)]
2691 self.index_store_ref().mutation_test_hook(
2692 &input.collection,
2693 crate::runtime::MutationTestPhase::BeforeEvents,
2694 );
2695 self.cdc_emit(
2696 crate::replication::cdc::ChangeOperation::Delete,
2697 &input.collection,
2698 input.id.raw(),
2699 "entity",
2700 );
2701 }
2702 Ok(DeleteEntityOutput {
2703 deleted,
2704 id: input.id,
2705 })
2706 }
2707}