Skip to main content

nunc/core/
tree.rs

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		// Load activities, trusting DB consistency
349		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		// Load categories, trusting DB consistency
368		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	//TODO: this should fail but does not
548	// it does not seem viable to enforce category_belongs_to validity via SQL
549	// triggers, so create_category would have to check whether the (parent)
550	// category is still valid, which seems overkill.
551	//db.create_category(&root, "sub").unwrap_err();
552
553	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}