1use std::collections::HashMap;
2
3use crate::application::ports::RuntimeEntityPort;
4use crate::json::{parse_json, to_vec as json_to_vec, Map, Value as JsonValue};
5use crate::storage::unified::devx::refs::{NodeRef, TableRef, VectorRef};
6use crate::storage::unified::{Metadata, MetadataValue, RefTarget, SparseVector, VectorData};
7use crate::storage::{EntityId, UnifiedEntity};
8use crate::{RedDBError, RedDBResult};
9use reddb_types::Value;
10
11#[derive(Debug, Clone)]
12pub struct CreateEntityOutput {
13 pub id: EntityId,
14 pub entity: Option<UnifiedEntity>,
15}
16
17#[derive(Debug, Clone)]
18pub struct AppliedEntityMutation {
19 pub id: EntityId,
20 pub collection: String,
21 pub entity: UnifiedEntity,
22 pub metadata: Option<crate::storage::unified::Metadata>,
23 pub modified_columns: Vec<String>,
24 pub persist_metadata: bool,
25 pub context_index_dirty: bool,
26 pub replaced_entity: Option<UnifiedEntity>,
29 pub replaced_entity_previous_xmax: u64,
31 pub pre_mutation_fields: Vec<(String, Value)>,
37}
38
39#[derive(Debug, Clone, Default, PartialEq)]
49pub struct RowDamageVector {
50 pub changed: Vec<(String, Value, Value)>,
53 pub added: Vec<(String, Value)>,
55 pub removed: Vec<(String, Value)>,
57}
58
59impl RowDamageVector {
60 pub fn is_empty(&self) -> bool {
64 self.changed.is_empty() && self.added.is_empty() && self.removed.is_empty()
65 }
66
67 pub fn touched_columns(&self) -> Vec<&str> {
72 let mut out: Vec<&str> =
73 Vec::with_capacity(self.changed.len() + self.added.len() + self.removed.len());
74 out.extend(self.changed.iter().map(|(c, _, _)| c.as_str()));
75 out.extend(self.added.iter().map(|(c, _)| c.as_str()));
76 out.extend(self.removed.iter().map(|(c, _)| c.as_str()));
77 out
78 }
79}
80
81pub fn row_damage_vector(
87 old_fields: &[(String, Value)],
88 new_fields: &[(String, Value)],
89) -> RowDamageVector {
90 let old_map: HashMap<&str, &Value> = old_fields.iter().map(|(k, v)| (k.as_str(), v)).collect();
95 let new_map: HashMap<&str, &Value> = new_fields.iter().map(|(k, v)| (k.as_str(), v)).collect();
96
97 let mut changed = Vec::new();
98 let mut added = Vec::new();
99 let mut removed = Vec::new();
100
101 for (name, new_value) in &new_map {
102 match old_map.get(name) {
103 Some(old_value) if old_value == new_value => {}
104 Some(old_value) => changed.push((
105 (*name).to_string(),
106 (*old_value).clone(),
107 (*new_value).clone(),
108 )),
109 None => added.push(((*name).to_string(), (*new_value).clone())),
110 }
111 }
112 for (name, old_value) in &old_map {
113 if !new_map.contains_key(name) {
114 removed.push(((*name).to_string(), (*old_value).clone()));
115 }
116 }
117
118 RowDamageVector {
119 changed,
120 added,
121 removed,
122 }
123}
124
125pub use reddb_engine::catalog::enforcement::{RowUpdateColumnRule, RowUpdateContractPlan};
126
127#[derive(Debug, Clone)]
128pub struct CreateRowInput {
129 pub collection: String,
130 pub fields: Vec<(String, Value)>,
131 pub metadata: Vec<(String, MetadataValue)>,
132 pub node_links: Vec<NodeRef>,
133 pub vector_links: Vec<VectorRef>,
134}
135
136#[derive(Debug, Clone)]
137pub struct CreateRowsBatchInput {
138 pub collection: String,
139 pub rows: Vec<CreateRowInput>,
140 pub suppress_events: bool,
142}
143
144#[derive(Debug, Clone)]
145pub struct CreateNodeEmbeddingInput {
146 pub name: String,
147 pub vector: Vec<f32>,
148 pub model: Option<String>,
149}
150
151#[derive(Debug, Clone)]
152pub struct CreateNodeTableLinkInput {
153 pub key: String,
154 pub table: TableRef,
155}
156
157#[derive(Debug, Clone)]
158pub struct CreateNodeGraphLinkInput {
159 pub target: EntityId,
160 pub edge_label: String,
161 pub weight: f32,
162}
163
164#[derive(Debug, Clone)]
165pub struct CreateNodeInput {
166 pub collection: String,
167 pub label: String,
168 pub node_type: Option<String>,
169 pub properties: Vec<(String, Value)>,
170 pub metadata: Vec<(String, MetadataValue)>,
171 pub embeddings: Vec<CreateNodeEmbeddingInput>,
172 pub table_links: Vec<CreateNodeTableLinkInput>,
173 pub node_links: Vec<CreateNodeGraphLinkInput>,
174}
175
176#[derive(Debug, Clone)]
177pub struct CreateEdgeInput {
178 pub collection: String,
179 pub label: String,
180 pub from: EntityId,
181 pub to: EntityId,
182 pub weight: Option<f32>,
183 pub properties: Vec<(String, Value)>,
184 pub metadata: Vec<(String, MetadataValue)>,
185}
186
187#[derive(Debug, Clone)]
188pub struct CreateVectorInput {
189 pub collection: String,
190 pub dense: Vec<f32>,
191 pub content: Option<String>,
192 pub metadata: Vec<(String, MetadataValue)>,
193 pub link_row: Option<TableRef>,
194 pub link_node: Option<NodeRef>,
195}
196
197#[derive(Debug, Clone)]
198pub struct CreateDocumentInput {
199 pub collection: String,
200 pub body: JsonValue,
201 pub metadata: Vec<(String, MetadataValue)>,
202 pub node_links: Vec<NodeRef>,
203 pub vector_links: Vec<VectorRef>,
204}
205
206#[derive(Debug, Clone)]
207pub struct CreateKvInput {
208 pub collection: String,
209 pub key: String,
210 pub value: Value,
211 pub metadata: Vec<(String, MetadataValue)>,
212}
213
214#[derive(Debug, Clone)]
215pub struct CreateTimeSeriesPointInput {
216 pub collection: String,
217 pub metric: String,
218 pub value: f64,
219 pub timestamp_ns: Option<u64>,
220 pub tags: Vec<(String, String)>,
221 pub metadata: Vec<(String, MetadataValue)>,
222}
223
224#[derive(Debug, Clone)]
225pub struct DeleteEntityInput {
226 pub collection: String,
227 pub id: EntityId,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum PatchEntityOperationType {
232 Set,
233 Replace,
234 Unset,
235}
236
237#[derive(Debug, Clone)]
238pub struct PatchEntityOperation {
239 pub op: PatchEntityOperationType,
240 pub path: Vec<String>,
241 pub value: Option<JsonValue>,
242}
243
244#[derive(Debug, Clone)]
245pub struct PatchEntityInput {
246 pub collection: String,
247 pub id: EntityId,
248 pub payload: JsonValue,
249 pub operations: Vec<PatchEntityOperation>,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub struct DeleteEntityOutput {
254 pub deleted: bool,
255 pub id: EntityId,
256}
257
258pub struct EntityUseCases<'a, P: ?Sized> {
259 runtime: &'a P,
260}
261
262impl<'a, P: RuntimeEntityPort + ?Sized> EntityUseCases<'a, P> {
263 pub fn new(runtime: &'a P) -> Self {
264 Self { runtime }
265 }
266
267 pub fn create_row(&self, input: CreateRowInput) -> RedDBResult<CreateEntityOutput> {
268 self.runtime.create_row(input)
269 }
270
271 pub fn create_rows_batch(
272 &self,
273 input: CreateRowsBatchInput,
274 ) -> RedDBResult<Vec<CreateEntityOutput>> {
275 self.runtime.create_rows_batch(input)
276 }
277
278 pub fn create_node(&self, input: CreateNodeInput) -> RedDBResult<CreateEntityOutput> {
279 self.runtime.create_node(input)
280 }
281
282 pub fn create_edge(&self, input: CreateEdgeInput) -> RedDBResult<CreateEntityOutput> {
283 self.runtime.create_edge(input)
284 }
285
286 pub fn create_vector(&self, input: CreateVectorInput) -> RedDBResult<CreateEntityOutput> {
287 self.runtime.create_vector(input)
288 }
289
290 pub fn create_document(&self, input: CreateDocumentInput) -> RedDBResult<CreateEntityOutput> {
291 self.runtime.create_document(input)
292 }
293
294 pub fn create_kv(&self, input: CreateKvInput) -> RedDBResult<CreateEntityOutput> {
295 self.runtime.create_kv(input)
296 }
297
298 pub fn create_timeseries_point(
299 &self,
300 input: CreateTimeSeriesPointInput,
301 ) -> RedDBResult<CreateEntityOutput> {
302 self.runtime.create_timeseries_point(input)
303 }
304
305 pub fn get_kv(&self, collection: &str, key: &str) -> RedDBResult<Option<(Value, EntityId)>> {
306 self.runtime.get_kv(collection, key)
307 }
308
309 pub fn delete_kv(&self, collection: &str, key: &str) -> RedDBResult<bool> {
310 self.runtime.delete_kv(collection, key)
311 }
312
313 pub fn patch(&self, input: PatchEntityInput) -> RedDBResult<CreateEntityOutput> {
314 self.runtime.patch_entity(input)
315 }
316
317 pub fn delete(&self, input: DeleteEntityInput) -> RedDBResult<DeleteEntityOutput> {
318 self.runtime.delete_entity(input)
319 }
320}
321
322pub(crate) fn json_to_storage_value(value: &JsonValue) -> RedDBResult<Value> {
323 match value {
324 JsonValue::Null => Ok(Value::Null),
325 JsonValue::Bool(value) => Ok(Value::Boolean(*value)),
326 JsonValue::Integer(value) => Ok(Value::Integer(*value)),
327 JsonValue::Number(value) => {
328 if value.fract().abs() < f64::EPSILON {
329 Ok(Value::Integer(*value as i64))
330 } else {
331 Ok(Value::Float(*value))
332 }
333 }
334 JsonValue::Decimal(value) => Ok(Value::DecimalText(value.clone())),
335 JsonValue::String(value) => Ok(Value::text(value.clone())),
336 JsonValue::Array(_) | JsonValue::Object(_) => json_to_vec(value)
337 .map(Value::Json)
338 .map_err(|err| RedDBError::Query(format!("failed to serialize JSON value: {err}"))),
339 }
340}
341
342pub(crate) fn json_to_metadata_value(value: &JsonValue) -> RedDBResult<MetadataValue> {
343 match value {
344 JsonValue::Null => Ok(MetadataValue::Null),
345 JsonValue::Bool(value) => Ok(MetadataValue::Bool(*value)),
346 JsonValue::Integer(value) => Ok(MetadataValue::Int(*value)),
347 JsonValue::Number(value) => {
348 if value.fract().abs() < f64::EPSILON {
349 Ok(MetadataValue::Int(*value as i64))
350 } else {
351 Ok(MetadataValue::Float(*value))
352 }
353 }
354 JsonValue::Decimal(value) => Ok(MetadataValue::String(value.clone())),
355 JsonValue::String(value) => Ok(MetadataValue::String(value.clone())),
356 JsonValue::Array(values) => {
357 let mut items = Vec::with_capacity(values.len());
358 for value in values {
359 items.push(json_to_metadata_value(value)?);
360 }
361 Ok(MetadataValue::Array(items))
362 }
363 JsonValue::Object(map) => {
364 let mut object = HashMap::with_capacity(map.len());
365 for (key, value) in map {
366 object.insert(key.clone(), json_to_metadata_value(value)?);
367 }
368 Ok(MetadataValue::Object(object))
369 }
370 }
371}
372
373pub(crate) fn apply_patch_operations_to_storage_map(
374 fields: &mut HashMap<String, Value>,
375 operations: &[PatchEntityOperation],
376) -> RedDBResult<()> {
377 if operations.is_empty() {
378 return Ok(());
379 }
380
381 let mut patch_target = JsonValue::Object(
382 fields
383 .iter()
384 .map(|(key, value)| (key.clone(), value.to_json()))
385 .collect(),
386 );
387 apply_patch_operations_to_json(&mut patch_target, operations)
388 .map_err(|error| RedDBError::Query(format!("patch fields failed: {error}")))?;
389
390 let JsonValue::Object(object) = patch_target else {
391 return Err(RedDBError::Query(
392 "patch operations require object roots".to_string(),
393 ));
394 };
395
396 let mut merged = HashMap::with_capacity(object.len());
397 for (key, value) in object {
398 merged.insert(key, json_to_storage_value(&value)?);
399 }
400 *fields = merged;
401 Ok(())
402}
403
404pub fn apply_patch_operations_to_json_public(
409 value: &mut JsonValue,
410 operations: &[PatchEntityOperation],
411) -> Result<(), String> {
412 apply_patch_operations_to_json(value, operations)
413}
414
415pub(crate) fn apply_patch_operations_to_json(
416 value: &mut JsonValue,
417 operations: &[PatchEntityOperation],
418) -> Result<(), String> {
419 for operation in operations {
420 if operation.path.is_empty() {
421 return Err("patch path cannot be empty".to_string());
422 }
423
424 match operation.op {
425 PatchEntityOperationType::Set | PatchEntityOperationType::Replace => {
426 let Some(patch_value) = &operation.value else {
427 return Err("set/replace operations require a value".to_string());
428 };
429 apply_patch_json_set(value, &operation.path, patch_value.clone())?;
430 }
431 PatchEntityOperationType::Unset => {
432 apply_patch_json_unset(value, &operation.path)?;
433 }
434 }
435 }
436 Ok(())
437}
438
439pub(crate) fn apply_patch_operations_to_vector_fields(
440 vector: &mut VectorData,
441 operations: &[PatchEntityOperation],
442) -> RedDBResult<()> {
443 if operations.is_empty() {
444 return Ok(());
445 }
446
447 let mut vector_target = JsonValue::Object({
448 let mut object = Map::new();
449 object.insert(
450 "dense".to_string(),
451 JsonValue::Array(
452 vector
453 .dense
454 .iter()
455 .map(|value| JsonValue::Number(*value as f64))
456 .collect(),
457 ),
458 );
459 object.insert(
460 "sparse".to_string(),
461 vector.sparse.as_ref().map_or(JsonValue::Null, |sparse| {
462 let mut object = Map::new();
463 object.insert(
464 "indices".to_string(),
465 JsonValue::Array(
466 sparse
467 .indices
468 .iter()
469 .map(|value| JsonValue::Number(*value as f64))
470 .collect(),
471 ),
472 );
473 object.insert(
474 "values".to_string(),
475 JsonValue::Array(
476 sparse
477 .values
478 .iter()
479 .map(|value| JsonValue::Number(*value as f64))
480 .collect(),
481 ),
482 );
483 object.insert(
484 "dimension".to_string(),
485 JsonValue::Number(sparse.dimension as f64),
486 );
487 JsonValue::Object(object)
488 }),
489 );
490 object.insert(
491 "content".to_string(),
492 match vector.content.as_ref() {
493 Some(value) => JsonValue::String(value.clone()),
494 None => JsonValue::Null,
495 },
496 );
497 object
498 });
499
500 let touched_dense = operations
501 .iter()
502 .any(|operation| operation.path.first().is_some_and(|key| key == "dense"));
503 let touched_sparse = operations
504 .iter()
505 .any(|operation| operation.path.first().is_some_and(|key| key == "sparse"));
506 let touched_content = operations
507 .iter()
508 .any(|operation| operation.path.first().is_some_and(|key| key == "content"));
509
510 apply_patch_operations_to_json(&mut vector_target, operations)
511 .map_err(|error| RedDBError::Query(format!("patch fields failed: {error}")))?;
512
513 let JsonValue::Object(object) = vector_target else {
514 return Err(RedDBError::Query(
515 "patch operations require object roots".to_string(),
516 ));
517 };
518
519 if touched_dense {
520 let Some(value) = object.get("dense") else {
521 return Err(RedDBError::Query(
522 "field 'dense' cannot be unset".to_string(),
523 ));
524 };
525 vector.dense = parse_patch_f32_vector(value, "dense")?;
526 }
527
528 if touched_content {
529 vector.content = match object.get("content") {
530 None | Some(JsonValue::Null) => None,
531 Some(value) => Some(
532 value
533 .as_str()
534 .ok_or_else(|| {
535 RedDBError::Query("field 'content' must be a string".to_string())
536 })?
537 .to_string(),
538 ),
539 };
540 }
541
542 if touched_sparse {
543 vector.sparse = match object.get("sparse") {
544 Some(value) => parse_sparse_vector_value(value)?,
545 None => None,
546 };
547 }
548
549 Ok(())
550}
551
552pub(crate) fn metadata_to_json(metadata: &Metadata) -> JsonValue {
553 JsonValue::Object(
554 metadata
555 .iter()
556 .map(|(key, value)| (key.clone(), metadata_value_to_json(value)))
557 .collect(),
558 )
559}
560
561pub(crate) fn metadata_from_json(payload: &JsonValue) -> RedDBResult<Metadata> {
562 let JsonValue::Object(object) = payload else {
563 return Err(RedDBError::Query(
564 "metadata patch requires an object".to_string(),
565 ));
566 };
567
568 let mut metadata = Metadata::new();
569 for (key, value) in object {
570 metadata.set(key.clone(), metadata_value_from_json(value)?);
571 }
572 Ok(metadata)
573}
574
575fn metadata_value_to_json(value: &MetadataValue) -> JsonValue {
576 match value {
577 MetadataValue::Null => JsonValue::Null,
578 MetadataValue::Bool(value) => JsonValue::Bool(*value),
579 MetadataValue::Int(value) => JsonValue::Number(*value as f64),
580 MetadataValue::Float(value) => JsonValue::Number(*value),
581 MetadataValue::String(value) => JsonValue::String(value.clone()),
582 MetadataValue::Bytes(value) => {
583 let mut object = Map::new();
584 object.insert(
585 "__redb_type".to_string(),
586 JsonValue::String("bytes".to_string()),
587 );
588 object.insert(
589 "value".to_string(),
590 JsonValue::Array(
591 value
592 .iter()
593 .map(|value| JsonValue::Number(*value as f64))
594 .collect(),
595 ),
596 );
597 JsonValue::Object(object)
598 }
599 MetadataValue::Array(values) => {
600 JsonValue::Array(values.iter().map(metadata_value_to_json).collect())
601 }
602 MetadataValue::Object(object) => JsonValue::Object(
603 object
604 .iter()
605 .map(|(key, value)| (key.clone(), metadata_value_to_json(value)))
606 .collect(),
607 ),
608 MetadataValue::Timestamp(value) => JsonValue::Number(*value as f64),
609 MetadataValue::Geo { lat, lon } => {
610 let mut object = Map::new();
611 object.insert(
612 "__redb_type".to_string(),
613 JsonValue::String("geo".to_string()),
614 );
615 object.insert("lat".to_string(), JsonValue::Number(*lat));
616 object.insert("lon".to_string(), JsonValue::Number(*lon));
617 JsonValue::Object(object)
618 }
619 MetadataValue::Reference(value) => {
620 let mut object = Map::new();
621 object.insert(
622 "__redb_type".to_string(),
623 JsonValue::String("reference".to_string()),
624 );
625 let (kind, collection, id) = match value {
626 RefTarget::TableRow { table, row_id } => ("table_row", table.as_str(), *row_id),
627 RefTarget::Node {
628 collection,
629 node_id,
630 } => ("node", collection.as_str(), node_id.raw()),
631 RefTarget::Edge {
632 collection,
633 edge_id,
634 } => ("edge", collection.as_str(), edge_id.raw()),
635 RefTarget::Vector {
636 collection,
637 vector_id,
638 } => ("vector", collection.as_str(), vector_id.raw()),
639 RefTarget::Entity {
640 collection,
641 entity_id,
642 } => ("entity", collection.as_str(), entity_id.raw()),
643 };
644 object.insert("kind".to_string(), JsonValue::String(kind.to_string()));
645 object.insert(
646 "collection".to_string(),
647 JsonValue::String(collection.to_string()),
648 );
649 object.insert("id".to_string(), JsonValue::Number(id as f64));
650 JsonValue::Object(object)
651 }
652 MetadataValue::References(values) => {
653 let mut object = Map::new();
654 object.insert(
655 "__redb_type".to_string(),
656 JsonValue::String("references".to_string()),
657 );
658 object.insert(
659 "values".to_string(),
660 JsonValue::Array(
661 values
662 .iter()
663 .map(|r| metadata_value_to_json(&MetadataValue::Reference(r.clone())))
664 .collect(),
665 ),
666 );
667 JsonValue::Object(object)
668 }
669 }
670}
671
672fn metadata_value_from_json(value: &JsonValue) -> RedDBResult<MetadataValue> {
673 match value {
674 JsonValue::Null => Ok(MetadataValue::Null),
675 JsonValue::Bool(value) => Ok(MetadataValue::Bool(*value)),
676 JsonValue::Integer(value) => Ok(MetadataValue::Int(*value)),
677 JsonValue::Number(value) => {
678 if value.fract().abs() < f64::EPSILON {
679 Ok(MetadataValue::Int(*value as i64))
680 } else {
681 Ok(MetadataValue::Float(*value))
682 }
683 }
684 JsonValue::Decimal(value) => Ok(MetadataValue::String(value.clone())),
685 JsonValue::String(value) => Ok(MetadataValue::String(value.clone())),
686 JsonValue::Array(values) => {
687 let mut out = Vec::with_capacity(values.len());
688 for value in values {
689 out.push(metadata_value_from_json(value)?);
690 }
691 Ok(MetadataValue::Array(out))
692 }
693 JsonValue::Object(object) => {
694 if let Some(marker) = object.get("__redb_type").and_then(JsonValue::as_str) {
695 match marker {
696 "bytes" => {
697 let values = object
698 .get("value")
699 .and_then(JsonValue::as_array)
700 .ok_or_else(|| {
701 RedDBError::Query(
702 "metadata marker 'bytes' requires array value".to_string(),
703 )
704 })?;
705 let mut out = Vec::with_capacity(values.len());
706 for value in values {
707 let value = value.as_i64().ok_or_else(|| {
708 RedDBError::Query(
709 "metadata bytes must contain integer values".to_string(),
710 )
711 })?;
712 if !(0..=255).contains(&value) {
713 return Err(RedDBError::Query(
714 "metadata bytes must contain values between 0 and 255"
715 .to_string(),
716 ));
717 }
718 out.push(value as u8);
719 }
720 return Ok(MetadataValue::Bytes(out));
721 }
722 "geo" => {
723 let lat =
724 object
725 .get("lat")
726 .and_then(JsonValue::as_f64)
727 .ok_or_else(|| {
728 RedDBError::Query(
729 "metadata marker 'geo' requires numeric 'lat'".to_string(),
730 )
731 })?;
732 let lon =
733 object
734 .get("lon")
735 .and_then(JsonValue::as_f64)
736 .ok_or_else(|| {
737 RedDBError::Query(
738 "metadata marker 'geo' requires numeric 'lon'".to_string(),
739 )
740 })?;
741 return Ok(MetadataValue::Geo { lat, lon });
742 }
743 "reference" => {
744 return parse_metadata_reference(object).map(MetadataValue::Reference)
745 }
746 "references" => {
747 let values = object
748 .get("values")
749 .and_then(JsonValue::as_array)
750 .ok_or_else(|| {
751 RedDBError::Query(
752 "metadata marker 'references' requires array 'values'"
753 .to_string(),
754 )
755 })?;
756 let mut references = Vec::with_capacity(values.len());
757 for value in values {
758 references.push(parse_metadata_reference_value(value)?);
759 }
760 return Ok(MetadataValue::References(references));
761 }
762 _ => {}
763 }
764 }
765
766 let mut out = HashMap::with_capacity(object.len());
767 for (key, value) in object {
768 out.insert(key.clone(), metadata_value_from_json(value)?);
769 }
770 Ok(MetadataValue::Object(out))
771 }
772 }
773}
774
775fn parse_metadata_reference(object: &Map<String, JsonValue>) -> RedDBResult<RefTarget> {
776 let kind = object
777 .get("kind")
778 .and_then(JsonValue::as_str)
779 .ok_or_else(|| RedDBError::Query("metadata reference requires 'kind'".to_string()))?;
780 let collection = object
781 .get("collection")
782 .and_then(JsonValue::as_str)
783 .ok_or_else(|| RedDBError::Query("metadata reference requires 'collection'".to_string()))?;
784 let id = object
785 .get("id")
786 .ok_or_else(|| RedDBError::Query("metadata reference requires 'id'".to_string()))?;
787 let id = parse_patch_u64_value(id, "id")?;
788
789 let target = match kind {
790 "table_row" | "table" => RefTarget::table(collection.to_string(), id),
791 "node" => RefTarget::node(collection.to_string(), EntityId::new(id)),
792 "edge" => RefTarget::Edge {
793 collection: collection.to_string(),
794 edge_id: EntityId::new(id),
795 },
796 "vector" => RefTarget::vector(collection.to_string(), EntityId::new(id)),
797 "entity" => RefTarget::Entity {
798 collection: collection.to_string(),
799 entity_id: EntityId::new(id),
800 },
801 _ => {
802 return Err(RedDBError::Query(format!(
803 "unsupported metadata reference kind '{kind}'"
804 )));
805 }
806 };
807
808 Ok(target)
809}
810
811fn parse_metadata_reference_value(value: &JsonValue) -> RedDBResult<RefTarget> {
812 let JsonValue::Object(object) = value else {
813 return Err(RedDBError::Query(
814 "metadata reference entries must be objects".to_string(),
815 ));
816 };
817 parse_metadata_reference(object)
818}
819
820fn parse_patch_u64_value(value: &JsonValue, field: &str) -> RedDBResult<u64> {
821 let Some(value) = value.as_f64() else {
822 return Err(RedDBError::Query(format!(
823 "field '{field}' must be a number"
824 )));
825 };
826 if value.is_sign_negative() {
827 return Err(RedDBError::Query(format!(
828 "field '{field}' cannot be negative"
829 )));
830 }
831 if value.fract().abs() > f64::EPSILON {
832 return Err(RedDBError::Query(format!(
833 "field '{field}' must be an integer"
834 )));
835 }
836 if value > u64::MAX as f64 {
837 return Err(RedDBError::Query(format!("field '{field}' is too large")));
838 }
839 Ok(value as u64)
840}
841
842fn parse_patch_f32_vector(value: &JsonValue, field: &str) -> RedDBResult<Vec<f32>> {
843 let values = value
844 .as_array()
845 .ok_or_else(|| RedDBError::Query(format!("field '{field}' must be an array")))?;
846 let mut out = Vec::with_capacity(values.len());
847 for value in values {
848 let number = value.as_f64().ok_or_else(|| {
849 RedDBError::Query(format!("field '{field}' must contain only numbers"))
850 })?;
851 out.push(number as f32);
852 }
853 if out.is_empty() {
854 return Err(RedDBError::Query(format!(
855 "field '{field}' cannot be empty"
856 )));
857 }
858 Ok(out)
859}
860
861fn parse_sparse_index_array(value: &JsonValue, field: &str) -> RedDBResult<Vec<u32>> {
862 let values = value
863 .as_array()
864 .ok_or_else(|| RedDBError::Query(format!("field '{field}' must be an array")))?;
865 let mut out = Vec::with_capacity(values.len());
866 for value in values {
867 let value = value.as_f64().ok_or_else(|| {
868 RedDBError::Query(format!("field '{field}' must contain only integers"))
869 })?;
870 if value.is_sign_negative() || value.fract().abs() > f64::EPSILON {
871 return Err(RedDBError::Query(format!(
872 "field '{field}' must contain only u32 values"
873 )));
874 }
875 if value > u32::MAX as f64 {
876 return Err(RedDBError::Query(format!(
877 "field '{field}' value is too large"
878 )));
879 }
880 out.push(value as u32);
881 }
882 Ok(out)
883}
884
885fn parse_sparse_value_array(value: &JsonValue, field: &str) -> RedDBResult<Vec<f32>> {
886 parse_patch_f32_vector(value, field)
887}
888
889fn parse_sparse_vector_value(value: &JsonValue) -> RedDBResult<Option<SparseVector>> {
890 match value {
891 JsonValue::Null => Ok(None),
892 JsonValue::Object(object) => {
893 let indices = parse_sparse_index_array(
894 object.get("indices").ok_or_else(|| {
895 RedDBError::Query("sparse metadata requires 'indices'".to_string())
896 })?,
897 "sparse.indices",
898 )?;
899 let values = parse_sparse_value_array(
900 object.get("values").ok_or_else(|| {
901 RedDBError::Query("sparse metadata requires 'values'".to_string())
902 })?,
903 "sparse.values",
904 )?;
905 if indices.len() != values.len() {
906 return Err(RedDBError::Query(
907 "sparse indices and values lengths must match".to_string(),
908 ));
909 }
910 let dimension = match object.get("dimension").and_then(JsonValue::as_f64) {
911 Some(value) => {
912 if value.is_sign_negative() || value.fract().abs() > f64::EPSILON {
913 return Err(RedDBError::Query(
914 "sparse dimension must be a non-negative integer".to_string(),
915 ));
916 }
917 if value > usize::MAX as f64 {
918 return Err(RedDBError::Query(
919 "sparse dimension is too large".to_string(),
920 ));
921 }
922 value as usize
923 }
924 None => indices
925 .iter()
926 .max()
927 .map_or(0, |index| (*index as usize) + 1),
928 };
929 if indices.iter().any(|index| (*index as usize) >= dimension) {
930 return Err(RedDBError::Query(
931 "sparse indices must be smaller than dimension".to_string(),
932 ));
933 }
934 Ok(Some(SparseVector::new(indices, values, dimension)))
935 }
936 _ => Err(RedDBError::Query(
937 "field 'sparse' must be an object or null".to_string(),
938 )),
939 }
940}
941
942fn apply_patch_json_set(
943 target: &mut JsonValue,
944 path: &[String],
945 value: JsonValue,
946) -> Result<(), String> {
947 if path.is_empty() {
948 return Err("patch path cannot be empty".to_string());
949 }
950
951 let mut current = target;
952 for segment in &path[..path.len() - 1] {
953 let JsonValue::Object(object) = current else {
954 return Err("patch path target must be an object".to_string());
955 };
956 let value = object
957 .entry(segment.clone())
958 .or_insert_with(|| JsonValue::Object(Map::new()));
959 if !matches!(value, JsonValue::Object(_)) {
960 *value = JsonValue::Object(Map::new());
961 }
962 current = value;
963 }
964
965 let JsonValue::Object(object) = current else {
966 return Err("patch path target must be an object".to_string());
967 };
968 object.insert(path[path.len() - 1].clone(), value);
969 Ok(())
970}
971
972fn apply_patch_json_unset(target: &mut JsonValue, path: &[String]) -> Result<(), String> {
973 if path.is_empty() {
974 return Err("patch path cannot be empty".to_string());
975 }
976
977 if path.len() == 1 {
978 let JsonValue::Object(object) = target else {
979 return Err("patch path target must be an object".to_string());
980 };
981 object.remove(&path[0]);
982 return Ok(());
983 }
984
985 let mut current = target;
986 for segment in &path[..path.len() - 1] {
987 let Some(value) = (match current {
988 JsonValue::Object(object) => object.get_mut(segment),
989 _ => {
990 return Err("patch path target must be an object".to_string());
991 }
992 }) else {
993 return Ok(());
994 };
995
996 if !matches!(value, JsonValue::Object(_)) {
997 return Ok(());
998 }
999 current = value;
1000 }
1001
1002 let JsonValue::Object(object) = current else {
1003 return Err("patch path target must be an object".to_string());
1004 };
1005 object.remove(&path[path.len() - 1]);
1006 Ok(())
1007}
1008
1009fn format_mac(bytes: &[u8; 6]) -> String {
1010 bytes
1011 .iter()
1012 .map(|byte| format!("{byte:02x}"))
1013 .collect::<Vec<_>>()
1014 .join(":")
1015}
1016
1017#[cfg(test)]
1018mod damage_vector_tests {
1019 use super::*;
1020
1021 fn s(n: &str) -> String {
1022 n.to_string()
1023 }
1024
1025 #[test]
1026 fn identical_rows_produce_empty_vector() {
1027 let old = vec![
1028 (s("name"), Value::text(s("alice"))),
1029 (s("age"), Value::Integer(30)),
1030 ];
1031 let new = old.clone();
1032 let dv = row_damage_vector(&old, &new);
1033 assert!(dv.is_empty());
1034 assert!(dv.touched_columns().is_empty());
1035 }
1036
1037 #[test]
1038 fn detects_changed_column_only() {
1039 let old = vec![
1040 (s("name"), Value::text(s("alice"))),
1041 (s("age"), Value::Integer(30)),
1042 ];
1043 let new = vec![
1044 (s("name"), Value::text(s("alice"))),
1045 (s("age"), Value::Integer(31)),
1046 ];
1047 let dv = row_damage_vector(&old, &new);
1048 assert_eq!(dv.changed.len(), 1);
1049 assert_eq!(dv.changed[0].0, "age");
1050 assert!(dv.added.is_empty());
1051 assert!(dv.removed.is_empty());
1052 assert_eq!(dv.touched_columns(), vec!["age"]);
1053 }
1054
1055 #[test]
1056 fn detects_added_and_removed_columns() {
1057 let old = vec![
1058 (s("name"), Value::text(s("alice"))),
1059 (s("nickname"), Value::text(s("al"))),
1060 ];
1061 let new = vec![
1062 (s("name"), Value::text(s("alice"))),
1063 (s("email"), Value::text(s("a@x.com"))),
1064 ];
1065 let dv = row_damage_vector(&old, &new);
1066 assert!(dv.changed.is_empty());
1067 assert_eq!(dv.added.len(), 1);
1068 assert_eq!(dv.added[0].0, "email");
1069 assert_eq!(dv.removed.len(), 1);
1070 assert_eq!(dv.removed[0].0, "nickname");
1071 }
1072
1073 #[test]
1074 fn field_order_does_not_affect_diff() {
1075 let old = vec![
1078 (s("name"), Value::text(s("bob"))),
1079 (s("age"), Value::Integer(42)),
1080 ];
1081 let new = vec![
1082 (s("age"), Value::Integer(42)),
1083 (s("name"), Value::text(s("bob"))),
1084 ];
1085 assert!(row_damage_vector(&old, &new).is_empty());
1086 }
1087
1088 #[test]
1089 fn mixed_changed_added_removed() {
1090 let old = vec![
1091 (s("a"), Value::Integer(1)),
1092 (s("b"), Value::Integer(2)),
1093 (s("gone"), Value::text(s("x"))),
1094 ];
1095 let new = vec![
1096 (s("a"), Value::Integer(10)), (s("b"), Value::Integer(2)), (s("new"), Value::Boolean(true)), ];
1100 let dv = row_damage_vector(&old, &new);
1101 assert_eq!(dv.changed.len(), 1);
1102 assert_eq!(dv.added.len(), 1);
1103 assert_eq!(dv.removed.len(), 1);
1104 let mut touched: Vec<&str> = dv.touched_columns();
1105 touched.sort();
1106 assert_eq!(touched, vec!["a", "gone", "new"]);
1107 }
1108}