1use rustc_hash::FxHashMap;
2use std::collections::{HashSet, VecDeque};
3use std::num::NonZeroUsize;
4use std::path::Path;
5use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
6use std::time::Duration;
7
8use parking_lot::{Mutex, MutexGuard};
9use rusqlite::{Connection, OpenFlags, params};
10
11use crate::errors::{MCSError, Result};
12use crate::mutation::{
13 MutationContext, MutationRequest, MutationResult, MutationService, ObservationUpdate,
14};
15use crate::storage::{Durability, SqliteTuning};
16use crate::types::{
17 Degree, Entity, EntityDescription, EntityInput, Observation, ObservationInput, Relation,
18};
19
20const OBSERVATION_JSON: &str = "json_object('body',o.body,'createdAtUs',o.created_us,'occurredAtUs',o.occurred_us,'originEntityName',o.origin_entity_name)";
22
23const MAX_TRAVERSAL_ENTITIES: usize = 500_000;
26const MAX_TRAVERSAL_RELS: usize = 2_000_000;
27
28fn sqlite_err(e: rusqlite::Error) -> MCSError {
29 MCSError::IoError(std::io::Error::other(e))
30}
31
32const fn is_not_found(e: &rusqlite::Error) -> bool {
33 matches!(e, rusqlite::Error::QueryReturnedNoRows)
34}
35
36#[inline(always)]
37pub fn name_hash(name: &str) -> i64 {
38 let mut h: u64 = 0xcbf29ce484222325;
39 for b in name.bytes() {
40 h ^= u64::from(b);
41 h = h.wrapping_mul(0x100000001b3);
42 }
43 h as i64
44}
45
46fn entity_name_lookup(conn: &Connection, name: &str) -> Result<Option<i64>> {
47 let h = name_hash(name);
48 let mut stmt = conn
49 .prepare_cached("SELECT id FROM entity WHERE name_hash = ?1 AND name = ?2 AND flags = 0")
50 .map_err(sqlite_err)?;
51 match stmt.query_row(params![h, name], |row| row.get::<_, i64>(0)) {
52 Ok(id) => Ok(Some(id)),
53 Err(e) if is_not_found(&e) => Ok(None),
54 Err(e) => Err(sqlite_err(e)),
55 }
56}
57
58fn lookup_type_id(conn: &Connection, type_name: &str, kind: i64) -> Option<i64> {
62 conn.prepare_cached("SELECT id FROM type_dict WHERE kind = ?1 AND name = ?2")
63 .ok()?
64 .query_row(params![kind, type_name], |row| row.get::<_, i64>(0))
65 .ok()
66}
67
68fn read_graph_stat(conn: &Connection, key: &str) -> Result<i64> {
69 conn.query_row(
70 "SELECT value FROM graph_stat WHERE key = ?1",
71 params![key],
72 |row| row.get(0),
73 )
74 .map_err(sqlite_err)
75}
76
77fn select_all_types(conn: &Connection, kind: i64) -> Result<Vec<(String, usize)>> {
78 let mut stmt = conn
79 .prepare_cached(
80 "SELECT name, count FROM type_dict WHERE kind = ?1 AND count > 0 ORDER BY count DESC",
81 )
82 .map_err(sqlite_err)?;
83 let rows = stmt
84 .query_map(params![kind], |row| {
85 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
86 })
87 .map_err(sqlite_err)?
88 .filter_map(|r| r.ok())
89 .collect();
90 Ok(rows)
91}
92
93fn select_type_catalog(
98 conn: &Connection,
99 kind: i64,
100) -> Result<Vec<(String, usize, Option<String>)>> {
101 let mut stmt = conn
102 .prepare_cached(
103 "SELECT name, count, desc FROM type_dict
104 WHERE kind = ?1 AND (count > 0 OR desc IS NOT NULL)
105 ORDER BY count DESC",
106 )
107 .map_err(sqlite_err)?;
108 let rows = stmt
109 .query_map(params![kind], |row| {
110 Ok((
111 row.get::<_, String>(0)?,
112 row.get::<_, i64>(1)? as usize,
113 row.get::<_, Option<String>>(2)?,
114 ))
115 })
116 .map_err(sqlite_err)?
117 .filter_map(|r| r.ok())
118 .collect();
119 Ok(rows)
120}
121
122fn int_csv(ids: &[i64]) -> String {
130 use std::fmt::Write as _;
131 let mut s = String::with_capacity(ids.len() * 8);
132 for (i, id) in ids.iter().enumerate() {
133 if i > 0 {
134 s.push(',');
135 }
136 let _ = write!(s, "{id}");
137 }
138 s
139}
140
141fn rel_values_literal(rels: &HashSet<(i64, i64, i64)>) -> String {
147 use std::fmt::Write as _;
148 let mut s = String::with_capacity(rels.len() * 16);
149 for (i, (f, t, tp)) in rels.iter().enumerate() {
150 if i > 0 {
151 s.push(',');
152 }
153 let _ = write!(s, "({f},{t},{tp})");
154 }
155 s
156}
157
158fn batch_entities_by_ids(conn: &Connection, ids: &[i64]) -> FxHashMap<i64, Entity> {
162 let mut map = FxHashMap::default();
163 if ids.is_empty() {
164 return map;
165 }
166 let sql = format!(
167 "SELECT e.id, e.name, t.name,
168 COALESCE((SELECT json_group_array({OBSERVATION_JSON} ORDER BY o.idx, o.id)
169 FROM observation o WHERE o.entity_id = e.id), '[]')
170 FROM entity e JOIN type_dict t ON t.id = e.type_id
171 WHERE e.id IN ({}) AND e.flags = 0",
172 int_csv(ids)
173 );
174 if let Ok(mut stmt) = conn.prepare(&sql)
175 && let Ok(rows) = stmt.query_map([], |row| {
176 Ok((
177 row.get::<_, i64>(0)?,
178 row.get::<_, String>(1)?,
179 row.get::<_, String>(2)?,
180 row.get::<_, String>(3)?,
181 ))
182 })
183 {
184 for (id, name, etype, obs_json) in rows.flatten() {
185 let observations: Vec<Observation> =
186 serde_json::from_str(&obs_json).unwrap_or_default();
187 map.insert(
188 id,
189 Entity {
190 name,
191 entity_type: etype,
192 observations,
193 },
194 );
195 }
196 }
197 map
198}
199
200fn batch_entity_lite_by_ids(
206 conn: &Connection,
207 ids: &[i64],
208) -> FxHashMap<i64, (String, String, i64)> {
209 let mut map = FxHashMap::default();
210 if ids.is_empty() {
211 return map;
212 }
213 let sql = format!(
214 "SELECT e.id, e.name, t.name, e.obs_count
215 FROM entity e JOIN type_dict t ON t.id = e.type_id
216 WHERE e.id IN ({}) AND e.flags = 0",
217 int_csv(ids)
218 );
219 if let Ok(mut stmt) = conn.prepare(&sql)
220 && let Ok(rows) = stmt.query_map([], |row| {
221 Ok((
222 row.get::<_, i64>(0)?,
223 row.get::<_, String>(1)?,
224 row.get::<_, String>(2)?,
225 row.get::<_, i64>(3)?,
226 ))
227 })
228 {
229 for (id, name, etype, oc) in rows.flatten() {
230 map.insert(id, (name, etype, oc));
231 }
232 }
233 map
234}
235
236fn fts_candidate_ids(conn: &Connection, query: &str, cap: usize) -> Vec<i64> {
240 let mut ids: Vec<i64> = Vec::new();
241 let mut seen: HashSet<i64> = HashSet::new();
242 let cap_i64 = cap as i64;
243
244 if let Ok(mut stmt) =
245 conn.prepare("SELECT rowid FROM name_fts WHERE name_fts MATCH ?1 ORDER BY rank LIMIT ?2")
246 && let Ok(rows) = stmt.query_map(params![query, cap_i64], |row| row.get::<_, i64>(0))
247 {
248 for id in rows.flatten() {
249 if seen.insert(id) {
250 ids.push(id);
251 }
252 }
253 }
254
255 if let Ok(mut stmt) = conn.prepare(
256 "SELECT entity_id FROM obs_fts JOIN observation ON obs_fts.rowid = observation.id
257 WHERE obs_fts MATCH ?1
258 GROUP BY entity_id
259 LIMIT ?2",
260 ) && let Ok(rows) = stmt.query_map(params![query, cap_i64], |row| row.get::<_, i64>(0))
261 {
262 for id in rows.flatten() {
263 if seen.insert(id) {
264 ids.push(id);
265 }
266 }
267 }
268
269 ids
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum Direction {
275 Outgoing,
276 Incoming,
277 Both,
278}
279
280impl Direction {
281 pub fn parse(s: Option<&str>) -> Self {
282 match s {
283 Some("OUTGOING") => Direction::Outgoing,
284 Some("INCOMING") => Direction::Incoming,
285 _ => Direction::Both,
286 }
287 }
288}
289
290pub fn push_json_str(buf: &mut String, raw: &str) {
293 buf.push('"');
294 let mut start = 0;
295 let bytes = raw.as_bytes();
296 for (i, &b) in bytes.iter().enumerate() {
297 let esc: u8 = match b {
298 b'"' => b'"',
299 b'\\' => b'\\',
300 b'\n' => b'n',
301 b'\r' => b'r',
302 b'\t' => b't',
303 0x08 => b'b',
304 0x0C => b'f',
305 0x00..=0x07 | 0x0B | 0x0E..=0x1F => continue, _ => continue,
307 };
308 buf.push_str(&raw[start..i]);
309 buf.push('\\');
310 buf.push(esc as char);
311 start = i + 1;
312 }
313 for (i, &b) in bytes.iter().enumerate().skip(start) {
315 if b <= 0x07 || b == 0x0B || (0x0E..=0x1F).contains(&b) {
316 buf.push_str(&raw[start..i]);
317 write_escape_unicode(buf, b);
318 start = i + 1;
319 }
320 }
321 buf.push_str(&raw[start..]);
322 buf.push('"');
323}
324
325#[inline(never)]
326fn write_escape_unicode(buf: &mut String, b: u8) {
327 use std::fmt::Write;
328 write!(buf, "\\u{:04x}", b).unwrap();
329}
330
331pub(crate) struct TxGuard<'a> {
334 conn: &'a Connection,
335 done: bool,
336}
337
338impl<'a> TxGuard<'a> {
339 pub(crate) fn begin(conn: &'a Connection) -> Result<Self> {
340 conn.execute_batch("BEGIN IMMEDIATE").map_err(sqlite_err)?;
345 Ok(Self { conn, done: false })
346 }
347
348 pub(crate) fn commit(mut self) -> Result<()> {
349 self.conn.execute_batch("COMMIT").map_err(sqlite_err)?;
350 self.done = true;
351 Ok(())
352 }
353}
354
355impl Drop for TxGuard<'_> {
356 fn drop(&mut self) {
357 if !self.done {
358 let _ = self.conn.execute_batch("ROLLBACK");
359 }
360 }
361}
362
363struct ReaderPool {
370 conns: Vec<Mutex<Connection>>,
371 next: AtomicUsize,
372}
373
374impl ReaderPool {
375 fn get(&self) -> MutexGuard<'_, Connection> {
379 for c in &self.conns {
380 if let Some(g) = c.try_lock() {
381 return g;
382 }
383 }
384 let i = self.next.fetch_add(1, Ordering::Relaxed) % self.conns.len();
385 self.conns[i].lock()
386 }
387}
388
389pub struct GraphHandle {
392 pub(crate) writer: Mutex<Connection>,
395 readers: ReaderPool,
397 seq_entity: AtomicI64,
398 seq_obs: AtomicI64,
399}
400
401fn open_reader(path: &Path, tuning: &SqliteTuning) -> Result<Connection> {
408 let conn = Connection::open_with_flags(
409 path,
410 OpenFlags::SQLITE_OPEN_READ_WRITE
411 | OpenFlags::SQLITE_OPEN_NO_MUTEX
412 | OpenFlags::SQLITE_OPEN_URI,
413 )
414 .map_err(sqlite_err)?;
415 conn.busy_timeout(Duration::from_millis(tuning.busy_timeout_ms))
416 .map_err(sqlite_err)?;
417 conn.execute_batch(&format!(
418 "PRAGMA query_only = ON;
419 PRAGMA cache_size = -{};
420 PRAGMA temp_store = MEMORY;
421 PRAGMA mmap_size = {};",
422 tuning.cache_size_kb, tuning.mmap_size
423 ))
424 .map_err(sqlite_err)?;
425 Ok(conn)
426}
427
428impl GraphHandle {
429 pub fn new(
430 path: &Path,
431 durability: Durability,
432 tuning: SqliteTuning,
433 _lru_cache_size: NonZeroUsize,
434 read_pool_size: usize,
435 ) -> Result<Self> {
436 let conn = Connection::open(path).map_err(sqlite_err)?;
437 conn.busy_timeout(Duration::from_millis(tuning.busy_timeout_ms))
440 .map_err(sqlite_err)?;
441
442 conn.execute_batch(&format!(
448 "PRAGMA page_size = {};
449 PRAGMA auto_vacuum = INCREMENTAL;",
450 tuning.page_size
451 ))
452 .map_err(sqlite_err)?;
453
454 conn.execute_batch(&format!(
455 "PRAGMA journal_mode = WAL;
456 PRAGMA foreign_keys = OFF;
457 PRAGMA cache_size = -{};
458 PRAGMA temp_store = MEMORY;
459 PRAGMA busy_timeout = {};
460 PRAGMA synchronous = NORMAL;
461 PRAGMA journal_size_limit = {};",
462 tuning.cache_size_kb, tuning.busy_timeout_ms, tuning.journal_size_limit
463 ))
464 .map_err(sqlite_err)?;
465
466 crate::schema::initialize_database(&conn)?;
467
468 conn.execute_batch(&format!("PRAGMA mmap_size = {};", tuning.mmap_size))
469 .map_err(sqlite_err)?;
470
471 let sync_pragma = match durability {
472 Durability::Sync => "PRAGMA synchronous = FULL",
473 Durability::Async => "PRAGMA synchronous = NORMAL",
474 };
475 conn.execute_batch(sync_pragma).map_err(sqlite_err)?;
476
477 conn.execute_batch("PRAGMA analysis_limit = 400;")
480 .map_err(sqlite_err)?;
481
482 conn.execute_batch("PRAGMA optimize;").map_err(sqlite_err)?;
483
484 let seq_entity = read_graph_stat(&conn, "entity_seq").unwrap_or(0);
485 let seq_obs = read_graph_stat(&conn, "obs_seq").unwrap_or(0);
486
487 let pool_size = read_pool_size.max(1);
490 let mut conns = Vec::with_capacity(pool_size);
491 for _ in 0..pool_size {
492 conns.push(Mutex::new(open_reader(path, &tuning)?));
493 }
494 let readers = ReaderPool {
495 conns,
496 next: AtomicUsize::new(0),
497 };
498
499 Ok(Self {
500 writer: Mutex::new(conn),
501 readers,
502 seq_entity: AtomicI64::new(seq_entity),
503 seq_obs: AtomicI64::new(seq_obs),
504 })
505 }
506
507 pub(crate) fn next_entity_id(&self) -> i64 {
508 self.seq_entity.fetch_add(1, Ordering::Relaxed) + 1
509 }
510
511 pub(crate) fn refresh_seqs(&self, conn: &Connection) -> Result<()> {
514 self.seq_entity
515 .fetch_max(read_graph_stat(conn, "entity_seq")?, Ordering::Relaxed);
516 self.seq_obs
517 .fetch_max(read_graph_stat(conn, "obs_seq")?, Ordering::Relaxed);
518 Ok(())
519 }
520
521 pub(crate) fn next_obs_id(&self) -> i64 {
522 self.seq_obs.fetch_add(1, Ordering::Relaxed) + 1
523 }
524
525 fn get_entity_id(&self, conn: &Connection, name: &str) -> Result<Option<(i64, i64, i64, i64)>> {
526 use rusqlite::OptionalExtension;
527 conn.query_row(
528 "SELECT id, type_id, out_deg, in_deg FROM entity WHERE name_hash = ?1 AND name = ?2 AND flags = 0",
529 params![name_hash(name), name],
530 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
531 ).optional().map_err(sqlite_err)
532 }
533
534 pub(crate) fn sync_seqs(&self, conn: &Connection) -> Result<()> {
535 let seq_e = self.seq_entity.load(Ordering::Relaxed);
536 let seq_o = self.seq_obs.load(Ordering::Relaxed);
537 conn.execute(
538 "UPDATE graph_stat SET value = CASE key WHEN 'entity_seq' THEN ?1 WHEN 'obs_seq' THEN ?2 ELSE value END
539 WHERE key IN ('entity_seq', 'obs_seq')",
540 params![seq_e, seq_o],
541 )
542 .map_err(sqlite_err)?;
543 Ok(())
544 }
545
546 pub fn get_entity(&self, name: &str) -> Result<Option<Entity>> {
549 let conn = self.readers.get();
550 let tx = conn.unchecked_transaction().map_err(sqlite_err)?;
553 let entity = crate::mutation::read_entity(&tx, name)?.map(|snapshot| snapshot.entity());
554 tx.commit().map_err(sqlite_err)?;
555 Ok(entity)
556 }
557
558 fn mutate(&self, request: MutationRequest) -> Result<MutationResult> {
559 MutationService::new(self)
560 .apply_with_result(request, MutationContext::local())
561 .map(|(_, result)| result)
562 }
563
564 pub fn create_entities(&self, entities: &[EntityInput]) -> Result<Vec<Entity>> {
565 match self.mutate(MutationRequest::CreateEntities {
566 entities: entities.to_vec(),
567 })? {
568 MutationResult::Entities(result) => Ok(result),
569 _ => unreachable!("create_entities always returns entities"),
570 }
571 }
572
573 pub fn upsert_entities(&self, entities: &[EntityInput]) -> Result<Vec<Entity>> {
574 match self.mutate(MutationRequest::UpsertEntities {
575 entities: entities.to_vec(),
576 })? {
577 MutationResult::Entities(result) => Ok(result),
578 _ => unreachable!("upsert_entities always returns entities"),
579 }
580 }
581
582 pub fn delete_entities(&self, names: &[String]) -> Result<()> {
583 self.mutate(MutationRequest::DeleteEntities {
584 names: names.to_vec(),
585 })
586 .map(|_| ())
587 }
588
589 pub fn create_relations(&self, relations: &[Relation]) -> Result<Vec<Relation>> {
590 match self.mutate(MutationRequest::CreateRelations {
591 relations: relations.to_vec(),
592 })? {
593 MutationResult::Relations(result) => Ok(result),
594 _ => unreachable!("create_relations always returns relations"),
595 }
596 }
597
598 pub fn delete_relations(&self, relations: &[Relation]) -> Result<()> {
599 self.mutate(MutationRequest::DeleteRelations {
600 relations: relations.to_vec(),
601 })
602 .map(|_| ())
603 }
604
605 pub fn add_observations(
606 &self,
607 entity_name: &str,
608 contents: &[ObservationInput],
609 ) -> Result<Vec<Observation>> {
610 match self.mutate(MutationRequest::AddObservations {
611 observations: vec![ObservationUpdate {
612 entity_name: entity_name.into(),
613 contents: contents.to_vec(),
614 }],
615 })? {
616 MutationResult::Observations(mut result) => Ok(result.remove(0).added_observations),
617 _ => unreachable!("add_observations always returns observations"),
618 }
619 }
620
621 pub fn delete_observations(
622 &self,
623 entity_name: &str,
624 observations: &[ObservationInput],
625 ) -> Result<()> {
626 self.mutate(MutationRequest::DeleteObservations {
627 observations: vec![ObservationUpdate {
628 entity_name: entity_name.into(),
629 contents: observations.to_vec(),
630 }],
631 })
632 .map(|_| ())
633 }
634
635 pub fn merge_entities(&self, source: &str, target: &str) -> Result<Entity> {
636 match self.mutate(MutationRequest::MergeEntities {
637 source: source.into(),
638 target: target.into(),
639 })? {
640 MutationResult::Entity(result) => Ok(result),
641 _ => unreachable!("merge_entities always returns an entity"),
642 }
643 }
644
645 pub fn rename_entity(&self, old_name: &str, new_name: &str) -> Result<Entity> {
646 match self.mutate(MutationRequest::RenameEntity {
647 old_name: old_name.into(),
648 new_name: new_name.into(),
649 })? {
650 MutationResult::Entity(result) => Ok(result),
651 _ => unreachable!("rename_entity always returns an entity"),
652 }
653 }
654
655 pub fn code_purge_file(&self, rel_path: &str) -> Result<usize> {
657 match self.mutate(MutationRequest::PurgeDefinedEntities {
658 name: rel_path.into(),
659 })? {
660 MutationResult::Count(count) => Ok(count),
661 _ => unreachable!("purge always returns a count"),
662 }
663 }
664
665 pub fn search_nodes_filtered(
666 &self,
667 query: &str,
668 filter_type: Option<&str>,
669 offset: usize,
670 limit: usize,
671 ) -> Vec<Entity> {
672 if query.is_empty() {
673 return Vec::new();
674 }
675 let conn = self.readers.get();
676
677 let cap = offset.saturating_add(limit);
682 let candidates = fts_candidate_ids(&conn, query, cap);
683 let mut by_id = batch_entities_by_ids(&conn, &candidates);
684
685 let mut results = Vec::new();
686 let mut count: usize = 0;
687 for eid in candidates {
688 let Some(entity) = by_id.remove(&eid) else {
689 continue;
690 };
691 if let Some(ft) = filter_type
692 && !ft.is_empty()
693 && entity.entity_type != ft
694 {
695 continue;
696 }
697 if count < offset {
698 count += 1;
699 continue;
700 }
701 if results.len() >= limit {
702 break;
703 }
704 results.push(entity);
705 count += 1;
706 }
707
708 results
709 }
710
711 pub fn search_nodes_lite_json(
718 &self,
719 query: &str,
720 filter_type: Option<&str>,
721 offset: usize,
722 limit: usize,
723 ) -> (String, usize, bool) {
724 use std::fmt::Write as _;
725 if query.is_empty() {
726 return ("[]".to_string(), 0, false);
727 }
728 let conn = self.readers.get();
729 let cap = offset.saturating_add(limit).saturating_add(1);
731 let candidates = fts_candidate_ids(&conn, query, cap);
732 let by_id = batch_entity_lite_by_ids(&conn, &candidates);
733 let ft = filter_type.filter(|s| !s.is_empty());
734
735 let mut arr = String::from("[");
736 let mut count: usize = 0; let mut returned: usize = 0;
738 let mut has_more = false;
739 for eid in candidates {
740 let Some((name, etype, oc)) = by_id.get(&eid) else {
741 continue;
742 };
743 if let Some(f) = ft
744 && etype != f
745 {
746 continue;
747 }
748 if count < offset {
749 count += 1;
750 continue;
751 }
752 if returned >= limit {
753 has_more = true;
754 break;
755 }
756 if returned > 0 {
757 arr.push(',');
758 }
759 arr.push_str("{\"name\":");
760 push_json_str(&mut arr, name);
761 arr.push_str(",\"entityType\":");
762 push_json_str(&mut arr, etype);
763 let _ = write!(arr, ",\"obsCount\":{oc}}}");
764 returned += 1;
765 count += 1;
766 }
767 arr.push(']');
768 (arr, returned, has_more)
769 }
770
771 pub fn read_graph_filtered(
772 &self,
773 filter_type: Option<&str>,
774 offset: usize,
775 limit: usize,
776 ) -> Result<String> {
777 self.read_graph_page(filter_type, offset, limit, true)
778 .map(|(json, _)| json)
779 }
780
781 pub fn read_graph_filtered_lite(
787 &self,
788 filter_type: Option<&str>,
789 offset: usize,
790 limit: usize,
791 ) -> Result<(String, usize)> {
792 self.read_graph_page(filter_type, offset, limit, false)
793 }
794
795 fn read_graph_page(
796 &self,
797 filter_type: Option<&str>,
798 offset: usize,
799 limit: usize,
800 include_obs: bool,
801 ) -> Result<(String, usize)> {
802 let conn = self.readers.get();
803
804 let limit_sql: i64 = if limit == usize::MAX {
805 -1
806 } else {
807 limit.min(i64::MAX as usize) as i64
808 };
809 let offset_sql: i64 = offset as i64;
810
811 let filter = filter_type.filter(|ft| !ft.is_empty());
817 let ids: Vec<i64> = if let Some(ft) = filter {
818 let mut stmt = conn
819 .prepare_cached(
820 "SELECT e.id FROM entity e
821 WHERE e.type_id = (SELECT id FROM type_dict WHERE kind = 0 AND name = ?1)
822 AND e.flags = 0
823 ORDER BY e.id LIMIT ?2 OFFSET ?3",
824 )
825 .map_err(sqlite_err)?;
826 stmt.query_map(params![ft, limit_sql, offset_sql], |r| r.get::<_, i64>(0))
827 .map_err(sqlite_err)?
828 .filter_map(|r| r.ok())
829 .collect()
830 } else {
831 let mut stmt = conn
832 .prepare_cached(
833 "SELECT e.id FROM entity e WHERE e.flags = 0
834 ORDER BY e.id LIMIT ?1 OFFSET ?2",
835 )
836 .map_err(sqlite_err)?;
837 stmt.query_map(params![limit_sql, offset_sql], |r| r.get::<_, i64>(0))
838 .map_err(sqlite_err)?
839 .filter_map(|r| r.ok())
840 .collect()
841 };
842
843 if ids.is_empty() {
844 return Ok((r#"{"entities":[],"relations":[]}"#.to_string(), 0));
845 }
846
847 let idlist = int_csv(&ids);
852 let returned = ids.len();
853
854 let obs_field = if include_obs {
859 format!("'observations', COALESCE((SELECT json_group_array({OBSERVATION_JSON} ORDER BY o.idx, o.id)
860 FROM observation o WHERE o.entity_id = e.id), json('[]'))")
861 } else {
862 "'obsCount', e.obs_count".to_owned()
863 };
864
865 let entities_json: String = {
866 let sql = format!(
867 "SELECT COALESCE(json_group_array(json_object(
868 'name', e.name,
869 'entityType', t.name,
870 {obs_field}
871 ) ORDER BY e.id), json('[]'))
872 FROM entity e
873 JOIN type_dict t ON t.id = e.type_id
874 WHERE e.id IN ({idlist}) AND e.flags = 0"
875 );
876 conn.query_row(&sql, [], |row| row.get::<_, String>(0))
877 .map_err(sqlite_err)?
878 };
879
880 let relations_json: String = {
881 let sql = format!(
882 "SELECT COALESCE(json_group_array(json_object(
883 'from', e1.name,
884 'to', e2.name,
885 'relationType', t.name
886 )), json('[]'))
887 FROM relation r
888 JOIN entity e1 ON e1.id = r.from_id
889 JOIN entity e2 ON e2.id = r.to_id
890 JOIN type_dict t ON t.id = r.type_id
891 WHERE r.from_id IN ({idlist}) AND r.to_id IN ({idlist})
892 AND e1.flags = 0 AND e2.flags = 0"
893 );
894 conn.query_row(&sql, [], |row| row.get::<_, String>(0))
895 .map_err(sqlite_err)?
896 };
897
898 let mut out = String::with_capacity(32 + entities_json.len() + relations_json.len());
899 out.push_str("{\"entities\":");
900 out.push_str(&entities_json);
901 out.push_str(",\"relations\":");
902 out.push_str(&relations_json);
903 out.push('}');
904 Ok((out, returned))
905 }
906
907 pub fn open_nodes(&self, names: &[String]) -> String {
908 let conn = self.readers.get();
909 let mut entity_ids: Vec<i64> = Vec::new();
910
911 for name in names {
912 let h = name_hash(name);
913 if let Ok(Some(id)) = conn
914 .query_row(
915 "SELECT id FROM entity WHERE name_hash = ?1 AND name = ?2 AND flags = 0",
916 params![h, name],
917 |row| row.get::<_, i64>(0),
918 )
919 .map(Some)
920 .or_else(|e| {
921 if is_not_found(&e) {
922 Ok(None)
923 } else {
924 Err(sqlite_err(e))
925 }
926 })
927 {
928 entity_ids.push(id);
929 }
930 }
931
932 if entity_ids.is_empty() {
933 return r#"{"entities":[],"relations":[]}"#.to_string();
934 }
935
936 let placeholders: Vec<String> = entity_ids.iter().map(|_| "?".to_string()).collect();
937 let ids_str = placeholders.join(",");
938
939 let entities_json: String = {
940 let sql = format!(
941 "SELECT COALESCE(json_group_array(json_object(
942 'name', e.name,
943 'entityType', t.name,
944 'observations', COALESCE((
945 SELECT json_group_array({OBSERVATION_JSON} ORDER BY o.idx, o.id)
946 FROM observation o WHERE o.entity_id = e.id
947 ), json('[]'))
948 ) ORDER BY e.id), json('[]'))
949 FROM entity e
950 JOIN type_dict t ON t.id = e.type_id
951 WHERE e.id IN ({ids_str}) AND e.flags = 0"
952 );
953 conn.query_row(&sql, rusqlite::params_from_iter(&entity_ids), |row| {
954 row.get::<_, String>(0)
955 })
956 .unwrap_or_else(|_| "[]".to_string())
957 };
958
959 let relations_json: String = {
960 let sql = format!(
961 "SELECT COALESCE(json_group_array(json_object(
962 'from', e1.name,
963 'to', e2.name,
964 'relationType', t.name
965 )), json('[]'))
966 FROM relation r
967 JOIN entity e1 ON e1.id = r.from_id
968 JOIN entity e2 ON e2.id = r.to_id
969 JOIN type_dict t ON t.id = r.type_id
970 WHERE (r.from_id IN ({ids_str}) OR r.to_id IN ({ids_str}))
971 AND e1.flags = 0 AND e2.flags = 0"
972 );
973 let all_params: Vec<&dyn rusqlite::types::ToSql> = entity_ids
974 .iter()
975 .map(|id| id as &dyn rusqlite::types::ToSql)
976 .chain(
977 entity_ids
978 .iter()
979 .map(|id| id as &dyn rusqlite::types::ToSql),
980 )
981 .collect();
982 let mut stmt = conn.prepare(&sql).unwrap();
983 stmt.query_row(all_params.as_slice(), |row| row.get::<_, String>(0))
984 .unwrap_or_else(|_| "[]".to_string())
985 };
986
987 let mut out = String::with_capacity(32 + entities_json.len() + relations_json.len());
988 out.push_str("{\"entities\":");
989 out.push_str(&entities_json);
990 out.push_str(",\"relations\":");
991 out.push_str(&relations_json);
992 out.push('}');
993 out
994 }
995
996 pub fn entities_exist(&self, names: &[String]) -> Result<Vec<bool>> {
997 let conn = self.readers.get();
998 let mut results = Vec::with_capacity(names.len());
999 for name in names {
1000 let h = name_hash(name);
1001 let exists: bool = conn
1002 .query_row(
1003 "SELECT 1 FROM entity WHERE name_hash = ?1 AND name = ?2 AND flags = 0",
1004 params![h, name],
1005 |_| Ok(()),
1006 )
1007 .is_ok();
1008 results.push(exists);
1009 }
1010 Ok(results)
1011 }
1012
1013 pub fn degree(&self, name: &str, direction: Direction) -> Result<usize> {
1014 let conn = self.readers.get();
1015 let (_, _, out_d, in_d) = match self.get_entity_id(&conn, name)? {
1016 Some(v) => v,
1017 None => {
1018 return Err(MCSError::InvalidParams(format!(
1019 "Entity '{name}' not found"
1020 )));
1021 }
1022 };
1023 Ok(match direction {
1024 Direction::Outgoing => out_d as usize,
1025 Direction::Incoming => in_d as usize,
1026 Direction::Both => (out_d + in_d) as usize,
1027 })
1028 }
1029
1030 pub fn get_entity_count(&self) -> Result<usize> {
1031 let conn = self.readers.get();
1032 read_graph_stat(&conn, "entities")
1033 .map(|v| v as usize)
1034 .map_err(|_| MCSError::MemoryError("Failed to read entity count".into()))
1035 }
1036
1037 pub fn get_relation_count(&self) -> Result<usize> {
1038 let conn = self.readers.get();
1039 read_graph_stat(&conn, "relations")
1040 .map(|v| v as usize)
1041 .map_err(|_| MCSError::MemoryError("Failed to read relation count".into()))
1042 }
1043
1044 pub fn search_relations(
1045 &self,
1046 from: Option<&str>,
1047 to: Option<&str>,
1048 rtype: Option<&str>,
1049 limit: Option<usize>,
1050 ) -> Vec<Relation> {
1051 let conn = self.readers.get();
1052 let mut results = Vec::new();
1053
1054 let from_id = from
1060 .filter(|f| !f.is_empty())
1061 .map(|f| entity_name_lookup(&conn, f).ok().flatten().unwrap_or(-1));
1062 let to_id = to
1063 .filter(|t| !t.is_empty())
1064 .map(|t| entity_name_lookup(&conn, t).ok().flatten().unwrap_or(-1));
1065 let type_id = rtype
1066 .filter(|rt| !rt.is_empty())
1067 .map(|rt| lookup_type_id(&conn, rt, 1).unwrap_or(-1));
1068
1069 match (from_id, to_id, type_id) {
1070 (Some(fid), Some(tid), Some(tpid)) => {
1071 if let Ok(mut stmt) = conn.prepare_cached(
1072 "SELECT e1.name, e2.name, t.name
1073 FROM relation r
1074 JOIN entity e1 ON e1.id = r.from_id
1075 JOIN entity e2 ON e2.id = r.to_id
1076 JOIN type_dict t ON t.id = r.type_id
1077 WHERE r.from_id = ?1 AND r.to_id = ?2 AND r.type_id = ?3
1078 AND e1.flags = 0 AND e2.flags = 0
1079 ORDER BY r.from_id, r.to_id",
1080 ) && let Ok(rows) = stmt.query_map(params![fid, tid, tpid], |row| {
1081 Ok(Relation {
1082 from: row.get(0)?,
1083 to: row.get(1)?,
1084 relation_type: row.get(2)?,
1085 })
1086 }) {
1087 for row in rows.flatten() {
1088 results.push(row);
1089 }
1090 }
1091 }
1092 (Some(fid), Some(tid), None) => {
1093 if let Ok(mut stmt) = conn.prepare_cached(
1094 "SELECT e1.name, e2.name, t.name
1095 FROM relation r
1096 JOIN entity e1 ON e1.id = r.from_id
1097 JOIN entity e2 ON e2.id = r.to_id
1098 JOIN type_dict t ON t.id = r.type_id
1099 WHERE r.from_id = ?1 AND r.to_id = ?2
1100 AND e1.flags = 0 AND e2.flags = 0
1101 ORDER BY r.from_id, r.to_id",
1102 ) && let Ok(rows) = stmt.query_map(params![fid, tid], |row| {
1103 Ok(Relation {
1104 from: row.get(0)?,
1105 to: row.get(1)?,
1106 relation_type: row.get(2)?,
1107 })
1108 }) {
1109 for row in rows.flatten() {
1110 results.push(row);
1111 }
1112 }
1113 }
1114 (Some(fid), None, Some(tpid)) => {
1115 if let Ok(mut stmt) = conn.prepare_cached(
1116 "SELECT e1.name, e2.name, t.name
1117 FROM relation r
1118 JOIN entity e1 ON e1.id = r.from_id
1119 JOIN entity e2 ON e2.id = r.to_id
1120 JOIN type_dict t ON t.id = r.type_id
1121 WHERE r.from_id = ?1 AND r.type_id = ?2
1122 AND e1.flags = 0 AND e2.flags = 0
1123 ORDER BY r.from_id, r.to_id",
1124 ) && let Ok(rows) = stmt.query_map(params![fid, tpid], |row| {
1125 Ok(Relation {
1126 from: row.get(0)?,
1127 to: row.get(1)?,
1128 relation_type: row.get(2)?,
1129 })
1130 }) {
1131 for row in rows.flatten() {
1132 results.push(row);
1133 }
1134 }
1135 }
1136 (None, Some(tid), Some(tpid)) => {
1137 if let Ok(mut stmt) = conn.prepare_cached(
1138 "SELECT e1.name, e2.name, t.name
1139 FROM relation r
1140 JOIN entity e1 ON e1.id = r.from_id
1141 JOIN entity e2 ON e2.id = r.to_id
1142 JOIN type_dict t ON t.id = r.type_id
1143 WHERE r.to_id = ?1 AND r.type_id = ?2
1144 AND e1.flags = 0 AND e2.flags = 0
1145 ORDER BY r.from_id, r.to_id",
1146 ) && let Ok(rows) = stmt.query_map(params![tid, tpid], |row| {
1147 Ok(Relation {
1148 from: row.get(0)?,
1149 to: row.get(1)?,
1150 relation_type: row.get(2)?,
1151 })
1152 }) {
1153 for row in rows.flatten() {
1154 results.push(row);
1155 }
1156 }
1157 }
1158 (Some(fid), None, None) => {
1159 if let Ok(mut stmt) = conn.prepare_cached(
1160 "SELECT e1.name, e2.name, t.name
1161 FROM relation r
1162 JOIN entity e1 ON e1.id = r.from_id
1163 JOIN entity e2 ON e2.id = r.to_id
1164 JOIN type_dict t ON t.id = r.type_id
1165 WHERE r.from_id = ?1
1166 AND e1.flags = 0 AND e2.flags = 0
1167 ORDER BY r.from_id, r.to_id",
1168 ) && let Ok(rows) = stmt.query_map(params![fid], |row| {
1169 Ok(Relation {
1170 from: row.get(0)?,
1171 to: row.get(1)?,
1172 relation_type: row.get(2)?,
1173 })
1174 }) {
1175 for row in rows.flatten() {
1176 results.push(row);
1177 }
1178 }
1179 }
1180 (None, Some(tid), None) => {
1181 if let Ok(mut stmt) = conn.prepare_cached(
1182 "SELECT e1.name, e2.name, t.name
1183 FROM relation r
1184 JOIN entity e1 ON e1.id = r.from_id
1185 JOIN entity e2 ON e2.id = r.to_id
1186 JOIN type_dict t ON t.id = r.type_id
1187 WHERE r.to_id = ?1
1188 AND e1.flags = 0 AND e2.flags = 0
1189 ORDER BY r.from_id, r.to_id",
1190 ) && let Ok(rows) = stmt.query_map(params![tid], |row| {
1191 Ok(Relation {
1192 from: row.get(0)?,
1193 to: row.get(1)?,
1194 relation_type: row.get(2)?,
1195 })
1196 }) {
1197 for row in rows.flatten() {
1198 results.push(row);
1199 }
1200 }
1201 }
1202 (None, None, Some(tpid)) => {
1203 if let Ok(mut stmt) = conn.prepare_cached(
1204 "SELECT e1.name, e2.name, t.name
1205 FROM relation r
1206 JOIN entity e1 ON e1.id = r.from_id
1207 JOIN entity e2 ON e2.id = r.to_id
1208 JOIN type_dict t ON t.id = r.type_id
1209 WHERE r.type_id = ?1
1210 AND e1.flags = 0 AND e2.flags = 0
1211 ORDER BY r.from_id, r.to_id",
1212 ) && let Ok(rows) = stmt.query_map(params![tpid], |row| {
1213 Ok(Relation {
1214 from: row.get(0)?,
1215 to: row.get(1)?,
1216 relation_type: row.get(2)?,
1217 })
1218 }) {
1219 for row in rows.flatten() {
1220 results.push(row);
1221 }
1222 }
1223 }
1224 (None, None, None) => {
1225 if let Ok(mut stmt) = conn.prepare_cached(
1226 "SELECT e1.name, e2.name, t.name
1227 FROM relation r
1228 JOIN entity e1 ON e1.id = r.from_id
1229 JOIN entity e2 ON e2.id = r.to_id
1230 JOIN type_dict t ON t.id = r.type_id
1231 WHERE e1.flags = 0 AND e2.flags = 0
1232 ORDER BY r.from_id, r.to_id",
1233 ) && let Ok(rows) = stmt.query_map([], |row| {
1234 Ok(Relation {
1235 from: row.get(0)?,
1236 to: row.get(1)?,
1237 relation_type: row.get(2)?,
1238 })
1239 }) {
1240 for row in rows.flatten() {
1241 results.push(row);
1242 }
1243 }
1244 }
1245 }
1246 if let Some(lim) = limit {
1247 results.truncate(lim);
1248 }
1249 results
1250 }
1251
1252 pub fn find_path(&self, from: &str, to: &str) -> Result<Option<Vec<String>>> {
1253 let conn = self.readers.get();
1254 let (from_id, _, _, _) = match self.get_entity_id(&conn, from)? {
1255 Some(v) => v,
1256 None => {
1257 return Err(MCSError::InvalidParams(format!(
1258 "Source entity '{from}' not found"
1259 )));
1260 }
1261 };
1262 let (to_id, _, _, _) = match self.get_entity_id(&conn, to)? {
1263 Some(v) => v,
1264 None => {
1265 return Err(MCSError::InvalidParams(format!(
1266 "Target entity '{to}' not found"
1267 )));
1268 }
1269 };
1270
1271 if from_id == to_id {
1272 return Ok(Some(vec![from.to_string()]));
1273 }
1274
1275 let mut visited = HashSet::new();
1277 let mut parent: FxHashMap<i64, i64> = FxHashMap::default();
1278 let mut queue = VecDeque::new();
1279 visited.insert(from_id);
1280 queue.push_back(from_id);
1281
1282 while let Some(cur) = queue.pop_front() {
1283 if cur == to_id {
1284 break;
1285 }
1286 if let Ok(mut stmt) =
1288 conn.prepare_cached("SELECT to_id FROM relation WHERE from_id = ?1")
1289 && let Ok(rows) = stmt.query_map(params![cur], |row| row.get::<_, i64>(0))
1290 {
1291 for row in rows.flatten() {
1292 if visited.insert(row) {
1293 parent.insert(row, cur);
1294 queue.push_back(row);
1295 }
1296 }
1297 }
1298 if let Ok(mut stmt) =
1300 conn.prepare_cached("SELECT from_id FROM relation WHERE to_id = ?1")
1301 && let Ok(rows) = stmt.query_map(params![cur], |row| row.get::<_, i64>(0))
1302 {
1303 for row in rows.flatten() {
1304 if visited.insert(row) {
1305 parent.insert(row, cur);
1306 queue.push_back(row);
1307 }
1308 }
1309 }
1310 }
1311
1312 if !parent.contains_key(&to_id) && to_id != from_id {
1313 return Ok(None);
1314 }
1315
1316 let mut path = Vec::new();
1317 let mut cur = to_id;
1318 path.push(cur);
1319 while let Some(&p) = parent.get(&cur) {
1320 path.push(p);
1321 cur = p;
1322 if cur == from_id {
1323 break;
1324 }
1325 }
1326 path.reverse();
1327
1328 let placeholders: Vec<String> = path.iter().map(|_| "?".to_string()).collect();
1329 let sql = format!(
1330 "SELECT id, name FROM entity WHERE id IN ({})",
1331 placeholders.join(",")
1332 );
1333 let name_map: FxHashMap<i64, String> = if let Ok(mut stmt) = conn.prepare(&sql)
1334 && let Ok(rows) = stmt.query_map(rusqlite::params_from_iter(&path), |row| {
1335 Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
1336 }) {
1337 rows.flatten().collect()
1338 } else {
1339 FxHashMap::default()
1340 };
1341
1342 let name_path: Vec<String> = path
1343 .iter()
1344 .filter_map(|id| name_map.get(id).cloned())
1345 .collect();
1346
1347 Ok(Some(name_path))
1348 }
1349
1350 pub fn compact(&self) -> Result<()> {
1351 self.mutate(MutationRequest::Compact).map(|_| ())
1352 }
1353
1354 pub fn neighbors(
1355 &self,
1356 name: &str,
1357 direction: Direction,
1358 rtype: Option<&str>,
1359 depth: u32,
1360 ) -> Result<String> {
1361 self._traverse(name, direction, rtype, depth, true)
1362 }
1363
1364 pub fn extract_subgraph(&self, names: &[String], depth: u32) -> Result<String> {
1365 if names.is_empty() {
1366 return Ok(r#"{"entities":[],"relations":[]}"#.to_string());
1367 }
1368
1369 let conn = self.readers.get();
1370 let mut all_entity_ids: HashSet<i64> = HashSet::new();
1371 let mut frontier: HashSet<i64> = HashSet::new();
1372 let mut all_rel_pairs: HashSet<(i64, i64, i64)> = HashSet::new();
1373
1374 for name in names {
1376 let h = name_hash(name);
1377 if let Ok(Some(id)) = conn
1378 .query_row(
1379 "SELECT id FROM entity WHERE name_hash = ?1 AND name = ?2 AND flags = 0",
1380 params![h, name],
1381 |row| row.get::<_, i64>(0),
1382 )
1383 .map(Some)
1384 .or_else(|e| {
1385 if is_not_found(&e) {
1386 Ok(None)
1387 } else {
1388 Err(sqlite_err(e))
1389 }
1390 })
1391 {
1392 all_entity_ids.insert(id);
1393 frontier.insert(id);
1394 }
1395 }
1396
1397 let mut current_depth = 0u32;
1398 while current_depth < depth && !frontier.is_empty() {
1399 let mut next_frontier: HashSet<i64> = HashSet::new();
1400
1401 const CHUNK: usize = 500;
1404 let frontier_ids: Vec<i64> = frontier.iter().copied().collect();
1405 for chunk in frontier_ids.chunks(CHUNK) {
1406 let placeholders: Vec<String> = chunk.iter().map(|_| "?".to_string()).collect();
1407 let in_clause = placeholders.join(",");
1408
1409 if let Ok(mut stmt) = conn.prepare(&format!(
1411 "SELECT from_id, to_id, type_id FROM relation WHERE from_id IN ({in_clause})",
1412 )) && let Ok(rows) = stmt.query_map(rusqlite::params_from_iter(chunk), |row| {
1413 Ok((
1414 row.get::<_, i64>(0)?,
1415 row.get::<_, i64>(1)?,
1416 row.get::<_, i64>(2)?,
1417 ))
1418 }) {
1419 for row in rows.flatten() {
1420 let (from_id, to_id, type_id) = row;
1421 all_rel_pairs.insert((from_id, to_id, type_id));
1422 if all_entity_ids.insert(to_id) {
1423 next_frontier.insert(to_id);
1424 }
1425 }
1426 }
1427
1428 if let Ok(mut stmt) = conn.prepare(&format!(
1430 "SELECT from_id, to_id, type_id FROM relation WHERE to_id IN ({in_clause})",
1431 )) && let Ok(rows) = stmt.query_map(rusqlite::params_from_iter(chunk), |row| {
1432 Ok((
1433 row.get::<_, i64>(0)?,
1434 row.get::<_, i64>(1)?,
1435 row.get::<_, i64>(2)?,
1436 ))
1437 }) {
1438 for row in rows.flatten() {
1439 let (from_id, to_id, type_id) = row;
1440 all_rel_pairs.insert((from_id, to_id, type_id));
1441 if all_entity_ids.insert(from_id) {
1442 next_frontier.insert(from_id);
1443 }
1444 }
1445 }
1446 }
1447 if all_entity_ids.len() > MAX_TRAVERSAL_ENTITIES
1448 || all_rel_pairs.len() > MAX_TRAVERSAL_RELS
1449 {
1450 break;
1451 }
1452 frontier = next_frontier;
1453 current_depth += 1;
1454 }
1455
1456 let entities_json: String = if all_entity_ids.is_empty() {
1457 "[]".to_string()
1458 } else {
1459 let ids: Vec<i64> = all_entity_ids.iter().copied().collect();
1460 let sql = format!(
1461 "SELECT COALESCE(json_group_array(json_object(
1462 'name', e.name,
1463 'entityType', t.name,
1464 'observations', COALESCE((
1465 SELECT json_group_array({OBSERVATION_JSON} ORDER BY o.idx, o.id)
1466 FROM observation o WHERE o.entity_id = e.id
1467 ), json('[]'))
1468 ) ORDER BY e.id), json('[]'))
1469 FROM entity e
1470 JOIN type_dict t ON t.id = e.type_id
1471 WHERE e.id IN ({}) AND e.flags = 0",
1472 int_csv(&ids)
1473 );
1474 conn.query_row(&sql, [], |row| row.get::<_, String>(0))
1475 .map_err(sqlite_err)?
1476 };
1477
1478 let relations_json: String = if all_rel_pairs.is_empty() {
1479 "[]".to_string()
1480 } else {
1481 let sql = format!(
1482 "WITH r(from_id, to_id, type_id) AS (VALUES {})
1483 SELECT COALESCE(json_group_array(json_object(
1484 'from', e1.name,
1485 'to', e2.name,
1486 'relationType', t.name
1487 )), json('[]'))
1488 FROM r
1489 JOIN entity e1 ON e1.id = r.from_id
1490 JOIN entity e2 ON e2.id = r.to_id
1491 JOIN type_dict t ON t.id = r.type_id
1492 WHERE e1.flags = 0 AND e2.flags = 0",
1493 rel_values_literal(&all_rel_pairs)
1494 );
1495 conn.query_row(&sql, [], |row| row.get::<_, String>(0))
1496 .map_err(sqlite_err)?
1497 };
1498
1499 let mut out = String::with_capacity(32 + entities_json.len() + relations_json.len());
1500 out.push_str("{\"entities\":");
1501 out.push_str(&entities_json);
1502 out.push_str(",\"relations\":");
1503 out.push_str(&relations_json);
1504 out.push('}');
1505 Ok(out)
1506 }
1507
1508 pub fn describe_entity(&self, name: &str) -> Result<EntityDescription> {
1509 let conn = self.readers.get();
1510 let tx = conn.unchecked_transaction().map_err(sqlite_err)?;
1514 let entity = crate::mutation::read_entity(&tx, name)?
1515 .ok_or_else(|| MCSError::InvalidParams(format!("Entity '{name}' not found")))?;
1516 let relations = crate::mutation::relations_for(&tx, name)?;
1517 let mut neighbors: Vec<String> = relations
1518 .iter()
1519 .map(|relation| {
1520 if relation.from == name {
1521 relation.to.clone()
1522 } else {
1523 relation.from.clone()
1524 }
1525 })
1526 .collect();
1527 neighbors.sort();
1528 neighbors.dedup();
1529 let incoming = relations
1533 .iter()
1534 .filter(|relation| relation.to == name)
1535 .count() as i64;
1536 let outgoing = relations
1537 .iter()
1538 .filter(|relation| relation.from == name)
1539 .count() as i64;
1540 tx.commit().map_err(sqlite_err)?;
1541
1542 Ok(EntityDescription {
1543 name: entity.name,
1544 entity_type: entity.entity_type,
1545 observations: entity.observations,
1546 relations,
1547 neighbors,
1548 degree: Degree { incoming, outgoing },
1549 })
1550 }
1551
1552 pub fn entity_type_counts(&self) -> Vec<(String, usize)> {
1553 let conn = self.readers.get();
1554 select_all_types(&conn, 0).unwrap_or_default()
1555 }
1556
1557 pub fn entity_type_catalog(&self) -> Vec<(String, usize, Option<String>)> {
1560 let conn = self.readers.get();
1561 select_type_catalog(&conn, 0).unwrap_or_default()
1562 }
1563
1564 pub fn ui_meta(&self) -> (Vec<(String, usize)>, usize, usize) {
1572 let conn = self.readers.get();
1573 let types = select_all_types(&conn, 0).unwrap_or_default();
1574 let entities = read_graph_stat(&conn, "entities").unwrap_or(0).max(0) as usize;
1575 let relations = read_graph_stat(&conn, "relations").unwrap_or(0).max(0) as usize;
1576 (types, entities, relations)
1577 }
1578
1579 pub fn relation_type_counts(&self) -> Vec<(String, usize)> {
1580 let conn = self.readers.get();
1581 select_all_types(&conn, 1).unwrap_or_default()
1582 }
1583
1584 pub fn relation_type_catalog(&self) -> Vec<(String, usize, Option<String>)> {
1587 let conn = self.readers.get();
1588 select_type_catalog(&conn, 1).unwrap_or_default()
1589 }
1590
1591 pub fn set_type_description(
1598 &self,
1599 kind: i64,
1600 name: &str,
1601 description: Option<&str>,
1602 ) -> Result<()> {
1603 let conn = self.writer.lock();
1604 let existing = lookup_type_id(&conn, name, kind);
1607 let description: Option<String> = description.map(|d| d.into());
1608 match existing {
1609 Some(id) => {
1610 conn.execute(
1611 "UPDATE type_dict SET desc = ?1 WHERE id = ?2",
1612 params![description, id],
1613 )
1614 .map_err(sqlite_err)?;
1615 }
1616 None => {
1617 conn.execute(
1618 "INSERT INTO type_dict(kind, name, count, desc) VALUES(?1, ?2, 0, ?3)",
1619 params![kind, name, description],
1620 )
1621 .map_err(sqlite_err)?;
1622 }
1623 }
1624 Ok(())
1625 }
1626
1627 pub fn entity_type_exists(&self, name: &str) -> bool {
1630 let conn = self.readers.get();
1631 lookup_type_id(&conn, name, 0).is_some()
1632 }
1633
1634 pub fn relation_type_exists(&self, name: &str) -> bool {
1637 let conn = self.readers.get();
1638 lookup_type_id(&conn, name, 1).is_some()
1639 }
1640
1641 pub fn batch_get_entities(&self, names: &[String]) -> Vec<Option<Entity>> {
1642 names
1643 .iter()
1644 .map(|n| self.get_entity(n).unwrap_or(None))
1645 .collect()
1646 }
1647
1648 pub fn find_all_paths(
1649 &self,
1650 from: &str,
1651 to: &str,
1652 max_depth: usize,
1653 max_paths: usize,
1654 ) -> Result<Vec<Vec<String>>> {
1655 let conn = self.readers.get();
1656 let (from_id, _, _, _) = match self.get_entity_id(&conn, from)? {
1657 Some(v) => v,
1658 None => {
1659 return Err(MCSError::InvalidParams(format!(
1660 "Source entity '{from}' not found"
1661 )));
1662 }
1663 };
1664 let (to_id, _, _, _) = match self.get_entity_id(&conn, to)? {
1665 Some(v) => v,
1666 None => {
1667 return Err(MCSError::InvalidParams(format!(
1668 "Target entity '{to}' not found"
1669 )));
1670 }
1671 };
1672
1673 if from_id == to_id {
1674 return Ok(vec![vec![from.to_string()]]);
1675 }
1676
1677 let mut all_paths: Vec<Vec<i64>> = Vec::new();
1679 let mut queue: VecDeque<(i64, Vec<i64>)> = VecDeque::new();
1680 queue.push_back((from_id, vec![from_id]));
1681
1682 const MAX_QUEUE_SIZE: usize = 10_000_000;
1683
1684 while let Some((cur, path)) = queue.pop_front() {
1685 if all_paths.len() >= max_paths {
1686 break;
1687 }
1688 if path.len() > max_depth {
1689 continue;
1690 }
1691
1692 if let Ok(mut stmt) =
1694 conn.prepare_cached("SELECT to_id FROM relation WHERE from_id = ?1")
1695 && let Ok(rows) = stmt.query_map(params![cur], |row| row.get::<_, i64>(0))
1696 {
1697 for next_id in rows.flatten() {
1698 if next_id == to_id {
1699 let mut full_path = path.clone();
1700 full_path.push(next_id);
1701 all_paths.push(full_path);
1702 if all_paths.len() >= max_paths {
1703 break;
1704 }
1705 } else if !path.contains(&next_id) && path.len() < max_depth {
1706 if queue.len() >= MAX_QUEUE_SIZE {
1707 return Err(MCSError::InvalidParams(
1708 "Path exploration queue exceeded limit (too many paths on highly connected graph)".to_string()
1709 ));
1710 }
1711 let mut new_path = path.clone();
1712 new_path.push(next_id);
1713 queue.push_back((next_id, new_path));
1714 }
1715 }
1716 }
1717
1718 if let Ok(mut stmt) =
1720 conn.prepare_cached("SELECT from_id FROM relation WHERE to_id = ?1")
1721 && let Ok(rows) = stmt.query_map(params![cur], |row| row.get::<_, i64>(0))
1722 {
1723 for next_id in rows.flatten() {
1724 if next_id == to_id {
1725 let mut full_path = path.clone();
1726 full_path.push(next_id);
1727 all_paths.push(full_path);
1728 if all_paths.len() >= max_paths {
1729 break;
1730 }
1731 } else if !path.contains(&next_id) && path.len() < max_depth {
1732 if queue.len() >= MAX_QUEUE_SIZE {
1733 return Err(MCSError::InvalidParams(
1734 "Path exploration queue exceeded limit (too many paths on highly connected graph)".to_string()
1735 ));
1736 }
1737 let mut new_path = path.clone();
1738 new_path.push(next_id);
1739 queue.push_back((next_id, new_path));
1740 }
1741 }
1742 }
1743 }
1744
1745 let all_ids: HashSet<i64> = all_paths.iter().flat_map(|p| p.iter()).copied().collect();
1747 let id_list: Vec<i64> = all_ids.into_iter().collect();
1748 let name_map: FxHashMap<i64, String> = if id_list.is_empty() {
1749 FxHashMap::default()
1750 } else {
1751 let placeholders: Vec<String> = id_list.iter().map(|_| "?".to_string()).collect();
1752 let sql = format!(
1753 "SELECT id, name FROM entity WHERE id IN ({})",
1754 placeholders.join(",")
1755 );
1756 if let Ok(mut stmt) = conn.prepare(&sql)
1757 && let Ok(rows) = stmt.query_map(rusqlite::params_from_iter(&id_list), |row| {
1758 Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
1759 })
1760 {
1761 rows.flatten().collect()
1762 } else {
1763 FxHashMap::default()
1764 }
1765 };
1766
1767 let mut named_paths: Vec<Vec<String>> = Vec::with_capacity(all_paths.len());
1768 for path_ids in all_paths {
1769 let named: Vec<String> = path_ids
1770 .iter()
1771 .filter_map(|id| name_map.get(id).cloned())
1772 .collect();
1773 named_paths.push(named);
1774 }
1775
1776 Ok(named_paths)
1777 }
1778
1779 pub fn export(&self, _format: &str, max_rows: i64) -> Result<String> {
1784 let conn = self.readers.get();
1785 conn.query_row(
1788 &format!(
1789 "SELECT json_object(
1790 'entities', COALESCE((
1791 SELECT json_group_array(json_object(
1792 'name', e.name,
1793 'entityType', t.name,
1794 'observations', COALESCE((
1795 SELECT json_group_array({OBSERVATION_JSON} ORDER BY o.idx, o.id)
1796 FROM observation o WHERE o.entity_id = e.id
1797 ), json('[]'))
1798 ) ORDER BY e.id)
1799 FROM (
1800 SELECT id, name, type_id FROM entity
1801 WHERE flags = 0 ORDER BY id LIMIT ?1
1802 ) e
1803 JOIN type_dict t ON t.id = e.type_id
1804 ), json('[]')),
1805 'relations', COALESCE((
1806 SELECT json_group_array(json_object(
1807 'from', e1.name,
1808 'to', e2.name,
1809 'relationType', t.name
1810 ))
1811 FROM (
1812 SELECT from_id, to_id, type_id FROM relation LIMIT ?1
1813 ) r
1814 JOIN entity e1 ON e1.id = r.from_id
1815 JOIN entity e2 ON e2.id = r.to_id
1816 JOIN type_dict t ON t.id = r.type_id
1817 WHERE e1.flags = 0 AND e2.flags = 0
1818 ), json('[]'))
1819 )"
1820 ),
1821 params![max_rows],
1822 |row| row.get::<_, String>(0),
1823 )
1824 .map_err(sqlite_err)
1825 }
1826
1827 pub fn wipe(&self) -> Result<()> {
1828 self.mutate(MutationRequest::Wipe).map(|_| ())
1829 }
1830
1831 pub fn run_maintenance(&self) -> Result<()> {
1834 let conn = self.writer.lock();
1835
1836 conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")
1837 .map_err(sqlite_err)?;
1838
1839 conn.execute_batch("PRAGMA optimize(0x10000);")
1840 .map_err(sqlite_err)?;
1841
1842 let tx = TxGuard::begin(&conn)?;
1843 conn.execute_batch(
1844 "INSERT INTO name_fts(name_fts) VALUES('optimize');
1845 INSERT INTO obs_fts(obs_fts) VALUES('optimize');",
1846 )
1847 .map_err(sqlite_err)?;
1848 tx.commit()?;
1849
1850 Ok(())
1851 }
1852
1853 pub fn checkpoint_passive(&self) -> Result<()> {
1857 let conn = self.writer.lock();
1858 conn.execute_batch("PRAGMA wal_checkpoint(PASSIVE);")
1859 .map_err(sqlite_err)?;
1860 Ok(())
1861 }
1862
1863 fn _traverse(
1864 &self,
1865 name: &str,
1866 direction: Direction,
1867 rtype: Option<&str>,
1868 depth: u32,
1869 _include_relations: bool,
1871 ) -> Result<String> {
1872 let conn = self.readers.get();
1873 let (start_id, _, _, _) = match self.get_entity_id(&conn, name)? {
1874 Some(v) => v,
1875 None => {
1876 return Err(MCSError::InvalidParams(format!(
1877 "Entity '{name}' not found"
1878 )));
1879 }
1880 };
1881
1882 let mut all_ids: HashSet<i64> = HashSet::new();
1883 let mut all_rels: HashSet<(i64, i64, i64)> = HashSet::new();
1884 let mut frontier: HashSet<i64> = HashSet::new();
1885 all_ids.insert(start_id);
1886 frontier.insert(start_id);
1887
1888 let type_filter: Option<i64> = rtype
1894 .filter(|rt| !rt.is_empty())
1895 .map(|rt| lookup_type_id(&conn, rt, 1).unwrap_or(-1));
1896
1897 let mut q_out_t = conn.prepare_cached(
1899 "SELECT to_id, type_id FROM relation WHERE from_id = ?1 AND type_id = ?2",
1900 );
1901 let mut q_out =
1902 conn.prepare_cached("SELECT to_id, type_id FROM relation WHERE from_id = ?1");
1903 let mut q_in_t = conn.prepare_cached(
1904 "SELECT from_id, type_id FROM relation WHERE to_id = ?1 AND type_id = ?2",
1905 );
1906 let mut q_in =
1907 conn.prepare_cached("SELECT from_id, type_id FROM relation WHERE to_id = ?1");
1908
1909 let mut cur_depth = 0u32;
1910 while cur_depth < depth && !frontier.is_empty() {
1911 let mut next_frontier: HashSet<i64> = HashSet::new();
1912
1913 for &fid in &frontier {
1914 if direction == Direction::Outgoing || direction == Direction::Both {
1915 if let Some(tid) = type_filter {
1916 if let Ok(ref mut stmt) = q_out_t
1917 && let Ok(rows) = stmt.query_map(params![fid, tid], |row| {
1918 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1919 })
1920 {
1921 for row in rows.flatten() {
1922 let (to_id, t_id) = row;
1923 all_rels.insert((fid, to_id, t_id));
1924 if all_ids.insert(to_id) {
1925 next_frontier.insert(to_id);
1926 }
1927 }
1928 }
1929 } else if let Ok(ref mut stmt) = q_out
1930 && let Ok(rows) = stmt.query_map(params![fid], |row| {
1931 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1932 })
1933 {
1934 for row in rows.flatten() {
1935 let (to_id, t_id) = row;
1936 all_rels.insert((fid, to_id, t_id));
1937 if all_ids.insert(to_id) {
1938 next_frontier.insert(to_id);
1939 }
1940 }
1941 }
1942 }
1943
1944 if direction == Direction::Incoming || direction == Direction::Both {
1945 if let Some(tid) = type_filter {
1946 if let Ok(ref mut stmt) = q_in_t
1947 && let Ok(rows) = stmt.query_map(params![fid, tid], |row| {
1948 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1949 })
1950 {
1951 for row in rows.flatten() {
1952 let (from_id, t_id) = row;
1953 all_rels.insert((from_id, fid, t_id));
1954 if all_ids.insert(from_id) {
1955 next_frontier.insert(from_id);
1956 }
1957 }
1958 }
1959 } else if let Ok(ref mut stmt) = q_in
1960 && let Ok(rows) = stmt.query_map(params![fid], |row| {
1961 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1962 })
1963 {
1964 for row in rows.flatten() {
1965 let (from_id, t_id) = row;
1966 all_rels.insert((from_id, fid, t_id));
1967 if all_ids.insert(from_id) {
1968 next_frontier.insert(from_id);
1969 }
1970 }
1971 }
1972 }
1973 }
1974
1975 if all_ids.len() > MAX_TRAVERSAL_ENTITIES || all_rels.len() > MAX_TRAVERSAL_RELS {
1979 break;
1980 }
1981
1982 frontier = next_frontier;
1983 cur_depth += 1;
1984 }
1985
1986 let entities_json: String = if all_ids.is_empty() {
1987 "[]".to_string()
1988 } else {
1989 let ids: Vec<i64> = all_ids.iter().copied().collect();
1990 let sql = format!(
1991 "SELECT COALESCE(json_group_array(json_object(
1992 'name', e.name,
1993 'entityType', t.name,
1994 'observations', COALESCE((
1995 SELECT json_group_array({OBSERVATION_JSON} ORDER BY o.idx, o.id)
1996 FROM observation o WHERE o.entity_id = e.id
1997 ), json('[]'))
1998 ) ORDER BY e.id), json('[]'))
1999 FROM entity e
2000 JOIN type_dict t ON t.id = e.type_id
2001 WHERE e.id IN ({}) AND e.flags = 0",
2002 int_csv(&ids)
2003 );
2004 conn.query_row(&sql, [], |row| row.get::<_, String>(0))
2005 .map_err(sqlite_err)?
2006 };
2007
2008 let relations_json: String = if all_rels.is_empty() {
2009 "[]".to_string()
2010 } else {
2011 let sql = format!(
2012 "WITH r(from_id, to_id, type_id) AS (VALUES {})
2013 SELECT COALESCE(json_group_array(json_object(
2014 'from', e1.name,
2015 'to', e2.name,
2016 'relationType', t.name
2017 )), json('[]'))
2018 FROM r
2019 JOIN entity e1 ON e1.id = r.from_id
2020 JOIN entity e2 ON e2.id = r.to_id
2021 JOIN type_dict t ON t.id = r.type_id
2022 WHERE e1.flags = 0 AND e2.flags = 0",
2023 rel_values_literal(&all_rels)
2024 );
2025 conn.query_row(&sql, [], |row| row.get::<_, String>(0))
2026 .map_err(sqlite_err)?
2027 };
2028
2029 let mut out = String::with_capacity(32 + entities_json.len() + relations_json.len());
2030 out.push_str("{\"entities\":");
2031 out.push_str(&entities_json);
2032 out.push_str(",\"relations\":");
2033 out.push_str(&relations_json);
2034 out.push('}');
2035 Ok(out)
2036 }
2037}
2038
2039#[cfg(test)]
2042mod tests {
2043 use super::*;
2044 use crate::types::EntityInput as Entity;
2045 use serde_json::Value;
2046 use std::ops::Deref;
2047 use std::path::PathBuf;
2048
2049 struct TestKg(GraphHandle, PathBuf);
2050
2051 impl Deref for TestKg {
2052 type Target = GraphHandle;
2053 fn deref(&self) -> &GraphHandle {
2054 &self.0
2055 }
2056 }
2057
2058 impl Drop for TestKg {
2059 fn drop(&mut self) {
2060 cleanup_db(&self.1);
2061 }
2062 }
2063
2064 fn cleanup_db(path: &std::path::Path) {
2065 let _ = std::fs::remove_file(path);
2066 let _ = std::fs::remove_file(path.with_extension("db-wal"));
2067 let _ = std::fs::remove_file(path.with_extension("db-shm"));
2068 }
2069
2070 fn new_kg() -> TestKg {
2071 use std::sync::atomic::AtomicU64;
2072 use std::sync::atomic::Ordering;
2073 static COUNTER: AtomicU64 = AtomicU64::new(0);
2074 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
2075 let dir = std::env::temp_dir();
2076 let path = dir.join(format!("kg_test_{}_{}.db", std::process::id(), n));
2077 cleanup_db(&path);
2078 let kg = GraphHandle::new(
2079 &path,
2080 Durability::Async,
2081 SqliteTuning::default(),
2082 NonZeroUsize::new(10000).unwrap(),
2083 4,
2084 )
2085 .expect("create KG");
2086 TestKg(kg, path)
2087 }
2088
2089 #[test]
2090 fn test_create_and_get_entity() {
2091 let kg = new_kg();
2092 let entities = vec![Entity {
2093 name: "test".into(),
2094 entity_type: "person".into(),
2095 observations: vec!["obs1".into(), "obs2".into()],
2096 }];
2097 let created = kg.create_entities(&entities).unwrap();
2098 assert_eq!(created.len(), 1);
2099
2100 let got = kg.get_entity("test").unwrap().unwrap();
2101 assert_eq!(got.name, "test");
2102 assert_eq!(got.entity_type, "person");
2103 assert_eq!(
2104 got.observations
2105 .iter()
2106 .map(|o| o.body.as_str())
2107 .collect::<Vec<_>>(),
2108 vec!["obs1", "obs2"]
2109 );
2110 }
2111
2112 #[test]
2113 fn test_get_nonexistent() {
2114 let kg = new_kg();
2115 assert!(kg.get_entity("nonexistent").unwrap().is_none());
2116 }
2117
2118 #[test]
2119 fn test_delete_entity() {
2120 let kg = new_kg();
2121 kg.create_entities(&[Entity {
2122 name: "del".into(),
2123 entity_type: "t".into(),
2124 observations: vec![],
2125 }])
2126 .unwrap();
2127 assert!(kg.get_entity("del").unwrap().is_some());
2128 kg.delete_entities(&["del".to_string()]).unwrap();
2129 assert!(kg.get_entity("del").unwrap().is_none());
2130 }
2131
2132 #[test]
2133 fn test_add_and_delete_observations() {
2134 let kg = new_kg();
2135 kg.create_entities(&[Entity {
2136 name: "obs_test".into(),
2137 entity_type: "t".into(),
2138 observations: vec!["a".into()],
2139 }])
2140 .unwrap();
2141
2142 let added = kg
2143 .add_observations("obs_test", &["b".into(), "c".into()])
2144 .unwrap();
2145 assert_eq!(added.len(), 2);
2146
2147 let ent = kg.get_entity("obs_test").unwrap().unwrap();
2148 assert!(ent.observations.iter().any(|o| o.body == "b"));
2149 assert!(ent.observations.iter().any(|o| o.body == "c"));
2150
2151 kg.delete_observations("obs_test", &["b".into()]).unwrap();
2152 let ent = kg.get_entity("obs_test").unwrap().unwrap();
2153 assert!(!ent.observations.iter().any(|o| o.body == "b"));
2154 assert!(ent.observations.iter().any(|o| o.body == "c"));
2155 assert!(ent.observations.iter().any(|o| o.body == "a"));
2156 }
2157
2158 #[test]
2159 fn test_create_relations() {
2160 let kg = new_kg();
2161 kg.create_entities(&[
2162 Entity {
2163 name: "A".into(),
2164 entity_type: "node".into(),
2165 observations: vec![],
2166 },
2167 Entity {
2168 name: "B".into(),
2169 entity_type: "node".into(),
2170 observations: vec![],
2171 },
2172 ])
2173 .unwrap();
2174
2175 let rels = kg
2176 .create_relations(&[Relation {
2177 from: "A".into(),
2178 to: "B".into(),
2179 relation_type: "edge".into(),
2180 }])
2181 .unwrap();
2182 assert_eq!(rels.len(), 1);
2183
2184 assert_eq!(kg.get_entity_count().unwrap(), 2);
2185 assert_eq!(kg.get_relation_count().unwrap(), 1);
2186 }
2187
2188 #[test]
2189 fn test_search_nodes() {
2190 let kg = new_kg();
2191 kg.create_entities(&[Entity {
2192 name: "Einstein".into(),
2193 entity_type: "scientist".into(),
2194 observations: vec!["physics".into(), "relativity".into()],
2195 }])
2196 .unwrap();
2197
2198 let results = kg.search_nodes_filtered("physics", None, 0, 10);
2199 assert_eq!(results.len(), 1);
2200 assert_eq!(results[0].name, "Einstein");
2201
2202 let results = kg.search_nodes_filtered("physics", Some("scientist"), 0, 10);
2203 assert_eq!(results.len(), 1);
2204
2205 let results = kg.search_nodes_filtered("physics", Some("nonexistent"), 0, 10);
2206 assert_eq!(results.len(), 0);
2207 }
2208
2209 #[test]
2210 fn test_find_path() {
2211 let kg = new_kg();
2212 kg.create_entities(&[
2213 Entity {
2214 name: "A".into(),
2215 entity_type: "n".into(),
2216 observations: vec![],
2217 },
2218 Entity {
2219 name: "B".into(),
2220 entity_type: "n".into(),
2221 observations: vec![],
2222 },
2223 Entity {
2224 name: "C".into(),
2225 entity_type: "n".into(),
2226 observations: vec![],
2227 },
2228 ])
2229 .unwrap();
2230
2231 kg.create_relations(&[
2232 Relation {
2233 from: "A".into(),
2234 to: "B".into(),
2235 relation_type: "e".into(),
2236 },
2237 Relation {
2238 from: "B".into(),
2239 to: "C".into(),
2240 relation_type: "e".into(),
2241 },
2242 ])
2243 .unwrap();
2244
2245 let path = kg.find_path("A", "C").unwrap().unwrap();
2246 assert_eq!(path, vec!["A", "B", "C"]);
2247 }
2248
2249 #[test]
2250 fn test_degree() {
2251 let kg = new_kg();
2252 kg.create_entities(&[
2253 Entity {
2254 name: "A".into(),
2255 entity_type: "n".into(),
2256 observations: vec![],
2257 },
2258 Entity {
2259 name: "B".into(),
2260 entity_type: "n".into(),
2261 observations: vec![],
2262 },
2263 Entity {
2264 name: "C".into(),
2265 entity_type: "n".into(),
2266 observations: vec![],
2267 },
2268 ])
2269 .unwrap();
2270
2271 kg.create_relations(&[
2272 Relation {
2273 from: "A".into(),
2274 to: "B".into(),
2275 relation_type: "e".into(),
2276 },
2277 Relation {
2278 from: "A".into(),
2279 to: "C".into(),
2280 relation_type: "e".into(),
2281 },
2282 ])
2283 .unwrap();
2284
2285 assert_eq!(kg.degree("A", Direction::Outgoing).unwrap(), 2);
2286 assert_eq!(kg.degree("A", Direction::Incoming).unwrap(), 0);
2287 assert_eq!(kg.degree("B", Direction::Incoming).unwrap(), 1);
2288 }
2289
2290 #[test]
2291 fn test_neighbors() {
2292 let kg = new_kg();
2293 kg.create_entities(&[
2294 Entity {
2295 name: "A".into(),
2296 entity_type: "n".into(),
2297 observations: vec![],
2298 },
2299 Entity {
2300 name: "B".into(),
2301 entity_type: "n".into(),
2302 observations: vec![],
2303 },
2304 ])
2305 .unwrap();
2306
2307 kg.create_relations(&[Relation {
2308 from: "A".into(),
2309 to: "B".into(),
2310 relation_type: "e".into(),
2311 }])
2312 .unwrap();
2313
2314 let result = kg.neighbors("A", Direction::Outgoing, None, 1).unwrap();
2315 let v: Value = serde_json::from_str(&result).unwrap();
2316 assert_eq!(v["entities"].as_array().unwrap().len(), 2);
2317 assert_eq!(v["relations"].as_array().unwrap().len(), 1);
2318 }
2319
2320 #[test]
2321 fn test_open_nodes() {
2322 let kg = new_kg();
2323 kg.create_entities(&[
2324 Entity {
2325 name: "X".into(),
2326 entity_type: "n".into(),
2327 observations: vec!["obs_x".into()],
2328 },
2329 Entity {
2330 name: "Y".into(),
2331 entity_type: "n".into(),
2332 observations: vec!["obs_y".into()],
2333 },
2334 ])
2335 .unwrap();
2336
2337 kg.create_relations(&[Relation {
2338 from: "X".into(),
2339 to: "Y".into(),
2340 relation_type: "e".into(),
2341 }])
2342 .unwrap();
2343
2344 let result = kg.open_nodes(&["X".into()]);
2345 let v: Value = serde_json::from_str(&result).unwrap();
2346 assert_eq!(v["entities"].as_array().unwrap().len(), 1);
2347 assert_eq!(v["relations"].as_array().unwrap().len(), 1);
2348 }
2349
2350 #[test]
2351 fn test_entities_exist() {
2352 let kg = new_kg();
2353 kg.create_entities(&[Entity {
2354 name: "exists".into(),
2355 entity_type: "t".into(),
2356 observations: vec![],
2357 }])
2358 .unwrap();
2359
2360 let res = kg
2361 .entities_exist(&["exists".into(), "missing".into()])
2362 .unwrap();
2363 assert_eq!(res, vec![true, false]);
2364 }
2365
2366 #[test]
2367 fn test_describe_entity() {
2368 let kg = new_kg();
2369 kg.create_entities(&[
2370 Entity {
2371 name: "A".into(),
2372 entity_type: "t".into(),
2373 observations: vec!["o".into()],
2374 },
2375 Entity {
2376 name: "B".into(),
2377 entity_type: "t".into(),
2378 observations: vec![],
2379 },
2380 Entity {
2381 name: "C".into(),
2382 entity_type: "t".into(),
2383 observations: vec![],
2384 },
2385 ])
2386 .unwrap();
2387
2388 kg.create_relations(&[
2389 Relation {
2390 from: "B".into(),
2391 to: "A".into(),
2392 relation_type: "inbound".into(),
2393 },
2394 Relation {
2395 from: "A".into(),
2396 to: "B".into(),
2397 relation_type: "outbound".into(),
2398 },
2399 Relation {
2400 from: "A".into(),
2401 to: "C".into(),
2402 relation_type: "other".into(),
2403 },
2404 Relation {
2405 from: "A".into(),
2406 to: "A".into(),
2407 relation_type: "self".into(),
2408 },
2409 ])
2410 .unwrap();
2411
2412 let entity = kg.describe_entity("A").unwrap();
2413 assert_eq!(entity.name, "A");
2414 assert_eq!(entity.entity_type, "t");
2415 assert_eq!(
2416 entity
2417 .observations
2418 .iter()
2419 .map(|o| o.body.as_str())
2420 .collect::<Vec<_>>(),
2421 ["o"]
2422 );
2423 assert_eq!(entity.relations.len(), 4);
2424 assert_eq!(
2425 entity.relations,
2426 vec![
2427 Relation {
2428 from: "A".into(),
2429 to: "A".into(),
2430 relation_type: "self".into(),
2431 },
2432 Relation {
2433 from: "A".into(),
2434 to: "B".into(),
2435 relation_type: "outbound".into(),
2436 },
2437 Relation {
2438 from: "A".into(),
2439 to: "C".into(),
2440 relation_type: "other".into(),
2441 },
2442 Relation {
2443 from: "B".into(),
2444 to: "A".into(),
2445 relation_type: "inbound".into(),
2446 },
2447 ]
2448 );
2449 assert_eq!(entity.neighbors, ["A", "B", "C"]);
2450 assert_eq!(entity.degree.incoming, 2);
2451 assert_eq!(entity.degree.outgoing, 3);
2452
2453 kg.writer
2457 .lock()
2458 .execute(
2459 "UPDATE entity SET out_deg = 99, in_deg = 88 WHERE name = 'A'",
2460 [],
2461 )
2462 .unwrap();
2463 let entity = kg.describe_entity("A").unwrap();
2464 assert_eq!(entity.degree.incoming, 2);
2465 assert_eq!(entity.degree.outgoing, 3);
2466 assert!(kg.describe_entity("missing").is_err());
2467 }
2468
2469 #[test]
2470 fn test_entity_type_counts() {
2471 let kg = new_kg();
2472 kg.create_entities(&[
2473 Entity {
2474 name: "a".into(),
2475 entity_type: "person".into(),
2476 observations: vec![],
2477 },
2478 Entity {
2479 name: "b".into(),
2480 entity_type: "person".into(),
2481 observations: vec![],
2482 },
2483 Entity {
2484 name: "c".into(),
2485 entity_type: "place".into(),
2486 observations: vec![],
2487 },
2488 ])
2489 .unwrap();
2490
2491 let counts = kg.entity_type_counts();
2492 let map: FxHashMap<_, _> = counts.into_iter().collect();
2493 assert_eq!(map.get("person"), Some(&2));
2494 assert_eq!(map.get("place"), Some(&1));
2495 }
2496
2497 #[test]
2498 fn test_relation_type_counts() {
2499 let kg = new_kg();
2500 kg.create_entities(&[
2501 Entity {
2502 name: "a".into(),
2503 entity_type: "n".into(),
2504 observations: vec![],
2505 },
2506 Entity {
2507 name: "b".into(),
2508 entity_type: "n".into(),
2509 observations: vec![],
2510 },
2511 Entity {
2512 name: "c".into(),
2513 entity_type: "n".into(),
2514 observations: vec![],
2515 },
2516 ])
2517 .unwrap();
2518
2519 kg.create_relations(&[
2520 Relation {
2521 from: "a".into(),
2522 to: "b".into(),
2523 relation_type: "knows".into(),
2524 },
2525 Relation {
2526 from: "a".into(),
2527 to: "c".into(),
2528 relation_type: "knows".into(),
2529 },
2530 ])
2531 .unwrap();
2532
2533 let counts = kg.relation_type_counts();
2534 let map: FxHashMap<_, _> = counts.into_iter().collect();
2535 assert_eq!(map.get("knows"), Some(&2));
2536 }
2537
2538 #[test]
2539 fn test_upsert_entities() {
2540 let kg = new_kg();
2541 kg.create_entities(&[Entity {
2542 name: "A".into(),
2543 entity_type: "OldType".into(),
2544 observations: vec!["old".into()],
2545 }])
2546 .unwrap();
2547 kg.create_relations(&[Relation {
2548 from: "A".into(),
2549 to: "A".into(),
2550 relation_type: "self".into(),
2551 }])
2552 .unwrap();
2553
2554 kg.upsert_entities(&[Entity {
2556 name: "A".into(),
2557 entity_type: "NewType".into(),
2558 observations: vec!["old".into(), "new".into()],
2559 }])
2560 .unwrap();
2561
2562 assert_eq!(kg.get_entity_count().unwrap(), 1);
2563 let ent = kg.get_entity("A").unwrap().unwrap();
2564 assert_eq!(ent.entity_type, "NewType");
2565 assert_eq!(
2566 ent.observations
2567 .iter()
2568 .map(|o| o.body.as_str())
2569 .collect::<Vec<_>>(),
2570 ["old", "new"]
2571 );
2572
2573 let type_counts: FxHashMap<_, _> = kg.entity_type_counts().into_iter().collect();
2574 assert_eq!(type_counts.get("OldType"), None);
2575 assert_eq!(type_counts.get("NewType"), Some(&1));
2576
2577 assert_eq!(
2578 kg.search_relations(Some("A"), Some("A"), Some("self"), None),
2579 [Relation {
2580 from: "A".into(),
2581 to: "A".into(),
2582 relation_type: "self".into(),
2583 }]
2584 );
2585 }
2586
2587 #[test]
2588 fn test_merge_entities() {
2589 let kg = new_kg();
2590 kg.create_entities(&[
2591 Entity {
2592 name: "source".into(),
2593 entity_type: "t".into(),
2594 observations: vec!["src_obs".into()],
2595 },
2596 Entity {
2597 name: "target".into(),
2598 entity_type: "t".into(),
2599 observations: vec!["tgt_obs".into()],
2600 },
2601 ])
2602 .unwrap();
2603
2604 kg.create_relations(&[Relation {
2605 from: "source".into(),
2606 to: "target".into(),
2607 relation_type: "e".into(),
2608 }])
2609 .unwrap();
2610
2611 let merged = kg.merge_entities("source", "target").unwrap();
2612 assert_eq!(merged.name, "target");
2613 assert!(kg.get_entity("source").unwrap().is_none());
2614 }
2615
2616 #[test]
2617 fn test_find_all_paths() {
2618 let kg = new_kg();
2619 kg.create_entities(&[
2620 Entity {
2621 name: "A".into(),
2622 entity_type: "n".into(),
2623 observations: vec![],
2624 },
2625 Entity {
2626 name: "B".into(),
2627 entity_type: "n".into(),
2628 observations: vec![],
2629 },
2630 Entity {
2631 name: "C".into(),
2632 entity_type: "n".into(),
2633 observations: vec![],
2634 },
2635 ])
2636 .unwrap();
2637
2638 kg.create_relations(&[
2639 Relation {
2640 from: "A".into(),
2641 to: "B".into(),
2642 relation_type: "e".into(),
2643 },
2644 Relation {
2645 from: "B".into(),
2646 to: "C".into(),
2647 relation_type: "e".into(),
2648 },
2649 Relation {
2650 from: "A".into(),
2651 to: "C".into(),
2652 relation_type: "e".into(),
2653 },
2654 ])
2655 .unwrap();
2656
2657 let paths = kg.find_all_paths("A", "C", 5, 10).unwrap();
2658 assert!(paths.len() >= 2);
2659 }
2660
2661 #[test]
2662 fn test_batch_get_entities() {
2663 let kg = new_kg();
2664 kg.create_entities(&[
2665 Entity {
2666 name: "a".into(),
2667 entity_type: "t".into(),
2668 observations: vec![],
2669 },
2670 Entity {
2671 name: "b".into(),
2672 entity_type: "t".into(),
2673 observations: vec![],
2674 },
2675 ])
2676 .unwrap();
2677
2678 let results = kg.batch_get_entities(&["a".into(), "missing".into(), "b".into()]);
2679 assert_eq!(results.len(), 3);
2680 assert!(results[0].is_some());
2681 assert!(results[1].is_none());
2682 assert!(results[2].is_some());
2683 }
2684
2685 #[test]
2686 fn test_export_graph() {
2687 let kg = new_kg();
2688 kg.create_entities(&[Entity {
2689 name: "exp".into(),
2690 entity_type: "t".into(),
2691 observations: vec!["o".into()],
2692 }])
2693 .unwrap();
2694
2695 let exported = kg.export("json", i64::MAX).unwrap();
2696 assert!(exported.contains("exp"));
2697 assert!(exported.contains("o"));
2698 }
2699
2700 #[test]
2701 fn test_graph_stats() {
2702 let kg = new_kg();
2703 assert_eq!(kg.get_entity_count().unwrap(), 0);
2704 assert_eq!(kg.get_relation_count().unwrap(), 0);
2705
2706 kg.create_entities(&[Entity {
2707 name: "s".into(),
2708 entity_type: "t".into(),
2709 observations: vec![],
2710 }])
2711 .unwrap();
2712
2713 assert_eq!(kg.get_entity_count().unwrap(), 1);
2714 }
2715
2716 #[test]
2717 fn test_read_graph_filtered() {
2718 let kg = new_kg();
2719 kg.create_entities(&[
2720 Entity {
2721 name: "p1".into(),
2722 entity_type: "person".into(),
2723 observations: vec![],
2724 },
2725 Entity {
2726 name: "p2".into(),
2727 entity_type: "place".into(),
2728 observations: vec![],
2729 },
2730 ])
2731 .unwrap();
2732
2733 let out = kg.read_graph_filtered(Some("person"), 0, 10).unwrap();
2734 let v: Value = serde_json::from_str(&out).unwrap();
2735 assert_eq!(v["entities"].as_array().unwrap().len(), 1);
2736 assert_eq!(v["entities"][0]["name"], "p1");
2737 }
2738
2739 #[test]
2740 fn test_wipe() {
2741 let kg = new_kg();
2742 kg.create_entities(&[Entity {
2743 name: "w".into(),
2744 entity_type: "t".into(),
2745 observations: vec!["o".into()],
2746 }])
2747 .unwrap();
2748 assert_eq!(kg.get_entity_count().unwrap(), 1);
2749
2750 kg.wipe().unwrap();
2751 assert_eq!(kg.get_entity_count().unwrap(), 0);
2752 }
2753
2754 #[test]
2755 fn test_push_json_str() {
2756 let mut buf = String::new();
2757 push_json_str(&mut buf, "hello");
2758 assert_eq!(buf, "\"hello\"");
2759 let mut buf = String::new();
2760 push_json_str(&mut buf, "he\"llo");
2761 assert_eq!(buf, "\"he\\\"llo\"");
2762 }
2763
2764 #[test]
2767 fn test_create_entities_empty_input() {
2768 let kg = new_kg();
2769 let created = kg.create_entities(&[]).unwrap();
2770 assert!(created.is_empty());
2771 }
2772
2773 #[test]
2774 fn test_create_entities_skip_empty_name() {
2775 let kg = new_kg();
2776 let created = kg
2777 .create_entities(&[Entity {
2778 name: "".into(),
2779 entity_type: "t".into(),
2780 observations: vec![],
2781 }])
2782 .unwrap();
2783 assert!(created.is_empty());
2784 assert_eq!(kg.get_entity_count().unwrap(), 0);
2785 }
2786
2787 #[test]
2788 fn test_create_entities_duplicate_names() {
2789 let kg = new_kg();
2790 let e = Entity {
2791 name: "dup".into(),
2792 entity_type: "t".into(),
2793 observations: vec!["obs".into()],
2794 };
2795 let first = kg.create_entities(std::slice::from_ref(&e)).unwrap();
2796 assert_eq!(first.len(), 1);
2797 let second = kg.create_entities(&[e]).unwrap();
2798 assert!(second.is_empty());
2799 assert_eq!(kg.get_entity_count().unwrap(), 1);
2800 }
2801
2802 #[test]
2803 fn test_create_entities_partial_duplicates() {
2804 let kg = new_kg();
2805 let created = kg
2806 .create_entities(&[
2807 Entity {
2808 name: "a".into(),
2809 entity_type: "t".into(),
2810 observations: vec![],
2811 },
2812 Entity {
2813 name: "b".into(),
2814 entity_type: "t".into(),
2815 observations: vec![],
2816 },
2817 ])
2818 .unwrap();
2819 assert_eq!(created.len(), 2);
2820
2821 let second = kg
2822 .create_entities(&[
2823 Entity {
2824 name: "b".into(),
2825 entity_type: "t".into(),
2826 observations: vec![],
2827 },
2828 Entity {
2829 name: "c".into(),
2830 entity_type: "t".into(),
2831 observations: vec![],
2832 },
2833 ])
2834 .unwrap();
2835 assert_eq!(second.len(), 1); assert_eq!(second[0].name, "c");
2837 assert_eq!(kg.get_entity_count().unwrap(), 3);
2838 }
2839
2840 #[test]
2841 fn test_create_entities_mixed_empty_and_valid() {
2842 let kg = new_kg();
2843 let created = kg
2844 .create_entities(&[
2845 Entity {
2846 name: "".into(),
2847 entity_type: "t".into(),
2848 observations: vec![],
2849 },
2850 Entity {
2851 name: "valid".into(),
2852 entity_type: "t".into(),
2853 observations: vec![],
2854 },
2855 Entity {
2856 name: "".into(),
2857 entity_type: "t".into(),
2858 observations: vec![],
2859 },
2860 ])
2861 .unwrap();
2862 assert_eq!(created.len(), 1);
2863 assert_eq!(created[0].name, "valid");
2864 assert_eq!(kg.get_entity_count().unwrap(), 1);
2865 }
2866
2867 #[test]
2868 fn test_create_entities_same_name_in_batch() {
2869 let kg = new_kg();
2870 let created = kg
2871 .create_entities(&[
2872 Entity {
2873 name: "dup_in_batch".into(),
2874 entity_type: "t".into(),
2875 observations: vec![],
2876 },
2877 Entity {
2878 name: "dup_in_batch".into(),
2879 entity_type: "t".into(),
2880 observations: vec![],
2881 },
2882 ])
2883 .unwrap();
2884 assert_eq!(created.len(), 1);
2885 assert_eq!(kg.get_entity_count().unwrap(), 1);
2886 }
2887
2888 #[test]
2891 fn test_create_relations_empty_input() {
2892 let kg = new_kg();
2893 let rels = kg.create_relations(&[]).unwrap();
2894 assert!(rels.is_empty());
2895 }
2896
2897 #[test]
2898 fn test_create_relations_nonexistent_from() {
2899 let kg = new_kg();
2900 kg.create_entities(&[Entity {
2901 name: "B".into(),
2902 entity_type: "t".into(),
2903 observations: vec![],
2904 }])
2905 .unwrap();
2906
2907 let rels = kg
2908 .create_relations(&[Relation {
2909 from: "A".into(),
2910 to: "B".into(),
2911 relation_type: "e".into(),
2912 }])
2913 .unwrap();
2914 assert!(rels.is_empty());
2915 assert_eq!(kg.get_relation_count().unwrap(), 0);
2916 }
2917
2918 #[test]
2919 fn test_create_relations_nonexistent_to() {
2920 let kg = new_kg();
2921 kg.create_entities(&[Entity {
2922 name: "A".into(),
2923 entity_type: "t".into(),
2924 observations: vec![],
2925 }])
2926 .unwrap();
2927
2928 let rels = kg
2929 .create_relations(&[Relation {
2930 from: "A".into(),
2931 to: "B".into(),
2932 relation_type: "e".into(),
2933 }])
2934 .unwrap();
2935 assert!(rels.is_empty());
2936 assert_eq!(kg.get_relation_count().unwrap(), 0);
2937 }
2938
2939 #[test]
2940 fn test_create_relations_both_nonexistent() {
2941 let kg = new_kg();
2942 let rels = kg
2943 .create_relations(&[Relation {
2944 from: "A".into(),
2945 to: "B".into(),
2946 relation_type: "e".into(),
2947 }])
2948 .unwrap();
2949 assert!(rels.is_empty());
2950 }
2951
2952 #[test]
2953 fn test_create_relations_self_loop() {
2954 let kg = new_kg();
2955 kg.create_entities(&[Entity {
2956 name: "self".into(),
2957 entity_type: "t".into(),
2958 observations: vec![],
2959 }])
2960 .unwrap();
2961
2962 let rels = kg
2963 .create_relations(&[Relation {
2964 from: "self".into(),
2965 to: "self".into(),
2966 relation_type: "loop".into(),
2967 }])
2968 .unwrap();
2969 assert_eq!(rels.len(), 1);
2970 assert_eq!(kg.get_relation_count().unwrap(), 1);
2971 assert_eq!(kg.degree("self", Direction::Outgoing).unwrap(), 1);
2972 assert_eq!(kg.degree("self", Direction::Incoming).unwrap(), 1);
2973 }
2974
2975 #[test]
2976 fn test_create_relations_duplicate() {
2977 let kg = new_kg();
2978 kg.create_entities(&[
2979 Entity {
2980 name: "A".into(),
2981 entity_type: "t".into(),
2982 observations: vec![],
2983 },
2984 Entity {
2985 name: "B".into(),
2986 entity_type: "t".into(),
2987 observations: vec![],
2988 },
2989 ])
2990 .unwrap();
2991
2992 let r = Relation {
2993 from: "A".into(),
2994 to: "B".into(),
2995 relation_type: "e".into(),
2996 };
2997 let first = kg.create_relations(std::slice::from_ref(&r)).unwrap();
2998 assert_eq!(first.len(), 1);
2999
3000 let second = kg.create_relations(&[r]).unwrap();
3001 assert!(second.is_empty());
3002 assert_eq!(kg.get_relation_count().unwrap(), 1);
3003 }
3004
3005 #[test]
3006 fn test_create_relations_new_type_auto_created() {
3007 let kg = new_kg();
3008 kg.create_entities(&[
3009 Entity {
3010 name: "A".into(),
3011 entity_type: "t".into(),
3012 observations: vec![],
3013 },
3014 Entity {
3015 name: "B".into(),
3016 entity_type: "t".into(),
3017 observations: vec![],
3018 },
3019 ])
3020 .unwrap();
3021
3022 let rels = kg
3023 .create_relations(&[Relation {
3024 from: "A".into(),
3025 to: "B".into(),
3026 relation_type: "brand_new_type".into(),
3027 }])
3028 .unwrap();
3029 assert_eq!(rels.len(), 1);
3030
3031 let counts = kg.relation_type_counts();
3032 let map: FxHashMap<_, _> = counts.into_iter().collect();
3033 assert_eq!(map.get("brand_new_type"), Some(&1));
3034 }
3035
3036 #[test]
3037 fn test_create_relations_degree_updates() {
3038 let kg = new_kg();
3039 kg.create_entities(&[
3040 Entity {
3041 name: "A".into(),
3042 entity_type: "t".into(),
3043 observations: vec![],
3044 },
3045 Entity {
3046 name: "B".into(),
3047 entity_type: "t".into(),
3048 observations: vec![],
3049 },
3050 Entity {
3051 name: "C".into(),
3052 entity_type: "t".into(),
3053 observations: vec![],
3054 },
3055 ])
3056 .unwrap();
3057
3058 kg.create_relations(&[
3059 Relation {
3060 from: "A".into(),
3061 to: "B".into(),
3062 relation_type: "e".into(),
3063 },
3064 Relation {
3065 from: "A".into(),
3066 to: "C".into(),
3067 relation_type: "e".into(),
3068 },
3069 ])
3070 .unwrap();
3071
3072 assert_eq!(kg.degree("A", Direction::Outgoing).unwrap(), 2);
3073 assert_eq!(kg.degree("A", Direction::Incoming).unwrap(), 0);
3074 assert_eq!(kg.degree("B", Direction::Incoming).unwrap(), 1);
3075 assert_eq!(kg.degree("C", Direction::Incoming).unwrap(), 1);
3076 assert_eq!(kg.degree("A", Direction::Both).unwrap(), 2);
3077 }
3078
3079 #[test]
3080 fn test_create_relations_delete_and_recreate() {
3081 let kg = new_kg();
3082 kg.create_entities(&[
3083 Entity {
3084 name: "A".into(),
3085 entity_type: "t".into(),
3086 observations: vec![],
3087 },
3088 Entity {
3089 name: "B".into(),
3090 entity_type: "t".into(),
3091 observations: vec![],
3092 },
3093 ])
3094 .unwrap();
3095
3096 let r = Relation {
3097 from: "A".into(),
3098 to: "B".into(),
3099 relation_type: "e".into(),
3100 };
3101 kg.create_relations(std::slice::from_ref(&r)).unwrap();
3102 assert_eq!(kg.get_relation_count().unwrap(), 1);
3103
3104 kg.delete_relations(std::slice::from_ref(&r)).unwrap();
3105 assert_eq!(kg.get_relation_count().unwrap(), 0);
3106
3107 let re = kg.create_relations(&[r]).unwrap();
3109 assert_eq!(re.len(), 1);
3110 assert_eq!(kg.get_relation_count().unwrap(), 1);
3111 }
3112
3113 #[test]
3116 fn test_create_entities_then_relations_then_delete_entity_with_relations() {
3117 let kg = new_kg();
3118 kg.create_entities(&[
3119 Entity {
3120 name: "A".into(),
3121 entity_type: "t".into(),
3122 observations: vec![],
3123 },
3124 Entity {
3125 name: "B".into(),
3126 entity_type: "t".into(),
3127 observations: vec![],
3128 },
3129 ])
3130 .unwrap();
3131 kg.create_relations(&[Relation {
3132 from: "A".into(),
3133 to: "B".into(),
3134 relation_type: "e".into(),
3135 }])
3136 .unwrap();
3137
3138 assert_eq!(kg.get_relation_count().unwrap(), 1);
3139
3140 kg.delete_entities(&["A".into()]).unwrap();
3142 assert!(kg.get_entity("A").unwrap().is_none());
3143 assert_eq!(kg.get_relation_count().unwrap(), 0);
3144 }
3145
3146 #[test]
3147 fn test_graph_stats_after_entity_with_observations() {
3148 let kg = new_kg();
3149 kg.create_entities(&[Entity {
3150 name: "stat".into(),
3151 entity_type: "t".into(),
3152 observations: vec!["o1".into(), "o2".into(), "o3".into()],
3153 }])
3154 .unwrap();
3155
3156 let ecount = kg.get_entity_count().unwrap();
3157 assert_eq!(ecount, 1);
3159
3160 kg.delete_entities(&["stat".into()]).unwrap();
3162 assert_eq!(kg.get_entity_count().unwrap(), 0);
3163 }
3164
3165 fn new_kg_with_pool(read_pool_size: usize) -> TestKg {
3168 use std::sync::atomic::AtomicU64;
3169 static COUNTER: AtomicU64 = AtomicU64::new(1_000_000);
3170 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
3171 let path = std::env::temp_dir().join(format!("kg_pool_{}_{}.db", std::process::id(), n));
3172 cleanup_db(&path);
3173 let kg = GraphHandle::new(
3174 &path,
3175 Durability::Async,
3176 SqliteTuning::default(),
3177 NonZeroUsize::new(10_000).unwrap(),
3178 read_pool_size,
3179 )
3180 .expect("create KG");
3181 TestKg(kg, path)
3182 }
3183
3184 fn seed_line(kg: &GraphHandle, n: usize) {
3185 let entities: Vec<Entity> = (0..n)
3186 .map(|i| Entity {
3187 name: format!("n{i}"),
3188 entity_type: "node".into(),
3189 observations: vec![format!("obs of n{i}").into()],
3190 })
3191 .collect();
3192 kg.create_entities(&entities).unwrap();
3193 let rels: Vec<Relation> = (0..n.saturating_sub(1))
3194 .map(|i| Relation {
3195 from: format!("n{i}"),
3196 to: format!("n{}", i + 1),
3197 relation_type: "edge".into(),
3198 })
3199 .collect();
3200 if !rels.is_empty() {
3201 kg.create_relations(&rels).unwrap();
3202 }
3203 }
3204
3205 fn count_relations(graph_json: &str) -> usize {
3206 let v: Value = serde_json::from_str(graph_json).unwrap();
3207 v["relations"].as_array().unwrap().len()
3208 }
3209
3210 fn count_entities(graph_json: &str) -> usize {
3211 let v: Value = serde_json::from_str(graph_json).unwrap();
3212 v["entities"].as_array().unwrap().len()
3213 }
3214
3215 #[test]
3218 fn test_pool_size_one_still_works() {
3219 let kg = new_kg_with_pool(1);
3220 seed_line(&kg, 5);
3221 assert_eq!(kg.get_entity_count().unwrap(), 5);
3222 assert!(kg.get_entity("n2").unwrap().is_some());
3223 let g = kg.read_graph_filtered(None, 0, usize::MAX).unwrap();
3224 assert_eq!(count_entities(&g), 5);
3225 }
3226
3227 #[test]
3228 fn test_reads_see_committed_writes() {
3229 let kg = new_kg_with_pool(4);
3232 kg.create_entities(&[Entity {
3233 name: "fresh".into(),
3234 entity_type: "t".into(),
3235 observations: vec!["v".into()],
3236 }])
3237 .unwrap();
3238 let got = kg.get_entity("fresh").unwrap().unwrap();
3240 assert_eq!(
3241 got.observations
3242 .iter()
3243 .map(|o| o.body.as_str())
3244 .collect::<Vec<_>>(),
3245 vec!["v"]
3246 );
3247 }
3248
3249 #[test]
3250 fn test_concurrent_readers_consistent() {
3251 let kg = new_kg_with_pool(4);
3254 seed_line(&kg, 50);
3255
3256 std::thread::scope(|s| {
3257 for _ in 0..8 {
3259 s.spawn(|| {
3260 for _ in 0..200 {
3261 let _ = kg.get_entity("n10");
3262 let _ = kg.search_nodes_filtered("obs", None, 0, 10);
3263 let _ = kg.read_graph_filtered(None, 0, 100);
3264 let _ = kg.get_entity_count();
3265 let _ = kg.neighbors("n10", Direction::Both, None, 2);
3266 }
3267 });
3268 }
3269 s.spawn(|| {
3271 for i in 100..160 {
3272 kg.create_entities(&[Entity {
3273 name: format!("w{i}"),
3274 entity_type: "node".into(),
3275 observations: vec![format!("w obs {i}").into()],
3276 }])
3277 .unwrap();
3278 }
3279 });
3280 });
3281
3282 assert_eq!(kg.get_entity_count().unwrap(), 110);
3284 assert!(kg.get_entity("w159").unwrap().is_some());
3285 }
3286
3287 #[test]
3288 fn test_reader_pool_rejects_writes_internally() {
3289 let kg = new_kg_with_pool(1);
3295 seed_line(&kg, 3);
3296 std::thread::scope(|s| {
3297 for _ in 0..4 {
3298 s.spawn(|| {
3299 for _ in 0..100 {
3300 let _ = kg.read_graph_filtered(None, 0, 10);
3301 }
3302 });
3303 }
3304 });
3305 assert_eq!(kg.get_entity_count().unwrap(), 3);
3306 }
3307
3308 #[test]
3311 fn test_read_graph_relations_scoped_to_page() {
3312 let kg = new_kg_with_pool(2);
3313 seed_line(&kg, 4);
3315
3316 let full = kg.read_graph_filtered(None, 0, usize::MAX).unwrap();
3318 assert_eq!(count_entities(&full), 4);
3319 assert_eq!(count_relations(&full), 3);
3320
3321 let page1 = kg.read_graph_filtered(None, 0, 1).unwrap();
3324 assert_eq!(count_entities(&page1), 1);
3325 assert_eq!(count_relations(&page1), 0);
3326
3327 let page2 = kg.read_graph_filtered(None, 0, 2).unwrap();
3330 assert_eq!(count_entities(&page2), 2);
3331 assert_eq!(count_relations(&page2), 1);
3332 }
3333
3334 #[test]
3335 fn test_read_graph_pagination_offset() {
3336 let kg = new_kg_with_pool(2);
3337 seed_line(&kg, 5);
3338 let g = kg.read_graph_filtered(None, 2, 2).unwrap();
3339 assert_eq!(count_entities(&g), 2);
3340 assert!(!g.contains("\"n0\""));
3342 assert!(!g.contains("\"n1\""));
3343 assert!(g.contains("\"n2\""));
3344 assert!(g.contains("\"n3\""));
3345 }
3346
3347 #[test]
3348 fn test_read_graph_empty() {
3349 let kg = new_kg_with_pool(2);
3350 let g = kg.read_graph_filtered(None, 0, usize::MAX).unwrap();
3351 assert_eq!(g, r#"{"entities":[],"relations":[]}"#);
3352 }
3353
3354 #[test]
3355 fn test_read_graph_filtered_by_type() {
3356 let kg = new_kg_with_pool(2);
3357 kg.create_entities(&[
3358 Entity {
3359 name: "p1".into(),
3360 entity_type: "person".into(),
3361 observations: vec![],
3362 },
3363 Entity {
3364 name: "q1".into(),
3365 entity_type: "place".into(),
3366 observations: vec![],
3367 },
3368 Entity {
3369 name: "p2".into(),
3370 entity_type: "person".into(),
3371 observations: vec![],
3372 },
3373 ])
3374 .unwrap();
3375 let g = kg
3376 .read_graph_filtered(Some("person"), 0, usize::MAX)
3377 .unwrap();
3378 assert_eq!(count_entities(&g), 2);
3379 assert!(g.contains("\"p1\""));
3380 assert!(g.contains("\"p2\""));
3381 assert!(!g.contains("\"q1\""));
3382 }
3383
3384 #[test]
3385 fn test_export_respects_max_rows() {
3386 let kg = new_kg_with_pool(2);
3387 seed_line(&kg, 5);
3388
3389 let full = kg.export("json", i64::MAX).unwrap();
3391 assert_eq!(count_entities(&full), 5);
3392 assert_eq!(count_relations(&full), 4);
3393
3394 let capped = kg.export("json", 2).unwrap();
3396 assert_eq!(count_entities(&capped), 2);
3397 assert_eq!(count_relations(&capped), 2);
3398 }
3399
3400 #[test]
3401 fn test_export_negative_max_rows_is_unbounded() {
3402 let kg = new_kg_with_pool(2);
3403 seed_line(&kg, 3);
3404 let out = kg.export("json", -1).unwrap();
3406 assert_eq!(count_entities(&out), 3);
3407 }
3408
3409 #[test]
3412 fn test_many_small_write_batches_stay_consistent() {
3413 let kg = new_kg_with_pool(2);
3414 for i in 0..100 {
3415 kg.create_entities(&[Entity {
3416 name: format!("e{i}"),
3417 entity_type: "t".into(),
3418 observations: vec![format!("o{i}").into()],
3419 }])
3420 .unwrap();
3421 }
3422 assert_eq!(kg.get_entity_count().unwrap(), 100);
3423 let hits = kg.search_nodes_filtered("e57", None, 0, 10);
3426 assert!(hits.iter().any(|e| e.name == "e57"));
3427 }
3428
3429 #[test]
3432 fn test_wipe_clears_name_and_obs_fts() {
3433 let kg = new_kg_with_pool(2);
3434 kg.create_entities(&[Entity {
3435 name: "Einstein".into(),
3436 entity_type: "scientist".into(),
3437 observations: vec!["physics".into()],
3438 }])
3439 .unwrap();
3440
3441 assert_eq!(kg.search_nodes_filtered("Einstein", None, 0, 10).len(), 1);
3443 assert_eq!(kg.search_nodes_filtered("physics", None, 0, 10).len(), 1);
3444
3445 kg.wipe().unwrap();
3446
3447 assert_eq!(kg.get_entity_count().unwrap(), 0);
3450 assert!(kg.search_nodes_filtered("Einstein", None, 0, 10).is_empty());
3451 assert!(kg.search_nodes_filtered("physics", None, 0, 10).is_empty());
3452 }
3453
3454 #[test]
3455 fn test_wipe_then_recreate_search_works() {
3456 let kg = new_kg_with_pool(2);
3459 kg.create_entities(&[Entity {
3460 name: "Einstein".into(),
3461 entity_type: "scientist".into(),
3462 observations: vec!["physics".into()],
3463 }])
3464 .unwrap();
3465 kg.wipe().unwrap();
3466
3467 kg.create_entities(&[Entity {
3468 name: "Einstein".into(),
3469 entity_type: "scientist".into(),
3470 observations: vec!["physics".into(), "relativity".into()],
3471 }])
3472 .unwrap();
3473
3474 let by_name = kg.search_nodes_filtered("Einstein", None, 0, 10);
3475 assert_eq!(by_name.len(), 1, "exactly one Einstein after recreate");
3476 let by_obs = kg.search_nodes_filtered("relativity", None, 0, 10);
3477 assert_eq!(by_obs.len(), 1);
3478 assert_eq!(kg.get_entity_count().unwrap(), 1);
3479 }
3480
3481 #[test]
3484 fn test_search_relations_missing_type_returns_empty() {
3485 let kg = new_kg_with_pool(2);
3486 seed_line(&kg, 3); let r = kg.search_relations(None, None, Some("does_not_exist"), None);
3490 assert!(r.is_empty());
3491 let types = kg.relation_type_counts();
3493 assert!(types.iter().all(|(t, _)| t != "does_not_exist"));
3494 }
3495
3496 #[test]
3497 fn test_entity_type_exists() {
3498 let kg = new_kg_with_pool(2);
3499 kg.create_entities(&[Entity {
3500 name: "a".into(),
3501 entity_type: "person".into(),
3502 observations: vec![],
3503 }])
3504 .unwrap();
3505 assert!(kg.entity_type_exists("person"));
3506 assert!(!kg.entity_type_exists("persn"));
3507 let types = kg.entity_type_counts();
3509 assert!(types.iter().all(|(t, _)| t != "persn"));
3510 }
3511
3512 #[test]
3513 fn test_type_descriptions_registry_and_catalog() {
3514 let kg = new_kg_with_pool(2);
3515 kg.set_type_description(0, "person", Some("A human being or persona"))
3517 .unwrap();
3518 let catalog = kg.entity_type_catalog();
3519 assert_eq!(
3520 catalog,
3521 vec![(
3522 "person".into(),
3523 0usize,
3524 Some("A human being or persona".into())
3525 )]
3526 );
3527 assert!(kg.entity_type_exists("person"));
3529 assert!(
3530 kg.entity_type_counts().is_empty(),
3531 "count 0 types stay out of counts"
3532 );
3533
3534 kg.set_type_description(0, "person", Some("A living human"))
3536 .unwrap();
3537 let catalog = kg.entity_type_catalog();
3538 assert_eq!(
3539 catalog,
3540 vec![("person".into(), 0usize, Some("A living human".into()))]
3541 );
3542
3543 kg.set_type_description(0, "person", None).unwrap();
3546 assert!(kg.entity_type_catalog().is_empty());
3547 assert!(
3548 kg.entity_type_exists("person"),
3549 "the row stays; only the desc clears"
3550 );
3551
3552 kg.create_entities(&[Entity {
3555 name: "alice".into(),
3556 entity_type: "person".into(),
3557 observations: vec![],
3558 }])
3559 .unwrap();
3560 assert_eq!(
3561 kg.entity_type_catalog(),
3562 vec![("person".into(), 1usize, None)]
3563 );
3564 assert_eq!(kg.entity_type_counts(), vec![("person".into(), 1usize)]);
3565
3566 kg.set_type_description(1, "works_at", Some("Employment link"))
3568 .unwrap();
3569 assert_eq!(
3570 kg.relation_type_catalog(),
3571 vec![("works_at".into(), 0usize, Some("Employment link".into()))]
3572 );
3573 assert!(kg.relation_type_exists("works_at"));
3574 }
3575
3576 #[test]
3577 fn test_type_description_length_not_capped_at_core() {
3578 let kg = new_kg_with_pool(2);
3580 let long = "x".repeat(20_000);
3581 kg.set_type_description(0, "person", Some(long.as_str()))
3582 .unwrap();
3583 assert_eq!(kg.entity_type_catalog().len(), 1);
3584 }
3585
3586 #[test]
3587 fn test_relation_type_exists() {
3588 let kg = new_kg_with_pool(2);
3589 kg.create_entities(&[
3590 Entity {
3591 name: "a".into(),
3592 entity_type: "person".into(),
3593 observations: vec![],
3594 },
3595 Entity {
3596 name: "b".into(),
3597 entity_type: "person".into(),
3598 observations: vec![],
3599 },
3600 ])
3601 .unwrap();
3602 kg.create_relations(&[Relation {
3603 from: "a".into(),
3604 to: "b".into(),
3605 relation_type: "knows".into(),
3606 }])
3607 .unwrap();
3608 assert!(kg.relation_type_exists("knows"));
3609 assert!(!kg.relation_type_exists("unknown_kind"));
3610 let types = kg.relation_type_counts();
3612 assert!(types.iter().all(|(t, _)| t != "unknown_kind"));
3613 }
3614
3615 #[test]
3616 fn test_search_relations_missing_from_returns_empty() {
3617 let kg = new_kg_with_pool(2);
3618 seed_line(&kg, 3);
3619 let r = kg.search_relations(Some("ghost"), None, None, None);
3620 assert!(r.is_empty(), "missing 'from' must not match every relation");
3621 }
3622
3623 #[test]
3624 fn test_search_relations_existing_filters_still_work() {
3625 let kg = new_kg_with_pool(2);
3626 seed_line(&kg, 3);
3627 let r = kg.search_relations(Some("n0"), None, Some("edge"), None);
3628 assert_eq!(r.len(), 1);
3629 assert_eq!(r[0].from, "n0");
3630 assert_eq!(r[0].to, "n1");
3631 }
3632
3633 #[test]
3634 fn test_neighbors_missing_type_returns_only_start() {
3635 let kg = new_kg_with_pool(2);
3636 seed_line(&kg, 3);
3637 let json = kg
3638 .neighbors("n0", Direction::Both, Some("nonexistent"), 2)
3639 .unwrap();
3640 assert_eq!(count_entities(&json), 1);
3642 assert_eq!(count_relations(&json), 0);
3643 }
3644
3645 #[test]
3646 fn test_neighbors_existing_type_filters() {
3647 let kg = new_kg_with_pool(2);
3648 kg.create_entities(&[
3649 Entity {
3650 name: "a".into(),
3651 entity_type: "n".into(),
3652 observations: vec![],
3653 },
3654 Entity {
3655 name: "b".into(),
3656 entity_type: "n".into(),
3657 observations: vec![],
3658 },
3659 Entity {
3660 name: "c".into(),
3661 entity_type: "n".into(),
3662 observations: vec![],
3663 },
3664 ])
3665 .unwrap();
3666 kg.create_relations(&[
3667 Relation {
3668 from: "a".into(),
3669 to: "b".into(),
3670 relation_type: "knows".into(),
3671 },
3672 Relation {
3673 from: "a".into(),
3674 to: "c".into(),
3675 relation_type: "likes".into(),
3676 },
3677 ])
3678 .unwrap();
3679 let json = kg
3680 .neighbors("a", Direction::Outgoing, Some("knows"), 1)
3681 .unwrap();
3682 assert!(json.contains("\"b\""));
3683 assert!(!json.contains("\"c\""));
3684 assert_eq!(count_relations(&json), 1);
3685 }
3686
3687 #[test]
3688 fn test_sqlite_tuning_applied_to_fresh_db() {
3689 use std::sync::atomic::AtomicU64;
3690 static COUNTER: AtomicU64 = AtomicU64::new(2_000_000);
3691 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
3692 let path = std::env::temp_dir().join(format!("kg_tuning_{}_{}.db", std::process::id(), n));
3693 cleanup_db(&path);
3694
3695 let tuning = SqliteTuning {
3696 page_size: 8192,
3697 ..SqliteTuning::default()
3698 };
3699 let kg = TestKg(
3700 GraphHandle::new(
3701 &path,
3702 Durability::Async,
3703 tuning,
3704 NonZeroUsize::new(64).unwrap(),
3705 2,
3706 )
3707 .expect("create KG"),
3708 path.clone(),
3709 );
3710 kg.create_entities(&[Entity {
3711 name: "a".into(),
3712 entity_type: "n".into(),
3713 observations: vec!["o".into()],
3714 }])
3715 .unwrap();
3716
3717 let probe = Connection::open(&path).unwrap();
3720 let page_size: i64 = probe
3721 .query_row("PRAGMA page_size", [], |r| r.get(0))
3722 .unwrap();
3723 assert_eq!(page_size, 8192);
3724 let auto_vacuum: i64 = probe
3725 .query_row("PRAGMA auto_vacuum", [], |r| r.get(0))
3726 .unwrap();
3727 assert_eq!(auto_vacuum, 2, "expected INCREMENTAL auto_vacuum");
3728 let journal: String = probe
3729 .query_row("PRAGMA journal_mode", [], |r| r.get(0))
3730 .unwrap();
3731 assert_eq!(journal.to_lowercase(), "wal");
3732 }
3733
3734 #[test]
3735 fn test_checkpoint_passive_is_noop_safe() {
3736 let kg = new_kg();
3737 kg.checkpoint_passive().unwrap();
3739 kg.create_entities(&[Entity {
3740 name: "a".into(),
3741 entity_type: "n".into(),
3742 observations: vec!["o".into()],
3743 }])
3744 .unwrap();
3745 kg.checkpoint_passive().unwrap();
3747 kg.checkpoint_passive().unwrap();
3748 assert!(kg.get_entity("a").unwrap().is_some());
3750 }
3751}