use super::*;
pub(crate) fn ensure_receipt_retention_watermark_table(
connection: &rusqlite::Connection,
) -> Result<(), ReceiptStoreError> {
connection.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS receipt_retention_watermark (
archived_through_entry_seq INTEGER NOT NULL,
archived_through_timestamp INTEGER NOT NULL,
archive_path TEXT NOT NULL,
archive_sha256 TEXT,
rotated_at INTEGER NOT NULL,
CHECK (archived_through_entry_seq >= 0)
);
CREATE TRIGGER IF NOT EXISTS receipt_retention_watermark_reject_update
BEFORE UPDATE ON receipt_retention_watermark
BEGIN
SELECT RAISE(ABORT, 'retention watermark ledger is append-only');
END;
CREATE TRIGGER IF NOT EXISTS receipt_retention_watermark_reject_delete
BEFORE DELETE ON receipt_retention_watermark
BEGIN
SELECT RAISE(ABORT, 'retention watermark ledger is append-only');
END;
-- Monotonic increase enforced at the DB level: a new mark must be
-- strictly greater than the current MAX. The subquery is NULL on an
-- empty ledger, so the WHEN clause is NULL (not true) and the first
-- mark is always accepted.
CREATE TRIGGER IF NOT EXISTS receipt_retention_watermark_reject_regression
BEFORE INSERT ON receipt_retention_watermark
WHEN NEW.archived_through_entry_seq
<= (SELECT MAX(archived_through_entry_seq) FROM receipt_retention_watermark)
BEGIN
SELECT RAISE(ABORT, 'retention watermark must increase monotonically');
END;
"#,
)?;
Ok(())
}
pub(crate) fn ensure_receipt_retention_tombstones(
connection: &rusqlite::Connection,
) -> Result<(), ReceiptStoreError> {
connection.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS receipt_retention_tombstones (
receipt_id TEXT PRIMARY KEY,
receipt_kind TEXT NOT NULL,
archived_through_entry_seq INTEGER NOT NULL,
tombstoned_at INTEGER NOT NULL
);
CREATE TRIGGER IF NOT EXISTS receipt_retention_tombstones_reject_update
BEFORE UPDATE ON receipt_retention_tombstones
BEGIN
SELECT RAISE(ABORT, 'receipt retention tombstones are append-only');
END;
CREATE TRIGGER IF NOT EXISTS receipt_retention_tombstones_reject_delete
BEFORE DELETE ON receipt_retention_tombstones
BEGIN
SELECT RAISE(ABORT, 'receipt retention tombstones are append-only');
END;
CREATE TRIGGER IF NOT EXISTS chio_tool_receipts_reject_archived_reuse
BEFORE INSERT ON chio_tool_receipts
WHEN EXISTS (SELECT 1 FROM receipt_retention_tombstones WHERE receipt_id = NEW.receipt_id)
BEGIN
SELECT RAISE(ABORT, 'receipt_id was archived by retention and cannot be re-appended');
END;
CREATE TRIGGER IF NOT EXISTS chio_child_receipts_reject_archived_reuse
BEFORE INSERT ON chio_child_receipts
WHEN EXISTS (SELECT 1 FROM receipt_retention_tombstones WHERE receipt_id = NEW.receipt_id)
BEGIN
SELECT RAISE(ABORT, 'receipt_id was archived by retention and cannot be re-appended');
END;
"#,
)?;
Ok(())
}
pub(crate) fn retention_watermark(
connection: &rusqlite::Connection,
) -> Result<Option<u64>, ReceiptStoreError> {
if !receipt_retention_watermark_table_exists(connection)? {
return Ok(None);
}
let value: Option<i64> = connection.query_row(
"SELECT MAX(archived_through_entry_seq) FROM receipt_retention_watermark",
[],
|row| row.get(0),
)?;
match value {
None => Ok(None),
Some(raw) => Ok(Some(sqlite_u64(raw, "retention watermark")?)),
}
}
pub(crate) fn latest_watermark_archive_path(
connection: &rusqlite::Connection,
) -> Result<Option<String>, ReceiptStoreError> {
if !receipt_retention_watermark_table_exists(connection)? {
return Ok(None);
}
let path: Option<String> = connection
.query_row(
"SELECT archive_path FROM receipt_retention_watermark \
ORDER BY archived_through_entry_seq DESC, rotated_at DESC LIMIT 1",
[],
|row| row.get(0),
)
.optional()?;
Ok(path)
}
fn receipt_retention_watermark_table_exists(
connection: &rusqlite::Connection,
) -> Result<bool, ReceiptStoreError> {
let exists: Option<i64> = connection
.query_row(
"SELECT 1 FROM sqlite_master \
WHERE type = 'table' AND name = 'receipt_retention_watermark'",
[],
|row| row.get(0),
)
.optional()?;
Ok(exists.is_some())
}
pub(crate) fn kernel_checkpoints_exist(
connection: &rusqlite::Connection,
) -> Result<bool, ReceiptStoreError> {
let count: i64 =
connection.query_row("SELECT COUNT(*) FROM kernel_checkpoints", [], |row| {
row.get(0)
})?;
Ok(count > 0)
}
pub(crate) fn insert_receipt_retention_watermark(
connection: &rusqlite::Connection,
archived_through_entry_seq: u64,
archived_through_timestamp: u64,
archive_path: &str,
archive_sha256: Option<&str>,
rotated_at: u64,
) -> Result<(), ReceiptStoreError> {
if let Some(current) = retention_watermark(connection)? {
if archived_through_entry_seq < current {
return Err(ReceiptStoreError::RetentionWatermarkRegression {
attempted: archived_through_entry_seq,
current,
});
}
}
connection.execute(
"INSERT INTO receipt_retention_watermark \
(archived_through_entry_seq, archived_through_timestamp, archive_path, archive_sha256, rotated_at) \
VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![
sqlite_i64(archived_through_entry_seq, "watermark entry_seq")?,
sqlite_i64(archived_through_timestamp, "watermark timestamp")?,
archive_path,
archive_sha256,
sqlite_i64(rotated_at, "watermark rotated_at")?,
],
)?;
Ok(())
}
pub(crate) fn migrate_auto_vacuum_incremental_if_needed(
connection: &rusqlite::Connection,
) -> Result<(), ReceiptStoreError> {
let mode: i64 = connection.query_row("PRAGMA auto_vacuum", [], |row| row.get(0))?;
if mode == 0 {
connection.execute_batch("PRAGMA auto_vacuum = INCREMENTAL; VACUUM;")?;
}
Ok(())
}