use kevy_index::{IndexSpec, IndexValue, SegmentStats};
use kevy_store::Store;
use super::args::{KnnArgs, Query, Shape, parse_groups_args};
use super::wire::{encode_hydration_row, encode_value, peek_hydration};
use super::{ST_BADARGS, ST_BUILDING, ST_NOINDEX, ST_OK, ST_OVERBUDGET};
use crate::index_runtime;
use crate::state::Ctx;
enum HitsOrChunk {
Hits(Vec<(Vec<u8>, IndexValue)>),
Chunk(Vec<u8>),
Verify {
spec: Box<IndexSpec>,
entries: Vec<(Vec<u8>, IndexValue)>,
stats: SegmentStats,
window: Option<kevy_index::WindowAudit>,
},
}
pub(super) fn op_query(ctx: &Ctx<'_>, store: &mut Store, argv: &[Vec<u8>], verb: &[u8]) -> Vec<u8> {
let Some(q) = Query::parse(argv) else {
return vec![ST_BADARGS];
};
if verb.eq_ignore_ascii_case(b"IDX.COUNT") {
if q.selects() || !q.fields.is_empty() || q.cursor_raw.is_some() {
return vec![ST_BADARGS];
}
if !q.filters.is_empty() {
return super::query_claused::run_claused_count(ctx, store, &q);
}
}
if q.cursor_raw.is_some() && q.selects() {
return super::query_claused::clause_chunk(super::query_claused::CURSOR_CLAUSE_CONFLICT);
}
if matches!(q.shape, Shape::Verify)
&& let Some(chunk) = verify_kind_stats(ctx, store, &q.name)
{
return chunk;
}
if q.has_clauses() {
return super::query_claused::run_claused_query(ctx, store, &q);
}
run_scalar_query(ctx, store, &q, verb)
}
fn verify_kind_stats(ctx: &Ctx<'_>, store: &mut Store, name: &[u8]) -> Option<Vec<u8>> {
let kind = ctx.state.catalogs.index().and_then(|c| c.get(name).map(|(s, _)| s.kind))?;
let res = match kind {
kevy_index::IndexKind::Agg => index_runtime::with_ready_agg(ctx, store, name, |a| {
let st = a.stats();
(b'a', vec![st.rows, st.approx_bytes, st.excluded, st.groups])
}),
kevy_index::IndexKind::Ann => index_runtime::with_ready_ann(ctx, store, name, |g| {
let st = g.stats();
(
b'v',
vec![
st.vectors,
st.approx_bytes,
st.tombstones,
st.links,
u64::from(st.rebuild_recommended),
],
)
}),
kevy_index::IndexKind::Text => {
index_runtime::with_ready_text_segment(ctx, store, name, |_, ts, _, _| {
let st = ts.stats();
(b't', vec![st.docs, st.approx_bytes, st.postings, st.tokens])
})
}
_ => return None,
};
Some(match res {
Ok((tag, values)) => {
let mut chunk = vec![ST_OK, tag];
for v in values {
chunk.extend_from_slice(&v.to_le_bytes());
}
chunk
}
Err(e) if e.as_wire().starts_with("INDEXBUILDING") => vec![ST_BUILDING],
Err(_) => vec![ST_NOINDEX],
})
}
fn run_scalar_query(ctx: &Ctx<'_>, store: &mut Store, q: &Query, verb: &[u8]) -> Vec<u8> {
let res =
index_runtime::with_ready_segment(ctx, store, &q.name, |spec, seg, win| match q.shape {
Shape::Range { .. } | Shape::Eq { .. } | Shape::Where(_) => {
let now = (kevy_store::now_unix_ms() / 1000) as i64;
let (min, max) = match q.bounds_for(spec, now) {
Ok(b) => b,
Err(chunk) => return HitsOrChunk::Chunk(chunk),
};
super::probe_window(ctx, &q.name, win, &min);
scalar_range_or_count(q, verb, spec, seg, win, &min, &max)
}
Shape::Verify => {
let mut entries: Vec<(Vec<u8>, IndexValue)> = Vec::new();
seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
HitsOrChunk::Verify {
spec: Box::new(spec.clone()),
entries,
stats: seg.stats(),
window: win.and_then(|w| w.audit(spec.ty)),
}
}
});
match res {
Ok(HitsOrChunk::Chunk(chunk)) => chunk,
Ok(HitsOrChunk::Hits(hits)) => encode_hits_chunk(store, &hits, &q.fields),
Ok(HitsOrChunk::Verify { spec, entries, stats, window }) => {
encode_verify_chunk(store, &spec, &entries, &stats, window)
}
Err(e) if e.as_wire().starts_with("INDEXBUILDING") => vec![ST_BUILDING],
Err(e) if e.as_wire().starts_with("INDEXOVERBUDGET") => vec![ST_OVERBUDGET],
Err(_) => vec![ST_NOINDEX],
}
}
fn scalar_range_or_count(
q: &Query,
verb: &[u8],
spec: &kevy_index::IndexSpec,
seg: &kevy_index::Segment,
win: Option<&index_runtime::WindowRt>,
min: &IndexValue,
max: &IndexValue,
) -> HitsOrChunk {
let cold = win.filter(|w| w.has_cold());
if verb.eq_ignore_ascii_case(b"IDX.COUNT") {
let cold_n = match cold.map(|w| w.cold_count(spec.ty, min, max)).transpose() {
Ok(n) => n.unwrap_or(0),
Err(_) => return HitsOrChunk::Chunk(vec![ST_NOINDEX]),
};
let mut chunk = vec![ST_OK];
chunk.extend_from_slice(&(seg.count(min, max) + cold_n).to_le_bytes());
return HitsOrChunk::Chunk(chunk);
}
let cursor = q.cursor(spec.ty);
let (hits, _) = seg.range(min, max, cursor.as_ref(), q.limit);
match cold.map(|w| w.cold_hits(spec.ty, min, max, cursor.as_ref(), q.limit)).transpose() {
Ok(None) => HitsOrChunk::Hits(hits),
Ok(Some(cold_hits)) => HitsOrChunk::Hits(merge_cold(hits, cold_hits, q.limit)),
Err(_) => HitsOrChunk::Chunk(vec![ST_NOINDEX]),
}
}
fn merge_cold(
hot: Vec<(Vec<u8>, IndexValue)>,
cold: Vec<(Vec<u8>, IndexValue)>,
limit: usize,
) -> Vec<(Vec<u8>, IndexValue)> {
let mut out = Vec::with_capacity(hot.len() + cold.len());
let (mut h, mut c) = (hot.into_iter().peekable(), cold.into_iter().peekable());
while out.len() < limit {
let take_cold = match (h.peek(), c.peek()) {
(None, None) => break,
(Some(_), None) => false,
(None, Some(_)) => true,
(Some((hk, hv)), Some((ck, cv))) => (cv, ck) < (hv, hk),
};
let (k, v) = if take_cold { c.next() } else { h.next() }.expect("peeked");
out.push((k, v));
}
out
}
fn encode_hits_chunk(
store: &mut Store,
hits: &[(Vec<u8>, IndexValue)],
fields: &[Vec<u8>],
) -> Vec<u8> {
let mut chunk = vec![ST_OK];
chunk.extend_from_slice(&(hits.len() as u32).to_le_bytes());
let keys: Vec<&[u8]> = hits.iter().map(|(k, _)| k.as_slice()).collect();
let rows = peek_hydration(store, &keys, fields);
for (i, (k, v)) in hits.iter().enumerate() {
chunk.extend_from_slice(&(k.len() as u32).to_le_bytes());
chunk.extend_from_slice(k);
encode_value(&mut chunk, v);
encode_hydration_row(&mut chunk, fields.len(), &rows[i]);
}
chunk
}
pub(super) fn op_explain(ctx: &Ctx<'_>, store: &mut Store, argv: &[Vec<u8>]) -> Vec<u8> {
let Some(cat) = ctx.state.catalogs.index() else {
return vec![ST_NOINDEX];
};
let name = argv.get(1).map(Vec::as_slice).unwrap_or(b"");
let Some(spec) = cat.iter().map(|(s, _)| s).find(|s| s.name.as_slice() == name) else {
return vec![ST_NOINDEX];
};
let shape = argv.get(2).map(Vec::as_slice).unwrap_or(b"");
let mut qargv = argv.to_vec();
qargv[0] = b"IDX.QUERY".to_vec();
let parsed = if name.eq_ignore_ascii_case(b"HYBRID") {
super::args::HybridArgs::parse(&qargv).is_some()
} else if shape.eq_ignore_ascii_case(b"MATCH") {
super::args::MatchArgs::parse(&qargv).is_some()
} else if shape.eq_ignore_ascii_case(b"KNN") {
KnnArgs::parse(&qargv).is_some()
} else if shape.eq_ignore_ascii_case(b"GROUP") || shape.eq_ignore_ascii_case(b"GROUPS") {
shape.eq_ignore_ascii_case(b"GROUP") || parse_groups_args(&qargv).is_some()
} else {
Query::parse(&qargv).is_some()
};
if !parsed {
return vec![ST_BADARGS];
}
let building = index_runtime::segment_building(ctx, store, &spec.name);
let entries = kind_entries(ctx, store, spec.kind, &spec.name);
let mut chunk = vec![ST_OK, u8::from(building)];
chunk.extend_from_slice(&entries.to_le_bytes());
chunk.push(shape.first().copied().unwrap_or(b'?').to_ascii_uppercase());
chunk
}
fn kind_entries(ctx: &Ctx<'_>, store: &mut Store, kind: kevy_index::IndexKind, name: &[u8]) -> u64 {
match kind {
kevy_index::IndexKind::Agg => {
index_runtime::with_ready_agg(ctx, store, name, |a| a.rows()).unwrap_or_default()
}
kevy_index::IndexKind::Ann => {
index_runtime::with_ready_ann(ctx, store, name, |g| g.vectors()).unwrap_or_default()
}
kevy_index::IndexKind::Text => {
index_runtime::with_ready_text_segment(ctx, store, name, |_, t, _, _| t.docs())
.unwrap_or_default()
}
_ => index_runtime::with_ready_segment(ctx, store, name, |_, s, _| s.stats().entries)
.unwrap_or_default(),
}
}
pub(super) fn op_list(ctx: &Ctx<'_>, store: &mut Store) -> Vec<u8> {
let Some(cat) = ctx.state.catalogs.index() else {
return vec![ST_OK];
};
let mut chunk = vec![ST_OK];
for (spec, _) in cat.iter() {
let building = index_runtime::segment_building(ctx, store, &spec.name);
let quad = if spec.kind == kevy_index::IndexKind::Agg {
index_runtime::with_ready_agg(ctx, store, &spec.name, |a| {
let st = a.stats();
(st.rows, st.approx_bytes, st.excluded, st.groups)
})
.unwrap_or_default()
} else if spec.kind == kevy_index::IndexKind::Ann {
index_runtime::with_ready_ann(ctx, store, &spec.name, |g| {
let st = g.stats();
(st.vectors, st.approx_bytes, st.tombstones, st.links)
})
.unwrap_or_default()
} else if spec.kind == kevy_index::IndexKind::Text {
index_runtime::with_ready_text_segment(ctx, store, &spec.name, |_, ts, _, _| {
let st = ts.stats();
(st.docs, st.approx_bytes, st.postings, st.tokens)
})
.unwrap_or_default()
} else {
index_runtime::with_ready_segment(ctx, store, &spec.name, |_, seg, _| {
let st = seg.stats();
(st.entries, st.approx_bytes, st.coerce_failures, st.duplicates)
})
.unwrap_or_default()
};
chunk.push(u8::from(building));
chunk.extend_from_slice(&quad.0.to_le_bytes());
chunk.extend_from_slice(&quad.1.to_le_bytes());
chunk.extend_from_slice(&quad.2.to_le_bytes());
chunk.extend_from_slice(&quad.3.to_le_bytes());
}
chunk
}
fn encode_verify_chunk(
store: &mut Store,
spec: &IndexSpec,
entries: &[(Vec<u8>, IndexValue)],
stats: &SegmentStats,
window: Option<kevy_index::WindowAudit>,
) -> Vec<u8> {
let mut pattern = spec.prefix.clone();
pattern.push(b'*');
let row_keys = store.collect_keys(Some(&pattern), None);
let indexed: std::collections::HashSet<&[u8]> =
entries.iter().map(|(k, _)| k.as_slice()).collect();
let (drift, missing) = store.peek_scope(|s| {
let mut drift = 0u64;
for (key, held) in entries {
match index_runtime::row_value(s, spec, key) {
index_runtime::RowValue::Value(actual) if &actual == held => {}
_ => drift += 1,
}
}
let cls =
crate::cmd_table_verify::classify_prefix_rows(s, spec, &row_keys, &indexed, window);
(drift, cls[4])
});
let mut chunk = vec![ST_OK, b's'];
chunk.extend_from_slice(&stats.entries.to_le_bytes());
chunk.extend_from_slice(&stats.approx_bytes.to_le_bytes());
chunk.extend_from_slice(&stats.coerce_failures.to_le_bytes());
chunk.extend_from_slice(&stats.duplicates.to_le_bytes());
chunk.extend_from_slice(&drift.to_le_bytes());
chunk.extend_from_slice(&(entries.len() as u64).to_le_bytes());
chunk.extend_from_slice(&missing.to_le_bytes());
chunk
}
#[cfg(test)]
#[path = "query_verify_tests.rs"]
mod verify_tests;