use std::fs;
use std::path::{Path, PathBuf};
use duckdb::{AccessMode, Config, Connection};
use crate::cache::repo_cache_dir;
use crate::facts::{apply_memory_pragmas, default_spill_dir};
use crate::quality_gates::ledger::now_utc_ts;
use crate::{CodeLoreError, Result};
use super::sarif_parse::ExternalFinding;
const STORE_FILENAME: &str = "external-findings.duckdb-ext";
const CREATE_TABLE: &str = "
CREATE TABLE IF NOT EXISTS external_findings (
engine TEXT NOT NULL,
engine_version TEXT NOT NULL,
rule_id TEXT NOT NULL,
path TEXT NOT NULL,
start_line INTEGER,
end_line INTEGER,
level TEXT NOT NULL,
fingerprint TEXT NOT NULL,
message TEXT NOT NULL,
ingested_at TEXT NOT NULL,
PRIMARY KEY (engine, fingerprint)
);
";
pub struct ExternalStore {
conn: Connection,
path: PathBuf,
}
impl ExternalStore {
pub fn open_existing(cache_root: &Path, repo_path: &Path) -> Result<Option<Self>> {
let path = repo_cache_dir(cache_root, repo_path).join(STORE_FILENAME);
if !path.exists() {
return Ok(None);
}
let conn = open_read_only(&path, cache_root)?;
Ok(Some(Self { conn, path }))
}
pub fn open_nonempty(cache_root: &Path, repo_path: &Path) -> Result<Option<Self>> {
match Self::open_existing(cache_root, repo_path)? {
Some(store) if store.has_findings_table()? && store.count()? > 0 => Ok(Some(store)),
_ => Ok(None),
}
}
fn has_findings_table(&self) -> Result<bool> {
let mut stmt = self
.conn
.prepare(
"SELECT COUNT(*) FROM information_schema.tables
WHERE table_name = 'external_findings'",
)
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: prepare table probe in {}: {e}",
self.path.display()
))
})?;
let present: u64 = stmt.query_row([], |row| row.get(0)).map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: table probe in {}: {e}",
self.path.display()
))
})?;
Ok(present > 0)
}
pub fn open_or_create(cache_root: &Path, repo_path: &Path) -> Result<Self> {
let dir = repo_cache_dir(cache_root, repo_path);
fs::create_dir_all(&dir).map_err(|e| {
CodeLoreError::Analysis(format!("external store: create dir {}: {e}", dir.display()))
})?;
let path = dir.join(STORE_FILENAME);
let conn = Connection::open(&path).map_err(|e| {
CodeLoreError::Analysis(format!("external store: open {}: {e}", path.display()))
})?;
apply_memory_pragmas(&conn, &default_spill_dir(Some(cache_root)))?;
conn.execute_batch(CREATE_TABLE).map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: create table in {}: {e}",
path.display()
))
})?;
Ok(Self { conn, path })
}
pub fn replace_engine(&self, engine: &str, findings: &[ExternalFinding]) -> Result<usize> {
let tx = self.conn.unchecked_transaction().map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: begin transaction in {}: {e}",
self.path.display()
))
})?;
tx.execute("DELETE FROM external_findings WHERE engine = ?", [engine])
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: delete engine {engine} in {}: {e}",
self.path.display()
))
})?;
if findings.is_empty() {
tx.commit().map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: commit in {}: {e}",
self.path.display()
))
})?;
return Ok(0);
}
let ingested_at = now_utc_ts();
let mut stmt = tx
.prepare(
"INSERT INTO external_findings
(engine, engine_version, rule_id, path, start_line, end_line,
level, fingerprint, message, ingested_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (engine, fingerprint) DO UPDATE SET
engine_version = excluded.engine_version,
rule_id = excluded.rule_id,
path = excluded.path,
start_line = excluded.start_line,
end_line = excluded.end_line,
level = excluded.level,
message = excluded.message,
ingested_at = excluded.ingested_at",
)
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: prepare insert in {}: {e}",
self.path.display()
))
})?;
for f in findings {
stmt.execute(duckdb::params![
f.engine,
f.engine_version,
f.rule_id,
f.path,
f.start_line,
f.end_line,
f.level,
f.fingerprint,
f.message,
ingested_at,
])
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: insert finding {} in {}: {e}",
f.fingerprint,
self.path.display()
))
})?;
}
drop(stmt);
tx.commit().map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: commit in {}: {e}",
self.path.display()
))
})?;
Ok(findings.len())
}
pub fn count(&self) -> Result<u64> {
let mut stmt = self
.conn
.prepare("SELECT COUNT(*) FROM external_findings")
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: prepare count in {}: {e}",
self.path.display()
))
})?;
let count: u64 = stmt.query_row([], |row| row.get(0)).map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: count in {}: {e}",
self.path.display()
))
})?;
Ok(count)
}
pub fn findings_by_path(&self) -> Result<std::collections::HashMap<String, PathFindings>> {
let mut stmt = self
.conn
.prepare(
"SELECT path, engine, level
FROM external_findings
ORDER BY path, engine",
)
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: prepare findings_by_path in {}: {e}",
self.path.display()
))
})?;
let mut map: std::collections::HashMap<String, PathFindings> =
std::collections::HashMap::new();
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: query findings_by_path in {}: {e}",
self.path.display()
))
})?;
for row in rows {
let (path, engine, level) = row.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: read row in findings_by_path: {e}"
))
})?;
let entry = map.entry(path).or_default();
entry.count += 1;
if !entry.engines.contains(&engine) {
entry.engines.push(engine);
}
entry.worst_level = worse_level(&entry.worst_level, &level);
}
Ok(map)
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
fn open_read_only(path: &Path, cache_root: &Path) -> Result<Connection> {
let config = Config::default()
.access_mode(AccessMode::ReadOnly)
.map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: configure read-only access for {}: {e}",
path.display()
))
})?;
let conn = Connection::open_with_flags(path, config).map_err(|e| {
CodeLoreError::Analysis(format!(
"external store: open read-only {}: {e}",
path.display()
))
})?;
apply_memory_pragmas(&conn, &default_spill_dir(Some(cache_root)))?;
Ok(conn)
}
#[derive(Debug, Default, Clone)]
pub struct PathFindings {
pub engines: Vec<String>,
pub count: usize,
pub worst_level: String,
}
fn worse_level(a: &str, b: &str) -> String {
fn rank(s: &str) -> u8 {
match s {
"error" => 2,
"warning" => 1,
_ => 0,
}
}
if rank(b) > rank(a) || a.is_empty() {
b.to_owned()
} else {
a.to_owned()
}
}
#[cfg(all(test, feature = "test-support"))]
mod tests {
use super::*;
fn sidecar_path(cache_root: &Path, repo_path: &Path) -> PathBuf {
let dir = repo_cache_dir(cache_root, repo_path);
fs::create_dir_all(&dir).expect("create cache dir");
dir.join(STORE_FILENAME)
}
#[test]
fn concurrent_readers_coexist_on_one_sidecar() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Path::new("/test/repo");
let writer = ExternalStore::open_or_create(dir.path(), repo).expect("open_or_create");
writer
.replace_engine(
"semgrep",
&[ExternalFinding {
engine: "semgrep".into(),
engine_version: "1.0".into(),
rule_id: "r/1".into(),
path: "src/a.rs".into(),
start_line: Some(1),
end_line: None,
level: "warning".into(),
fingerprint: "fp-1".into(),
message: "m".into(),
}],
)
.expect("seed finding");
drop(writer);
let reader_a = ExternalStore::open_existing(dir.path(), repo)
.expect("open reader a")
.expect("sidecar exists");
let reader_b = ExternalStore::open_existing(dir.path(), repo)
.expect("open reader b — second read-only open must not be blocked")
.expect("sidecar exists");
assert_eq!(reader_a.count().expect("count a"), 1);
assert_eq!(reader_b.count().expect("count b"), 1);
}
#[test]
fn tableless_sidecar_maps_to_none() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Path::new("/test/repo");
let path = sidecar_path(dir.path(), repo);
{
let conn = Connection::open(&path).expect("create tableless duckdb file");
conn.execute_batch("CREATE TABLE unrelated (x INTEGER)")
.expect("create unrelated table");
}
let store = ExternalStore::open_existing(dir.path(), repo)
.expect("open_existing on tableless file")
.expect("file exists so Some");
assert!(
!store.has_findings_table().expect("table probe"),
"external_findings table must be reported absent"
);
let reader = ExternalStore::open_nonempty(dir.path(), repo)
.expect("open_nonempty must not error on a tableless sidecar");
assert!(
reader.is_none(),
"a tableless sidecar must read as None, not error"
);
}
#[test]
fn empty_but_valid_sidecar_maps_to_none() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Path::new("/test/repo");
let writer = ExternalStore::open_or_create(dir.path(), repo).expect("open_or_create");
assert_eq!(writer.count().expect("count"), 0);
drop(writer);
let reader = ExternalStore::open_nonempty(dir.path(), repo).expect("open_nonempty");
assert!(reader.is_none(), "empty sidecar must read as None");
}
}