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