goosedump 0.12.46

Browse, search, summarize, compact, and learn from coding-agent sessions
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) Jarkko Sakkinen 2026

//! Retention of immutable evidence revisions.

use std::collections::HashSet;

use rusqlite::{Connection, params_from_iter};

const PRUNE_BATCH_SIZE: usize = 500;

pub(super) fn prune_stale_evidence_revisions(
    conn: &Connection,
    completed_ids: &HashSet<i64>,
) -> anyhow::Result<usize> {
    let mut completed_ids = completed_ids.iter().copied().collect::<Vec<_>>();
    completed_ids.sort_unstable();

    let mut deleted = 0;
    for batch in completed_ids.chunks(PRUNE_BATCH_SIZE) {
        deleted += prune(conn, Some(batch))?;
    }
    Ok(deleted)
}

pub(super) fn prune_all_stale_evidence_revisions(conn: &Connection) -> anyhow::Result<usize> {
    prune(conn, None)
}

fn prune(conn: &Connection, completed_ids: Option<&[i64]>) -> anyhow::Result<usize> {
    let scope = completed_ids.map_or_else(String::new, |ids| {
        let placeholders = std::iter::repeat_n("?", ids.len())
            .collect::<Vec<_>>()
            .join(",");
        format!(
            " AND EXISTS (
                  SELECT 1
                  FROM evidence AS touched
                  WHERE touched.id IN ({placeholders})
                    AND touched.provider = evidence.provider
                    AND touched.session_id = evidence.session_id
                    AND touched.entry_id = evidence.entry_id
              )"
        )
    });
    let sql = format!(
        "DELETE FROM evidence
         WHERE extraction_completed_at IS NOT NULL
           AND NOT EXISTS (
               SELECT 1 FROM claim_evidence
               WHERE claim_evidence.evidence_id = evidence.id
           )
           AND EXISTS (
               SELECT 1
               FROM evidence AS newer
               WHERE newer.provider = evidence.provider
                 AND newer.session_id = evidence.session_id
                 AND newer.entry_id = evidence.entry_id
                 AND newer.extraction_completed_at IS NOT NULL
                 AND (newer.created_at > evidence.created_at
                      OR (newer.created_at = evidence.created_at
                          AND newer.id > evidence.id))
           ){scope}"
    );
    let deleted = match completed_ids {
        Some(ids) => conn.execute(&sql, params_from_iter(ids.iter()))?,
        None => conn.execute(&sql, [])?,
    };
    Ok(deleted)
}