Decompose create from positioning
create and add_to_backlog now just append. The CLI composes
create + move within a transaction when --before/--after is given.

Assisted-by: Claude Opus 4.6 via pi
change sumuwwxrytwxztqszwmlppsulxrtrkqq
commit 132686124bc7f15efa9f3c4db88641a97e11ddf7
author Alpha Chen <alpha@kejadlen.dev>
date
parent upxorlxp
diff --git a/src/bin/ranger/commands/task.rs b/src/bin/ranger/commands/task.rs
index f333300..00a8943 100644
--- a/src/bin/ranger/commands/task.rs
+++ b/src/bin/ranger/commands/task.rs
@@ -119,8 +119,6 @@ pub enum TaskCommands {
 }
 
 pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result<()> {
-    let mut conn = pool.acquire().await?;
-
     match command {
         TaskCommands::Create {
             title,
@@ -131,43 +129,46 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
             tag,
             position,
         } => {
-            let bl = ops::backlog::get_by_name(&mut conn, &backlog).await?;
+            let mut tx = pool.begin().await?;
+
+            let bl = ops::backlog::get_by_name(&mut tx, &backlog).await?;
             let parent_id = if let Some(parent_key) = &parent {
-                Some(
-                    ops::task::get_by_key_prefix(&mut conn, parent_key)
-                        .await?
-                        .id,
-                )
+                Some(ops::task::get_by_key_prefix(&mut tx, parent_key).await?.id)
             } else {
                 None
             };
-            let (before_id, after_id) = position.resolve(&mut conn).await?;
+            let (before_id, after_id) = position.resolve(&mut tx).await?;
             let state = state.map(|s| s.parse::<State>()).transpose()?;
 
             let task = ops::task::create(
-                &mut conn,
+                &mut tx,
                 ops::task::CreateTask {
                     title: &title,
                     backlog_id: bl.id,
                     state,
                     parent_id,
                     description: description.as_deref(),
-                    before_task_id: before_id,
-                    after_task_id: after_id,
                 },
             )
             .await?;
 
+            if before_id.is_some() || after_id.is_some() {
+                ops::task::move_task(&mut tx, task.id, bl.id, before_id, after_id).await?;
+            }
+
             if let Some(tags) = &tag {
                 for tag_name in tags.split(',').map(str::trim) {
-                    let t = ops::tag::get_or_create(&mut conn, tag_name).await?;
-                    ops::tag::add_to_task(&mut conn, task.id, t.id).await?;
+                    let t = ops::tag::get_or_create(&mut tx, tag_name).await?;
+                    ops::tag::add_to_task(&mut tx, task.id, t.id).await?;
                 }
             }
 
+            tx.commit().await?;
+
             output::print(&task, json, print_task);
         }
         TaskCommands::List { backlog, state } => {
+            let mut conn = pool.acquire().await?;
             let state = state.map(|s| s.parse::<State>()).transpose()?;
 
             if let Some(backlog_key) = &backlog {
@@ -190,6 +191,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
             }
         }
         TaskCommands::Show { key } => {
+            let mut conn = pool.acquire().await?;
             let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
             let comments = ops::comment::list(&mut conn, task.id).await?;
             let tags = ops::tag::list_for_task(&mut conn, task.id).await?;
@@ -233,6 +235,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
             description,
             state,
         } => {
+            let mut conn = pool.acquire().await?;
             let state = state.map(|s| s.parse::<State>()).transpose()?;
 
             let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
@@ -251,6 +254,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
             backlog,
             position,
         } => {
+            let mut conn = pool.acquire().await?;
             let bl = ops::backlog::get_by_name(&mut conn, &backlog).await?;
             let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
             let (before_id, after_id) = position.resolve(&mut conn).await?;
@@ -259,18 +263,21 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
             println!("Moved {} {}", &task.key[..8], task.title);
         }
         TaskCommands::Add { task, backlog } => {
+            let mut conn = pool.acquire().await?;
             let t = ops::task::get_by_key_prefix(&mut conn, &task).await?;
             let bl = ops::backlog::get_by_name(&mut conn, &backlog).await?;
             ops::task::add_to_backlog(&mut conn, t.id, bl.id).await?;
             println!("Added {} to {}", &t.key[..8], bl.name);
         }
         TaskCommands::Remove { task, backlog } => {
+            let mut conn = pool.acquire().await?;
             let t = ops::task::get_by_key_prefix(&mut conn, &task).await?;
             let bl = ops::backlog::get_by_name(&mut conn, &backlog).await?;
             ops::task::remove_from_backlog(&mut conn, t.id, bl.id).await?;
             println!("Removed {} from {}", &t.key[..8], bl.name);
         }
         TaskCommands::Delete { key } => {
+            let mut conn = pool.acquire().await?;
             let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
             ops::task::delete(&mut conn, task.id).await?;
             println!("Deleted {} {}", &task.key[..8], task.title);
diff --git a/src/ops/blocker.rs b/src/ops/blocker.rs
index 5446cc6..e05bc09 100644
--- a/src/ops/blocker.rs
+++ b/src/ops/blocker.rs
@@ -70,8 +70,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -84,8 +82,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -111,8 +107,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -125,8 +119,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
diff --git a/src/ops/comment.rs b/src/ops/comment.rs
index fe3f966..3f9285c 100644
--- a/src/ops/comment.rs
+++ b/src/ops/comment.rs
@@ -54,8 +54,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
diff --git a/src/ops/tag.rs b/src/ops/tag.rs
index e09d842..9c74076 100644
--- a/src/ops/tag.rs
+++ b/src/ops/tag.rs
@@ -100,8 +100,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -128,8 +126,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
diff --git a/src/ops/task.rs b/src/ops/task.rs
index 1ddd695..69d1394 100644
--- a/src/ops/task.rs
+++ b/src/ops/task.rs
@@ -10,8 +10,6 @@ pub struct CreateTask<'a> {
     pub state: Option<State>,
     pub parent_id: Option<i64>,
     pub description: Option<&'a str>,
-    pub before_task_id: Option<i64>,
-    pub after_task_id: Option<i64>,
 }
 
 pub async fn create(
@@ -34,15 +32,17 @@ pub async fn create(
     .fetch_one(&mut *conn)
     .await?;
 
-    let new_pos = resolve_position(
-        &mut *conn,
-        params.backlog_id,
-        task.id,
-        params.before_task_id,
-        params.after_task_id,
+    let last_pos: Option<String> = sqlx::query_scalar(
+        "SELECT position FROM backlog_tasks \
+         WHERE backlog_id = ? \
+         ORDER BY position DESC LIMIT 1",
     )
+    .bind(params.backlog_id)
+    .fetch_optional(&mut *conn)
     .await?;
 
+    let new_pos = position::midpoint(last_pos.as_deref(), None);
+
     sqlx::query("INSERT INTO backlog_tasks (backlog_id, task_id, position) VALUES (?, ?, ?)")
         .bind(params.backlog_id)
         .bind(task.id)
@@ -278,20 +278,12 @@ pub async fn add_to_backlog(
     task_id: i64,
     backlog_id: i64,
 ) -> Result<(), RangerError> {
-    // Get the task's state to find proper position
-    let state: String = sqlx::query_scalar("SELECT state FROM tasks WHERE id = ?")
-        .bind(task_id)
-        .fetch_one(&mut *conn)
-        .await?;
-
     let last_pos: Option<String> = sqlx::query_scalar(
-        "SELECT bt.position FROM backlog_tasks bt \
-         JOIN tasks t ON t.id = bt.task_id \
-         WHERE bt.backlog_id = ? AND t.state = ? \
-         ORDER BY bt.position DESC LIMIT 1",
+        "SELECT position FROM backlog_tasks \
+         WHERE backlog_id = ? \
+         ORDER BY position DESC LIMIT 1",
     )
     .bind(backlog_id)
-    .bind(&state)
     .fetch_optional(&mut *conn)
     .await?;
 
@@ -355,8 +347,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -391,8 +381,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -405,8 +393,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -419,8 +405,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -446,8 +430,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -460,8 +442,6 @@ mod tests {
                 state: Some(State::Queued),
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -489,8 +469,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -513,8 +491,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -549,8 +525,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -578,8 +552,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -606,8 +578,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -620,8 +590,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -634,8 +602,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -665,8 +631,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -679,8 +643,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -693,8 +655,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -724,8 +684,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -738,8 +696,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -752,8 +708,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -783,8 +737,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -797,8 +749,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -811,8 +761,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -842,8 +790,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -856,8 +802,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -870,8 +814,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -901,8 +843,6 @@ mod tests {
                 state: None,
                 parent_id: None,
                 description: None,
-                before_task_id: None,
-                after_task_id: None,
             },
         )
         .await
@@ -917,172 +857,4 @@ mod tests {
         let tasks = list(&mut conn, bl.id, None).await.unwrap();
         assert_eq!(tasks.len(), 0);
     }
-
-    #[tokio::test]
-    async fn create_task_before() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "First",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: None,
-                after_task_id: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
-            &mut conn,
-            CreateTask {
-                title: "Second",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: None,
-                after_task_id: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        // Create a task before t1 — should produce order: t3, t1, t2
-        let t3 = create(
-            &mut conn,
-            CreateTask {
-                title: "Before first",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: Some(t1.id),
-                after_task_id: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        let tasks = list(&mut conn, bl.id, None).await.unwrap();
-        assert_eq!(tasks[0].id, t3.id, "t3 should be first");
-        assert_eq!(tasks[1].id, t1.id, "t1 should be second");
-        assert_eq!(tasks[2].id, t2.id, "t2 should be third");
-    }
-
-    #[tokio::test]
-    async fn create_task_after() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "First",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: None,
-                after_task_id: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
-            &mut conn,
-            CreateTask {
-                title: "Second",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: None,
-                after_task_id: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        // Create a task after t1 — should produce order: t1, t3, t2
-        let t3 = create(
-            &mut conn,
-            CreateTask {
-                title: "After first",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: None,
-                after_task_id: Some(t1.id),
-            },
-        )
-        .await
-        .unwrap();
-
-        let tasks = list(&mut conn, bl.id, None).await.unwrap();
-        assert_eq!(tasks[0].id, t1.id, "t1 should be first");
-        assert_eq!(tasks[1].id, t3.id, "t3 should be second");
-        assert_eq!(tasks[2].id, t2.id, "t2 should be third");
-    }
-
-    #[tokio::test]
-    async fn create_task_between() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "First",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: None,
-                after_task_id: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
-            &mut conn,
-            CreateTask {
-                title: "Second",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: None,
-                after_task_id: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        // Create a task after t1 and before t2 — should produce order: t1, t3, t2
-        let t3 = create(
-            &mut conn,
-            CreateTask {
-                title: "Between",
-                backlog_id: bl.id,
-                state: None,
-                parent_id: None,
-                description: None,
-                before_task_id: Some(t2.id),
-                after_task_id: Some(t1.id),
-            },
-        )
-        .await
-        .unwrap();
-
-        let tasks = list(&mut conn, bl.id, None).await.unwrap();
-        assert_eq!(tasks[0].id, t1.id, "t1 should be first");
-        assert_eq!(tasks[1].id, t3.id, "t3 should be second");
-        assert_eq!(tasks[2].id, t2.id, "t2 should be third");
-    }
 }