Skip to main content

grit_core/
model.rs

1//! Read-side row types.
2
3use rusqlite::Row;
4use serde::{Deserialize, Serialize};
5use serde_json::Value as Json;
6use uuid::Uuid;
7
8use crate::clock::TimestampMs;
9use crate::error::Result;
10
11/// An entity node as stored.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct Node {
14    /// UUIDv7 identity.
15    pub id: Uuid,
16    /// Entity kind.
17    pub kind: String,
18    /// Display name.
19    pub name: String,
20    /// Summary text.
21    pub summary: String,
22    /// Arbitrary JSON attributes.
23    pub attrs: Json,
24    /// Namespace.
25    pub group_id: String,
26    /// System time: first believed.
27    pub created_at: TimestampMs,
28    /// System time: belief retracted (set by merge), `None` = current.
29    pub expired_at: Option<TimestampMs>,
30    /// If merged away, the absorbing node.
31    pub merged_into: Option<Uuid>,
32}
33
34/// A fact edge as stored (the "fat edge").
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct Edge {
37    /// UUIDv7 identity.
38    pub id: Uuid,
39    /// Source node.
40    pub src: Uuid,
41    /// Destination node.
42    pub dst: Uuid,
43    /// Relation label.
44    pub rel: String,
45    /// Natural-language fact sentence.
46    pub fact: String,
47    /// Arbitrary JSON attributes.
48    pub attrs: Json,
49    /// Namespace.
50    pub group_id: String,
51    /// Event time: fact became true.
52    pub valid_at: Option<TimestampMs>,
53    /// Event time: fact stopped being true.
54    pub invalid_at: Option<TimestampMs>,
55    /// System time: first believed.
56    pub created_at: TimestampMs,
57    /// System time: belief retracted.
58    pub expired_at: Option<TimestampMs>,
59}
60
61/// A provenance episode as stored.
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct Episode {
64    /// UUIDv7 identity.
65    pub id: Uuid,
66    /// Origin of the content.
67    pub source: String,
68    /// Free-form source-kind tag (e.g. `"message"`, `"text"`, `"json"`).
69    /// Opaque to grit — callers filter on it. Empty for episodes written
70    /// before schema v3.
71    #[serde(default)]
72    pub kind: String,
73    /// Raw text.
74    pub content: String,
75    /// Event time.
76    pub occurred_at: TimestampMs,
77    /// Namespace.
78    pub group_id: String,
79    /// System time: recorded.
80    pub created_at: TimestampMs,
81}
82
83/// A connected fragment returned by traversal: nodes plus the edges that
84/// connect them, all valid at the requested instant.
85#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct Subgraph {
87    /// Nodes reached (including the seeds).
88    pub nodes: Vec<Node>,
89    /// Edges walked.
90    pub edges: Vec<Edge>,
91}
92
93pub(crate) fn uuid_col(row: &Row<'_>, idx: usize) -> rusqlite::Result<Uuid> {
94    let s: String = row.get(idx)?;
95    Uuid::parse_str(&s).map_err(|e| {
96        rusqlite::Error::FromSqlConversionFailure(idx, rusqlite::types::Type::Text, Box::new(e))
97    })
98}
99
100fn uuid_col_opt(row: &Row<'_>, idx: usize) -> rusqlite::Result<Option<Uuid>> {
101    let s: Option<String> = row.get(idx)?;
102    s.map(|s| {
103        Uuid::parse_str(&s).map_err(|e| {
104            rusqlite::Error::FromSqlConversionFailure(idx, rusqlite::types::Type::Text, Box::new(e))
105        })
106    })
107    .transpose()
108}
109
110fn json_col(row: &Row<'_>, idx: usize) -> rusqlite::Result<Json> {
111    let s: String = row.get(idx)?;
112    serde_json::from_str(&s).map_err(|e| {
113        rusqlite::Error::FromSqlConversionFailure(idx, rusqlite::types::Type::Text, Box::new(e))
114    })
115}
116
117/// Column list matching [`Node::from_row`]; keep the two in sync.
118pub(crate) const NODE_COLS: &str =
119    "id, kind, name, summary, attrs, group_id, created_at, expired_at, merged_into";
120
121impl Node {
122    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
123        Ok(Self {
124            id: uuid_col(row, 0)?,
125            kind: row.get(1)?,
126            name: row.get(2)?,
127            summary: row.get(3)?,
128            attrs: json_col(row, 4)?,
129            group_id: row.get(5)?,
130            created_at: row.get(6)?,
131            expired_at: row.get(7)?,
132            merged_into: uuid_col_opt(row, 8)?,
133        })
134    }
135}
136
137/// Column list matching [`Edge::from_row`]; keep the two in sync.
138pub(crate) const EDGE_COLS: &str =
139    "id, src, dst, rel, fact, attrs, group_id, valid_at, invalid_at, created_at, expired_at";
140
141impl Edge {
142    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
143        Ok(Self {
144            id: uuid_col(row, 0)?,
145            src: uuid_col(row, 1)?,
146            dst: uuid_col(row, 2)?,
147            rel: row.get(3)?,
148            fact: row.get(4)?,
149            attrs: json_col(row, 5)?,
150            group_id: row.get(6)?,
151            valid_at: row.get(7)?,
152            invalid_at: row.get(8)?,
153            created_at: row.get(9)?,
154            expired_at: row.get(10)?,
155        })
156    }
157}
158
159/// Column list matching [`Episode::from_row`]; keep the two in sync.
160pub(crate) const EPISODE_COLS: &str =
161    "id, source, kind, content, occurred_at, group_id, created_at";
162
163impl Episode {
164    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
165        Ok(Self {
166            id: uuid_col(row, 0)?,
167            source: row.get(1)?,
168            kind: row.get(2)?,
169            content: row.get(3)?,
170            occurred_at: row.get(4)?,
171            group_id: row.get(5)?,
172            created_at: row.get(6)?,
173        })
174    }
175}
176
177pub(crate) fn collect<T>(
178    rows: rusqlite::MappedRows<'_, impl FnMut(&Row<'_>) -> rusqlite::Result<T>>,
179) -> Result<Vec<T>> {
180    let mut out = Vec::new();
181    for row in rows {
182        out.push(row?);
183    }
184    Ok(out)
185}