Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,27 +185,41 @@ const INDEX_LEARNING_TAGS_TAG: &str =
"CREATE INDEX IF NOT EXISTS idx_learning_tags_tag ON learning_tags(tag);";

const CREATE_TASK_READINESS_VIEW: &str = r#"
CREATE VIEW IF NOT EXISTS task_readiness AS
DROP VIEW IF EXISTS task_readiness;
CREATE VIEW task_readiness AS
WITH RECURSIVE ancestor_chain(task_id, ancestor_id, depth) AS (
SELECT id, id, 0 FROM tasks
UNION ALL
SELECT ac.task_id, t.parent_task_id, ac.depth + 1
FROM ancestor_chain ac
JOIN tasks t ON t.id = ac.ancestor_id
WHERE t.parent_task_id IS NOT NULL
AND ac.depth < 32
),
effective_unmet AS (
SELECT
ac.task_id,
COUNT(CASE
WHEN d.kind IN ('blocks', 'feeds_into')
AND upstream.status NOT IN ('done', 'done_partial')
THEN 1
END) AS unmet_deps
FROM ancestor_chain ac
LEFT JOIN dependencies d ON d.to_task = ac.ancestor_id
LEFT JOIN tasks upstream ON upstream.id = d.from_task
GROUP BY ac.task_id
)
SELECT
t.id,
t.status,
COUNT(CASE
WHEN d.kind IN ('blocks', 'feeds_into')
AND upstream.status NOT IN ('done', 'done_partial')
THEN 1
END) AS unmet_deps,
COALESCE(eu.unmet_deps, 0) AS unmet_deps,
CASE
WHEN t.status = 'pending'
AND COUNT(CASE
WHEN d.kind IN ('blocks','feeds_into')
AND upstream.status NOT IN ('done','done_partial')
THEN 1 END) = 0
AND COALESCE(eu.unmet_deps, 0) = 0
THEN 1 ELSE 0
END AS promotable
FROM tasks t
LEFT JOIN dependencies d ON d.to_task = t.id
LEFT JOIN tasks upstream ON upstream.id = d.from_task
GROUP BY t.id;
LEFT JOIN effective_unmet eu ON eu.task_id = t.id;
"#;

