turn_stats: create the session_id index after the column exists
This commit is contained in:
parent
d090df3c36
commit
3af3c3a613
1 changed files with 102 additions and 2 deletions
|
|
@ -51,8 +51,11 @@ CREATE TABLE IF NOT EXISTS turn_stats (
|
|||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_turn_stats_started
|
||||
ON turn_stats (started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_turn_stats_session
|
||||
ON turn_stats (session_id);
|
||||
-- NOTE: the index on session_id is created in MIGRATIONS, not here. On an
|
||||
-- existing pre-session db the `CREATE TABLE IF NOT EXISTS` above is a no-op
|
||||
-- (the old table has no session_id column), so indexing session_id in this
|
||||
-- batch would fail with `no such column` and abort the whole SCHEMA apply —
|
||||
-- which disables the stats sink. The column is added by MIGRATIONS first.
|
||||
-- One row per fresh claude session (minted when --continue is suppressed).
|
||||
-- turn_stats.session_id FKs here so per-session stats (first-turn tokens,
|
||||
-- per-session totals, turn count, duration) are one GROUP BY away.
|
||||
|
|
@ -75,6 +78,9 @@ const MIGRATIONS: &[&str] = &[
|
|||
// Nullable FK to sessions.id — no default; pre-migration rows stay NULL
|
||||
// (the surface treats NULL as "no session", inert until capture lands).
|
||||
"ALTER TABLE turn_stats ADD COLUMN session_id INTEGER",
|
||||
// Index on session_id — must run AFTER the column is added, so it lives
|
||||
// here rather than in SCHEMA (see the note there). Idempotent.
|
||||
"CREATE INDEX IF NOT EXISTS idx_turn_stats_session ON turn_stats (session_id)",
|
||||
];
|
||||
|
||||
/// One row to be inserted. `Option`-wrapped fields default to NULL
|
||||
|
|
@ -306,3 +312,97 @@ impl TurnStats {
|
|||
fn default_path() -> PathBuf {
|
||||
crate::paths::harness_dir().join("hyperhive-turn-stats.sqlite")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A `turn_stats` db in the *pre-session* shape: the original table with no
|
||||
/// `session_id` column, plus the `started_at` index. This is what every
|
||||
/// agent created before the sessions feature has on disk.
|
||||
fn seed_pre_session_db(path: &Path) {
|
||||
let conn = Connection::open(path).unwrap();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE turn_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
started_at INTEGER NOT NULL,
|
||||
ended_at INTEGER NOT NULL,
|
||||
duration_ms INTEGER NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
wake_from TEXT NOT NULL,
|
||||
input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
last_input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
last_output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
last_cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
last_cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
tool_call_count INTEGER NOT NULL DEFAULT 0,
|
||||
tool_call_breakdown_json TEXT,
|
||||
open_threads_count INTEGER,
|
||||
open_reminders_count INTEGER,
|
||||
result_kind TEXT NOT NULL,
|
||||
note TEXT
|
||||
);
|
||||
CREATE INDEX idx_turn_stats_started ON turn_stats (started_at DESC);",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn sample_row() -> TurnStatRow {
|
||||
TurnStatRow {
|
||||
started_at: 100,
|
||||
ended_at: 101,
|
||||
duration_ms: 1_000,
|
||||
model: "opus".to_owned(),
|
||||
wake_from: "recv".to_owned(),
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
last_input_tokens: 10,
|
||||
last_output_tokens: 5,
|
||||
last_cache_read_input_tokens: 0,
|
||||
last_cache_creation_input_tokens: 0,
|
||||
tool_call_count: 1,
|
||||
tool_call_breakdown_json: None,
|
||||
open_threads_count: None,
|
||||
open_reminders_count: None,
|
||||
result_kind: "ok",
|
||||
note: None,
|
||||
session_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression: a pre-session db must `open()` cleanly (the `session_id`
|
||||
/// index used to live in `SCHEMA` and aborted the apply with
|
||||
/// `no such column`, silently disabling the stats sink), get the column
|
||||
/// added by `MIGRATIONS`, and then accept writes.
|
||||
#[test]
|
||||
fn open_upgrades_pre_session_db_and_writes() {
|
||||
let path = std::env::temp_dir().join("hyperhive-turnstats-pre-session-regression.sqlite");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
seed_pre_session_db(&path);
|
||||
|
||||
let stats = TurnStats::open(&path).expect("open() must succeed on a pre-session db");
|
||||
stats.record(&sample_row());
|
||||
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
let rows: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM turn_stats", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(rows, 1, "the row must be written once the column is added");
|
||||
let has_session_id: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('turn_stats') \
|
||||
WHERE name = 'session_id'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(has_session_id, 1, "MIGRATIONS must add session_id");
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue