bcode_session 0.0.1-alpha.0

Session lifecycle and attachment management for bcode
//! Strict current writer-contract validation.

use crate::db::{SessionDbError, SessionDbResult};
use switchy::database::{Database, query::FilterableQuery};

pub async fn validate_writer_contract(
    db: &dyn Database,
    expected_writer_epoch: u32,
    expected_schema_version: u32,
    legacy_writer_epoch: u32,
    contract_id: i32,
) -> SessionDbResult<()> {
    let row = db
        .select("session_storage_contract")
        .columns(&["schema_version", "writer_epoch"])
        .where_eq("contract_id", contract_id)
        .execute_first(db)
        .await?;
    let Some(row) = row.as_ref() else {
        return Err(SessionDbError::WriterIncompatible {
            actual: Some(u64::from(legacy_writer_epoch)),
            expected: u64::from(expected_writer_epoch),
        });
    };
    let schema_version = non_negative_u64(row, "schema_version")?;
    if schema_version != u64::from(expected_schema_version) {
        return Err(SessionDbError::ProjectionIncompatible {
            projection: "session_storage_contract",
            actual: schema_version,
            expected: u64::from(expected_schema_version),
        });
    }
    let actual = non_negative_u64(row, "writer_epoch")?;
    let expected = u64::from(expected_writer_epoch);
    if actual != expected {
        return Err(SessionDbError::WriterIncompatible {
            actual: Some(actual),
            expected,
        });
    }
    Ok(())
}

fn 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())
}