use super::*;
use rusqlite::Connection;
use serde_json::json;
use std::path::PathBuf;
use tempfile::NamedTempFile;
fn create_test_db_with_references() -> (NamedTempFile, Connection) {
let db_file = NamedTempFile::new().expect("failed to create temp file");
let conn = Connection::open(db_file.path()).expect("failed to open database");
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL,
data TEXT NOT NULL,
name TEXT
)",
[],
)
.expect("failed to execute SQL");
conn.execute(
"CREATE TABLE graph_edges (
id INTEGER PRIMARY KEY,
from_id INTEGER NOT NULL,
to_id INTEGER NOT NULL,
edge_type TEXT NOT NULL
)",
[],
)
.expect("failed to execute SQL");
let symbol_data = json!({
"symbol_id": "sym1",
"name": "test_func",
"kind": "Function",
"kind_normalized": "function"
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, data) VALUES (1, 'Symbol', ?1)",
[symbol_data],
)
.expect("failed to execute SQL");
let ref1_data = json!({
"file": "/test/file.rs",
"byte_start": 50,
"byte_end": 60,
"start_line": 3,
"start_col": 5,
"end_line": 3,
"end_col": 14
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES
(10, 'Reference', 'ref to test_func', ?1)",
[ref1_data],
)
.expect("failed to execute SQL");
let ref2_data = json!({
"file": "/test/file.rs",
"byte_start": 100,
"byte_end": 112,
"start_line": 7,
"start_col": 0,
"end_line": 7,
"end_col": 12
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES
(11, 'Reference', 'ref to TestStruct', ?1)",
[ref2_data],
)
.expect("failed to execute SQL");
let ref3_data = json!({
"file": "/test/other.rs",
"byte_start": 200,
"byte_end": 210,
"start_line": 10,
"start_col": 0,
"end_line": 10,
"end_col": 10
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES
(12, 'Reference', 'ref to helper', ?1)",
[ref3_data],
)
.expect("failed to execute SQL");
conn.execute(
"INSERT INTO graph_edges (from_id, to_id, edge_type) VALUES (10, 1, 'REFERENCES')",
[],
)
.expect("failed to execute SQL");
(db_file, conn)
}
fn create_test_db_with_method_calls() -> (NamedTempFile, Connection) {
let db_file = NamedTempFile::new().expect("failed to create temp file");
let conn = Connection::open(db_file.path()).expect("failed to open database");
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL,
data TEXT NOT NULL,
name TEXT
)",
[],
)
.expect("failed to execute SQL");
conn.execute(
"CREATE TABLE graph_edges (
id INTEGER PRIMARY KEY,
from_id INTEGER NOT NULL,
to_id INTEGER NOT NULL,
edge_type TEXT NOT NULL
)",
[],
)
.expect("failed to execute SQL");
let process_symbol = json!({
"symbol_id": "sym_process",
"name": "process",
"kind": "Function",
"kind_normalized": "function"
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES (1, 'Symbol', 'process', ?1)",
[process_symbol],
)
.expect("failed to execute SQL");
let other_symbol = json!({
"symbol_id": "sym_other",
"name": "other_method",
"kind": "Function",
"kind_normalized": "function"
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES (2, 'Symbol', 'other_method', ?1)",
[other_symbol],
)
.expect("failed to execute SQL");
let call_data = [
(
20,
"run_pipeline",
"process",
"/test/pipeline.rs",
300,
330,
12,
),
(
21,
"run_pipeline",
"process",
"/test/pipeline.rs",
400,
430,
18,
),
(22, "main", "process", "/test/main.rs", 100, 130, 5),
(23, "main", "other_method", "/test/main.rs", 200, 230, 9),
];
for (id, caller, callee, file, byte_start, byte_end, line) in call_data {
let data = json!({
"file": file,
"caller": caller,
"callee": callee,
"byte_start": byte_start,
"byte_end": byte_end,
"start_line": line,
"start_col": 4,
"end_line": line,
"end_col": 16
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES (?1, 'Call', ?2, ?3)",
rusqlite::params![id, format!("{} calls {}", caller, callee), data],
)
.expect("failed to execute SQL");
}
for (from_id, to_id) in [(20, 1), (21, 1), (22, 1), (23, 2)] {
conn.execute(
"INSERT INTO graph_edges (from_id, to_id, edge_type) VALUES (?1, ?2, 'CALLS')",
rusqlite::params![from_id, to_id],
)
.expect("failed to execute SQL");
}
let ref_data = json!({
"file": "/test/main.rs",
"byte_start": 100,
"byte_end": 112,
"start_line": 5,
"start_col": 4,
"end_line": 5,
"end_col": 16
})
.to_string();
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES (30, 'Reference', 'ref to process', ?1)",
[ref_data],
)
.expect("failed to execute SQL");
conn.execute(
"INSERT INTO graph_edges (from_id, to_id, edge_type) VALUES (30, 1, 'REFERENCES')",
[],
)
.expect("failed to execute SQL");
(db_file, conn)
}
#[test]
fn test_search_references_method_call_sites() {
let (db_file, _conn) = create_test_db_with_method_calls();
let options = SearchOptions {
db_path: db_file.path(),
query: "process",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
3,
"Method with 3 known call sites should return 3 references (Reference+Call span deduped)"
);
assert_eq!(
result.total_count, 3,
"Count query must return the same total as the row query"
);
for r in &result.results {
assert_eq!(r.referenced_symbol, "process");
}
let mut spans: Vec<(&str, u64)> = result
.results
.iter()
.map(|r| (r.span.file_path.as_str(), r.span.start_line))
.collect();
spans.sort();
assert_eq!(
spans,
vec![
("/test/main.rs", 5),
("/test/pipeline.rs", 12),
("/test/pipeline.rs", 18),
]
);
}
#[test]
fn test_search_references_method_call_sites_path_filter() {
let (db_file, _conn) = create_test_db_with_method_calls();
let path_filter = PathBuf::from("/test/pipeline.rs");
let options = SearchOptions {
db_path: db_file.path(),
query: "process",
path_filter: Some(&path_filter),
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
2,
"Path filter should restrict call-site references to /test/pipeline.rs"
);
assert_eq!(result.total_count, 2);
for r in &result.results {
assert_eq!(r.span.file_path, "/test/pipeline.rs");
}
}
#[test]
fn test_search_references_basic() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test_func",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
1,
"Should find 1 reference to test_func"
);
assert_eq!(result.results[0].referenced_symbol, "test_func");
assert_eq!(result.query, "test_func");
}
#[test]
fn test_search_references_empty_results() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "nonexistent",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
0,
"Should find 0 references for nonexistent symbol"
);
}
#[test]
fn test_search_references_prefix_match() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
1,
"Should find 1 reference with 'test' prefix"
);
assert_eq!(result.results[0].referenced_symbol, "test_func");
}
#[test]
fn test_search_references_regex_mode() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test.*",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: true,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
1,
"Should find 1 reference matching regex 'test.*'"
);
assert_eq!(result.results[0].referenced_symbol, "test_func");
}
#[test]
fn test_search_references_regex_no_match() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "xyz.*",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: true,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
0,
"Should find 0 references matching regex 'xyz.*'"
);
}
#[test]
fn test_search_references_score_exact_match() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test_func",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(result.results.len(), 1);
assert_eq!(
result.results[0].score,
Some(100),
"Exact match should have score 100"
);
}
#[test]
fn test_search_references_limit() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test",
path_filter: None,
kind_filter: None,
limit: 1,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
1,
"Limit should restrict results to 1"
);
}
#[test]
fn test_search_references_total_count() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test_func",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(result.total_count, 1, "Total count should be 1");
}
#[test]
fn test_search_references_path_filter() {
let (db_file, _conn) = create_test_db_with_references();
let path_filter = PathBuf::from("/test/file.rs");
let options = SearchOptions {
db_path: db_file.path(),
query: "test_func",
path_filter: Some(&path_filter),
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: false,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(
result.results.len(),
1,
"Should find 1 reference in /test/file.rs"
);
assert_eq!(result.results[0].span.file_path, "/test/file.rs");
}
#[test]
fn test_search_references_include_score() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test_func",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
assert_eq!(result.results.len(), 1);
assert!(
result.results[0].score.is_some(),
"Score should be included when include_score=true"
);
}
#[test]
fn test_search_references_ordering() {
let (db_file, _conn) = create_test_db_with_references();
let options = SearchOptions {
db_path: db_file.path(),
query: "test",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (result, _partial) = search_references(options).expect("search_references should succeed");
for i in 1..result.results.len() {
let prev_score = result.results[i - 1].score.unwrap_or(0);
let curr_score = result.results[i].score.unwrap_or(0);
assert!(
prev_score >= curr_score,
"Results should be sorted by score descending"
);
}
}