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);
|
||||
"#,
|
||||
),
|
||||
(
|
||||
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.
|
||||
@@ -579,7 +600,7 @@ mod tests {
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, 12);
|
||||
assert_eq!(count, 13);
|
||||
|
||||
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
||||
.execute(&pool)
|
||||
@@ -598,7 +619,7 @@ mod tests {
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, 12);
|
||||
assert_eq!(count, 13);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1054,4 +1075,41 @@ mod tests {
|
||||
"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