1use crate::error::{Error, Result};
6use crate::types::*;
7use rusqlite::Connection;
8use std::collections::HashMap;
9
10fn faces_table_exists(conn: &Connection) -> bool {
12 conn.query_row(
13 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='faces'",
14 [],
15 |r| r.get::<_, i64>(0),
16 )
17 .map(|n| n > 0)
18 .unwrap_or(false)
19}
20
21pub fn faces_list(conn: &Connection) -> Result<FacesData> {
33 if !faces_table_exists(conn) {
34 return Ok(FacesData::default());
35 }
36 let mut people: HashMap<String, PersonData> = HashMap::new();
37 {
38 let mut stmt = conn.prepare(
39 "SELECT f.id, f.hash, f.person_label, COALESCE(p.full_name, f.person_label) \
43 FROM faces f LEFT JOIN people p ON p.name = f.person_label \
44 WHERE f.confirmed = 1 AND f.person_label IS NOT NULL \
45 ORDER BY f.person_label, f.is_primary DESC, f.id ASC",
46 )?;
47 let rows = stmt.query_map([], |r| {
48 Ok((
49 r.get::<_, i64>(0)?,
50 r.get::<_, String>(1)?,
51 r.get::<_, String>(2)?,
52 r.get::<_, String>(3)?,
53 ))
54 })?;
55 for row in rows {
56 let (id, hash, label, full_name) = row?;
57 let person = people.entry(label.clone()).or_insert(PersonData {
58 label: label.clone(),
59 full_name,
60 face_ids: vec![],
61 representative_id: id,
62 hashes: vec![],
63 });
64 person.face_ids.push(id);
65 if !person.hashes.contains(&hash) {
66 person.hashes.push(hash);
67 }
68 }
69 }
70
71 let mut cluster_map: HashMap<i64, ClusterData> = HashMap::new();
72 {
73 let mut stmt = conn.prepare(
74 "SELECT id, hash, cluster_id FROM faces \
75 WHERE cluster_id IS NOT NULL AND (confirmed = 0 OR person_label IS NULL) \
76 ORDER BY cluster_id, id",
77 )?;
78 let rows = stmt.query_map([], |r| {
79 Ok((
80 r.get::<_, i64>(0)?,
81 r.get::<_, String>(1)?,
82 r.get::<_, i64>(2)?,
83 ))
84 })?;
85 for row in rows {
86 let (id, hash, cid) = row?;
87 let cluster = cluster_map.entry(cid).or_insert(ClusterData {
88 cluster_id: cid,
89 face_ids: vec![],
90 hashes: vec![],
91 });
92 cluster.face_ids.push(id);
93 if !cluster.hashes.contains(&hash) {
94 cluster.hashes.push(hash);
95 }
96 }
97 }
98
99 let mut singletons: Vec<SingletonData> = vec![];
100 {
101 let mut stmt = conn.prepare(
102 "SELECT id, hash FROM faces \
103 WHERE cluster_id IS NULL AND (confirmed = 0 OR person_label IS NULL) \
104 ORDER BY id",
105 )?;
106 let rows = stmt.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)))?;
107 for row in rows {
108 let (id, hash) = row?;
109 singletons.push(SingletonData { face_id: id, hash });
110 }
111 }
112
113 let mut people: Vec<PersonData> = people.into_values().collect();
125 people.sort_by_key(|a| a.full_name.to_lowercase());
126 let mut clusters: Vec<ClusterData> = cluster_map.into_values().collect();
127 clusters.sort_by(|a, b| {
128 b.face_ids
129 .len()
130 .cmp(&a.face_ids.len())
131 .then(a.cluster_id.cmp(&b.cluster_id))
132 });
133
134 Ok(FacesData {
135 people,
136 clusters,
137 singletons,
138 })
139}
140
141pub fn cluster_detail(conn: &Connection, cluster_id: i64) -> Result<ClusterDetail> {
143 let mut stmt = conn.prepare(
144 "SELECT f.id, f.hash, fh.path FROM faces f \
145 JOIN file_hashes fh ON f.hash = fh.hash \
146 WHERE f.cluster_id = ?1 ORDER BY f.id",
147 )?;
148 let faces = stmt
149 .query_map([cluster_id], |r| {
150 Ok(ClusterFaceData {
151 face_id: r.get(0)?,
152 hash: r.get(1)?,
153 path: r.get(2)?,
154 })
155 })?
156 .collect::<rusqlite::Result<Vec<_>>>()?;
157 Ok(ClusterDetail { cluster_id, faces })
158}
159
160pub fn person_detail(conn: &Connection, name: &str) -> Result<PersonDetail> {
162 let name = videre_core::person::normalize(name).unwrap_or_else(|| name.to_string());
166 let name = name.as_str();
167 let mut stmt = conn.prepare(
168 "SELECT f.id, f.hash, fh.path, f.is_primary FROM faces f \
169 JOIN file_hashes fh ON f.hash = fh.hash \
170 WHERE f.person_label = ?1 AND f.confirmed = 1 \
171 ORDER BY f.is_primary DESC, f.id",
172 )?;
173 let faces = stmt
174 .query_map([name], |r| {
175 Ok(PersonFaceData {
176 face_id: r.get(0)?,
177 hash: r.get(1)?,
178 path: r.get(2)?,
179 is_primary: r.get::<_, i64>(3)? != 0,
180 })
181 })?
182 .collect::<rusqlite::Result<Vec<_>>>()?;
183 let full_name: String = conn
186 .query_row(
187 "SELECT full_name FROM people WHERE name = ?1",
188 rusqlite::params![name],
189 |r| r.get(0),
190 )
191 .unwrap_or_else(|_| name.to_string());
192 Ok(PersonDetail {
193 label: name.to_string(),
194 full_name,
195 faces,
196 })
197}
198
199pub fn search_person(conn: &Connection, name: &str) -> Result<Vec<String>> {
202 Ok(videre_core::person_search::search_by_person(
203 conn, name, None,
204 )?)
205}
206
207pub fn assign(conn: &Connection, face_ids: &[i64], person_label: &str) -> Result<()> {
210 let display = crate::label::sanitize_person_label(person_label).ok_or(Error::Invalid)?;
214 let label = videre_core::person::normalize(&display).ok_or(Error::Invalid)?;
215 if face_ids.is_empty() {
218 return Err(Error::Invalid);
219 }
220 conn.execute_batch("BEGIN")?;
225 let result = (|| -> Result<()> {
226 conn.execute(
227 "INSERT INTO people (name, full_name) VALUES (?1, ?2) ON CONFLICT(name) DO NOTHING",
228 rusqlite::params![&label, &display],
229 )?;
230 for id in face_ids {
231 let n = conn.execute(
232 "UPDATE faces SET person_label = ?1, confirmed = 1 WHERE id = ?2",
233 rusqlite::params![label, id],
234 )?;
235 if n == 0 {
236 return Err(Error::NotFound);
237 }
238 }
239 Ok(())
240 })();
241 match result {
242 Ok(()) => {
243 conn.execute_batch("COMMIT")?;
244 Ok(())
245 }
246 Err(e) => {
247 let _ = conn.execute_batch("ROLLBACK");
248 Err(e)
249 }
250 }
251}
252
253pub fn new_person(conn: &Connection, face_ids: &[i64], label: &str) -> Result<()> {
257 assign(conn, face_ids, label)
258}
259
260pub fn remove_face(conn: &Connection, face_id: i64) -> Result<()> {
262 let n = conn.execute(
266 "UPDATE faces SET cluster_id = NULL, person_label = NULL, confirmed = 0, is_primary = 0 WHERE id = ?1",
267 [face_id],
268 )?;
269 if n == 0 {
270 return Err(Error::NotFound);
271 }
272 Ok(())
273}
274
275pub fn dissolve_cluster(conn: &Connection, cluster_id: i64) -> Result<()> {
277 let n = conn.execute(
281 "UPDATE faces SET cluster_id = NULL WHERE cluster_id = ?1",
282 [cluster_id],
283 )?;
284 if n == 0 {
285 return Err(Error::NotFound);
286 }
287 Ok(())
288}
289
290pub fn set_full_name(conn: &Connection, name: &str, full_name: &str) -> Result<()> {
301 let display = crate::label::sanitize_person_label(full_name).ok_or(Error::Invalid)?;
302 let name = videre_core::person::normalize(name).ok_or(Error::Invalid)?;
303 let n = conn.execute(
304 "UPDATE people SET full_name = ?1 WHERE name = ?2",
305 rusqlite::params![display, name],
306 )?;
307 if n == 0 {
308 return Err(Error::NotFound);
309 }
310 Ok(())
311}
312
313pub fn delete_person(conn: &Connection, label: &str) -> Result<()> {
314 let label = videre_core::person::normalize(label).unwrap_or_else(|| label.to_string());
315 conn.execute(
316 "UPDATE faces SET person_label = NULL, confirmed = 0, is_primary = 0 WHERE person_label = ?1",
317 rusqlite::params![label],
318 )?;
319 Ok(())
320}
321
322pub fn set_primary(conn: &Connection, face_id: i64, person_label: &str) -> Result<()> {
327 let person_label =
328 videre_core::person::normalize(person_label).unwrap_or_else(|| person_label.to_string());
329 conn.execute_batch("BEGIN")?;
330 let result = (|| -> Result<()> {
331 conn.execute(
332 "UPDATE faces SET is_primary = 0 WHERE person_label = ?1",
333 rusqlite::params![person_label],
334 )?;
335 let n = conn.execute(
340 "UPDATE faces SET is_primary = 1, confirmed = 1, person_label = ?1 WHERE id = ?2 AND person_label = ?1",
341 rusqlite::params![person_label, face_id],
342 )?;
343 if n == 0 {
344 return Err(Error::NotFound);
345 }
346 Ok(())
347 })();
348 match result {
349 Ok(()) => {
350 conn.execute_batch("COMMIT")?;
351 Ok(())
352 }
353 Err(e) => {
354 let _ = conn.execute_batch("ROLLBACK");
355 Err(e)
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 pub(super) fn seed() -> Connection {
371 let conn = Connection::open_in_memory().unwrap();
372 videre_core::face_db::create_faces_table(&conn).unwrap();
373 conn.execute_batch(
374 "CREATE TABLE file_hashes (hash TEXT PRIMARY KEY, path TEXT);
375 INSERT INTO file_hashes VALUES ('h1','/p/1.jpg'),('h2','/p/2.jpg'),
376 ('h3','/p/3.jpg'),('h4','/p/4.jpg'),('h5','/p/5.jpg');
377 -- Labels are stored in identity form, as `assign` writes them and
378 -- as the migration leaves them; `people` carries what a reader
379 -- sees. Seeding raw 'Alice' would test a state the application no
380 -- longer produces.
381 INSERT INTO people (name, full_name) VALUES ('alice','Alice');
382 INSERT INTO faces (id,hash,bbox,embedding,cluster_id,person_label,confirmed,is_primary) VALUES
383 (1,'h1','0,0,9,9',X'0000',NULL,'alice',1,1),
384 (2,'h2','0,0,9,9',X'0000',NULL,'alice',1,0),
385 (3,'h3','0,0,9,9',X'0000',7,NULL,0,0),
386 (4,'h4','0,0,9,9',X'0000',7,NULL,0,0),
387 (5,'h5','0,0,9,9',X'0000',NULL,NULL,0,0);",
388 )
389 .unwrap();
390 videre_core::db::ensure_file_hashes_columns(&conn);
391 conn
392 }
393
394 #[test]
395 fn the_list_comes_back_in_the_same_order_every_time() {
396 let conn = seed();
403 conn.execute_batch(
407 "INSERT INTO file_hashes (hash, path) VALUES ('h6','/p/6.jpg'),('h7','/p/7.jpg'),
410 ('h8','/p/8.jpg'),('h9','/p/9.jpg'),('h10','/p/10.jpg');
411 INSERT INTO faces (id,hash,bbox,embedding,cluster_id,person_label,confirmed,is_primary) VALUES
412 (6,'h6','0,0,9,9',X'0000',9,NULL,0,0),
413 (7,'h7','0,0,9,9',X'0000',9,NULL,0,0),
414 (8,'h8','0,0,9,9',X'0000',9,NULL,0,0),
415 (9,'h9','0,0,9,9',X'0000',3,NULL,0,0),
416 (10,'h10','0,0,9,9',X'0000',NULL,'Bob',1,0);",
417 )
418 .unwrap();
419
420 let a = faces_list(&conn).unwrap();
423 let b = faces_list(&conn).unwrap();
424
425 let ids = |f: &FacesData| -> Vec<i64> { f.clusters.iter().map(|c| c.cluster_id).collect() };
426 let names =
427 |f: &FacesData| -> Vec<String> { f.people.iter().map(|p| p.label.clone()).collect() };
428 assert!(ids(&a).len() >= 3, "fixture must have several clusters");
429 assert_eq!(
430 ids(&a),
431 ids(&b),
432 "cluster order must not change between calls"
433 );
434 assert_eq!(
435 names(&a),
436 names(&b),
437 "people order must not change between calls"
438 );
439
440 let sizes: Vec<usize> = a.clusters.iter().map(|c| c.face_ids.len()).collect();
443 let mut want = sizes.clone();
444 want.sort_unstable_by(|x, y| y.cmp(x));
445 assert_eq!(
446 sizes, want,
447 "clusters must be ordered largest first, got {sizes:?}"
448 );
449 }
450
451 #[test]
452 fn faces_list_splits_people_clusters_singletons() {
453 let conn = seed();
454 let d = faces_list(&conn).unwrap();
455 assert_eq!(d.people.len(), 1);
456 assert_eq!(d.people[0].label, "alice");
458 assert_eq!(d.people[0].full_name, "Alice");
459 assert_eq!(
460 d.people[0].representative_id, 1,
461 "primary face is representative"
462 );
463 assert_eq!(d.clusters.len(), 1);
464 assert_eq!(d.clusters[0].cluster_id, 7);
465 assert_eq!(d.clusters[0].face_ids, vec![3, 4]);
466 assert_eq!(d.singletons.len(), 1);
467 assert_eq!(d.singletons[0].face_id, 5);
468 }
469
470 #[test]
471 fn person_detail_marks_primary() {
472 let conn = seed();
473 let p = person_detail(&conn, "Alice").unwrap();
474 assert_eq!(p.faces.len(), 2);
475 assert!(p.faces[0].is_primary, "primary sorts first and is flagged");
476 assert!(!p.faces[1].is_primary);
477 }
478
479 #[test]
480 fn cluster_detail_lists_faces() {
481 let conn = seed();
482 let c = cluster_detail(&conn, 7).unwrap();
483 assert_eq!(c.cluster_id, 7);
484 assert_eq!(
485 c.faces.iter().map(|f| f.face_id).collect::<Vec<_>>(),
486 vec![3, 4]
487 );
488 }
489
490 #[test]
491 fn assign_labels_and_confirms() {
492 let conn = seed();
493 assign(&conn, &[3, 4], "Bob").unwrap();
494 let p = person_detail(&conn, "Bob").unwrap();
495 assert_eq!(p.faces.len(), 2, "both faces now confirmed under Bob");
496 }
497
498 #[test]
499 fn assign_rejects_empty_label() {
500 let conn = seed();
501 assert!(matches!(assign(&conn, &[3], " "), Err(Error::Invalid)));
502 }
503
504 #[test]
505 fn remove_face_unassigns_everything() {
506 let conn = seed();
507 remove_face(&conn, 1).unwrap();
508 let (cid, label, confirmed, prim): (Option<i64>, Option<String>, i64, i64) = conn
509 .query_row(
510 "SELECT cluster_id, person_label, confirmed, is_primary FROM faces WHERE id=1",
511 [],
512 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
513 )
514 .unwrap();
515 assert_eq!((cid, label, confirmed, prim), (None, None, 0, 0));
516 }
517
518 #[test]
519 fn dissolve_cluster_nulls_cluster_id() {
520 let conn = seed();
521 dissolve_cluster(&conn, 7).unwrap();
522 assert_eq!(faces_list(&conn).unwrap().clusters.len(), 0);
523 assert_eq!(
524 faces_list(&conn).unwrap().singletons.len(),
525 3,
526 "3,4 join 5 as singletons"
527 );
528 }
529
530 #[test]
531 fn delete_person_unassigns_without_touching_cluster() {
532 let conn = seed();
533 conn.execute("UPDATE faces SET cluster_id = 42 WHERE id = 1", [])
537 .unwrap();
538 delete_person(&conn, "Alice").unwrap();
539 assert_eq!(faces_list(&conn).unwrap().people.len(), 0, "Alice is gone");
540 let (cid, label, confirmed): (Option<i64>, Option<String>, i64) = conn
541 .query_row(
542 "SELECT cluster_id, person_label, confirmed FROM faces WHERE id = 1",
543 [],
544 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
545 )
546 .unwrap();
547 assert_eq!(cid, Some(42), "cluster_id must be preserved");
548 assert_eq!(label, None, "person_label cleared");
549 assert_eq!(confirmed, 0, "confirmed cleared");
550 }
551
552 #[test]
553 fn set_primary_is_exclusive_per_person() {
554 let conn = seed();
555 set_primary(&conn, 2, "Alice").unwrap();
556 let primaries: Vec<i64> = {
557 let mut s = conn
558 .prepare("SELECT id FROM faces WHERE person_label='alice' AND is_primary=1")
559 .unwrap();
560 s.query_map([], |r| r.get(0))
561 .unwrap()
562 .collect::<rusqlite::Result<_>>()
563 .unwrap()
564 };
565 assert_eq!(primaries, vec![2], "exactly one primary, now face 2");
566 }
567
568 #[test]
569 fn renaming_only_the_spelling_keeps_the_identity() {
570 let conn = seed();
573 set_full_name(&conn, "alice", "Alice Smith").unwrap();
574 let (name, full): (String, String) = conn
575 .query_row("SELECT name, full_name FROM people", [], |r| {
576 Ok((r.get(0)?, r.get(1)?))
577 })
578 .unwrap();
579 assert_eq!(name, "alice", "identity is unchanged");
580 assert_eq!(full, "Alice Smith", "only the display name moved");
581 assert_eq!(person_detail(&conn, "alice").unwrap().faces.len(), 2);
582 }
583
584 #[test]
591 fn assign_a_missing_face_is_not_found() {
592 let conn = seed();
593 assert!(matches!(assign(&conn, &[999], "Bob"), Err(Error::NotFound)));
594 }
595
596 #[test]
597 fn assign_is_atomic_when_one_face_is_missing() {
598 let conn = seed();
602 assert!(matches!(
603 assign(&conn, &[3, 999], "Bob"),
604 Err(Error::NotFound)
605 ));
606 let (label, confirmed): (Option<String>, i64) = conn
607 .query_row(
608 "SELECT person_label, confirmed FROM faces WHERE id = 3",
609 [],
610 |r| Ok((r.get(0)?, r.get(1)?)),
611 )
612 .unwrap();
613 assert_eq!(label, None, "face 3 must not have been labelled");
614 assert_eq!(confirmed, 0, "face 3 must not have been confirmed");
615 let bob: i64 = conn
616 .query_row("SELECT COUNT(*) FROM people WHERE name = 'bob'", [], |r| {
617 r.get(0)
618 })
619 .unwrap();
620 assert_eq!(
621 bob, 0,
622 "no person may be created when the assign rolls back"
623 );
624 }
625
626 #[test]
627 fn assign_rejects_empty_face_ids() {
628 let conn = seed();
631 assert!(matches!(assign(&conn, &[], "Bob"), Err(Error::Invalid)));
632 }
633
634 #[test]
635 fn remove_face_missing_is_not_found() {
636 let conn = seed();
637 assert!(matches!(remove_face(&conn, 999), Err(Error::NotFound)));
638 }
639
640 #[test]
641 fn dissolve_cluster_missing_is_not_found() {
642 let conn = seed();
643 assert!(matches!(dissolve_cluster(&conn, 999), Err(Error::NotFound)));
644 }
645
646 #[test]
647 fn set_primary_missing_face_is_not_found() {
648 let conn = seed();
649 assert!(matches!(
650 set_primary(&conn, 999, "Alice"),
651 Err(Error::NotFound)
652 ));
653 }
654
655 #[test]
656 fn set_primary_face_of_another_person_is_not_found_and_rolls_back() {
657 let conn = seed();
661 assert!(matches!(
662 set_primary(&conn, 5, "Alice"),
663 Err(Error::NotFound)
664 ));
665 let primary: i64 = conn
666 .query_row(
667 "SELECT id FROM faces WHERE person_label = 'alice' AND is_primary = 1",
668 [],
669 |r| r.get(0),
670 )
671 .unwrap();
672 assert_eq!(
673 primary, 1,
674 "the original primary must be restored on rollback"
675 );
676 }
677
678 #[test]
679 fn delete_person_missing_is_idempotent_success() {
680 let conn = seed();
686 assert!(delete_person(&conn, "Nobody").is_ok());
687 }
688}
689
690#[cfg(test)]
691mod identity_tests {
692 use super::tests::seed;
693 use super::*;
694
695 fn people(conn: &Connection) -> Vec<(String, String)> {
696 conn.prepare("SELECT name, full_name FROM people ORDER BY name")
697 .unwrap()
698 .query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
699 .unwrap()
700 .collect::<rusqlite::Result<_>>()
701 .unwrap()
702 }
703
704 #[test]
705 fn assign_stores_the_identity_and_records_the_display_name() {
706 let conn = seed();
707 assign(&conn, &[3], "Işıl Özyeğin").unwrap();
708
709 let label: String = conn
710 .query_row("SELECT person_label FROM faces WHERE id = 3", [], |r| {
711 r.get(0)
712 })
713 .unwrap();
714 assert_eq!(label, "isil_ozyegin", "faces hold the identity");
715 assert!(
716 people(&conn).contains(&("isil_ozyegin".into(), "Işıl Özyeğin".into())),
717 "and the spelling is kept for display"
718 );
719 }
720
721 #[test]
722 fn assigning_an_existing_name_in_another_case_joins_that_person() {
723 let conn = seed();
726 assign(&conn, &[3], "ALICE").unwrap();
727 assert_eq!(people(&conn).len(), 1, "still one person, not two");
728 assert_eq!(person_detail(&conn, "alice").unwrap().faces.len(), 3);
729 assert_eq!(
730 people(&conn)[0].1,
731 "Alice",
732 "the existing spelling is not overwritten by the new casing"
733 );
734 }
735
736 #[test]
737 fn assign_rejects_a_name_with_no_usable_identity() {
738 let conn = seed();
741 assert!(matches!(assign(&conn, &[3], "!!!"), Err(Error::Invalid)));
742 }
743
744 #[test]
745 fn person_detail_resolves_every_form_of_the_name() {
746 let conn = seed();
747 for form in ["alice", "Alice", "ALICE", " alice "] {
748 assert_eq!(
749 person_detail(&conn, form).unwrap().faces.len(),
750 2,
751 "form {form:?}"
752 );
753 }
754 }
755
756 #[test]
757 fn person_detail_reports_the_display_name() {
758 let d = person_detail(&seed(), "alice").unwrap();
759 assert_eq!(d.label, "alice");
760 assert_eq!(d.full_name, "Alice");
761 }
762
763 #[test]
764 fn person_detail_falls_back_when_there_is_no_people_row() {
765 let conn = seed();
767 conn.execute(
768 "INSERT INTO faces (id,hash,bbox,embedding,person_label,confirmed) \
769 VALUES (9,'h9','0,0,9,9',X'0000','orphan',1)",
770 [],
771 )
772 .unwrap();
773 let d = person_detail(&conn, "orphan").unwrap();
774 assert_eq!(d.full_name, "orphan", "falls back to the identity");
775 }
776
777 #[test]
778 fn set_full_name_changes_only_the_display_name() {
779 let conn = seed();
780 set_full_name(&conn, "alice", "Alice Smith").unwrap();
781 assert_eq!(people(&conn), vec![("alice".into(), "Alice Smith".into())]);
782 assert_eq!(
783 person_detail(&conn, "alice").unwrap().faces.len(),
784 2,
785 "no face was touched"
786 );
787 }
788
789 #[test]
790 fn set_full_name_accepts_any_form_of_the_identity() {
791 let conn = seed();
792 set_full_name(&conn, "ALICE", "Alice Smith").unwrap();
793 assert_eq!(people(&conn)[0].1, "Alice Smith");
794 }
795
796 #[test]
797 fn set_full_name_on_a_missing_person_is_not_found() {
798 assert!(matches!(
799 set_full_name(&seed(), "nobody", "Someone"),
800 Err(Error::NotFound)
801 ));
802 }
803
804 #[test]
805 fn set_full_name_rejects_an_empty_display_name() {
806 assert!(matches!(
808 set_full_name(&seed(), "alice", " "),
809 Err(Error::Invalid)
810 ));
811 }
812
813 #[test]
814 fn delete_person_accepts_any_form_of_the_name() {
815 let conn = seed();
816 delete_person(&conn, "Alice").unwrap();
817 let left: i64 = conn
818 .query_row(
819 "SELECT COUNT(*) FROM faces WHERE person_label IS NOT NULL",
820 [],
821 |r| r.get(0),
822 )
823 .unwrap();
824 assert_eq!(left, 0, "faces are unassigned whichever form was passed");
825 }
826
827 #[test]
828 fn set_primary_accepts_any_form_of_the_name() {
829 let conn = seed();
830 set_primary(&conn, 2, "ALICE").unwrap();
831 let primary: i64 = conn
832 .query_row(
833 "SELECT id FROM faces WHERE person_label='alice' AND is_primary=1",
834 [],
835 |r| r.get(0),
836 )
837 .unwrap();
838 assert_eq!(primary, 2);
839 }
840}
841
842#[cfg(test)]
843mod never_run_tests {
844 use super::*;
845
846 #[test]
858 fn a_library_that_never_ran_detection_is_empty_not_an_error() {
859 let conn = Connection::open_in_memory().unwrap();
860 conn.execute_batch(
861 "CREATE TABLE file_hashes (path TEXT PRIMARY KEY, hash TEXT NOT NULL);
862 CREATE TABLE people (name TEXT PRIMARY KEY, full_name TEXT);",
863 )
864 .unwrap();
865
866 let data = faces_list(&conn).expect("a library with no faces table is not an error");
867 assert!(data.people.is_empty());
868 assert!(data.clusters.is_empty());
869 assert!(data.singletons.is_empty());
870 }
871}