qql-core 0.1.5

Parser, typed AST, validation, and transformations for the Qdrant Query Language
Documentation
use crate::ast::Stmt;
use crate::parser::Parser;

#[test]
fn create_collection_dense() {
    let s = Parser::parse("CREATE COLLECTION docs (dense VECTOR(384, COSINE));").unwrap();
    assert!(matches!(s, Stmt::CreateCollection(_)));
}

#[test]
fn create_collection_with_sparse() {
    let s =
        Parser::parse("CREATE COLLECTION docs (dense VECTOR(768, DOT), sparse SPARSE);").unwrap();
    assert!(matches!(s, Stmt::CreateCollection(_)));
}

#[test]
fn create_collection_explicit_dense_model() {
    let s = Parser::parse("CREATE COLLECTION docs USING DENSE MODEL 'all-minilm:l6-v2';").unwrap();
    assert!(matches!(s, Stmt::CreateCollection(_)));
}

#[test]
fn create_hybrid_with_arbitrary_vector_names() {
    let stmt = Parser::parse(
        "CREATE COLLECTION docs HYBRID \
         DENSE VECTOR semantic_v2 SPARSE VECTOR lexical_v2;",
    )
    .unwrap();
    let Stmt::CreateCollection(create) = stmt else {
        panic!("expected CREATE COLLECTION");
    };
    assert!(matches!(
        create.mode,
        crate::ast::CollectionMode::Hybrid {
            dense_vector: Some(ref dense),
            sparse_vector: Some(ref sparse),
        } if dense == "semantic_v2" && sparse == "lexical_v2"
    ));
}

#[test]
fn create_collection_with_hnsw() {
    let s = Parser::parse(
        "CREATE COLLECTION docs (d VECTOR(128, EUCLID)) WITH HNSW (m = 16, ef_construct = 100);",
    )
    .unwrap();
    assert!(matches!(s, Stmt::CreateCollection(_)));
}

#[test]
fn create_collection_with_params() {
    let s = Parser::parse(
        "CREATE COLLECTION docs (d VECTOR(4, DOT)) WITH PARAMS (replication_factor = 3, on_disk_payload = true);",
    ).unwrap();
    assert!(matches!(s, Stmt::CreateCollection(_)));
}

#[test]
fn alter_collection() {
    let s = Parser::parse("ALTER COLLECTION docs WITH VECTOR (on_disk = true) WITH HNSW (m = 32);")
        .unwrap();
    assert!(matches!(s, Stmt::AlterCollection(_)));
}

#[test]
fn drop_collection() {
    let s = Parser::parse("DROP COLLECTION docs;").unwrap();
    assert!(matches!(s, Stmt::DropCollection(_)));
}

#[test]
fn create_index() {
    let s = Parser::parse(
        "CREATE INDEX ON COLLECTION docs FOR title TYPE text WITH (lowercase = true, tokenizer = 'word');",
    ).unwrap();
    assert!(matches!(s, Stmt::CreateIndex(_)));
}

#[test]
fn create_index_numeric_options() {
    let s = Parser::parse(
        "CREATE INDEX ON COLLECTION docs FOR year TYPE integer \
         WITH (lookup = true, range = true, is_principal = true);",
    )
    .unwrap();
    assert!(matches!(s, Stmt::CreateIndex(_)));
}

#[test]
fn show_collections() {
    let s = Parser::parse("SHOW COLLECTIONS;").unwrap();
    assert!(matches!(s, Stmt::ShowCollections));
}

#[test]
fn show_collection() {
    let s = Parser::parse("SHOW COLLECTION docs;").unwrap();
    assert!(matches!(s, Stmt::ShowCollection(ref c) if c == "docs"));
}

#[test]
fn upsert_simple() {
    let s = Parser::parse("UPSERT INTO docs VALUES {id: 1, title: 'hello', vector: [0.1, 0.2]};")
        .unwrap();
    let Stmt::Upsert(u) = s else { panic!() };
    assert_eq!(u.points.len(), 1);
    assert_eq!(u.collection, "docs");
}

#[test]
fn upsert_with_sparse() {
    let s = Parser::parse(
        "UPSERT INTO docs VALUES {id: 'p1', title: 'doc', vector: {indices: [0, 3], values: [5.0, 8.0]}};",
    ).unwrap();
    assert!(matches!(s, Stmt::Upsert(_)));
}

#[test]
fn upsert_named_vectors() {
    let s = Parser::parse(
        "UPSERT INTO docs VALUES {id: 1, title: 'x', vector: {dense: [1.0, 2.0], sp: {indices: [7], values: [0.5]}}};",
    ).unwrap();
    assert!(matches!(s, Stmt::Upsert(_)));
}

