1mod 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#[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
31pub fn get_note_by_filename(
33 conn: &Connection,
34 filename: &str,
35) -> rusqlite::Result<Option<NoteDescribeResult>> {
36 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 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 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 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 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 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 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 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 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 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 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 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 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 let result = get_note_by_filename(&conn, "nonexistent.md").unwrap();
206 assert!(result.is_none());
207 }
208}