pub fn init_db(path: &str) -> Result<Database> {
Expand Down
201 changes: 201 additions & 0 deletions src/db/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,207 @@ fn promote_sweep_moves_pending_to_ready() {
assert_eq!(updated.status, TaskStatus::Ready);
}

#[test]
fn test_child_of_composite_with_unmet_parent_deps_not_ready() {
let db_path = test_db_path();
let db = init_db(&db_path).unwrap();
let project = create_project(&db, "CompositeBlocked", None, None).unwrap();

let blocker = create_task(
&db,
&make_task(&project.id, "blocker", TaskStatus::Pending),
&[],
)
.unwrap();

let mut parent = make_task(&project.id, "parent", TaskStatus::Pending);
parent.is_composite = true;
let parent = create_task(&db, &parent, &[]).unwrap();

let mut child = make_task(&project.id, "child", TaskStatus::Pending);
child.parent_task_id = Some(parent.id.clone());
let child = create_task(&db, &child, &[]).unwrap();

add_dependency(
&db,
&blocker.id,
&parent.id,
DependencyKind::FeedsInto,
DependencyCondition::All,
None,
)
.unwrap();

promote_ready_tasks(&db).unwrap();

assert_eq!(
get_task(&db, &blocker.id).unwrap().status,
TaskStatus::Ready
);
assert_eq!(
get_task(&db, &parent.id).unwrap().status,
TaskStatus::Pending
);
assert_eq!(
get_task(&db, &child.id).unwrap().status,
TaskStatus::Pending
);
}

#[test]
fn test_child_promotes_when_parent_deps_satisfied() {
let db_path = test_db_path();
let db = init_db(&db_path).unwrap();
let project = create_project(&db, "CompositeUnblocked", None, None).unwrap();

let blocker = create_task(
&db,
&make_task(&project.id, "blocker", TaskStatus::Pending),
&[],
)
.unwrap();

let mut parent = make_task(&project.id, "parent", TaskStatus::Pending);
parent.is_composite = true;
let parent = create_task(&db, &parent, &[]).unwrap();

let mut child = make_task(&project.id, "child", TaskStatus::Pending);
child.parent_task_id = Some(parent.id.clone());
let child = create_task(&db, &child, &[]).unwrap();

add_dependency(
&db,
&blocker.id,
&parent.id,
DependencyKind::FeedsInto,
DependencyCondition::All,
None,
)
.unwrap();

promote_ready_tasks(&db).unwrap();
complete_task(&db, &blocker.id, None).unwrap();
promote_ready_tasks(&db).unwrap();

assert_eq!(get_task(&db, &parent.id).unwrap().status, TaskStatus::Ready);
assert_eq!(get_task(&db, &child.id).unwrap().status, TaskStatus::Ready);
}

#[test]
fn test_grandchild_inherits_through_two_levels() {
let db_path = test_db_path();
let db = init_db(&db_path).unwrap();
let project = create_project(&db, "CompositeGrandchild", None, None).unwrap();

let blocker = create_task(
&db,
&make_task(&project.id, "blocker", TaskStatus::Pending),
&[],
)
.unwrap();

let mut parent = make_task(&project.id, "parent", TaskStatus::Pending);
parent.is_composite = true;
let parent = create_task(&db, &parent, &[]).unwrap();

let mut child = make_task(&project.id, "child", TaskStatus::Pending);
child.parent_task_id = Some(parent.id.clone());
child.is_composite = true;
let child = create_task(&db, &child, &[]).unwrap();

let mut grandchild = make_task(&project.id, "grandchild", TaskStatus::Pending);
grandchild.parent_task_id = Some(child.id.clone());
let grandchild = create_task(&db, &grandchild, &[]).unwrap();

add_dependency(
&db,
&blocker.id,
&parent.id,
DependencyKind::Blocks,
DependencyCondition::All,
None,
)
.unwrap();

promote_ready_tasks(&db).unwrap();

assert_eq!(
get_task(&db, &blocker.id).unwrap().status,
TaskStatus::Ready
);
assert_eq!(
get_task(&db, &parent.id).unwrap().status,
TaskStatus::Pending
);
assert_eq!(
get_task(&db, &child.id).unwrap().status,
TaskStatus::Pending
);
assert_eq!(
get_task(&db, &grandchild.id).unwrap().status,
TaskStatus::Pending
);

complete_task(&db, &blocker.id, None).unwrap();
promote_ready_tasks(&db).unwrap();

assert_eq!(get_task(&db, &parent.id).unwrap().status, TaskStatus::Ready);
assert_eq!(get_task(&db, &child.id).unwrap().status, TaskStatus::Ready);
assert_eq!(
get_task(&db, &grandchild.id).unwrap().status,
TaskStatus::Ready
);
}

#[test]
fn test_no_parent_existing_behavior_preserved() {
let db_path = test_db_path();
let db = init_db(&db_path).unwrap();
let project = create_project(&db, "FlatDeps", None, None).unwrap();

let upstream = create_task(
&db,
&make_task(&project.id, "upstream", TaskStatus::Pending),
&[],
)
.unwrap();
let downstream = create_task(
&db,
&make_task(&project.id, "downstream", TaskStatus::Pending),
&[],
)
.unwrap();

add_dependency(
&db,
&upstream.id,
&downstream.id,
DependencyKind::FeedsInto,
DependencyCondition::All,
None,
)
.unwrap();

promote_ready_tasks(&db).unwrap();

assert_eq!(
get_task(&db, &upstream.id).unwrap().status,
TaskStatus::Ready
);
assert_eq!(
get_task(&db, &downstream.id).unwrap().status,
TaskStatus::Pending
);

complete_task(&db, &upstream.id, None).unwrap();
promote_ready_tasks(&db).unwrap();

assert_eq!(
get_task(&db, &downstream.id).unwrap().status,
TaskStatus::Ready
);
}

#[test]
fn sweeper_reclaims_retries_and_rolls_up_composites() {
let db_path = test_db_path();
Expand Down
Loading