use std::{marker::PhantomData, sync::Arc};
use brk_error::Result;
use brk_traversable::{Index, SeriesLeaf, SeriesLeafWithSchema, Traversable, TreeNode};
use brk_types::{
Date, Day1, Day3, Epoch, Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1,
Month3, Month6, Timestamp, Version, Week1, Year1, Year10,
};
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::{
AnyExportableVec, AnyVec, Database, EagerVec, Formattable, ImportableVec, LazyVec, PcoVec,
PcoVecValue, ReadableBoxedVec, ReadableCloneableVec, ReadableVec, Rw, StorageMode, TypedVec,
UnaryTransform, VecIndex, VecValue, short_type_name,
};
use crate::indexes;
type StoredDay<T, M> = <M as StorageMode>::Stored<EagerVec<PcoVec<Day1, T>>>;
type DayMapping<I, T> = LazyVec<I, Day1, I, T>;
type Repeated<I, T> = DailyView<I, T, RepeatDay>;
type Last<I, T> = DailyView<I, T, LastDay>;
pub trait DailyValue: VecValue + Formattable + JsonSchema + Serialize {}
impl<T> DailyValue for T where T: VecValue + Formattable + JsonSchema + Serialize {}
pub struct RepeatDay;
pub struct LastDay;
#[derive(Clone)]
pub(crate) struct DailyMappings {
height: DayMapping<Height, Day1>,
minute10: DayMapping<Minute10, Timestamp>,
minute30: DayMapping<Minute30, Timestamp>,
hour1: DayMapping<Hour1, Timestamp>,
hour4: DayMapping<Hour4, Timestamp>,
hour12: DayMapping<Hour12, Timestamp>,
day3: DayMapping<Day3, Date>,
week1: DayMapping<Week1, Date>,
month1: DayMapping<Month1, Date>,
month3: DayMapping<Month3, Date>,
month6: DayMapping<Month6, Date>,
year1: DayMapping<Year1, Date>,
year10: DayMapping<Year10, Date>,
halving: DayMapping<Halving, Timestamp>,
epoch: DayMapping<Epoch, Timestamp>,
}
impl DailyMappings {
pub(crate) fn new(indexes: &indexes::Vecs) -> Self {
let height = LazyVec::init(
"day1",
Version::ZERO,
indexes.height.day1_read_only_boxed_clone(),
|_, day| day,
);
Self {
height,
minute10: timestamp_mapping(indexes.timestamp.minute10.read_only_boxed_clone()),
minute30: timestamp_mapping(indexes.timestamp.minute30.read_only_boxed_clone()),
hour1: timestamp_mapping(indexes.timestamp.hour1.read_only_boxed_clone()),
hour4: timestamp_mapping(indexes.timestamp.hour4.read_only_boxed_clone()),
hour12: timestamp_mapping(indexes.timestamp.hour12.read_only_boxed_clone()),
day3: date_mapping(indexes.day3.date.read_only_boxed_clone()),
week1: date_mapping(indexes.week1.date.read_only_boxed_clone()),
month1: date_mapping(indexes.month1.date.read_only_boxed_clone()),
month3: date_mapping(indexes.month3.date.read_only_boxed_clone()),
month6: date_mapping(indexes.month6.date.read_only_boxed_clone()),
year1: date_mapping(indexes.year1.date.read_only_boxed_clone()),
year10: date_mapping(indexes.year10.date.read_only_boxed_clone()),
halving: timestamp_mapping(indexes.timestamp.halving.read_only_boxed_clone()),
epoch: timestamp_mapping(indexes.timestamp.epoch.read_only_boxed_clone()),
}
}
}
fn timestamp_mapping<I: VecIndex>(
source: ReadableBoxedVec<I, Timestamp>,
) -> DayMapping<I, Timestamp> {
LazyVec::init("day1", Version::ZERO, source, |_, timestamp| {
Day1::try_from(Date::from(timestamp)).unwrap_or_default()
})
}
fn date_mapping<I: VecIndex>(source: ReadableBoxedVec<I, Date>) -> DayMapping<I, Date> {
LazyVec::init("day1", Version::ZERO, source, |_, date| {
Day1::try_from(date).unwrap_or_default()
})
}
#[derive(Clone, Traversable)]
#[traversable(merge)]
pub struct DailyViews<T>
where
T: DailyValue,
{
pub height: Repeated<Height, T>,
pub minute10: Repeated<Minute10, T>,
pub minute30: Repeated<Minute30, T>,
pub hour1: Repeated<Hour1, T>,
pub hour4: Repeated<Hour4, T>,
pub hour12: Repeated<Hour12, T>,
pub day3: Last<Day3, T>,
pub week1: Last<Week1, T>,
pub month1: Last<Month1, T>,
pub month3: Last<Month3, T>,
pub month6: Last<Month6, T>,
pub year1: Last<Year1, T>,
pub year10: Last<Year10, T>,
pub halving: Last<Halving, T>,
pub epoch: Last<Epoch, T>,
}
impl<T> DailyViews<T>
where
T: DailyValue,
{
pub(crate) fn new(
name: &str,
source: ReadableBoxedVec<Day1, T>,
version: Version,
mappings: &DailyMappings,
) -> Self {
Self {
height: repeated(name, source.clone(), version, &mappings.height),
minute10: repeated(name, source.clone(), version, &mappings.minute10),
minute30: repeated(name, source.clone(), version, &mappings.minute30),
hour1: repeated(name, source.clone(), version, &mappings.hour1),
hour4: repeated(name, source.clone(), version, &mappings.hour4),
hour12: repeated(name, source.clone(), version, &mappings.hour12),
day3: last(name, source.clone(), version, &mappings.day3),
week1: last(name, source.clone(), version, &mappings.week1),
month1: last(name, source.clone(), version, &mappings.month1),
month3: last(name, source.clone(), version, &mappings.month3),
month6: last(name, source.clone(), version, &mappings.month6),
year1: last(name, source.clone(), version, &mappings.year1),
year10: last(name, source.clone(), version, &mappings.year10),
halving: last(name, source.clone(), version, &mappings.halving),
epoch: last(name, source, version, &mappings.epoch),
}
}
}
#[derive(Traversable)]
#[traversable(merge)]
pub struct DailyMetric<T, M: StorageMode = Rw>
where
T: DailyValue + PcoVecValue,
{
pub day1: StoredDay<T, M>,
#[traversable(flatten)]
pub views: Box<DailyViews<T>>,
}
impl<T> DailyMetric<T>
where
T: DailyValue + PcoVecValue,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
mappings: &DailyMappings,
) -> Result<Self> {
let day1 = EagerVec::forced_import(db, name, version)?;
let source = day1.read_only_boxed_clone();
let views = Box::new(DailyViews::new(name, source, version, mappings));
Ok(Self { day1, views })
}
}
type LazyDay<T, S> = LazyVec<Day1, T, Day1, S>;
#[derive(Clone, Traversable)]
#[traversable(merge)]
pub struct LazyDailyMetric<T, S>
where
T: DailyValue,
S: VecValue,
{
pub day1: LazyDay<T, S>,
#[traversable(flatten)]
pub views: Box<DailyViews<T>>,
}
impl<T, S> LazyDailyMetric<T, S>
where
T: DailyValue,
S: VecValue,
{
pub(crate) fn from_source<F>(
name: &str,
version: Version,
source: ReadableBoxedVec<Day1, S>,
mappings: &DailyMappings,
) -> Self
where
F: UnaryTransform<S, T>,
{
let day1 = LazyVec::transformed::<F>(name, version, source);
let views = Box::new(DailyViews::new(
name,
day1.read_only_boxed_clone(),
version,
mappings,
));
Self { day1, views }
}
}
fn repeated<I, T, V>(
name: &str,
source: ReadableBoxedVec<Day1, T>,
version: Version,
mapping: &V,
) -> Repeated<I, T>
where
I: VecIndex,
T: VecValue,
V: ReadableCloneableVec<I, Day1> + ?Sized,
{
DailyView::new(name, version, source, mapping.read_only_boxed_clone())
}
fn last<I, T, V>(
name: &str,
source: ReadableBoxedVec<Day1, T>,
version: Version,
mapping: &V,
) -> Last<I, T>
where
I: VecIndex,
T: VecValue,
V: ReadableCloneableVec<I, Day1> + ?Sized,
{
DailyView::new(name, version, source, mapping.read_only_boxed_clone())
}
pub trait DayStrategy: Send + Sync + 'static {
fn mapping_end(to: usize, mapping_len: usize) -> usize;
fn source_index(mapping: &[Day1], index: usize, source_len: usize) -> Option<usize>;
}
impl DayStrategy for RepeatDay {
fn mapping_end(to: usize, _mapping_len: usize) -> usize {
to
}
fn source_index(mapping: &[Day1], index: usize, source_len: usize) -> Option<usize> {
repeated_source_index(mapping, index, source_len)
}
}
impl DayStrategy for LastDay {
fn mapping_end(to: usize, mapping_len: usize) -> usize {
to.saturating_add(1).min(mapping_len)
}
fn source_index(mapping: &[Day1], index: usize, source_len: usize) -> Option<usize> {
last_source_index(mapping, index, source_len)
}
}
pub struct DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
{
name: Arc<str>,
version: Version,
source: ReadableBoxedVec<Day1, T>,
mapping: ReadableBoxedVec<I, Day1>,
_phantom: PhantomData<fn() -> S>,
}
impl<I, T, S> Clone for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
{
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
version: self.version,
source: self.source.clone(),
mapping: self.mapping.clone(),
_phantom: PhantomData,
}
}
}
impl<I, T, S> DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
S: DayStrategy,
{
fn new(
name: &str,
version: Version,
source: ReadableBoxedVec<Day1, T>,
mapping: ReadableBoxedVec<I, Day1>,
) -> Self {
Self {
name: Arc::from(name),
version,
source,
mapping,
_phantom: PhantomData,
}
}
fn try_fold_values<B, E, F>(
&self,
from: usize,
to: usize,
init: B,
f: F,
) -> std::result::Result<B, E>
where
F: FnMut(B, Option<T>) -> std::result::Result<B, E>,
{
let mapping_len = self.mapping.len();
let to = to.min(mapping_len);
if from >= to {
return Ok(init);
}
let mapping = self
.mapping
.collect_range_dyn(from, S::mapping_end(to, mapping_len));
let source_len = self.source.len();
try_fold_mapped(
&*self.source,
0,
to - from,
|index| S::source_index(&mapping, index, source_len),
init,
f,
)
}
fn fold_values<B, F>(&self, from: usize, to: usize, init: B, mut f: F) -> B
where
F: FnMut(B, Option<T>) -> B,
{
match self.try_fold_values(from, to, init, |acc, value| {
Ok::<_, std::convert::Infallible>(f(acc, value))
}) {
Ok(result) => result,
Err(error) => match error {},
}
}
}
impl<I, T, S> AnyVec for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
S: DayStrategy,
{
fn version(&self) -> Version {
self.version + self.source.version() + self.mapping.version()
}
fn name(&self) -> &str {
&self.name
}
fn len(&self) -> usize {
self.mapping.len()
}
fn index_type_to_string(&self) -> &'static str {
I::to_string()
}
fn region_names(&self) -> Vec<String> {
vec![]
}
fn value_type_to_size_of(&self) -> usize {
size_of::<Option<T>>()
}
fn value_type_to_string(&self) -> &'static str {
short_type_name::<Option<T>>()
}
}
impl<I, T, S> TypedVec for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
S: DayStrategy,
{
type I = I;
type T = Option<T>;
}
impl<I, T, S> ReadableVec<I, Option<T>> for DailyView<I, T, S>
where
I: VecIndex,
T: VecValue,
S: DayStrategy,
{
fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec<Option<T>>) {
self.fold_values(from, to, (), |(), value| buf.push(value));
}
fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(Option<T>)) {
self.fold_values(from, to, (), |(), value| f(value));
}
fn fold_range_at<B, F: FnMut(B, Option<T>) -> B>(
&self,
from: usize,
to: usize,
init: B,
f: F,
) -> B {
self.fold_values(from, to, init, f)
}
fn try_fold_range_at<B, E, F: FnMut(B, Option<T>) -> std::result::Result<B, E>>(
&self,
from: usize,
to: usize,
init: B,
f: F,
) -> std::result::Result<B, E> {
self.try_fold_values(from, to, init, f)
}
fn collect_one_at(&self, index: usize) -> Option<Option<T>> {
let mapping_len = self.mapping.len();
if index >= mapping_len {
return None;
}
let mapping = self
.mapping
.collect_range_dyn(index, S::mapping_end(index.saturating_add(1), mapping_len));
Some(
S::source_index(&mapping, 0, self.source.len())
.and_then(|day| self.source.collect_one_at(day)),
)
}
}
impl<I, T, S> Traversable for DailyView<I, T, S>
where
I: VecIndex,
T: DailyValue,
S: DayStrategy,
{
fn to_tree_node(&self) -> TreeNode {
let indexes = Index::try_from(I::to_string()).ok().into_iter().collect();
let leaf = SeriesLeaf::new(
self.name().to_string(),
self.value_type_to_string().to_string(),
indexes,
);
let schema = schemars::SchemaGenerator::default().into_root_schema_for::<Option<T>>();
let schema_json = serde_json::to_value(schema).unwrap_or_default();
TreeNode::Leaf(SeriesLeafWithSchema::new(leaf, schema_json))
}
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec> {
std::iter::once(self as &dyn AnyExportableVec)
}
}
fn try_fold_mapped<T, S, B, E, F, G>(
source: &S,
from: usize,
to: usize,
mut source_index: G,
init: B,
mut f: F,
) -> std::result::Result<B, E>
where
T: VecValue,
S: ReadableVec<Day1, T> + ?Sized,
F: FnMut(B, Option<T>) -> std::result::Result<B, E>,
G: FnMut(usize) -> Option<usize>,
{
let mut indices = Vec::with_capacity(to - from);
let mut slots: Vec<Option<u32>> = Vec::with_capacity(to - from);
for output_index in from..to {
let Some(source_index) = source_index(output_index) else {
slots.push(None);
continue;
};
let slot = match indices.last() {
Some(&last) if last == source_index => indices.len() - 1,
Some(&last) => {
debug_assert!(last < source_index);
indices.push(source_index);
indices.len() - 1
}
None => {
indices.push(source_index);
0
}
};
debug_assert!(u32::try_from(slot).is_ok());
slots.push(Some(slot as u32));
}
let values = source.read_sorted_at(&indices);
slots.into_iter().try_fold(init, |acc, slot| match slot {
Some(slot) => f(acc, Some(values[slot as usize].clone())),
None => f(acc, None),
})
}
fn repeated_source_index(mapping: &[Day1], index: usize, source_len: usize) -> Option<usize> {
let day = mapping[index].to_usize();
(day < source_len).then_some(day)
}
fn last_source_index(mapping: &[Day1], index: usize, source_len: usize) -> Option<usize> {
let first = mapping[index].to_usize();
let next_first = mapping
.get(index + 1)
.map(|day| day.to_usize())
.unwrap_or(source_len)
.min(source_len);
(first < next_first).then_some(next_first - 1)
}
#[cfg(test)]
mod tests {
use super::*;
use brk_types::{StoredBool, StoredF64};
use vecdb::{AnyStoredVec, WritableVec};
#[test]
fn repeat_uses_the_same_daily_value_throughout_the_day() {
let mapping = [Day1::from(0), Day1::from(0), Day1::from(1)];
assert_eq!(repeated_source_index(&mapping, 0, 2), Some(0));
assert_eq!(repeated_source_index(&mapping, 1, 2), Some(0));
assert_eq!(repeated_source_index(&mapping, 2, 2), Some(1));
assert_eq!(repeated_source_index(&mapping, 2, 1), None);
}
#[test]
fn coarser_period_uses_its_last_available_day() {
let mapping = [Day1::from(0), Day1::from(3), Day1::from(6)];
assert_eq!(last_source_index(&mapping, 0, 8), Some(2));
assert_eq!(last_source_index(&mapping, 1, 8), Some(5));
assert_eq!(last_source_index(&mapping, 2, 8), Some(7));
assert_eq!(last_source_index(&mapping, 1, 5), Some(4));
assert_eq!(last_source_index(&mapping, 2, 5), None);
}
#[test]
fn repeated_view_maps_ranges_and_preserves_missing_days() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path =
std::env::temp_dir().join(format!("brk-daily-view-{}-{suffix}", std::process::id()));
let db = Database::open(&path).unwrap();
let mut source: EagerVec<PcoVec<Day1, StoredF64>> =
EagerVec::forced_import(&db, "source", Version::ONE).unwrap();
let mut mapping: EagerVec<PcoVec<Height, Day1>> =
EagerVec::forced_import(&db, "mapping", Version::ONE).unwrap();
for value in [10.0, 20.0, 30.0] {
source.push(StoredF64::from(value));
}
for day in [0, 0, 1, 2, 3] {
mapping.push(Day1::from(day));
}
source.write().unwrap();
mapping.write().unwrap();
let view = DailyView::<Height, StoredF64, RepeatDay>::new(
"test",
Version::ONE,
source.read_only_boxed_clone(),
mapping.read_only_boxed_clone(),
);
assert_eq!(
view.collect_range_at(0, 5),
vec![
Some(StoredF64::from(10.0)),
Some(StoredF64::from(10.0)),
Some(StoredF64::from(20.0)),
Some(StoredF64::from(30.0)),
None,
]
);
drop(view);
drop(mapping);
drop(source);
drop(db);
std::fs::remove_dir_all(path).unwrap();
}
#[test]
fn repeated_view_supports_stored_booleans() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path =
std::env::temp_dir().join(format!("brk-daily-bool-{}-{suffix}", std::process::id()));
let db = Database::open(&path).unwrap();
let mut source: EagerVec<PcoVec<Day1, StoredBool>> =
EagerVec::forced_import(&db, "source", Version::ONE).unwrap();
let mut mapping: EagerVec<PcoVec<Height, Day1>> =
EagerVec::forced_import(&db, "mapping", Version::ONE).unwrap();
source.push(StoredBool::FALSE);
source.push(StoredBool::TRUE);
for day in [0, 0, 1, 1] {
mapping.push(Day1::from(day));
}
source.write().unwrap();
mapping.write().unwrap();
let view = DailyView::<Height, StoredBool, RepeatDay>::new(
"test",
Version::ONE,
source.read_only_boxed_clone(),
mapping.read_only_boxed_clone(),
);
assert_eq!(
view.collect_range_at(0, 4),
vec![
Some(StoredBool::FALSE),
Some(StoredBool::FALSE),
Some(StoredBool::TRUE),
Some(StoredBool::TRUE),
]
);
drop(view);
drop(mapping);
drop(source);
drop(db);
std::fs::remove_dir_all(path).unwrap();
}
}