1use anyhow::Result;
2use chrono::Utc;
3use rusqlite::params;
4
5use super::{issue_from_row, validate_priority, validate_status, Database};
6use super::{MAX_DESCRIPTION_LEN, MAX_TITLE_LEN};
7use crate::models::Issue;
8
9impl Database {
10 pub fn create_issue(
11 &self,
12 title: &str,
13 description: Option<&str>,
14 priority: &str,
15 ) -> Result<i64> {
16 self.create_issue_with_parent(title, description, priority, None)
17 }
18
19 pub fn create_subissue(
20 &self,
21 parent_id: i64,
22 title: &str,
23 description: Option<&str>,
24 priority: &str,
25 ) -> Result<i64> {
26 self.create_issue_with_parent(title, description, priority, Some(parent_id))
27 }
28
29 fn create_issue_with_parent(
30 &self,
31 title: &str,
32 description: Option<&str>,
33 priority: &str,
34 parent_id: Option<i64>,
35 ) -> Result<i64> {
36 validate_priority(priority)?;
37 if title.len() > MAX_TITLE_LEN {
38 anyhow::bail!(
39 "Title exceeds maximum length of {} characters",
40 MAX_TITLE_LEN
41 );
42 }
43 if let Some(d) = description {
44 if d.len() > MAX_DESCRIPTION_LEN {
45 anyhow::bail!(
46 "Description exceeds maximum length of {} bytes",
47 MAX_DESCRIPTION_LEN
48 );
49 }
50 }
51 let now = Utc::now().to_rfc3339();
52 self.conn.execute(
53 "INSERT INTO issues (title, description, priority, parent_id, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, 'open', ?5, ?5)",
54 params![title, description, priority, parent_id, now],
55 )?;
56 Ok(self.conn.last_insert_rowid())
57 }
58
59 pub fn get_subissues(&self, parent_id: i64) -> Result<Vec<Issue>> {
60 let mut stmt = self.conn.prepare(
61 "SELECT id, title, description, status, priority, parent_id, created_at, updated_at, closed_at FROM issues WHERE parent_id = ?1 ORDER BY id",
62 )?;
63
64 let issues = stmt
65 .query_map([parent_id], issue_from_row)?
66 .collect::<std::result::Result<Vec<_>, _>>()?;
67
68 Ok(issues)
69 }
70
71 pub fn get_issue(&self, id: i64) -> Result<Option<Issue>> {
72 let mut stmt = self.conn.prepare(
73 "SELECT id, title, description, status, priority, parent_id, created_at, updated_at, closed_at FROM issues WHERE id = ?1",
74 )?;
75
76 let issue = stmt.query_row([id], issue_from_row).ok();
77
78 Ok(issue)
79 }
80
81 pub fn require_issue(&self, id: i64) -> Result<Issue> {
83 self.get_issue(id)?
84 .ok_or_else(|| anyhow::anyhow!("Issue {} not found", crate::utils::format_issue_id(id)))
85 }
86
87 pub fn list_issues(
88 &self,
89 status_filter: Option<&str>,
90 label_filter: Option<&str>,
91 priority_filter: Option<&str>,
92 ) -> Result<Vec<Issue>> {
93 let mut sql = String::from(
94 "SELECT DISTINCT i.id, i.title, i.description, i.status, i.priority, i.parent_id, i.created_at, i.updated_at, i.closed_at FROM issues i",
95 );
96 let mut conditions = Vec::new();
97 let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
98
99 if label_filter.is_some() {
100 sql.push_str(" JOIN labels l ON i.id = l.issue_id");
101 }
102
103 if let Some(status) = status_filter {
104 if status != "all" {
105 validate_status(status)?;
106 conditions.push("i.status = ?".to_string());
107 params_vec.push(Box::new(status.to_string()));
108 }
109 }
110
111 if let Some(label) = label_filter {
112 conditions.push("l.label = ?".to_string());
113 params_vec.push(Box::new(label.to_string()));
114 }
115
116 if let Some(priority) = priority_filter {
117 conditions.push("i.priority = ?".to_string());
118 params_vec.push(Box::new(priority.to_string()));
119 }
120
121 if !conditions.is_empty() {
122 sql.push_str(" WHERE ");
123 sql.push_str(&conditions.join(" AND "));
124 }
125
126 sql.push_str(" ORDER BY i.id DESC");
127
128 let mut stmt = self.conn.prepare(&sql)?;
129 let params_refs: Vec<&dyn rusqlite::ToSql> =
130 params_vec.iter().map(|p| p.as_ref()).collect();
131
132 let issues = stmt
133 .query_map(params_refs.as_slice(), issue_from_row)?
134 .collect::<std::result::Result<Vec<_>, _>>()?;
135
136 Ok(issues)
137 }
138
139 pub fn update_issue(
140 &self,
141 id: i64,
142 title: Option<&str>,
143 description: Option<&str>,
144 priority: Option<&str>,
145 ) -> Result<bool> {
146 if let Some(t) = title {
147 if t.len() > MAX_TITLE_LEN {
148 anyhow::bail!(
149 "Title exceeds maximum length of {} characters",
150 MAX_TITLE_LEN
151 );
152 }
153 }
154 if let Some(d) = description {
155 if d.len() > MAX_DESCRIPTION_LEN {
156 anyhow::bail!(
157 "Description exceeds maximum length of {} bytes",
158 MAX_DESCRIPTION_LEN
159 );
160 }
161 }
162 if let Some(p) = priority {
163 validate_priority(p)?;
164 }
165
166 let now = Utc::now().to_rfc3339();
167 let mut updates = vec!["updated_at = ?1".to_string()];
168 let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(now)];
169
170 if let Some(t) = title {
171 updates.push(format!("title = ?{}", params_vec.len() + 1));
172 params_vec.push(Box::new(t.to_string()));
173 }
174
175 if let Some(d) = description {
176 updates.push(format!("description = ?{}", params_vec.len() + 1));
177 params_vec.push(Box::new(d.to_string()));
178 }
179
180 if let Some(p) = priority {
181 updates.push(format!("priority = ?{}", params_vec.len() + 1));
182 params_vec.push(Box::new(p.to_string()));
183 }
184
185 params_vec.push(Box::new(id));
186 let sql = format!(
187 "UPDATE issues SET {} WHERE id = ?{}",
188 updates.join(", "),
189 params_vec.len()
190 );
191
192 let params_refs: Vec<&dyn rusqlite::ToSql> =
193 params_vec.iter().map(|p| p.as_ref()).collect();
194 let rows = self.conn.execute(&sql, params_refs.as_slice())?;
195 Ok(rows > 0)
196 }
197
198 pub fn close_issue(&self, id: i64) -> Result<bool> {
199 let now = Utc::now().to_rfc3339();
200 let rows = self.conn.execute(
201 "UPDATE issues SET status = 'closed', closed_at = ?1, updated_at = ?1 WHERE id = ?2",
202 params![now, id],
203 )?;
204 Ok(rows > 0)
205 }
206
207 pub fn reopen_issue(&self, id: i64) -> Result<bool> {
208 let now = Utc::now().to_rfc3339();
209 let rows = self.conn.execute(
210 "UPDATE issues SET status = 'open', closed_at = NULL, updated_at = ?1 WHERE id = ?2",
211 params![now, id],
212 )?;
213 Ok(rows > 0)
214 }
215
216 pub fn delete_issue(&self, id: i64) -> Result<bool> {
217 let rows = self
218 .conn
219 .execute("DELETE FROM issues WHERE id = ?1", [id])?;
220 Ok(rows > 0)
221 }
222
223 pub fn update_parent(&self, id: i64, parent_id: Option<i64>) -> Result<bool> {
224 let now = chrono::Utc::now().to_rfc3339();
225 let rows = self.conn.execute(
226 "UPDATE issues SET parent_id = ?1, updated_at = ?2 WHERE id = ?3",
227 params![parent_id, now, id],
228 )?;
229 Ok(rows > 0)
230 }
231
232 pub fn search_issues(&self, query: &str) -> Result<Vec<Issue>> {
234 let escaped = query.replace('%', "\\%").replace('_', "\\_");
235 let pattern = format!("%{}%", escaped);
236 let mut stmt = self.conn.prepare(
237 r#"
238 SELECT DISTINCT i.id, i.title, i.description, i.status, i.priority, i.parent_id, i.created_at, i.updated_at, i.closed_at
239 FROM issues i
240 LEFT JOIN comments c ON i.id = c.issue_id
241 WHERE i.title LIKE ?1 ESCAPE '\' COLLATE NOCASE
242 OR i.description LIKE ?1 ESCAPE '\' COLLATE NOCASE
243 OR c.content LIKE ?1 ESCAPE '\' COLLATE NOCASE
244 ORDER BY i.id DESC
245 "#,
246 )?;
247
248 let issues = stmt
249 .query_map([&pattern], issue_from_row)?
250 .collect::<std::result::Result<Vec<_>, _>>()?;
251
252 Ok(issues)
253 }
254}