use chrono::{DateTime, Utc};
use sqlmodel_core::Value;
use crate::db::{DbConnection, StoredMemory};
use crate::models::DomainError;
use super::{
ASK_MEMORY_REVISION_PAGE_SIZE, corpus_storage_error, validity_contains, withheld_memory_ids,
};
pub(super) fn load_memory_revisions(
connection: &DbConnection,
workspace_id: &str,
reference_time: DateTime<Utc>,
) -> Result<Vec<StoredMemory>, DomainError> {
load_with_hydration_observer(connection, workspace_id, reference_time, |_| {})
}
pub(super) fn load_command_advice_revisions(
connection: &DbConnection,
workspace_id: &str,
reference_time: DateTime<Utc>,
) -> Result<Vec<StoredMemory>, DomainError> {
load_selected_revisions(
connection,
workspace_id,
reference_time,
"SELECT id, valid_from, valid_to FROM memories WHERE workspace_id = ?1 AND tombstoned_at IS NULL AND kind IN ('risk', 'anti-pattern', 'failure', 'rule') ORDER BY id ASC",
|_| {},
)
}
fn load_with_hydration_observer(
connection: &DbConnection,
workspace_id: &str,
reference_time: DateTime<Utc>,
before_hydration: impl FnMut(&[&str]),
) -> Result<Vec<StoredMemory>, DomainError> {
load_selected_revisions(
connection,
workspace_id,
reference_time,
"SELECT id, valid_from, valid_to FROM memories WHERE workspace_id = ?1 AND tombstoned_at IS NULL ORDER BY id ASC",
before_hydration,
)
}
fn load_selected_revisions(
connection: &DbConnection,
workspace_id: &str,
reference_time: DateTime<Utc>,
selection_sql: &str,
mut before_hydration: impl FnMut(&[&str]),
) -> Result<Vec<StoredMemory>, DomainError> {
let rows = connection
.query(selection_sql, &[Value::Text(workspace_id.to_owned())])
.map_err(|_| corpus_storage_error())?;
if rows.is_empty() {
return Ok(Vec::new());
}
let withheld = withheld_memory_ids(connection, workspace_id, reference_time)?;
let mut admitted = Vec::new();
for row in &rows {
let id = match row.get(0) {
Some(Value::Text(id)) => id.as_str(),
_ => return Err(corpus_storage_error()),
};
let valid_from = optional_timestamp(row.get(1))?;
let valid_to = optional_timestamp(row.get(2))?;
if validity_contains(valid_from, valid_to, reference_time)? && !withheld.contains(id) {
admitted.push(id);
}
}
let mut memories = Vec::with_capacity(admitted.len());
for page in admitted.chunks(ASK_MEMORY_REVISION_PAGE_SIZE) {
before_hydration(page);
let mut loaded = connection
.get_memories_batch(page)
.map_err(|_| corpus_storage_error())?;
for id in page {
let memory = loaded.remove(*id).ok_or_else(corpus_storage_error)?;
if memory.id != *id
|| memory.workspace_id != workspace_id
|| memory.tombstoned_at.is_some()
|| !validity_contains(
memory.valid_from.as_deref(),
memory.valid_to.as_deref(),
reference_time,
)?
{
return Err(corpus_storage_error());
}
memories.push(memory);
}
}
Ok(memories)
}
fn optional_timestamp(value: Option<&Value>) -> Result<Option<&str>, DomainError> {
match value {
Some(Value::Null) => Ok(None),
Some(Value::Text(value)) => Ok(Some(value.as_str())),
_ => Err(corpus_storage_error()),
}
}
#[cfg(test)]
#[path = "ask_memory_admission_tests.rs"]
mod tests;