git-vdb 0.1.1

An embedded vector database backed by immutable Git trees
Documentation
use crate::{Condition, Filter, JsonObject, PointId, Range};
use serde_json::Value;

pub(crate) fn matches_filter(filter: &Filter, id: &PointId, payload: &JsonObject) -> bool {
    filter
        .must
        .iter()
        .all(|condition| matches_condition(condition, id, payload))
        && filter
            .must_not
            .iter()
            .all(|condition| !matches_condition(condition, id, payload))
        && (filter.should.is_empty()
            || filter
                .should
                .iter()
                .any(|condition| matches_condition(condition, id, payload)))
}

fn matches_condition(condition: &Condition, id: &PointId, payload: &JsonObject) -> bool {
    match condition {
        Condition::Field {
            key,
            matches,
            range,
        } => {
            let Some(value) = dot_path(payload, key) else {
                return false;
            };
            matches
                .as_ref()
                .is_none_or(|expected| scalar_equal(value, &expected.value))
                && range.as_ref().is_none_or(|range| in_range(value, range))
                && (matches.is_some() || range.is_some())
        }
        Condition::HasId { has_id } => has_id.contains(id),
        Condition::Nested(filter) => matches_filter(filter, id, payload),
    }
}

fn dot_path<'a>(payload: &'a JsonObject, path: &str) -> Option<&'a Value> {
    let mut segments = path.split('.');
    let first = segments.next()?;
    if first.is_empty() {
        return None;
    }
    let mut value = payload.get(first)?;
    for segment in segments {
        if segment.is_empty() {
            return None;
        }
        value = value.as_object()?.get(segment)?;
    }
    Some(value)
}

fn scalar_equal(left: &Value, right: &Value) -> bool {
    match (left, right) {
        (Value::Number(left), Value::Number(right)) => left.as_f64() == right.as_f64(),
        (Value::String(_), Value::String(_))
        | (Value::Bool(_), Value::Bool(_))
        | (Value::Null, Value::Null) => left == right,
        _ => false,
    }
}

fn in_range(value: &Value, range: &Range) -> bool {
    let Some(number) = value.as_f64() else {
        return false;
    };
    range.gt.is_none_or(|bound| number > bound)
        && range.gte.is_none_or(|bound| number >= bound)
        && range.lt.is_none_or(|bound| number < bound)
        && range.lte.is_none_or(|bound| number <= bound)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Condition, Filter, MatchValue};
    use serde_json::json;

    #[test]
    fn nested_qdrant_style_filter() {
        let payload = json!({"meta": {"topic": "rust"}, "year": 2026})
            .as_object()
            .unwrap()
            .clone();
        let filter = Filter {
            must: vec![Condition::Field {
                key: "meta.topic".into(),
                matches: Some(MatchValue {
                    value: json!("rust"),
                }),
                range: None,
            }],
            must_not: vec![Condition::matches("year", 2025)],
            should: vec![],
        };
        assert!(matches_filter(&filter, &PointId::from("a"), &payload));
    }
}