sift-query-sqlx 0.1.0

PostgreSQL renderer and sqlx executor for SiftQuery
Documentation
#[allow(dead_code, unused_imports)]
#[path = "renderer_coverage/support.rs"]
mod support;

use sift_query_sqlx::QueryShapeCache;
use std::sync::Arc;
use support::*;

fn make_plan<E: SearchEntity>(raw: &str) -> QueryPlan<E> {
    let policy = QueryPolicy::default();
    let query = ParamParser::new(&policy)
        .unwrap()
        .parse::<E>(&RawParams::parse(raw).unwrap())
        .unwrap();
    QueryPlan::new(&query, &ServerScope::unrestricted(), QueryTarget::MapPage).unwrap()
}

#[test]
fn hot_templates_rebuild_current_scalars_arrays_like_and_paging() {
    for (first, second) in [
        ("id=1", "id=9"),
        ("id=1&id-op=ne", "id=9&id-op=ne"),
        ("id=1&id=2&id-op=bt", "id=3&id=4&id-op=bt"),
        ("id=1&id=2&id-op=nb", "id=3&id=4&id-op=nb"),
        ("id=1&id-op=il", "id=3&id=4&id=5&id-op=il"),
        ("id=1&id-op=ni", "id=3&id=4&id-op=ni"),
        ("name=a%25&name-op=sw", "name=b_&name-op=sw"),
        ("name=a%25&name-op=ew", "name=b_&name-op=ew"),
        (
            "name=a%25&name-op=ct&name-ic=true",
            "name=b_&name-op=ct&name-ic=true",
        ),
        ("name=a&name=b&name-op=ol", "name=c&name=d&name-op=ol"),
        (
            "name=a&name-op=nk&name-ic=true",
            "name=b&name-op=nk&name-ic=true",
        ),
        ("id=1&size=2&page=1", "id=3&size=4&page=2"),
        ("id-op=nl", "id-op=nl"),
        ("id-op=at", "id-op=at"),
    ] {
        let cache = QueryShapeCache::new(4).unwrap();
        let first = make_plan::<User>(first);
        let second = make_plan::<User>(second);
        let (cold, hit) = cache.render_list(&first).unwrap();
        assert!(!hit);
        assert_eq!(cold, PgRenderer.render_list(&first).unwrap());
        let (hot, hit) = cache.render_list(&second).unwrap();
        assert!(hit);
        assert!(Arc::ptr_eq(&cold.sql, &hot.sql));
        assert_eq!(hot, PgRenderer.render_list(&second).unwrap());
    }
}

#[test]
fn topology_case_projection_order_and_operator_arity_do_not_alias() {
    let pairs = [
        ("a.id=1&b.age=2&gexpr=a|b", "a.id=1&b.age=2&gexpr=a%26b"),
        ("name=x&name-op=ct", "name=x&name-op=ct&name-ic=true"),
        ("name=x&name-op=ol", "name=x&name=y&name-op=ol"),
        ("onlySelect=id,name", "onlySelect=id,age"),
        ("sort=id&order=asc", "sort=id&order=desc"),
    ];
    for (first, second) in pairs {
        let cache = QueryShapeCache::new(2).unwrap();
        cache.render_list(&make_plan::<User>(first)).unwrap();
        let second = make_plan::<User>(second);
        let (rendered, hit) = cache.render_list(&second).unwrap();
        assert!(!hit, "{first} / {second:?}");
        assert_eq!(rendered, PgRenderer.render_list(&second).unwrap());
    }
}

fn check_all<E: SearchEntity>(
    plan: &QueryPlan<E>,
    options: Option<&SearchOptions<E>>,
    field: FieldId,
) {
    let cache = QueryShapeCache::new(8).unwrap();
    for _ in 0..2 {
        assert_eq!(
            cache.render_list(plan).unwrap().0,
            PgRenderer.render_list(plan).unwrap()
        );
        assert_eq!(
            cache.render_cluster(plan, options).unwrap().0,
            PgRenderer.render_cluster(plan, options).unwrap()
        );
        assert_eq!(
            cache.render_sum(plan, field).unwrap().0,
            PgRenderer.render_sum(plan, field).unwrap()
        );
    }
    assert_eq!(cache.stats().hits, 3);
}

