@@ -14,6 +14,13 @@ CREATE TABLE IF NOT EXISTS runs (
1414 runtime_s REAL,
1515 success INTEGER NOT NULL DEFAULT 1,
1616 error_msg TEXT,
17+ env_key TEXT,
18+ julia_version TEXT,
19+ os_arch TEXT,
20+ manifest_sha TEXT,
21+ nthreads INTEGER,
22+ blas_threads INTEGER,
23+ pinned INTEGER,
1724 UNIQUE(commit_hash, case_name)
1825);
1926
@@ -35,6 +42,14 @@ CREATE INDEX IF NOT EXISTS idx_runs_case ON runs(case_name);
3542CREATE INDEX IF NOT EXISTS idx_quantities_run ON quantities(run_id);
3643"""
3744
45+ """
46+ Value of a possibly-absent SQLite column, falling back to `default`.
47+
48+ SQLite.jl returns `missing` for NULL while Julia's `something` only skips `nothing`, so columns
49+ added by a later schema migration (NULL on every pre-existing row) need both cases handled.
50+ """
51+ _column (x, default) = (x === nothing || x === missing ) ? default : x
52+
3853""" Materialize SQLite query results as a Vector of NamedTuples."""
3954function query_rows (db:: SQLite.DB , sql:: String , params= ())
4055 result = DBInterface. execute (db, sql, params)
@@ -44,6 +59,29 @@ function query_rows(db::SQLite.DB, sql::String, params=())
4459 return [NamedTuple {keys(ct)} (Tuple (col[i] for col in values (ct))) for i in 1 : nrows]
4560end
4661
62+ """
63+ Environment columns added to `runs` after the original schema shipped. Databases created before
64+ fingerprinting keep their rows, with NULL in these columns — such rows never match a computed
65+ `env_key`, so they are re-run rather than silently trusted.
66+ """
67+ const ENV_COLUMNS = [
68+ (" env_key" , " TEXT" ), (" julia_version" , " TEXT" ), (" os_arch" , " TEXT" ),
69+ (" manifest_sha" , " TEXT" ), (" nthreads" , " INTEGER" ), (" blas_threads" , " INTEGER" ),
70+ (" pinned" , " INTEGER" )
71+ ]
72+
73+ """ Add any `runs` columns missing from a database created by an earlier harness version."""
74+ function migrate_schema! (db:: SQLite.DB )
75+ existing = Set (String[])
76+ for row in query_rows (db, " PRAGMA table_info(runs)" )
77+ push! (existing, String (something (row. name, " " )))
78+ end
79+ for (col, sqltype) in ENV_COLUMNS
80+ col in existing && continue
81+ DBInterface. execute (db, " ALTER TABLE runs ADD COLUMN $col $sqltype " )
82+ end
83+ end
84+
4785function open_database (path:: String ):: SQLite.DB
4886 db = SQLite. DB (path)
4987 DBInterface. execute (db, " PRAGMA journal_mode=WAL" )
@@ -53,19 +91,47 @@ function open_database(path::String)::SQLite.DB
5391 isempty (s) && continue
5492 DBInterface. execute (db, s)
5593 end
94+ migrate_schema! (db)
5695 return db
5796end
5897
5998function close_database (db:: SQLite.DB )
6099 SQLite. close (db)
61100end
62101
63- function is_cached (db:: SQLite.DB , commit_hash:: String , case_name:: String ):: Bool
64- rows = query_rows (db, " SELECT id FROM runs WHERE commit_hash = ? AND case_name = ? AND success = 1" ,
65- (commit_hash, case_name))
102+ """
103+ Is there a usable cached result for this (commit, case)?
104+
105+ With `expected_key` supplied, a cached run only counts when it was produced in the same
106+ environment. Rows predating fingerprinting hold NULL and therefore never match — the cache
107+ entries most likely to be misleading are exactly the ones that get re-run.
108+ """
109+ function is_cached (db:: SQLite.DB , commit_hash:: String , case_name:: String ;
110+ expected_key:: Union{String,Nothing} = nothing ):: Bool
111+ if expected_key === nothing
112+ rows = query_rows (db, " SELECT id FROM runs WHERE commit_hash = ? AND case_name = ? AND success = 1" ,
113+ (commit_hash, case_name))
114+ return ! isempty (rows)
115+ end
116+ rows = query_rows (db,
117+ " SELECT id FROM runs WHERE commit_hash = ? AND case_name = ? AND success = 1 AND env_key = ?" ,
118+ (commit_hash, case_name, expected_key))
66119 return ! isempty (rows)
67120end
68121
122+ """
123+ Environment key stored for a cached run, or `nothing` when the run is absent or predates
124+ fingerprinting. Used to explain *why* a cached result was rejected.
125+ """
126+ function cached_env_key (db:: SQLite.DB , commit_hash:: String , case_name:: String ):: Union{String,Nothing}
127+ rows = query_rows (db, " SELECT env_key FROM runs WHERE commit_hash = ? AND case_name = ?" ,
128+ (commit_hash, case_name))
129+ isempty (rows) && return nothing
130+ key = _column (first (rows). env_key, nothing )
131+ key === nothing && return nothing
132+ return String (key)
133+ end
134+
69135function delete_cached (db:: SQLite.DB , commit_hash:: String , case_name:: String )
70136 # ON DELETE CASCADE handles quantities cleanup automatically
71137 DBInterface. execute (db, " DELETE FROM runs WHERE commit_hash = ? AND case_name = ?" ,
@@ -76,18 +142,23 @@ function store_run(db::SQLite.DB, commit_hash::AbstractString, commit_short::Abs
76142 commit_date:: AbstractString , commit_msg:: AbstractString ,
77143 case_name:: AbstractString , runtime_s:: Float64 ,
78144 extracted:: Vector{ExtractedQuantity} ;
79- success:: Bool = true , error_msg:: AbstractString = " " )
145+ success:: Bool = true , error_msg:: AbstractString = " " ,
146+ fingerprint:: EnvFingerprint = UNKNOWN_ENV)
80147 ran_at = Dates. format (Dates. now (), " yyyy-mm-ddTHH:MM:SS" )
81148
82149 SQLite. transaction (db) do
83150 delete_cached (db, String (commit_hash), String (case_name))
84151
85152 DBInterface. execute (db,
86153 """ INSERT INTO runs
87- (commit_hash, commit_short, commit_date, commit_msg, case_name, ran_at, runtime_s, success, error_msg)
88- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""" ,
154+ (commit_hash, commit_short, commit_date, commit_msg, case_name, ran_at, runtime_s, success, error_msg,
155+ env_key, julia_version, os_arch, manifest_sha, nthreads, blas_threads, pinned)
156+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" ,
89157 (String (commit_hash), String (commit_short), String (commit_date), String (commit_msg),
90- String (case_name), ran_at, runtime_s, success ? 1 : 0 , String (error_msg)))
158+ String (case_name), ran_at, runtime_s, success ? 1 : 0 , String (error_msg),
159+ env_key (fingerprint), fingerprint. julia_version, fingerprint. os_arch,
160+ fingerprint. manifest_sha, fingerprint. nthreads, fingerprint. blas_threads,
161+ fingerprint. pinned ? 1 : 0 ))
91162
92163 run_id = SQLite. last_insert_rowid (db)
93164
@@ -147,18 +218,28 @@ Get run info for a (commit, case) pair. Returns NamedTuple or nothing.
147218"""
148219function get_run_info (db:: SQLite.DB , commit_hash:: String , case_name:: String )
149220 rows = query_rows (db,
150- """ SELECT commit_short, commit_date, commit_msg, runtime_s, success, error_msg
221+ """ SELECT commit_short, commit_date, commit_msg, runtime_s, success, error_msg,
222+ julia_version, os_arch, manifest_sha, nthreads, blas_threads, pinned
151223 FROM runs WHERE commit_hash = ? AND case_name = ?""" ,
152224 (commit_hash, case_name))
153225 isempty (rows) && return nothing
154226 row = first (rows)
227+ fingerprint = EnvFingerprint (
228+ String (_column (row. julia_version, " " )),
229+ String (_column (row. os_arch, " " )),
230+ String (_column (row. manifest_sha, " " )),
231+ Int (_column (row. nthreads, - 1 )),
232+ Int (_column (row. blas_threads, - 1 )),
233+ _column (row. pinned, 0 ) == 1
234+ )
155235 return (
156236 commit_short = something (row. commit_short, " " ),
157237 commit_date = something (row. commit_date, " " ),
158238 commit_msg = something (row. commit_msg, " " ),
159239 runtime_s = something (row. runtime_s, 0.0 ),
160240 success = coalesce (row. success, 0 ) == 1 ,
161241 error_msg = something (row. error_msg, " " ),
242+ fingerprint = fingerprint,
162243 )
163244end
164245
0 commit comments