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