1use std::collections::HashMap;
2
3use crate::application::entity::{
4 AppliedEntityMutation, CreateDocumentInput, CreateKvInput, CreateTimeSeriesPointInput,
5 RowUpdateColumnRule, RowUpdateContractPlan,
6};
7use crate::application::ttl_payload::{
8 has_internal_ttl_metadata, normalize_ttl_patch_operations, parse_top_level_ttl_metadata_entries,
9};
10use crate::json::{to_vec as json_to_vec, Value as JsonValue};
11use crate::storage::query::resolve_declared_data_type;
12use crate::storage::schema::{coerce as coerce_schema_value, DataType, Value};
13use crate::storage::unified::MetadataValue;
14
15use super::*;
16
17const TREE_METADATA_PREFIX: &str = "red.tree.";
18const TREE_CHILD_EDGE_LABEL: &str = "TREE_CHILD";
19
20fn apply_collection_default_ttl(
21 db: &crate::storage::unified::devx::RedDB,
22 collection: &str,
23 metadata: &mut Vec<(String, MetadataValue)>,
24) {
25 if has_internal_ttl_metadata(metadata) {
26 return;
27 }
28
29 let Some(default_ttl_ms) = db.collection_default_ttl_ms(collection) else {
30 return;
31 };
32
33 metadata.push((
34 "_ttl_ms".to_string(),
35 if default_ttl_ms <= i64::MAX as u64 {
36 MetadataValue::Int(default_ttl_ms as i64)
37 } else {
38 MetadataValue::Timestamp(default_ttl_ms)
39 },
40 ));
41}
42
43fn refresh_context_index(
44 db: &crate::storage::unified::devx::RedDB,
45 collection: &str,
46 id: crate::storage::EntityId,
47) -> RedDBResult<()> {
48 let store = db.store();
49 let Some(entity) = store.get(collection, id) else {
50 return Ok(());
51 };
52
53 store.context_index().index_entity(collection, &entity);
54 Ok(())
55}
56
57pub(crate) fn entity_row_fields_snapshot(
61 entity: &crate::storage::UnifiedEntity,
62) -> Vec<(String, Value)> {
63 let mut fields = match &entity.data {
64 crate::storage::EntityData::Row(row) => {
65 if let Some(named) = &row.named {
66 named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
67 } else if let Some(schema) = &row.schema {
68 schema
69 .iter()
70 .zip(row.columns.iter())
71 .map(|(name, value)| (name.clone(), value.clone()))
72 .collect()
73 } else {
74 Vec::new()
75 }
76 }
77 crate::storage::EntityData::Node(node) => node
78 .properties
79 .iter()
80 .map(|(k, v)| (k.clone(), v.clone()))
81 .collect(),
82 crate::storage::EntityData::Edge(edge) => edge
83 .properties
84 .iter()
85 .map(|(k, v)| (k.clone(), v.clone()))
86 .collect(),
87 _ => Vec::new(),
88 };
89 expand_binary_document_body_snapshot(&mut fields);
90 fields
91}
92
93fn expand_binary_document_body_snapshot(fields: &mut Vec<(String, Value)>) {
94 let Some(body_idx) = fields.iter().position(|(name, _)| name == "body") else {
95 return;
96 };
97 let Value::Json(bytes) = &fields[body_idx].1 else {
98 return;
99 };
100 let body_fields = crate::document_body::body_fields(bytes);
101 if let Some(body_json) = crate::document_body::decode_container_to_json(bytes) {
102 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
103 fields[body_idx].1 = Value::Json(json_bytes);
104 }
105 }
106 let Some(body_fields) = body_fields else {
107 return;
108 };
109 for (name, value) in body_fields {
110 if fields.iter().all(|(existing, _)| existing != &name) {
111 fields.push((name, value));
112 }
113 }
114}
115
116fn ensure_collection_model_contract(
117 db: &crate::storage::unified::devx::RedDB,
118 collection: &str,
119 requested_model: crate::catalog::CollectionModel,
120) -> RedDBResult<()> {
121 if let Some(contract) = db.collection_contract(collection) {
122 if !contract_enforces_model(&contract) {
123 return Ok(());
124 }
125 if collection_model_allows(contract.declared_model, requested_model) {
126 return Ok(());
127 }
128 if requested_model == crate::catalog::CollectionModel::Vector
135 && contract
136 .ai_policy
137 .as_ref()
138 .is_some_and(|policy| policy.embed.is_some() || policy.vision.is_some())
139 {
140 return Ok(());
141 }
142 return Err(crate::RedDBError::InvalidOperation(format!(
143 "collection '{}' is declared as '{}' and does not allow '{}' writes",
144 collection,
145 collection_model_name(contract.declared_model),
146 collection_model_name(requested_model)
147 )));
148 }
149
150 let now = implicit_contract_unix_ms();
151 db.save_collection_contract(crate::physical::CollectionContract {
152 name: collection.to_string(),
153 declared_model: requested_model,
154 schema_mode: crate::catalog::SchemaMode::Dynamic,
155 origin: crate::physical::ContractOrigin::Implicit,
156 version: 1,
157 created_at_unix_ms: now,
158 updated_at_unix_ms: now,
159 default_ttl_ms: db.collection_default_ttl_ms(collection),
160 vector_dimension: None,
161 vector_metric: None,
162 context_index_fields: Vec::new(),
163 declared_columns: Vec::new(),
164 table_def: matches!(requested_model, crate::catalog::CollectionModel::Table)
165 .then(|| crate::storage::schema::TableDef::new(collection.to_string())),
166 timestamps_enabled: false,
167 context_index_enabled: false,
168 metrics_raw_retention_ms: None,
169 metrics_rollup_policies: Vec::new(),
170 metrics_tenant_identity: None,
171 metrics_namespace: None,
172 append_only: false,
175 subscriptions: Vec::new(),
176 analytics_config: Vec::new(),
177 session_key: None,
178 session_gap_ms: None,
179 retention_duration_ms: None,
180 analytical_storage: None,
181
182 ai_policy: None,
183 })
184 .map(|_| ())
185 .map_err(|err| crate::RedDBError::Internal(err.to_string()))
186}
187
188fn implicit_contract_unix_ms() -> u128 {
189 std::time::SystemTime::now()
190 .duration_since(std::time::UNIX_EPOCH)
191 .unwrap_or_default()
192 .as_millis()
193}
194
195fn collection_model_allows(
196 declared_model: crate::catalog::CollectionModel,
197 requested_model: crate::catalog::CollectionModel,
198) -> bool {
199 declared_model == requested_model || declared_model == crate::catalog::CollectionModel::Mixed
200}
201
202fn ensure_vector_dimension_contract(
203 db: &crate::storage::unified::devx::RedDB,
204 collection: &str,
205 actual_dimension: usize,
206) -> RedDBResult<()> {
207 let Some(expected_dimension) = db
208 .collection_contract(collection)
209 .and_then(|contract| contract.vector_dimension)
210 else {
211 return Ok(());
212 };
213 if expected_dimension == actual_dimension {
214 return Ok(());
215 }
216 Err(crate::RedDBError::Query(format!(
217 "vector dimension mismatch for collection '{collection}': expected {expected_dimension}, got {actual_dimension}"
218 )))
219}
220
221fn collection_model_name(model: crate::catalog::CollectionModel) -> &'static str {
222 match model {
223 crate::catalog::CollectionModel::Table => "table",
224 crate::catalog::CollectionModel::Document => "document",
225 crate::catalog::CollectionModel::Graph => "graph",
226 crate::catalog::CollectionModel::Vector => "vector",
227 crate::catalog::CollectionModel::Hll => "hll",
228 crate::catalog::CollectionModel::Sketch => "sketch",
229 crate::catalog::CollectionModel::Filter => "filter",
230 crate::catalog::CollectionModel::Kv => "kv",
231 crate::catalog::CollectionModel::Config => "config",
232 crate::catalog::CollectionModel::Vault => "vault",
233 crate::catalog::CollectionModel::Mixed => "mixed",
234 crate::catalog::CollectionModel::TimeSeries => "timeseries",
235 crate::catalog::CollectionModel::Queue => "queue",
236 crate::catalog::CollectionModel::Metrics => "metrics",
237 }
238}
239
240#[derive(Clone)]
241struct UniquenessRule {
242 name: String,
243 columns: Vec<String>,
244 primary_key: bool,
245}
246
247#[derive(Copy, Clone, PartialEq, Eq)]
248enum NormalizeMode {
249 Insert,
253 Update,
257}
258
259mod collection_contract_enforcement {
260 use super::*;
261
262 pub(super) struct CollectionContractWriteEnforcer<'a> {
263 db: &'a crate::storage::unified::devx::RedDB,
264 collection: &'a str,
265 }
266
267 impl<'a> CollectionContractWriteEnforcer<'a> {
268 pub(super) fn new(
269 db: &'a crate::storage::unified::devx::RedDB,
270 collection: &'a str,
271 ) -> Self {
272 Self { db, collection }
273 }
274
275 pub(super) fn ensure_model(
276 &self,
277 requested_model: crate::catalog::CollectionModel,
278 ) -> RedDBResult<()> {
279 ensure_collection_model_contract(self.db, self.collection, requested_model)
280 }
281
282 pub(super) fn normalize_insert_fields(
283 &self,
284 fields: Vec<(String, Value)>,
285 ) -> RedDBResult<Vec<(String, Value)>> {
286 normalize_row_fields_for_contract_with_mode(
287 self.db,
288 self.collection,
289 fields,
290 NormalizeMode::Insert,
291 )
292 }
293
294 pub(super) fn normalize_update_fields(
295 &self,
296 fields: Vec<(String, Value)>,
297 ) -> RedDBResult<Vec<(String, Value)>> {
298 normalize_row_fields_for_contract_with_mode(
299 self.db,
300 self.collection,
301 fields,
302 NormalizeMode::Update,
303 )
304 }
305
306 pub(super) fn enforce_row_uniqueness(
307 &self,
308 fields: &[(String, Value)],
309 exclude_id: Option<crate::storage::EntityId>,
310 ) -> RedDBResult<()> {
311 enforce_row_uniqueness(self.db, self.collection, fields, exclude_id)
312 }
313
314 pub(super) fn enforce_batch_uniqueness(
315 &self,
316 rows: &[Vec<(String, Value)>],
317 ) -> RedDBResult<()> {
318 enforce_row_batch_uniqueness(self.db, self.collection, rows)
319 }
320
321 pub(super) fn requires_uniqueness_check(&self, modified_columns: &[String]) -> bool {
322 row_update_requires_uniqueness_check(self.db, self.collection, modified_columns)
323 }
324 }
325}
326
327use collection_contract_enforcement::CollectionContractWriteEnforcer;
328
329fn normalize_row_fields_for_contract_with_mode(
330 db: &crate::storage::unified::devx::RedDB,
331 collection: &str,
332 fields: Vec<(String, Value)>,
333 mode: NormalizeMode,
334) -> RedDBResult<Vec<(String, Value)>> {
335 let Some(contract) = db.collection_contract(collection) else {
336 return Ok(fields);
337 };
338
339 if contract.declared_model != crate::catalog::CollectionModel::Table
340 || (contract.declared_columns.is_empty()
341 && contract
342 .table_def
343 .as_ref()
344 .map(|table| table.columns.is_empty())
345 .unwrap_or(true))
346 {
347 return Ok(fields);
348 }
349
350 let existing_created_at = if contract.timestamps_enabled && mode == NormalizeMode::Update {
360 fields
361 .iter()
362 .find(|(n, _)| n == "created_at")
363 .map(|(_, v)| v.clone())
364 } else {
365 None
366 };
367
368 if contract.timestamps_enabled && mode == NormalizeMode::Insert {
373 for (name, _) in &fields {
374 if name == "created_at" || name == "updated_at" {
375 return Err(crate::RedDBError::Query(format!(
376 "collection '{}' manages '{}' automatically — do not set it in INSERT",
377 collection, name
378 )));
379 }
380 }
381 }
382
383 let mut provided = std::collections::BTreeMap::new();
384 for (name, value) in &fields {
385 provided.insert(name.clone(), value.clone());
386 }
387
388 let resolved_columns = resolved_contract_columns(&contract)?;
389 let declared_names: std::collections::BTreeSet<String> = resolved_columns
390 .iter()
391 .map(|column| column.name.clone())
392 .collect();
393 let unknown_fields: Vec<String> = fields
394 .iter()
395 .filter(|(name, _)| !declared_names.contains(name))
396 .map(|(name, _)| name.clone())
397 .collect();
398 if matches!(contract.schema_mode, crate::catalog::SchemaMode::Strict)
399 && !unknown_fields.is_empty()
400 {
401 return Err(crate::RedDBError::Query(format!(
402 "collection '{}' is strict and does not allow undeclared fields: {}",
403 collection,
404 unknown_fields.join(", ")
405 )));
406 }
407 let mut normalized = Vec::new();
408 let now_ms = current_unix_ms_u64();
409
410 for column in &resolved_columns {
411 match provided.remove(&column.name) {
412 Some(value) => {
413 if contract.timestamps_enabled && mode == NormalizeMode::Update {
418 match column.name.as_str() {
419 "created_at" => {
420 normalized.push((
421 column.name.clone(),
422 existing_created_at
423 .clone()
424 .unwrap_or(Value::UnsignedInteger(now_ms)),
425 ));
426 continue;
427 }
428 "updated_at" => {
429 normalized.push((column.name.clone(), Value::UnsignedInteger(now_ms)));
430 continue;
431 }
432 _ => {}
433 }
434 }
435 normalized.push((
436 column.name.clone(),
437 normalize_contract_value(collection, column, value)?,
438 ));
439 }
440 None => {
441 if contract.timestamps_enabled
445 && (column.name == "created_at" || column.name == "updated_at")
446 {
447 normalized.push((column.name.clone(), Value::UnsignedInteger(now_ms)));
448 continue;
449 }
450 if let Some(default) = &column.default {
451 normalized.push((
452 column.name.clone(),
453 coerce_contract_literal(collection, &column.name, column, default)?,
454 ));
455 } else if column.not_null {
456 return Err(crate::RedDBError::Query(format!(
457 "missing required column '{}' for collection '{}'",
458 column.name, collection
459 )));
460 }
461 }
462 }
463 }
464
465 for (name, value) in fields {
466 if !declared_names.contains(&name) {
467 normalized.push((name, value));
468 }
469 }
470
471 Ok(normalized)
472}
473
474fn current_unix_ms_u64() -> u64 {
475 std::time::SystemTime::now()
476 .duration_since(std::time::UNIX_EPOCH)
477 .map(|d| d.as_millis() as u64)
478 .unwrap_or(0)
479}
480
481fn enforce_row_uniqueness(
482 db: &crate::storage::unified::devx::RedDB,
483 collection: &str,
484 fields: &[(String, Value)],
485 exclude_id: Option<crate::storage::EntityId>,
486) -> RedDBResult<()> {
487 let Some(contract) = db.collection_contract(collection) else {
488 return Ok(());
489 };
490 if contract.declared_model != crate::catalog::CollectionModel::Table
491 && contract.declared_model != crate::catalog::CollectionModel::Mixed
492 {
493 return Ok(());
494 }
495
496 let rules = resolved_uniqueness_rules(&contract);
497 if rules.is_empty() {
498 return Ok(());
499 }
500
501 let store = db.store();
502 let Some(manager) = store.get_collection(collection) else {
503 return Ok(());
504 };
505
506 let input_fields: std::collections::BTreeMap<String, Value> = fields.iter().cloned().collect();
507
508 for rule in &rules {
509 let mut expected_signatures = Vec::new();
510 let mut skip_rule = false;
511
512 for column in &rule.columns {
513 match input_fields.get(column) {
514 Some(Value::Null) | None if rule.primary_key => {
515 return Err(crate::RedDBError::Query(format!(
516 "primary key '{}' in collection '{}' requires non-null column '{}'",
517 rule.name, collection, column
518 )))
519 }
520 Some(Value::Null) | None => {
521 skip_rule = true;
522 break;
523 }
524 Some(value) => {
525 expected_signatures.push((column.clone(), value_signature(value)));
526 }
527 }
528 }
529
530 if skip_rule {
531 continue;
532 }
533
534 for entity in manager.query_all(|_| true) {
535 if exclude_id.map(|id| id == entity.id).unwrap_or(false) {
536 continue;
537 }
538 let Some(existing_fields) = row_fields_from_entity(&entity) else {
539 continue;
540 };
541
542 let duplicate = expected_signatures.iter().all(|(column, expected)| {
543 existing_fields
544 .get(column)
545 .map(|value| value_signature(value) == *expected)
546 .unwrap_or(false)
547 });
548
549 if duplicate {
550 let qualifier = if rule.primary_key {
551 "primary key"
552 } else {
553 "unique constraint"
554 };
555 return Err(crate::RedDBError::Query(format!(
556 "{} '{}' violated on collection '{}' for columns [{}]",
557 qualifier,
558 rule.name,
559 collection,
560 rule.columns.join(", ")
561 )));
562 }
563 }
564 }
565
566 Ok(())
567}
568
569fn enforce_row_batch_uniqueness(
570 db: &crate::storage::unified::devx::RedDB,
571 collection: &str,
572 rows: &[Vec<(String, Value)>],
573) -> RedDBResult<()> {
574 let Some(contract) = db.collection_contract(collection) else {
575 return Ok(());
576 };
577 if contract.declared_model != crate::catalog::CollectionModel::Table
578 && contract.declared_model != crate::catalog::CollectionModel::Mixed
579 {
580 return Ok(());
581 }
582
583 let rules = resolved_uniqueness_rules(&contract);
584 if rules.is_empty() {
585 return Ok(());
586 }
587
588 for rule in &rules {
589 let mut seen = std::collections::HashMap::<String, usize>::new();
590 for (row_index, fields) in rows.iter().enumerate() {
591 let input_fields: std::collections::BTreeMap<String, Value> =
592 fields.iter().cloned().collect();
593 let mut signatures = Vec::new();
594 let mut skip_rule = false;
595
596 for column in &rule.columns {
597 match input_fields.get(column) {
598 Some(Value::Null) | None if rule.primary_key => {
599 return Err(crate::RedDBError::Query(format!(
600 "primary key '{}' in collection '{}' requires non-null column '{}'",
601 rule.name, collection, column
602 )))
603 }
604 Some(Value::Null) | None => {
605 skip_rule = true;
606 break;
607 }
608 Some(value) => signatures.push(format!("{column}={}", value_signature(value))),
609 }
610 }
611
612 if skip_rule {
613 continue;
614 }
615
616 let signature = signatures.join("|");
617 if let Some(previous_index) = seen.insert(signature, row_index) {
618 return Err(crate::RedDBError::Query(format!(
619 "batch insert violates uniqueness rule '{}' in collection '{}' between rows {} and {}",
620 rule.name,
621 collection,
622 previous_index + 1,
623 row_index + 1
624 )));
625 }
626 }
627 }
628
629 Ok(())
630}
631
632fn row_update_requires_uniqueness_check(
633 db: &crate::storage::unified::devx::RedDB,
634 collection: &str,
635 modified_columns: &[String],
636) -> bool {
637 if modified_columns.is_empty() {
638 return false;
639 }
640
641 let Some(contract) = db.collection_contract(collection) else {
642 return false;
643 };
644 if contract.declared_model != crate::catalog::CollectionModel::Table
645 && contract.declared_model != crate::catalog::CollectionModel::Mixed
646 {
647 return false;
648 }
649
650 let rules = resolved_uniqueness_rules(&contract);
651 if rules.is_empty() {
652 return false;
653 }
654
655 rules.iter().any(|rule| {
656 rule.columns.iter().any(|column| {
657 modified_columns
658 .iter()
659 .any(|modified| modified.eq_ignore_ascii_case(column))
660 })
661 })
662}
663
664pub(crate) fn build_row_update_contract_plan(
665 db: &crate::storage::unified::devx::RedDB,
666 collection: &str,
667) -> RedDBResult<Option<RowUpdateContractPlan>> {
668 let Some(contract) = db.collection_contract(collection) else {
669 return Ok(None);
670 };
671
672 let declared_rules = if contract.declared_model == crate::catalog::CollectionModel::Table
673 && !(contract.declared_columns.is_empty()
674 && contract
675 .table_def
676 .as_ref()
677 .map(|table| table.columns.is_empty())
678 .unwrap_or(true))
679 {
680 resolved_contract_columns(&contract)?
681 .into_iter()
682 .map(|rule| {
683 (
684 rule.name.clone(),
685 RowUpdateColumnRule {
686 name: rule.name,
687 data_type: rule.data_type,
688 data_type_name: rule.data_type_name,
689 not_null: rule.not_null,
690 enum_variants: rule.enum_variants,
691 },
692 )
693 })
694 .collect()
695 } else {
696 HashMap::new()
697 };
698
699 let unique_columns = if matches!(
700 contract.declared_model,
701 crate::catalog::CollectionModel::Table | crate::catalog::CollectionModel::Mixed
702 ) {
703 resolved_uniqueness_rules(&contract)
704 .into_iter()
705 .flat_map(|rule| rule.columns.into_iter())
706 .map(|column| (column, ()))
707 .collect()
708 } else {
709 HashMap::new()
710 };
711
712 Ok(Some(RowUpdateContractPlan {
713 timestamps_enabled: contract.timestamps_enabled,
714 strict_schema: matches!(contract.schema_mode, crate::catalog::SchemaMode::Strict),
715 declared_rules,
716 unique_columns,
717 }))
718}
719
720pub(crate) fn normalize_row_update_assignment_with_plan(
721 collection: &str,
722 column: &str,
723 value: Value,
724 row_contract_plan: Option<&RowUpdateContractPlan>,
725) -> RedDBResult<Value> {
726 let Some(plan) = row_contract_plan else {
727 return Ok(value);
728 };
729
730 if plan.timestamps_enabled && (column == "created_at" || column == "updated_at") {
731 return Err(crate::RedDBError::Query(format!(
732 "collection '{}' manages '{}' automatically — do not set it in UPDATE",
733 collection, column
734 )));
735 }
736
737 if let Some(rule) = plan.declared_rules.get(column) {
738 let rule = ResolvedColumnRule {
739 name: rule.name.clone(),
740 data_type: rule.data_type,
741 data_type_name: rule.data_type_name.clone(),
742 not_null: rule.not_null,
743 default: None,
744 enum_variants: rule.enum_variants.clone(),
745 };
746 normalize_contract_value(collection, &rule, value)
747 } else if plan.strict_schema {
748 Err(crate::RedDBError::Query(format!(
749 "collection '{}' is strict and does not allow undeclared fields: {}",
750 collection, column
751 )))
752 } else {
753 Ok(value)
754 }
755}
756
757pub(crate) fn normalize_row_update_value_for_rule(
758 collection: &str,
759 value: Value,
760 row_rule: Option<&RowUpdateColumnRule>,
761) -> RedDBResult<Value> {
762 let Some(rule) = row_rule else {
763 return Ok(value);
764 };
765
766 let rule = ResolvedColumnRule {
767 name: rule.name.clone(),
768 data_type: rule.data_type,
769 data_type_name: rule.data_type_name.clone(),
770 not_null: rule.not_null,
771 default: None,
772 enum_variants: rule.enum_variants.clone(),
773 };
774 normalize_contract_value(collection, &rule, value)
775}
776
777fn set_row_field(row: &mut crate::storage::unified::entity::RowData, name: &str, value: Value) {
778 if let Some(named) = row.named.as_mut() {
779 named.insert(name.to_string(), value);
780 return;
781 }
782
783 if let Some(schema) = row.schema.as_ref() {
784 if let Some(index) = schema.iter().position(|column| column == name) {
785 if let Some(slot) = row.columns.get_mut(index) {
786 *slot = value;
787 return;
788 }
789 }
790
791 let mut named = HashMap::with_capacity(schema.len().saturating_add(1));
792 for (column, current) in schema.iter().zip(row.columns.iter()) {
793 named.insert(column.clone(), current.clone());
794 }
795 named.insert(name.to_string(), value);
796 row.named = Some(named);
797 return;
798 }
799
800 let mut named = HashMap::with_capacity(1);
801 named.insert(name.to_string(), value);
802 row.named = Some(named);
803}
804
805fn collect_row_fields(row: &crate::storage::unified::entity::RowData) -> Vec<(String, Value)> {
806 if let Some(named) = row.named.as_ref() {
807 named
808 .iter()
809 .map(|(key, value)| (key.clone(), value.clone()))
810 .collect()
811 } else if let Some(schema) = row.schema.as_ref() {
812 schema
813 .iter()
814 .cloned()
815 .zip(row.columns.iter().cloned())
816 .collect()
817 } else {
818 Vec::new()
819 }
820}
821
822fn apply_row_field_assignments_raw<I>(
823 row: &mut crate::storage::unified::entity::RowData,
824 field_assignments: I,
825) where
826 I: IntoIterator<Item = (String, Value)>,
827{
828 for (column, value) in field_assignments {
829 set_row_field(row, &column, value);
830 }
831}
832
833fn apply_row_field_assignments_incremental<I>(
834 collection: &str,
835 row: &mut crate::storage::unified::entity::RowData,
836 field_assignments: I,
837 row_contract_plan: Option<&RowUpdateContractPlan>,
838) -> RedDBResult<()>
839where
840 I: IntoIterator<Item = (String, Value)>,
841{
842 for (column, value) in field_assignments {
843 let value = normalize_row_update_assignment_with_plan(
844 collection,
845 &column,
846 value,
847 row_contract_plan,
848 )?;
849
850 set_row_field(row, &column, value);
851 }
852
853 Ok(())
854}
855
856fn resolved_uniqueness_rules(
857 contract: &crate::physical::CollectionContract,
858) -> Vec<UniquenessRule> {
859 let mut rules = Vec::new();
860
861 if let Some(table_def) = &contract.table_def {
862 if !table_def.primary_key.is_empty() {
863 rules.push(UniquenessRule {
864 name: "primary_key".to_string(),
865 columns: table_def.primary_key.clone(),
866 primary_key: true,
867 });
868 }
869
870 for constraint in &table_def.constraints {
871 if matches!(
872 constraint.constraint_type,
873 crate::storage::schema::ConstraintType::PrimaryKey
874 ) && !constraint.columns.is_empty()
875 {
876 rules.push(UniquenessRule {
877 name: constraint.name.clone(),
878 columns: constraint.columns.clone(),
879 primary_key: true,
880 });
881 } else if matches!(
882 constraint.constraint_type,
883 crate::storage::schema::ConstraintType::Unique
884 ) && !constraint.columns.is_empty()
885 {
886 rules.push(UniquenessRule {
887 name: constraint.name.clone(),
888 columns: constraint.columns.clone(),
889 primary_key: false,
890 });
891 }
892 }
893 } else {
894 for column in &contract.declared_columns {
895 if column.primary_key {
896 rules.push(UniquenessRule {
897 name: format!("pk_{}", column.name),
898 columns: vec![column.name.clone()],
899 primary_key: true,
900 });
901 } else if column.unique {
902 rules.push(UniquenessRule {
903 name: format!("uniq_{}", column.name),
904 columns: vec![column.name.clone()],
905 primary_key: false,
906 });
907 }
908 }
909 }
910
911 let mut dedup = std::collections::BTreeSet::new();
912 rules
913 .into_iter()
914 .filter(|rule| dedup.insert((rule.primary_key, rule.columns.clone())))
915 .collect()
916}
917
918fn row_fields_from_entity(
919 entity: &crate::storage::UnifiedEntity,
920) -> Option<std::collections::BTreeMap<String, Value>> {
921 match &entity.data {
922 crate::storage::EntityData::Row(row) => {
923 if let Some(named) = &row.named {
924 Some(
925 named
926 .iter()
927 .map(|(key, value)| (key.clone(), value.clone()))
928 .collect(),
929 )
930 } else {
931 row.schema.as_ref().map(|schema| {
932 schema
933 .iter()
934 .cloned()
935 .zip(row.columns.iter().cloned())
936 .collect()
937 })
938 }
939 }
940 _ => None,
941 }
942}
943
944fn value_signature(value: &Value) -> String {
945 format!("{value:?}")
946}
947
948fn normalize_contract_value(
949 collection: &str,
950 column: &ResolvedColumnRule,
951 value: Value,
952) -> RedDBResult<Value> {
953 if matches!(value, Value::Null) {
954 if column.not_null {
955 return Err(crate::RedDBError::Query(format!(
956 "column '{}' in collection '{}' cannot be null",
957 column.name, collection
958 )));
959 }
960 return Ok(Value::Null);
961 }
962
963 let target = column.data_type;
964 if value_matches_declared_type(&value, target) {
965 return Ok(value);
966 }
967
968 let Some(raw) = value_to_coercion_input(&value) else {
969 return Err(crate::RedDBError::Query(format!(
970 "column '{}' in collection '{}' requires type '{}' but value cannot be coerced",
971 column.name, collection, column.data_type_name
972 )));
973 };
974
975 coerce_contract_literal(collection, &column.name, column, &raw)
976}
977
978fn coerce_contract_literal(
979 collection: &str,
980 column_name: &str,
981 column: &ResolvedColumnRule,
982 raw: &str,
983) -> RedDBResult<Value> {
984 let target = column.data_type;
985 match target {
986 DataType::Blob => Ok(Value::Blob(raw.as_bytes().to_vec())),
987 DataType::Json => Err(crate::RedDBError::Query(format!(
992 "column '{column_name}' in collection '{collection}' requires an inline strict-JSON \
993 literal (e.g. `{{\"k\": \"v\"}}`) or `JSON_PARSE(<expr>)`; a bare string is not \
994 coerced to JSON (ADR 0067)"
995 ))),
996 DataType::Timestamp => raw.parse::<i64>().map(Value::Timestamp).map_err(|err| {
997 crate::RedDBError::Query(format!(
998 "failed to coerce column '{}' in collection '{}' to '{}': {}",
999 column_name, collection, column.data_type_name, err
1000 ))
1001 }),
1002 DataType::Duration => raw.parse::<i64>().map(Value::Duration).map_err(|err| {
1003 crate::RedDBError::Query(format!(
1004 "failed to coerce column '{}' in collection '{}' to '{}': {}",
1005 column_name, collection, column.data_type_name, err
1006 ))
1007 }),
1008 DataType::Vector | DataType::Array => Err(crate::RedDBError::Query(format!(
1009 "column '{}' in collection '{}' requires '{}' and only typed values are accepted for this type",
1010 column_name, collection, column.data_type_name
1011 ))),
1012 _ => coerce_schema_value(raw, target, Some(column.enum_variants.as_slice())).map_err(
1013 |err| {
1014 crate::RedDBError::Query(format!(
1015 "failed to coerce column '{}' in collection '{}' to '{}': {}",
1016 column_name, collection, column.data_type_name, err
1017 ))
1018 },
1019 ),
1020 }
1021}
1022
1023struct ResolvedColumnRule {
1024 name: String,
1025 data_type: DataType,
1026 data_type_name: String,
1027 not_null: bool,
1028 default: Option<String>,
1029 enum_variants: Vec<String>,
1030}
1031
1032fn resolved_contract_columns(
1033 contract: &crate::physical::CollectionContract,
1034) -> RedDBResult<Vec<ResolvedColumnRule>> {
1035 if let Some(table_def) = &contract.table_def {
1036 return Ok(table_def
1037 .columns
1038 .iter()
1039 .map(|column| ResolvedColumnRule {
1040 name: column.name.clone(),
1041 data_type: column.data_type,
1042 data_type_name: data_type_name(column.data_type).to_string(),
1043 not_null: !column.nullable,
1044 default: column
1045 .default
1046 .as_ref()
1047 .map(|bytes| String::from_utf8_lossy(bytes).to_string()),
1048 enum_variants: column.enum_variants.clone(),
1049 })
1050 .collect());
1051 }
1052
1053 contract
1054 .declared_columns
1055 .iter()
1056 .map(|column| {
1057 let data_type = column
1058 .sql_type
1059 .as_ref()
1060 .map(crate::storage::query::resolve_sql_type_name)
1061 .transpose()
1062 .map_err(|err| crate::RedDBError::Query(err.to_string()))?
1063 .unwrap_or(parse_declared_data_type(&column.data_type)?);
1064 Ok(ResolvedColumnRule {
1065 name: column.name.clone(),
1066 data_type,
1067 data_type_name: column.data_type.clone(),
1068 not_null: column.not_null,
1069 default: column.default.clone(),
1070 enum_variants: column.enum_variants.clone(),
1071 })
1072 })
1073 .collect()
1074}
1075
1076fn parse_declared_data_type(value: &str) -> RedDBResult<DataType> {
1077 resolve_declared_data_type(value).map_err(|err| crate::RedDBError::Query(err.to_string()))
1078}
1079
1080fn data_type_name(data_type: DataType) -> &'static str {
1081 match data_type {
1082 DataType::Integer => "integer",
1083 DataType::UnsignedInteger => "unsigned_integer",
1084 DataType::Float => "float",
1085 DataType::Text => "text",
1086 DataType::Blob => "blob",
1087 DataType::Boolean => "boolean",
1088 DataType::Timestamp => "timestamp",
1089 DataType::Duration => "duration",
1090 DataType::IpAddr => "ipaddr",
1091 DataType::MacAddr => "macaddr",
1092 DataType::Vector => "vector",
1093 DataType::Nullable => "nullable",
1094 DataType::Unknown => "unknown",
1095 DataType::Json => "json",
1096 DataType::Uuid => "uuid",
1097 DataType::NodeRef => "noderef",
1098 DataType::EdgeRef => "edgeref",
1099 DataType::VectorRef => "vectorref",
1100 DataType::RowRef => "rowref",
1101 DataType::Color => "color",
1102 DataType::Email => "email",
1103 DataType::Url => "url",
1104 DataType::Phone => "phone",
1105 DataType::Semver => "semver",
1106 DataType::Cidr => "cidr",
1107 DataType::Date => "date",
1108 DataType::Time => "time",
1109 DataType::Decimal => "decimal",
1110 DataType::DecimalText => "decimal_text",
1111 DataType::Enum => "enum",
1112 DataType::Array => "array",
1113 DataType::TimestampMs => "timestamp_ms",
1114 DataType::Ipv4 => "ipv4",
1115 DataType::Ipv6 => "ipv6",
1116 DataType::Subnet => "subnet",
1117 DataType::Port => "port",
1118 DataType::Latitude => "latitude",
1119 DataType::Longitude => "longitude",
1120 DataType::GeoPoint => "geopoint",
1121 DataType::Country2 => "country2",
1122 DataType::Country3 => "country3",
1123 DataType::Lang2 => "lang2",
1124 DataType::Lang5 => "lang5",
1125 DataType::Currency => "currency",
1126 DataType::AssetCode => "asset_code",
1127 DataType::Money => "money",
1128 DataType::ColorAlpha => "color_alpha",
1129 DataType::BigInt => "bigint",
1130 DataType::KeyRef => "keyref",
1131 DataType::DocRef => "docref",
1132 DataType::TableRef => "tableref",
1133 DataType::PageRef => "pageref",
1134 DataType::Secret => "secret",
1135 DataType::Password => "password",
1136 DataType::TextZstd => "text",
1137 DataType::BlobZstd => "blob",
1138 }
1139}
1140
1141fn value_matches_declared_type(value: &Value, target: DataType) -> bool {
1142 matches!(
1143 (value, target),
1144 (Value::Null, _)
1145 | (Value::Integer(_), DataType::Integer)
1146 | (Value::UnsignedInteger(_), DataType::UnsignedInteger)
1147 | (Value::Float(_), DataType::Float)
1148 | (Value::Text(_), DataType::Text)
1149 | (Value::Blob(_), DataType::Blob)
1150 | (Value::Boolean(_), DataType::Boolean)
1151 | (Value::Timestamp(_), DataType::Timestamp)
1152 | (Value::Duration(_), DataType::Duration)
1153 | (Value::IpAddr(_), DataType::IpAddr)
1154 | (Value::MacAddr(_), DataType::MacAddr)
1155 | (Value::Vector(_), DataType::Vector)
1156 | (Value::Json(_), DataType::Json)
1157 | (Value::Uuid(_), DataType::Uuid)
1158 | (Value::NodeRef(_), DataType::NodeRef)
1159 | (Value::EdgeRef(_), DataType::EdgeRef)
1160 | (Value::VectorRef(_, _), DataType::VectorRef)
1161 | (Value::RowRef(_, _), DataType::RowRef)
1162 | (Value::Color(_), DataType::Color)
1163 | (Value::Email(_), DataType::Email)
1164 | (Value::Url(_), DataType::Url)
1165 | (Value::Phone(_), DataType::Phone)
1166 | (Value::Semver(_), DataType::Semver)
1167 | (Value::Cidr(_, _), DataType::Cidr)
1168 | (Value::Date(_), DataType::Date)
1169 | (Value::Time(_), DataType::Time)
1170 | (Value::Decimal(_), DataType::Decimal)
1171 | (Value::DecimalText(_), DataType::DecimalText)
1172 | (Value::EnumValue(_), DataType::Enum)
1173 | (Value::Array(_), DataType::Array)
1174 | (Value::TimestampMs(_), DataType::TimestampMs)
1175 | (Value::Ipv4(_), DataType::Ipv4)
1176 | (Value::Ipv6(_), DataType::Ipv6)
1177 | (Value::Subnet(_, _), DataType::Subnet)
1178 | (Value::Port(_), DataType::Port)
1179 | (Value::Latitude(_), DataType::Latitude)
1180 | (Value::Longitude(_), DataType::Longitude)
1181 | (Value::GeoPoint(_, _), DataType::GeoPoint)
1182 | (Value::Country2(_), DataType::Country2)
1183 | (Value::Country3(_), DataType::Country3)
1184 | (Value::Lang2(_), DataType::Lang2)
1185 | (Value::Lang5(_), DataType::Lang5)
1186 | (Value::Currency(_), DataType::Currency)
1187 | (Value::ColorAlpha(_), DataType::ColorAlpha)
1188 | (Value::BigInt(_), DataType::BigInt)
1189 | (Value::KeyRef(_, _), DataType::KeyRef)
1190 | (Value::DocRef(_, _), DataType::DocRef)
1191 | (Value::TableRef(_), DataType::TableRef)
1192 | (Value::PageRef(_), DataType::PageRef)
1193 | (Value::Secret(_), DataType::Secret)
1194 | (Value::Password(_), DataType::Password)
1195 )
1196}
1197
1198fn value_to_coercion_input(value: &Value) -> Option<String> {
1199 match value {
1200 Value::Null => None,
1201 Value::Integer(value) => Some(value.to_string()),
1202 Value::UnsignedInteger(value) => Some(value.to_string()),
1203 Value::Float(value) => Some(value.to_string()),
1204 Value::Text(value) => Some(value.to_string()),
1205 Value::Blob(value) => String::from_utf8(value.clone()).ok(),
1206 Value::Boolean(value) => Some(value.to_string()),
1207 Value::Timestamp(value) => Some(value.to_string()),
1208 Value::Duration(value) => Some(value.to_string()),
1209 Value::IpAddr(value) => Some(value.to_string()),
1210 Value::MacAddr(value) => Some(format!(
1211 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
1212 value[0], value[1], value[2], value[3], value[4], value[5]
1213 )),
1214 Value::Json(value) => Some(String::from_utf8_lossy(value).to_string()),
1215 Value::Email(value) => Some(value.clone()),
1216 Value::Url(value) => Some(value.clone()),
1217 Value::Phone(value) => Some(value.to_string()),
1218 Value::Semver(value) => Some(format!(
1219 "{}.{}.{}",
1220 value / 1_000_000,
1221 (value / 1_000) % 1_000,
1222 value % 1_000
1223 )),
1224 Value::Date(value) => Some(value.to_string()),
1225 Value::Time(value) => Some(value.to_string()),
1226 Value::Decimal(value) => Some(value.to_string()),
1227 Value::DecimalText(value) => Some(value.clone()),
1228 Value::TimestampMs(value) => Some(value.to_string()),
1229 Value::Ipv4(value) => Some(format!(
1230 "{}.{}.{}.{}",
1231 (value >> 24) & 0xFF,
1232 (value >> 16) & 0xFF,
1233 (value >> 8) & 0xFF,
1234 value & 0xFF
1235 )),
1236 Value::Port(value) => Some(value.to_string()),
1237 Value::Latitude(value) => Some((*value as f64 / 1_000_000.0).to_string()),
1238 Value::Longitude(value) => Some((*value as f64 / 1_000_000.0).to_string()),
1239 Value::GeoPoint(lat, lon) => Some(format!(
1240 "{},{}",
1241 *lat as f64 / 1_000_000.0,
1242 *lon as f64 / 1_000_000.0
1243 )),
1244 Value::BigInt(value) => Some(value.to_string()),
1245 Value::TableRef(value) => Some(value.clone()),
1246 Value::PageRef(value) => Some(value.to_string()),
1247 Value::Password(value) => Some(value.clone()),
1248 _ => None,
1249 }
1250}
1251
1252fn dedupe_modified_columns(mut modified_columns: Vec<String>) -> Vec<String> {
1253 if modified_columns.is_empty() {
1254 return modified_columns;
1255 }
1256
1257 let mut unique = Vec::with_capacity(modified_columns.len());
1258 for column in modified_columns.drain(..) {
1259 if !unique
1260 .iter()
1261 .any(|existing: &String| existing.eq_ignore_ascii_case(&column))
1262 {
1263 unique.push(column);
1264 }
1265 }
1266 unique
1267}
1268
1269fn reject_document_array_position_path(path: &[String]) -> RedDBResult<()> {
1270 if path.iter().any(|segment| segment.parse::<usize>().is_ok()) {
1271 return Err(crate::RedDBError::Query(
1272 "array positional document patch paths are unsupported; replace the array or full document body instead"
1273 .to_string(),
1274 ));
1275 }
1276 Ok(())
1277}
1278
1279fn reject_document_patch_path_through_scalar(body: &JsonValue, path: &[String]) -> RedDBResult<()> {
1285 if path.len() < 2 {
1286 return Ok(());
1287 }
1288 let scalar_error = |prefix: &str| {
1289 crate::RedDBError::Query(format!(
1290 "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",
1291 path.join("."),
1292 prefix
1293 ))
1294 };
1295 let mut current = body;
1296 for (idx, segment) in path[..path.len() - 1].iter().enumerate() {
1299 let JsonValue::Object(map) = current else {
1300 return Err(scalar_error(&path[..idx].join(".")));
1301 };
1302 match map.get(segment) {
1303 Some(next) => current = next,
1304 None => return Ok(()),
1305 }
1306 }
1307 if !matches!(current, JsonValue::Object(_)) {
1309 return Err(scalar_error(&path[..path.len() - 1].join(".")));
1310 }
1311 Ok(())
1312}
1313
1314fn apply_document_body_patch_operations(
1320 body: &mut JsonValue,
1321 operations: &[PatchEntityOperation],
1322) -> RedDBResult<()> {
1323 for op in operations {
1324 reject_document_array_position_path(&op.path)?;
1325 if matches!(op.op, PatchEntityOperationType::Set) {
1326 reject_document_patch_path_through_scalar(body, &op.path)?;
1327 }
1328 }
1329 apply_patch_operations_to_json(body, operations).map_err(crate::RedDBError::Query)
1330}
1331
1332fn document_body_from_named(fields: &HashMap<String, Value>) -> RedDBResult<JsonValue> {
1333 match fields.get("body") {
1334 Some(Value::Json(bytes)) => {
1335 if let Some(body) = crate::document_body::decode_container_to_json(bytes) {
1338 Ok(body)
1339 } else {
1340 crate::json::from_slice(bytes).map_err(|err| {
1341 crate::RedDBError::Query(format!("failed to decode document body: {err}"))
1342 })
1343 }
1344 }
1345 Some(_) => Err(crate::RedDBError::Query(
1346 "document body field must contain JSON".to_string(),
1347 )),
1348 None => Ok(JsonValue::Object(Default::default())),
1349 }
1350}
1351
1352fn document_body_from_assignment(value: &Value) -> RedDBResult<JsonValue> {
1353 match value {
1354 Value::Json(bytes) | Value::Blob(bytes) => crate::json::from_slice(bytes)
1355 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1356 Value::Text(text) => crate::json::from_str(text.as_ref())
1357 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1358 Value::Integer(value) => crate::json::from_str(&value.to_string())
1359 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1360 Value::UnsignedInteger(value) => crate::json::from_str(&value.to_string())
1361 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1362 Value::Float(value) => crate::json::from_str(&value.to_string())
1363 .map_err(|err| crate::RedDBError::Query(format!("invalid JSON body: {err}"))),
1364 other => Err(crate::RedDBError::Query(format!(
1365 "column 'body' expected JSON body, got {other:?}"
1366 ))),
1367 }
1368}
1369
1370fn document_body_set_operation(column: &str, value: Value) -> PatchEntityOperation {
1371 PatchEntityOperation {
1372 op: PatchEntityOperationType::Set,
1373 path: column.split('.').map(str::to_string).collect(),
1374 value: Some(crate::presentation::entity_json::storage_value_to_json(
1375 &value,
1376 )),
1377 }
1378}
1379
1380fn replace_document_fields_body(
1381 fields: &mut HashMap<String, Value>,
1382 body: JsonValue,
1383 binary: bool,
1384 modified_columns: &mut Vec<String>,
1385) -> RedDBResult<()> {
1386 modified_columns.push("body".to_string());
1387
1388 let old_keys: Vec<String> = fields
1389 .keys()
1390 .filter(|key| key.as_str() != "body")
1391 .cloned()
1392 .collect();
1393 for key in old_keys {
1394 fields.remove(&key);
1395 modified_columns.push(key);
1396 }
1397
1398 let body_bytes = crate::document_body::serialize_document_body(&body, binary)?;
1399 fields.insert("body".to_string(), Value::Json(body_bytes));
1400
1401 Ok(())
1402}
1403
1404fn ensure_no_reserved_document_body_fields(body: &JsonValue, collection: &str) -> RedDBResult<()> {
1405 if let JsonValue::Object(map) = body {
1406 crate::reserved_fields::ensure_no_reserved_public_item_fields(
1407 map.keys().map(String::as_str),
1408 &format!("document '{collection}'"),
1409 )?;
1410 }
1411 Ok(())
1412}
1413
1414fn replace_document_row_body(
1415 row: &mut crate::storage::unified::entity::RowData,
1416 body: JsonValue,
1417 collection: &str,
1418 binary: bool,
1419 modified_columns: &mut Vec<String>,
1420) -> RedDBResult<()> {
1421 ensure_no_reserved_document_body_fields(&body, collection)?;
1422 let fields = row.named.get_or_insert_with(Default::default);
1423 replace_document_fields_body(fields, body, binary, modified_columns)?;
1424
1425 row.columns.clear();
1429 row.schema = None;
1430 Ok(())
1431}
1432
1433fn document_merge_patch_payload(payload: &JsonValue) -> &JsonValue {
1434 if let JsonValue::Object(object) = payload {
1435 if object.len() == 1 {
1436 if let Some(body) = object.get("body") {
1437 return body;
1438 }
1439 }
1440 }
1441 payload
1442}
1443
1444fn apply_document_json_merge_patch(body: &mut JsonValue, patch: &JsonValue) -> RedDBResult<()> {
1445 if !matches!(patch, JsonValue::Object(_)) {
1446 *body = patch.clone();
1447 return Ok(());
1448 }
1449
1450 if matches!(patch, JsonValue::Object(object) if object.is_empty()) {
1451 if !matches!(body, JsonValue::Object(_)) {
1452 *body = JsonValue::Object(Default::default());
1453 }
1454 return Ok(());
1455 }
1456
1457 let mut operations = Vec::new();
1458 collect_document_json_merge_patch_operations(body, Vec::new(), patch, &mut operations);
1459 apply_patch_operations_to_json(body, &operations).map_err(crate::RedDBError::Query)
1460}
1461
1462fn collect_document_json_merge_patch_operations(
1463 target: &JsonValue,
1464 path: Vec<String>,
1465 patch: &JsonValue,
1466 operations: &mut Vec<PatchEntityOperation>,
1467) {
1468 let JsonValue::Object(patch_object) = patch else {
1469 operations.push(PatchEntityOperation {
1470 op: PatchEntityOperationType::Set,
1471 path,
1472 value: Some(patch.clone()),
1473 });
1474 return;
1475 };
1476
1477 for (key, value) in patch_object {
1478 let mut child_path = path.clone();
1479 child_path.push(key.clone());
1480 match value {
1481 JsonValue::Null => operations.push(PatchEntityOperation {
1482 op: PatchEntityOperationType::Unset,
1483 path: child_path,
1484 value: None,
1485 }),
1486 JsonValue::Object(object) if object.is_empty() => {
1487 let child_target = match target {
1488 JsonValue::Object(target_object) => target_object.get(key),
1489 _ => None,
1490 };
1491 if !matches!(child_target, Some(JsonValue::Object(_))) {
1492 operations.push(PatchEntityOperation {
1493 op: PatchEntityOperationType::Set,
1494 path: child_path,
1495 value: Some(JsonValue::Object(Default::default())),
1496 });
1497 }
1498 }
1499 JsonValue::Object(_) => {
1500 let child_target = match target {
1501 JsonValue::Object(target_object) => target_object.get(key),
1502 _ => None,
1503 }
1504 .unwrap_or(&JsonValue::Null);
1505 collect_document_json_merge_patch_operations(
1506 child_target,
1507 child_path,
1508 value,
1509 operations,
1510 );
1511 }
1512 _ => operations.push(PatchEntityOperation {
1513 op: PatchEntityOperationType::Set,
1514 path: child_path,
1515 value: Some(value.clone()),
1516 }),
1517 }
1518 }
1519}
1520
1521impl RedDBRuntime {
1522 pub(crate) fn apply_loaded_patch_entity_core(
1523 &self,
1524 collection: String,
1525 mut entity: crate::storage::UnifiedEntity,
1526 payload: JsonValue,
1527 operations: Vec<PatchEntityOperation>,
1528 ) -> RedDBResult<AppliedEntityMutation> {
1529 let id = entity.id;
1530 let previous_xmax = entity.xmax;
1531 let operations = normalize_ttl_patch_operations(operations)?;
1532 let pre_mutation_fields = entity_row_fields_snapshot(&entity);
1535
1536 let patch_versions = matches!(
1548 entity.data,
1549 crate::storage::EntityData::Row(_)
1550 | crate::storage::EntityData::Node(_)
1551 | crate::storage::EntityData::Edge(_)
1552 ) && self.vcs_is_versioned(&collection).unwrap_or(false);
1553 let versioned_update_xid = if patch_versions {
1554 match self.current_xid() {
1555 Some(xid) => Some(xid),
1556 None => {
1557 let snapshot_manager = self.snapshot_manager();
1558 let xid = snapshot_manager.begin();
1559 snapshot_manager.commit(xid);
1560 Some(xid)
1561 }
1562 }
1563 } else {
1564 None
1565 };
1566 let mut replaced_entity = versioned_update_xid.map(|xid| {
1567 let mut old = entity.clone();
1568 old.set_xmax(xid);
1569 old
1570 });
1571
1572 let db = self.db();
1573 let store = db.store();
1574 let Some(manager) = store.get_collection(&collection) else {
1575 return Err(crate::RedDBError::NotFound(format!(
1576 "collection not found: {collection}"
1577 )));
1578 };
1579
1580 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
1581 let mut metadata_changed = false;
1582 let mut modified_columns: Vec<String> = Vec::new();
1583 let mut context_index_dirty = false;
1584 let mut graph_node_type: Option<String> = None;
1585 let mut graph_edge_weight: Option<f32> = None;
1586
1587 let row_contract_timestamps = db
1588 .collection_contract(&collection)
1589 .map(|c| c.timestamps_enabled)
1590 .unwrap_or(false);
1591
1592 match &mut entity.data {
1593 crate::storage::EntityData::Row(row) => {
1594 let is_document_collection = db
1595 .collection_contract(&collection)
1596 .map(|contract| {
1597 contract.declared_model == crate::catalog::CollectionModel::Document
1598 })
1599 .unwrap_or(false);
1600 let binary_body = self.binary_document_body_enabled();
1601 let mut field_ops = Vec::new();
1602 let mut metadata_ops = Vec::new();
1603 let mut document_body_ops = Vec::new();
1604 let mut document_body_replace: Option<JsonValue> = None;
1605 let has_patch_operations = !operations.is_empty();
1606
1607 for mut op in operations {
1608 let Some(root) = op.path.first().map(String::as_str) else {
1609 return Err(crate::RedDBError::Query(
1610 "patch path cannot be empty".to_string(),
1611 ));
1612 };
1613
1614 match root {
1615 "body" if is_document_collection => {
1616 if op.path.len() == 1 {
1617 match op.op {
1618 PatchEntityOperationType::Set
1619 | PatchEntityOperationType::Replace => {
1620 let Some(value) = op.value.take() else {
1621 return Err(crate::RedDBError::Query(
1622 "document body replacement requires a value"
1623 .to_string(),
1624 ));
1625 };
1626 document_body_replace = Some(value);
1627 }
1628 PatchEntityOperationType::Unset => {
1629 return Err(crate::RedDBError::Query(
1630 "document body cannot be unset; replace it instead"
1631 .to_string(),
1632 ));
1633 }
1634 }
1635 continue;
1636 }
1637 if op.path.len() < 2 {
1638 return Err(crate::RedDBError::Query(
1639 "document body patch paths require a nested key; use payload.body for full replacement"
1640 .to_string(),
1641 ));
1642 }
1643 op.path.remove(0);
1644 reject_document_array_position_path(&op.path)?;
1645 document_body_ops.push(op);
1646 }
1647 "fields" | "named" => {
1648 if op.path.len() < 2 {
1649 return Err(crate::RedDBError::Query(
1650 "patch path 'fields' requires a nested key".to_string(),
1651 ));
1652 }
1653 if row_contract_timestamps {
1654 let leaf = op.path.get(1).map(String::as_str);
1655 if matches!(leaf, Some("created_at") | Some("updated_at")) {
1656 return Err(crate::RedDBError::Query(format!(
1657 "collection '{}' manages '{}' automatically — do not set it in UPDATE",
1658 collection,
1659 leaf.unwrap_or("")
1660 )));
1661 }
1662 }
1663 op.path.remove(0);
1664 field_ops.push(op);
1665 }
1666 "metadata" => {
1667 if op.path.len() < 2 {
1668 return Err(crate::RedDBError::Query(
1669 "patch path 'metadata' requires a nested key".to_string(),
1670 ));
1671 }
1672 op.path.remove(0);
1673 metadata_ops.push(op);
1674 }
1675 _ => {
1676 return Err(crate::RedDBError::Query(format!(
1677 "unsupported patch target '{root}' for table rows. Use fields/*, metadata/*, or weight"
1678 )));
1679 }
1680 }
1681 }
1682
1683 if document_body_replace.is_some() || !document_body_ops.is_empty() {
1684 context_index_dirty = true;
1685 let mut body = match document_body_replace {
1686 Some(body) => body,
1687 None => document_body_from_named(
1688 row.named.get_or_insert_with(Default::default),
1689 )?,
1690 };
1691 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
1692 replace_document_row_body(
1693 row,
1694 body,
1695 &collection,
1696 binary_body,
1697 &mut modified_columns,
1698 )?;
1699 }
1700
1701 if is_document_collection && !has_patch_operations {
1702 let mut body =
1703 document_body_from_named(row.named.get_or_insert_with(Default::default))?;
1704 let previous_body = body.clone();
1705 apply_document_json_merge_patch(
1706 &mut body,
1707 document_merge_patch_payload(&payload),
1708 )?;
1709 if body != previous_body {
1710 context_index_dirty = true;
1711 replace_document_row_body(
1712 row,
1713 body,
1714 &collection,
1715 binary_body,
1716 &mut modified_columns,
1717 )?;
1718 }
1719 }
1720
1721 if !field_ops.is_empty() {
1722 context_index_dirty = true;
1723 let named = row.named.get_or_insert_with(Default::default);
1724 if is_document_collection {
1725 let mut body = document_body_from_named(named)?;
1729 apply_patch_operations_to_json(&mut body, &field_ops)
1730 .map_err(crate::RedDBError::Query)?;
1731 replace_document_row_body(
1732 row,
1733 body,
1734 &collection,
1735 binary_body,
1736 &mut modified_columns,
1737 )?;
1738 } else {
1739 for op in &field_ops {
1740 if let Some(col) = op.path.first() {
1741 modified_columns.push(col.clone());
1742 }
1743 }
1744 apply_patch_operations_to_storage_map(named, &field_ops)?;
1745 }
1746 }
1747
1748 if let Some(fields) = payload
1749 .get("fields")
1750 .and_then(crate::json::Value::as_object)
1751 {
1752 if row_contract_timestamps {
1753 for key in fields.keys() {
1754 if key == "created_at" || key == "updated_at" {
1755 return Err(crate::RedDBError::Query(format!(
1756 "collection '{}' manages '{}' automatically — do not set it in UPDATE",
1757 collection, key
1758 )));
1759 }
1760 }
1761 }
1762 context_index_dirty = true;
1763 let named = row.named.get_or_insert_with(Default::default);
1764 if is_document_collection {
1765 let mut body = document_body_from_named(named)?;
1768 if let JsonValue::Object(map) = &mut body {
1769 for (key, value) in fields {
1770 map.insert(key.clone(), value.clone());
1771 }
1772 }
1773 replace_document_row_body(
1774 row,
1775 body,
1776 &collection,
1777 binary_body,
1778 &mut modified_columns,
1779 )?;
1780 } else {
1781 for (key, value) in fields {
1782 modified_columns.push(key.clone());
1783 named.insert(key.clone(), json_to_storage_value(value)?);
1784 }
1785 }
1786 }
1787
1788 if !metadata_ops.is_empty() {
1789 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1790 let metadata = patch_metadata.get_or_insert_with(|| {
1791 store.get_metadata(&collection, id).unwrap_or_default()
1792 });
1793 let mut metadata_json = metadata_to_json(metadata);
1794 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1795 .map_err(crate::RedDBError::Query)?;
1796 *metadata = metadata_from_json(&metadata_json)?;
1797 metadata_changed = true;
1798 }
1799
1800 if !modified_columns.is_empty() || row_contract_timestamps {
1801 let contract = CollectionContractWriteEnforcer::new(&db, &collection);
1802 let current_fields = if let Some(named) = row.named.take() {
1803 named.into_iter().collect::<Vec<_>>()
1804 } else if let Some(schema) = row.schema.as_ref() {
1805 schema
1806 .iter()
1807 .cloned()
1808 .zip(row.columns.iter().cloned())
1809 .collect::<Vec<_>>()
1810 } else {
1811 Vec::new()
1812 };
1813 let normalized_fields = contract.normalize_update_fields(current_fields)?;
1814 if row_contract_timestamps {
1815 modified_columns.push("updated_at".to_string());
1816 context_index_dirty = true;
1817 }
1818 if contract.requires_uniqueness_check(&modified_columns) {
1819 contract.enforce_row_uniqueness(&normalized_fields, Some(id))?;
1820 }
1821 row.named = Some(normalized_fields.into_iter().collect());
1822 }
1823 }
1824 crate::storage::EntityData::Node(node) => {
1825 let mut field_ops = Vec::new();
1826 let mut metadata_ops = Vec::new();
1827 let mut node_type_ops = Vec::new();
1828
1829 for mut op in operations {
1830 let Some(root) = op.path.first().map(String::as_str) else {
1831 return Err(crate::RedDBError::Query(
1832 "patch path cannot be empty".to_string(),
1833 ));
1834 };
1835
1836 match root {
1837 "fields" | "properties" => {
1838 if op.path.len() < 2 {
1839 return Err(crate::RedDBError::Query(
1840 "patch path 'fields' requires a nested key".to_string(),
1841 ));
1842 }
1843 op.path.remove(0);
1844 field_ops.push(op);
1845 }
1846 "metadata" => {
1847 if op.path.len() < 2 {
1848 return Err(crate::RedDBError::Query(
1849 "patch path 'metadata' requires a nested key".to_string(),
1850 ));
1851 }
1852 op.path.remove(0);
1853 metadata_ops.push(op);
1854 }
1855 "node_type" => {
1856 if op.path.len() != 1 {
1857 return Err(crate::RedDBError::Query(
1858 "patch path 'node_type' does not allow nested keys".to_string(),
1859 ));
1860 }
1861 op.path.clear();
1862 node_type_ops.push(op);
1863 }
1864 _ => {
1865 return Err(crate::RedDBError::Query(format!(
1866 "unsupported patch target '{root}' for graph nodes. Use fields/*, properties/*, node_type, or metadata/*"
1867 )));
1868 }
1869 }
1870 }
1871
1872 for op in node_type_ops {
1873 context_index_dirty = true;
1874 let value = op.value.ok_or_else(|| {
1875 crate::RedDBError::Query("node_type operations require a value".to_string())
1876 })?;
1877
1878 match op.op {
1879 PatchEntityOperationType::Unset => {
1880 return Err(crate::RedDBError::Query(
1881 "node_type cannot be unset through patch operations".to_string(),
1882 ));
1883 }
1884 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
1885 let Some(node_type) = value.as_str() else {
1886 return Err(crate::RedDBError::Query(
1887 "node_type operation requires a text value".to_string(),
1888 ));
1889 };
1890 graph_node_type = Some(node_type.to_string());
1891 modified_columns.push("node_type".to_string());
1892 }
1893 }
1894 }
1895
1896 if !field_ops.is_empty() {
1897 context_index_dirty = true;
1898 apply_patch_operations_to_storage_map(&mut node.properties, &field_ops)?;
1899 }
1900
1901 if let Some(fields) = payload
1902 .get("fields")
1903 .and_then(crate::json::Value::as_object)
1904 {
1905 context_index_dirty = true;
1906 for (key, value) in fields {
1907 node.properties
1908 .insert(key.clone(), json_to_storage_value(value)?);
1909 }
1910 }
1911
1912 if !metadata_ops.is_empty() {
1913 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
1914 let metadata = patch_metadata.get_or_insert_with(|| {
1915 store.get_metadata(&collection, id).unwrap_or_default()
1916 });
1917 let mut metadata_json = metadata_to_json(metadata);
1918 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
1919 .map_err(crate::RedDBError::Query)?;
1920 *metadata = metadata_from_json(&metadata_json)?;
1921 metadata_changed = true;
1922 }
1923 }
1924 crate::storage::EntityData::Edge(edge) => {
1925 let mut field_ops = Vec::new();
1926 let mut metadata_ops = Vec::new();
1927 let mut weight_ops = Vec::new();
1928
1929 for mut op in operations {
1930 let Some(root) = op.path.first().map(String::as_str) else {
1931 return Err(crate::RedDBError::Query(
1932 "patch path cannot be empty".to_string(),
1933 ));
1934 };
1935
1936 match root {
1937 "fields" | "properties" => {
1938 if op.path.len() < 2 {
1939 return Err(crate::RedDBError::Query(
1940 "patch path 'fields' requires a nested key".to_string(),
1941 ));
1942 }
1943 op.path.remove(0);
1944 field_ops.push(op);
1945 }
1946 "weight" => {
1947 if op.path.len() != 1 {
1948 return Err(crate::RedDBError::Query(
1949 "patch path 'weight' does not allow nested keys".to_string(),
1950 ));
1951 }
1952 op.path.clear();
1953 weight_ops.push(op);
1954 }
1955 "metadata" => {
1956 if op.path.len() < 2 {
1957 return Err(crate::RedDBError::Query(
1958 "patch path 'metadata' requires a nested key".to_string(),
1959 ));
1960 }
1961 op.path.remove(0);
1962 metadata_ops.push(op);
1963 }
1964 _ => {
1965 return Err(crate::RedDBError::Query(format!(
1966 "unsupported patch target '{root}' for graph edges. Use fields/*, weight, metadata/*"
1967 )));
1968 }
1969 }
1970 }
1971
1972 if !field_ops.is_empty() {
1973 context_index_dirty = true;
1974 apply_patch_operations_to_storage_map(&mut edge.properties, &field_ops)?;
1975 }
1976
1977 for op in weight_ops {
1978 context_index_dirty = true;
1979 let value = op.value.ok_or_else(|| {
1980 crate::RedDBError::Query("weight operations require a value".to_string())
1981 })?;
1982
1983 match op.op {
1984 PatchEntityOperationType::Unset => {
1985 return Err(crate::RedDBError::Query(
1986 "weight cannot be unset through patch operations".to_string(),
1987 ));
1988 }
1989 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
1990 let Some(weight) = value.as_f64() else {
1991 return Err(crate::RedDBError::Query(
1992 "weight operation requires a numeric value".to_string(),
1993 ));
1994 };
1995 graph_edge_weight = Some(weight as f32);
1996 edge.weight = weight as f32;
1997 modified_columns.push("weight".to_string());
1998 }
1999 }
2000 }
2001
2002 if let Some(fields) = payload
2003 .get("fields")
2004 .and_then(crate::json::Value::as_object)
2005 {
2006 context_index_dirty = true;
2007 for (key, value) in fields {
2008 edge.properties
2009 .insert(key.clone(), json_to_storage_value(value)?);
2010 }
2011 }
2012
2013 if !metadata_ops.is_empty() {
2014 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
2015 let metadata = patch_metadata.get_or_insert_with(|| {
2016 store.get_metadata(&collection, id).unwrap_or_default()
2017 });
2018 let mut metadata_json = metadata_to_json(metadata);
2019 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
2020 .map_err(crate::RedDBError::Query)?;
2021 *metadata = metadata_from_json(&metadata_json)?;
2022 metadata_changed = true;
2023 }
2024 }
2025 crate::storage::EntityData::Vector(vector) => {
2026 let mut field_ops = Vec::new();
2027 let mut metadata_ops = Vec::new();
2028
2029 for mut op in operations {
2030 let Some(root) = op.path.first().map(String::as_str) else {
2031 return Err(crate::RedDBError::Query(
2032 "patch path cannot be empty".to_string(),
2033 ));
2034 };
2035
2036 match root {
2037 "fields" => {
2038 if op.path.len() < 2 {
2039 return Err(crate::RedDBError::Query(
2040 "patch path 'fields' requires a nested key".to_string(),
2041 ));
2042 }
2043 op.path.remove(0);
2044 let Some(target) = op.path.first().map(String::as_str) else {
2045 return Err(crate::RedDBError::Query(
2046 "patch path requires a target under fields".to_string(),
2047 ));
2048 };
2049 if !matches!(target, "dense" | "content" | "sparse") {
2050 return Err(crate::RedDBError::Query(format!(
2051 "unsupported vector patch target '{target}'"
2052 )));
2053 }
2054 field_ops.push(op);
2055 }
2056 "metadata" => {
2057 if op.path.len() < 2 {
2058 return Err(crate::RedDBError::Query(
2059 "patch path 'metadata' requires a nested key".to_string(),
2060 ));
2061 }
2062 op.path.remove(0);
2063 metadata_ops.push(op);
2064 }
2065 _ => {
2066 return Err(crate::RedDBError::Query(format!(
2067 "unsupported patch target '{root}' for vectors. Use fields/* or metadata/*"
2068 )));
2069 }
2070 }
2071 }
2072
2073 if !field_ops.is_empty() {
2074 context_index_dirty = true;
2075 apply_patch_operations_to_vector_fields(vector, &field_ops)?;
2076 }
2077
2078 if let Some(fields) = payload
2079 .get("fields")
2080 .and_then(crate::json::Value::as_object)
2081 {
2082 context_index_dirty = true;
2083 if let Some(content) =
2084 fields.get("content").and_then(crate::json::Value::as_str)
2085 {
2086 vector.content = Some(content.to_string());
2087 }
2088 if let Some(dense) = fields.get("dense") {
2089 vector.dense = dense
2090 .as_array()
2091 .ok_or_else(|| {
2092 crate::RedDBError::Query(
2093 "field 'dense' must be an array".to_string(),
2094 )
2095 })?
2096 .iter()
2097 .map(|value| {
2098 value.as_f64().map(|value| value as f32).ok_or_else(|| {
2099 crate::RedDBError::Query(
2100 "field 'dense' must contain only numbers".to_string(),
2101 )
2102 })
2103 })
2104 .collect::<Result<Vec<_>, _>>()?;
2105 }
2106 }
2107
2108 if !metadata_ops.is_empty() {
2109 ensure_non_tree_reserved_metadata_patch_paths(&metadata_ops)?;
2110 let metadata = patch_metadata.get_or_insert_with(|| {
2111 store.get_metadata(&collection, id).unwrap_or_default()
2112 });
2113 let mut metadata_json = metadata_to_json(metadata);
2114 apply_patch_operations_to_json(&mut metadata_json, &metadata_ops)
2115 .map_err(crate::RedDBError::Query)?;
2116 *metadata = metadata_from_json(&metadata_json)?;
2117 metadata_changed = true;
2118 }
2119 }
2120 crate::storage::EntityData::TimeSeries(_)
2121 | crate::storage::EntityData::QueueMessage(_) => {
2122 return Err(crate::RedDBError::Query(
2123 "patch operations are not supported for TimeSeries or QueueMessage entities"
2124 .to_string(),
2125 ));
2126 }
2127 }
2128
2129 if let Some(node_type) = graph_node_type {
2130 if let crate::storage::EntityKind::GraphNode(node) = &mut entity.kind {
2131 node.node_type = node_type;
2132 }
2133 }
2134 if let Some(weight) = graph_edge_weight {
2135 if let crate::storage::EntityKind::GraphEdge(edge) = &mut entity.kind {
2136 edge.weight = (weight * 1000.0) as u32;
2137 }
2138 }
2139
2140 if let Some(metadata) = payload
2141 .get("metadata")
2142 .and_then(crate::json::Value::as_object)
2143 {
2144 let patch_metadata = patch_metadata
2145 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
2146 for (key, value) in metadata {
2147 ensure_non_tree_reserved_metadata_key(key)?;
2148 patch_metadata.set(key.clone(), json_to_metadata_value(value)?);
2149 }
2150 metadata_changed = true;
2151 }
2152
2153 for (key, value) in parse_top_level_ttl_metadata_entries(&payload)? {
2154 let patch_metadata = patch_metadata
2155 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default());
2156 if matches!(value, crate::storage::unified::MetadataValue::Null) {
2157 patch_metadata.remove(&key);
2158 } else {
2159 patch_metadata.set(key, value);
2160 }
2161 metadata_changed = true;
2162 }
2163
2164 entity.updated_at = std::time::SystemTime::now()
2165 .duration_since(std::time::UNIX_EPOCH)
2166 .unwrap_or_default()
2167 .as_secs();
2168
2169 if let Some(xid) = versioned_update_xid {
2175 let logical_id = entity.logical_id();
2176 entity.id = store.next_entity_id();
2177 entity.set_logical_id(logical_id);
2178 entity.set_xmin(xid);
2179 entity.set_xmax(0);
2180 if let Some(old) = replaced_entity.as_mut() {
2181 old.set_xmax(xid);
2182 }
2183 }
2184
2185 modified_columns = dedupe_modified_columns(modified_columns);
2186
2187 Ok(AppliedEntityMutation {
2188 id: entity.id,
2189 collection,
2190 entity,
2191 metadata: patch_metadata,
2192 modified_columns,
2193 persist_metadata: metadata_changed,
2194 context_index_dirty,
2195 replaced_entity,
2196 replaced_entity_previous_xmax: previous_xmax,
2197 pre_mutation_fields,
2198 })
2199 }
2200
2201 pub(crate) fn apply_loaded_sql_update_row_core(
2202 &self,
2203 collection: String,
2204 mut entity: crate::storage::UnifiedEntity,
2205 static_field_assignments: &[(String, Value)],
2206 dynamic_field_assignments: Vec<(String, Value)>,
2207 static_metadata_assignments: &[(String, MetadataValue)],
2208 dynamic_metadata_assignments: Vec<(String, MetadataValue)>,
2209 row_contract_plan: Option<&RowUpdateContractPlan>,
2210 row_modified_columns_template: &[String],
2211 row_touches_unique_columns: bool,
2212 ) -> RedDBResult<AppliedEntityMutation> {
2213 let id = entity.id;
2214 let previous_xmax = entity.xmax;
2215 let db = self.db();
2216 let store = db.store();
2217 let Some(_) = store.get_collection(&collection) else {
2218 return Err(crate::RedDBError::NotFound(format!(
2219 "collection not found: {collection}"
2220 )));
2221 };
2222
2223 let versioned_update_xid = match self.current_xid() {
2224 Some(xid) => Some(xid),
2225 None => {
2226 let snapshot_manager = self.snapshot_manager();
2227 let xid = snapshot_manager.begin();
2228 snapshot_manager.commit(xid);
2229 Some(xid)
2230 }
2231 };
2232 let mut replaced_entity = versioned_update_xid.map(|xid| {
2233 let mut old = entity.clone();
2234 old.set_xmax(xid);
2235 old
2236 });
2237
2238 let mut patch_metadata: Option<crate::storage::unified::Metadata> = None;
2239 let row_contract_timestamps = row_contract_plan
2240 .map(|plan| plan.timestamps_enabled)
2241 .unwrap_or(false);
2242 let mut metadata_changed = false;
2243 let mut modified_columns = row_modified_columns_template.to_vec();
2244 let mut context_index_dirty = !modified_columns.is_empty();
2245
2246 let pre_mutation_fields = entity_row_fields_snapshot(&entity);
2250
2251 let crate::storage::EntityData::Row(row) = &mut entity.data else {
2252 return Err(crate::RedDBError::Query(
2253 "SQL row update fast path requires a row entity".to_string(),
2254 ));
2255 };
2256
2257 let _ = row_contract_plan;
2258
2259 let is_document_collection = db
2265 .collection_contract(&collection)
2266 .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Document)
2267 .unwrap_or(false);
2268 let kv_key = if row.get_field("value").is_some() || row.columns.len() >= 2 {
2269 row.get_field("key")
2270 .cloned()
2271 .or_else(|| row.columns.first().cloned())
2272 } else {
2273 None
2274 };
2275 if is_document_collection {
2276 let body_assignment = static_field_assignments
2280 .iter()
2281 .chain(dynamic_field_assignments.iter())
2282 .rev()
2283 .find(|(column, _)| column == "body")
2284 .cloned();
2285
2286 match body_assignment {
2287 Some((_, value)) => {
2288 let body = document_body_from_assignment(&value)?;
2289 replace_document_row_body(
2290 row,
2291 body,
2292 &collection,
2293 self.binary_document_body_enabled(),
2294 &mut modified_columns,
2295 )?;
2296 context_index_dirty = true;
2297 }
2298 None => {
2299 let mut static_row_assignments = Vec::new();
2302 let mut dynamic_row_assignments = Vec::new();
2303 let mut document_body_ops = Vec::new();
2304 for (column, value) in static_field_assignments.iter().cloned() {
2305 if column == "body" {
2306 static_row_assignments.push((column, value));
2307 } else {
2308 document_body_ops.push(document_body_set_operation(&column, value));
2309 }
2310 }
2311 for (column, value) in dynamic_field_assignments {
2312 if column == "body" {
2313 dynamic_row_assignments.push((column, value));
2314 } else {
2315 document_body_ops.push(document_body_set_operation(&column, value));
2316 }
2317 }
2318 apply_row_field_assignments_raw(row, static_row_assignments);
2319 apply_row_field_assignments_raw(row, dynamic_row_assignments);
2320 if !document_body_ops.is_empty() {
2321 let mut body = document_body_from_named(
2322 row.named.get_or_insert_with(Default::default),
2323 )?;
2324 apply_document_body_patch_operations(&mut body, &document_body_ops)?;
2325 replace_document_row_body(
2326 row,
2327 body,
2328 &collection,
2329 self.binary_document_body_enabled(),
2330 &mut modified_columns,
2331 )?;
2332 context_index_dirty = true;
2333 }
2334 }
2335 }
2336 } else {
2337 let mut all_assignments: Vec<(String, Value)> = static_field_assignments.to_vec();
2339 all_assignments.extend(dynamic_field_assignments);
2340 apply_row_field_assignments_raw(row, all_assignments);
2341 if let Some(key) = kv_key {
2342 set_row_field(row, "key", key);
2343 }
2344 }
2345
2346 for (key, value) in static_metadata_assignments
2347 .iter()
2348 .cloned()
2349 .chain(dynamic_metadata_assignments)
2350 {
2351 ensure_non_tree_reserved_metadata_key(&key)?;
2352 patch_metadata
2353 .get_or_insert_with(|| store.get_metadata(&collection, id).unwrap_or_default())
2354 .set(key, value);
2355 metadata_changed = true;
2356 }
2357
2358 if !modified_columns.is_empty() || row_contract_timestamps {
2359 let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2360 if row_contract_timestamps {
2361 context_index_dirty = true;
2362 set_row_field(
2363 row,
2364 "updated_at",
2365 Value::UnsignedInteger(current_unix_ms_u64()),
2366 );
2367 modified_columns.push("updated_at".to_string());
2368 }
2369 if row_touches_unique_columns {
2370 let current_fields = collect_row_fields(row);
2371 contract.enforce_row_uniqueness(¤t_fields, Some(id))?;
2372 }
2373 }
2374
2375 entity.updated_at = std::time::SystemTime::now()
2376 .duration_since(std::time::UNIX_EPOCH)
2377 .unwrap_or_default()
2378 .as_secs();
2379
2380 if let Some(xid) = versioned_update_xid {
2381 let logical_id = entity.logical_id();
2382 entity.id = store.next_entity_id();
2383 entity.set_logical_id(logical_id);
2384 entity.set_xmin(xid);
2385 entity.set_xmax(0);
2386 if let Some(old) = replaced_entity.as_mut() {
2387 old.set_xmax(xid);
2388 }
2389 }
2390
2391 modified_columns = dedupe_modified_columns(modified_columns);
2392
2393 Ok(AppliedEntityMutation {
2394 id: entity.id,
2395 collection,
2396 entity,
2397 metadata: patch_metadata,
2398 modified_columns,
2399 persist_metadata: metadata_changed,
2400 context_index_dirty,
2401 replaced_entity,
2402 replaced_entity_previous_xmax: previous_xmax,
2403 pre_mutation_fields,
2404 })
2405 }
2406
2407 pub(crate) fn persist_applied_entity_mutations(
2408 &self,
2409 applied: &[AppliedEntityMutation],
2410 ) -> RedDBResult<()> {
2411 if applied.is_empty() {
2412 return Ok(());
2413 }
2414
2415 let store = self.db().store();
2416 let collection = &applied[0].collection;
2417 let Some(manager) = store.get_collection(collection) else {
2418 return Err(crate::RedDBError::NotFound(format!(
2419 "collection not found: {collection}"
2420 )));
2421 };
2422
2423 let mut ordinary = Vec::with_capacity(applied.len());
2424 for item in applied {
2425 if let Some(old_version) = item.replaced_entity.as_ref() {
2426 store
2427 .install_versioned_table_row_update(
2428 collection,
2429 old_version.clone(),
2430 item.entity.clone(),
2431 if item.persist_metadata {
2432 item.metadata.as_ref()
2433 } else {
2434 None
2435 },
2436 )
2437 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
2438 if self.current_xid().is_some() {
2439 self.record_pending_versioned_update(
2440 crate::runtime::impl_core::current_connection_id(),
2441 collection,
2442 old_version.id,
2443 item.entity.id,
2444 old_version.xmax,
2445 item.replaced_entity_previous_xmax,
2446 );
2447 }
2448 } else {
2449 ordinary.push(item);
2450 }
2451 }
2452 if ordinary.is_empty() {
2453 return Ok(());
2454 }
2455
2456 manager
2457 .update_hot_batch_with_metadata(ordinary.iter().map(|item| {
2458 (
2459 &item.entity,
2460 item.modified_columns.as_slice(),
2461 if item.persist_metadata {
2462 item.metadata.as_ref()
2463 } else {
2464 None
2465 },
2466 )
2467 }))
2468 .map_err(|err| crate::RedDBError::Query(err.to_string()))?;
2469
2470 let indexed_cols = self
2479 .index_store_ref()
2480 .indexed_columns_set(collection.as_str());
2481 let all_hot = !indexed_cols.is_empty()
2482 && ordinary.iter().all(|item| {
2483 !item.persist_metadata
2484 && !item
2485 .modified_columns
2486 .iter()
2487 .any(|c| indexed_cols.contains(c))
2488 })
2489 || indexed_cols.is_empty() && ordinary.iter().all(|item| !item.persist_metadata);
2490
2491 let entity_refs: Vec<&crate::storage::UnifiedEntity> =
2495 ordinary.iter().map(|item| &item.entity).collect();
2496 let persist_fn = if all_hot {
2497 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager_wal_only
2498 } else {
2499 crate::storage::unified::UnifiedStore::persist_entity_refs_to_pager
2500 };
2501 persist_fn(store.as_ref(), collection, &entity_refs)
2502 .map_err(|err| crate::RedDBError::Internal(err.to_string()))
2503 }
2504
2505 pub(crate) fn flush_applied_entity_mutation(
2506 &self,
2507 applied: &AppliedEntityMutation,
2508 ) -> RedDBResult<()> {
2509 let store = self.db().store();
2510 if applied.context_index_dirty {
2511 store
2512 .context_index()
2513 .index_entity(&applied.collection, &applied.entity);
2514 }
2515 let mut changed_columns: Option<Vec<String>> = None;
2523 if !applied.pre_mutation_fields.is_empty() {
2524 let post = entity_row_fields_snapshot(&applied.entity);
2525 if !post.is_empty() {
2526 let damage = crate::application::entity::row_damage_vector(
2527 &applied.pre_mutation_fields,
2528 &post,
2529 );
2530 if !damage.is_empty() {
2531 changed_columns = Some(
2532 damage
2533 .touched_columns()
2534 .into_iter()
2535 .map(str::to_string)
2536 .collect(),
2537 );
2538 }
2539
2540 let indexed_cols: std::collections::HashSet<String> = self
2552 .index_store_ref()
2553 .indexed_columns_set_with_parents(applied.collection.as_str());
2554 let pre_index_fields = self
2558 .index_store_ref()
2559 .augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
2560 let post_index_fields = self
2561 .index_store_ref()
2562 .augment_body_derived_index_fields(&post, &indexed_cols);
2563 let modified_cols: std::collections::HashSet<String> =
2564 crate::application::entity::row_damage_vector(
2565 &pre_index_fields,
2566 &post_index_fields,
2567 )
2568 .touched_columns()
2569 .into_iter()
2570 .map(str::to_string)
2571 .collect();
2572 if let Some(old_version) = applied.replaced_entity.as_ref() {
2573 let old_index_fields: Vec<(String, Value)> = pre_index_fields
2574 .iter()
2575 .filter(|(col, _)| indexed_cols.contains(col))
2576 .cloned()
2577 .collect();
2578 let new_index_fields: Vec<(String, Value)> = post_index_fields
2579 .iter()
2580 .filter(|(col, _)| indexed_cols.contains(col))
2581 .cloned()
2582 .collect();
2583 if !old_index_fields.is_empty() {
2584 self.index_store_ref()
2585 .index_entity_delete(
2586 &applied.collection,
2587 old_version.id,
2588 &old_index_fields,
2589 )
2590 .map_err(crate::RedDBError::Internal)?;
2591 }
2592 if !new_index_fields.is_empty() {
2593 self.index_store_ref()
2594 .index_entity_insert(
2595 &applied.collection,
2596 applied.entity.id,
2597 &new_index_fields,
2598 )
2599 .map_err(crate::RedDBError::Internal)?;
2600 }
2601 } else {
2602 let decision = crate::storage::engine::hot_update::decide(
2603 &crate::storage::engine::hot_update::HotUpdateInputs {
2604 collection: applied.collection.as_str(),
2605 indexed_columns: &indexed_cols,
2606 modified_columns: &modified_cols,
2607 new_tuple_size: 0,
2611 page_free_space: usize::MAX,
2612 },
2613 );
2614 if !decision.can_hot {
2615 self.index_store_ref()
2616 .index_entity_update(
2617 &applied.collection,
2618 applied.id,
2619 &pre_index_fields,
2620 &post_index_fields,
2621 )
2622 .map_err(crate::RedDBError::Internal)?;
2623 } else {
2624 tracing::debug!(
2628 collection = %reddb_wire::audit_safe_log_field(&applied.collection),
2629 "hot_update fast-path: skipped index_entity_update"
2630 );
2631 }
2632 }
2633 }
2634 }
2635 self.cdc_emit_prebuilt_with_columns(
2636 crate::replication::cdc::ChangeOperation::Update,
2637 &applied.collection,
2638 &applied.entity,
2639 "entity",
2640 applied.metadata.as_ref(),
2641 true,
2642 changed_columns,
2643 );
2644 Ok(())
2645 }
2646
2647 pub(crate) fn apply_loaded_patch_entity(
2648 &self,
2649 collection: String,
2650 entity: crate::storage::UnifiedEntity,
2651 payload: JsonValue,
2652 operations: Vec<PatchEntityOperation>,
2653 ) -> RedDBResult<CreateEntityOutput> {
2654 let applied =
2655 self.apply_loaded_patch_entity_core(collection, entity, payload, operations)?;
2656 self.persist_applied_entity_mutations(std::slice::from_ref(&applied))?;
2657 self.flush_applied_entity_mutation(&applied)?;
2658 Ok(CreateEntityOutput {
2659 id: applied.id,
2660 entity: Some(public_document_entity(applied.entity)),
2661 })
2662 }
2663}
2664
2665fn public_document_entity(
2674 mut entity: crate::storage::UnifiedEntity,
2675) -> crate::storage::UnifiedEntity {
2676 let crate::storage::EntityData::Row(row) = &mut entity.data else {
2677 return entity;
2678 };
2679 let Some(named) = row.named.as_mut() else {
2680 return entity;
2681 };
2682 let Some(Value::Json(bytes)) = named.get("body") else {
2683 return entity;
2684 };
2685 let bytes = bytes.clone();
2686 if let Some(fields) = crate::document_body::body_fields(&bytes) {
2687 for (name, value) in fields {
2688 named.entry(name).or_insert(value);
2689 }
2690 }
2691 if let Some(body_json) = crate::document_body::decode_container_to_json(&bytes) {
2692 if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
2693 named.insert("body".to_string(), Value::Json(json_bytes));
2694 }
2695 }
2696 entity
2697}
2698
2699fn ensure_non_tree_reserved_metadata_patch_paths(
2700 operations: &[PatchEntityOperation],
2701) -> RedDBResult<()> {
2702 for operation in operations {
2703 let Some(key) = operation.path.first().map(String::as_str) else {
2704 continue;
2705 };
2706 ensure_non_tree_reserved_metadata_key(key)?;
2707 }
2708 Ok(())
2709}
2710
2711fn ensure_non_tree_reserved_metadata_key(key: &str) -> RedDBResult<()> {
2712 if key.starts_with(TREE_METADATA_PREFIX) {
2713 return Err(crate::RedDBError::Query(format!(
2714 "metadata key '{}' is reserved for managed trees",
2715 key
2716 )));
2717 }
2718 Ok(())
2719}
2720
2721fn ensure_non_tree_reserved_metadata_entries(
2722 metadata: &[(String, MetadataValue)],
2723) -> RedDBResult<()> {
2724 for (key, _) in metadata {
2725 ensure_non_tree_reserved_metadata_key(key)?;
2726 }
2727 Ok(())
2728}
2729
2730fn ensure_non_tree_structural_edge_label(label: &str) -> RedDBResult<()> {
2731 if label.eq_ignore_ascii_case(TREE_CHILD_EDGE_LABEL) {
2732 return Err(crate::RedDBError::Query(format!(
2733 "edge label '{}' is reserved for managed trees",
2734 TREE_CHILD_EDGE_LABEL
2735 )));
2736 }
2737 Ok(())
2738}
2739
2740impl RedDBRuntime {
2741 pub(crate) fn create_node_unchecked(
2742 &self,
2743 input: CreateNodeInput,
2744 ) -> RedDBResult<CreateEntityOutput> {
2745 let db = self.db();
2746 let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
2747 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
2748 let mut metadata = input.metadata;
2749 apply_collection_default_ttl(&db, &input.collection, &mut metadata);
2750 let mut builder = db.node(&input.collection, &input.label);
2751
2752 if let Some(node_type) = input.node_type {
2753 builder = builder.node_type(node_type);
2754 }
2755
2756 for (key, value) in input.properties {
2757 builder = builder.property(key, value);
2758 }
2759
2760 for (key, value) in metadata {
2761 builder = builder.metadata(key, value);
2762 }
2763
2764 for embedding in input.embeddings {
2765 if let Some(model) = embedding.model {
2766 builder = builder.embedding_with_model(embedding.name, embedding.vector, model);
2767 } else {
2768 builder = builder.embedding(embedding.name, embedding.vector);
2769 }
2770 }
2771
2772 for link in input.table_links {
2773 builder = builder.link_to_table(link.key, link.table);
2774 }
2775
2776 for link in input.node_links {
2777 builder = builder.link_to_weighted(link.target, link.edge_label, link.weight);
2778 }
2779
2780 let id = builder.save()?;
2781 self.stamp_xmin_if_in_txn(&input.collection, id);
2784 refresh_context_index(&db, &input.collection, id)?;
2785 self.cdc_emit(
2786 crate::replication::cdc::ChangeOperation::Insert,
2787 &input.collection,
2788 id.raw(),
2789 "graph_node",
2790 );
2791 Ok(CreateEntityOutput {
2792 id,
2793 entity: db.store().get(&input.collection, id),
2794 })
2795 }
2796
2797 pub(crate) fn create_edge_unchecked(
2798 &self,
2799 input: CreateEdgeInput,
2800 ) -> RedDBResult<CreateEntityOutput> {
2801 let db = self.db();
2802 let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
2803 contract.ensure_model(crate::catalog::CollectionModel::Graph)?;
2804 let mut metadata = input.metadata;
2805 apply_collection_default_ttl(&db, &input.collection, &mut metadata);
2806 let mut builder = db
2807 .edge(&input.collection, &input.label)
2808 .from(input.from)
2809 .to(input.to);
2810
2811 if let Some(weight) = input.weight {
2812 builder = builder.weight(weight);
2813 }
2814
2815 for (key, value) in input.properties {
2816 builder = builder.property(key, value);
2817 }
2818
2819 for (key, value) in metadata {
2820 builder = builder.metadata(key, value);
2821 }
2822
2823 let id = builder.save()?;
2824 self.stamp_xmin_if_in_txn(&input.collection, id);
2827 refresh_context_index(&db, &input.collection, id)?;
2828 self.cdc_emit(
2829 crate::replication::cdc::ChangeOperation::Insert,
2830 &input.collection,
2831 id.raw(),
2832 "graph_edge",
2833 );
2834 Ok(CreateEntityOutput {
2835 id,
2836 entity: db.store().get(&input.collection, id),
2837 })
2838 }
2839}
2840
2841fn create_rows_batch_prevalidated_columnar_with_outputs(
2842 runtime: &RedDBRuntime,
2843 collection: String,
2844 column_names: std::sync::Arc<Vec<String>>,
2845 rows: Vec<Vec<crate::storage::schema::Value>>,
2846) -> RedDBResult<Vec<CreateEntityOutput>> {
2847 use crate::storage::{
2848 unified::{EntityData, EntityKind, RowData},
2849 EntityId, UnifiedEntity,
2850 };
2851 use std::sync::Arc;
2852
2853 if rows.is_empty() {
2854 return Ok(Vec::new());
2855 }
2856 runtime.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2857 runtime.check_batch_size(rows.len())?;
2858 runtime.check_db_size()?;
2859
2860 let db = runtime.db();
2861 let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2862 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2863
2864 let store = db.store();
2865 let table_arc: Arc<str> = Arc::from(collection.as_str());
2866
2867 let indexed_cols = runtime
2868 .index_store_ref()
2869 .indexed_columns_set(collection.as_str());
2870 let has_secondary_indexes = !indexed_cols.is_empty();
2871 let mut field_snapshots: Vec<Vec<(String, crate::storage::schema::Value)>> =
2872 if has_secondary_indexes {
2873 Vec::with_capacity(rows.len())
2874 } else {
2875 Vec::new()
2876 };
2877
2878 let entities: Vec<UnifiedEntity> = rows
2879 .into_iter()
2880 .map(|values| {
2881 if has_secondary_indexes {
2882 let mut snap: Vec<(String, crate::storage::schema::Value)> =
2883 Vec::with_capacity(indexed_cols.len());
2884 for (name, val) in column_names.iter().zip(values.iter()) {
2885 if indexed_cols.contains(name) {
2886 snap.push((name.clone(), val.clone()));
2887 }
2888 }
2889 field_snapshots.push(snap);
2890 }
2891 let mut row = RowData::new(values);
2892 row.schema = Some(Arc::clone(&column_names));
2893 UnifiedEntity::new(
2894 EntityId::new(0),
2895 EntityKind::TableRow {
2896 table: Arc::clone(&table_arc),
2897 row_id: 0,
2898 },
2899 EntityData::Row(row),
2900 )
2901 })
2902 .collect();
2903
2904 let ids = store
2905 .bulk_insert(&collection, entities)
2906 .map_err(|e| crate::RedDBError::Internal(format!("{e:?}")))?;
2907
2908 if has_secondary_indexes {
2909 let index_rows: Vec<(EntityId, Vec<(String, crate::storage::schema::Value)>)> = ids
2910 .iter()
2911 .zip(field_snapshots)
2912 .map(|(id, fields)| (*id, fields))
2913 .collect();
2914 runtime
2915 .index_store_ref()
2916 .index_entity_insert_batch(&collection, &index_rows)
2917 .map_err(crate::RedDBError::Internal)?;
2918 }
2919
2920 runtime.invalidate_result_cache();
2921 runtime.cdc_emit_insert_batch_no_cache_invalidate(&collection, &ids, "table");
2922
2923 Ok(ids
2924 .into_iter()
2925 .map(|id| CreateEntityOutput { id, entity: None })
2926 .collect())
2927}
2928
2929impl RuntimeEntityPort for RedDBRuntime {
2930 fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
2931 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
2932 let db = self.db();
2933 let CreateRowInput {
2934 collection,
2935 fields,
2936 metadata: input_metadata,
2937 node_links,
2938 vector_links,
2939 } = input;
2940 let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2941 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2942 let mut metadata = input_metadata;
2943 apply_collection_default_ttl(&db, &collection, &mut metadata);
2944 let fields = contract.normalize_insert_fields(fields)?;
2945 contract.enforce_row_uniqueness(&fields, None)?;
2946 let engine = self.mutation_engine();
2948 let result = engine.apply(
2949 collection.clone(),
2950 vec![crate::runtime::mutation::MutationRow {
2951 fields,
2952 metadata,
2953 node_links,
2954 vector_links,
2955 }],
2956 )?;
2957 let id = result.ids[0];
2958 Ok(CreateEntityOutput {
2963 id,
2964 entity: db.store().get(&collection, id),
2965 })
2966 }
2967
2968 fn create_rows_batch(
2969 &self,
2970 input: CreateRowsBatchInput,
2971 ) -> RedDBResult<Vec<CreateEntityOutput>> {
2972 if input.rows.is_empty() {
2973 return Ok(Vec::new());
2974 }
2975 self.check_batch_size(input.rows.len())?;
2976 self.check_db_size()?;
2977
2978 let db = self.db();
2979 let collection = input.collection;
2980 let contract = CollectionContractWriteEnforcer::new(&db, &collection);
2981 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
2982
2983 let mut prepared_rows = Vec::with_capacity(input.rows.len());
2984 let mut uniqueness_rows = Vec::with_capacity(input.rows.len());
2985 for row in input.rows {
2986 if row.collection != collection {
2987 return Err(crate::RedDBError::Query(format!(
2988 "batch row collection mismatch: expected '{}', got '{}'",
2989 collection, row.collection
2990 )));
2991 }
2992
2993 let mut metadata = row.metadata;
2994 apply_collection_default_ttl(&db, &collection, &mut metadata);
2995 let fields = contract.normalize_insert_fields(row.fields)?;
2996 contract.enforce_row_uniqueness(&fields, None)?;
2997 uniqueness_rows.push(fields.clone());
2998 prepared_rows.push((fields, metadata, row.node_links, row.vector_links));
2999 }
3000
3001 contract.enforce_batch_uniqueness(&uniqueness_rows)?;
3002
3003 let engine = {
3006 let e = self.mutation_engine();
3007 if input.suppress_events {
3008 e.with_suppress_events()
3009 } else {
3010 e
3011 }
3012 };
3013 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> = prepared_rows
3014 .into_iter()
3015 .map(|(fields, metadata, node_links, vector_links)| {
3016 crate::runtime::mutation::MutationRow {
3017 fields,
3018 metadata,
3019 node_links,
3020 vector_links,
3021 }
3022 })
3023 .collect();
3024
3025 let result = engine
3026 .apply(collection.clone(), mutation_rows)
3027 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
3028
3029 let store = db.store();
3030 Ok(result
3031 .ids
3032 .into_iter()
3033 .map(|id| CreateEntityOutput {
3034 id,
3035 entity: store.get(&collection, id),
3036 })
3037 .collect())
3038 }
3039
3040 fn create_rows_batch_prevalidated_columnar(
3041 &self,
3042 collection: String,
3043 column_names: std::sync::Arc<Vec<String>>,
3044 rows: Vec<Vec<crate::storage::schema::Value>>,
3045 ) -> RedDBResult<usize> {
3046 create_rows_batch_prevalidated_columnar_with_outputs(self, collection, column_names, rows)
3047 .map(|outputs| outputs.len())
3048 }
3049
3050 fn create_rows_batch_columnar(
3051 &self,
3052 collection: String,
3053 column_names: std::sync::Arc<Vec<String>>,
3054 rows: Vec<Vec<crate::storage::schema::Value>>,
3055 ) -> RedDBResult<usize> {
3056 self.create_rows_batch_columnar_with_outputs(collection, column_names, rows)
3057 .map(|outputs| outputs.len())
3058 }
3059
3060 fn create_rows_batch_columnar_with_outputs(
3061 &self,
3062 collection: String,
3063 column_names: std::sync::Arc<Vec<String>>,
3064 rows: Vec<Vec<crate::storage::schema::Value>>,
3065 ) -> RedDBResult<Vec<CreateEntityOutput>> {
3066 if rows.is_empty() {
3067 return Ok(Vec::new());
3068 }
3069 self.check_batch_size(rows.len())?;
3070 self.check_db_size()?;
3071
3072 let db = self.db();
3073 let contract = CollectionContractWriteEnforcer::new(&db, &collection);
3074 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
3075
3076 let needs_normalisation = match db.collection_contract(&collection) {
3086 Some(c) => {
3087 c.declared_model == crate::catalog::CollectionModel::Table
3088 && (!c.declared_columns.is_empty()
3089 || c.table_def
3090 .as_ref()
3091 .map(|t| !t.columns.is_empty())
3092 .unwrap_or(false))
3093 }
3094 None => false,
3095 };
3096 if !needs_normalisation {
3097 return create_rows_batch_prevalidated_columnar_with_outputs(
3098 self,
3099 collection,
3100 column_names,
3101 rows,
3102 );
3103 }
3104
3105 let ncols = column_names.len();
3112 let tuple_rows: Vec<CreateRowInput> = rows
3113 .into_iter()
3114 .map(|values| {
3115 let mut fields: Vec<(String, crate::storage::schema::Value)> =
3116 Vec::with_capacity(ncols);
3117 for (name, value) in column_names.iter().zip(values) {
3118 fields.push((name.clone(), value));
3119 }
3120 CreateRowInput {
3121 collection: collection.clone(),
3122 fields,
3123 metadata: Vec::new(),
3124 node_links: Vec::new(),
3125 vector_links: Vec::new(),
3126 }
3127 })
3128 .collect();
3129 self.create_rows_batch(CreateRowsBatchInput {
3130 collection,
3131 rows: tuple_rows,
3132 suppress_events: false,
3133 })
3134 }
3135
3136 fn create_rows_batch_prevalidated(&self, input: CreateRowsBatchInput) -> RedDBResult<usize> {
3137 if input.rows.is_empty() {
3138 return Ok(0);
3139 }
3140 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3141 self.check_batch_size(input.rows.len())?;
3142 self.check_db_size()?;
3143
3144 let db = self.db();
3145 let collection = input.collection;
3146 let contract = CollectionContractWriteEnforcer::new(&db, &collection);
3151 contract.ensure_model(crate::catalog::CollectionModel::Table)?;
3152
3153 let default_ttl_ms = db.collection_default_ttl_ms(&collection);
3159
3160 let mutation_rows: Vec<crate::runtime::mutation::MutationRow> =
3161 Vec::with_capacity(input.rows.len());
3162 let mut mutation_rows = mutation_rows;
3163 for row in input.rows {
3164 if row.collection != collection {
3165 return Err(crate::RedDBError::Query(format!(
3166 "batch row collection mismatch: expected '{}', got '{}'",
3167 collection, row.collection
3168 )));
3169 }
3170 let mut metadata = row.metadata;
3171 if let Some(ttl) = default_ttl_ms {
3172 if !has_internal_ttl_metadata(&metadata) {
3173 metadata.push((
3174 "_ttl_ms".to_string(),
3175 if ttl <= i64::MAX as u64 {
3176 MetadataValue::Int(ttl as i64)
3177 } else {
3178 MetadataValue::Timestamp(ttl)
3179 },
3180 ));
3181 }
3182 }
3183 mutation_rows.push(crate::runtime::mutation::MutationRow {
3184 fields: row.fields,
3185 metadata,
3186 node_links: row.node_links,
3187 vector_links: row.vector_links,
3188 });
3189 }
3190
3191 let engine = self.mutation_engine();
3192 let result = engine
3193 .apply(collection, mutation_rows)
3194 .map_err(|e| crate::RedDBError::Internal(e.to_string()))?;
3195 Ok(result.ids.len())
3196 }
3197
3198 fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
3199 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3200 crate::reserved_fields::ensure_no_reserved_public_item_fields(
3201 input.properties.iter().map(|(key, _)| key.as_str()),
3202 &format!("node '{}'", input.collection),
3203 )?;
3204 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
3205 self.create_node_unchecked(input)
3206 }
3207
3208 fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
3209 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3210 crate::reserved_fields::ensure_no_reserved_public_item_fields(
3211 input.properties.iter().map(|(key, _)| key.as_str()),
3212 &format!("edge '{}'", input.collection),
3213 )?;
3214 ensure_non_tree_structural_edge_label(&input.label)?;
3215 ensure_non_tree_reserved_metadata_entries(&input.metadata)?;
3216 self.create_edge_unchecked(input)
3217 }
3218
3219 fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
3220 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3221 let db = self.db();
3222 let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3223 contract.ensure_model(crate::catalog::CollectionModel::Vector)?;
3224 ensure_vector_dimension_contract(&db, &input.collection, input.dense.len())?;
3225 let mut metadata = input.metadata;
3226 apply_collection_default_ttl(&db, &input.collection, &mut metadata);
3227 let mut builder = db.vector(&input.collection).dense(input.dense);
3228
3229 if let Some(content) = input.content {
3230 builder = builder.content(content);
3231 }
3232
3233 for (key, value) in metadata {
3234 builder = builder.metadata(key, value);
3235 }
3236
3237 if let Some(link_row) = input.link_row {
3238 builder = builder.link_to_table(link_row);
3239 }
3240
3241 if let Some(link_node) = input.link_node {
3242 builder = builder.link_to_node(link_node);
3243 }
3244
3245 let id = builder.save()?;
3246 let dense_for_turbo = match db.store().get(&input.collection, id).map(|e| e.data) {
3247 Some(crate::storage::unified::EntityData::Vector(data)) => Some(data.dense.clone()),
3248 _ => None,
3249 };
3250 self.stamp_xmin_if_in_txn(&input.collection, id);
3253 refresh_context_index(&db, &input.collection, id)?;
3254 if let (Some(state), Some(dense)) = (db.turbo_state(&input.collection), dense_for_turbo) {
3260 state.ensure_populated(&db.store(), &input.collection);
3264 use crate::runtime::turbo_crash_inject::{fire, InjectionPoint};
3266 fire(InjectionPoint::BeforeWalFsync);
3267 let _ = db
3268 .store()
3269 .append_vector_insert_record(&input.collection, id.raw(), &dense);
3270 fire(InjectionPoint::BeforeIndexCommit);
3271 let mut index = state.index.lock();
3272 index.insert(id, dense.clone());
3273 fire(InjectionPoint::BeforeExtentFsync);
3280 if let Some(extent) = state.extent.lock().as_mut() {
3281 let packed = index.encode_for_extent(&dense);
3282 let _ = extent.append(&packed);
3283 }
3284 }
3285 self.cdc_emit(
3286 crate::replication::cdc::ChangeOperation::Insert,
3287 &input.collection,
3288 id.raw(),
3289 "vector",
3290 );
3291 Ok(CreateEntityOutput {
3292 id,
3293 entity: db.store().get(&input.collection, id),
3294 })
3295 }
3296
3297 fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
3298 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3299 let db = self.db();
3300 let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3301 contract.ensure_model(crate::catalog::CollectionModel::Document)?;
3302
3303 if let JsonValue::Object(ref map) = input.body {
3304 crate::reserved_fields::ensure_no_reserved_public_item_fields(
3305 map.keys().map(String::as_str),
3306 &format!("document '{}'", input.collection),
3307 )?;
3308 }
3309
3310 let binary_body = self.binary_document_body_enabled();
3314 let body_bytes = crate::document_body::serialize_document_body(&input.body, binary_body)?;
3315 let fields: Vec<(String, crate::storage::schema::Value)> = vec![(
3316 "body".to_string(),
3317 crate::storage::schema::Value::Json(body_bytes),
3318 )];
3319
3320 let mut metadata = input.metadata;
3321 apply_collection_default_ttl(&db, &input.collection, &mut metadata);
3322 let columns: Vec<(&str, crate::storage::schema::Value)> = fields
3323 .iter()
3324 .map(|(key, value)| (key.as_str(), value.clone()))
3325 .collect();
3326 let mut builder = db.row(&input.collection, columns);
3327
3328 for (key, value) in metadata {
3329 builder = builder.metadata(key, value);
3330 }
3331
3332 for node in input.node_links {
3333 builder = builder.link_to_node(node);
3334 }
3335
3336 for vector in input.vector_links {
3337 builder = builder.link_to_vector(vector);
3338 }
3339
3340 let id = builder.save()?;
3341 self.stamp_xmin_if_in_txn(&input.collection, id);
3343 self.index_store_ref()
3344 .index_entity_insert(&input.collection, id, &fields)
3345 .map_err(crate::RedDBError::Internal)?;
3346 refresh_context_index(&db, &input.collection, id)?;
3347 self.cdc_emit(
3348 crate::replication::cdc::ChangeOperation::Insert,
3349 &input.collection,
3350 id.raw(),
3351 "document",
3352 );
3353 Ok(CreateEntityOutput {
3354 id,
3355 entity: db.store().get(&input.collection, id),
3356 })
3357 }
3358
3359 fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
3360 let db = self.db();
3361 let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3362 let declared_model = db
3363 .collection_contract(&input.collection)
3364 .map(|contract| contract.declared_model);
3365 let value = if declared_model == Some(crate::catalog::CollectionModel::Vault) {
3366 contract.ensure_model(crate::catalog::CollectionModel::Vault)?;
3367 self.seal_vault_value(&input.collection, input.value)?
3368 } else {
3369 contract.ensure_model(crate::catalog::CollectionModel::Kv)?;
3370 input.value
3371 };
3372 let fields = vec![
3373 (
3374 "key".to_string(),
3375 crate::storage::schema::Value::text(input.key),
3376 ),
3377 ("value".to_string(), value),
3378 ];
3379 let collection = input.collection;
3380 let result = self.mutation_engine().apply(
3381 collection.clone(),
3382 vec![crate::runtime::mutation::MutationRow {
3383 fields,
3384 metadata: input.metadata,
3385 node_links: Vec::new(),
3386 vector_links: Vec::new(),
3387 }],
3388 )?;
3389 let id = result.ids[0];
3390 Ok(CreateEntityOutput {
3391 id,
3392 entity: db.store().get(&collection, id),
3393 })
3394 }
3395
3396 fn create_timeseries_point(
3397 &self,
3398 input: CreateTimeSeriesPointInput,
3399 ) -> RedDBResult<CreateEntityOutput> {
3400 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3401 let db = self.db();
3402 let contract = CollectionContractWriteEnforcer::new(&db, &input.collection);
3403 contract.ensure_model(crate::catalog::CollectionModel::TimeSeries)?;
3404
3405 let mut fields = vec![
3406 (
3407 "metric".to_string(),
3408 crate::storage::schema::Value::text(input.metric),
3409 ),
3410 (
3411 "value".to_string(),
3412 crate::storage::schema::Value::Float(input.value),
3413 ),
3414 ];
3415
3416 if let Some(timestamp_ns) = input.timestamp_ns {
3417 fields.push((
3418 "timestamp".to_string(),
3419 crate::storage::schema::Value::UnsignedInteger(timestamp_ns),
3420 ));
3421 }
3422
3423 if !input.tags.is_empty() {
3424 let tags_json = JsonValue::Object(
3425 input
3426 .tags
3427 .into_iter()
3428 .map(|(key, value)| (key, JsonValue::String(value)))
3429 .collect(),
3430 );
3431 let tags_bytes = json_to_vec(&tags_json).map_err(|err| {
3432 crate::RedDBError::Query(format!("failed to serialize timeseries tags: {err}"))
3433 })?;
3434 fields.push((
3435 "tags".to_string(),
3436 crate::storage::schema::Value::Json(tags_bytes),
3437 ));
3438 }
3439
3440 let collection = input.collection;
3441 let id = self.insert_timeseries_point(&collection, fields, input.metadata)?;
3442 self.stamp_xmin_if_in_txn(&collection, id);
3445 refresh_context_index(&db, &collection, id)?;
3446
3447 Ok(CreateEntityOutput {
3448 id,
3449 entity: db.store().get(&collection, id),
3450 })
3451 }
3452
3453 fn get_kv(
3454 &self,
3455 collection: &str,
3456 key: &str,
3457 ) -> RedDBResult<Option<(crate::storage::schema::Value, crate::storage::EntityId)>> {
3458 let db = self.db();
3459 ensure_collection_model_read(&db, collection, crate::catalog::CollectionModel::Kv)?;
3460 let store = db.store();
3461 let Some(manager) = store.get_collection(collection) else {
3462 return Ok(None);
3463 };
3464 let entities = manager.query_all(|_| true);
3465 for entity in entities {
3466 if let crate::storage::EntityData::Row(ref row) = entity.data {
3467 if let Some(ref named) = row.named {
3468 if let Some(crate::storage::schema::Value::Text(ref k)) = named.get("key") {
3469 if &**k == key {
3470 let value = named
3471 .get("value")
3472 .cloned()
3473 .unwrap_or(crate::storage::schema::Value::Null);
3474 return Ok(Some((value, entity.id)));
3475 }
3476 }
3477 }
3478 }
3479 }
3480 Ok(None)
3481 }
3482
3483 fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
3484 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3485 let found = self.get_kv(collection, key)?;
3486 if let Some((_, id)) = found {
3487 let db = self.db();
3488 let store = db.store();
3489 let deleted = store
3490 .delete(collection, id)
3491 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
3492 if deleted {
3493 store.context_index().remove_entity(id);
3494 }
3495 Ok(deleted)
3496 } else {
3497 Ok(false)
3498 }
3499 }
3500
3501 fn patch_entity(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
3502 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3503 let PatchEntityInput {
3504 collection,
3505 id,
3506 payload,
3507 operations,
3508 } = input;
3509 let db = self.db();
3510 let store = db.store();
3511 let Some(manager) = store.get_collection(&collection) else {
3512 return Err(crate::RedDBError::NotFound(format!(
3513 "collection not found: {collection}"
3514 )));
3515 };
3516 let Some(entity) = manager.get(id) else {
3517 return Err(crate::RedDBError::NotFound(format!(
3518 "entity not found: {}",
3519 id.raw()
3520 )));
3521 };
3522 self.apply_loaded_patch_entity(collection, entity, payload, operations)
3523 }
3524
3525 fn delete_entity(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
3526 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
3527 let store = self.db().store();
3528 let pre_delete_fields = store
3532 .get(&input.collection, input.id)
3533 .as_ref()
3534 .map(entity_row_fields_snapshot)
3535 .unwrap_or_default();
3536 let deleted = store
3540 .delete(&input.collection, input.id)
3541 .map_err(|err| crate::RedDBError::Internal(err.to_string()))?;
3542 if deleted {
3543 store.context_index().remove_entity(input.id);
3544 if !pre_delete_fields.is_empty() {
3547 self.index_store_ref()
3548 .index_entity_delete(&input.collection, input.id, &pre_delete_fields)
3549 .map_err(crate::RedDBError::Internal)?;
3550 }
3551 self.cdc_emit(
3552 crate::replication::cdc::ChangeOperation::Delete,
3553 &input.collection,
3554 input.id.raw(),
3555 "entity",
3556 );
3557 }
3558 Ok(DeleteEntityOutput {
3559 deleted,
3560 id: input.id,
3561 })
3562 }
3563}