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
1946 let db = runtime.db();
1947 let contract = CollectionContractWriteEnforcer::new(runtime, &db, &collection);
1948 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
1949
1950 let store = db.store();
1951 let table_arc: Arc<str> = Arc::from(collection.as_str());
1952
1953 let indexed_cols = runtime
1954 .index_store_ref()
1955 .indexed_columns_set(collection.as_str());
1956 let has_secondary_indexes = !indexed_cols.is_empty();
1957 let mut field_snapshots: Vec<Vec<(String, reddb_types::Value)>> = if has_secondary_indexes {
1958 Vec::with_capacity(rows.len())
1959 } else {
1960 Vec::new()
1961 };
1962
1963 let entities: Vec<UnifiedEntity> = rows
1964 .into_iter()
1965 .map(|values| {
1966 if has_secondary_indexes {
1967 let mut snap: Vec<(String, reddb_types::Value)> =
1968 Vec::with_capacity(indexed_cols.len());
1969 for (name, val) in column_names.iter().zip(values.iter()) {
1970 if indexed_cols.contains(name) {
1971 snap.push((name.clone(), val.clone()));
1972 }
1973 }
1974 field_snapshots.push(snap);
1975 }
1976 let mut row = RowData::new(values);
1977 row.schema = Some(Arc::clone(&column_names));
1978 UnifiedEntity::new(
1979 EntityId::new(0),
1980 EntityKind::TableRow {
1981 table: Arc::clone(&table_arc),
1982 row_id: 0,
1983 },
1984 EntityData::Row(row),
1985 )
1986 })
1987 .collect();
1988
1989 let ids = store
1990 .bulk_insert(&collection, entities)
1991 .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
1992
1993 if has_secondary_indexes {
1994 let index_rows: Vec<(EntityId, Vec<(String, reddb_types::Value)>)> = ids
1995 .iter()
1996 .zip(field_snapshots)
1997 .map(|(id, fields)| (*id, fields))
1998 .collect();
1999 runtime
2000 .index_store_ref()
2001 .index_entity_insert_batch(&collection, &index_rows)
2002 .map_err(crate::RedDBError::Internal)?;
2003 }
2004
2005 runtime.invalidate_result_cache();
2006 runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
2007
2008 Ok(ids
2009 .into_iter()
2010 .map(|id| CreateEntityOutput { id, entity: None })
2011 .collect())
2012}
2013
2014impl RuntimeEntityPort for RedDBRuntime {
2015 fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
2016 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2017 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2018 self,
2019 &input.collection,
2020 );
2021 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2022 let db = self.db();
2023 let CreateRowInput {
2024 collection,
2025 fields,
2026 metadata: input_metadata,
2027 node_links,
2028 vector_links,
2029 } = input;
2030 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2031 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2032 let mut metadata = input_metadata;
2033 contract.apply_default_ttl(&mut metadata);
2034 let fields = contract.normalize_insert_fields(fields)?;
2035 contract.enforce_row_uniqueness(&fields, None)?;
2036 let engine = self.mutation_engine();
2038 let result = engine.apply(
2039 collection.clone(),
2040 vec![crate::runtime::mutation::MutationRow {
2041 fields,
2042 metadata,
2043 node_links,
2044 vector_links,
2045 }],
2046 )?;
2047 let id = result.ids[0];
2048 Ok(CreateEntityOutput {
2053 id,
2054 entity: db.store().get(&collection, id),
2055 })
2056 }
2057
2058 fn create_rows_batch(
2059 &self,
2060 input: CreateRowsBatchInput,
2061 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2062 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2063 self,
2064 &input.collection,
2065 );
2066 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2067 if input.rows.is_empty() {
2068 return Ok(Vec::new());
2069 }
2070 self.check_batch_size(input.rows.len())?;
2071 self.check_db_size()?;
2072
2073 let db = self.db();
2074 let collection = input.collection;
2075 let suppress_events = input.suppress_events;
2076 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2077 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2078
2079 let mut prepared_rows = Vec::with_capacity(input.rows.len());
2080 let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
2081 for row in input.rows {
2082 if row.collection != collection {
2083 return Err(crate::RedDBError::Query(format!(
2084 "batch row collection mismatch: expected '{}', got '{}'",
2085 collection, row.collection
2086 )));
2087 }
2088
2089 let mut metadata = row.metadata;
2090 contract.apply_default_ttl(&mut metadata);
2091 let fields = contract.normalize_insert_fields(row.fields)?;
2092 contract.enforce_row_uniqueness(&fields, None)?;
2093 uniqueness_rows.push(fields.clone());
2094 prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
2095 }
2096
2097 contract.enforce_batch_uniqueness(&uniqueness_rows)?;
2098
2099 let engine = {
2102 let e = self.mutation_engine();
2103 if suppress_events {
2104 e.with_suppress_events()
2105 } else {
2106 e
2107 }
2108 };
2109 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
2110 .into_iter()
2111 .map(|(fields, metadata, node_links, vector_links)| {
2112 crate::runtime::mutation::MutationRow {
2113 fields,
2114 metadata,
2115 node_links,
2116 vector_links,
2117 }
2118 })
2119 .collect();
2120
2121 let result = engine.apply(collection.clone(), mutation_rows)?;
2122
2123 let store = db.store();
2124 Ok(result
2125 .ids
2126 .into_iter()
2127 .map(|id| CreateEntityOutput {
2128 id,
2129 entity: store.get(&collection, id),
2130 })
2131 .collect())
2132 }
2133
2134 fn create_rows_batch_prevalidated_columnar(
2135 &self,
2136 collection: String,
2137 column_names: std::sync::Arc<Vec<String>>,
2138 rows: Vec<Vec<reddb_types::Value>>,
2139 ) -> RedDBResult<usize> {
2140 create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
2141 .map(|outputs| outputs.len())
2142 }
2143
2144 fn create_rows_batch_columnar(
2145 &self,
2146 collection: String,
2147 column_names: std::sync::Arc<Vec<String>>,
2148 rows: Vec<Vec<reddb_types::Value>>,
2149 ) -> RedDBResult<usize> {
2150 self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
2151 .map(|outputs| outputs.len())
2152 }
2153
2154 fn create_rows_batch_columnar_with_outputs(
2155 &self,
2156 collection: String,
2157 column_names: std::sync::Arc<Vec<String>>,
2158 rows: Vec<Vec<reddb_types::Value>>,
2159 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2160 if rows.is_empty() {
2161 return Ok(Vec::new());
2162 }
2163 self.check_batch_size(rows.len())?;
2164 self.check_db_size()?;
2165
2166 let db = self.db();
2167 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2168 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2169
2170 let needs_normalisation = self.index_store_ref().has_unique_index(&collection)
2180 || match db.collection_contract(&collection) {
2181 Some(c) => {
2182 c.declared_model == crate::catalog::CollectionModel::Table
2183 && (!c.declared_columns.is_empty()
2184 || c.table_def
2185 .as_ref()
2186 .map(|t| !t.columns.is_empty())
2187 .unwrap_or(false))
2188 }
2189 None => false,
2190 };
2191 if !needs_normalisation {
2192 return create_rows_batch_prevalidated_columnar_with_outputs(
2193 self,
2194 collection,
2195 column_names,
2196 rows,
2197 );
2198 }
2199
2200 let ncols = column_names.len();
2207 let tuple_rows: Vec<CreateRowInput> = rows
2208 .into_iter()
2209 .map(|values| {
2210 let mut fields: Vec<(String, reddb_types::Value)> = Vec::with_capacity(ncols);
2211 for (name, value) in column_names.iter().zip(values) {
2212 fields.push((name.clone(), value));
2213 }
2214 CreateRowInput {
2215 collection: collection.clone(),
2216 fields,
2217 metadata: Vec::new(),
2218 node_links: Vec::new(),
2219 vector_links: Vec::new(),
2220 }
2221 })
2222 .collect();
2223 self.create_rows_batch(CreateRowsBatchInput {
2224 collection,
2225 rows: tuple_rows,
2226 suppress_events: false,
2227 })
2228 }
2229
2230 fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
2231 self.create_rows_batch_prevalidated_with_outputs(input)
2232 .map(|outputs| outputs.len())
2233 }
2234
2235 fn create_rows_batch_prevalidated_with_outputs(
2236 &self,
2237 input: CreateRowsBatchInput,
2238 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2239 if input.rows.is_empty() {
2240 return Ok(Vec::new());
2241 }
2242 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2243 self.check_batch_size(input.rows.len())?;
2244 self.check_db_size()?;
2245
2246 let db = self.db();
2247 let collection = input.collection;
2248 let suppress_events = input.suppress_events;
2249 let contract = CollectionContractWriteEnforcer::new(self, &db, &collection);
2254 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2255
2256 let default_ttl_plan = contract.default_ttl_plan();
2262
2263 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
2264 Vec::with_capacity(input.rows.len());
2265 let mut mutation_rows = mutation_rows;
2266 for row in input.rows {
2267 if row.collection != collection {
2268 return Err(crate::RedDBError::Query(format!(
2269 "batch row collection mismatch: expected '{}', got '{}'",
2270 collection, row.collection
2271 )));
2272 }
2273 let mut metadata = row.metadata;
2274 default_ttl_plan.apply(&mut metadata);
2275 mutation_rows.push(crate::runtime::mutation::MutationRow {
2276 fields: row.fields,
2277 metadata,
2278 node_links: row.node_links,
2279 vector_links: row.vector_links,
2280 });
2281 }
2282
2283 let engine = {
2284 let engine = self.mutation_engine();
2285 if suppress_events {
2286 engine.with_suppress_events()
2287 } else {
2288 engine
2289 }
2290 };
2291 let result = engine.apply(collection.clone(), mutation_rows)?;
2292 let store = db.store();
2293 Ok(result
2294 .ids
2295 .into_iter()
2296 .map(|id| CreateEntityOutput {
2297 id,
2298 entity: store.get(&collection, id),
2299 })
2300 .collect())
2301 }
2302
2303 fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
2304 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2305 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2306 input.properties.iter().map(|(key, _)| key.as_str()),
2307 &format!("node '{}'", input.collection),
2308 )?;
2309 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2310 self.create_node_unchecked(input)
2311 }
2312
2313 fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
2314 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2315 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2316 input.properties.iter().map(|(key, _)| key.as_str()),
2317 &format!("edge '{}'", input.collection),
2318 )?;
2319 ensure_non_tree_structural_edge_label(&input.label)?;
2320 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
2321 self.create_edge_unchecked(input)
2322 }
2323
2324 fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
2325 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2326 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2327 let mut vector_data = crate::storage::VectorData::new(input.dense.clone());
2328 vector_data.content = input.content.clone();
2329 let candidate = crate::storage::UnifiedEntity::new(
2330 crate::storage::EntityId::new(0),
2331 crate::storage::EntityKind::Vector {
2332 collection: input.collection.clone(),
2333 },
2334 crate::storage::EntityData::Vector(vector_data),
2335 );
2336 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2337 let db = self.db();
2338 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2339 contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
2340 contract.ensure_vector_dimension(input.dense.len())?;
2341 let mut metadata = input.metadata;
2342 contract.apply_default_ttl(&mut metadata);
2343 let mut builder = db.vector(&input.collection).dense(input.dense);
2344
2345 if let Some(content) = input.content {
2346 builder = builder.content(content);
2347 }
2348
2349 for (key, value) in metadata {
2350 builder = builder.metadata(key, value);
2351 }
2352
2353 if let Some(link_row) = input.link_row {
2354 builder = builder.link_to_table(link_row);
2355 }
2356
2357 if let Some(link_node) = input.link_node {
2358 builder = builder.link_to_node(link_node);
2359 }
2360
2361 let id = builder.save_with_xmin(self.current_xid())?;
2364 let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
2365 Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
2366 _ => None,
2367 };
2368 refresh_context_index(&db, &input.collection, id)?;
2369 if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
2375 state.ensure_populated(&db.store(), &input.collection);
2379 use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
2381 fire(InjectionPoint::BeforeWalFsync);
2382 let _ = db
2383 .store()
2384 .append_vector_insert_record(&input.collection, id.raw(), &dense);
2385 fire(InjectionPoint::BeforeIndexCommit);
2386 let mut index = state.index.lock();
2387 index.insert(id, dense.clone());
2388 fire(InjectionPoint::BeforeExtentFsync);
2395 if let Some(extent) = state.extent.lock().as_mut() {
2396 let packed = index.encode_for_extent(&dense);
2397 let _ = extent.append(&packed);
2398 }
2399 }
2400 self.cdc_emit(
2401 crate::replication::cdc::ChangeOperation::Insert,
2402 &input.collection,
2403 id.raw(),
2404 "vector",
2405 );
2406 Ok(CreateEntityOutput {
2407 id,
2408 entity: db.store().get(&input.collection, id),
2409 })
2410 }
2411
2412 fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
2413 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2414 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2415 let db = self.db();
2416 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2417 contract.ensure_model(crate::catalog::CollectionModel::Document)?;
2418
2419 if let JsonValue::Object(ref map) = input.body {
2420 crate::reserved_fields::ensure_no_reserved_public_item_fields(
2421 map.keys().map(String::as_str),
2422 &format!("document '{}'", input.collection),
2423 )?;
2424 }
2425
2426 let mut fields: Vec<(String, reddb_types::Value)> = vec![(
2430 "body".to_string(),
2431 reddb_types::Value::Json(
2432 crate::json::to_vec(&input.body)
2433 .map_err(|err| crate::RedDBError::Query(err.to_string()))?,
2434 ),
2435 )];
2436 crate::runtime::mutation::enforce_tenant_fields(self, &input.collection, &mut fields)?;
2437 let (field_names, field_values) =
2438 crate::runtime::impl_dml_support::pairwise_columns_values(&fields);
2439 let scoped_body =
2440 crate::runtime::impl_dml_support::find_document_body_json(&field_names, &field_values)?;
2441 let body_bytes = crate::document_body::serialize_document_body(
2442 &scoped_body,
2443 self.binary_document_body_enabled(),
2444 )?;
2445 if let Some((_, body)) = fields.iter_mut().find(|(name, _)| name == "body") {
2446 *body = reddb_types::Value::Json(body_bytes);
2447 }
2448 let mut row = crate::storage::RowData::new(Vec::new());
2449 row.named = Some(fields.iter().cloned().collect());
2450 let candidate = crate::storage::UnifiedEntity::new(
2451 crate::storage::EntityId::new(0),
2452 crate::storage::EntityKind::TableRow {
2453 table: std::sync::Arc::from(input.collection.as_str()),
2454 row_id: 0,
2455 },
2456 crate::storage::EntityData::Row(row),
2457 );
2458 ensure_insert_entity_rls(self, &input.collection, &candidate)?;
2459
2460 let mut metadata = input.metadata;
2461 contract.apply_default_ttl(&mut metadata);
2462 let columns: Vec<(&str, reddb_types::Value)> = fields
2463 .iter()
2464 .map(|(key, value)| (key.as_str(), value.clone()))
2465 .collect();
2466 let mut builder = db.row(&input.collection, columns);
2467
2468 for (key, value) in metadata {
2469 builder = builder.metadata(key, value);
2470 }
2471
2472 for node in input.node_links {
2473 builder = builder.link_to_node(node);
2474 }
2475
2476 for vector in input.vector_links {
2477 builder = builder.link_to_vector(vector);
2478 }
2479
2480 let id = builder.save()?;
2481 self.stamp_xmin_if_in_txn(&input.collection, id);
2483 self.index_store_ref()
2484 .index_entity_insert(&input.collection, id, &fields)
2485 .map_err(crate::RedDBError::Internal)?;
2486 refresh_context_index(&db, &input.collection, id)?;
2487 self.cdc_emit(
2488 crate::replication::cdc::ChangeOperation::Insert,
2489 &input.collection,
2490 id.raw(),
2491 "document",
2492 );
2493 Ok(CreateEntityOutput {
2494 id,
2495 entity: db.store().get(&input.collection, id),
2496 })
2497 }
2498
2499 fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
2500 crate::runtime::kv_write::create(self, input, None)
2501 }
2502
2503 fn create_timeseries_point(
2504 &self,
2505 input: CreateTimeSeriesPointInput,
2506 ) -> RedDBResult<CreateEntityOutput> {
2507 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2508 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Insert, &input.collection)?;
2509 let db = self.db();
2510 let contract = CollectionContractWriteEnforcer::new(self, &db, &input.collection);
2511 contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
2512
2513 let mut fields = vec![
2514 ("metric".to_string(), reddb_types::Value::text(input.metric)),
2515 ("value".to_string(), reddb_types::Value::Float(input.value)),
2516 ];
2517
2518 if let Some(timestamp_ns) = input.timestamp_ns {
2519 fields.push((
2520 "timestamp".to_string(),
2521 reddb_types::Value::UnsignedInteger(timestamp_ns),
2522 ));
2523 }
2524
2525 if !input.tags.is_empty() {
2526 let tags_json = JsonValue::Object(
2527 input
2528 .tags
2529 .into_iter()
2530 .map(|(key, value)| (key, JsonValue::String(value)))
2531 .collect(),
2532 );
2533 let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
2534 crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
2535 })?;
2536 fields.push(("tags".to_string(), reddb_types::Value::Json(tags_bytes)));
2537 }
2538
2539 let collection = input.collection;
2540 let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
2541 self.stamp_xmin_if_in_txn(&collection, id);
2544 refresh_context_index(&db, &collection, id)?;
2545
2546 Ok(CreateEntityOutput {
2547 id,
2548 entity: db.store().get(&collection, id),
2549 })
2550 }
2551
2552 fn get_kv(
2553 &self,
2554 collection: &str,
2555 key: &str,
2556 ) -> RedDBResult<Option<(reddb_types::Value, crate::storage::EntityId)>> {
2557 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Select, collection)?;
2558 let resolver = crate::runtime::kv_read::KvReadResolver::for_rest_kv(self, collection)?;
2559 let Some(record) = resolver.record(key)? else {
2560 return Ok(None);
2561 };
2562 let visible = self
2563 .db()
2564 .store()
2565 .get(collection, record.id)
2566 .is_some_and(|entity| {
2567 crate::runtime::rls_injection::entity_passes_rls_action(
2568 self,
2569 collection,
2570 reddb_rql::ast::PolicyAction::Select,
2571 &entity,
2572 )
2573 });
2574 Ok(visible.then_some((record.value.unwrap_or(Value::Null), record.id)))
2575 }
2576
2577 fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
2578 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2579 crate::runtime::impl_kv::KvAtomicOps::new(self).delete(
2580 crate::catalog::CollectionModel::Kv,
2581 collection,
2582 key,
2583 )
2584 }
2585
2586 fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
2587 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2588 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Update, &input.collection)?;
2589 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2590 self,
2591 &input.collection,
2592 );
2593 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2594 let topology_lock = self
2595 .index_store_ref()
2596 .collection_topology_lock(&input.collection);
2597 let topology_guard = topology_lock.read();
2598 let PatchEntityInput {
2599 collection,
2600 id,
2601 payload,
2602 operations,
2603 } = input;
2604 let db = self.db();
2605 let store = db.store();
2606 let Some(manager) = store.get_collection(&collection) else {
2607 return Err(crate::RedDBError::NotFound(format!(
2608 "collection not found: {collection}"
2609 )));
2610 };
2611 let Some(entity) = manager.get(id) else {
2612 return Err(crate::RedDBError::NotFound(format!(
2613 "entity not found: {}",
2614 id.raw()
2615 )));
2616 };
2617 if !crate::runtime::rls_injection::entity_passes_rls_action(
2618 self,
2619 &collection,
2620 reddb_rql::ast::PolicyAction::Update,
2621 &entity,
2622 ) {
2623 return Err(crate::RedDBError::NotFound(format!(
2624 "entity not found: {}",
2625 id.raw()
2626 )));
2627 }
2628 self.apply_loaded_patch_entity(collection, entity, payload, operations, topology_guard)
2629 }
2630
2631 fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
2632 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2633 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Delete, &input.collection)?;
2634 let constraint_lock = crate::application::collection_contract_enforcer::row_constraint_lock(
2635 self,
2636 &input.collection,
2637 );
2638 let _constraint_guard = constraint_lock.as_ref().map(|lock| lock.lock());
2639 let topology_lock = self
2640 .index_store_ref()
2641 .collection_topology_lock(&input.collection);
2642 let topology_guard = topology_lock.read();
2643 let store = self.db().store();
2644 let Some(entity) = store.get(&input.collection, input.id) else {
2648 return Ok(DeleteEntityOutput {
2649 deleted: false,
2650 id: input.id,
2651 });
2652 };
2653 if !crate::runtime::rls_injection::entity_passes_rls_action(
2654 self,
2655 &input.collection,
2656 reddb_rql::ast::PolicyAction::Delete,
2657 &entity,
2658 ) {
2659 return Ok(DeleteEntityOutput {
2660 deleted: false,
2661 id: input.id,
2662 });
2663 }
2664 let pre_delete_fields = entity_row_fields_snapshot(&entity);
2665 let deleted = store
2669 .delete(&input.collection, input.id)
2670 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2671 if deleted {
2672 #[cfg(test)]
2673 self.index_store_ref().mutation_test_hook(
2674 &input.collection,
2675 crate::runtime::MutationTestPhase::StoragePublished,
2676 );
2677 store.context_index().remove_entity(input.id);
2678 if !pre_delete_fields.is_empty() {
2681 self.index_store_ref()
2682 .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
2683 .map_err(crate::RedDBError::Internal)?;
2684 }
2685 drop(topology_guard);
2686 #[cfg(test)]
2687 self.index_store_ref().mutation_test_hook(
2688 &input.collection,
2689 crate::runtime::MutationTestPhase::BeforeEvents,
2690 );
2691 self.cdc_emit(
2692 crate::replication::cdc::ChangeOperation::Delete,
2693 &input.collection,
2694 input.id.raw(),
2695 "entity",
2696 );
2697 }
2698 Ok(DeleteEntityOutput {
2699 deleted,
2700 id: input.id,
2701 })
2702 }
2703}