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