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