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