datacules-agentdb 0.4.2

Single-file embedded database for AI agents. SQL + Vector Search + Full-Text Search + Hybrid Queries + Memory Graphs.
Documentation
#[cfg(test)]
mod tests {
    use agentdb::filter_matches;
    use serde_json::json;

    #[test]
    fn test_exact_match_pass() {
        assert!(filter_matches(
            &json!({"role":"user"}),
            &json!({"role":"user"})
        ));
    }

    #[test]
    fn test_exact_match_fail() {
        assert!(!filter_matches(
            &json!({"role":"user"}),
            &json!({"role":"agent"})
        ));
    }

    #[test]
    fn test_gt_pass() {
        assert!(filter_matches(
            &json!({"score":8}),
            &json!({"score":{"$gt":5}})
        ));
    }

    #[test]
    fn test_gt_fail() {
        assert!(!filter_matches(
            &json!({"score":3}),
            &json!({"score":{"$gt":5}})
        ));
    }

    #[test]
    fn test_gte_boundary() {
        assert!(filter_matches(&json!({"v":7}), &json!({"v":{"$gte":7}})));
        assert!(!filter_matches(&json!({"v":6}), &json!({"v":{"$gte":7}})));
    }

    #[test]
    fn test_lt_lte() {
        assert!(filter_matches(&json!({"v":3}), &json!({"v":{"$lt":5}})));
        assert!(filter_matches(&json!({"v":5}), &json!({"v":{"$lte":5}})));
        assert!(!filter_matches(&json!({"v":6}), &json!({"v":{"$lte":5}})));
    }

    #[test]
    fn test_ne() {
        assert!(filter_matches(
            &json!({"role":"user"}),
            &json!({"role":{"$ne":"agent"}})
        ));
        assert!(!filter_matches(
            &json!({"role":"user"}),
            &json!({"role":{"$ne":"user"}})
        ));
    }

    #[test]
    fn test_in_pass() {
        assert!(filter_matches(
            &json!({"lang":"en"}),
            &json!({"lang":{"$in":["en","fr"]}})
        ));
    }

    #[test]
    fn test_in_fail() {
        assert!(!filter_matches(
            &json!({"lang":"de"}),
            &json!({"lang":{"$in":["en","fr"]}})
        ));
    }

    #[test]
    fn test_nin() {
        assert!(filter_matches(
            &json!({"lang":"de"}),
            &json!({"lang":{"$nin":["en","fr"]}})
        ));
        assert!(!filter_matches(
            &json!({"lang":"en"}),
            &json!({"lang":{"$nin":["en","fr"]}})
        ));
    }

    #[test]
    fn test_exists_true() {
        assert!(filter_matches(
            &json!({"score":5}),
            &json!({"score":{"$exists":true}})
        ));
        assert!(!filter_matches(
            &json!({"other":1}),
            &json!({"score":{"$exists":true}})
        ));
    }

    #[test]
    fn test_exists_false() {
        assert!(filter_matches(
            &json!({"other":1}),
            &json!({"score":{"$exists":false}})
        ));
    }

    #[test]
    fn test_multi_field_all_pass() {
        assert!(filter_matches(
            &json!({"lang":"en","score":9,"role":"user"}),
            &json!({"lang":"en","score":{"$gte":8},"role":{"$ne":"agent"}})
        ));
    }

    #[test]
    fn test_multi_field_one_fail() {
        assert!(!filter_matches(
            &json!({"lang":"en","score":6}),
            &json!({"lang":"en","score":{"$gte":8}})
        ));
    }
}

#[cfg(test)]
mod batch_tests {
    use agentdb::{AgentDB, BatchEntry, DistanceMetric, SearchOptions};
    use serde_json::json;

    fn open() -> AgentDB {
        AgentDB::open(":memory:").unwrap()
    }

    fn v(seed: f32) -> Vec<f32> {
        vec![seed, seed * 0.5, seed * 0.25, seed * 0.1]
    }

    #[test]
    fn test_batch_upsert_count() {
        let db = open();
        let col = db.vectors().collection("col", 4).unwrap();
        let batch = (0..50u32)
            .map(|i| BatchEntry {
                id: format!("v{}", i),
                vector: v(i as f32 / 50.0),
                metadata: Some(json!({"i": i})),
            })
            .collect();
        let n = col.upsert_batch(batch).unwrap();
        assert_eq!(n, 50);
        assert_eq!(col.count().unwrap(), 50);
    }

    #[test]
    fn test_batch_dim_mismatch_rolls_back() {
        let db = open();
        let col = db.vectors().collection("col", 4).unwrap();
        let batch = vec![
            BatchEntry {
                id: "good".into(),
                vector: vec![1.0, 0.0, 0.0, 0.0],
                metadata: None,
            },
            BatchEntry {
                id: "bad".into(),
                vector: vec![1.0, 0.0],
                metadata: None,
            },
        ];
        assert!(col.upsert_batch(batch).is_err());
        assert_eq!(col.count().unwrap(), 0);
    }

