1#[cfg(feature = "semantic")]
11pub mod semantic;
12
13use serde::{Deserialize, Serialize};
14use std::path::PathBuf;
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct Node {
19 pub id: i64,
21 pub name: String,
23 pub kind: NodeKind,
25 pub language: Language,
27 pub file_path: String,
29 pub line_start: u32,
31 pub line_end: u32,
33 pub content: Option<String>,
35}
36
37#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
39#[serde(rename_all = "snake_case")]
40pub enum NodeKind {
41 File,
42 Module,
43 Function,
44 Method,
45 Class,
46 Struct,
47 Enum,
48 Trait,
49 Interface,
50 Constant,
51 Variable,
52 TypeAlias,
53 Import,
54}
55
56impl std::fmt::Display for NodeKind {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 let s = match self {
59 Self::File => "file",
60 Self::Module => "module",
61 Self::Function => "function",
62 Self::Method => "method",
63 Self::Class => "class",
64 Self::Struct => "struct",
65 Self::Enum => "enum",
66 Self::Trait => "trait",
67 Self::Interface => "interface",
68 Self::Constant => "constant",
69 Self::Variable => "variable",
70 Self::TypeAlias => "type_alias",
71 Self::Import => "import",
72 };
73 write!(f, "{}", s)
74 }
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
79#[serde(rename_all = "lowercase")]
80pub enum Language {
81 Rust,
82 Python,
83 Go,
84 TypeScript,
85 JavaScript,
86 Java,
87 Cpp,
88 C,
89 Unknown,
90}
91
92impl Language {
93 pub fn from_extension(ext: &str) -> Self {
95 match ext.to_lowercase().as_str() {
96 "rs" => Self::Rust,
97 "py" => Self::Python,
98 "go" => Self::Go,
99 "ts" | "tsx" => Self::TypeScript,
100 "js" | "jsx" | "mjs" | "cjs" => Self::JavaScript,
101 "java" => Self::Java,
102 "cpp" | "cc" | "cxx" | "hpp" => Self::Cpp,
103 "c" | "h" => Self::C,
104 _ => Self::Unknown,
105 }
106 }
107
108 pub fn extensions(&self) -> &[&str] {
110 match self {
111 Self::Rust => &["rs"],
112 Self::Python => &["py"],
113 Self::Go => &["go"],
114 Self::TypeScript => &["ts", "tsx"],
115 Self::JavaScript => &["js", "jsx", "mjs", "cjs"],
116 Self::Java => &["java"],
117 Self::Cpp => &["cpp", "cc", "cxx", "hpp"],
118 Self::C => &["c", "h"],
119 Self::Unknown => &[],
120 }
121 }
122}
123
124impl std::fmt::Display for Language {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 let s = match self {
127 Self::Rust => "rust",
128 Self::Python => "python",
129 Self::Go => "go",
130 Self::TypeScript => "typescript",
131 Self::JavaScript => "javascript",
132 Self::Java => "java",
133 Self::Cpp => "cpp",
134 Self::C => "c",
135 Self::Unknown => "unknown",
136 };
137 write!(f, "{}", s)
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
143pub struct Edge {
144 pub from_id: i64,
146 pub to_id: i64,
148 pub kind: EdgeKind,
150 pub confidence: f32,
152}
153
154#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
156#[serde(rename_all = "snake_case")]
157pub enum EdgeKind {
158 Calls,
160 Imports,
162 Contains,
164 Inherits,
166 Implements,
168 References,
170 DependsOn,
172}
173
174impl std::fmt::Display for EdgeKind {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 let s = match self {
177 Self::Calls => "calls",
178 Self::Imports => "imports",
179 Self::Contains => "contains",
180 Self::Inherits => "inherits",
181 Self::Implements => "implements",
182 Self::References => "references",
183 Self::DependsOn => "depends_on",
184 };
185 write!(f, "{}", s)
186 }
187}
188
189#[derive(Debug, thiserror::Error)]
191pub enum DeagleError {
192 #[error("Database error: {0}")]
193 Database(#[from] rusqlite::Error),
194 #[error("IO error: {0}")]
195 Io(#[from] std::io::Error),
196 #[error("Parse error in {file}: {message}")]
197 Parse { file: String, message: String },
198 #[error("{0}")]
199 Other(String),
200}
201
202pub type Result<T> = std::result::Result<T, DeagleError>;
203
204pub struct GraphDb {
206 conn: rusqlite::Connection,
207}
208
209impl GraphDb {
210 pub fn open(path: &std::path::Path) -> Result<Self> {
212 let conn = rusqlite::Connection::open(path)?;
213 let db = Self { conn };
214 db.init_schema()?;
215 Ok(db)
216 }
217
218 pub fn in_memory() -> Result<Self> {
220 let conn = rusqlite::Connection::open_in_memory()?;
221 let db = Self { conn };
222 db.init_schema()?;
223 Ok(db)
224 }
225
226 fn init_schema(&self) -> Result<()> {
227 self.conn.execute_batch(
228 "
229 CREATE TABLE IF NOT EXISTS nodes (
230 id INTEGER PRIMARY KEY AUTOINCREMENT,
231 name TEXT NOT NULL,
232 kind TEXT NOT NULL,
233 language TEXT NOT NULL,
234 file_path TEXT NOT NULL,
235 line_start INTEGER NOT NULL,
236 line_end INTEGER NOT NULL,
237 content TEXT
238 );
239 CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
240 CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);
241 CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_path);
242
243 CREATE TABLE IF NOT EXISTS edges (
244 id INTEGER PRIMARY KEY AUTOINCREMENT,
245 from_id INTEGER NOT NULL REFERENCES nodes(id),
246 to_id INTEGER NOT NULL REFERENCES nodes(id),
247 kind TEXT NOT NULL,
248 confidence REAL NOT NULL DEFAULT 1.0
249 );
250 CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_id);
251 CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_id);
252 CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind);
253
254 CREATE TABLE IF NOT EXISTS metadata (
255 key TEXT PRIMARY KEY,
256 value TEXT NOT NULL
257 );
258 "
259 )?;
260 Ok(())
261 }
262
263 pub fn insert_node(&self, node: &Node) -> Result<i64> {
265 self.conn.execute(
266 "INSERT INTO nodes (name, kind, language, file_path, line_start, line_end, content)
267 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
268 rusqlite::params![
269 node.name,
270 node.kind.to_string(),
271 node.language.to_string(),
272 node.file_path,
273 node.line_start,
274 node.line_end,
275 node.content,
276 ],
277 )?;
278 Ok(self.conn.last_insert_rowid())
279 }
280
281 pub fn insert_edge(&self, edge: &Edge) -> Result<()> {
283 self.conn.execute(
284 "INSERT INTO edges (from_id, to_id, kind, confidence) VALUES (?1, ?2, ?3, ?4)",
285 rusqlite::params![edge.from_id, edge.to_id, edge.kind.to_string(), edge.confidence],
286 )?;
287 Ok(())
288 }
289
290 pub fn search_nodes(&self, query: &str) -> Result<Vec<Node>> {
292 let mut stmt = self.conn.prepare(
293 "SELECT id, name, kind, language, file_path, line_start, line_end, content
294 FROM nodes WHERE name LIKE ?1 ORDER BY name"
295 )?;
296 let pattern = format!("%{}%", query);
297 let rows = stmt.query_map([&pattern], |row| {
298 Ok(Node {
299 id: row.get(0)?,
300 name: row.get(1)?,
301 kind: serde_json::from_str(&format!("\"{}\"", row.get::<_, String>(2)?))
302 .unwrap_or(NodeKind::Function),
303 language: serde_json::from_str(&format!("\"{}\"", row.get::<_, String>(3)?))
304 .unwrap_or(Language::Unknown),
305 file_path: row.get(4)?,
306 line_start: row.get(5)?,
307 line_end: row.get(6)?,
308 content: row.get(7)?,
309 })
310 })?;
311 rows.collect::<std::result::Result<Vec<_>, _>>().map_err(DeagleError::from)
312 }
313
314 pub fn edges_from(&self, node_id: i64) -> Result<Vec<Edge>> {
316 let mut stmt = self.conn.prepare(
317 "SELECT from_id, to_id, kind, confidence FROM edges WHERE from_id = ?1"
318 )?;
319 let rows = stmt.query_map([node_id], |row| {
320 Ok(Edge {
321 from_id: row.get(0)?,
322 to_id: row.get(1)?,
323 kind: serde_json::from_str(&format!("\"{}\"", row.get::<_, String>(2)?))
324 .unwrap_or(EdgeKind::Calls),
325 confidence: row.get(3)?,
326 })
327 })?;
328 rows.collect::<std::result::Result<Vec<_>, _>>().map_err(DeagleError::from)
329 }
330
331 pub fn node_count(&self) -> Result<usize> {
333 let count: i64 = self.conn.query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))?;
334 Ok(count as usize)
335 }
336
337 pub fn edge_count(&self) -> Result<usize> {
339 let count: i64 = self.conn.query_row("SELECT COUNT(*) FROM edges", [], |r| r.get(0))?;
340 Ok(count as usize)
341 }
342
343 pub fn clear(&self) -> Result<()> {
345 self.conn.execute_batch("DELETE FROM edges; DELETE FROM nodes;")?;
346 Ok(())
347 }
348
349 pub fn path(&self) -> Option<PathBuf> {
351 self.conn.path().map(PathBuf::from)
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_language_from_extension() {
361 assert_eq!(Language::from_extension("rs"), Language::Rust);
362 assert_eq!(Language::from_extension("py"), Language::Python);
363 assert_eq!(Language::from_extension("go"), Language::Go);
364 assert_eq!(Language::from_extension("ts"), Language::TypeScript);
365 assert_eq!(Language::from_extension("tsx"), Language::TypeScript);
366 assert_eq!(Language::from_extension("js"), Language::JavaScript);
367 assert_eq!(Language::from_extension("java"), Language::Java);
368 assert_eq!(Language::from_extension("cpp"), Language::Cpp);
369 assert_eq!(Language::from_extension("c"), Language::C);
370 assert_eq!(Language::from_extension("xyz"), Language::Unknown);
371 }
372
373 #[test]
374 fn test_language_display() {
375 assert_eq!(Language::Rust.to_string(), "rust");
376 assert_eq!(Language::Python.to_string(), "python");
377 }
378
379 #[test]
380 fn test_node_kind_display() {
381 assert_eq!(NodeKind::Function.to_string(), "function");
382 assert_eq!(NodeKind::Struct.to_string(), "struct");
383 assert_eq!(NodeKind::TypeAlias.to_string(), "type_alias");
384 }
385
386 #[test]
387 fn test_edge_kind_display() {
388 assert_eq!(EdgeKind::Calls.to_string(), "calls");
389 assert_eq!(EdgeKind::Imports.to_string(), "imports");
390 assert_eq!(EdgeKind::Contains.to_string(), "contains");
391 }
392
393 #[test]
394 fn test_graph_db_in_memory() {
395 let db = GraphDb::in_memory().unwrap();
396 assert_eq!(db.node_count().unwrap(), 0);
397 assert_eq!(db.edge_count().unwrap(), 0);
398 }
399
400 #[test]
401 fn test_insert_and_search_node() {
402 let db = GraphDb::in_memory().unwrap();
403 let node = Node {
404 id: 0,
405 name: "process_request".to_string(),
406 kind: NodeKind::Function,
407 language: Language::Rust,
408 file_path: "src/handler.rs".to_string(),
409 line_start: 42,
410 line_end: 68,
411 content: Some("pub fn process_request() {}".to_string()),
412 };
413 let id = db.insert_node(&node).unwrap();
414 assert!(id > 0);
415 assert_eq!(db.node_count().unwrap(), 1);
416
417 let results = db.search_nodes("process").unwrap();
418 assert_eq!(results.len(), 1);
419 assert_eq!(results[0].name, "process_request");
420 assert_eq!(results[0].kind, NodeKind::Function);
421 assert_eq!(results[0].language, Language::Rust);
422 }
423
424 #[test]
425 fn test_insert_edge_and_query() {
426 let db = GraphDb::in_memory().unwrap();
427 let n1 = Node {
428 id: 0, name: "main".into(), kind: NodeKind::Function,
429 language: Language::Rust, file_path: "src/main.rs".into(),
430 line_start: 1, line_end: 10, content: None,
431 };
432 let n2 = Node {
433 id: 0, name: "handler".into(), kind: NodeKind::Function,
434 language: Language::Rust, file_path: "src/lib.rs".into(),
435 line_start: 5, line_end: 20, content: None,
436 };
437 let id1 = db.insert_node(&n1).unwrap();
438 let id2 = db.insert_node(&n2).unwrap();
439
440 let edge = Edge {
441 from_id: id1, to_id: id2,
442 kind: EdgeKind::Calls, confidence: 1.0,
443 };
444 db.insert_edge(&edge).unwrap();
445 assert_eq!(db.edge_count().unwrap(), 1);
446
447 let edges = db.edges_from(id1).unwrap();
448 assert_eq!(edges.len(), 1);
449 assert_eq!(edges[0].to_id, id2);
450 assert_eq!(edges[0].kind, EdgeKind::Calls);
451 }
452
453 #[test]
454 fn test_search_case_insensitive() {
455 let db = GraphDb::in_memory().unwrap();
456 let node = Node {
457 id: 0, name: "MyStruct".into(), kind: NodeKind::Struct,
458 language: Language::Rust, file_path: "src/types.rs".into(),
459 line_start: 1, line_end: 5, content: None,
460 };
461 db.insert_node(&node).unwrap();
462
463 let results = db.search_nodes("mystruct").unwrap();
464 assert_eq!(results.len(), 1);
465 }
466
467 #[test]
468 fn test_clear_db() {
469 let db = GraphDb::in_memory().unwrap();
470 let node = Node {
471 id: 0, name: "test".into(), kind: NodeKind::Function,
472 language: Language::Rust, file_path: "t.rs".into(),
473 line_start: 1, line_end: 1, content: None,
474 };
475 db.insert_node(&node).unwrap();
476 assert_eq!(db.node_count().unwrap(), 1);
477 db.clear().unwrap();
478 assert_eq!(db.node_count().unwrap(), 0);
479 }
480
481 #[test]
482 fn test_node_serialization() {
483 let node = Node {
484 id: 1, name: "test_fn".into(), kind: NodeKind::Function,
485 language: Language::Python, file_path: "app.py".into(),
486 line_start: 10, line_end: 25, content: Some("def test_fn(): pass".into()),
487 };
488 let json = serde_json::to_string(&node).unwrap();
489 let parsed: Node = serde_json::from_str(&json).unwrap();
490 assert_eq!(parsed.name, "test_fn");
491 assert_eq!(parsed.kind, NodeKind::Function);
492 assert_eq!(parsed.language, Language::Python);
493 }
494
495 #[test]
496 fn test_language_extensions() {
497 assert!(Language::Rust.extensions().contains(&"rs"));
498 assert!(Language::TypeScript.extensions().contains(&"tsx"));
499 assert!(Language::Unknown.extensions().is_empty());
500 }
501
502 #[test]
503 fn test_multiple_nodes_same_name() {
504 let db = GraphDb::in_memory().unwrap();
505 for file in &["a.rs", "b.rs", "c.rs"] {
506 db.insert_node(&Node {
507 id: 0, name: "new".into(), kind: NodeKind::Method,
508 language: Language::Rust, file_path: file.to_string(),
509 line_start: 1, line_end: 5, content: None,
510 }).unwrap();
511 }
512 let results = db.search_nodes("new").unwrap();
513 assert_eq!(results.len(), 3, "Should find all 3 nodes named 'new'");
514 }
515
516 #[test]
517 fn test_search_empty_query() {
518 let db = GraphDb::in_memory().unwrap();
519 db.insert_node(&Node {
520 id: 0, name: "hello".into(), kind: NodeKind::Function,
521 language: Language::Rust, file_path: "t.rs".into(),
522 line_start: 1, line_end: 1, content: None,
523 }).unwrap();
524 let results = db.search_nodes("").unwrap();
526 assert_eq!(results.len(), 1);
527 }
528
529 #[test]
530 fn test_edges_from_nonexistent_node() {
531 let db = GraphDb::in_memory().unwrap();
532 let edges = db.edges_from(999).unwrap();
533 assert!(edges.is_empty());
534 }
535
536 #[test]
537 fn test_multiple_edge_types() {
538 let db = GraphDb::in_memory().unwrap();
539 let id1 = db.insert_node(&Node {
540 id: 0, name: "A".into(), kind: NodeKind::Struct,
541 language: Language::Rust, file_path: "a.rs".into(),
542 line_start: 1, line_end: 5, content: None,
543 }).unwrap();
544 let id2 = db.insert_node(&Node {
545 id: 0, name: "B".into(), kind: NodeKind::Trait,
546 language: Language::Rust, file_path: "b.rs".into(),
547 line_start: 1, line_end: 5, content: None,
548 }).unwrap();
549
550 db.insert_edge(&Edge { from_id: id1, to_id: id2, kind: EdgeKind::Implements, confidence: 1.0 }).unwrap();
551 db.insert_edge(&Edge { from_id: id1, to_id: id2, kind: EdgeKind::References, confidence: 0.8 }).unwrap();
552
553 let edges = db.edges_from(id1).unwrap();
554 assert_eq!(edges.len(), 2);
555 assert!(edges.iter().any(|e| e.kind == EdgeKind::Implements));
556 assert!(edges.iter().any(|e| e.kind == EdgeKind::References));
557 }
558
559 #[test]
560 fn test_edge_confidence_stored() {
561 let db = GraphDb::in_memory().unwrap();
562 let id1 = db.insert_node(&Node {
563 id: 0, name: "x".into(), kind: NodeKind::Function,
564 language: Language::Rust, file_path: "x.rs".into(),
565 line_start: 1, line_end: 1, content: None,
566 }).unwrap();
567 let id2 = db.insert_node(&Node {
568 id: 0, name: "y".into(), kind: NodeKind::Function,
569 language: Language::Rust, file_path: "y.rs".into(),
570 line_start: 1, line_end: 1, content: None,
571 }).unwrap();
572
573 db.insert_edge(&Edge { from_id: id1, to_id: id2, kind: EdgeKind::Calls, confidence: 0.75 }).unwrap();
574 let edges = db.edges_from(id1).unwrap();
575 assert!((edges[0].confidence - 0.75).abs() < 0.01);
576 }
577
578 #[test]
579 fn test_node_with_none_content() {
580 let node = Node {
581 id: 0, name: "no_content".into(), kind: NodeKind::Function,
582 language: Language::Go, file_path: "main.go".into(),
583 line_start: 1, line_end: 10, content: None,
584 };
585 let json = serde_json::to_string(&node).unwrap();
586 let parsed: Node = serde_json::from_str(&json).unwrap();
587 assert!(parsed.content.is_none());
588 assert_eq!(parsed.language, Language::Go);
589 }
590}