Skip to main content

armour_typ/
graph.rs

1//! Declarative, serializable schema-graph model. No executable code here —
2//! the registry/validator live in `armdb::schema`; this model is what a
3//! future RPC `GetSchema` extension can ship to a GUI.
4
5use serde::Serialize;
6
7use crate::refs::PathSeg;
8use crate::typ::Typ;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11pub enum CollectionKind {
12    /// Primary collection owning an entity (FK target for its brand).
13    Entity,
14    /// Derived collection synchronized from a source (hook/task).
15    SecondaryIndex,
16    /// Same entity duplicated for another access path.
17    Denormalized,
18    /// Recomputable counters/aggregates.
19    Counters,
20    /// Not related to other collections.
21    Standalone,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
25pub enum OnMissing {
26    /// Missing target — error finding.
27    Error,
28    /// Missing target — warning finding.
29    Warn,
30    /// Missing target is legal (e.g. reply to a deleted message).
31    AllowMissing,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub enum RelationKind {
36    Fk {
37        /// Path of the ID field inside key/value ("k"/"v" prefix in `field_in`).
38        field_path: Vec<PathSeg>,
39        brand: String,
40        on_missing: OnMissing,
41        /// false — graph-only edge; validator reports it as UnvalidatedEdge
42        /// unless an explicit fk declaration covers it.
43        validated: bool,
44    },
45    /// `to` is a secondary index of `from`.
46    Index,
47    /// `to` is a denormalization of `from`.
48    Denorm,
49    /// Field `field` in `from` is recomputed from `to`.
50    Counter { field: String },
51}
52
53/// Value-model of a collection (how the value is serialized on disk).
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
55pub enum ValueModel {
56    /// Fixed-size, zerocopy-transmute (ZeroTree/ZeroMap).
57    Zero,
58    /// Codec value inline in the index (TypedTree/TypedMap).
59    Typed,
60    /// Codec value on disk + LRU cache, pointer in the index (VarTypedTree/VarTypedMap).
61    VarTyped,
62}
63
64/// Collection shape (access pattern).
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
66pub enum CollectionShape {
67    /// Ordered scan / range / prefix (SkipList).
68    Tree,
69    /// Point-lookup, no iteration (HashMap).
70    Map,
71}
72
73/// Durability backend.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
75pub enum StorageBackend {
76    /// Append-only log + compaction.
77    Bitcask,
78    /// In-place pwrite into fixed slots.
79    Fixed,
80}
81
82/// Physical storage class of a collection — fixed at `db.open_*`.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
84pub struct StorageClass {
85    pub model: ValueModel,
86    pub shape: CollectionShape,
87    pub backend: StorageBackend,
88}
89
90#[derive(Debug, Clone, Serialize)]
91pub struct CollectionNode {
92    pub name: String,
93    pub kind: CollectionKind,
94    /// Value type descriptor (already serializable, reused as-is).
95    pub ty: Typ,
96    /// Brand of SelfId if the key is a branded ID.
97    pub self_brand: Option<String>,
98    /// Physical storage class. `None` for external nodes (no handle).
99    pub storage: Option<StorageClass>,
100}
101
102#[derive(Debug, Clone, Serialize)]
103pub struct RelationEdge {
104    pub from: String,
105    pub to: String,
106    pub kind: RelationKind,
107}
108
109#[derive(Debug, Clone, Serialize, Default)]
110pub struct SchemaGraph {
111    pub collections: Vec<CollectionNode>,
112    pub relations: Vec<RelationEdge>,
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::scalar::ScalarTyp;
119
120    #[test]
121    fn graph_serializes_to_json() {
122        let g = SchemaGraph {
123            collections: vec![CollectionNode {
124                name: "users".into(),
125                kind: CollectionKind::Entity,
126                ty: Typ::Scalar(ScalarTyp::U64),
127                self_brand: Some("USR".into()),
128                storage: None,
129            }],
130            relations: vec![RelationEdge {
131                from: "messages".into(),
132                to: "users".into(),
133                kind: RelationKind::Fk {
134                    field_path: vec![PathSeg::Field("sender_id")],
135                    brand: "USR".into(),
136                    on_missing: OnMissing::Error,
137                    validated: true,
138                },
139            }],
140        };
141        let json = serde_json::to_string(&g).expect("serialize");
142        assert!(json.contains("\"users\""));
143        assert!(json.contains("\"sender_id\""));
144    }
145}