sqlite_knowledge_graph/version/
diff.rs1use super::store;
4use super::Version;
5use super::VersionDiff;
6use crate::error::Result;
7use crate::graph::entity::Entity;
8use crate::graph::relation::Relation;
9
10pub 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
34pub 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 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); set_validity(&conn, "kg_entities", e2, 0b11); set_validity(&conn, "kg_entities", e3, 0b10); 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); set_validity(&conn, "kg_relations", r2, 0b10); 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 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}