use serde::{Deserialize, Serialize};
use rete_core::{eval_query, CountingReader, QueryOutput, RangeReader, Rete};
use super::card::ExampleQuery;
pub(crate) const BUILD_INFO_SCHEMA: u8 = 1;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct BuildInfo {
#[serde(default)]
pub schema: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub built_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub builder: Option<String>,
#[serde(default, skip_serializing_if = "BuildParams::is_empty")]
pub params: BuildParams,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub query_costs: Option<QueryCosts>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dropped_queries: Vec<DroppedQuery>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct BuildParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub no_pyramid: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub text_index: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub materialize: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub reason: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pyramid_algo: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_budget_mb: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub codec: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub card_top_n: Option<u32>,
}
impl BuildParams {
fn is_empty(&self) -> bool {
*self == BuildParams::default()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct QueryCosts {
pub context: CostContext,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub queries: Vec<QueryCost>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct CostContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct QueryCost {
pub id: String,
pub bytes: u64,
pub requests: u64,
pub rows: u64,
pub debug_ms: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct DroppedQuery {
pub id: String,
pub why: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub contradicts_claim: bool,
}
impl BuildInfo {
pub(crate) fn to_json_bytes(&self) -> Vec<u8> {
serde_json::to_vec(self).expect("BuildInfo serializes")
}
pub(crate) fn from_json_bytes(b: &[u8]) -> anyhow::Result<Self> {
serde_json::from_slice(b).map_err(|e| anyhow::anyhow!("malformed build info: {e}"))
}
}
pub(crate) fn builder_version() -> String {
format!("rete-cli {}", env!("CARGO_PKG_VERSION"))
}
pub(crate) fn codec_name(codec: u8) -> &'static str {
match codec {
rete_core::CODEC_ZSTD => "zstd",
rete_core::CODEC_NONE => "none",
_ => "unknown",
}
}
pub(crate) fn new_build_info(params: BuildParams) -> BuildInfo {
BuildInfo {
schema: BUILD_INFO_SCHEMA,
built_at: Some(now_rfc3339()),
builder: Some(builder_version()),
params,
query_costs: None,
dropped_queries: Vec::new(),
}
}
pub(crate) fn now_rfc3339() -> String {
let secs = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
});
epoch_to_rfc3339(secs)
}
fn epoch_to_rfc3339(secs: i64) -> String {
let days = secs.div_euclid(86_400);
let rem = secs.rem_euclid(86_400);
let (h, m, s) = (rem / 3600, (rem / 60) % 60, rem % 60);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if mo <= 2 { y + 1 } else { y };
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}
struct ImageReader(std::sync::Arc<Vec<u8>>);
impl RangeReader for ImageReader {
fn len(&self) -> u64 {
self.0.len() as u64
}
fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
let start = offset as usize;
let end = start
.checked_add(len as usize)
.filter(|&e| e <= self.0.len())
.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "range out of bounds")
})?;
Ok(self.0[start..end].to_vec())
}
}
pub(crate) struct BudgetReader<R> {
inner: R,
budget: u64,
spent: std::sync::atomic::AtomicU64,
}
impl<R: RangeReader> BudgetReader<R> {
pub(crate) fn new(inner: R, budget: u64) -> Self {
Self {
inner,
budget,
spent: std::sync::atomic::AtomicU64::new(0),
}
}
}
impl<R: RangeReader> RangeReader for BudgetReader<R> {
fn len(&self) -> u64 {
self.inner.len()
}
fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
let spent = self
.spent
.fetch_add(len, std::sync::atomic::Ordering::Relaxed)
+ len;
if spent > self.budget {
return Err(std::io::Error::other(format!(
"byte budget exhausted: this query wanted more than {} bytes",
self.budget
)));
}
self.inner.read_at(offset, len)
}
fn read_many(&self, ranges: &[(u64, u64)]) -> std::io::Result<Vec<Vec<u8>>> {
let want: u64 = ranges.iter().map(|&(_, l)| l).sum();
let spent = self
.spent
.fetch_add(want, std::sync::atomic::Ordering::Relaxed)
+ want;
if spent > self.budget {
return Err(std::io::Error::other(format!(
"byte budget exhausted: this query wanted more than {} bytes",
self.budget
)));
}
self.inner.read_many(ranges)
}
fn concurrency(&self) -> usize {
self.inner.concurrency()
}
}
pub(crate) struct Measured {
pub cost: QueryCost,
pub error: Option<String>,
pub useless: Option<String>,
pub vacuous: bool,
}
struct Graded {
rows: u64,
error: Option<String>,
useless: Option<String>,
vacuous: bool,
}
impl Graded {
fn failed(why: String) -> Self {
Self {
rows: 0,
error: Some(why.clone()),
useless: Some(why),
vacuous: false,
}
}
}
fn grade(out: &QueryOutput) -> Graded {
let answers = |rows: u64| Graded {
rows,
error: None,
useless: None,
vacuous: false,
};
let empty = |why: &str| Graded {
rows: 0,
error: None,
useless: Some(why.to_string()),
vacuous: false,
};
match out {
QueryOutput::Select(_, rows) if rows.is_empty() => empty("measured 0 rows on the file"),
QueryOutput::Select(_, rows) if rows.iter().all(|b| b.is_empty()) => Graded {
rows: rows.len() as u64,
error: None,
useless: Some(format!(
"measured {} row(s) binding no variable at all — an aggregate over an empty \
solution sequence, which is a row without an answer in it",
rows.len()
)),
vacuous: true,
},
QueryOutput::Select(_, rows) => answers(rows.len() as u64),
QueryOutput::Ask(false) => empty("measured ASK false on the file"),
QueryOutput::Ask(true) => answers(1),
QueryOutput::Construct(ts) if ts.is_empty() => empty("constructed no triples on the file"),
QueryOutput::Construct(ts) => answers(ts.len() as u64),
other => Graded::failed(format!("returned an unexpected result form: {other:?}")),
}
}
fn measure_one<R: RangeReader + Send + Sync + 'static>(
reader: std::sync::Arc<CountingReader<R>>,
q: &ExampleQuery,
) -> Measured {
let start = std::time::Instant::now();
let graded = match Rete::open_ranged_lazy(reader.clone()) {
Ok(rete) => match eval_query(&rete, &q.sparql) {
Ok(result) => grade(&result),
Err(e) => Graded::failed(format!("failed to run: {e}")),
},
Err(e) => Graded::failed(format!("the file did not open: {e}")),
};
Measured {
cost: QueryCost {
id: q.id.clone(),
bytes: reader.bytes_read(),
requests: reader.requests(),
rows: graded.rows,
debug_ms: start.elapsed().as_millis() as u64,
},
error: graded.error,
useless: graded.useless,
vacuous: graded.vacuous,
}
}
pub(crate) fn measure_query<R, F>(open: F, q: &ExampleQuery) -> anyhow::Result<Measured>
where
R: RangeReader + Send + Sync + 'static,
F: FnOnce() -> anyhow::Result<R>,
{
Ok(measure_one(
std::sync::Arc::new(CountingReader::new(open()?)),
q,
))
}
pub(crate) fn measure_query_costs(
image: std::sync::Arc<Vec<u8>>,
queries: &[ExampleQuery],
) -> Vec<Measured> {
queries
.iter()
.map(|q| {
measure_one(
std::sync::Arc::new(CountingReader::new(ImageReader(image.clone()))),
q,
)
})
.collect()
}
pub(crate) fn query_costs(queries: Vec<QueryCost>) -> QueryCosts {
QueryCosts {
context: cost_context(IN_MEMORY_TRANSPORT),
queries,
}
}
pub(crate) const IN_MEMORY_TRANSPORT: &str =
"local in-memory image; cold lazy open per query; logical range reads, no block cache";
pub(crate) const COST_NOTE: &str =
"bytes/requests are properties of file layout + query (portable); debug_ms is one machine's \
reference timing, not a guarantee";
pub(crate) fn cost_context(transport: &str) -> CostContext {
CostContext {
engine: Some(builder_version()),
transport: Some(transport.to_string()),
note: Some(COST_NOTE.to_string()),
}
}
const REWRITE_CHUNK: usize = 4 << 20;
pub(crate) fn write_build_info_streaming(
path: &std::path::Path,
info: &[u8],
) -> anyhow::Result<u64> {
use std::io::{Read, Seek, SeekFrom, Write};
let mut src = std::fs::File::open(path)?;
let file_len = src.metadata()?.len();
let mut head = vec![0u8; rete_core::format::HEADER_LEN];
src.read_exact(&mut head)?;
let before = rete_core::format::Header::from_bytes(&head)?;
let plan = rete_core::plan_build_info(&head, file_len, info.len() as u64)?;
let tmp = path.with_extension("rete.costs-tmp");
{
let mut out = std::io::BufWriter::new(std::fs::File::create(&tmp)?);
out.write_all(&plan.header)?;
let mut copy = |from: u64, to: u64, out: &mut dyn Write| -> anyhow::Result<()> {
src.seek(SeekFrom::Start(from))?;
let mut left = to - from;
let mut buf = vec![0u8; REWRITE_CHUNK.min(left.max(1) as usize)];
while left > 0 {
let n = buf.len().min(left as usize);
src.read_exact(&mut buf[..n])?;
out.write_all(&buf[..n])?;
left -= n as u64;
}
Ok(())
};
copy(rete_core::format::HEADER_LEN as u64, plan.insert, &mut out)?;
out.write_all(info)?;
copy(plan.tail_start, file_len, &mut out)?;
out.flush()?;
out.into_inner()
.map_err(|e| anyhow::anyhow!("flushing {}: {e}", tmp.display()))?
.sync_all()?;
}
drop(src);
let mut written_head = vec![0u8; rete_core::format::HEADER_LEN];
std::fs::File::open(&tmp)?.read_exact(&mut written_head)?;
let after = rete_core::format::Header::from_bytes(&written_head)
.map_err(|e| anyhow::anyhow!("the rewritten file has no readable header: {e}"))?;
if after.content_hash != before.content_hash {
let _ = std::fs::remove_file(&tmp);
anyhow::bail!("refusing to commit: the rewrite changed the content hash");
}
if after.build_info_len != info.len() as u64 {
let _ = std::fs::remove_file(&tmp);
anyhow::bail!("refusing to commit: the rewritten build-info section is the wrong length");
}
std::fs::rename(&tmp, path)?;
Ok(plan.new_len)
}
pub(crate) fn format_build_info(info: &BuildInfo) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(out, " build:");
let opt = |out: &mut String, label: &str, v: &Option<String>| {
if let Some(v) = v {
let _ = writeln!(out, " {label:<11}: {v}");
}
};
opt(&mut out, "built at", &info.built_at);
opt(&mut out, "builder", &info.builder);
let p = &info.params;
if !p.is_empty() {
let mut flags: Vec<String> = Vec::new();
if let Some(c) = &p.command {
flags.push(c.clone());
}
if let Some(f) = &p.format {
flags.push(format!("--format {f}"));
}
if p.no_pyramid {
flags.push("--no-pyramid".into());
}
if p.text_index {
flags.push("--text-index".into());
}
if p.materialize {
flags.push("--materialize".into());
}
if p.reason {
flags.push("--reason".into());
}
if let Some(a) = &p.pyramid_algo {
flags.push(format!("--pyramid-algo {a}"));
}
if let Some(mb) = p.memory_budget_mb {
flags.push(format!("--memory-budget-mb {mb}"));
}
let _ = writeln!(out, " params : {}", flags.join(" "));
if let Some(c) = &p.codec {
let _ = writeln!(out, " codec : {c}");
}
if let Some(n) = p.card_top_n {
let _ = writeln!(out, " card top-N : {n}");
}
}
if let Some(costs) = &info.query_costs {
let _ = writeln!(
out,
" query costs (cold; bytes/requests portable, ms = one machine's debug ref):"
);
if let Some(t) = &costs.context.transport {
let _ = writeln!(out, " measured over: {t}");
}
for c in &costs.queries {
let _ = writeln!(
out,
" {:<14} {:>9} B in {:>2} req · {:>5} row(s) · {} ms",
c.id, c.bytes, c.requests, c.rows, c.debug_ms
);
}
}
if !info.dropped_queries.is_empty() {
let _ = writeln!(
out,
" starter queries generated, measured, and NOT shipped:"
);
for d in &info.dropped_queries {
let _ = writeln!(
out,
" {:<14} {}{}",
d.id,
d.why,
if d.contradicts_claim {
" [generator defect: its template claimed this could not happen]"
} else {
""
}
);
}
}
while out.ends_with('\n') {
out.pop();
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc3339_formats_known_epochs() {
assert_eq!(epoch_to_rfc3339(0), "1970-01-01T00:00:00Z");
assert_eq!(epoch_to_rfc3339(951_782_400), "2000-02-29T00:00:00Z");
assert_eq!(epoch_to_rfc3339(1_785_801_600), "2026-08-04T00:00:00Z");
assert_eq!(epoch_to_rfc3339(86_399), "1970-01-01T23:59:59Z");
}
#[test]
fn build_info_round_trips_and_omits_absent_fields() {
let info = new_build_info(BuildParams {
command: Some("build".into()),
no_pyramid: true,
codec: Some(codec_name(rete_core::CODEC_ZSTD).into()),
card_top_n: Some(100),
..Default::default()
});
let bytes = info.to_json_bytes();
let back = BuildInfo::from_json_bytes(&bytes).unwrap();
assert_eq!(info, back);
let text = String::from_utf8(bytes).unwrap();
assert!(text.contains("built_at"));
assert!(text.contains("rete-cli"));
assert!(text.contains("no_pyramid"));
assert!(!text.contains("memory_budget_mb"));
assert!(!text.contains("query_costs"));
assert!(!text.contains("text_index"));
}
#[test]
fn empty_build_info_parses_from_empty_object() {
let b = BuildInfo::from_json_bytes(b"{}").unwrap();
assert!(b.built_at.is_none() && b.builder.is_none());
assert!(b.params.is_empty());
}
#[test]
fn every_result_shape_grades_the_same_way_for_both_callers() {
use rete_core::Binding;
let unbound = Binding::default();
let bound = Binding::from([("s".to_string(), "http://x/a".to_string())]);
let vars = vec!["s".to_string()];
let triple = || ("s".to_string(), "p".to_string(), "o".to_string());
let cases = [
(
QueryOutput::Select(vars.clone(), vec![]),
0,
true,
false,
false,
),
(
QueryOutput::Select(vars.clone(), vec![unbound.clone()]),
1,
true,
true,
false,
),
(
QueryOutput::Select(vars.clone(), vec![bound.clone(), unbound.clone()]),
2,
false,
false,
false,
),
(
QueryOutput::Select(vars.clone(), vec![bound]),
1,
false,
false,
false,
),
(QueryOutput::Ask(false), 0, true, false, false),
(QueryOutput::Ask(true), 1, false, false, false),
(QueryOutput::Construct(vec![]), 0, true, false, false),
(
QueryOutput::Construct(vec![triple()]),
1,
false,
false,
false,
),
];
for (out, rows, useless, vacuous, error) in cases {
let g = grade(&out);
let what = format!("{out:?}");
assert_eq!(g.rows, rows, "rows for {what}");
assert_eq!(g.useless.is_some(), useless, "useless for {what}");
assert_eq!(g.vacuous, vacuous, "vacuous for {what}");
assert_eq!(g.error.is_some(), error, "error for {what}");
assert_eq!(
g.useless.is_some() && !g.vacuous,
useless && !vacuous,
"zero-row emptiness for {what}"
);
}
let f = Graded::failed("the file did not open: nope".into());
assert_eq!(f.rows, 0);
assert_eq!(f.error.as_deref(), f.useless.as_deref());
assert!(
!f.vacuous,
"a failure is a zero-row emptiness, not a vacuous one"
);
}
}