use super::{Node, Scope, catalog, model};
use crate::{
store::{self, RequestView, RunSummary, RunView, reader::Reader},
types::RunId,
};
use std::{path::PathBuf, sync::Arc};
use time::OffsetDateTime;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page {
pub scope: Scope,
pub open: Option<RunId>,
pub offset: u32,
pub tail: bool,
pub limit: u32,
}
pub fn older_offset(offset: u32, limit: u32, shown: u32) -> Option<u32> {
(shown >= limit).then(|| offset.saturating_add(limit))
}
pub fn newer_offset(offset: u32, limit: u32) -> Option<u32> {
(offset > 0).then(|| offset.saturating_sub(limit))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Selection {
pub state: PathBuf,
pub page: Page,
pub revision: u64,
}
pub struct Input {
pub state: PathBuf,
pub initial: Option<PathBuf>,
pub catalog: catalog::Catalog,
pub initialized: bool,
pub page: Page,
pub full: bool,
pub displayed: Option<RunId>,
pub runs: Option<Vec<RunSummary>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Origin {
identity: Option<crate::types::StateId>,
database: Option<crate::platform::FileIdentity>,
}
impl Origin {
pub fn replaces(&self, previous: &Self) -> bool {
previous.database.is_some() && previous.database != self.database
|| previous.identity.is_some() && previous.identity != self.identity
}
pub fn identity(&self) -> Option<&crate::types::StateId> {
self.identity.as_ref()
}
pub fn read(state: &std::path::Path) -> Result<Self, String> {
let path = state.join(store::DATABASE);
let database =
match crate::platform::open_no_follow(std::fs::File::options().read(true), &path) {
Ok(file) => Some(crate::platform::file_identity(&file).map_err(|e| e.to_string())?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => return Err(error.to_string()),
};
Ok(Self {
identity: store::read_state_id(state)?,
database,
})
}
}
pub struct Snapshot {
pub origin: Origin,
pub catalog: catalog::Catalog,
pub scope: Scope,
pub runs: Vec<RunSummary>,
pub selected_id: Option<RunId>,
pub selected: Option<(RunView, Vec<RequestView>, Arc<Vec<Node>>)>,
}
#[derive(Default)]
pub struct Loader {
reader: Reader,
reset_git: bool,
}
impl Loader {
#[cfg(any(test, feature = "test-support"))]
pub fn traced_statements(&self) -> Vec<String> {
self.reader.traced_statements()
}
pub fn load(&mut self, mut input: Input) -> Result<Snapshot, String> {
if self.reset_git {
input.catalog.reset_git();
}
let result = load(&mut self.reader, input);
if result.is_ok() {
self.reset_git = false;
}
result
}
pub fn reset(&mut self) {
self.reader.reset();
self.reset_git = true;
}
}
pub struct Completed {
pub selection: Selection,
pub loader: Loader,
pub result: Result<Snapshot, String>,
#[cfg(any(test, feature = "test-support"))]
pub queries: Vec<String>,
}
impl Completed {
pub fn run(selection: Selection, mut loader: Loader, input: Input) -> Self {
#[cfg(any(test, feature = "test-support"))]
loader.traced_statements();
let result = loader.load(input);
#[cfg(any(test, feature = "test-support"))]
let queries = loader.traced_statements();
Self {
selection,
loader,
result,
#[cfg(any(test, feature = "test-support"))]
queries,
}
}
}
fn load(reader: &mut Reader, input: Input) -> Result<Snapshot, String> {
let mut catalog = input.catalog;
let mut scope = input.page.scope.clone();
if !input
.state
.join(store::DATABASE)
.try_exists()
.map_err(|error| error.to_string())?
{
store::check_probe_files(&input.state)?;
catalog.update(&[], input.initial.as_deref());
if !input.initialized {
scope = catalog.initial(input.initial.as_deref());
}
reader.reset();
return Ok(Snapshot {
origin: Origin::default(),
catalog,
scope,
runs: Vec::new(),
selected_id: None,
selected: None,
});
}
let saved = reader.snapshot_optional(&input.state, |db, cache, state| {
let reuse_page = input.runs.is_some()
&& !input.full
&& input.initialized
&& cache.has_catalog()
&& !cache.changed();
if input.initialized {
catalog.selected = Some(scope.clone());
}
if input.full || !input.initialized || !cache.has_catalog() || cache.changed() {
let facts = cache.catalog(db, state)?;
if input.initialized {
catalog.selected = Some(scope.clone());
}
catalog.update(&facts, input.initial.as_deref());
if !input.initialized {
scope = catalog.initial(input.initial.as_deref());
catalog.selected = Some(scope.clone());
catalog.update(&facts, input.initial.as_deref());
}
}
if !input.full && input.initialized {
catalog.rebuild();
}
let paths = catalog.paths(&scope);
let runs = if reuse_page {
input.runs.clone().expect("accepted same-database page")
} else {
cache.page(
db,
state,
paths.as_deref(),
input.page.limit,
input.page.offset,
input.page.open.as_ref(),
)?
};
let selected = input
.page
.open
.as_ref()
.and_then(|id| runs.iter().find(|run| &run.id == id))
.or_else(|| {
if input.page.tail {
runs.iter().take(input.page.limit as usize).next_back()
} else {
runs.first()
}
});
let selected_id = selected.map(|run| run.id.clone());
let selected = match selected.filter(|run| run.readable()) {
Some(run) => cache.selected(
db,
state,
&run.id,
Some(run.unreadable.clone()),
input.displayed.as_ref() != Some(&run.id),
)?,
None => None,
};
Ok((cache.identity(db)?, runs, selected_id, selected))
})?;
let Some((identity, runs, selected_id, selected)) = saved else {
catalog.update(&[], input.initial.as_deref());
if !input.initialized {
scope = catalog.initial(input.initial.as_deref());
}
return Ok(Snapshot {
origin: Origin {
identity: None,
database: reader.database(),
},
catalog,
scope,
runs: Vec::new(),
selected_id: None,
selected: None,
});
};
let selected = selected.map(|(run, requests)| {
let nodes = Arc::new(model::tree(&run, &requests, OffsetDateTime::now_utc()));
(run, requests, nodes)
});
reader.require_current(&input.state)?;
Ok(Snapshot {
origin: Origin {
identity,
database: reader.database(),
},
catalog,
scope,
runs,
selected_id,
selected,
})
}