1use anyhow::{Context, Result};
9use rusqlite::{params, Connection};
10use std::collections::HashMap;
11
12use crate::cfg::{BlockId, Path, PathKind};
13
14#[derive(Debug, Clone)]
19pub struct PathCache {
20 _private: (),
21}
22
23impl PathCache {
24 pub fn new() -> Self {
26 Self { _private: () }
27 }
28}
29
30impl Default for PathCache {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36pub fn store_paths(conn: &mut Connection, function_id: i64, paths: &[Path]) -> Result<()> {
61 conn.execute("BEGIN IMMEDIATE TRANSACTION", [])
63 .context("Failed to begin transaction for store_paths")?;
64
65 let mut insert_path_stmt = conn.prepare_cached(
67 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
68 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
69 ).context("Failed to prepare cfg_paths insert statement")?;
70
71 let mut insert_element_stmt = conn
72 .prepare_cached(
73 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id)
74 VALUES (?1, ?2, ?3)",
75 )
76 .context("Failed to prepare cfg_path_elements insert statement")?;
77
78 let now = chrono::Utc::now().timestamp();
79
80 for path in paths {
81 let kind_str = path_kind_to_str(path.kind);
83
84 insert_path_stmt
86 .execute(params![
87 &path.path_id,
88 function_id,
89 kind_str,
90 path.entry as i64,
91 path.exit as i64,
92 path.len() as i64,
93 now,
94 ])
95 .with_context(|| format!("Failed to insert path {}", path.path_id))?;
96
97 for (idx, &block_id) in path.blocks.iter().enumerate() {
99 insert_element_stmt
100 .execute(params![&path.path_id, idx as i64, block_id as i64,])
101 .with_context(|| {
102 format!("Failed to insert element {} for path {}", idx, path.path_id)
103 })?;
104 }
105 }
106
107 conn.execute("COMMIT", [])
109 .context("Failed to commit transaction for store_paths")?;
110
111 Ok(())
112}
113
114const BATCH_SIZE: usize = 20;
119
120pub fn store_paths_batch(conn: &mut Connection, function_id: i64, paths: &[Path]) -> Result<()> {
158 conn.execute("BEGIN IMMEDIATE TRANSACTION", [])
160 .context("Failed to begin transaction for store_paths_batch")?;
161
162 let _old_journal: String = conn
164 .query_row("PRAGMA journal_mode", [], |row| row.get(0))
165 .unwrap_or_else(|_| "delete".to_string());
166 let old_sync: i64 = conn
167 .query_row("PRAGMA synchronous", [], |row| row.get(0))
168 .unwrap_or(2);
169
170 conn.execute("PRAGMA cache_size = -64000", [])
172 .context("Failed to set cache_size")?;
173
174 let now = chrono::Utc::now().timestamp();
175
176 for path in paths {
177 let kind_str = path_kind_to_str(path.kind);
178
179 {
181 let mut insert_path_stmt = conn.prepare_cached(
182 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
183 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
184 ).context("Failed to prepare cfg_paths insert statement")?;
185
186 insert_path_stmt
187 .execute(params![
188 &path.path_id,
189 function_id,
190 kind_str,
191 path.entry as i64,
192 path.exit as i64,
193 path.len() as i64,
194 now,
195 ])
196 .with_context(|| format!("Failed to insert path {}", path.path_id))?;
197 }
198
199 insert_elements_batch(conn, &path.path_id, &path.blocks)?;
201 }
202
203 let _ = conn.execute(&format!("PRAGMA synchronous = {}", old_sync), []);
205 conn.execute("COMMIT", [])
209 .context("Failed to commit transaction for store_paths_batch")?;
210
211 Ok(())
212}
213
214fn insert_elements_batch(conn: &mut Connection, path_id: &str, blocks: &[BlockId]) -> Result<()> {
222 if blocks.is_empty() {
223 return Ok(());
224 }
225
226 for chunk in blocks.chunks(BATCH_SIZE) {
228 let mut sql = String::from(
229 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES ",
230 );
231
232 for (i, _) in chunk.iter().enumerate() {
233 if i > 0 {
234 sql.push_str(", ");
235 }
236 sql.push_str("(?, ?, ?)");
237 }
238
239 let mut flat_params: Vec<rusqlite::types::Value> = Vec::new();
241 for (i, &block_id) in chunk.iter().enumerate() {
242 flat_params.push(rusqlite::types::Value::Text(path_id.to_string()));
243 flat_params.push(rusqlite::types::Value::Integer(i as i64));
244 flat_params.push(rusqlite::types::Value::Integer(block_id as i64));
245 }
246
247 let params_ref: Vec<&dyn rusqlite::ToSql> = flat_params
249 .iter()
250 .map(|v| v as &dyn rusqlite::ToSql)
251 .collect();
252
253 conn.execute(&sql, params_ref.as_slice())
254 .with_context(|| format!("Failed to batch insert {} elements", chunk.len()))?;
255 }
256
257 Ok(())
258}
259
260fn path_kind_to_str(kind: PathKind) -> &'static str {
262 match kind {
263 PathKind::Normal => "Normal",
264 PathKind::Error => "Error",
265 PathKind::Degenerate => "Degenerate",
266 PathKind::Unreachable => "Unreachable",
267 }
268}
269
270fn str_to_path_kind(s: &str) -> Result<PathKind> {
272 match s {
273 "Normal" => Ok(PathKind::Normal),
274 "Error" => Ok(PathKind::Error),
275 "Degenerate" => Ok(PathKind::Degenerate),
276 "Unreachable" => Ok(PathKind::Unreachable),
277 _ => anyhow::bail!("Invalid path_kind in database: {}", s),
278 }
279}
280
281pub fn get_cached_paths(conn: &mut Connection, function_id: i64) -> Result<Vec<Path>> {
303 let mut stmt = conn
305 .prepare_cached(
306 "SELECT p.path_id, p.path_kind, p.entry_block, p.exit_block,
307 pe.block_id, pe.sequence_order
308 FROM cfg_paths p
309 JOIN cfg_path_elements pe ON p.path_id = pe.path_id
310 WHERE p.function_id = ?1
311 ORDER BY p.path_id, pe.sequence_order",
312 )
313 .context("Failed to prepare get_cached_paths query")?;
314
315 let mut path_data: HashMap<String, PathData> = HashMap::new();
317
318 let rows = stmt
319 .query_map(params![function_id], |row| {
320 Ok((
321 row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, i64>(2)?, row.get::<_, i64>(3)?, row.get::<_, i64>(4)?, row.get::<_, i64>(5)?, ))
328 })
329 .context("Failed to execute get_cached_paths query")?;
330
331 for row in rows {
332 let (path_id, kind_str, entry_block, exit_block, block_id, _sequence_order) = row?;
333 let entry = entry_block as BlockId;
334 let exit = exit_block as BlockId;
335 let kind = str_to_path_kind(&kind_str)
336 .with_context(|| format!("Invalid path_kind '{}' in database", kind_str))?;
337
338 path_data
339 .entry(path_id)
340 .or_insert_with(|| PathData {
341 path_id: String::new(), kind,
343 entry,
344 exit,
345 blocks: Vec::new(),
346 })
347 .blocks
348 .push(block_id as BlockId);
349 }
350
351 let mut paths = Vec::new();
353 for (path_id, data) in path_data {
354 let path = Path::with_id(path_id, data.blocks, data.kind);
356 paths.push(path);
357 }
358
359 Ok(paths)
360}
361
362struct PathData {
364 path_id: String,
365 kind: PathKind,
366 entry: BlockId,
367 exit: BlockId,
368 blocks: Vec<BlockId>,
369}
370
371pub fn invalidate_function_paths(conn: &mut Connection, function_id: i64) -> Result<()> {
393 conn.execute("BEGIN IMMEDIATE TRANSACTION", [])
395 .context("Failed to begin transaction for invalidate_function_paths")?;
396
397 conn.execute(
399 "DELETE FROM cfg_path_elements
400 WHERE path_id IN (SELECT path_id FROM cfg_paths WHERE function_id = ?1)",
401 params![function_id],
402 )
403 .context("Failed to delete cfg_path_elements")?;
404
405 conn.execute(
407 "DELETE FROM cfg_paths WHERE function_id = ?1",
408 params![function_id],
409 )
410 .context("Failed to delete cfg_paths")?;
411
412 conn.execute("COMMIT", [])
414 .context("Failed to commit transaction for invalidate_function_paths")?;
415
416 Ok(())
417}
418
419pub fn update_function_paths_if_changed(
449 conn: &mut Connection,
450 function_id: i64,
451 _new_hash: &str,
452 paths: &[Path],
453) -> Result<bool> {
454 invalidate_function_paths(conn, function_id)?;
462
463 store_paths(conn, function_id, paths)?;
465
466 Ok(true)
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 fn create_test_db() -> Connection {
475 let mut conn = Connection::open_in_memory().unwrap();
476
477 conn.execute(
479 "CREATE TABLE magellan_meta (
480 id INTEGER PRIMARY KEY CHECK (id = 1),
481 magellan_schema_version INTEGER NOT NULL,
482 sqlitegraph_schema_version INTEGER NOT NULL,
483 created_at INTEGER NOT NULL
484 )",
485 [],
486 )
487 .unwrap();
488
489 conn.execute(
490 "CREATE TABLE graph_entities (
491 id INTEGER PRIMARY KEY AUTOINCREMENT,
492 kind TEXT NOT NULL,
493 name TEXT NOT NULL,
494 file_path TEXT,
495 data TEXT NOT NULL
496 )",
497 [],
498 )
499 .unwrap();
500
501 conn.execute(
503 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
504 VALUES (1, 7, 3, 0)",
505 [],
506 ).unwrap();
507
508 crate::storage::create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION)
510 .unwrap();
511
512 conn.execute(
514 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
515 rusqlite::params!("function", "test_func", "test.rs", "{}"),
516 )
517 .unwrap();
518
519 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
521
522 conn
523 }
524
525 fn create_mock_paths() -> Vec<Path> {
527 vec![
528 Path::new(vec![0, 1, 2], PathKind::Normal),
529 Path::new(vec![0, 1, 3], PathKind::Normal),
530 Path::new(vec![0, 2], PathKind::Error),
531 ]
532 }
533
534 #[test]
535 fn test_path_cache_new() {
536 let cache = PathCache::new();
537 let _ = cache;
538 }
539
540 #[test]
541 fn test_path_cache_default() {
542 let cache = PathCache::default();
543 let _ = cache;
544 }
545
546 #[test]
547 fn test_path_kind_to_str() {
548 assert_eq!(path_kind_to_str(PathKind::Normal), "Normal");
549 assert_eq!(path_kind_to_str(PathKind::Error), "Error");
550 assert_eq!(path_kind_to_str(PathKind::Degenerate), "Degenerate");
551 assert_eq!(path_kind_to_str(PathKind::Unreachable), "Unreachable");
552 }
553
554 #[test]
555 fn test_str_to_path_kind() {
556 assert_eq!(str_to_path_kind("Normal").unwrap(), PathKind::Normal);
557 assert_eq!(str_to_path_kind("Error").unwrap(), PathKind::Error);
558 assert_eq!(
559 str_to_path_kind("Degenerate").unwrap(),
560 PathKind::Degenerate
561 );
562 assert_eq!(
563 str_to_path_kind("Unreachable").unwrap(),
564 PathKind::Unreachable
565 );
566 assert!(str_to_path_kind("Invalid").is_err());
567 }
568
569 #[test]
570 fn test_store_paths_inserts_paths() {
571 let mut conn = create_test_db();
572 let function_id: i64 = 1; let paths = create_mock_paths();
574
575 store_paths(&mut conn, function_id, &paths).unwrap();
577
578 let path_count: i64 = conn
580 .query_row(
581 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
582 params![function_id],
583 |row| row.get(0),
584 )
585 .unwrap();
586
587 assert_eq!(path_count, 3, "Should have 3 paths");
588
589 let element_count: i64 = conn
591 .query_row("SELECT COUNT(*) FROM cfg_path_elements", [], |row| {
592 row.get(0)
593 })
594 .unwrap();
595
596 assert_eq!(element_count, 8, "Should have 8 elements (3+3+2)");
597 }
598
599 #[test]
600 fn test_store_paths_path_metadata() {
601 let mut conn = create_test_db();
602 let function_id: i64 = 1;
603 let paths = create_mock_paths();
604
605 store_paths(&mut conn, function_id, &paths).unwrap();
606
607 let mut stmt = conn
609 .prepare(
610 "SELECT path_id, path_kind, entry_block, exit_block, length
611 FROM cfg_paths
612 WHERE function_id = ?
613 ORDER BY entry_block, exit_block",
614 )
615 .unwrap();
616
617 let rows: Vec<_> = stmt
618 .query_map(params![function_id], |row| {
619 Ok((
620 row.get::<_, String>(0)?,
621 row.get::<_, String>(1)?,
622 row.get::<_, i64>(2)?,
623 row.get::<_, i64>(3)?,
624 row.get::<_, i64>(4)?,
625 ))
626 })
627 .unwrap()
628 .filter_map(Result::ok)
629 .collect();
630
631 assert_eq!(rows.len(), 3);
632
633 let row = &rows[0];
635 assert_eq!(row.2, 0); assert_eq!(row.3, 2); assert_eq!(row.4, 3); assert_eq!(row.1, "Normal"); assert!(!row.0.is_empty());
642 assert!(row.0.chars().all(|c| c.is_ascii_hexdigit()));
643 }
644
645 #[test]
646 fn test_store_paths_path_elements_order() {
647 let mut conn = create_test_db();
648 let function_id: i64 = 1;
649 let paths = create_mock_paths();
650
651 store_paths(&mut conn, function_id, &paths).unwrap();
652
653 let path_id: String = conn
655 .query_row(
656 "SELECT path_id FROM cfg_paths WHERE function_id = ? LIMIT 1",
657 params![function_id],
658 |row| row.get(0),
659 )
660 .unwrap();
661
662 let mut stmt = conn
664 .prepare(
665 "SELECT block_id FROM cfg_path_elements
666 WHERE path_id = ?
667 ORDER BY sequence_order",
668 )
669 .unwrap();
670
671 let blocks: Vec<BlockId> = stmt
672 .query_map(params![path_id], |row| Ok(row.get::<_, i64>(0)? as BlockId))
673 .unwrap()
674 .filter_map(Result::ok)
675 .collect();
676
677 assert_eq!(blocks, vec![0, 1, 2]);
679 }
680
681 #[test]
682 fn test_store_paths_empty_list() {
683 let mut conn = create_test_db();
684 let function_id: i64 = 1;
685 let paths: Vec<Path> = vec![];
686
687 store_paths(&mut conn, function_id, &paths).unwrap();
689
690 let count: i64 = conn
692 .query_row(
693 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
694 params![function_id],
695 |row| row.get(0),
696 )
697 .unwrap();
698
699 assert_eq!(count, 0);
700 }
701
702 #[test]
703 fn test_store_paths_foreign_key_constraint() {
704 let mut conn = create_test_db();
705 let invalid_function_id: i64 = 9999; let paths = create_mock_paths();
707
708 let result = store_paths(&mut conn, invalid_function_id, &paths);
710
711 assert!(result.is_err(), "Should fail with invalid function_id");
712 }
713
714 #[test]
715 fn test_store_paths_deduplication_by_path_id() {
716 let mut conn = create_test_db();
717 let function_id: i64 = 1;
718 let paths = create_mock_paths();
719
720 store_paths(&mut conn, function_id, &paths).unwrap();
722
723 let result = store_paths(&mut conn, function_id, &paths);
726
727 assert!(result.is_err(), "Should fail on duplicate path_id");
729 }
730
731 #[test]
734 fn test_get_cached_paths_empty() {
735 let mut conn = create_test_db();
736 let function_id: i64 = 1;
737
738 let paths = get_cached_paths(&mut conn, function_id).unwrap();
740 assert_eq!(paths.len(), 0);
741 }
742
743 #[test]
744 fn test_get_cached_paths_retrieves_stored_paths() {
745 let mut conn = create_test_db();
746 let function_id: i64 = 1;
747 let original_paths = create_mock_paths();
748
749 store_paths(&mut conn, function_id, &original_paths).unwrap();
751
752 let retrieved_paths = get_cached_paths(&mut conn, function_id).unwrap();
754
755 assert_eq!(retrieved_paths.len(), original_paths.len());
757
758 for orig in &original_paths {
760 assert!(
761 retrieved_paths
762 .iter()
763 .any(|p| p.blocks == orig.blocks && p.kind == orig.kind),
764 "Path {:?} not found in retrieved paths",
765 orig.blocks
766 );
767 }
768 }
769
770 #[test]
771 fn test_get_cached_paths_block_order_preserved() {
772 let mut conn = create_test_db();
773 let function_id: i64 = 1;
774
775 let paths = vec![
777 Path::new(vec![0, 1, 2, 3], PathKind::Normal),
778 Path::new(vec![5, 4, 3, 2, 1], PathKind::Error),
779 ];
780
781 store_paths(&mut conn, function_id, &paths).unwrap();
782 let retrieved = get_cached_paths(&mut conn, function_id).unwrap();
783
784 assert_eq!(retrieved.len(), 2);
785
786 let path1 = retrieved
788 .iter()
789 .find(|p| p.blocks == vec![0, 1, 2, 3])
790 .unwrap();
791 assert_eq!(path1.blocks, vec![0, 1, 2, 3]);
792
793 let path2 = retrieved
794 .iter()
795 .find(|p| p.blocks == vec![5, 4, 3, 2, 1])
796 .unwrap();
797 assert_eq!(path2.blocks, vec![5, 4, 3, 2, 1]);
798 }
799
800 #[test]
801 fn test_get_cached_paths_kind_preserved() {
802 let mut conn = create_test_db();
803 let function_id: i64 = 1;
804
805 let paths = vec![
806 Path::new(vec![0], PathKind::Normal),
807 Path::new(vec![1], PathKind::Error),
808 Path::new(vec![2], PathKind::Degenerate),
809 Path::new(vec![3], PathKind::Unreachable),
810 ];
811
812 store_paths(&mut conn, function_id, &paths).unwrap();
813 let retrieved = get_cached_paths(&mut conn, function_id).unwrap();
814
815 assert_eq!(retrieved.len(), 4);
816
817 assert!(retrieved.iter().any(|p| p.kind == PathKind::Normal));
819 assert!(retrieved.iter().any(|p| p.kind == PathKind::Error));
820 assert!(retrieved.iter().any(|p| p.kind == PathKind::Degenerate));
821 assert!(retrieved.iter().any(|p| p.kind == PathKind::Unreachable));
822 }
823
824 #[test]
825 fn test_get_cached_paths_invalid_kind_returns_error() {
826 let mut conn = create_test_db();
827 let function_id: i64 = 1;
828
829 conn.execute(
831 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
832 VALUES (?, ?, ?, ?, ?, ?, ?)",
833 params!("invalid_path_id", function_id, "InvalidKind", 0, 0, 1, 0),
834 ).unwrap();
835
836 conn.execute(
838 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id)
839 VALUES (?, ?, ?)",
840 params!("invalid_path_id", 0, 0),
841 )
842 .unwrap();
843
844 let result = get_cached_paths(&mut conn, function_id);
846 assert!(result.is_err(), "Should fail on invalid path_kind");
847 }
848
849 #[test]
850 fn test_get_cached_paths_roundtrip() {
851 let mut conn = create_test_db();
852 let function_id: i64 = 1;
853
854 let paths = vec![
856 Path::new(vec![0, 1, 2, 3, 4, 5], PathKind::Normal),
857 Path::new(vec![0, 1, 3, 5], PathKind::Normal),
858 Path::new(vec![0, 2, 4, 5], PathKind::Error),
859 Path::new(vec![0, 5], PathKind::Degenerate),
860 ];
861
862 store_paths(&mut conn, function_id, &paths).unwrap();
863 let retrieved = get_cached_paths(&mut conn, function_id).unwrap();
864
865 assert_eq!(retrieved.len(), paths.len());
867
868 let mut sorted_orig: Vec<_> = paths.iter().collect();
870 let mut sorted_ret: Vec<_> = retrieved.iter().collect();
871 sorted_orig.sort_by_key(|p| p.blocks.clone());
872 sorted_ret.sort_by_key(|p| p.blocks.clone());
873
874 for (orig, ret) in sorted_orig.iter().zip(sorted_ret.iter()) {
875 assert_eq!(orig.blocks, ret.blocks, "Block sequence mismatch");
876 assert_eq!(orig.kind, ret.kind, "PathKind mismatch");
877 }
878 }
879
880 #[test]
883 fn test_invalidate_function_paths_deletes_all_paths() {
884 let mut conn = create_test_db();
885 let function_id: i64 = 1;
886 let paths = create_mock_paths();
887
888 store_paths(&mut conn, function_id, &paths).unwrap();
890
891 let count_before: i64 = conn
893 .query_row(
894 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
895 params![function_id],
896 |row| row.get(0),
897 )
898 .unwrap();
899 assert_eq!(count_before, 3);
900
901 invalidate_function_paths(&mut conn, function_id).unwrap();
903
904 let count_after: i64 = conn
906 .query_row(
907 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
908 params![function_id],
909 |row| row.get(0),
910 )
911 .unwrap();
912 assert_eq!(count_after, 0);
913 }
914
915 #[test]
916 fn test_invalidate_function_paths_deletes_elements() {
917 let mut conn = create_test_db();
918 let function_id: i64 = 1;
919 let paths = create_mock_paths();
920
921 store_paths(&mut conn, function_id, &paths).unwrap();
923
924 let count_before: i64 = conn
926 .query_row("SELECT COUNT(*) FROM cfg_path_elements", [], |row| {
927 row.get(0)
928 })
929 .unwrap();
930 assert!(count_before > 0);
931
932 invalidate_function_paths(&mut conn, function_id).unwrap();
934
935 let count_after: i64 = conn
937 .query_row(
938 "SELECT COUNT(*) FROM cfg_path_elements
939 WHERE path_id IN (SELECT path_id FROM cfg_paths WHERE function_id = ?)",
940 params![function_id],
941 |row| row.get(0),
942 )
943 .unwrap();
944 assert_eq!(count_after, 0);
945 }
946
947 #[test]
948 fn test_invalidate_function_paths_idempotent() {
949 let mut conn = create_test_db();
950 let function_id: i64 = 1;
951
952 invalidate_function_paths(&mut conn, function_id).unwrap();
954
955 invalidate_function_paths(&mut conn, function_id).unwrap();
957 }
958
959 #[test]
960 fn test_invalidate_function_paths_then_retrieve_empty() {
961 let mut conn = create_test_db();
962 let function_id: i64 = 1;
963 let paths = create_mock_paths();
964
965 store_paths(&mut conn, function_id, &paths).unwrap();
967 let before = get_cached_paths(&mut conn, function_id).unwrap();
968 assert_eq!(before.len(), 3);
969
970 invalidate_function_paths(&mut conn, function_id).unwrap();
972
973 let after = get_cached_paths(&mut conn, function_id).unwrap();
975 assert_eq!(after.len(), 0);
976 }
977
978 #[test]
979 fn test_invalidate_function_paths_only_target_function() {
980 let mut conn = create_test_db();
981
982 conn.execute(
984 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
985 rusqlite::params!("function", "func1", "test.rs", "{}"),
986 )
987 .unwrap();
988 conn.execute(
989 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
990 rusqlite::params!("function", "func2", "test.rs", "{}"),
991 )
992 .unwrap();
993
994 let function_id_1: i64 = 1;
995 let function_id_2: i64 = 2;
996
997 let paths_1 = vec![
999 Path::new(vec![0, 1, 2], PathKind::Normal),
1000 Path::new(vec![0, 1, 3], PathKind::Normal),
1001 Path::new(vec![0, 2], PathKind::Error),
1002 ];
1003 let paths_2 = vec![
1004 Path::new(vec![10, 11, 12], PathKind::Normal),
1005 Path::new(vec![10, 11, 13], PathKind::Normal),
1006 Path::new(vec![10, 12], PathKind::Error),
1007 ];
1008
1009 store_paths(&mut conn, function_id_1, &paths_1).unwrap();
1011 store_paths(&mut conn, function_id_2, &paths_2).unwrap();
1012
1013 let count_1_before: i64 = conn
1015 .query_row(
1016 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1017 params![function_id_1],
1018 |row| row.get(0),
1019 )
1020 .unwrap();
1021 let count_2_before: i64 = conn
1022 .query_row(
1023 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1024 params![function_id_2],
1025 |row| row.get(0),
1026 )
1027 .unwrap();
1028 assert_eq!(count_1_before, 3);
1029 assert_eq!(count_2_before, 3);
1030
1031 invalidate_function_paths(&mut conn, function_id_1).unwrap();
1033
1034 let count_1_after: i64 = conn
1036 .query_row(
1037 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1038 params![function_id_1],
1039 |row| row.get(0),
1040 )
1041 .unwrap();
1042 assert_eq!(count_1_after, 0);
1043
1044 let count_2_after: i64 = conn
1046 .query_row(
1047 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1048 params![function_id_2],
1049 |row| row.get(0),
1050 )
1051 .unwrap();
1052 assert_eq!(count_2_after, 3);
1053 }
1054
1055 #[test]
1058 fn test_update_function_paths_if_changed_first_call() {
1059 let mut conn = create_test_db();
1060 let function_id: i64 = 1;
1061 let paths = create_mock_paths();
1062 let hash = "abc123";
1063
1064 let updated =
1066 update_function_paths_if_changed(&mut conn, function_id, hash, &paths).unwrap();
1067 assert!(updated, "First call should return true (updated)");
1068
1069 let count: i64 = conn
1071 .query_row(
1072 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1073 params![function_id],
1074 |row| row.get(0),
1075 )
1076 .unwrap();
1077 assert_eq!(count, 3);
1078 }
1079
1080 #[test]
1081 fn test_update_function_paths_if_changed_same_hash() {
1082 let mut conn = create_test_db();
1083 let function_id: i64 = 1;
1084 let paths = create_mock_paths();
1085 let hash = "abc123";
1086
1087 let updated1 =
1089 update_function_paths_if_changed(&mut conn, function_id, hash, &paths).unwrap();
1090 assert!(updated1);
1091
1092 let updated2 =
1094 update_function_paths_if_changed(&mut conn, function_id, hash, &paths).unwrap();
1095 assert!(
1096 updated2,
1097 "Same hash should return true (hash caching not available with Magellan)"
1098 );
1099 }
1100
1101 #[test]
1102 fn test_update_function_paths_if_changed_different_hash() {
1103 let mut conn = create_test_db();
1104 let function_id: i64 = 1;
1105 let paths1 = create_mock_paths();
1106 let paths2 = vec![Path::new(vec![0, 1], PathKind::Normal)];
1107
1108 let updated1 =
1110 update_function_paths_if_changed(&mut conn, function_id, "hash1", &paths1).unwrap();
1111 assert!(updated1);
1112
1113 let count1: i64 = conn
1115 .query_row(
1116 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1117 params![function_id],
1118 |row| row.get(0),
1119 )
1120 .unwrap();
1121 assert_eq!(count1, 3);
1122
1123 let updated2 =
1125 update_function_paths_if_changed(&mut conn, function_id, "hash2", &paths2).unwrap();
1126 assert!(updated2, "Different hash should return true (updated)");
1127
1128 let count2: i64 = conn
1130 .query_row(
1131 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1132 params![function_id],
1133 |row| row.get(0),
1134 )
1135 .unwrap();
1136 assert_eq!(count2, 1, "Old paths should be invalidated and replaced");
1137
1138 }
1140
1141 #[test]
1142 fn test_update_function_paths_if_changed_three_calls() {
1143 let mut conn = create_test_db();
1144 let function_id: i64 = 1;
1145 let paths = create_mock_paths();
1146
1147 let u1 = update_function_paths_if_changed(&mut conn, function_id, "hash1", &paths).unwrap();
1149 assert!(u1);
1150
1151 let u2 = update_function_paths_if_changed(&mut conn, function_id, "hash1", &paths).unwrap();
1153 assert!(u2);
1154
1155 let u3 = update_function_paths_if_changed(&mut conn, function_id, "hash2", &paths).unwrap();
1157 assert!(u3);
1158
1159 let u4 = update_function_paths_if_changed(&mut conn, function_id, "hash2", &paths).unwrap();
1161 assert!(u4);
1162 }
1163
1164 #[test]
1165 fn test_update_function_paths_if_changed_with_existing_cfg_block() {
1166 let mut conn = create_test_db();
1167 let function_id: i64 = 1;
1168
1169 conn.execute(
1171 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
1172 start_line, start_col, end_line, end_col)
1173 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
1174 params![function_id, "entry", "return", 0, 10, 1, 0, 1, 10],
1175 )
1176 .unwrap();
1177
1178 let paths = create_mock_paths();
1179
1180 let updated =
1182 update_function_paths_if_changed(&mut conn, function_id, "new_hash", &paths).unwrap();
1183 assert!(updated);
1184
1185 let count: i64 = conn
1187 .query_row(
1188 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
1189 params![function_id],
1190 |row| row.get(0),
1191 )
1192 .unwrap();
1193 assert_eq!(count, 1, "Should still have only one cfg_blocks entry");
1194 }
1195
1196 #[test]
1197 fn test_update_function_paths_if_changed_creates_placeholder() {
1198 let mut conn = create_test_db();
1199 let function_id: i64 = 1;
1200 let paths = create_mock_paths();
1201
1202 update_function_paths_if_changed(&mut conn, function_id, "hash1", &paths).unwrap();
1208
1209 let path_count: i64 = conn
1211 .query_row(
1212 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1213 params![function_id],
1214 |row| row.get(0),
1215 )
1216 .unwrap();
1217 assert_eq!(
1218 path_count, 3,
1219 "Should store all paths without cfg_blocks entry"
1220 );
1221 }
1222
1223 #[test]
1224 fn test_update_function_paths_if_changed_invalidates_old() {
1225 let mut conn = create_test_db();
1226 let function_id: i64 = 1;
1227
1228 let paths1 = vec![
1229 Path::new(vec![0, 1, 2], PathKind::Normal),
1230 Path::new(vec![0, 1, 3], PathKind::Normal),
1231 ];
1232 let paths2 = vec![Path::new(vec![0, 2], PathKind::Error)];
1233
1234 update_function_paths_if_changed(&mut conn, function_id, "hash1", &paths1).unwrap();
1236
1237 let retrieved1 = get_cached_paths(&mut conn, function_id).unwrap();
1239 assert_eq!(retrieved1.len(), 2);
1240
1241 update_function_paths_if_changed(&mut conn, function_id, "hash2", &paths2).unwrap();
1243
1244 let retrieved2 = get_cached_paths(&mut conn, function_id).unwrap();
1246 assert_eq!(retrieved2.len(), 1);
1247 assert_eq!(retrieved2[0].blocks, vec![0, 2]);
1248 assert_eq!(retrieved2[0].kind, PathKind::Error);
1249 }
1250
1251 #[test]
1254 fn test_store_paths_batch_inserts_correctly() {
1255 let mut conn = create_test_db();
1256 let function_id: i64 = 1;
1257 let paths = create_mock_paths();
1258
1259 store_paths_batch(&mut conn, function_id, &paths).unwrap();
1261
1262 let path_count: i64 = conn
1264 .query_row(
1265 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1266 params![function_id],
1267 |row| row.get(0),
1268 )
1269 .unwrap();
1270
1271 assert_eq!(path_count, 3, "Should have 3 paths");
1272
1273 let element_count: i64 = conn
1275 .query_row("SELECT COUNT(*) FROM cfg_path_elements", [], |row| {
1276 row.get(0)
1277 })
1278 .unwrap();
1279
1280 assert_eq!(element_count, 8, "Should have 8 elements (3+3+2)");
1281 }
1282
1283 #[test]
1284 fn test_store_paths_batch_empty_list() {
1285 let mut conn = create_test_db();
1286 let function_id: i64 = 1;
1287 let paths: Vec<Path> = vec![];
1288
1289 store_paths_batch(&mut conn, function_id, &paths).unwrap();
1291
1292 let count: i64 = conn
1294 .query_row(
1295 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1296 params![function_id],
1297 |row| row.get(0),
1298 )
1299 .unwrap();
1300
1301 assert_eq!(count, 0);
1302 }
1303
1304 #[test]
1305 fn test_store_paths_batch_preserves_metadata() {
1306 let mut conn = create_test_db();
1307 let function_id: i64 = 1;
1308 let paths = create_mock_paths();
1309
1310 store_paths_batch(&mut conn, function_id, &paths).unwrap();
1311
1312 let mut stmt = conn
1314 .prepare(
1315 "SELECT path_id, path_kind, entry_block, exit_block, length
1316 FROM cfg_paths
1317 WHERE function_id = ?
1318 ORDER BY entry_block, exit_block",
1319 )
1320 .unwrap();
1321
1322 let rows: Vec<_> = stmt
1323 .query_map(params![function_id], |row| {
1324 Ok((
1325 row.get::<_, String>(0)?,
1326 row.get::<_, String>(1)?,
1327 row.get::<_, i64>(2)?,
1328 row.get::<_, i64>(3)?,
1329 row.get::<_, i64>(4)?,
1330 ))
1331 })
1332 .unwrap()
1333 .filter_map(Result::ok)
1334 .collect();
1335
1336 assert_eq!(rows.len(), 3);
1337
1338 let row = &rows[0];
1340 assert_eq!(row.2, 0); assert_eq!(row.3, 2); assert_eq!(row.4, 3); assert_eq!(row.1, "Normal"); }
1345
1346 #[test]
1347 fn test_store_paths_batch_performance_100_paths() {
1348 use std::time::Instant;
1349
1350 let mut conn = create_test_db();
1351 let function_id: i64 = 1;
1352
1353 let paths: Vec<Path> = (0..100)
1356 .map(|i| Path::new(vec![0, 1, i, 2, i % 5 + 10], PathKind::Normal))
1357 .collect();
1358
1359 let start = Instant::now();
1360 store_paths_batch(&mut conn, function_id, &paths).unwrap();
1361 let duration = start.elapsed();
1362
1363 let count: i64 = conn
1365 .query_row(
1366 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1367 params![function_id],
1368 |row| row.get(0),
1369 )
1370 .unwrap();
1371 assert_eq!(count, 100);
1372
1373 let element_count: i64 = conn
1375 .query_row("SELECT COUNT(*) FROM cfg_path_elements", [], |row| {
1376 row.get(0)
1377 })
1378 .unwrap();
1379 assert_eq!(element_count, 500); assert!(
1383 duration < std::time::Duration::from_millis(100),
1384 "store_paths_batch took {:?}, expected <100ms",
1385 duration
1386 );
1387 }
1388
1389 #[test]
1390 #[ignore = "benchmark test - run with cargo test -- --ignored"]
1391 fn test_store_paths_batch_benchmark_large() {
1392 use std::time::Instant;
1393
1394 let mut conn = create_test_db();
1395 let function_id: i64 = 1;
1396
1397 let paths: Vec<Path> = (0..1000)
1399 .map(|i| Path::new(vec![0, 1, i, 2, 3, i % 10 + 100], PathKind::Normal))
1400 .collect();
1401
1402 let start = Instant::now();
1403 store_paths_batch(&mut conn, function_id, &paths).unwrap();
1404 let duration = start.elapsed();
1405
1406 println!("store_paths_batch for 1000 paths took {:?}", duration);
1407
1408 let count: i64 = conn
1410 .query_row(
1411 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
1412 params![function_id],
1413 |row| row.get(0),
1414 )
1415 .unwrap();
1416 assert_eq!(count, 1000);
1417 }
1418}