use rusqlite::Connection;
use tracing::debug;
use crate::core::errors::{Result, TgaError};
use super::column_names;
pub(super) fn apply(conn: &Connection) -> Result<()> {
let commits_cols = column_names(conn, "commits")?;
if !commits_cols.iter().any(|c| c == "agentic_mode") {
conn.execute_batch(
"ALTER TABLE commits ADD COLUMN agentic_mode TEXT NOT NULL DEFAULT 'none';",
)
.map_err(|e| {
TgaError::MigrationError(format!(
"migration 21 (agentic_mode) commits ALTER failed: {e}"
))
})?;
} else {
debug!(
"migration v21: agentic_mode already present in commits \
(pre-release build detected), skipping ADD COLUMN"
);
}
conn.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_commits_agentic_mode ON commits(agentic_mode);",
)
.map_err(|e| {
TgaError::MigrationError(format!(
"migration 21 (agentic_mode) commits index failed: {e}"
))
})?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS fact_weekly_engineer ( \
author_email TEXT NOT NULL, \
iso_year INTEGER NOT NULL, \
iso_week INTEGER NOT NULL, \
repository TEXT NOT NULL, \
net_commits INTEGER NOT NULL DEFAULT 0, \
agentic_count INTEGER NOT NULL DEFAULT 0, \
ide_assisted_count INTEGER NOT NULL DEFAULT 0, \
agentic_pct REAL NOT NULL DEFAULT 0.0, \
formula_version TEXT NOT NULL DEFAULT 'v1', \
computed_at INTEGER NOT NULL DEFAULT 0, \
PRIMARY KEY (author_email, iso_year, iso_week, repository) \
); \
CREATE INDEX IF NOT EXISTS idx_fwe_week ON fact_weekly_engineer (iso_year, iso_week); \
CREATE INDEX IF NOT EXISTS idx_fwe_author ON fact_weekly_engineer (author_email); \
CREATE INDEX IF NOT EXISTS idx_fwe_repo ON fact_weekly_engineer (repository);",
)
.map_err(|e| {
TgaError::MigrationError(format!(
"migration 21 (agentic_mode) fact_weekly_engineer CREATE TABLE failed: {e}"
))
})?;
Ok(())
}
#[cfg(test)]
pub(super) mod tests {
use crate::core::db::Database;
use rusqlite::params;
#[test]
pub(crate) fn migration_v21_adds_agentic_mode_and_fwe() {
let db = Database::open_in_memory().expect("open db");
let conn = db.connection();
conn.execute(
"INSERT INTO commits \
(sha, author_name, author_email, timestamp, message, repository, \
is_ai_assisted, ai_tool, agentic_mode) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![
"sha_v21_agentic",
"Alice",
"alice@example.com",
"2026-01-01T00:00:00Z",
"feat: full-agentic\n\nCo-Authored-By: Claude Opus <noreply@anthropic.com>",
"testrepo",
1_i64,
"claude",
"full_agentic",
],
)
.expect("insert full_agentic commit");
let mode: String = conn
.query_row(
"SELECT agentic_mode FROM commits WHERE sha = 'sha_v21_agentic'",
[],
|r| r.get(0),
)
.expect("read agentic_mode");
assert_eq!(mode, "full_agentic");
conn.execute(
"INSERT INTO commits \
(sha, author_name, author_email, timestamp, message, repository) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
"sha_v21_none",
"Bob",
"bob@example.com",
"2026-01-02T00:00:00Z",
"chore: plain human commit",
"testrepo",
],
)
.expect("insert plain commit");
let default_mode: String = conn
.query_row(
"SELECT agentic_mode FROM commits WHERE sha = 'sha_v21_none'",
[],
|r| r.get(0),
)
.expect("read default agentic_mode");
assert_eq!(
default_mode, "none",
"agentic_mode must default to 'none' for pre-migration rows"
);
conn.execute(
"INSERT OR REPLACE INTO fact_weekly_engineer \
(author_email, iso_year, iso_week, repository, \
net_commits, agentic_count, ide_assisted_count, agentic_pct, \
formula_version, computed_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
params![
"alice@example.com",
2026_i64,
3_i64,
"testrepo",
10_i64, 7_i64, 1_i64, 70.0_f64, "v1",
1_000_000_i64,
],
)
.expect("insert fwe row");
let (net, agentic, ide, pct): (i64, i64, i64, f64) = conn
.query_row(
"SELECT net_commits, agentic_count, ide_assisted_count, agentic_pct \
FROM fact_weekly_engineer \
WHERE author_email = 'alice@example.com' AND iso_year = 2026 AND iso_week = 3",
[],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
)
.expect("read fwe row");
assert_eq!(net, 10);
assert_eq!(agentic, 7);
assert_eq!(ide, 1);
assert!(
(pct - 70.0).abs() < 1e-9,
"agentic_pct must be 70.0, got {pct}"
);
conn.execute(
"INSERT OR REPLACE INTO fact_weekly_engineer \
(author_email, iso_year, iso_week, repository, \
net_commits, agentic_count, ide_assisted_count, agentic_pct, \
formula_version, computed_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
params![
"alice@example.com",
2026_i64,
3_i64,
"testrepo",
10_i64,
8_i64,
1_i64,
80.0_f64, "v1",
2_000_000_i64,
],
)
.expect("upsert fwe row");
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM fact_weekly_engineer \
WHERE author_email = 'alice@example.com' AND iso_year = 2026 AND iso_week = 3",
[],
|r| r.get(0),
)
.expect("count fwe rows");
assert_eq!(count, 1, "UPSERT must not duplicate the grain row");
let new_pct: f64 = conn
.query_row(
"SELECT agentic_pct FROM fact_weekly_engineer \
WHERE author_email = 'alice@example.com' AND iso_year = 2026 AND iso_week = 3",
[],
|r| r.get(0),
)
.expect("read new pct");
assert!(
(new_pct - 80.0).abs() < 1e-9,
"UPSERT must overwrite agentic_pct with 80.0, got {new_pct}"
);
}
#[test]
pub(crate) fn migration_v21_is_idempotent_when_agentic_mode_already_exists() {
use rusqlite::Connection;
let mut conn = Connection::open_in_memory().expect("open raw connection");
super::super::ensure_migrations_table(&conn).expect("ensure table");
for m in super::super::MIGRATIONS {
if m.version > 20 {
break;
}
let tx = conn.transaction().expect("begin tx");
if m.version == 17 {
super::super::v17::apply(&tx).expect("v17");
} else {
tx.execute_batch(m.sql)
.unwrap_or_else(|e| panic!("migration {} failed: {e}", m.version));
}
tx.execute(
"INSERT INTO schema_migrations(version, name, applied_at) VALUES (?1, ?2, ?3)",
rusqlite::params![m.version, m.name, "2026-01-01T00:00:00Z"],
)
.expect("record migration");
tx.commit().expect("commit");
}
conn.execute_batch(
"ALTER TABLE commits ADD COLUMN agentic_mode TEXT NOT NULL DEFAULT 'none';",
)
.expect("pre-release ALTER TABLE (simulating old dev build)");
super::super::run(&mut conn)
.expect("migration v21 must succeed even when agentic_mode already exists");
conn.execute(
"INSERT INTO commits \
(sha, author_name, author_email, timestamp, message, repository, agentic_mode) \
VALUES ('sha_idem21', 'Test', 't@e.com', '2026-01-01T00:00:00Z', 'msg', 'repo', \
'ide_assisted')",
[],
)
.expect("insert with agentic_mode post-migration");
let mode: String = conn
.query_row(
"SELECT agentic_mode FROM commits WHERE sha = 'sha_idem21'",
[],
|r| r.get(0),
)
.expect("read agentic_mode");
assert_eq!(mode, "ide_assisted");
}
}