1use std::cmp::Ordering;
2use std::fmt::Write;
3use std::vec::IntoIter;
4
5use conciliator::{
6 Conciliator,
7 Print,
8 Paint,
9 Inline,
10 Buffer,
11 Tree as PrintTree
12};
13use rusqlite::{
14 OptionalExtension,
15 Result as SqlResult
16};
17
18use crate::data::{
19 Activity,
20 Category,
21 category::Path
22};
23
24use super::Database;
25
26#[derive(Debug, Eq, PartialEq, Default)]
27pub struct MemTree {
28 activities: Vec<Option<Activity>>,
29 categories: Vec<Option<Category>>
30}
31
32pub struct PrintableTree<'a> {
33 description: &'a str,
34 tree: MemTree
35}
36
37pub trait Tree: Database{
38 fn get_activity(&self, id: u64) -> SqlResult<Activity> {
39 self.db().query_row(
40 "SELECT id, name, category_id \
41 FROM activity \
42 JOIN activity_belongs_to ON id = activity_id \
43 WHERE id = ?1",
44 [id],
45 |row| row.try_into()
46 )
47 }
48 fn get_category(&self, id: u64) -> SqlResult<Category> {
49 self.db().query_row(
50 "SELECT id, name, path \
51 FROM category \
52 JOIN category_belongs_to ON id = category_id \
53 WHERE id = ?1",
54 [id],
55 |row| row.try_into()
56 )
57 }
58
59 fn find_activity(&self, name: &str) -> SqlResult<Option<Activity>> {
60 self.db().query_row(
61 "SELECT id, name, category_id \
62 FROM activity \
63 JOIN activity_belongs_to ON id = activity_id \
64 WHERE name LIKE ?1",
65 [name],
66 |row| row.try_into()
67 ).optional()
68 }
69 fn find_activities(&self, name: &str) -> SqlResult<Vec<Activity>> {
70 let mut stmt = self.db().prepare(
71 "SELECT id, name, category_id \
72 FROM activity \
73 JOIN activity_belongs_to ON id = activity_id \
74 WHERE name LIKE ?1 \
75 ORDER BY id"
76 )?;
77 let mut rows = stmt.query([name])?;
78 let mut acts = Vec::new();
79 while let Some(row) = rows.next()? {
80 acts.push(row.try_into()?);
81 }
82 Ok(acts)
83 }
84 fn find_activity_in(
85 &self,
86 cat: &Category,
87 name: &str)
88 -> SqlResult<Option<Activity>>
89 {
90 self.db().query_row(
91 "SELECT id, name, category_id \
92 FROM activity \
93 JOIN activity_belongs_to ON id = activity_id \
94 WHERE category_id = ?1 AND name LIKE ?2",
95 (cat.id, name),
96 |row| row.try_into()
97 ).optional()
98 }
99 fn find_category(&self, name: &str) -> SqlResult<Option<Category>> {
100 self.db().query_row(
101 "SELECT id, name, path \
102 FROM category \
103 JOIN category_belongs_to ON id = category_id \
104 WHERE name LIKE ?1",
105 [name],
106 |row| row.try_into()
107 ).optional()
108 }
109 fn find_categories(&self, name: &str) -> SqlResult<Vec<Category>> {
110 let mut stmt = self.db().prepare(
111 "SELECT id, name, path \
112 FROM category \
113 JOIN category_belongs_to ON id = category_id \
114 WHERE name LIKE ?1 \
115 ORDER BY path"
116 )?;
117 let mut rows = stmt.query([name])?;
118 let mut cats = Vec::new();
119 while let Some(row) = rows.next()? {
120 cats.push(row.try_into()?);
121 }
122 Ok(cats)
123 }
124 fn find_category_in(
125 &self,
126 cat: &Category,
127 name: &str)
128 -> SqlResult<Option<Category>>
129 {
130 let mut path = cat.path.serialize();
131 write!(path, "{}/", cat.id).unwrap();
132 self.db().query_row(
133 "SELECT id, name, path \
134 FROM category \
135 JOIN category_belongs_to ON id = category_id \
136 WHERE path = ?1 AND name LIKE ?2",
137 (path, name),
138 |row| row.try_into()
139 ).optional()
140 }
141 fn find_category_at(&self, path: &str) -> SqlResult<Option<Category>> {
142 let path = match path.split_once('/') {
143 Some(("", path)) if !path.is_empty() => path,
144 Some(..) | None => return Ok(None)
145 };
146 let mut stmt = self.db().prepare(
147 "SELECT id, name, path \
148 FROM category \
149 JOIN category_belongs_to ON id = category_id \
150 WHERE path = ?1 AND name LIKE ?2"
151 )?;
152 let mut cur_path = String::from("/");
153 let mut maybe_cat: Option<Category> = None;
154
155 for next in path.split_terminator('/') {
156 let queried = stmt
157 .query_row((&cur_path, next), |row| row.try_into())
158 .optional()?;
159 maybe_cat = queried;
160 match &maybe_cat {
161 Some(cat) => write!(cur_path, "{}/", cat.id).unwrap(),
162 None => break
163 }
164 }
165 Ok(maybe_cat)
166 }
167 fn find_root_category(&self, name: &str) -> SqlResult<Option<Category>> {
168 self.db().query_row(
169 "SELECT id, name, path \
170 FROM category \
171 JOIN category_belongs_to ON id = category_id \
172 WHERE name LIKE ?1 AND path = '/'",
173 [name],
174 |row| row.try_into()
175 ).optional()
176 }
177
178 fn get_activities(&self) -> SqlResult<Vec<Activity>> {
179 let mut stmt = self.db().prepare(
180 "SELECT id, name, category_id \
181 FROM activity \
182 JOIN activity_belongs_to ON id = activity_id \
183 ORDER BY id"
184 )?;
185 let mut rows = stmt.query([])?;
186 let mut acts = Vec::new();
187 while let Some(row) = rows.next()? {
188 acts.push(row.try_into()?);
189 }
190 Ok(acts)
191 }
192 fn get_categories(&self) -> SqlResult<Vec<Category>> {
193 let mut stmt = self.db().prepare(
194 "SELECT id, name, path \
195 FROM category \
196 JOIN category_belongs_to ON id = category_id \
197 ORDER BY id"
198 )?;
199 let mut rows = stmt.query([])?;
200 let mut cats = Vec::new();
201 while let Some(row) = rows.next()? {
202 cats.push(row.try_into()?);
203 }
204 Ok(cats)
205 }
206
207 fn get_activities_under(&self, cat: &Category) -> SqlResult<Vec<Activity>> {
208 let mut stmt = self.db().prepare(
209 "SELECT id, name, activity_belongs_to.category_id \
210 FROM activity \
211 JOIN activity_belongs_to ON id = activity_id \
212 JOIN category_belongs_to \
213 ON activity_belongs_to.category_id \
214 = category_belongs_to.category_id \
215 WHERE activity_belongs_to.category_id = ?1 \
216 OR path LIKE (?2 || ?1 || '/%') \
217 ORDER BY id"
218 )?;
219 let mut rows = stmt.query((cat.id, &cat.path))?;
220 let mut acts = Vec::new();
221 while let Some(row) = rows.next()? {
222 acts.push(row.try_into()?);
223 }
224 Ok(acts)
225 }
226
227 fn resolve_path_string(&self, path: &Path) -> SqlResult<String> {
228 let mut stmt = self.db()
229 .prepare("SELECT name FROM category WHERE id = ?1")?;
230
231 let folder = |mut s, id| -> SqlResult<String> {
232 stmt.query_row(
233 [id],
234 |row| {
235 let name = row.get_ref(0)?.as_str()?;
236 s += name;
237 Ok(())
238 }
239 )?;
240 s += "/";
241 Ok(s)
242 };
243
244 path.0
245 .iter()
246 .try_fold(String::from("/"), folder)
247 }
248 fn get_path_string(&self, category_id: u64) -> SqlResult<String> {
249 let cat = self.get_category(category_id)?;
250 let mut s = self.resolve_path_string(&cat.path)?;
251 s += &cat.name;
252 s += "/";
253 Ok(s)
254 }
255
256 fn create_root_category(&self, name: &str) -> SqlResult<u64> {
257 let tx = self.db().unchecked_transaction()?;
258 let id = self.db()
259 .prepare("INSERT INTO category (name) VALUES (?1)")?
260 .insert([name])?;
261
262 self.db().prepare(
263 "INSERT INTO category_belongs_to (category_id, path) \
264 VALUES (?1, '/')"
265 )?.insert([id])?;
266 tx.commit().map(|()| id as u64)
267 }
268
269 fn create_category(&self, cat: &Category, name: &str)
270 -> SqlResult<u64>
271 {
272 let tx = self.db().unchecked_transaction()?;
273
274 let id = self.db()
275 .prepare("INSERT INTO category (name) VALUES (?1)")?
276 .insert([name])?;
277
278 let mut path = cat.path.serialize();
279 path.push_str(&cat.id.to_string());
280 path.push('/');
281
282 self.db().prepare(
283 "INSERT INTO category_belongs_to (category_id, path) \
284 VALUES (?1, ?2)"
285 )?.insert((id, path))?;
286 tx.commit().map(|()| id as u64)
287 }
288
289 fn create_activity(&self, cat: &Category, name: &str)
290 -> SqlResult<u64>
291 {
292 let tx = self.db().unchecked_transaction()?;
293
294 let id = self.db()
295 .prepare("INSERT INTO activity (name) VALUES (?1)")?
296 .insert([name])?;
297
298 self.db().prepare(
299 "INSERT INTO activity_belongs_to (activity_id, category_id) \
300 VALUES (?1, ?2)"
301 )?.insert((id, cat.id))?;
302 tx.commit().map(|()| id as u64)
303 }
304
305 fn rename_activity(&self, act: &Activity) -> SqlResult<bool> {
306 self.db()
307 .prepare("UPDATE activity SET name = ?2 WHERE id = ?1")?
308 .execute((act.id, &act.name))
309 .map(|i| i == 1)
310 }
311
312 fn move_activity(&self, act: &Activity) -> SqlResult<bool> {
313 self.db()
314 .prepare(
315 "UPDATE activity_belongs_to \
316 SET category_id = ?2 \
317 WHERE activity_id = ?1"
318 )?
319 .execute((act.id, act.category_id))
320 .map(|i| i == 1)
321 }
322 fn delete_activity(&self, act: &Activity) -> SqlResult<bool> {
323 let tx = self.db().unchecked_transaction()?;
324 self.db()
325 .prepare(
326 "DELETE FROM activity_belongs_to \
327 WHERE activity_id == ?1"
328 )?
329 .execute([act.id])?;
330
331 let deleted = self.db()
332 .prepare("DELETE FROM activity WHERE id == ?1")?
333 .execute([act.id])
334 .map(|i| i == 1)?;
335
336 tx.commit().map(|()| deleted)
337 }
338 fn delete_category(&self, cat: &Category) -> SqlResult<bool> {
339 self.db()
340 .prepare("DELETE FROM category WHERE id == ?1")?
341 .execute([cat.id])
342 .map(|i| i == 1)
343 }
344
345 fn load_into_memory(&self) -> SqlResult<MemTree> {
346 let mut activities = Vec::new();
347 let mut categories = Vec::new();
348 let mut stmt = self.db().prepare(
350 "SELECT id, name, category_id \
351 FROM activity \
352 JOIN activity_belongs_to ON id = activity_id \
353 ORDER BY id"
354 )?;
355 let mut rows = stmt.query([])?;
356 while let Some(row) = rows.next()? {
357 let activity: Activity = row.try_into()?;
358 match Ord::cmp(&(activity.id as usize), &activities.len()) {
359 Ordering::Equal => activities.push(Some(activity)),
360 Ordering::Greater => {
361 activities.resize_with(activity.id as usize, || None);
362 activities.push(Some(activity));
363 }
364 Ordering::Less => unreachable!("activities out of order")
365 }
366 }
367 let mut stmt = self.db().prepare(
369 "SELECT id, name, path \
370 FROM category \
371 JOIN category_belongs_to ON id = category_id \
372 ORDER BY id"
373 )?;
374 let mut rows = stmt.query([])?;
375 while let Some(row) = rows.next()? {
376 let category: Category = row.try_into()?;
377 match Ord::cmp(&(category.id as usize), &categories.len()) {
378 Ordering::Equal => categories.push(Some(category)),
379 Ordering::Greater => {
380 categories.resize_with(category.id as usize, || None);
381 categories.push(Some(category));
382 }
383 Ordering::Less => unreachable!("categories out of order")
384 }
385 }
386 Ok(
387 MemTree {activities, categories}
388 )
389 }
390
391 fn print_tree<'a>(&self, desc: &'a str) -> SqlResult<PrintableTree<'a>> {
392 let mem_tree = self.load_into_memory()?;
393
394 Ok(PrintableTree{description: desc, tree: mem_tree})
395 }
396}
397
398impl<Db: Database + ?Sized> Tree for Db {}
399
400
401impl<'a> Print for PrintableTree<'a> {
402 fn print<C: Conciliator + ?Sized>(mut self, con: &C) {
403 enum Node {
404 Category(Category),
405 Activity(Activity)
406 }
407 impl Inline for Node {
408 fn inline(&self, buffer: &mut Buffer) {
409 match self {
410 Self::Category(cat) => {buffer
411 .push_alpha_bold("[")
412 .push(cat)
413 .push_alpha_bold("]");
414 },
415 Self::Activity(act) => act.inline(buffer)
416 }
417 }
418 }
419
420 let root_cats = self.tree.categories
421 .iter_mut()
422 .filter(|sub_cat| match sub_cat {
423 Some(cat) => cat.path.0.is_empty(),
424 None => false
425 })
426 .filter_map(Option::take)
427 .map(Node::Category)
428 .collect::<Vec<_>>()
429 .into_iter();
430
431 let next = move |node: Node| -> Option<IntoIter<Node>> {
432 match node {
433 Node::Category(cat) => {
434 let mut path = cat.path.0.clone();
435 path.push(cat.id);
436 let mut refs: Vec<_> = self.tree.categories
437 .iter_mut()
438 .filter(|sub_cat| match sub_cat {
439 Some(c) => c.path.is_child_of(&cat),
440 None => false
441 })
442 .filter_map(Option::take)
443 .map(Node::Category)
444 .collect();
445 refs.extend(self.tree.activities
446 .iter_mut()
447 .filter(|opt_act| match opt_act {
448 Some(act) => act.category_id == cat.id,
449 None => false
450 })
451 .filter_map(Option::take)
452 .map(Node::Activity)
453 );
454 Some(refs.into_iter())
455 },
456 Node::Activity(..) => None
457 }
458 };
459 PrintTree::new(self.description, root_cats, next).print(con)
460 }
461}
462
463
464#[test]
465fn empty() {
466 let db = crate::Nunc::testing().unwrap();
467 assert!(db.get_activities().unwrap().is_empty());
468 assert!(db.get_categories().unwrap().is_empty());
469
470 assert!(db.get_activity(0).is_err());
471 assert!(db.get_activity(1).is_err());
472 assert!(db.get_category(0).is_err());
473 assert!(db.get_category(1).is_err());
474}
475
476#[test]
477fn create() -> SqlResult<()> {
478 let db = crate::Nunc::testing().unwrap();
479
480 let root_id = db.create_root_category("root")?;
481 let cats = db.get_categories()?;
482 assert_eq!(cats.len(), 1);
483 assert_eq!(cats[0], db.get_category(root_id)?);
484 assert_eq!(cats[0].id, root_id);
485 assert_eq!(cats[0].name, "root");
486 assert!(cats[0].path.0.is_empty());
487
488 let sub_id = db.create_category(&cats[0], "sub")?;
489 let cats = db.get_categories()?;
490 assert_eq!(cats.len(), 2);
491 assert_eq!(cats[0], db.get_category(root_id)?);
492 assert_eq!(cats[0].id, root_id);
493 assert_eq!(cats[0].name, "root");
494 assert!(cats[0].path.0.is_empty());
495 assert_eq!(cats[1], db.get_category(sub_id)?);
496 assert_eq!(cats[1].id, sub_id);
497 assert_eq!(cats[1].name, "sub");
498 assert_eq!(cats[1].path.0, vec![cats[0].id]);
499
500 let root_act_id = db.create_activity(&cats[0], "act_in_root")?;
501 let sub_act_id = db.create_activity(&cats[1], "act_in_sub")?;
502 let acts = db.get_activities()?;
503 assert_eq!(acts.len(), 2);
504 assert_eq!(acts[0], db.get_activity(root_act_id)?);
505 assert_eq!(acts[0].id, root_act_id);
506 assert_eq!(acts[0].name, "act_in_root");
507 assert_eq!(acts[1], db.get_activity(sub_act_id)?);
508 assert_eq!(acts[1].id, sub_act_id);
509 assert_eq!(acts[1].name, "act_in_sub");
510 Ok(())
511}
512
513#[test]
514fn delete() -> SqlResult<()> {
515 let db = crate::Nunc::testing().unwrap();
516
517 let root = db.get_category(
518 db.create_root_category("root")?
519 )?;
520 db.delete_category(&root)?;
521 assert!(db.find_category("root")?.is_none());
522
523 let root = db.get_category(
524 db.create_root_category("root")?
525 )?;
526
527 db.create_activity(&root, "activity")?;
528 db.delete_category(&root).unwrap_err();
529 let act = db.find_activity("activity")?.unwrap();
530 db.delete_activity(&act)?;
531 db.delete_category(&root)?;
532 assert!(db.find_activity("activity")?.is_none());
533 assert!(db.find_category("root")?.is_none());
534 db.create_activity(&root, "activity").unwrap_err();
535
536 let root = db.get_category(
537 db.create_root_category("root")?
538 )?;
539 let sub = db.get_category(
540 db.create_category(&root, "sub")?
541 )?;
542 db.delete_category(&root).unwrap_err();
543 db.delete_category(&sub)?;
544 db.delete_category(&root)?;
545 assert!(db.find_category("sub")?.is_none());
546 assert!(db.find_category("root")?.is_none());
547 Ok(())
554}
555
556#[test]
557fn tree_constraints() -> SqlResult<()> {
558 let db = crate::Nunc::testing().unwrap();
559
560 let root = db.get_category(
561 db.create_root_category("root")?
562 )?;
563
564 db.create_activity(&root, "activity")?;
565 db.create_activity(&root, "activity").unwrap_err();
566
567 db.create_activity(&root, "activity_2")?;
568 let mut act_2 = db.find_activity("activity_2")?.unwrap();
569
570 act_2.name = "activity".to_owned();
571 db.rename_activity(&act_2).unwrap_err();
572
573
574 let root_2 = db.get_category(
575 db.create_root_category("root_2")?
576 )?;
577
578 let mut act_2 = db.find_activity("activity_2")?.unwrap();
579
580 act_2.category_id = root_2.id;
581 db.move_activity(&act_2)?;
582
583 act_2.name = "activity".to_owned();
584 db.rename_activity(&act_2)?;
585 Ok(())
586}
587
588#[test]
589fn activities_under() -> SqlResult<()> {
590 let nunc = crate::Nunc::testing().unwrap();
591 assert!(crate::tests::add_example_tree(&nunc).is_ok());
592
593 let alpha = nunc.find_category("Alpha")?.unwrap();
594 let beta = nunc.find_category("Beta")?.unwrap();
595 let gamma = nunc.find_category("Gamma")?.unwrap();
596 let hobby = nunc.find_category("Hobby")?.unwrap();
597 let language = nunc.find_category("Language")?.unwrap();
598
599 let check_act_names = |cat, expected| -> SqlResult<()> {
600 let mut act_names: Vec<_> = nunc.get_activities_under(cat)?
601 .drain(..)
602 .map(|act| act.name)
603 .collect();
604 act_names.sort();
605 assert_eq!(act_names, expected);
606 Ok(())
607 };
608
609 check_act_names(&alpha, vec!["Chess", "Run", "Russian"])?;
610 check_act_names(&beta, vec!["Fitness", "Programming"])?;
611 check_act_names(&gamma, vec!["Eating", "Sleep"])?;
612 check_act_names(&language, vec!["Russian"])?;
613 check_act_names(&hobby, vec!["Chess"])?;
614
615 nunc.create_root_category("empty")?;
616 let empty = nunc.find_category("empty")?.unwrap();
617 check_act_names(&empty, vec![])?;
618
619 let three = nunc.get_category(nunc.create_category(&hobby, "Three")?)?;
620 assert_eq!(nunc.resolve_path_string(&three.path)?, "/Alpha/Hobby/");
621 nunc.create_activity(&three, "Deep")?;
622 check_act_names(&three, vec!["Deep"])?;
623 check_act_names(&alpha, vec!["Chess", "Deep", "Run", "Russian"])?;
624 Ok(())
625}