Skip to main content

sqlite_knowledge_graph/version/
snapshot.rs

1//! Version snapshot operations — add/remove entities and relations to/from versions.
2
3use rusqlite::params;
4
5use super::store;
6use crate::error::Result;
7
8/// Add an entity to a version (set the bit in its validity bitstring).
9pub fn version_add_entity(
10    conn: &rusqlite::Connection,
11    version_id: i64,
12    entity_id: i64,
13) -> Result<()> {
14    let bit = store::version_bit_for(conn, version_id)?;
15    store::ensure_entity_exists(conn, entity_id)?;
16
17    conn.execute(
18        "UPDATE kg_entities SET validity = COALESCE(validity, 0) | ?1 WHERE id = ?2",
19        params![bit, entity_id],
20    )?;
21    Ok(())
22}
23
24/// Remove an entity from a version (clear the bit). If result is 0, set to NULL.
25pub fn version_remove_entity(
26    conn: &rusqlite::Connection,
27    version_id: i64,
28    entity_id: i64,
29) -> Result<()> {
30    let bit = store::version_bit_for(conn, version_id)?;
31    store::ensure_entity_exists(conn, entity_id)?;
32
33    // Clear the bit, then check if result is 0 → set to NULL
34    conn.execute(
35        "UPDATE kg_entities SET validity = CASE \
36         WHEN (COALESCE(validity, 0) & ~?1) = 0 THEN NULL \
37         ELSE validity & ~?1 \
38         END \
39         WHERE id = ?2",
40        params![bit, entity_id],
41    )?;
42    Ok(())
43}
44
45/// Add a relation to a version.
46pub fn version_add_relation(
47    conn: &rusqlite::Connection,
48    version_id: i64,
49    relation_id: i64,
50) -> Result<()> {
51    let bit = store::version_bit_for(conn, version_id)?;
52    store::ensure_relation_exists(conn, relation_id)?;
53
54    conn.execute(
55        "UPDATE kg_relations SET validity = COALESCE(validity, 0) | ?1 WHERE id = ?2",
56        params![bit, relation_id],
57    )?;
58    Ok(())
59}
60
61/// Remove a relation from a version. If result is 0, set to NULL.
62pub fn version_remove_relation(
63    conn: &rusqlite::Connection,
64    version_id: i64,
65    relation_id: i64,
66) -> Result<()> {
67    let bit = store::version_bit_for(conn, version_id)?;
68    store::ensure_relation_exists(conn, relation_id)?;
69
70    conn.execute(
71        "UPDATE kg_relations SET validity = CASE \
72         WHEN (COALESCE(validity, 0) & ~?1) = 0 THEN NULL \
73         ELSE validity & ~?1 \
74         END \
75         WHERE id = ?2",
76        params![bit, relation_id],
77    )?;
78    Ok(())
79}
80
81/// Bulk add all entities to a version in a single operation.
82pub fn version_snapshot_entities(conn: &rusqlite::Connection, version_id: i64) -> Result<()> {
83    let bit = store::version_bit_for(conn, version_id)?;
84    conn.execute(
85        "UPDATE kg_entities SET validity = COALESCE(validity, 0) | ?1",
86        [bit],
87    )?;
88    Ok(())
89}
90
91/// Bulk add all relations to a version in a single operation.
92pub fn version_snapshot_relations(conn: &rusqlite::Connection, version_id: i64) -> Result<()> {
93    let bit = store::version_bit_for(conn, version_id)?;
94    conn.execute(
95        "UPDATE kg_relations SET validity = COALESCE(validity, 0) | ?1",
96        [bit],
97    )?;
98    Ok(())
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use rusqlite::Connection;
105
106    fn setup() -> Connection {
107        let conn = Connection::open_in_memory().unwrap();
108        crate::schema::create_schema(&conn).unwrap();
109        conn
110    }
111
112    fn insert_entity(conn: &Connection, name: &str) -> i64 {
113        conn.execute(
114            "INSERT INTO kg_entities (entity_type, name) VALUES ('test', ?1)",
115            [name],
116        )
117        .unwrap();
118        conn.last_insert_rowid()
119    }
120
121    fn insert_relation(conn: &Connection, src: i64, tgt: i64) -> i64 {
122        conn.execute(
123            "INSERT INTO kg_relations (source_id, target_id, rel_type) VALUES (?1, ?2, 'rel')",
124            rusqlite::params![src, tgt],
125        )
126        .unwrap();
127        conn.last_insert_rowid()
128    }
129
130    fn get_validity(conn: &Connection, table: &str, id: i64) -> Option<i64> {
131        conn.query_row(
132            &format!("SELECT validity FROM {table} WHERE id = ?1"),
133            [id],
134            |r| r.get(0),
135        )
136        .unwrap()
137    }
138
139    #[test]
140    fn test_add_unversioned_entity_to_version() {
141        let conn = setup();
142        let eid = insert_entity(&conn, "A");
143        let vid = super::super::store::create_version(&conn, "v1", "main", None, None).unwrap();
144
145        version_add_entity(&conn, vid, eid).unwrap();
146
147        let v = get_validity(&conn, "kg_entities", eid);
148        assert_eq!(v, Some(1)); // 0b1 — version 1 (bit 0)
149    }
150
151    #[test]
152    fn test_add_entity_to_additional_version() {
153        let conn = setup();
154        let eid = insert_entity(&conn, "A");
155        let v1 = super::super::store::create_version(&conn, "v1", "main", None, None).unwrap();
156        let v2 = super::super::store::create_version(&conn, "v2", "main", None, None).unwrap();
157
158        version_add_entity(&conn, v1, eid).unwrap();
159        version_add_entity(&conn, v2, eid).unwrap();
160
161        let v = get_validity(&conn, "kg_entities", eid);
162        assert_eq!(v, Some(0b11)); // versions 1 and 2
163    }
164
165    #[test]
166    fn test_remove_entity_from_one_of_multiple_versions() {
167        let conn = setup();
168        let eid = insert_entity(&conn, "A");
169        let v1 = super::super::store::create_version(&conn, "v1", "main", None, None).unwrap();
170        let v2 = super::super::store::create_version(&conn, "v2", "main", None, None).unwrap();
171
172        version_add_entity(&conn, v1, eid).unwrap();
173        version_add_entity(&conn, v2, eid).unwrap();
174        version_remove_entity(&conn, v1, eid).unwrap();
175
176        let v = get_validity(&conn, "kg_entities", eid);
177        assert_eq!(v, Some(0b10)); // only version 2
178    }
179
180    #[test]
181    fn test_remove_entity_from_only_version_returns_null() {
182        let conn = setup();
183        let eid = insert_entity(&conn, "A");
184        let v1 = super::super::store::create_version(&conn, "v1", "main", None, None).unwrap();
185
186        version_add_entity(&conn, v1, eid).unwrap();
187        version_remove_entity(&conn, v1, eid).unwrap();
188
189        let v = get_validity(&conn, "kg_entities", eid);
190        assert_eq!(v, None); // back to unversioned
191    }
192
193    #[test]
194    fn test_bulk_snapshot_entities() {
195        let conn = setup();
196        let e1 = insert_entity(&conn, "A");
197        let e2 = insert_entity(&conn, "B");
198        let vid = super::super::store::create_version(&conn, "v1", "main", None, None).unwrap();
199
200        version_snapshot_entities(&conn, vid).unwrap();
201
202        assert_eq!(get_validity(&conn, "kg_entities", e1), Some(1));
203        assert_eq!(get_validity(&conn, "kg_entities", e2), Some(1));
204    }
205
206    #[test]
207    fn test_add_relation_to_version() {
208        let conn = setup();
209        let e1 = insert_entity(&conn, "A");
210        let e2 = insert_entity(&conn, "B");
211        let rid = insert_relation(&conn, e1, e2);
212        let vid = super::super::store::create_version(&conn, "v1", "main", None, None).unwrap();
213
214        version_add_relation(&conn, vid, rid).unwrap();
215
216        let v = get_validity(&conn, "kg_relations", rid);
217        assert_eq!(v, Some(1));
218    }
219
220    #[test]
221    fn test_nonexistent_entity_error() {
222        let conn = setup();
223        let vid = super::super::store::create_version(&conn, "v1", "main", None, None).unwrap();
224        let err = version_add_entity(&conn, vid, 999).unwrap_err();
225        assert!(matches!(err, crate::error::Error::EntityNotFound(999)));
226    }
227
228    #[test]
229    fn test_nonexistent_version_error() {
230        let conn = setup();
231        let eid = insert_entity(&conn, "A");
232        let err = version_add_entity(&conn, 999, eid).unwrap_err();
233        assert!(matches!(err, crate::error::Error::VersionNotFound(999)));
234    }
235}