use crate::db::SessionDbResult;
use crate::db_event_store::{event_created_at_ms, seq_to_value};
use crate::db_row::{i64_to_u64, required_i64, required_string};
use bcode_session_models::SessionEvent;
use std::collections::BTreeMap;
use switchy::database::{Database, DatabaseValue, query::FilterableQuery};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaterializedProjection {
Submissions,
SessionState,
InputHistory,
Transcript,
ToolRuns,
ArtifactReferences,
RuntimeWork,
SessionUsage,
TurnEvidence,
Milestones,
RequestContextOccupancy,
}
impl MaterializedProjection {
const ALL: [Self; 11] = [
Self::Milestones,
Self::Submissions,
Self::TurnEvidence,
Self::SessionState,
Self::InputHistory,
Self::Transcript,
Self::ToolRuns,
Self::ArtifactReferences,
Self::RuntimeWork,
Self::SessionUsage,
Self::RequestContextOccupancy,
];
#[must_use]
pub const fn all() -> &'static [Self] {
&Self::ALL
}
#[must_use]
pub const fn schema_version(self) -> u32 {
match self {
Self::RequestContextOccupancy => 4,
Self::Transcript => 3,
Self::ToolRuns | Self::Submissions => 2,
Self::Milestones
| Self::SessionState
| Self::InputHistory
| Self::ArtifactReferences
| Self::RuntimeWork
| Self::TurnEvidence
| Self::SessionUsage => 1,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Milestones => "milestones",
Self::Submissions => "submissions",
Self::SessionState => "session_state",
Self::InputHistory => "input_history",
Self::Transcript => "transcript",
Self::ToolRuns => "tool_runs",
Self::ArtifactReferences => "artifact_references",
Self::RuntimeWork => "runtime_work",
Self::TurnEvidence => "turn_evidence",
Self::SessionUsage => "session_usage",
Self::RequestContextOccupancy => "context_occupancy",
}
}
}
pub async fn projection_checkpoint_snapshot(
db: &dyn Database,
) -> SessionDbResult<BTreeMap<String, ProjectionCheckpointState>> {
let rows = db
.select("projection_checkpoints")
.columns(&["projection_name", "projection_version", "last_event_seq"])
.where_in(
"projection_name",
MaterializedProjection::all()
.iter()
.map(|projection| DatabaseValue::String(projection.as_str().to_owned()))
.collect::<Vec<_>>(),
)
.execute(db)
.await?;
rows.into_iter()
.map(|row| {
let name = required_string(&row, "projection_name")?;
let state = ProjectionCheckpointState {
version: required_i64(&row, "projection_version").map(i64_to_u64)?,
checkpoint: required_i64(&row, "last_event_seq").map(i64_to_u64)?,
};
Ok((name, state))
})
.collect()
}
pub async fn update_projection_checkpoint(
db: &dyn Database,
projection: MaterializedProjection,
event: &SessionEvent,
) -> SessionDbResult<()> {
db.upsert("projection_checkpoints")
.unique(&["projection_name"])
.value("projection_name", projection.as_str())
.value("last_event_seq", seq_to_value(event.sequence))
.value(
"projection_version",
DatabaseValue::Int64(i64::from(projection.schema_version())),
)
.value("updated_at_ms", seq_to_value(event_created_at_ms(event)))
.execute(db)
.await?;
Ok(())
}
pub const BASE_MATERIALIZED_PROJECTIONS: [MaterializedProjection; 10] = [
MaterializedProjection::Submissions,
MaterializedProjection::Milestones,
MaterializedProjection::TurnEvidence,
MaterializedProjection::SessionState,
MaterializedProjection::InputHistory,
MaterializedProjection::Transcript,
MaterializedProjection::ToolRuns,
MaterializedProjection::ArtifactReferences,
MaterializedProjection::RuntimeWork,
MaterializedProjection::SessionUsage,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProjectionCheckpointState {
pub(crate) version: u64,
pub(crate) checkpoint: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn projection_inventory_is_complete_and_stable() {
assert_eq!(MaterializedProjection::all().len(), 11);
assert_eq!(
MaterializedProjection::RequestContextOccupancy.schema_version(),
4
);
assert_eq!(MaterializedProjection::Transcript.schema_version(), 3);
assert_eq!(MaterializedProjection::ToolRuns.schema_version(), 2);
assert_eq!(
MaterializedProjection::ArtifactReferences.as_str(),
"artifact_references"
);
}
}