#[test]
fn grouped_distinct_summaries_and_both_server_scopes_keep_their_semantics() {
    let query = ParamParser::new(&QueryPolicy::default())
        .unwrap()
        .parse::<Score>(&RawParams::parse("total=5&total-op=gt").unwrap())
        .unwrap();
    let row = ServerScope::new()
        .ge(
            FieldRef::<Score, i64, sift_query_core::GroupKeyKind>::new(FieldId(0)),
            10,
        )
        .build()
        .unwrap();
    let group = sift_query_core::GroupScope::new()
        .ge(
            FieldRef::<Score, i64, sift_query_core::AggregateKind>::new(FieldId(1)),
            2,
        )
        .build()
        .unwrap();
    let plan = QueryPlan::new_with_scopes(&query, &row, &group, QueryTarget::MapPage).unwrap();
    let total = FieldRef::<Score, i64, sift_query_core::AggregateKind>::new(FieldId(1));
    let options = SearchOptions::builder()
        .sum(total)
        .avg(total)
        .max(total)
        .build()
        .unwrap();
    check_all(&plan, Some(&options), FieldId(1));
    let distinct = make_plan::<DistinctUser>("id=1&name=x&name-op=ct");
    let id = FieldRef::<DistinctUser, i64, ScalarKind>::new(FieldId(0));
    check_all(
        &distinct,
        Some(&SearchOptions::builder().sum(id).build().unwrap()),
        FieldId(0),
    );
    let cache = QueryShapeCache::new(2).unwrap();
    let plan = make_plan::<User>("id=1&size=3");
    assert!(!cache.render_window_list(&plan).unwrap().1);
    let plan = make_plan::<User>("id=8&size=4");
    let (hot, hit) = cache.render_window_list(&plan).unwrap();
    assert!(hit);
    assert_eq!(hot, PgRenderer.render_window_list(&plan).unwrap());
}

#[test]
fn templates_are_bounded_and_thread_safe() {
    let cache = Arc::new(QueryShapeCache::new(2).unwrap());
    cache.render_list(&make_plan::<User>("id=1")).unwrap();
    std::thread::scope(|threads| {
        for value in 0..8 {
            let cache = Arc::clone(&cache);
            threads.spawn(move || {
                let plan = make_plan::<User>(&format!("id={value}"));
                assert_eq!(
                    cache.render_list(&plan).unwrap().0,
                    PgRenderer.render_list(&plan).unwrap()
                );
            });
        }
    });
    assert_eq!(cache.stats().hits, 8);
    cache.render_list(&make_plan::<User>("age=2")).unwrap();
    cache.render_list(&make_plan::<User>("name=x")).unwrap();
    assert_eq!(cache.stats().entries, 2);
    assert_eq!(cache.stats().evictions, 1);
    cache.clear();
    assert_eq!(cache.stats().entries, 0);
}

#[test]
fn cursor_null_shapes_and_repeated_keyset_binds_match_uncached_rendering() {
    use sift_query_core::{CursorCodec, CursorValue};
    let codec = CursorCodec::new([42; 32]).unwrap();
    let policy = QueryPolicy::default().with_cursor_codec(codec.clone());
    let parser = ParamParser::new(&policy).unwrap();
    let base = parser
        .parse::<User>(&RawParams::parse("sort=name&order=asc&cursor=").unwrap())
        .unwrap();
    let cache = QueryShapeCache::new(8).unwrap();
    for values in [
        vec![
            CursorValue::Scalar(ScalarValue::Text("A".to_owned())),
            CursorValue::Scalar(ScalarValue::I64(1)),
        ],
        vec![
            CursorValue::Scalar(ScalarValue::Text("B".to_owned())),
            CursorValue::Scalar(ScalarValue::I64(2)),
        ],
        vec![CursorValue::Null, CursorValue::Scalar(ScalarValue::I64(2))],
        vec![
            CursorValue::Scalar(ScalarValue::Text("B".to_owned())),
            CursorValue::Null,
        ],
        vec![CursorValue::Null, CursorValue::Null],
    ] {
        let token = codec.encode::<User>(base.order(), &values).unwrap();
        let query = parser
            .parse::<User>(
                &RawParams::parse(&format!("sort=name&order=asc&cursor={token}")).unwrap(),
            )
            .unwrap();
        let plan =
            QueryPlan::new(&query, &ServerScope::unrestricted(), QueryTarget::MapPage).unwrap();
        for _ in 0..2 {
            assert_eq!(
                cache.render_list(&plan).unwrap().0,
                PgRenderer.render_list(&plan).unwrap()
            );
        }
    }
    assert_eq!(cache.stats().entries, 4);
}

