···1919 h
2020}
21212222+// When SCHEMA changes: (1) append a new hash to SCHEMA_HASHES, (2) add a migration to MIGRATIONS.
2223const SCHEMA: &str = "
2324CREATE TABLE IF NOT EXISTS events (
2425 id INTEGER PRIMARY KEY AUTOINCREMENT,
···2829 text TEXT,
2930 drv_path TEXT,
3031 cache_url TEXT,
3131- start_time TEXT,
3232- end_time TEXT,
3232+ start_time INTEGER,
3333+ end_time INTEGER,
3334 duration_ms INTEGER,
3435 total_bytes INTEGER
3536);
3737+CREATE INDEX IF NOT EXISTS idx_events_type_start ON events(event_type, start_time);
3638";
37393838-// Append a new hash entry when the schema changes - SCHEMA_VERSION auto-increments.
3940const SCHEMA_HASHES: &[u32] = &[
4040- 0x9bc94a70, // v1
4141+ 0x9bc94a70, // v1: TEXT timestamps, no indexes
4242+ 0xee061d32, // v2: INTEGER timestamps (Unix seconds), idx_events_type_start
4143];
4244const SCHEMA_VERSION: u32 = SCHEMA_HASHES.len() as u32;
4345const _: () = assert!(
4446 schema_hash(SCHEMA.as_bytes()) == SCHEMA_HASHES[SCHEMA_VERSION as usize - 1],
4545- "schema changed - append new hash to SCHEMA_HASHES"
4747+ "schema changed - append new hash to SCHEMA_HASHES and add a migration in MIGRATIONS"
4648);
4949+5050+// (target_version, sql). Table rebuild because SQLite does not support ALTER COLUMN.
5151+const MIGRATIONS: &[(u32, &str)] = &[
5252+ (2, "
5353+ CREATE TABLE events_new (
5454+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5555+ nix_id INTEGER,
5656+ parent_id INTEGER,
5757+ event_type INTEGER,
5858+ text TEXT,
5959+ drv_path TEXT,
6060+ cache_url TEXT,
6161+ start_time INTEGER,
6262+ end_time INTEGER,
6363+ duration_ms INTEGER,
6464+ total_bytes INTEGER
6565+ );
6666+ INSERT INTO events_new
6767+ SELECT id, nix_id, parent_id, event_type, text, drv_path, cache_url,
6868+ CAST(strftime('%s', start_time) AS INTEGER),
6969+ CAST(strftime('%s', end_time) AS INTEGER),
7070+ duration_ms, total_bytes
7171+ FROM events;
7272+ DROP TABLE events;
7373+ ALTER TABLE events_new RENAME TO events;
7474+ CREATE INDEX IF NOT EXISTS idx_events_type_start ON events(event_type, start_time);
7575+ "),
7676+];
47774878#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4979#[repr(u64)]
···194224 let conn = Connection::open(path)
195225 .with_context(|| format!("Failed to open database at {}", path.display()))?;
196226197197- // Check schema version; reset if mismatched.
198198- let version: u32 = conn
227227+ let db_version: u32 = conn
199228 .query_row("PRAGMA user_version", [], |r| r.get(0))
200229 .unwrap_or(0);
201230202202- if version != 0 && version != SCHEMA_VERSION {
203203- info!(current = version, expected = SCHEMA_VERSION, "Schema version mismatch, resetting database");
204204- drop(conn);
205205- std::fs::remove_file(path)
206206- .with_context(|| format!("Failed to remove stale database at {}", path.display()))?;
207207- return open_db(path);
231231+ assert!(db_version <= u32::MAX);
232232+233233+ if db_version > SCHEMA_VERSION {
234234+ anyhow::bail!(
235235+ "Database at {} was created by a newer version of nod \
236236+ (db version {}, this binary knows version {}). \
237237+ Delete it manually to start fresh, or upgrade nod.",
238238+ path.display(), db_version, SCHEMA_VERSION
239239+ );
208240 }
209241210210- conn.execute_batch(SCHEMA)
211211- .context("Failed to create schema")?;
212212- conn.execute_batch(&format!("PRAGMA user_version = {}", SCHEMA_VERSION))
213213- .context("Failed to set schema version")?;
242242+ if db_version == 0 {
243243+ conn.execute_batch(SCHEMA).context("Failed to create schema")?;
244244+ conn.execute_batch(&format!("PRAGMA user_version = {SCHEMA_VERSION}"))
245245+ .context("Failed to set schema version")?;
246246+ info!(version = SCHEMA_VERSION, "Initialized fresh database");
247247+ } else if db_version < SCHEMA_VERSION {
248248+ info!(from = db_version, to = SCHEMA_VERSION, "Migrating database");
249249+ let mut current = db_version;
250250+ for &(target, sql) in MIGRATIONS {
251251+ if current >= target { continue; }
252252+ assert!(target <= SCHEMA_VERSION);
253253+ assert!(target == current + 1, "migration gap: {} -> {}", current, target);
254254+ info!(from = current, to = target, "Running migration");
255255+ conn.execute_batch("BEGIN").context("Failed to begin migration transaction")?;
256256+ conn.execute_batch(sql)
257257+ .with_context(|| format!("Migration v{current} -> v{target} failed"))?;
258258+ conn.execute_batch("COMMIT").context("Failed to commit migration")?;
259259+ conn.execute_batch(&format!("PRAGMA user_version = {target}"))
260260+ .context("Failed to update schema version after migration")?;
261261+ current = target;
262262+ }
263263+ assert_eq!(current, SCHEMA_VERSION, "migrations did not reach current schema version");
264264+ info!(version = SCHEMA_VERSION, "Migration complete");
265265+ }
266266+214267 conn.execute_batch("
215268 PRAGMA journal_mode = WAL;
216269 PRAGMA synchronous = NORMAL;
···362415 rusqlite::params![
363416 act.id as i64, act.parent_id as i64, act.event_type as i64,
364417 act.text, drv_path, cache_url,
365365- act.start_time.to_rfc3339(), end_time.to_rfc3339(),
418418+ act.start_time.timestamp(), end_time.timestamp(),
366419 duration_ms, act.total_bytes as i64,
367420 ],
368421 ).context("Failed to insert event")?;
+4-12
src/stats.rs
···3232pub fn collect_stats(db: &Mutex<Connection>, since: Option<i64>) -> Result<Stats> {
3333 let conn = db.lock().unwrap();
34343535- // SQL NULL makes the WHERE condition vacuously true, giving us "no filter".
3636- let since_str: Option<String> = since
3737- .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
3838- .map(|dt| dt.to_rfc3339());
3939- let p = since_str.as_deref();
3535+ let p: Option<i64> = since;
40364137 let (build_count, build_total_ms, subst_count, subst_total_ms, download_bytes, download_ms) =
4238 conn.query_row(
···254250255251 let conn = db.lock().unwrap();
256252257257- let since_str: Option<String> = since
258258- .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
259259- .map(|dt| dt.to_rfc3339());
260260- let p = since_str.as_deref();
253253+ let p: Option<i64> = since;
261254 let drv_ref = drv.as_deref();
262255 let fmt = bucket.strftime_fmt();
263256264264- // Rows come out ordered by bucket then time, so grouping by sequential scan is valid.
265257 // FileTransfer (101) has NULL drv_path and is intentionally excluded by the drv filter.
266258 let mut stmt = conn.prepare(
267267- "SELECT strftime(?3, start_time), event_type, duration_ms, total_bytes
259259+ "SELECT strftime(?3, start_time, 'unixepoch'), event_type, duration_ms, total_bytes
268260 FROM events
269261 WHERE event_type IN (101, 105, 108)
270262 AND (?1 IS NULL OR start_time >= ?1)
271263 AND (?2 IS NULL OR drv_path LIKE '%' || ?2 || '%')
272272- ORDER BY strftime(?3, start_time) ASC, start_time ASC",
264264+ ORDER BY strftime(?3, start_time, 'unixepoch') ASC, start_time ASC",
273265 ).context("Failed to prepare trend query")?;
274266275267 let mut buckets: Vec<TrendBucket> = vec![];