Skip to main content

reddb_server/application/
entity.rs

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    /// Prior physical version retained for MVCC history. Present when an
27    /// UPDATE creates a new physical entity for the same logical row.
28    pub replaced_entity: Option<UnifiedEntity>,
29    /// xmax carried by `replaced_entity` before this mutation stamped it.
30    pub replaced_entity_previous_xmax: u64,
31    /// Snapshot of the row's named fields BEFORE the mutation was
32    /// applied. Carried so the post-write secondary-index hook can
33    /// `delete(old) + insert(new)` for changed indexed columns.
34    /// Empty when the entity isn't a row or the row carried neither a
35    /// `named` map nor a `schema` Arc.
36    pub pre_mutation_fields: Vec<(String, Value)>,
37}
38
39/// Damage-vector for a row update: the minimal diff between the
40/// pre-mutation and post-mutation field state. Downstream consumers
41/// (secondary-index maintainer, CDC emitter, eventually the pager's
42/// in-place update path) work off this struct so they only touch
43/// columns that actually changed.
44///
45/// Future work (Fase 5): the pager can decide whether `changed` fits
46/// in the existing cell's slotted footprint and patch bytes in place
47/// instead of rewriting the whole row.
48#[derive(Debug, Clone, Default, PartialEq)]
49pub struct RowDamageVector {
50    /// Columns present in both `old` and `new` whose value differs.
51    /// `(column, old_value, new_value)`.
52    pub changed: Vec<(String, Value, Value)>,
53    /// Columns present in `new` but not in `old`.
54    pub added: Vec<(String, Value)>,
55    /// Columns present in `old` but not in `new`.
56    pub removed: Vec<(String, Value)>,
57}
58
59impl RowDamageVector {
60    /// True when no columns changed, were added, or were removed.
61    /// Callers can short-circuit index/CDC/in-place work when this
62    /// holds.
63    pub fn is_empty(&self) -> bool {
64        self.changed.is_empty() && self.added.is_empty() && self.removed.is_empty()
65    }
66
67    /// Names of every column the update touched. Equivalent to the
68    /// current `modified_columns` list carried on `AppliedEntityMutation`
69    /// but computed from the ground truth of old/new rather than
70    /// from what the SQL executor thinks it wrote.
71    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
81/// Compute the damage-vector between an old and new row snapshot.
82/// Both inputs are field lists (as carried by `CreateRowInput` and
83/// `AppliedEntityMutation.pre_mutation_fields`); the order of fields
84/// is not significant. Columns present in both sides with identical
85/// values don't appear in any bucket.
86pub fn row_damage_vector(
87    old_fields: &[(String, Value)],
88    new_fields: &[(String, Value)],
89) -> RowDamageVector {
90    // HashMap<&str, &Value> keyed by column name for O(1) membership
91    // checks. Both inputs are expected to be small (tens of columns),
92    // so the allocation overhead is negligible vs the O(N*M) pairwise
93    // comparison we'd otherwise need.
94    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    /// When true, no event subscriptions fire for this batch (SUPPRESS EVENTS).
141    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
404/// `pub` re-export of [`apply_patch_operations_to_json`] for the HTTP layer.
405/// The KV patch handler (issue #751) parses a JSON value, applies operations
406/// against the live JSON tree, and writes back — without reaching the
407/// entity-row patch core that this module otherwise owns.
408pub 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        // `(name, age)` and `(age, name)` with identical values should
1076        // diff as empty — column order is a presentation concern.
1077        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)),     // changed
1097            (s("b"), Value::Integer(2)),      // unchanged
1098            (s("new"), Value::Boolean(true)), // added
1099        ];
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}