Skip to main content

sqlite_knowledge_graph/version/
diff.rs

1//! Version comparison and entity history.
2
3use super::store;
4use super::Version;
5use super::VersionDiff;
6use crate::error::Result;
7use crate::graph::entity::Entity;
8use crate::graph::relation::Relation;
9
10/// Compare two versions and return added/removed/common entities and relations.
11pub fn version_compare(conn: &rusqlite::Connection, v1_id: i64, v2_id: i64) -> Result<VersionDiff> {
12    store::ensure_version_exists(conn, v1_id)?;
13    store::ensure_version_exists(conn, v2_id)?;
14
15    let ents1 = entity_ids_in_version(conn, v1_id)?;
16    let ents2 = entity_ids_in_version(conn, v2_id)?;
17
18    let rels1 = relation_ids_in_version(conn, v1_id)?;
19    let rels2 = relation_ids_in_version(conn, v2_id)?;
20
21    let (added_e, removed_e, common_e) = partition(&ents1, &ents2);
22    let (added_r, removed_r, common_r) = partition(&rels1, &rels2);
23
24    Ok(VersionDiff {
25        added_entities: load_entities(conn, &added_e)?,
26        removed_entities: load_entities(conn, &removed_e)?,
27        common_entities: load_entities(conn, &common_e)?,
28        added_relations: load_relations(conn, &added_r)?,
29        removed_relations: load_relations(conn, &removed_r)?,
30        common_relations: load_relations(conn, &common_r)?,
31    })
32}
33
34/// Return all versions that contain a given entity.
35pub fn version_entity_history(conn: &rusqlite::Connection, entity_id: i64) -> Result<Vec<Version>> {
36    store::ensure_entity_exists(conn, entity_id)?;
37
38    let validity: Option<i64> = conn.query_row(
39        "SELECT validity FROM kg_entities WHERE id = ?1",
40        [entity_id],
41        |r| r.get(0),
42    )?;
43
44    let Some(bits) = validity else {
45        return Ok(Vec::new());
46    };
47
48    // Resolve the set bits back to their owning versions (newest first).
49    store::versions_for_bits(conn, bits)
50}
51
52fn entity_ids_in_version(conn: &rusqlite::Connection, version_id: i64) -> Result<Vec<i64>> {
53    let bit = store::version_bit_for(conn, version_id)?;
54    let mut stmt = conn.prepare("SELECT id FROM kg_entities WHERE (validity & ?1) != 0")?;
55    let ids: Vec<i64> = stmt
56        .query_map([bit], |r| r.get(0))?
57        .filter_map(|r| r.ok())
58        .collect();
59    Ok(ids)
60}
61
62fn relation_ids_in_version(conn: &rusqlite::Connection, version_id: i64) -> Result<Vec<i64>> {
63    let bit = store::version_bit_for(conn, version_id)?;
64    let mut stmt = conn.prepare("SELECT id FROM kg_relations WHERE (validity & ?1) != 0")?;
65    let ids: Vec<i64> = stmt
66        .query_map([bit], |r| r.get(0))?
67        .filter_map(|r| r.ok())
68        .collect();
69    Ok(ids)
70}
71
72fn partition(ids1: &[i64], ids2: &[i64]) -> (Vec<i64>, Vec<i64>, Vec<i64>) {
73    let set1: std::collections::HashSet<i64> = ids1.iter().copied().collect();
74    let set2: std::collections::HashSet<i64> = ids2.iter().copied().collect();
75
76    let added: Vec<i64> = set2.difference(&set1).copied().collect();
77    let removed: Vec<i64> = set1.difference(&set2).copied().collect();
78    let common: Vec<i64> = set1.intersection(&set2).copied().collect();
79
80    (added, removed, common)
81}
82
83fn load_entities(conn: &rusqlite::Connection, ids: &[i64]) -> Result<Vec<Entity>> {
84    crate::graph::entity::get_entities_by_ids(conn, ids)
85}
86
87fn load_relations(conn: &rusqlite::Connection, ids: &[i64]) -> Result<Vec<Relation>> {
88    let mut stmt = conn.prepare(
89        "SELECT id, source_id, target_id, rel_type, weight, properties, created_at \
90         FROM kg_relations WHERE id = ?1",
91    )?;
92    let mut result = Vec::new();
93    for &id in ids {
94        let rel = stmt.query_row([id], |row| {
95            let props_json: Option<String> = row.get(5)?;
96            let properties = props_json
97                .and_then(|j| serde_json::from_str(&j).ok())
98                .unwrap_or_default();
99            Ok(Relation {
100                id: Some(row.get(0)?),
101                source_id: row.get(1)?,
102                target_id: row.get(2)?,
103                rel_type: row.get(3)?,
104                weight: crate::row_get_weight(row, 4)?,
105                properties,
106                created_at: row.get(6)?,
107            })
108        })?;
109        result.push(rel);
110    }
111    Ok(result)
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use rusqlite::Connection;
118
119    fn setup() -> Connection {
120        let conn = Connection::open_in_memory().unwrap();
121        crate::schema::create_schema(&conn).unwrap();
122        conn
123    }
124
125    fn add_entity(conn: &Connection, name: &str) -> i64 {
126        conn.execute(
127            "INSERT INTO kg_entities (entity_type, name) VALUES ('test', ?1)",
128            [name],
129        )
130        .unwrap();
131        conn.last_insert_rowid()
132    }
133
134    fn add_relation(conn: &Connection, src: i64, tgt: i64) -> i64 {
135        conn.execute(
136            "INSERT INTO kg_relations (source_id, target_id, rel_type) VALUES (?1, ?2, 'rel')",
137            rusqlite::params![src, tgt],
138        )
139        .unwrap();
140        conn.last_insert_rowid()
141    }
142
143    fn make_version(conn: &Connection, name: &str) -> i64 {
144        super::super::store::create_version(conn, name, "main", None, None).unwrap()
145    }
146
147    fn set_validity(conn: &Connection, table: &str, id: i64, val: i64) {
148        conn.execute(
149            &format!("UPDATE {table} SET validity = ?1 WHERE id = ?2"),
150            rusqlite::params![val, id],
151        )
152        .unwrap();
153    }
154
155    #[test]
156    fn test_added_entities() {
157        let conn = setup();
158        let e1 = add_entity(&conn, "A");
159        let e2 = add_entity(&conn, "B");
160        let e3 = add_entity(&conn, "C");
161        let v1 = make_version(&conn, "v1");
162        let v2 = make_version(&conn, "v2");
163
164        set_validity(&conn, "kg_entities", e1, 0b01); // v1
165        set_validity(&conn, "kg_entities", e2, 0b11); // v1 + v2
166        set_validity(&conn, "kg_entities", e3, 0b10); // v2
167
168        let diff = version_compare(&conn, v1, v2).unwrap();
169        assert_eq!(diff.added_entities.len(), 1);
170        assert_eq!(diff.added_entities[0].name, "C");
171        assert_eq!(diff.removed_entities.len(), 1);
172        assert_eq!(diff.removed_entities[0].name, "A");
173        assert_eq!(diff.common_entities.len(), 1);
174        assert_eq!(diff.common_entities[0].name, "B");
175    }
176
177    #[test]
178    fn test_relations_diff() {
179        let conn = setup();
180        let e1 = add_entity(&conn, "A");
181        let e2 = add_entity(&conn, "B");
182        let e3 = add_entity(&conn, "C");
183        let r1 = add_relation(&conn, e1, e2);
184        let r2 = add_relation(&conn, e2, e3);
185        let v1 = make_version(&conn, "v1");
186        let v2 = make_version(&conn, "v2");
187
188        set_validity(&conn, "kg_relations", r1, 0b11); // both
189        set_validity(&conn, "kg_relations", r2, 0b10); // v2 only
190
191        let diff = version_compare(&conn, v1, v2).unwrap();
192        assert_eq!(diff.added_relations.len(), 1);
193        assert_eq!(diff.common_relations.len(), 1);
194    }
195
196    #[test]
197    fn test_entity_history_multi_version() {
198        let conn = setup();
199        let e1 = add_entity(&conn, "A");
200        let v1 = make_version(&conn, "v1");
201        let v2 = make_version(&conn, "v2");
202
203        // validity = bit for v1 | bit for v2
204        let validity = super::super::store::version_bit_for(&conn, v1).unwrap()
205            | super::super::store::version_bit_for(&conn, v2).unwrap();
206        set_validity(&conn, "kg_entities", e1, validity);
207
208        let history = version_entity_history(&conn, e1).unwrap();
209        assert_eq!(history.len(), 2);
210        let ids: Vec<i64> = history.iter().map(|v| v.id).collect();
211        assert!(ids.contains(&v1));
212        assert!(ids.contains(&v2));
213    }
214
215    #[test]
216    fn test_entity_history_unversioned() {
217        let conn = setup();
218        let e1 = add_entity(&conn, "A");
219
220        let history = version_entity_history(&conn, e1).unwrap();
221        assert!(history.is_empty());
222    }
223}