Skip to main content

issundb_core/
schema.rs

1use deepsize::DeepSizeOf;
2use serde::{Deserialize, Serialize};
3use zerocopy::{FromBytes, Immutable, IntoBytes};
4
5pub type NodeId = u64;
6pub type EdgeId = u64;
7pub type LabelId = u32;
8pub type TypeId = u32;
9pub type PropKeyId = u32;
10
11/// Supported languages for Full-Text Search indexing and stemming.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[repr(u8)]
14pub enum Language {
15    English = 1,
16    Spanish = 2,
17    French = 3,
18    German = 4,
19    Italian = 5,
20    Portuguese = 6,
21}
22
23impl Default for Language {
24    fn default() -> Self {
25        Self::English
26    }
27}
28
29impl Language {
30    pub fn from_u8(val: u8) -> Self {
31        match val {
32            2 => Self::Spanish,
33            3 => Self::French,
34            4 => Self::German,
35            5 => Self::Italian,
36            6 => Self::Portuguese,
37            _ => Self::English,
38        }
39    }
40
41    pub fn to_u8(self) -> u8 {
42        self as u8
43    }
44}
45
46impl std::str::FromStr for Language {
47    type Err = crate::error::Error;
48
49    /// Parse a language name. Case-insensitive. Accepts the six supported
50    /// stemming languages. This is the canonical mapping every binding parses
51    /// through, so an unknown name is rejected rather than silently defaulting.
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        match s.to_lowercase().as_str() {
54            "english" => Ok(Self::English),
55            "spanish" => Ok(Self::Spanish),
56            "french" => Ok(Self::French),
57            "german" => Ok(Self::German),
58            "italian" => Ok(Self::Italian),
59            "portuguese" => Ok(Self::Portuguese),
60            other => Err(crate::error::Error::InvalidArgument(format!(
61                "unknown language '{other}' (expected one of: english, spanish, french, german, italian, portuguese)"
62            ))),
63        }
64    }
65}
66
67/// One adjacency entry stored as a raw LMDB duplicate value.
68///
69/// Fixed 20-byte `#[repr(C, packed)]` layout satisfies `DUPFIXED` (all
70/// duplicate values for a key must have identical size). `DUPSORT` orders
71/// duplicates lexicographically over these raw bytes.
72#[derive(Clone, Copy, Debug, IntoBytes, FromBytes, Immutable)]
73#[repr(C, packed)]
74pub struct AdjEntry {
75    pub edge_type: TypeId, // 4 bytes
76    pub other: NodeId,     // 8 bytes: dst for out_adj, src for in_adj
77    pub edge_id: EdgeId,   // 8 bytes
78}
79
80impl DeepSizeOf for AdjEntry {
81    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
82        // AdjEntry is a fixed-size packed struct of primitives: no heap allocations.
83        0
84    }
85}
86
87/// Stored in the `nodes` LMDB sub-database as msgpack bytes.
88///
89/// A node carries a set of labels. The set may be empty (an unlabeled node) and
90/// labels are stored in insertion order. Use [`NodeRecord::primary_label`] when a
91/// single representative label is needed for display.
92#[derive(Debug, Clone, Serialize, Deserialize, DeepSizeOf)]
93pub struct NodeRecord {
94    pub labels: Vec<LabelId>,
95    pub props: Vec<u8>, // msgpack-encoded user properties
96}
97
98impl NodeRecord {
99    /// The first label assigned to the node, if any. Used where a single
100    /// representative label is needed (display, REST/MCP responses, vector hits).
101    pub fn primary_label(&self) -> Option<LabelId> {
102        self.labels.first().copied()
103    }
104
105    /// Returns true if the node carries the given label id.
106    pub fn has_label(&self, id: LabelId) -> bool {
107        self.labels.contains(&id)
108    }
109}
110
111/// Stored in the `edges` LMDB sub-database as msgpack bytes.
112#[derive(Debug, Clone, Serialize, Deserialize, DeepSizeOf)]
113pub struct EdgeRecord {
114    pub src: NodeId,
115    pub dst: NodeId,
116    pub edge_type: TypeId,
117    pub props: Vec<u8>, // msgpack-encoded user properties
118}
119
120/// The result of a single adjacency lookup entry returned by
121/// [`crate::Graph::out_neighbors`] and [`crate::Graph::in_neighbors`].
122#[derive(Debug, Clone, PartialEq)]
123pub struct NeighborEntry {
124    pub node: NodeId,
125    pub edge: EdgeId,
126    pub edge_type: TypeId,
127}
128
129/// A neighbor entry with a direction flag, returned by [`crate::Graph::all_neighbors`].
130#[derive(Debug, Clone, PartialEq)]
131pub struct DirectedNeighborEntry {
132    pub node: NodeId,
133    pub edge: EdgeId,
134    pub edge_type: TypeId,
135    /// `true` for outgoing edges, `false` for incoming.
136    pub outgoing: bool,
137}
138
139/// A path with an associated total weight, returned by weighted path algorithms.
140#[derive(Debug, Clone, PartialEq)]
141pub struct WeightedPath {
142    pub nodes: Vec<NodeId>,
143    pub total_weight: f64,
144}
145
146/// A typed property value used in index lookups and range queries.
147///
148/// Use this instead of raw `serde_json::Value` when querying nodes or edges
149/// by property.
150#[derive(Debug, Clone, PartialEq)]
151pub enum PropValue {
152    Bool(bool),
153    Int(i64),
154    Float(f64),
155    Str(String),
156}
157
158impl PropValue {
159    /// Convert to the `serde_json::Value` representation used in internal
160    /// property encoding.
161    pub(crate) fn into_json(self) -> serde_json::Value {
162        match self {
163            PropValue::Bool(b) => serde_json::Value::Bool(b),
164            PropValue::Int(i) => serde_json::Value::Number(i.into()),
165            PropValue::Float(f) => serde_json::json!(f),
166            PropValue::Str(s) => serde_json::Value::String(s),
167        }
168    }
169}
170
171impl From<bool> for PropValue {
172    fn from(v: bool) -> Self {
173        PropValue::Bool(v)
174    }
175}
176impl From<i64> for PropValue {
177    fn from(v: i64) -> Self {
178        PropValue::Int(v)
179    }
180}
181impl From<f64> for PropValue {
182    fn from(v: f64) -> Self {
183        PropValue::Float(v)
184    }
185}
186impl From<String> for PropValue {
187    fn from(v: String) -> Self {
188        PropValue::Str(v)
189    }
190}
191impl<'a> From<&'a str> for PropValue {
192    fn from(v: &'a str) -> Self {
193        PropValue::Str(v.to_string())
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn language_from_str_is_case_insensitive_and_rejects_unknown() {
203        assert_eq!("english".parse::<Language>().unwrap(), Language::English);
204        assert_eq!("German".parse::<Language>().unwrap(), Language::German);
205        assert_eq!(
206            "PORTUGUESE".parse::<Language>().unwrap(),
207            Language::Portuguese
208        );
209        assert!("klingon".parse::<Language>().is_err());
210    }
211}