Skip to main content

obsidian_cli_inspector/
query.rs

1// Query module for retrieving and searching vault data
2mod links;
3mod search;
4mod tags;
5
6pub use links::{
7    diagnose_broken_links, get_backlinks, get_dead_ends, get_forward_links, get_orphans,
8    get_unresolved_links, BrokenLinkResult, DiagnoseResult, LinkResult,
9};
10pub use search::{search_chunks, SearchResult};
11pub use tags::{
12    get_notes_by_tag, get_notes_by_tags_and, get_notes_by_tags_or, list_tags, TagResult,
13};
14
15use rusqlite::{Connection, OptionalExtension};
16use serde::Serialize;
17
18/// Metadata for a single note
19#[derive(Debug, Serialize)]
20pub struct NoteDescribeResult {
21    pub id: i64,
22    pub path: String,
23    pub title: String,
24    pub mtime: i64,
25    pub hash: String,
26    pub created_at: String,
27    pub updated_at: String,
28    pub frontmatter: Option<String>,
29}
30
31/// Get note metadata by path or title
32pub fn get_note_by_filename(
33    conn: &Connection,
34    filename: &str,
35) -> rusqlite::Result<Option<NoteDescribeResult>> {
36    // First try to find by exact path match
37    let result = conn
38        .query_row(
39            "SELECT id, path, title, mtime, hash, created_at, updated_at, frontmatter_json
40             FROM notes
41             WHERE path = ?1 OR title = ?1",
42            [filename],
43            |row| {
44                Ok(NoteDescribeResult {
45                    id: row.get(0)?,
46                    path: row.get(1)?,
47                    title: row.get(2)?,
48                    mtime: row.get(3)?,
49                    hash: row.get(4)?,
50                    created_at: row.get(5)?,
51                    updated_at: row.get(6)?,
52                    frontmatter: row.get(7)?,
53                })
54            },
55        )
56        .optional();
57
58    // If not found by exact match, try partial match on path or title
59    if let Ok(None) = result {
60        conn.query_row(
61            "SELECT id, path, title, mtime, hash, created_at, updated_at, frontmatter_json
62             FROM notes
63             WHERE path LIKE ?1 OR title LIKE ?1
64             LIMIT 1",
65            [format!("%{filename}%")],
66            |row| {
67                Ok(NoteDescribeResult {
68                    id: row.get(0)?,
69                    path: row.get(1)?,
70                    title: row.get(2)?,
71                    mtime: row.get(3)?,
72                    hash: row.get(4)?,
73                    created_at: row.get(5)?,
74                    updated_at: row.get(6)?,
75                    frontmatter: row.get(7)?,
76                })
77            },
78        )
79        .optional()
80    } else {
81        result
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_note_describe_result_creation() {
91        let note = NoteDescribeResult {
92            id: 1,
93            path: "test.md".to_string(),
94            title: "Test".to_string(),
95            mtime: 1234567890,
96            hash: "abc123".to_string(),
97            created_at: "2024-01-01".to_string(),
98            updated_at: "2024-01-02".to_string(),
99            frontmatter: Some("{}".to_string()),
100        };
101
102        assert_eq!(note.id, 1);
103        assert_eq!(note.path, "test.md");
104        assert!(note.frontmatter.is_some());
105    }
106
107    #[test]
108    fn test_note_describe_result_no_frontmatter() {
109        let note = NoteDescribeResult {
110            id: 1,
111            path: "test.md".to_string(),
112            title: "Test".to_string(),
113            mtime: 1234567890,
114            hash: "abc123".to_string(),
115            created_at: "2024-01-01".to_string(),
116            updated_at: "2024-01-02".to_string(),
117            frontmatter: None,
118        };
119
120        assert!(note.frontmatter.is_none());
121    }
122
123    #[test]
124    fn test_get_note_by_filename_exact_path() {
125        let conn = Connection::open_in_memory().unwrap();
126
127        // Create table
128        conn.execute(
129            "CREATE TABLE notes (id INTEGER PRIMARY KEY, path TEXT, title TEXT, mtime INTEGER, hash TEXT, created_at TEXT, updated_at TEXT, frontmatter_json TEXT)",
130            [],
131        ).unwrap();
132
133        // Insert test data using params
134        conn.execute(
135            "INSERT INTO notes (path, title, mtime, hash, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
136            rusqlite::params!["test.md", "Test Note", 1234567890_i64, "hash123", "2024-01-01", "2024-01-02"],
137        ).unwrap();
138
139        // Test exact path match
140        let result = get_note_by_filename(&conn, "test.md").unwrap();
141        assert!(result.is_some());
142        assert_eq!(result.unwrap().title, "Test Note");
143    }
144
145    #[test]
146    fn test_get_note_by_filename_exact_title() {
147        let conn = Connection::open_in_memory().unwrap();
148
149        // Create table
150        conn.execute(
151            "CREATE TABLE notes (id INTEGER PRIMARY KEY, path TEXT, title TEXT, mtime INTEGER, hash TEXT, created_at TEXT, updated_at TEXT, frontmatter_json TEXT)",
152            [],
153        ).unwrap();
154
155        // Insert test data
156        conn.execute(
157            "INSERT INTO notes (path, title, mtime, hash, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
158            rusqlite::params!["test.md", "Test Note", 1234567890_i64, "hash123", "2024-01-01", "2024-01-02"],
159        ).unwrap();
160
161        // Test exact title match
162        let result = get_note_by_filename(&conn, "Test Note").unwrap();
163        assert!(result.is_some());
164        assert_eq!(result.unwrap().title, "Test Note");
165    }
166
167    #[test]
168    fn test_get_note_by_filename_partial_match() {
169        let conn = Connection::open_in_memory().unwrap();
170
171        // Create table
172        conn.execute(
173            "CREATE TABLE notes (id INTEGER PRIMARY KEY, path TEXT, title TEXT, mtime INTEGER, hash TEXT, created_at TEXT, updated_at TEXT, frontmatter_json TEXT)",
174            [],
175        ).unwrap();
176
177        // Insert test data
178        conn.execute(
179            "INSERT INTO notes (path, title, mtime, hash, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
180            rusqlite::params!["test.md", "Test Note", 1234567890_i64, "hash123", "2024-01-01", "2024-01-02"],
181        ).unwrap();
182
183        // Test partial match
184        let result = get_note_by_filename(&conn, "Test").unwrap();
185        assert!(result.is_some());
186    }
187
188    #[test]
189    fn test_get_note_by_filename_not_found() {
190        let conn = Connection::open_in_memory().unwrap();
191
192        // Create table
193        conn.execute(
194            "CREATE TABLE notes (id INTEGER PRIMARY KEY, path TEXT, title TEXT, mtime INTEGER, hash TEXT, created_at TEXT, updated_at TEXT, frontmatter_json TEXT)",
195            [],
196        ).unwrap();
197
198        // Insert test data
199        conn.execute(
200            "INSERT INTO notes (path, title, mtime, hash, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
201            rusqlite::params!["test.md", "Test Note", 1234567890_i64, "hash123", "2024-01-01", "2024-01-02"],
202        ).unwrap();
203
204        // Test not found
205        let result = get_note_by_filename(&conn, "nonexistent.md").unwrap();
206        assert!(result.is_none());
207    }
208}