kevy-embedded 4.0.0

Embedded mode for kevy — in-process Redis-compatible KV without the server/runtime.
Documentation
//! The optional clause tail every `IDX.QUERY` shape shares — `LIMIT`,
//! `CURSOR`, `FIELDS`, and the MATCH-only clauses. Split from
//! `idx_query.rs` for the 500-LOC house rule (a `#[path]` child, so the
//! dispatch keeps reaching it directly).

/// One parsed clause tail. `highlight` is `None` unless the query asked
/// for it; `Some(empty)` means every field.
pub(super) struct Tail {
    pub(super) limit: usize,
    pub(super) cursor_raw: Option<Vec<u8>>,
    pub(super) fields: Vec<Vec<u8>>,
    pub(super) highlight: Option<Vec<Vec<u8>>>,
    /// `TYPO n`: edit budget for each bare term; 0 = exact.
    pub(super) typo: u32,
    /// `OFFSET n`: hits to skip before `LIMIT`.
    pub(super) offset: usize,
    /// `IN <field…>`: declared field names to score within; empty = the
    /// whole document.
    pub(super) scope: Vec<Vec<u8>>,
    /// `FILTER …`: non-scoring predicates over stored values, ANDed.
    /// Owned here because the borrowed form the store takes points at
    /// them. Shared by MATCH and — since the capacity arc's G1 — the
    /// scalar RANGE/EQ shapes, so the clause set is feature-independent.
    pub(super) filters: Vec<FilterClause>,
    /// `SORT <field> ASC|DESC`: select by a stored value rather than by
    /// score.
    pub(super) sort: Option<(Vec<u8>, bool)>,
    /// `DISTINCT <field>`: at most one hit per value of a stored field.
    pub(super) distinct: Option<Vec<u8>>,
    /// `FACET <field…>`: count each field's values over the match set.
    pub(super) facets: Vec<Vec<u8>>,
}

/// One parsed `FILTER <field> RANGE <min> <max>` / `EQ <v>`.
pub(super) struct FilterClause {
    pub(super) field: Vec<u8>,
    pub(super) shape: FilterShape,
}

pub(super) enum FilterShape {
    Range(Vec<u8>, Vec<u8>),
    Eq(Vec<u8>),
}

impl FilterClause {
    /// The borrowed form [`crate::MatchOpts`] takes.
    pub(super) fn as_value_filter(&self) -> crate::ValueFilter<'_> {
        match &self.shape {
            FilterShape::Range(min, max) => {
                crate::ValueFilter::Range { field: &self.field, min, max }
            }
            FilterShape::Eq(v) => crate::ValueFilter::Eq { field: &self.field, value: v },
        }
    }
}

/// A tail clause keyword — the boundary a variadic clause collects up to.
fn is_tail_keyword(a: &[u8]) -> bool {
    a.eq_ignore_ascii_case(b"LIMIT")
        || a.eq_ignore_ascii_case(b"CURSOR")
        || a.eq_ignore_ascii_case(b"FIELDS")
        || a.eq_ignore_ascii_case(b"HIGHLIGHT")
        || a.eq_ignore_ascii_case(b"TYPO")
        || a.eq_ignore_ascii_case(b"OFFSET")
        || a.eq_ignore_ascii_case(b"IN")
        || a.eq_ignore_ascii_case(b"FILTER")
        || a.eq_ignore_ascii_case(b"SORT")
        || a.eq_ignore_ascii_case(b"DISTINCT")
        || a.eq_ignore_ascii_case(b"FACET")
}

/// The clauses only a MATCH takes (the shared stored-value clauses are
/// routed by `apply_tail_clause` itself).
fn apply_match_clause(argv: &[Vec<u8>], i: usize, t: &mut Tail) -> Option<usize> {
    let a = &argv[i];
    if a.eq_ignore_ascii_case(b"HIGHLIGHT") {
        let (hs, next) = collect_until_keyword(argv, i + 1);
        t.highlight = Some(hs);
        Some(next)
    } else if a.eq_ignore_ascii_case(b"TYPO") {
        t.typo = match argv.get(i + 1)?.as_slice() {
            b"0" => 0,
            b"1" => 1,
            b"2" => 2,
            _ => return None,
        };
        Some(i + 2)
    } else if a.eq_ignore_ascii_case(b"IN") {
        let (fs, next) = collect_until_keyword(argv, i + 1);
        if fs.is_empty() {
            return None;
        }
        t.scope = fs;
        Some(next)
    } else {
        None
    }
}

/// `SORT <field> ASC|DESC`.
fn apply_sort(argv: &[Vec<u8>], i: usize, t: &mut Tail) -> Option<usize> {
    let field = argv.get(i + 1)?.clone();
    let dir = argv.get(i + 2)?;
    let desc = if dir.eq_ignore_ascii_case(b"DESC") {
        true
    } else if dir.eq_ignore_ascii_case(b"ASC") {
        false
    } else {
        return None;
    };
    t.sort = Some((field, desc));
    Some(i + 3)
}

