use std::collections::BTreeMap;
use palimpsest_wal::{Datum, TableId};
use crate::palimpsest::wal::Row;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ColumnLocation {
pub table: TableId,
pub column: usize,
}
impl ColumnLocation {
#[must_use]
pub const fn new(table: TableId, column: usize) -> Self {
Self { table, column }
}
}
pub type RowKey = Vec<u8>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToastOutcome {
Cached(Datum),
PointSelect(Datum),
Missing,
}
impl ToastOutcome {
#[must_use]
pub fn into_datum(self) -> Option<Datum> {
match self {
Self::Cached(value) | Self::PointSelect(value) => Some(value),
Self::Missing => None,
}
}
}
pub trait PointSelect {
fn point_select(&self, location: &ColumnLocation, key: &RowKey) -> Option<Datum>;
}
#[derive(Debug, Clone, Default)]
pub struct ArrangementCache {
rows: BTreeMap<(TableId, RowKey), Row>,
}
impl ArrangementCache {
#[must_use]
pub const fn new() -> Self {
Self {
rows: BTreeMap::new(),
}
}
pub fn insert(&mut self, table: TableId, key: RowKey, row: Row) {
self.rows.insert((table, key), row);
}
pub fn remove(&mut self, table: TableId, key: &RowKey) -> Option<Row> {
self.rows.remove(&(table, key.clone()))
}
#[must_use]
pub fn get(&self, location: &ColumnLocation, key: &RowKey) -> Option<Datum> {
let row = self.rows.get(&(location.table, key.clone()))?;
row.get(location.column).cloned()
}
#[must_use]
pub fn len(&self) -> usize {
self.rows.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct ToastResolver<S> {
cache: ArrangementCache,
source: S,
cached_hits: usize,
point_select_hits: usize,
misses: usize,
}
impl<S> ToastResolver<S>
where
S: PointSelect,
{
pub fn new(source: S) -> Self {
Self {
cache: ArrangementCache::new(),
source,
cached_hits: 0,
point_select_hits: 0,
misses: 0,
}
}
pub fn resolve(&mut self, location: &ColumnLocation, key: &RowKey) -> ToastOutcome {
if let Some(value) = self.cache.get(location, key) {
self.cached_hits = self.cached_hits.saturating_add(1);
return ToastOutcome::Cached(value);
}
match self.source.point_select(location, key) {
Some(value) => {
self.point_select_hits = self.point_select_hits.saturating_add(1);
ToastOutcome::PointSelect(value)
}
None => {
self.misses = self.misses.saturating_add(1);
ToastOutcome::Missing
}
}
}
pub fn record_row(&mut self, table: TableId, key: RowKey, row: Row) {
self.cache.insert(table, key, row);
}
pub fn forget_row(&mut self, table: TableId, key: &RowKey) {
self.cache.remove(table, key);
}
#[must_use]
pub const fn stats(&self) -> ToastStats {
ToastStats {
cached_hits: self.cached_hits,
point_select_hits: self.point_select_hits,
misses: self.misses,
}
}
#[must_use]
pub const fn source(&self) -> &S {
&self.source
}
#[must_use]
pub const fn cache(&self) -> &ArrangementCache {
&self.cache
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ToastStats {
pub cached_hits: usize,
pub point_select_hits: usize,
pub misses: usize,
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::collections::BTreeMap;
use palimpsest_wal::{Datum, TableId};
use smallvec::smallvec;
use super::{
ArrangementCache, ColumnLocation, PointSelect, RowKey, ToastOutcome, ToastResolver,
};
fn key_bytes(value: i32) -> RowKey {
value.to_be_bytes().to_vec()
}
struct FakeSource {
rows: BTreeMap<(TableId, RowKey), Vec<Datum>>,
calls: RefCell<usize>,
}
impl FakeSource {
fn new() -> Self {
Self {
rows: BTreeMap::new(),
calls: RefCell::new(0),
}
}
fn insert(&mut self, table: TableId, key: RowKey, row: Vec<Datum>) {
self.rows.insert((table, key), row);
}
}
impl PointSelect for FakeSource {
fn point_select(&self, location: &ColumnLocation, key: &RowKey) -> Option<Datum> {
*self.calls.borrow_mut() += 1;
self.rows
.get(&(location.table, key.clone()))
.and_then(|row: &Vec<Datum>| row.get(location.column).cloned())
}
}
#[test]
fn cached_hit_does_not_call_point_select() {
let table = TableId::new(7);
let mut resolver = ToastResolver::new(FakeSource::new());
resolver.record_row(
table,
key_bytes(1),
smallvec![Datum::I32(1), Datum::Text("hello".into())],
);
let outcome = resolver.resolve(&ColumnLocation::new(table, 1), &key_bytes(1));
assert!(matches!(outcome, ToastOutcome::Cached(_)));
assert_eq!(*resolver.source().calls.borrow(), 0);
assert_eq!(resolver.stats().cached_hits, 1);
}
#[test]
fn point_select_falls_back_when_cache_misses() {
let table = TableId::new(7);
let mut source = FakeSource::new();
source.insert(
table,
key_bytes(1),
vec![Datum::I32(1), Datum::Text("late".into())],
);
let mut resolver = ToastResolver::new(source);
let outcome = resolver.resolve(&ColumnLocation::new(table, 1), &key_bytes(1));
assert_eq!(outcome.into_datum(), Some(Datum::Text("late".into())));
assert_eq!(resolver.stats().point_select_hits, 1);
assert_eq!(*resolver.source().calls.borrow(), 1);
}
#[test]
fn missing_outcome_increments_miss_counter() {
let table = TableId::new(7);
let mut resolver = ToastResolver::new(FakeSource::new());
let outcome = resolver.resolve(&ColumnLocation::new(table, 1), &key_bytes(99));
assert_eq!(outcome, ToastOutcome::Missing);
assert_eq!(resolver.stats().misses, 1);
}
#[test]
fn arrangement_cache_supports_remove_and_len() {
let mut cache = ArrangementCache::new();
cache.insert(
TableId::new(7),
key_bytes(1),
smallvec![Datum::I32(1), Datum::Text("v".into())],
);
assert_eq!(cache.len(), 1);
assert_eq!(
cache.get(&ColumnLocation::new(TableId::new(7), 1), &key_bytes(1)),
Some(Datum::Text("v".into()))
);
assert!(cache.remove(TableId::new(7), &key_bytes(1)).is_some());
assert!(cache.is_empty());
}
}