1use flow_core::{CreateFeatureInput, DependencyRef, Feature, FeatureGraphNode, FeatureStats, Result};
2use rusqlite::{Connection, OptionalExtension, Row};
3use std::collections::{HashMap, HashSet};
4
5pub struct FeatureStore;
6
7impl FeatureStore {
8 pub fn create(conn: &Connection, input: &CreateFeatureInput) -> Result<Feature> {
10 let priority = input.priority.map_or_else(|| {
11 let max_priority: Option<i32> = conn
13 .query_row("SELECT MAX(priority) FROM features", [], |row| row.get(0))
14 .ok()
15 .flatten();
16 max_priority.unwrap_or(0) + 1
17 }, |p| p);
18
19 for dep_ref in &input.dependencies {
21 if let DependencyRef::Id(dep_id) = dep_ref {
22 let exists: bool = conn
23 .query_row(
24 "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
25 [dep_id],
26 |row| row.get(0),
27 )
28 .map_err(|e| {
29 flow_core::FlowError::Database(format!("dependency check failed: {e}"))
30 })?;
31 if !exists {
32 return Err(flow_core::FlowError::NotFound(format!(
33 "dependency {dep_id} not found"
34 )));
35 }
36 }
37 }
38
39 let deps_json = serde_json::to_string(
40 &input
41 .dependencies
42 .iter()
43 .filter_map(|d| match d {
44 DependencyRef::Id(id) => Some(*id),
45 DependencyRef::Index { .. } => None,
46 })
47 .collect::<Vec<_>>(),
48 )?;
49 let steps_json = serde_json::to_string(&input.steps)?;
50
51 conn.execute(
52 r"
53 INSERT INTO features (name, description, priority, category, steps, dependencies)
54 VALUES (?, ?, ?, ?, ?, ?)
55 ",
56 rusqlite::params![
57 &input.name,
58 &input.description,
59 priority,
60 &input.category,
61 steps_json,
62 deps_json,
63 ],
64 )
65 .map_err(|e| flow_core::FlowError::Database(format!("insert failed: {e}")))?;
66
67 let id = conn.last_insert_rowid();
68 Self::get_by_id(conn, id)?.ok_or_else(|| {
69 flow_core::FlowError::Database("feature not found after insert".to_string())
70 })
71 }
72
73 pub fn create_bulk(conn: &Connection, inputs: &[CreateFeatureInput]) -> Result<Vec<Feature>> {
75 if inputs.is_empty() {
76 return Ok(vec![]);
77 }
78
79 for (idx, input) in inputs.iter().enumerate() {
81 for dep_ref in &input.dependencies {
82 match dep_ref {
83 DependencyRef::Id(dep_id) => {
84 let exists: bool = conn
85 .query_row(
86 "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
87 [dep_id],
88 |row| row.get(0),
89 )
90 .map_err(|e| {
91 flow_core::FlowError::Database(format!(
92 "dependency check failed: {e}"
93 ))
94 })?;
95 if !exists {
96 return Err(flow_core::FlowError::NotFound(format!(
97 "dependency {dep_id} not found for feature at index {idx}"
98 )));
99 }
100 }
101 DependencyRef::Index { index } => {
102 if *index >= inputs.len() {
103 return Err(flow_core::FlowError::BadRequest(format!(
104 "index {index} out of bounds for feature at index {idx}"
105 )));
106 }
107 if *index == idx {
109 return Err(flow_core::FlowError::BadRequest(format!(
110 "feature at index {idx} cannot depend on itself"
111 )));
112 }
113 }
114 }
115 }
116 }
117
118 conn.execute_batch("BEGIN IMMEDIATE")
120 .map_err(|e| flow_core::FlowError::Database(format!("begin transaction failed: {e}")))?;
121
122 let result = (|| -> Result<Vec<Feature>> {
123 let max_priority: Option<i32> = conn
125 .query_row("SELECT MAX(priority) FROM features", [], |row| row.get(0))
126 .ok()
127 .flatten();
128 let mut next_priority = max_priority.unwrap_or(0) + 1;
129
130 let mut feature_ids = Vec::new();
132 for input in inputs {
133 let priority = input.priority.unwrap_or_else(|| {
134 let p = next_priority;
135 next_priority += 1;
136 p
137 });
138
139 let steps_json = serde_json::to_string(&input.steps)?;
140 let deps_json = "[]"; conn.execute(
143 r"
144 INSERT INTO features (name, description, priority, category, steps, dependencies)
145 VALUES (?, ?, ?, ?, ?, ?)
146 ",
147 rusqlite::params![
148 &input.name,
149 &input.description,
150 priority,
151 &input.category,
152 steps_json,
153 deps_json,
154 ],
155 )
156 .map_err(|e| flow_core::FlowError::Database(format!("bulk insert failed: {e}")))?;
157
158 feature_ids.push(conn.last_insert_rowid());
159 }
160
161 for (idx, input) in inputs.iter().enumerate() {
163 let feature_id = feature_ids[idx];
164 let mut resolved_deps = Vec::new();
165
166 for dep_ref in &input.dependencies {
167 match dep_ref {
168 DependencyRef::Id(id) => resolved_deps.push(*id),
169 DependencyRef::Index { index } => resolved_deps.push(feature_ids[*index]),
170 }
171 }
172
173 if !resolved_deps.is_empty() {
174 let deps_json = serde_json::to_string(&resolved_deps)?;
175 conn.execute(
176 "UPDATE features SET dependencies = ? WHERE id = ?",
177 rusqlite::params![deps_json, feature_id],
178 )
179 .map_err(|e| {
180 flow_core::FlowError::Database(format!("dependency update failed: {e}"))
181 })?;
182 }
183 }
184
185 feature_ids
187 .into_iter()
188 .map(|id| {
189 Self::get_by_id(conn, id)?
190 .ok_or_else(|| flow_core::FlowError::Database("feature disappeared".to_string()))
191 })
192 .collect()
193 })();
194
195 match result {
196 Ok(features) => {
197 conn.execute_batch("COMMIT")
198 .map_err(|e| flow_core::FlowError::Database(format!("commit failed: {e}")))?;
199 Ok(features)
200 }
201 Err(e) => {
202 let _ = conn.execute_batch("ROLLBACK");
203 Err(e)
204 }
205 }
206 }
207
208 pub fn get_by_id(conn: &Connection, id: i64) -> Result<Option<Feature>> {
210 let result = conn
211 .query_row(
212 "SELECT * FROM features WHERE id = ?",
213 [id],
214 row_to_feature,
215 )
216 .optional()
217 .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?;
218 Ok(result)
219 }
220
221 pub fn get_all(conn: &Connection) -> Result<Vec<Feature>> {
223 let mut stmt = conn
224 .prepare("SELECT * FROM features ORDER BY priority ASC")
225 .map_err(|e| flow_core::FlowError::Database(format!("prepare failed: {e}")))?;
226
227 let features = stmt
228 .query_map([], row_to_feature)
229 .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?
230 .collect::<std::result::Result<Vec<_>, _>>()
231 .map_err(|e| flow_core::FlowError::Database(format!("row parse failed: {e}")))?;
232
233 Ok(features)
234 }
235
236 pub fn get_stats(conn: &Connection) -> Result<FeatureStats> {
238 let (total, passing, in_progress): (usize, usize, usize) = conn
239 .query_row(
240 r"
241 SELECT
242 COUNT(*),
243 SUM(CASE WHEN passes = 1 THEN 1 ELSE 0 END),
244 SUM(CASE WHEN in_progress = 1 THEN 1 ELSE 0 END)
245 FROM features
246 ",
247 [],
248 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
249 )
250 .map_err(|e| flow_core::FlowError::Database(format!("stats query failed: {e}")))?;
251
252 let failing = total.saturating_sub(passing).saturating_sub(in_progress);
253
254 let all_features = Self::get_all(conn)?;
256 let blocked = all_features
257 .iter()
258 .filter(|f| {
259 f.dependencies.iter().any(|dep_id| {
260 all_features
261 .iter()
262 .find(|d| d.id == *dep_id)
263 .is_some_and(|d| !d.passes)
264 })
265 })
266 .count();
267
268 Ok(FeatureStats {
269 total,
270 passing,
271 failing,
272 in_progress,
273 blocked,
274 })
275 }
276
277 pub fn get_ready(conn: &Connection) -> Result<Vec<Feature>> {
279 let all_features = Self::get_all(conn)?;
280 let passing_ids: HashSet<i64> = all_features
281 .iter()
282 .filter(|f| f.passes)
283 .map(|f| f.id)
284 .collect();
285
286 let ready: Vec<Feature> = all_features
287 .into_iter()
288 .filter(|f| {
289 !f.in_progress
290 && !f.passes
291 && f.dependencies.iter().all(|dep_id| passing_ids.contains(dep_id))
292 })
293 .collect();
294
295 Ok(ready)
296 }
297
298 pub fn get_blocked(conn: &Connection) -> Result<Vec<Feature>> {
300 let all_features = Self::get_all(conn)?;
301 let passing_ids: HashSet<i64> = all_features
302 .iter()
303 .filter(|f| f.passes)
304 .map(|f| f.id)
305 .collect();
306
307 let blocked: Vec<Feature> = all_features
308 .into_iter()
309 .filter(|f| {
310 !f.dependencies.is_empty()
311 && f.dependencies
312 .iter()
313 .any(|dep_id| !passing_ids.contains(dep_id))
314 })
315 .collect();
316
317 Ok(blocked)
318 }
319
320 pub fn claim_and_get(conn: &Connection, id: i64) -> Result<Feature> {
322 let rows_affected = conn
323 .execute(
324 r"
325 UPDATE features
326 SET in_progress = 1, updated_at = datetime('now')
327 WHERE id = ? AND in_progress = 0 AND passes = 0
328 ",
329 [id],
330 )
331 .map_err(|e| flow_core::FlowError::Database(format!("claim failed: {e}")))?;
332
333 if rows_affected == 0 {
334 return Err(flow_core::FlowError::Conflict(format!(
335 "feature {id} is already claimed or completed"
336 )));
337 }
338
339 Self::get_by_id(conn, id)?
340 .ok_or_else(|| flow_core::FlowError::NotFound(format!("feature {id} not found")))
341 }
342
343 pub fn mark_passing(conn: &Connection, id: i64) -> Result<()> {
345 let rows = conn
346 .execute(
347 "UPDATE features SET passes = 1, in_progress = 0, updated_at = datetime('now') WHERE id = ?",
348 [id],
349 )
350 .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
351 if rows == 0 {
352 return Err(flow_core::FlowError::NotFound(format!(
353 "feature {id} not found"
354 )));
355 }
356 Ok(())
357 }
358
359 pub fn mark_failing(conn: &Connection, id: i64) -> Result<()> {
361 let rows = conn
362 .execute(
363 "UPDATE features SET passes = 0, in_progress = 0, updated_at = datetime('now') WHERE id = ?",
364 [id],
365 )
366 .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
367 if rows == 0 {
368 return Err(flow_core::FlowError::NotFound(format!(
369 "feature {id} not found"
370 )));
371 }
372 Ok(())
373 }
374
375 pub fn mark_in_progress(conn: &Connection, id: i64) -> Result<()> {
377 let rows = conn
378 .execute(
379 "UPDATE features SET in_progress = 1, updated_at = datetime('now') WHERE id = ? AND in_progress = 0",
380 [id],
381 )
382 .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
383
384 if rows == 0 {
385 return Err(flow_core::FlowError::Conflict(format!(
386 "feature {id} is already in progress"
387 )));
388 }
389 Ok(())
390 }
391
392 pub fn clear_in_progress(conn: &Connection, id: i64) -> Result<()> {
394 conn.execute(
395 "UPDATE features SET in_progress = 0, updated_at = datetime('now') WHERE id = ?",
396 [id],
397 )
398 .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
399 Ok(())
400 }
401
402 pub fn skip(conn: &Connection, id: i64) -> Result<()> {
404 let max_priority: Option<i32> = conn
405 .query_row("SELECT MAX(priority) FROM features", [], |row| row.get(0))
406 .ok()
407 .flatten();
408 let new_priority = max_priority.unwrap_or(0) + 1;
409
410 conn.execute(
411 "UPDATE features SET priority = ?, updated_at = datetime('now') WHERE id = ?",
412 rusqlite::params![new_priority, id],
413 )
414 .map_err(|e| flow_core::FlowError::Database(format!("skip failed: {e}")))?;
415 Ok(())
416 }
417
418 pub fn add_dependency(conn: &Connection, feature_id: i64, dep_id: i64) -> Result<()> {
421 conn.execute_batch("SAVEPOINT add_dep")
422 .map_err(|e| flow_core::FlowError::Database(format!("savepoint failed: {e}")))?;
423
424 let result = (|| -> Result<()> {
425 let exists: bool = conn
427 .query_row(
428 "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
429 [dep_id],
430 |row| row.get(0),
431 )
432 .map_err(|e| flow_core::FlowError::Database(format!("exists check failed: {e}")))?;
433
434 if !exists {
435 return Err(flow_core::FlowError::NotFound(format!(
436 "dependency {dep_id} not found"
437 )));
438 }
439
440 let deps_json: String = conn
442 .query_row(
443 "SELECT dependencies FROM features WHERE id = ?",
444 [feature_id],
445 |row| row.get(0),
446 )
447 .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?;
448
449 let mut deps: Vec<i64> = serde_json::from_str(&deps_json)?;
450
451 if !deps.contains(&dep_id) {
452 deps.push(dep_id);
453 let new_deps_json = serde_json::to_string(&deps)?;
454 conn.execute(
455 "UPDATE features SET dependencies = ?, updated_at = datetime('now') WHERE id = ?",
456 rusqlite::params![new_deps_json, feature_id],
457 )
458 .map_err(|e| {
459 flow_core::FlowError::Database(format!("dependency add failed: {e}"))
460 })?;
461 }
462
463 Ok(())
464 })();
465
466 match result {
467 Ok(()) => {
468 conn.execute_batch("RELEASE SAVEPOINT add_dep")
469 .map_err(|e| flow_core::FlowError::Database(format!("release failed: {e}")))?;
470 Ok(())
471 }
472 Err(e) => {
473 let _ = conn.execute_batch("ROLLBACK TO SAVEPOINT add_dep");
474 Err(e)
475 }
476 }
477 }
478
479 pub fn remove_dependency(conn: &Connection, feature_id: i64, dep_id: i64) -> Result<()> {
482 conn.execute_batch("SAVEPOINT rm_dep")
483 .map_err(|e| flow_core::FlowError::Database(format!("savepoint failed: {e}")))?;
484
485 let result = (|| -> Result<()> {
486 let deps_json: String = conn
487 .query_row(
488 "SELECT dependencies FROM features WHERE id = ?",
489 [feature_id],
490 |row| row.get(0),
491 )
492 .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?;
493
494 let mut deps: Vec<i64> = serde_json::from_str(&deps_json)?;
495 deps.retain(|&id| id != dep_id);
496
497 let new_deps_json = serde_json::to_string(&deps)?;
498 conn.execute(
499 "UPDATE features SET dependencies = ?, updated_at = datetime('now') WHERE id = ?",
500 rusqlite::params![new_deps_json, feature_id],
501 )
502 .map_err(|e| {
503 flow_core::FlowError::Database(format!("dependency remove failed: {e}"))
504 })?;
505
506 Ok(())
507 })();
508
509 match result {
510 Ok(()) => {
511 conn.execute_batch("RELEASE SAVEPOINT rm_dep")
512 .map_err(|e| flow_core::FlowError::Database(format!("release failed: {e}")))?;
513 Ok(())
514 }
515 Err(e) => {
516 let _ = conn.execute_batch("ROLLBACK TO SAVEPOINT rm_dep");
517 Err(e)
518 }
519 }
520 }
521
522 pub fn set_dependencies(conn: &Connection, feature_id: i64, dep_ids: &[i64]) -> Result<()> {
525 conn.execute_batch("SAVEPOINT set_deps")
526 .map_err(|e| flow_core::FlowError::Database(format!("savepoint failed: {e}")))?;
527
528 let result = (|| -> Result<()> {
529 for dep_id in dep_ids {
531 let exists: bool = conn
532 .query_row(
533 "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
534 [dep_id],
535 |row| row.get(0),
536 )
537 .map_err(|e| {
538 flow_core::FlowError::Database(format!("exists check failed: {e}"))
539 })?;
540
541 if !exists {
542 return Err(flow_core::FlowError::NotFound(format!(
543 "dependency {dep_id} not found"
544 )));
545 }
546 }
547
548 let deps_json = serde_json::to_string(dep_ids)?;
549 conn.execute(
550 "UPDATE features SET dependencies = ?, updated_at = datetime('now') WHERE id = ?",
551 rusqlite::params![deps_json, feature_id],
552 )
553 .map_err(|e| flow_core::FlowError::Database(format!("set dependencies failed: {e}")))?;
554
555 Ok(())
556 })();
557
558 match result {
559 Ok(()) => {
560 conn.execute_batch("RELEASE SAVEPOINT set_deps")
561 .map_err(|e| flow_core::FlowError::Database(format!("release failed: {e}")))?;
562 Ok(())
563 }
564 Err(e) => {
565 let _ = conn.execute_batch("ROLLBACK TO SAVEPOINT set_deps");
566 Err(e)
567 }
568 }
569 }
570
571 pub fn get_graph(conn: &Connection) -> Result<Vec<FeatureGraphNode>> {
573 let features = Self::get_all(conn)?;
574 let passing_ids: HashSet<i64> = features
575 .iter()
576 .filter(|f| f.passes)
577 .map(|f| f.id)
578 .collect();
579
580 let mut dependents_map: HashMap<i64, Vec<i64>> = HashMap::new();
582 for feature in &features {
583 for dep_id in &feature.dependencies {
584 dependents_map
585 .entry(*dep_id)
586 .or_default()
587 .push(feature.id);
588 }
589 }
590
591 let graph: Vec<FeatureGraphNode> = features
592 .into_iter()
593 .map(|f| {
594 let blocked = !f.dependencies.is_empty()
595 && f.dependencies
596 .iter()
597 .any(|dep_id| !passing_ids.contains(dep_id));
598
599 FeatureGraphNode {
600 id: f.id,
601 name: f.name,
602 category: f.category,
603 priority: f.priority,
604 passes: f.passes,
605 in_progress: f.in_progress,
606 blocked,
607 dependencies: f.dependencies,
608 dependents: dependents_map.get(&f.id).cloned().unwrap_or_default(),
609 }
610 })
611 .collect();
612
613 Ok(graph)
614 }
615}
616
617fn row_to_feature(row: &Row) -> rusqlite::Result<Feature> {
619 let steps_json: String = row.get("steps")?;
620 let steps: Vec<String> = serde_json::from_str(&steps_json).map_err(|e| {
621 rusqlite::Error::FromSqlConversionFailure(
622 0,
623 rusqlite::types::Type::Text,
624 Box::new(e),
625 )
626 })?;
627
628 let deps_json: String = row.get("dependencies")?;
629 let dependencies: Vec<i64> = serde_json::from_str(&deps_json).map_err(|e| {
630 rusqlite::Error::FromSqlConversionFailure(
631 0,
632 rusqlite::types::Type::Text,
633 Box::new(e),
634 )
635 })?;
636
637 let passes_int: i32 = row.get("passes")?;
638 let in_progress_int: i32 = row.get("in_progress")?;
639
640 Ok(Feature {
641 id: row.get("id")?,
642 priority: row.get("priority")?,
643 category: row.get("category")?,
644 name: row.get("name")?,
645 description: row.get("description")?,
646 steps,
647 passes: passes_int != 0,
648 in_progress: in_progress_int != 0,
649 dependencies,
650 created_at: row.get("created_at")?,
651 updated_at: row.get("updated_at")?,
652 })
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::Database;
659
660 #[test]
661 fn test_create_and_get() {
662 let db = Database::open_in_memory().unwrap();
663 let conn = db.writer().lock().unwrap();
664
665 let feature = FeatureStore::create(
666 &conn,
667 &CreateFeatureInput {
668 name: "Test Feature".to_string(),
669 description: "Test Description".to_string(),
670 priority: Some(1),
671 category: "Test".to_string(),
672 steps: vec!["Step 1".to_string(), "Step 2".to_string()],
673 dependencies: vec![],
674 },
675 )
676 .unwrap();
677
678 assert_eq!(feature.id, 1);
679 assert_eq!(feature.name, "Test Feature");
680 assert_eq!(feature.priority, 1);
681 assert_eq!(feature.steps.len(), 2);
682
683 let retrieved = FeatureStore::get_by_id(&conn, 1).unwrap().unwrap();
684 assert_eq!(retrieved.name, "Test Feature");
685 }
686
687 #[test]
688 fn test_bulk_create_with_index_dependencies() {
689 let db = Database::open_in_memory().unwrap();
690 let conn = db.writer().lock().unwrap();
691
692 let inputs = vec![
693 CreateFeatureInput {
694 name: "Feature A".to_string(),
695 description: "".to_string(),
696 priority: None,
697 category: "".to_string(),
698 steps: vec![],
699 dependencies: vec![],
700 },
701 CreateFeatureInput {
702 name: "Feature B".to_string(),
703 description: "".to_string(),
704 priority: None,
705 category: "".to_string(),
706 steps: vec![],
707 dependencies: vec![DependencyRef::Index { index: 0 }],
708 },
709 CreateFeatureInput {
710 name: "Feature C".to_string(),
711 description: "".to_string(),
712 priority: None,
713 category: "".to_string(),
714 steps: vec![],
715 dependencies: vec![DependencyRef::Index { index: 0 }, DependencyRef::Index { index: 1 }],
716 },
717 ];
718
719 let features = FeatureStore::create_bulk(&conn, &inputs).unwrap();
720 assert_eq!(features.len(), 3);
721
722 assert_eq!(features[1].dependencies, vec![features[0].id]);
724
725 assert_eq!(
727 features[2].dependencies,
728 vec![features[0].id, features[1].id]
729 );
730 }
731
732 #[test]
733 fn test_claim_and_get() {
734 let db = Database::open_in_memory().unwrap();
735 let conn = db.writer().lock().unwrap();
736
737 let feature = FeatureStore::create(
738 &conn,
739 &CreateFeatureInput {
740 name: "Claimable".to_string(),
741 description: "".to_string(),
742 priority: Some(1),
743 category: "".to_string(),
744 steps: vec![],
745 dependencies: vec![],
746 },
747 )
748 .unwrap();
749
750 let claimed = FeatureStore::claim_and_get(&conn, feature.id).unwrap();
752 assert!(claimed.in_progress);
753
754 let result = FeatureStore::claim_and_get(&conn, feature.id);
756 assert!(result.is_err());
757 assert!(matches!(result.unwrap_err(), flow_core::FlowError::Conflict(_)));
758 }
759
760 #[test]
761 fn test_mark_passing_failing() {
762 let db = Database::open_in_memory().unwrap();
763 let conn = db.writer().lock().unwrap();
764
765 let feature = FeatureStore::create(
766 &conn,
767 &CreateFeatureInput {
768 name: "Test".to_string(),
769 description: "".to_string(),
770 priority: Some(1),
771 category: "".to_string(),
772 steps: vec![],
773 dependencies: vec![],
774 },
775 )
776 .unwrap();
777
778 FeatureStore::mark_passing(&conn, feature.id).unwrap();
780 let updated = FeatureStore::get_by_id(&conn, feature.id).unwrap().unwrap();
781 assert!(updated.passes);
782 assert!(!updated.in_progress);
783
784 FeatureStore::mark_failing(&conn, feature.id).unwrap();
786 let updated = FeatureStore::get_by_id(&conn, feature.id).unwrap().unwrap();
787 assert!(!updated.passes);
788 assert!(!updated.in_progress);
789 }
790
791 #[test]
792 fn test_dependencies() {
793 let db = Database::open_in_memory().unwrap();
794 let conn = db.writer().lock().unwrap();
795
796 let f1 = FeatureStore::create(
797 &conn,
798 &CreateFeatureInput {
799 name: "F1".to_string(),
800 description: "".to_string(),
801 priority: Some(1),
802 category: "".to_string(),
803 steps: vec![],
804 dependencies: vec![],
805 },
806 )
807 .unwrap();
808
809 let f2 = FeatureStore::create(
810 &conn,
811 &CreateFeatureInput {
812 name: "F2".to_string(),
813 description: "".to_string(),
814 priority: Some(2),
815 category: "".to_string(),
816 steps: vec![],
817 dependencies: vec![],
818 },
819 )
820 .unwrap();
821
822 FeatureStore::add_dependency(&conn, f2.id, f1.id).unwrap();
824 let updated = FeatureStore::get_by_id(&conn, f2.id).unwrap().unwrap();
825 assert_eq!(updated.dependencies, vec![f1.id]);
826
827 FeatureStore::remove_dependency(&conn, f2.id, f1.id).unwrap();
829 let updated = FeatureStore::get_by_id(&conn, f2.id).unwrap().unwrap();
830 assert!(updated.dependencies.is_empty());
831
832 FeatureStore::set_dependencies(&conn, f2.id, &[f1.id]).unwrap();
834 let updated = FeatureStore::get_by_id(&conn, f2.id).unwrap().unwrap();
835 assert_eq!(updated.dependencies, vec![f1.id]);
836 }
837
838 #[test]
839 fn test_get_ready_and_blocked() {
840 let db = Database::open_in_memory().unwrap();
841 let conn = db.writer().lock().unwrap();
842
843 let f1 = FeatureStore::create(
844 &conn,
845 &CreateFeatureInput {
846 name: "F1".to_string(),
847 description: "".to_string(),
848 priority: Some(1),
849 category: "".to_string(),
850 steps: vec![],
851 dependencies: vec![],
852 },
853 )
854 .unwrap();
855
856 let f2 = FeatureStore::create(
857 &conn,
858 &CreateFeatureInput {
859 name: "F2".to_string(),
860 description: "".to_string(),
861 priority: Some(2),
862 category: "".to_string(),
863 steps: vec![],
864 dependencies: vec![DependencyRef::Id(f1.id)],
865 },
866 )
867 .unwrap();
868
869 let ready = FeatureStore::get_ready(&conn).unwrap();
871 assert_eq!(ready.len(), 1);
872 assert_eq!(ready[0].id, f1.id);
873
874 let blocked = FeatureStore::get_blocked(&conn).unwrap();
875 assert_eq!(blocked.len(), 1);
876 assert_eq!(blocked[0].id, f2.id);
877
878 FeatureStore::mark_passing(&conn, f1.id).unwrap();
880
881 let ready = FeatureStore::get_ready(&conn).unwrap();
883 assert_eq!(ready.len(), 1);
884 assert_eq!(ready[0].id, f2.id);
885
886 let blocked = FeatureStore::get_blocked(&conn).unwrap();
887 assert_eq!(blocked.len(), 0);
888 }
889
890 #[test]
891 fn test_get_stats() {
892 let db = Database::open_in_memory().unwrap();
893 let conn = db.writer().lock().unwrap();
894
895 let f1 = FeatureStore::create(
896 &conn,
897 &CreateFeatureInput {
898 name: "F1".to_string(),
899 description: "".to_string(),
900 priority: Some(1),
901 category: "".to_string(),
902 steps: vec![],
903 dependencies: vec![],
904 },
905 )
906 .unwrap();
907
908 let f2 = FeatureStore::create(
909 &conn,
910 &CreateFeatureInput {
911 name: "F2".to_string(),
912 description: "".to_string(),
913 priority: Some(2),
914 category: "".to_string(),
915 steps: vec![],
916 dependencies: vec![],
917 },
918 )
919 .unwrap();
920
921 FeatureStore::mark_passing(&conn, f1.id).unwrap();
922 FeatureStore::mark_in_progress(&conn, f2.id).unwrap();
923
924 let stats = FeatureStore::get_stats(&conn).unwrap();
925 assert_eq!(stats.total, 2);
926 assert_eq!(stats.passing, 1);
927 assert_eq!(stats.in_progress, 1);
928 assert_eq!(stats.failing, 0);
929 }
930
931 #[test]
932 fn test_get_graph() {
933 let db = Database::open_in_memory().unwrap();
934 let conn = db.writer().lock().unwrap();
935
936 let f1 = FeatureStore::create(
937 &conn,
938 &CreateFeatureInput {
939 name: "F1".to_string(),
940 description: "".to_string(),
941 priority: Some(1),
942 category: "Cat1".to_string(),
943 steps: vec![],
944 dependencies: vec![],
945 },
946 )
947 .unwrap();
948
949 let f2 = FeatureStore::create(
950 &conn,
951 &CreateFeatureInput {
952 name: "F2".to_string(),
953 description: "".to_string(),
954 priority: Some(2),
955 category: "Cat2".to_string(),
956 steps: vec![],
957 dependencies: vec![DependencyRef::Id(f1.id)],
958 },
959 )
960 .unwrap();
961
962 let graph = FeatureStore::get_graph(&conn).unwrap();
963 assert_eq!(graph.len(), 2);
964
965 let f1_node = graph.iter().find(|n| n.id == f1.id).unwrap();
967 assert_eq!(f1_node.dependents, vec![f2.id]);
968
969 let f2_node = graph.iter().find(|n| n.id == f2.id).unwrap();
971 assert!(f2_node.blocked);
972 assert_eq!(f2_node.dependencies, vec![f1.id]);
973 }
974}