/// `FACET <field…>`.
fn apply_facet(argv: &[Vec<u8>], i: usize, t: &mut Tail) -> Option<usize> {
    let (fs, next) = collect_until_keyword(argv, i + 1);
    if fs.is_empty() {
        return None;
    }
    t.facets = fs;
    Some(next)
}

/// `DISTINCT <field>`.
fn apply_distinct(argv: &[Vec<u8>], i: usize, t: &mut Tail) -> Option<usize> {
    t.distinct = Some(argv.get(i + 1)?.clone());
    Some(i + 2)
}

fn apply_filter(argv: &[Vec<u8>], i: usize, t: &mut Tail) -> Option<usize> {
    let field = argv.get(i + 1)?.clone();
    let mode = argv.get(i + 2)?;
    if mode.eq_ignore_ascii_case(b"RANGE") {
        let shape = FilterShape::Range(argv.get(i + 3)?.clone(), argv.get(i + 4)?.clone());
        t.filters.push(FilterClause { field, shape });
        Some(i + 5)
    } else if mode.eq_ignore_ascii_case(b"EQ") {
        t.filters.push(FilterClause { field, shape: FilterShape::Eq(argv.get(i + 3)?.clone()) });
        Some(i + 4)
    } else {
        None
    }
}

/// Collect a variadic clause's args from `start` until the next keyword.
fn collect_until_keyword(argv: &[Vec<u8>], start: usize) -> (Vec<Vec<u8>>, usize) {
    let mut i = start;
    let mut out = Vec::new();
    while i < argv.len() && !is_tail_keyword(&argv[i]) {
        out.push(argv[i].clone());
        i += 1;
    }
    (out, i)
}

/// Which clause set a verb's tail admits. `Scalar` (RANGE / EQ) takes
/// the stored-value clauses but not the text-only ones (HIGHLIGHT /
/// TYPO / IN); `Match` takes everything.
#[derive(Clone, Copy, PartialEq)]
pub(super) enum TailMode {
    Scalar,
    Match,
}

pub(super) fn parse_tail(
    argv: &[Vec<u8>],
    mut i: usize,
    default_limit: usize,
    cap: usize,
    mode: TailMode,
) -> Option<Tail> {
    let mut t = Tail {
        limit: default_limit,
        cursor_raw: None,
        fields: Vec::new(),
        highlight: None,
        typo: 0,
        offset: 0,
        scope: Vec::new(),
        filters: Vec::new(),
        sort: None,
        distinct: None,
        facets: Vec::new(),
    };
    while i < argv.len() {
        i = apply_tail_clause(argv, i, &mut t, mode)?;
    }
    t.limit = t.limit.clamp(1, cap);
    t.offset = t.offset.min(10_000);
    Some(t)
}

/// Apply the tail clause starting at `i`; returns the next index, or
/// `None` on a syntax error. The mode gates the text-only clauses
/// (HIGHLIGHT / TYPO / IN) so a RANGE query cannot smuggle them in; the
/// stored-value clauses are shared. A scalar `FIELDS` consumes the rest
/// of the argv (the server's terminal contract); a MATCH one collects
/// up to the next keyword.
fn apply_tail_clause(
    argv: &[Vec<u8>],
    i: usize,
    t: &mut Tail,
    mode: TailMode,
) -> Option<usize> {
    let a = &argv[i];
    if a.eq_ignore_ascii_case(b"LIMIT") {
        t.limit = std::str::from_utf8(argv.get(i + 1)?).ok()?.parse().ok()?;
        Some(i + 2)
    } else if a.eq_ignore_ascii_case(b"CURSOR") {
        t.cursor_raw = Some(argv.get(i + 1)?.clone());
        Some(i + 2)
    } else if a.eq_ignore_ascii_case(b"FIELDS") {
        let (fs, next) = if mode == TailMode::Scalar {
            (argv[i + 1..].to_vec(), argv.len())
        } else {
            collect_until_keyword(argv, i + 1)
        };
        if fs.is_empty() {
            return None;
        }
        t.fields = fs;
        Some(next)
    } else if a.eq_ignore_ascii_case(b"OFFSET") {
        t.offset = std::str::from_utf8(argv.get(i + 1)?).ok()?.parse().ok()?;
        Some(i + 2)
    } else if a.eq_ignore_ascii_case(b"FILTER") {
        apply_filter(argv, i, t)
    } else if a.eq_ignore_ascii_case(b"SORT") {
        apply_sort(argv, i, t)
    } else if a.eq_ignore_ascii_case(b"DISTINCT") {
        apply_distinct(argv, i, t)
    } else if a.eq_ignore_ascii_case(b"FACET") {
        apply_facet(argv, i, t)
    } else if mode == TailMode::Match {
        apply_match_clause(argv, i, t)
    } else {
        None
    }
}