use std::path::Path;
use rusqlite::Connection;
use tracing::{debug, info};
use crate::core::config::expand_path;
use crate::core::errors::{Result, TgaError};
pub mod azdo_iterations;
pub mod collection_runs;
pub mod migrations;
pub mod work_items;
pub use azdo_iterations::{list_iterations, upsert_iteration};
pub use collection_runs::{is_week_collected, record_collection_run, repo_count_for_week};
pub use work_items::{
get_work_item, get_work_items_for_commit, link_commit_work_item, list_work_items,
upsert_work_item, WorkItemRow,
};
pub struct Database {
conn: Connection,
}
impl Database {
pub fn open(path: &Path) -> Result<Database> {
let resolved = expand_path(path);
debug!(path = %resolved.display(), "opening database");
let conn = Connection::open(&resolved)?;
Self::apply_pragmas(&conn)?;
let mut db = Database { conn };
migrations::run(&mut db.conn)?;
info!(path = %resolved.display(), "database ready");
Ok(db)
}
pub fn open_in_memory() -> Result<Database> {
let conn = Connection::open_in_memory()?;
Self::apply_pragmas(&conn)?;
let mut db = Database { conn };
migrations::run(&mut db.conn)?;
Ok(db)
}
fn apply_pragmas(conn: &Connection) -> Result<()> {
let mode: String = conn
.query_row("PRAGMA journal_mode=WAL", [], |row| row.get(0))
.map_err(TgaError::from)?;
debug!(journal_mode = %mode, "applied WAL pragma");
conn.execute_batch(
"PRAGMA synchronous = NORMAL; \
PRAGMA foreign_keys = ON; \
PRAGMA cache_size = -65536; \
PRAGMA temp_store = MEMORY; \
PRAGMA mmap_size = 268435456;",
)?;
debug!("applied SQLite tuning pragmas (cache=64MB, mmap=256MB, temp=memory)");
Ok(())
}
pub fn connection(&self) -> &Connection {
&self.conn
}
pub fn connection_mut(&mut self) -> &mut Connection {
&mut self.conn
}
pub fn journal_mode(&self) -> Result<String> {
let mode: String = self
.conn
.query_row("PRAGMA journal_mode", [], |row| row.get(0))
.map_err(TgaError::from)?;
Ok(mode)
}
pub fn schema_version(&self) -> Result<i64> {
let v: i64 = self
.conn
.query_row(
"SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
[],
|row| row.get(0),
)
.map_err(TgaError::from)?;
Ok(v)
}
pub fn wal_checkpoint(&self, mode: CheckpointMode) -> Result<()> {
let mode_str = match mode {
CheckpointMode::Passive => "PASSIVE",
CheckpointMode::Truncate => "TRUNCATE",
};
let (busy, log, checkpointed): (i64, i64, i64) = self
.conn
.query_row(&format!("PRAGMA wal_checkpoint({mode_str})"), [], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})
.map_err(TgaError::from)?;
info!(
mode = mode_str,
busy, log, checkpointed, "WAL checkpoint complete"
);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckpointMode {
Passive,
Truncate,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wal_checkpoint_succeeds_on_in_memory_db() {
let db = Database::open_in_memory().expect("open");
db.wal_checkpoint(CheckpointMode::Passive)
.expect("passive checkpoint must not fail");
db.wal_checkpoint(CheckpointMode::Truncate)
.expect("truncate checkpoint must not fail");
}
#[test]
fn wal_checkpoint_truncate_zeroes_wal_on_file_db() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
let db_path = tmp.path().to_path_buf();
tmp.keep().expect("keep tempfile");
{
let db = Database::open(&db_path).expect("open");
assert_eq!(db.journal_mode().expect("journal_mode"), "wal");
for i in 0..100 {
db.connection()
.execute(
"INSERT INTO commits \
(sha, author_name, author_email, timestamp, message, repository) \
VALUES (?1, 'a', 'a@x', '2024-01-01T00:00:00Z', 'msg', 'repo')",
rusqlite::params![format!("sha-{i}")],
)
.expect("insert");
}
db.wal_checkpoint(CheckpointMode::Truncate)
.expect("truncate checkpoint");
}
let wal_path = db_path.with_extension("db-wal");
if wal_path.exists() {
let wal_size = std::fs::metadata(&wal_path).expect("wal metadata").len();
assert_eq!(
wal_size, 0,
"WAL file must be zero bytes after TRUNCATE checkpoint, got {wal_size} bytes"
);
}
let db2 = Database::open(&db_path).expect("reopen");
let count: i64 = db2
.connection()
.query_row("SELECT COUNT(*) FROM commits", [], |row| row.get(0))
.expect("count");
assert_eq!(count, 100, "all 100 rows must be durable after checkpoint");
let _ = std::fs::remove_file(&db_path);
let _ = std::fs::remove_file(&wal_path);
let shm_path = db_path.with_extension("db-shm");
let _ = std::fs::remove_file(&shm_path);
}
}