#[test]
fn current_scope_values_and_custom_templates_are_never_confused() {
    let cache = QueryShapeCache::new(4).unwrap();
    let policy = QueryPolicy::default();
    let query = QueryBuilder::<User>::new(&policy)
        .eq(FieldRef::<User, i64, ScalarKind>::new(FieldId(0)), 2)
        .build()
        .unwrap();
    for value in [7, 8] {
        let scope = ServerScope::new()
            .eq(FieldRef::<User, i64, ScalarKind>::new(FieldId(0)), value)
            .build()
            .unwrap();
        let plan = QueryPlan::new(&query, &scope, QueryTarget::TypedPage).unwrap();
        assert_eq!(
            cache.render_list(&plan).unwrap().0,
            PgRenderer.render_list(&plan).unwrap()
        );
    }
    let field = FieldRef::<User, i64, ScalarKind>::new(FieldId(0));
    for template in ["{field} > {0}", "{field} < {0}"] {
        let operator = CustomOperator::new("compare", template, 1).unwrap();
        let filter = FilterExpr::custom(field, operator, [3_i64]).unwrap();
        let query = QueryBuilder::<User>::new(&policy)
            .filter(filter)
            .build()
            .unwrap();
        let plan =
            QueryPlan::new(&query, &ServerScope::unrestricted(), QueryTarget::TypedPage).unwrap();
        assert!(!cache.render_list(&plan).unwrap().1);
        assert_eq!(
            cache.render_list(&plan).unwrap().0,
            PgRenderer.render_list(&plan).unwrap()
        );
    }
}

#[test]
fn warm_templates_preserve_renderer_validation_errors() {
    use sift_query_core::{Condition, PredicateArena, PredicatePhase};
    let cache = QueryShapeCache::new(4).unwrap();
    let mut plan = make_plan::<User>("name=x&name-op=nk");
    cache.render_list(&plan).unwrap();
    plan.row_predicate = PredicateArena::from_condition(Condition {
        field: FieldId(1),
        operator: Operator::NotLike,
        values: vec![ScalarValue::I64(1)],
        ignore_case: false,
        phase: PredicatePhase::Row,
    });
    assert!(PgRenderer.render_list(&plan).is_err());
    assert!(cache.render_list(&plan).is_err());
    let mut plan = make_plan::<User>("size=3");
    cache.render_window_list(&plan).unwrap();
    plan.paging.size = 0;
    assert_eq!(
        cache.render_window_list(&plan).unwrap_err(),
        RenderError::WrongTarget
    );
}

#[test]
fn warm_in_list_templates_recheck_element_types() {
    use sift_query_core::{Condition, PredicateArena, PredicatePhase};
    let cache = QueryShapeCache::new(4).unwrap();
    let mut plan = make_plan::<User>("id=1&id=2&id-op=il");
    cache.render_list(&plan).unwrap();
    plan.row_predicate = PredicateArena::from_condition(Condition {
        field: FieldId(0),
        operator: Operator::InList,
        values: vec![ScalarValue::Text("wrong-type".to_owned())],
        ignore_case: false,
        phase: PredicatePhase::Row,
    });
    assert!(PgRenderer.render_list(&plan).is_err());
    assert!(cache.render_list(&plan).is_err());
}