feat(gamification): migration v13 — auto_completed column
Adds a flag on tasks to distinguish manual completions from the cascade auto-completion performed by complete_subtask_and_check_parent. Partial index on (done_at, auto_completed) supports the Phase 8 daily-count query without bloating the tasks index footprint. Forward-only: pre-migration completed rows default to 0 (they count). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -430,6 +430,27 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[
|
|||||||
ON implementation_rules(enabled, trigger_kind);
|
ON implementation_rules(enabled, trigger_kind);
|
||||||
"#,
|
"#,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
13,
|
||||||
|
"gamification: auto_completed flag for cascade-completed parents",
|
||||||
|
r#"
|
||||||
|
-- Phase 8 of the feature-complete roadmap. Parents that close via
|
||||||
|
-- the complete_subtask_and_check_parent cascade must not count
|
||||||
|
-- towards daily completion totals. The user already got credit
|
||||||
|
-- for ticking the subtask. This column distinguishes manual
|
||||||
|
-- completions (0) from cascade completions (1). The daily-count
|
||||||
|
-- query then excludes auto_completed = 1.
|
||||||
|
--
|
||||||
|
-- Partial index keeps the index small: only completed rows occupy
|
||||||
|
-- it, since uncompleted rows have done_at IS NULL.
|
||||||
|
ALTER TABLE tasks ADD COLUMN auto_completed INTEGER NOT NULL DEFAULT 0
|
||||||
|
CHECK (auto_completed IN (0, 1));
|
||||||
|
|
||||||
|
CREATE INDEX idx_tasks_done_at_auto_completed
|
||||||
|
ON tasks(done_at, auto_completed)
|
||||||
|
WHERE done_at IS NOT NULL;
|
||||||
|
"#,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Split SQL into individual statements, respecting BEGIN...END trigger blocks.
|
/// Split SQL into individual statements, respecting BEGIN...END trigger blocks.
|
||||||
@@ -579,7 +600,7 @@ mod tests {
|
|||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count, 12);
|
assert_eq!(count, 13);
|
||||||
|
|
||||||
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
@@ -598,7 +619,7 @@ mod tests {
|
|||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count, 12);
|
assert_eq!(count, 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1054,4 +1075,41 @@ mod tests {
|
|||||||
"schema_version must not advance past the failed migration"
|
"schema_version must not advance past the failed migration"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn migration_v13_adds_auto_completed_column() {
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.expect("pool");
|
||||||
|
run_migrations(&pool).await.expect("migrate");
|
||||||
|
|
||||||
|
// Column exists.
|
||||||
|
let info = sqlx::query("PRAGMA table_info(tasks)")
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.expect("pragma");
|
||||||
|
let names: Vec<String> = info.iter().map(|r| r.get::<String, _>("name")).collect();
|
||||||
|
assert!(
|
||||||
|
names.iter().any(|n| n == "auto_completed"),
|
||||||
|
"expected auto_completed column, got {names:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Existing completed rows default to 0. Insert a pre-existing-looking
|
||||||
|
// task via raw SQL to simulate a row from before the migration.
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO tasks (id, text, bucket, done, done_at) \
|
||||||
|
VALUES ('t1', 'pre-existing', 'inbox', 1, '2026-04-20 12:00:00')",
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("insert");
|
||||||
|
|
||||||
|
let auto: i64 = sqlx::query_scalar("SELECT auto_completed FROM tasks WHERE id = 't1'")
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.expect("query");
|
||||||
|
assert_eq!(auto, 0, "pre-existing completed rows must default to 0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user