use crate::db::{SessionDbError, SessionDbResult};
use crate::persisted::{decode_session_event, encode_session_event};
pub use bcode_session_models::persisted_session_event_kind_name as event_kind_name;
use bcode_session_models::{SessionEvent, SessionId};
use switchy::database::{Database, DatabaseValue, query::SortDirection};
pub const fn event_created_at_ms(event: &SessionEvent) -> u64 {
event.timestamp_ms
}
pub fn seq_to_value(sequence: u64) -> DatabaseValue {
DatabaseValue::Int64(i64::try_from(sequence).unwrap_or(i64::MAX))
}
pub async fn insert_event(
db: &dyn Database,
event: &SessionEvent,
activity_timestamp_ms: Option<u64>,
) -> SessionDbResult<()> {
insert_event_payload(
db,
event,
activity_timestamp_ms,
encode_session_event(event)?,
)
.await
}
pub async fn insert_event_payload(
db: &dyn Database,
event: &SessionEvent,
activity_timestamp_ms: Option<u64>,
payload: String,
) -> SessionDbResult<()> {
db.insert("events")
.value("event_seq", seq_to_value(event.sequence))
.value("event_type", event_kind_name(&event.kind))
.value(
"schema_version",
DatabaseValue::Int32(i32::from(event.schema_version)),
)
.value(
"created_at_ms",
seq_to_value(activity_timestamp_ms.unwrap_or_else(|| event_created_at_ms(event))),
)
.value("payload", payload)
.execute(db)
.await?;
Ok(())
}
pub async fn strict_events(db: &dyn Database) -> SessionDbResult<Vec<SessionEvent>> {
let rows = db
.select("events")
.columns(&["payload"])
.sort("event_seq", SortDirection::Asc)
.execute(db)
.await?;
rows.into_iter()
.map(|row| decode_session_event(&required_string(&row, "payload")?).map_err(Into::into))
.collect()
}
pub fn strict_event_from_row(
row: &switchy::database::Row,
session_id: SessionId,
) -> SessionDbResult<SessionEvent> {
let event_seq = required_non_negative_u64(row, "event_seq")?;
let payload = required_string(row, "payload")?;
let event = decode_session_event(&payload)?;
if event.sequence != event_seq {
return Err(SessionDbError::InvalidCanonicalSequence {
expected: event_seq,
actual: event.sequence,
});
}
if event.session_id != session_id {
return Err(SessionDbError::InvalidRow {
column: "events.session_id".to_owned(),
});
}
Ok(event)
}
pub async fn last_sequence(db: &dyn Database) -> SessionDbResult<Option<u64>> {
let row = db
.select("events")
.columns(&["event_seq"])
.sort("event_seq", SortDirection::Desc)
.limit(1)
.execute_first(db)
.await?;
let value = row
.as_ref()
.and_then(|row| row.get("event_seq"))
.and_then(|value| value.as_i64());
match value {
Some(value) if value.is_negative() => Err(SessionDbError::InvalidRow {
column: "event_seq".to_owned(),
}),
Some(value) => Ok(Some(value.cast_unsigned())),
None => Ok(None),
}
}
fn required_string(row: &switchy::database::Row, column: &str) -> SessionDbResult<String> {
row.get(column)
.and_then(|value| value.as_str().map(ToOwned::to_owned))
.ok_or_else(|| SessionDbError::InvalidRow {
column: column.to_owned(),
})
}
fn required_non_negative_u64(row: &switchy::database::Row, column: &str) -> SessionDbResult<u64> {
let value = row
.get(column)
.and_then(|value| value.as_i64())
.ok_or_else(|| SessionDbError::InvalidRow {
column: column.to_owned(),
})?;
if value.is_negative() {
return Err(SessionDbError::InvalidRow {
column: column.to_owned(),
});
}
Ok(value.cast_unsigned())
}