    #[test]
    fn test_batch_empty_ok() {
        let db = open();
        let col = db.vectors().collection("col", 4).unwrap();
        let n = col.upsert_batch(vec![]).unwrap();
        assert_eq!(n, 0);
    }

    #[test]
    fn test_batch_then_search() {
        let db = open();
        let col = db.vectors().collection("col", 4).unwrap();
        let batch = (0..20u32)
            .map(|i| BatchEntry {
                id: format!("v{}", i),
                vector: v(i as f32 / 20.0),
                metadata: Some(json!({"idx": i})),
            })
            .collect();
        col.upsert_batch(batch).unwrap();
        let results = col
            .search(
                &[0.95, 0.475, 0.2375, 0.095],
                SearchOptions {
                    top_k: 3,
                    metric: DistanceMetric::Cosine,
                    filter: None,
                },
            )
            .unwrap();
        assert_eq!(results.len(), 3);
    }

    #[test]
    fn test_filter_gt_after_batch() {
        let db = open();
        let col = db.vectors().collection("col", 4).unwrap();
        let batch = (0..10u32)
            .map(|i| BatchEntry {
                id: format!("v{}", i),
                vector: v(i as f32 / 10.0 + 0.1),
                metadata: Some(json!({"score": i})),
            })
            .collect();
        col.upsert_batch(batch).unwrap();
        let results = col
            .search(
                &[0.5, 0.25, 0.125, 0.05],
                SearchOptions {
                    top_k: 10,
                    metric: DistanceMetric::Cosine,
                    filter: Some(json!({"score": {"$gt": 5}})),
                },
            )
            .unwrap();
        assert!(results
            .iter()
            .all(|r| { r.metadata.as_ref().unwrap()["score"].as_u64().unwrap() > 5 }));
    }
}

#[cfg(test)]
mod hybrid_tests {
    use agentdb::{AgentDB, BatchEntry, HybridQuery};

    fn open() -> AgentDB {
        AgentDB::open(":memory:").unwrap()
    }

    fn v(seed: f32) -> Vec<f32> {
        (0..8)
            .map(|i| ((seed + i as f32) * 0.1).sin().abs())
            .collect()
    }

    #[test]
    fn test_hybrid_returns_top_k() {
        let db = open();
        let col = db.vectors().collection("docs", 8).unwrap();
        let batch = vec![
            BatchEntry {
                id: "a".into(),
                vector: v(1.0),
                metadata: None,
            },
            BatchEntry {
                id: "b".into(),
                vector: v(2.0),
                metadata: None,
            },
            BatchEntry {
                id: "c".into(),
                vector: v(3.0),
                metadata: None,
            },
        ];
        col.upsert_batch(batch).unwrap();
        let graph = db.memory();
        graph.add_node("root", "session", None).unwrap();
        graph.add_node("a", "doc", None).unwrap();
        graph.add_node("b", "doc", None).unwrap();
        graph.add_edge("root", "a", "read", 0.9).unwrap();
        graph.add_edge("root", "b", "read", 0.5).unwrap();
        let results = db
            .hybrid_query(HybridQuery {
                anchor_node: "root",
                embedding: &v(1.0),
                collection: "docs",
                graph_depth: 1,
                top_k: 2,
                alpha: 0.5,
                filter: None,
            })
            .unwrap();
        assert_eq!(results.len(), 2);
    }

    #[test]
    fn test_hybrid_alpha_pure_vector() {
        let db = open();
        let col = db.vectors().collection("docs", 8).unwrap();
        col.upsert_batch(vec![BatchEntry {
            id: "x".into(),
            vector: v(1.0),
            metadata: None,
        }])
        .unwrap();
        let graph = db.memory();
        graph.add_node("root", "session", None).unwrap();
        let results = db
            .hybrid_query(HybridQuery {
                anchor_node: "root",
                embedding: &v(1.0),
                collection: "docs",
                graph_depth: 1,
                top_k: 1,
                alpha: 1.0,
                filter: None,
            })
            .unwrap();
        assert!(!results.is_empty());
        assert!(results[0].graph_weight == 0.0);
    }

    #[test]
    fn test_hybrid_graph_boosts_rank() {
        let db = open();
        let col = db.vectors().collection("docs", 8).unwrap();
        col.upsert_batch(vec![
            BatchEntry {
                id: "connected".into(),
                vector: v(2.0),
                metadata: None,
            },
            BatchEntry {
                id: "disconnected".into(),
                vector: v(2.1),
                metadata: None,
            },
        ])
        .unwrap();
        let graph = db.memory();
        graph.add_node("root", "session", None).unwrap();
        graph.add_node("connected", "doc", None).unwrap();
        graph.add_node("disconnected", "doc", None).unwrap();
        graph.add_edge("root", "connected", "read", 1.0).unwrap();
        let results = db
            .hybrid_query(HybridQuery {
                anchor_node: "root",
                embedding: &v(2.0),
                collection: "docs",
                graph_depth: 1,
                top_k: 2,
                alpha: 0.4,
                filter: None,
            })
            .unwrap();
        let ids: Vec<&str> = results.iter().map(|r| r.id.as_str()).collect();
        assert_eq!(ids[0], "connected");
    }
}