use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use core_traits::Entid;
use mentat_core::{HasSchema, Keyword, Schema};
use mentat_db::db;
use public_traits::errors::Result;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AutoIndex {
Off,
#[default]
Schema,
Adaptive,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IndexAction {
Create {
index: String,
attribute: Entid,
ident: Option<Keyword>,
uses: u32,
},
Drop {
index: String,
attribute: Entid,
ident: Option<Keyword>,
},
}
impl fmt::Display for IndexAction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let who = |a: &Entid, i: &Option<Keyword>| match i {
Some(k) => format!("{k}"),
None => a.to_string(),
};
match self {
IndexAction::Create {
index,
attribute,
ident,
uses,
} => write!(
f,
"create {index} on {} ({uses} value-filtered queries)",
who(attribute, ident)
),
IndexAction::Drop {
index,
attribute,
ident,
} => write!(f, "drop {index} on {} (idle)", who(attribute, ident)),
}
}
}
pub const PERIOD: u32 = 1000;
const USER0: Entid = 0x10000;
#[derive(Debug)]
pub(crate) struct Advisor {
pub(crate) mode: AutoIndex,
pub(crate) min_uses: u32,
pub(crate) idle: Duration,
uses: BTreeMap<Entid, u32>,
queries: u32,
rejected: BTreeSet<Entid>,
indexed: BTreeSet<Entid>,
}
impl Default for Advisor {
fn default() -> Advisor {
let mode = match std::env::var("MENTAT_AUTO_INDEX").as_deref() {
Ok("off") => AutoIndex::Off,
Ok("adaptive") => AutoIndex::Adaptive,
_ => AutoIndex::default(),
};
Advisor {
mode,
min_uses: 16,
idle: Duration::from_secs(24 * 3600),
uses: BTreeMap::new(),
queries: 0,
rejected: BTreeSet::new(),
indexed: BTreeSet::new(),
}
}
}
fn now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn registry(conn: &rusqlite::Connection) -> Result<Vec<(String, Entid, i64, bool)>> {
if !db::has_table(conn, "mentat_managed_indexes")? {
return Ok(vec![]);
}
let mut stmt = conn.prepare("SELECT name, a, last_used, kind FROM mentat_managed_indexes")?;
let rows: Vec<(String, Entid, i64, bool)> = stmt
.query_map([], |r| {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get::<_, String>(3)? == "schema",
))
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
Ok(rows
.into_iter()
.filter(|(n, a, _, _)| *n == db::value_index_name(*a))
.collect())
}
fn selective(conn: &rusqlite::Connection, index: &str) -> Result<bool> {
let stat: Option<String> = conn
.query_row(
"SELECT stat FROM sqlite_stat1 WHERE idx = ?",
[index],
|r| r.get(0),
)
.ok();
let n: Vec<u64> = stat
.unwrap_or_default()
.split_whitespace()
.filter_map(|x| x.parse().ok())
.collect();
Ok(match n[..] {
[rows, _, per_value, ..] => per_value * 20 <= rows.max(1),
_ => true, })
}
impl Advisor {
pub(crate) fn record(&mut self, attrs: &BTreeSet<Entid>) -> bool {
self.queries += 1;
let mut due = self.queries >= PERIOD;
for a in attrs {
let n = self.uses.entry(*a).or_insert(0);
*n += 1;
due |= *n == self.min_uses && !self.indexed.contains(a) && !self.rejected.contains(a);
}
due
}
pub(crate) fn tune(
&mut self,
conn: &rusqlite::Connection,
schema: &Schema,
dry_run: bool,
) -> Result<Vec<IndexAction>> {
if self.mode == AutoIndex::Off {
return Ok(vec![]);
}
let tx = conn.unchecked_transaction()?;
if !dry_run {
db::sync_schema_indexes(&tx, schema)?;
db::refresh_value_index_stats(&tx)?;
}
let managed = registry(&tx)?;
let ident = |a: Entid| schema.get_ident(a).cloned();
let now = now();
let mut actions = vec![];
let have: BTreeSet<Entid> = managed.iter().map(|(_, a, _, _)| *a).collect();
if self.mode == AutoIndex::Adaptive {
for (&a, &uses) in &self.uses {
let eligible = a >= USER0
&& schema
.attribute_for_entid(a)
.is_some_and(|attr| !attr.fulltext);
if uses >= self.min_uses
&& eligible
&& !have.contains(&a)
&& !self.rejected.contains(&a)
{
actions.push(IndexAction::Create {
index: db::value_index_name(a),
attribute: a,
ident: ident(a),
uses,
});
}
}
}
for (index, a, last_used, is_schema) in &managed {
if *is_schema {
continue; }
let used = self.uses.get(a).copied().unwrap_or(0) > 0;
let idle = now.saturating_sub(*last_used) as u64 >= self.idle.as_secs();
if self.mode == AutoIndex::Schema || (!used && idle) {
actions.push(IndexAction::Drop {
index: index.clone(),
attribute: *a,
ident: ident(*a),
});
}
}
if dry_run {
return Ok(actions);
}
for (_, a, _, _) in &managed {
if self.uses.get(a).copied().unwrap_or(0) > 0 {
tx.execute(
"UPDATE mentat_managed_indexes SET last_used = ? WHERE a = ?",
rusqlite::params![now, a],
)?;
}
}
let mut rejected = BTreeSet::new();
for action in &actions {
match action {
IndexAction::Create {
index, attribute, ..
} => {
db::create_value_index(&tx, *attribute, "adaptive")?;
if !selective(&tx, index)? {
db::drop_value_index(&tx, *attribute)?;
rejected.insert(*attribute);
}
}
IndexAction::Drop { attribute, .. } => {
db::drop_value_index(&tx, *attribute)?;
}
}
}
tx.commit()?;
self.indexed = registry(conn)?.into_iter().map(|(_, a, _, _)| a).collect();
actions.retain(
|a| !matches!(a, IndexAction::Create { attribute, .. } if rejected.contains(attribute)),
);
self.rejected.extend(rejected);
self.uses.clear();
self.queries = 0;
Ok(actions)
}
}