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

View File

@@ -13,20 +13,15 @@ use vecdb::AnyStoredVec;
#[cfg(feature = "tokio")]
mod r#async;
mod output;
mod resolved;
mod vecs;
mod r#impl;
#[cfg(feature = "tokio")]
pub use r#async::*;
pub use brk_types::{
DataRange, DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics,
Pagination, PaginationIndex,
};
pub use r#impl::BLOCK_TXS_PAGE_SIZE;
pub use output::{LegacyValue, Output, OutputLegacy};
use resolved::ResolvedQuery;
pub use vecs::Vecs;
#[derive(Clone)]

View File

@@ -1,78 +0,0 @@
use brk_types::Format;
/// New format with MetricData metadata wrapper
#[derive(Debug)]
pub enum Output {
Json(Vec<u8>),
CSV(String),
}
impl Output {
#[allow(clippy::inherent_to_string)]
pub fn to_string(self) -> String {
match self {
Output::CSV(s) => s,
Output::Json(v) => unsafe { String::from_utf8_unchecked(v) },
}
}
pub fn default(format: Format) -> Self {
match format {
Format::CSV => Output::CSV(String::new()),
Format::JSON => Output::Json(br#"{"len":0,"from":0,"to":0,"data":[]}"#.to_vec()),
}
}
}
/// Deprecated: Raw JSON without metadata wrapper
#[derive(Debug)]
pub enum OutputLegacy {
Json(LegacyValue),
CSV(String),
}
impl OutputLegacy {
#[allow(clippy::inherent_to_string)]
pub fn to_string(self) -> String {
match self {
OutputLegacy::CSV(s) => s,
OutputLegacy::Json(v) => unsafe { String::from_utf8_unchecked(v.to_vec()) },
}
}
pub fn default(format: Format) -> Self {
match format {
Format::CSV => OutputLegacy::CSV(String::new()),
Format::JSON => OutputLegacy::Json(LegacyValue::List(b"[]".to_vec())),
}
}
}
/// Deprecated: Raw JSON without metadata wrapper.
#[derive(Debug)]
pub enum LegacyValue {
Matrix(Vec<Vec<u8>>),
List(Vec<u8>),
Value(Vec<u8>),
}
impl LegacyValue {
pub fn to_vec(self) -> Vec<u8> {
match self {
LegacyValue::Value(v) | LegacyValue::List(v) => v,
LegacyValue::Matrix(m) => {
let total_size = m.iter().map(|v| v.len()).sum::<usize>() + m.len() + 1;
let mut buf = Vec::with_capacity(total_size);
buf.push(b'[');
for (i, vec) in m.into_iter().enumerate() {
if i > 0 {
buf.push(b',');
}
buf.extend(vec);
}
buf.push(b']');
buf
}
}
}
}

View File

@@ -0,0 +1,23 @@
use brk_types::{Etag, Format};
use vecdb::AnyExportableVec;
/// A resolved metric query ready for formatting.
/// Contains the vecs and metadata needed to build an ETag or format the output.
pub struct ResolvedQuery {
pub(crate) vecs: Vec<&'static dyn AnyExportableVec>,
pub(crate) format: Format,
pub(crate) version: u64,
pub(crate) total: usize,
pub(crate) start: usize,
pub(crate) end: usize,
}
impl ResolvedQuery {
pub fn etag(&self) -> Etag {
Etag::from_metric(self.version, self.total, self.start, self.end)
}
pub fn format(&self) -> Format {
self.format
}
}