use crate::catalog::ValType;
use crate::table::{TableCatalog, TableSpec};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdviseShape {
Range,
Where(Vec<Vec<u8>>),
Match,
Filter(Vec<u8>),
}
#[derive(Debug, Clone)]
pub struct AdviseEntry {
pub name: Vec<u8>,
pub shape: AdviseShape,
pub count: u64,
pub sample: Vec<Vec<u8>>,
}
#[derive(Debug)]
pub struct AdviseLog {
entries: Vec<AdviseEntry>,
cap: usize,
}
impl Default for AdviseLog {
fn default() -> Self {
Self::new()
}
}
pub const ADVISE_CAP: usize = 128;
pub const AUTODECLARE_AFTER: u64 = 16;
pub fn apply_auto(spec: &mut TableSpec, e: &AdviseEntry) -> Option<Vec<u8>> {
if spec.auto_added.len() >= spec.autodeclare {
return None;
}
let dot = e.name.iter().position(|&b| b == b'.')?;
let (table, suffix) = (&e.name[..dot], &e.name[dot + 1..]);
if table != spec.name {
return None;
}
let entry = auto_entry(spec, &e.name, suffix, &e.shape)?;
spec.auto_added.push(entry.clone());
Some(entry)
}
fn auto_entry(
spec: &mut TableSpec,
name: &[u8],
suffix: &[u8],
shape: &AdviseShape,
) -> Option<Vec<u8>> {
match shape {
AdviseShape::Range => {
spec.column_type(suffix)?;
if spec.indexes.iter().any(|ix| ix.column == suffix) {
return None;
}
spec.indexes.push(crate::table::TableIndex {
column: suffix.to_vec(),
kind: crate::IndexKind::Range,
values: Vec::new(),
});
Some(name.to_vec())
}
AdviseShape::Where(cols) => {
for c in cols {
spec.column_type(c)?;
}
if cols.is_empty() || spec.orderpaths.iter().any(|op| op.name == suffix) {
return None;
}
spec.orderpaths.push(crate::table::OrderPath {
name: suffix.to_vec(),
on: cols.iter().map(|c| (c.clone(), false)).collect(),
});
Some(name.to_vec())
}
AdviseShape::Filter(field) => {
spec.column_type(field)?;
let ix = spec.indexes.iter_mut().find(|ix| ix.column == suffix)?;
if ix.values.iter().any(|v| v == field) {
return None;
}
ix.values.push(field.clone());
let mut entry = name.to_vec();
entry.push(b'#');
entry.extend_from_slice(field);
Some(entry)
}
AdviseShape::Match => None,
}
}
impl AdviseLog {
#[must_use]
pub fn new() -> Self {
Self::with_cap(ADVISE_CAP)
}
#[must_use]
pub fn with_cap(cap: usize) -> Self {
Self { entries: Vec::new(), cap: cap.max(1) }
}
pub fn observe(&mut self, name: &[u8], shape: AdviseShape, argv: &[Vec<u8>]) -> u64 {
if let Some(e) =
self.entries.iter_mut().find(|e| e.name == name && e.shape == shape)
{
e.count += 1;
return e.count;
}
if self.entries.len() >= self.cap {
let (weakest, _) = self
.entries
.iter()
.enumerate()
.min_by_key(|(_, e)| e.count)
.expect("cap >= 1, so a full log is non-empty");
self.entries.swap_remove(weakest);
}
self.entries.push(AdviseEntry {
name: name.to_vec(),
shape,
count: 1,
sample: argv.to_vec(),
});
1
}
#[must_use]
pub fn entries(&self) -> Vec<&AdviseEntry> {
let mut v: Vec<&AdviseEntry> = self.entries.iter().collect();
v.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name)));
v
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
#[derive(Debug)]
pub struct UsageCell {
pub hits: std::sync::atomic::AtomicU64,
pub last_hit_s: std::sync::atomic::AtomicI64,
pub declared_s: std::sync::atomic::AtomicI64,
pub min_margin: std::sync::atomic::AtomicI64,
}
impl Default for UsageCell {
fn default() -> Self {
use std::sync::atomic::{AtomicI64, AtomicU64};
Self {
hits: AtomicU64::new(0),
last_hit_s: AtomicI64::new(0),
declared_s: AtomicI64::new(0),
min_margin: AtomicI64::new(i64::MAX),
}
}
}
impl UsageCell {
#[must_use]
pub fn declared_at(now_s: i64) -> Self {
let c = Self::default();
c.declared_s.store(now_s, std::sync::atomic::Ordering::Relaxed);
c
}
pub fn probe(&self, margin: i64) {
self.min_margin.fetch_min(margin, std::sync::atomic::Ordering::Relaxed);
}
pub fn hit(&self, now_s: i64) {
use std::sync::atomic::Ordering::Relaxed;
self.hits.fetch_add(1, Relaxed);
self.last_hit_s.store(now_s, Relaxed);
}
#[must_use]
pub fn read(&self) -> (u64, i64, i64) {
use std::sync::atomic::Ordering::Relaxed;
(self.hits.load(Relaxed), self.last_hit_s.load(Relaxed), self.declared_s.load(Relaxed))
}
}
#[must_use]
pub fn narrow_advice(spec: &TableSpec, margin: i64) -> Option<String> {
let w = spec.window.as_ref()?;
if margin == i64::MAX || w.bucket <= 0 {
return None;
}
let narrow = margin - margin.rem_euclid(w.bucket);
if narrow <= 0 {
return None;
}
let new_span = (w.span - narrow).max(w.bucket);
if new_span >= w.span {
return None;
}
Some(format!(
"WINDOW {} SPAN {} — every observed query kept a margin of {}; SPAN {} still serves them",
String::from_utf8_lossy(&w.column),
w.span,
margin,
new_span
))
}
#[must_use]
pub fn advice_of(e: &AdviseEntry, cat: &TableCatalog) -> Option<String> {
let dot = e.name.iter().position(|&b| b == b'.')?;
let (table, suffix) = (&e.name[..dot], &e.name[dot + 1..]);
let t = cat.get(table)?;
let show = |b: &[u8]| String::from_utf8_lossy(b).into_owned();
match &e.shape {
AdviseShape::Range => {
let ty = t.column_type(suffix)?;
Some(format!(
"TABLE.DECLARE {} … INDEX {} range (column type {})",
show(table),
show(suffix),
ty.tag()
))
}
AdviseShape::Where(cols) => {
for c in cols {
t.column_type(c)?;
}
let list =
cols.iter().map(|c| show(c)).collect::<Vec<_>>().join(" THEN ");
Some(format!(
"TABLE.DECLARE {} … ORDERPATH {} ON {}",
show(table),
show(suffix),
list
))
}
AdviseShape::Match => {
let ty = t.column_type(suffix)?;
(ty == ValType::Str).then(|| {
format!(
"IDX.CREATE {} ON PREFIX {} FIELD {} TYPE str KIND text",
show(&e.name),
show(&t.prefix),
show(suffix)
)
})
}
AdviseShape::Filter(field) => {
let ty = t.column_type(field)?;
Some(format!(
"add VALUES {} (type {}) to the {} declaration",
show(field),
ty.tag(),
show(&e.name)
))
}
}
}
#[cfg(test)]
#[path = "advise_tests.rs"]
mod tests;