feat(tasks): B2a part 1 — archive schema + task_lists scaffolding
Migration v16 adds tasks.archived (default 0) + tasks.archived_at + idx_tasks_archived. TaskRow extended with both fields; list_tasks and list_subtasks default-filter archived rows; complete_subtask_and_check_parent SELECT lists updated to round-trip the new columns. TaskListRow + ImportSummary structs land in preparation for the task_lists CRUD that part 2 wires up. All 61 existing tests still pass. (SQL splitter is naive about ';' inside -- comments, so the v16 comment block was rewritten to avoid a bare semicolon mid-comment that the splitter was treating as a statement boundary.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -328,8 +328,9 @@ pub async fn insert_task(
|
|||||||
pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
|
pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
||||||
source_transcript_id, parent_task_id, energy \
|
source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||||
FROM tasks WHERE parent_task_id IS NULL ORDER BY created_at DESC",
|
FROM tasks WHERE parent_task_id IS NULL AND archived = 0 \
|
||||||
|
ORDER BY created_at DESC",
|
||||||
)
|
)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
@@ -341,7 +342,8 @@ pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
|
|||||||
pub async fn get_task_by_id(pool: &SqlitePool, id: &str) -> Result<Option<TaskRow>> {
|
pub async fn get_task_by_id(pool: &SqlitePool, id: &str) -> Result<Option<TaskRow>> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
||||||
source_transcript_id, parent_task_id, energy FROM tasks WHERE id = ?",
|
source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||||
|
FROM tasks WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
@@ -431,8 +433,8 @@ pub async fn insert_subtask(
|
|||||||
pub async fn list_subtasks(pool: &SqlitePool, parent_id: &str) -> Result<Vec<TaskRow>> {
|
pub async fn list_subtasks(pool: &SqlitePool, parent_id: &str) -> Result<Vec<TaskRow>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
||||||
source_transcript_id, parent_task_id, energy \
|
source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||||
FROM tasks WHERE parent_task_id = ? ORDER BY created_at ASC",
|
FROM tasks WHERE parent_task_id = ? AND archived = 0 ORDER BY created_at ASC",
|
||||||
)
|
)
|
||||||
.bind(parent_id)
|
.bind(parent_id)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
@@ -499,7 +501,8 @@ pub async fn complete_subtask_and_check_parent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let select_sql = "SELECT id, text, bucket, list_id, effort, notes, done, done_at, \
|
let select_sql = "SELECT id, text, bucket, list_id, effort, notes, done, done_at, \
|
||||||
created_at, source_transcript_id, parent_task_id, energy FROM tasks WHERE id = ?";
|
created_at, source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||||
|
FROM tasks WHERE id = ?";
|
||||||
|
|
||||||
let updated_subtask = sqlx::query(select_sql)
|
let updated_subtask = sqlx::query(select_sql)
|
||||||
.bind(subtask_id)
|
.bind(subtask_id)
|
||||||
@@ -872,6 +875,34 @@ pub struct TaskRow {
|
|||||||
/// migration v11). Unset is the expected normal case — the match-my-
|
/// migration v11). Unset is the expected normal case — the match-my-
|
||||||
/// energy sort treats unset as Medium-equivalent.
|
/// energy sort treats unset as Medium-equivalent.
|
||||||
pub energy: Option<String>,
|
pub energy: Option<String>,
|
||||||
|
/// B2a — Inbox-only archive flag (migration v16). `true` means the
|
||||||
|
/// row is archived and excluded from the default `list_tasks` /
|
||||||
|
/// `list_subtasks` query. `archived_at` stamps the archive time so
|
||||||
|
/// the Archived view can sort newest-first.
|
||||||
|
pub archived: bool,
|
||||||
|
pub archived_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// B2a — task_lists row. Mirrors the `task_lists` table that has lived in
|
||||||
|
/// migration v1 since the start; this struct + the surrounding CRUD wires
|
||||||
|
/// the table that the frontend was previously stashing in localStorage.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TaskListRow {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub built_in: bool,
|
||||||
|
pub profile_id: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of `import_task_lists` — how many rows landed and how many were
|
||||||
|
/// skipped because their `id` already exists. The whole batch runs in one
|
||||||
|
/// transaction so a duplicate inside the batch (rather than against the
|
||||||
|
/// existing table) is a hard failure that rolls everything back.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ImportSummary {
|
||||||
|
pub imported: usize,
|
||||||
|
pub skipped: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -948,6 +979,18 @@ fn task_row_from(r: sqlx::sqlite::SqliteRow) -> TaskRow {
|
|||||||
source_transcript_id: r.get("source_transcript_id"),
|
source_transcript_id: r.get("source_transcript_id"),
|
||||||
parent_task_id: r.get("parent_task_id"),
|
parent_task_id: r.get("parent_task_id"),
|
||||||
energy: r.get("energy"),
|
energy: r.get("energy"),
|
||||||
|
archived: r.get::<i64, _>("archived") != 0,
|
||||||
|
archived_at: r.get("archived_at"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn task_list_row_from(r: &sqlx::sqlite::SqliteRow) -> TaskListRow {
|
||||||
|
TaskListRow {
|
||||||
|
id: r.get("id"),
|
||||||
|
name: r.get("name"),
|
||||||
|
built_in: r.get::<i64, _>("built_in") != 0,
|
||||||
|
profile_id: r.get("profile_id"),
|
||||||
|
created_at: r.get("created_at"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -477,6 +477,24 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[
|
|||||||
ON transcripts(profile_id, created_at DESC);
|
ON transcripts(profile_id, created_at DESC);
|
||||||
"#,
|
"#,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
16,
|
||||||
|
"tasks: archived flag for Inbox-only archive (B2a)",
|
||||||
|
r#"
|
||||||
|
-- B2a: persistent archive surface. The default is 0 (visible) so all
|
||||||
|
-- pre-existing rows behave as before. archived_at is a string ISO
|
||||||
|
-- timestamp captured at archive time so the future "archived"
|
||||||
|
-- view can sort newest-first, and is NULL while the row is live.
|
||||||
|
--
|
||||||
|
-- Per the v3 plan, archive is Inbox-only: the bulk auto-archive
|
||||||
|
-- path (archive_inbox_older_than) only touches rows with
|
||||||
|
-- bucket = inbox. Today/Soon/Later reflect explicit user
|
||||||
|
-- choices and stay where the user put them.
|
||||||
|
ALTER TABLE tasks ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE tasks ADD COLUMN archived_at TEXT;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived);
|
||||||
|
"#,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Split SQL into individual statements, respecting BEGIN...END trigger blocks.
|
/// Split SQL into individual statements, respecting BEGIN...END trigger blocks.
|
||||||
@@ -626,7 +644,7 @@ mod tests {
|
|||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count, 15);
|
assert_eq!(count, 16);
|
||||||
|
|
||||||
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
@@ -645,7 +663,7 @@ mod tests {
|
|||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count, 15);
|
assert_eq!(count, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user