#[test]
fn upsert_with_embedding() {
    let s =
        Parser::parse("UPSERT INTO docs VALUES {id: 1, text: 'hello'} USING DENSE MODEL 'nomic';")
            .unwrap();
    assert!(matches!(s, Stmt::Upsert(_)));
}

#[test]
fn upsert_embedding_targets_may_be_inferred() {
    for source in [
        "UPSERT INTO docs VALUES {id: 1, text: 'hello'} USING DENSE;",
        "UPSERT INTO docs VALUES {id: 1, text: 'hello'} USING SPARSE;",
        "UPSERT INTO docs VALUES {id: 1, text: 'hello'} USING HYBRID;",
    ] {
        assert!(matches!(Parser::parse(source), Ok(Stmt::Upsert(_))));
    }
}

#[test]
fn upsert_with_embed_directive() {
    let s = Parser::parse(
        "UPSERT INTO docs VALUES {id: 1, title: 'doc'} EMBED title INTO dense_vec USING MODEL 'embed';",
    ).unwrap();
    assert!(matches!(s, Stmt::Upsert(_)));
}

#[test]
fn embed_directive_accepts_explicit_dense_role_without_model() {
    let s = Parser::parse(
        "UPSERT INTO docs VALUES {id: 1, title: 'doc'} \
         EMBED title INTO semantic_v2 USING DENSE;",
    )
    .unwrap();
    assert!(matches!(s, Stmt::Upsert(_)));
}

#[test]
fn delete_by_id() {
    let s = Parser::parse("DELETE FROM docs WHERE id = 42;").unwrap();
    assert!(matches!(s, Stmt::Delete(_)));
}

#[test]
fn delete_by_filter() {
    let s = Parser::parse("DELETE FROM docs WHERE status = 'inactive';").unwrap();
    assert!(matches!(s, Stmt::Delete(_)));
}

#[test]
fn update_vector() {
    let s = Parser::parse("UPDATE docs SET VECTOR dense = [0.3, 0.7] WHERE id = 'p1';").unwrap();
    assert!(matches!(s, Stmt::UpdateVector(_)));
}

#[test]
fn update_payload() {
    let s =
        Parser::parse("UPDATE docs SET PAYLOAD = {status: 'active', priority: 5} WHERE id = 42;")
            .unwrap();
    assert!(matches!(s, Stmt::UpdatePayload(_)));
}

#[test]
fn scroll_basic() {
    let s = Parser::parse("SCROLL FROM docs LIMIT 50;").unwrap();
    let Stmt::Scroll(sc) = s else { panic!() };
    assert_eq!(sc.collection, "docs");
    assert_eq!(sc.limit, 50);
    assert!(sc.with_vector.is_none());
}

#[test]
fn scroll_with_filter() {
    let s = Parser::parse("SCROLL FROM docs WHERE active = true AFTER 10 LIMIT 20;").unwrap();
    assert!(matches!(s, Stmt::Scroll(_)));
}

#[test]
fn scroll_with_vector_all() {
    use crate::ast::VectorSelector;
    let s = Parser::parse("SCROLL FROM docs WITH VECTOR LIMIT 10;").unwrap();
    let Stmt::Scroll(sc) = s else { panic!() };
    assert_eq!(sc.with_vector, Some(VectorSelector::All));
}

#[test]
fn query_bare_with_vector_defaults_to_all() {
    use crate::ast::{QueryOutput, VectorSelector};
    let s = Parser::parse("QUERY 'hello' FROM docs WITH VECTOR LIMIT 10;").unwrap();
    let Stmt::Query(q) = s else { panic!() };
    assert_eq!(
        q.output,
        QueryOutput {
            payload: None,
            vectors: Some(VectorSelector::All),
        }
    );
}

#[test]
fn create_index_rejects_legacy_quoted_type() {
    let error =
        Parser::parse("CREATE INDEX ON COLLECTION docs FOR title TYPE 'text';").unwrap_err();
    assert_eq!(error.code, "QQL-PARSE-INDEX-TYPE");
}

#[test]
fn scroll_with_vector_true_and_after() {
    use crate::ast::{PointId, VectorSelector};
    let s = Parser::parse("SCROLL FROM docs AFTER 'abc-uuid' WITH VECTOR true LIMIT 5;").unwrap();
    let Stmt::Scroll(sc) = s else { panic!() };
    assert_eq!(sc.after, Some(PointId::String("abc-uuid".into())));
    assert_eq!(sc.with_vector, Some(VectorSelector::All));
}

#[test]
fn scroll_with_named_vectors() {
    use crate::ast::VectorSelector;
    let s = Parser::parse("SCROLL FROM docs WITH VECTOR (dense, sparse) LIMIT 3;").unwrap();
    let Stmt::Scroll(sc) = s else { panic!() };
    assert_eq!(
        sc.with_vector,
        Some(VectorSelector::Names(vec!["dense".into(), "sparse".into()]))
    );
}