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    /// Raw text.
69    pub content: String,
70    /// Event time.
71    pub occurred_at: TimestampMs,
72    /// Namespace.
73    pub group_id: String,
74    /// System time: recorded.
75    pub created_at: TimestampMs,
76}
77
78/// A connected fragment returned by traversal: nodes plus the edges that
79/// connect them, all valid at the requested instant.
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub struct Subgraph {
82    /// Nodes reached (including the seeds).
83    pub nodes: Vec<Node>,
84    /// Edges walked.
85    pub edges: Vec<Edge>,
86}
87
88pub(crate) fn uuid_col(row: &Row<'_>, idx: usize) -> rusqlite::Result<Uuid> {
89    let s: String = row.get(idx)?;
90    Uuid::parse_str(&s).map_err(|e| {
91        rusqlite::Error::FromSqlConversionFailure(idx, rusqlite::types::Type::Text, Box::new(e))
92    })
93}
94
95fn uuid_col_opt(row: &Row<'_>, idx: usize) -> rusqlite::Result<Option<Uuid>> {
96    let s: Option<String> = row.get(idx)?;
97    s.map(|s| {
98        Uuid::parse_str(&s).map_err(|e| {
99            rusqlite::Error::FromSqlConversionFailure(idx, rusqlite::types::Type::Text, Box::new(e))
100        })
101    })
102    .transpose()
103}
104
105fn json_col(row: &Row<'_>, idx: usize) -> rusqlite::Result<Json> {
106    let s: String = row.get(idx)?;
107    serde_json::from_str(&s).map_err(|e| {
108        rusqlite::Error::FromSqlConversionFailure(idx, rusqlite::types::Type::Text, Box::new(e))
109    })
110}
111
112/// Column list matching [`Node::from_row`]; keep the two in sync.
113pub(crate) const NODE_COLS: &str =
114    "id, kind, name, summary, attrs, group_id, created_at, expired_at, merged_into";
115
116impl Node {
117    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
118        Ok(Self {
119            id: uuid_col(row, 0)?,
120            kind: row.get(1)?,
121            name: row.get(2)?,
122            summary: row.get(3)?,
123            attrs: json_col(row, 4)?,
124            group_id: row.get(5)?,
125            created_at: row.get(6)?,
126            expired_at: row.get(7)?,
127            merged_into: uuid_col_opt(row, 8)?,
128        })
129    }
130}
131
132/// Column list matching [`Edge::from_row`]; keep the two in sync.
133pub(crate) const EDGE_COLS: &str =
134    "id, src, dst, rel, fact, attrs, group_id, valid_at, invalid_at, created_at, expired_at";
135
136impl Edge {
137    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
138        Ok(Self {
139            id: uuid_col(row, 0)?,
140            src: uuid_col(row, 1)?,
141            dst: uuid_col(row, 2)?,
142            rel: row.get(3)?,
143            fact: row.get(4)?,
144            attrs: json_col(row, 5)?,
145            group_id: row.get(6)?,
146            valid_at: row.get(7)?,
147            invalid_at: row.get(8)?,
148            created_at: row.get(9)?,
149            expired_at: row.get(10)?,
150        })
151    }
152}
153
154/// Column list matching [`Episode::from_row`]; keep the two in sync.
155pub(crate) const EPISODE_COLS: &str = "id, source, content, occurred_at, group_id, created_at";
156
157impl Episode {
158    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
159        Ok(Self {
160            id: uuid_col(row, 0)?,
161            source: row.get(1)?,
162            content: row.get(2)?,
163            occurred_at: row.get(3)?,
164            group_id: row.get(4)?,
165            created_at: row.get(5)?,
166        })
167    }
168}
169
170pub(crate) fn collect<T>(
171    rows: rusqlite::MappedRows<'_, impl FnMut(&Row<'_>) -> rusqlite::Result<T>>,
172) -> Result<Vec<T>> {
173    let mut out = Vec::new();
174    for row in rows {
175        out.push(row?);
176    }
177    Ok(out)
178}