use crate::backend::{as_u64, Backend, Value};
use crate::error::{Result, StoreError};
const META_TABLE: &str = "_agent_store_meta";
#[derive(Clone, Debug)]
pub struct Generation {
key: String,
}
impl Generation {
pub fn new(key: impl Into<String>) -> Self {
Self { key: key.into() }
}
pub fn ensure_schema(db: &dyn Backend) -> Result<()> {
db.exec(
&format!(
"CREATE TABLE IF NOT EXISTS {META_TABLE} (\
key TEXT PRIMARY KEY, ival INTEGER NOT NULL)"
),
&[],
)?;
Ok(())
}
pub fn current(&self, db: &dyn Backend) -> Result<u64> {
let rows = db.query(
&format!("SELECT ival FROM {META_TABLE} WHERE key = ?"),
&[Value::Text(self.key.clone())],
)?;
match rows.first() {
Some(row) => as_u64(&row[0]),
None => Ok(0),
}
}
pub fn bump(&self, db: &dyn Backend) -> Result<u64> {
let rows = db.query(
&format!(
"INSERT INTO {META_TABLE} (key, ival) VALUES (?, 1) \
ON CONFLICT(key) DO UPDATE SET ival = ival + 1 RETURNING ival"
),
&[Value::Text(self.key.clone())],
)?;
let row = rows
.first()
.ok_or_else(|| StoreError::Backend("bump: RETURNING produced no row".into()))?;
as_u64(&row[0])
}
pub fn set(&self, db: &dyn Backend, value: u64) -> Result<()> {
let current = self.current(db)?;
if value < current {
return Err(StoreError::NonMonotonicGeneration {
key: self.key.clone(),
current,
attempted: value,
});
}
db.exec(
&format!(
"INSERT INTO {META_TABLE} (key, ival) VALUES (?, ?) \
ON CONFLICT(key) DO UPDATE SET ival = excluded.ival"
),
&[Value::Text(self.key.clone()), Value::Int(value as i64)],
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::SqliteBackend;
fn db() -> SqliteBackend {
let db = SqliteBackend::in_memory().unwrap();
Generation::ensure_schema(&db).unwrap();
db
}
#[test]
fn starts_at_zero_then_increments() {
let db = db();
let g = Generation::new("report");
assert_eq!(g.current(&db).unwrap(), 0);
assert_eq!(g.bump(&db).unwrap(), 1);
assert_eq!(g.bump(&db).unwrap(), 2);
assert_eq!(g.current(&db).unwrap(), 2);
}
#[test]
fn counters_are_independent() {
let db = db();
let a = Generation::new("a");
let b = Generation::new("b");
a.bump(&db).unwrap();
a.bump(&db).unwrap();
b.bump(&db).unwrap();
assert_eq!(a.current(&db).unwrap(), 2);
assert_eq!(b.current(&db).unwrap(), 1);
}
#[test]
fn set_advances_but_never_rewinds() {
let db = db();
let g = Generation::new("report");
g.set(&db, 10).unwrap();
assert_eq!(g.current(&db).unwrap(), 10);
let err = g.set(&db, 3).unwrap_err();
assert!(matches!(
err,
StoreError::NonMonotonicGeneration {
current: 10,
attempted: 3,
..
}
));
assert_eq!(g.current(&db).unwrap(), 10);
}
}