global: snapshot

This commit is contained in:
nym21
2026-01-15 23:34:43 +01:00
parent b0d933a7ab
commit 967d2c7f35
67 changed files with 6854 additions and 5210 deletions

View File

@@ -3,12 +3,15 @@ use std::collections::BTreeMap;
use brk_error::{Error, Result};
use brk_traversable::TreeNode;
use brk_types::{
DetailedMetricCount, Format, Index, IndexInfo, Limit, Metric, MetricData, PaginatedMetrics,
Pagination, PaginationIndex,
DetailedMetricCount, Format, Index, IndexInfo, Limit, Metric, MetricData, MetricOutput,
MetricSelection, Output, PaginatedMetrics, Pagination, PaginationIndex,
};
use vecdb::AnyExportableVec;
use crate::{vecs::{IndexToVec, MetricToVec}, DataRangeFormat, MetricSelection, Output, Query};
use crate::{
Query, ResolvedQuery,
vecs::{IndexToVec, MetricToVec},
};
/// Estimated bytes per column header
const CSV_HEADER_BYTES_PER_COL: usize = 10;
@@ -33,19 +36,25 @@ impl Query {
// Metric doesn't exist, suggest alternatives
Error::MetricNotFound {
metric: metric.to_string(),
suggestion: self.match_metric(metric, Limit::MIN).first().map(|s| s.to_string()),
suggestion: self
.match_metric(metric, Limit::MIN)
.first()
.map(|s| s.to_string()),
}
}
pub(crate) fn columns_to_csv(
columns: &[&dyn AnyExportableVec],
from: Option<i64>,
to: Option<i64>,
start: usize,
end: usize,
) -> Result<String> {
if columns.is_empty() {
return Ok(String::new());
}
let from = Some(start as i64);
let to = Some(end as i64);
let num_rows = columns[0].range_count(from, to);
let num_cols = columns.len();
@@ -79,82 +88,6 @@ impl Query {
Ok(csv)
}
/// Format single metric - returns `MetricData`
pub fn format(
&self,
metric: &dyn AnyExportableVec,
params: &DataRangeFormat,
) -> Result<Output> {
let len = metric.len();
let from = params.start().map(|start| metric.i64_to_usize(start));
let to = params.end_for_len(len).map(|end| metric.i64_to_usize(end));
Ok(match params.format() {
Format::CSV => Output::CSV(Self::columns_to_csv(
&[metric],
from.map(|v| v as i64),
to.map(|v| v as i64),
)?),
Format::JSON => {
let mut buf = Vec::new();
MetricData::serialize(metric, from, to, &mut buf)?;
Output::Json(buf)
}
})
}
/// Format multiple metrics - returns `Vec<MetricData>`
pub fn format_bulk(
&self,
metrics: &[&dyn AnyExportableVec],
params: &DataRangeFormat,
) -> Result<Output> {
// Use min length across metrics for consistent count resolution
let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0);
let from = params.start().map(|start| {
metrics
.iter()
.map(|v| v.i64_to_usize(start))
.min()
.unwrap_or_default()
});
let to = params.end_for_len(min_len).map(|end| {
metrics
.iter()
.map(|v| v.i64_to_usize(end))
.min()
.unwrap_or_default()
});
let format = params.format();
Ok(match format {
Format::CSV => Output::CSV(Self::columns_to_csv(
metrics,
from.map(|v| v as i64),
to.map(|v| v as i64),
)?),
Format::JSON => {
if metrics.is_empty() {
return Ok(Output::default(format));
}
let mut buf = Vec::new();
buf.push(b'[');
for (i, vec) in metrics.iter().enumerate() {
if i > 0 {
buf.push(b',');
}
MetricData::serialize(*vec, from, to, &mut buf)?;
}
buf.push(b']');
Output::Json(buf)
}
})
}
/// Search for vecs matching the given metrics and index.
/// Returns error if no metrics requested or any requested metric is not found.
pub fn search(&self, params: &MetricSelection) -> Result<Vec<&'static dyn AnyExportableVec>> {
@@ -185,22 +118,34 @@ impl Query {
.sum()
}
/// Search and format single metric
pub fn search_and_format(&self, params: MetricSelection) -> Result<Output> {
self.search_and_format_checked(params, usize::MAX)
}
/// Search and format single metric with weight limit
pub fn search_and_format_checked(
/// Resolve query metadata without formatting (cheap).
/// Use with `format` for lazy formatting after ETag check.
pub fn resolve(
&self,
params: MetricSelection,
max_weight: usize,
) -> Result<Output> {
) -> Result<ResolvedQuery> {
let vecs = self.search(&params)?;
let metric = vecs.first().expect("search guarantees non-empty on success");
let total = vecs.iter().map(|v| v.len()).min().unwrap_or(0);
let version: u64 = vecs.iter().map(|v| u64::from(v.version())).sum();
let weight = Self::weight(&vecs, params.start(), params.end_for_len(metric.len()));
let start = params
.start()
.map(|s| vecs.iter().map(|v| v.i64_to_usize(s)).min().unwrap_or(0))
.unwrap_or(0);
let end = params
.end_for_len(total)
.map(|e| {
vecs.iter()
.map(|v| v.i64_to_usize(e))
.min()
.unwrap_or(total)
})
.unwrap_or(total);
let weight = Self::weight(&vecs, Some(start as i64), Some(end as i64));
if weight > max_weight {
return Err(Error::WeightExceeded {
requested: weight,
@@ -208,32 +153,57 @@ impl Query {
});
}
self.format(*metric, &params.range)
Ok(ResolvedQuery {
vecs,
format: params.format(),
version,
total,
start,
end,
})
}
/// Search and format bulk metrics
pub fn search_and_format_bulk(&self, params: MetricSelection) -> Result<Output> {
self.search_and_format_bulk_checked(params, usize::MAX)
}
/// Format a resolved query (expensive).
/// Call after ETag/cache checks to avoid unnecessary work.
pub fn format(&self, resolved: ResolvedQuery) -> Result<MetricOutput> {
let ResolvedQuery {
vecs,
format,
version,
total,
start,
end,
} = resolved;
/// Search and format bulk metrics with weight limit (for DDoS prevention)
pub fn search_and_format_bulk_checked(
&self,
params: MetricSelection,
max_weight: usize,
) -> Result<Output> {
let vecs = self.search(&params)?;
let output = match format {
Format::CSV => Output::CSV(Self::columns_to_csv(&vecs, start, end)?),
Format::JSON => {
if vecs.len() == 1 {
let mut buf = Vec::new();
MetricData::serialize(vecs[0], start, end, &mut buf)?;
Output::Json(buf)
} else {
let mut buf = Vec::new();
buf.push(b'[');
for (i, vec) in vecs.iter().enumerate() {
if i > 0 {
buf.push(b',');
}
MetricData::serialize(*vec, start, end, &mut buf)?;
}
buf.push(b']');
Output::Json(buf)
}
}
};
let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty");
let weight = Self::weight(&vecs, params.start(), params.end_for_len(min_len));
if weight > max_weight {
return Err(Error::WeightExceeded {
requested: weight,
max: max_weight,
});
}
self.format_bulk(&vecs, &params.range)
Ok(MetricOutput {
output,
version,
total,
start,
end,
})
}
pub fn metric_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> {

View File

@@ -1,73 +1,65 @@
use brk_error::{Error, Result};
use brk_types::Format;
use vecdb::AnyExportableVec;
use brk_error::Result;
use brk_types::{Format, LegacyValue, MetricOutputLegacy, OutputLegacy};
use crate::{DataRangeFormat, LegacyValue, MetricSelection, OutputLegacy, Query};
use crate::{Query, ResolvedQuery};
impl Query {
/// Deprecated - raw data without MetricData wrapper
pub fn format_legacy(&self, metrics: &[&dyn AnyExportableVec], params: &DataRangeFormat) -> Result<OutputLegacy> {
let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0);
/// Deprecated - format a resolved query as legacy output (expensive).
pub fn format_legacy(&self, resolved: ResolvedQuery) -> Result<MetricOutputLegacy> {
let ResolvedQuery {
vecs,
format,
version,
total,
start,
end,
} = resolved;
let from = params
.start()
.map(|start| metrics.iter().map(|v| v.i64_to_usize(start)).min().unwrap_or_default());
if vecs.is_empty() {
return Ok(MetricOutputLegacy {
output: OutputLegacy::default(format),
version: 0,
total: 0,
start: 0,
end: 0,
});
}
let to = params
.end_for_len(min_len)
.map(|end| metrics.iter().map(|v| v.i64_to_usize(end)).min().unwrap_or_default());
let from = Some(start as i64);
let to = Some(end as i64);
let format = params.format();
Ok(match format {
Format::CSV => OutputLegacy::CSV(Self::columns_to_csv(metrics, from.map(|v| v as i64), to.map(|v| v as i64))?),
let output = match format {
Format::CSV => OutputLegacy::CSV(Self::columns_to_csv(&vecs, start, end)?),
Format::JSON => {
if metrics.is_empty() {
return Ok(OutputLegacy::default(format));
}
if metrics.len() == 1 {
let metric = metrics[0];
let count = metric.range_count(from.map(|v| v as i64), to.map(|v| v as i64));
if vecs.len() == 1 {
let metric = vecs[0];
let count = metric.range_count(from, to);
let mut buf = Vec::new();
if count == 1 {
metric.write_json_value(from, &mut buf)?;
metric.write_json_value(Some(start), &mut buf)?;
OutputLegacy::Json(LegacyValue::Value(buf))
} else {
metric.write_json(from, to, &mut buf)?;
metric.write_json(Some(start), Some(end), &mut buf)?;
OutputLegacy::Json(LegacyValue::List(buf))
}
} else {
let mut values = Vec::with_capacity(metrics.len());
for vec in metrics {
let mut values = Vec::with_capacity(vecs.len());
for vec in &vecs {
let mut buf = Vec::new();
vec.write_json(from, to, &mut buf)?;
vec.write_json(Some(start), Some(end), &mut buf)?;
values.push(buf);
}
OutputLegacy::Json(LegacyValue::Matrix(values))
}
}
};
Ok(MetricOutputLegacy {
output,
version,
total,
start,
end,
})
}
/// Deprecated - use search_and_format instead
pub fn search_and_format_legacy(&self, params: MetricSelection) -> Result<OutputLegacy> {
self.search_and_format_legacy_checked(params, usize::MAX)
}
/// Deprecated - use search_and_format_checked instead
pub fn search_and_format_legacy_checked(&self, params: MetricSelection, max_weight: usize) -> Result<OutputLegacy> {
let vecs = self.search(&params)?;
let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty");
let weight = Self::weight(&vecs, params.start(), params.end_for_len(min_len));
if weight > max_weight {
return Err(Error::WeightExceeded {
requested: weight,
max: max_weight,
});
}
self.format_legacy(&vecs, &params.range)
}
}