sift-query-sqlx 0.1.0

PostgreSQL renderer and sqlx executor for SiftQuery
Documentation
use sift_query_core::{
    CursorCodec, CursorValue, CustomOperator, EntityLimits, EntityMeta, FieldId, FieldKind,
    FieldMeta, FieldRef, FilterExpr, Operator, OperatorSet, OrderMeta, ParamParser, QueryBuilder,
    QueryPlan, QueryPolicy, QueryTarget, RawParams, ScalarKind, ScalarValue, SearchEntity,
    ServerScope, SortOrder, ValueType,
};
use sift_query_sqlx::{PgBindValue, PgRenderer};

const ID_OPS: &[Operator] = &[
    Operator::Eq,
    Operator::Ne,
    Operator::Gt,
    Operator::Ge,
    Operator::Lt,
    Operator::Le,
    Operator::Between,
    Operator::NotBetween,
    Operator::InList,
    Operator::NotIn,
    Operator::IsNull,
    Operator::NotNull,
    Operator::AlwaysTrue,
    Operator::AlwaysFalse,
];
const NAME_OPS: &[Operator] = &[
    Operator::Eq,
    Operator::Ne,
    Operator::IsNull,
    Operator::NotNull,
    Operator::StartWith,
    Operator::EndWith,
    Operator::Contain,
    Operator::OrLike,
    Operator::NotLike,
    Operator::Empty,
    Operator::NotEmpty,
];
static FIELDS: &[FieldMeta] = &[
    FieldMeta {
        id: FieldId(0),
        public_name: "id",
        column: "u.id",
        result_alias: Some("id"),
        value_type: ValueType::I64,
        kind: FieldKind::Scalar,
        filterable: true,
        selectable: true,
        sortable: true,
        operators: OperatorSet(ID_OPS),
    },
    FieldMeta {
        id: FieldId(1),
        public_name: "name",
        column: "u.name",
        result_alias: Some("name"),
        value_type: ValueType::Text,
        kind: FieldKind::Scalar,
        filterable: true,
        selectable: true,
        sortable: true,
        operators: OperatorSet(NAME_OPS),
    },
];
static ORDER: &[OrderMeta] = &[OrderMeta {
    field: FieldId(0),
    direction: SortOrder::Asc,
}];
static META: EntityMeta = EntityMeta {
    entity_name: "User",
    from: "app_user u",
    base_where: Some("u.deleted = false"),
    fixed_having: None,
    distinct: false,
    fields: FIELDS,
    default_order: ORDER,
    stable_order: ORDER,
    limits: EntityLimits {
        max_page_size: 100,
        max_offset: 20_000,
        timeout_ms: 5_000,
    },
    schema_fingerprint: 7,
};

struct User;
impl SearchEntity for User {
    const META: &'static EntityMeta = &META;
}

static DISTINCT_META: EntityMeta = EntityMeta {
    distinct: true,
    schema_fingerprint: 8,
    ..META
};

#[derive(Debug)]
struct DistinctUser;
impl SearchEntity for DistinctUser {
    const META: &'static EntityMeta = &DISTINCT_META;
}

const SCORE_OPS: &[Operator] = &[Operator::Ge];
static SCORE_FIELDS: &[FieldMeta] = &[
    FieldMeta {
        id: FieldId(0),
        public_name: "courseId",
        column: "s.course_id",
        result_alias: Some("course_id"),
        value_type: ValueType::I64,
        kind: FieldKind::GroupKey,
        filterable: true,
        selectable: true,
        sortable: true,
        operators: OperatorSet(ID_OPS),
    },
    FieldMeta {
        id: FieldId(1),
        public_name: "total",
        column: "sum(s.score)",
        result_alias: Some("total"),
        value_type: ValueType::I64,
        kind: FieldKind::Aggregate,
        filterable: true,
        selectable: true,
        sortable: true,
        operators: OperatorSet(SCORE_OPS),
    },
];
static SCORE_ORDER: &[OrderMeta] = &[OrderMeta {
    field: FieldId(0),
    direction: SortOrder::Asc,
}];
static SCORE_META: EntityMeta = EntityMeta {
    entity_name: "Score",
    from: "student_score s",
    base_where: None,
    fixed_having: None,
    distinct: false,
    fields: SCORE_FIELDS,
    default_order: SCORE_ORDER,
    stable_order: SCORE_ORDER,
    limits: EntityLimits {
        max_page_size: 100,
        max_offset: 20_000,
        timeout_ms: 5_000,
    },
    schema_fingerprint: 9,
};

struct Score;
impl SearchEntity for Score {
    const META: &'static EntityMeta = &SCORE_META;
}

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

#[test]
fn in_list_uses_one_array_slot_and_stable_shape() {
    let one = PgRenderer.render_list(&plan("id=1&id-op=il")).unwrap();
    let many = PgRenderer.render_list(&plan("id=1&id=2&id-op=il")).unwrap();
    assert_eq!(one.sql, many.sql);
    assert_eq!(one.shape_id, many.shape_id);
    assert_eq!(many.binds.len(), 3);
    assert!(many.sql.contains("u.id = ANY($1)"));
    assert!(many.sql.ends_with("LIMIT $2 OFFSET $3"));
}

#[test]
fn contain_escapes_user_wildcards() {
    let rendered = PgRenderer
        .render_list(&plan("name=a%25_%5C&name-op=ct"))
        .unwrap();
    assert!(rendered.sql.contains("u.name LIKE $1 ESCAPE '\\'"));
    assert_eq!(
        rendered.binds[0],
        PgBindValue::Scalar(ScalarValue::Text("%a\\%\\_\\\\%".to_owned()))
    );
}

#[test]
fn count_omits_order_and_paging() {
    let rendered = PgRenderer.render_count(&plan("name=Alice")).unwrap();
    assert!(!rendered.sql.contains("ORDER BY"));
    assert!(!rendered.sql.contains("LIMIT"));
    assert_eq!(rendered.binds.len(), 1);
}

#[test]
fn facet_rendering_is_bounded_and_reuses_authorization_predicates() {
    let plan = plan("name=Al&name-op=sw");
    let rendered = PgRenderer
        .render_facet(&plan, FieldId(1), 12, false)
        .unwrap();
    assert!(
        rendered
            .sql
            .contains("u.deleted = false AND u.name LIKE $1 ESCAPE '\\'")
    );
    assert!(rendered.sql.contains("u.name IS NOT NULL"));
    assert!(rendered.sql.contains("GROUP BY u.name"));
    assert!(
        rendered
            .sql
            .contains("ORDER BY count(*) DESC, u.name ASC LIMIT $2")
    );
    assert_eq!(rendered.binds.len(), 2);
}

#[test]
fn facet_can_exclude_its_own_user_filter() {
    let plan = plan("name=Al&name-op=sw");
    let rendered = PgRenderer
        .render_facet_with_options(&plan, FieldId(1), 12, true, true)
        .unwrap();
    assert!(rendered.sql.contains("u.deleted = false"));
    assert!(!rendered.sql.contains("u.name LIKE"));
    assert!(rendered.sql.contains("LIMIT $1"));
    assert_eq!(rendered.binds.len(), 1);
}

#[test]
fn renders_all_builtin_operator_shapes() {
    let cases = [
        ("id=1", "u.id = $1"),
        ("id=1&id-op=ne", "u.id <> $1"),
        ("id=1&id-op=gt", "u.id > $1"),
        ("id=1&id-op=ge", "u.id >= $1"),
        ("id=1&id-op=lt", "u.id < $1"),
        ("id=1&id-op=le", "u.id <= $1"),
        ("id=1&id=2&id-op=bt", "u.id BETWEEN $1 AND $2"),
        ("id=1&id=2&id-op=nb", "u.id NOT BETWEEN $1 AND $2"),
        ("id=1&id=2&id-op=il", "u.id = ANY($1)"),
        ("id=1&id=2&id-op=ni", "u.id <> ALL($1)"),
        ("id-op=nl", "u.id IS NULL"),
        ("id-op=nn", "u.id IS NOT NULL"),
        ("name-op=ey", "(u.name IS NULL OR u.name = '')"),
        ("name-op=ny", "(u.name IS NOT NULL AND u.name <> '')"),
        ("name=Al&name-op=sw", "u.name LIKE $1 ESCAPE '\\'"),
        ("name=ce&name-op=ew", "u.name LIKE $1 ESCAPE '\\'"),
        ("name=li&name-op=ct", "u.name LIKE $1 ESCAPE '\\'"),
        ("name=%25A%25&name-op=nk", "u.name NOT LIKE $1"),
        (
            "name=A%25&name=B%25&name-op=ol",
            "(u.name LIKE $1 OR u.name LIKE $2)",
        ),
        ("id-op=at", "1 = 1"),
        ("id-op=af", "1 = 0"),
    ];
    for (query, expected) in cases {
        let rendered = PgRenderer.render_list(&plan(query)).unwrap();
        assert!(
            rendered.sql.contains(expected),
            "expected {expected:?} in {:?}",
            rendered.sql
        );
    }
}

#[test]
fn grouped_count_uses_minimal_inner_projection() {
    let raw = RawParams::parse("total=10&total-op=ge").unwrap();
    let policy = QueryPolicy::default();
    let query = ParamParser::new(&policy)
        .unwrap()
        .parse::<Score>(&raw)
        .unwrap();
    let plan = QueryPlan::new(&query, &ServerScope::unrestricted(), QueryTarget::Count).unwrap();
    let rendered = PgRenderer.render_count(&plan).unwrap();
    assert_eq!(
        rendered.sql.as_ref(),
        "SELECT count(*) AS \"sq_count\" FROM (SELECT 1 FROM student_score s \
         GROUP BY s.course_id HAVING sum(s.score) >= $1) sq_cluster"
    );
}

#[test]
fn distinct_map_rejects_a_hidden_stable_order_column() {
    let raw = RawParams::parse("onlySelect=name").unwrap();
    let policy = QueryPolicy::default();
    let query = ParamParser::new(&policy)
        .unwrap()
        .parse::<DistinctUser>(&raw)
        .unwrap();
    let error =
        QueryPlan::new(&query, &ServerScope::unrestricted(), QueryTarget::MapPage).unwrap_err();
    assert!(matches!(
        error,
        sift_query_core::PlanError::Param(sift_query_core::ParamError::DistinctOrderNotProjected(
            _
        ))
    ));
}

#[test]
fn cursor_uses_keyset_predicate_without_offset() {
    let codec = CursorCodec::new([0x44; 32]).unwrap();
    let policy = QueryPolicy::default().with_cursor_codec(codec.clone());
    let base = ParamParser::new(&policy)
        .unwrap()
        .parse::<User>(&RawParams::parse("cursor=&size=10").unwrap())
        .unwrap();
    let token = codec
        .encode::<User>(base.order(), &[CursorValue::Scalar(ScalarValue::I64(10))])
        .unwrap();
    let raw = RawParams::parse(&format!("cursor={token}&size=10")).unwrap();
    let query = ParamParser::new(&policy)
        .unwrap()
        .parse::<User>(&raw)
        .unwrap();
    let plan =
        QueryPlan::new(&query, &ServerScope::unrestricted(), QueryTarget::TypedPage).unwrap();
    let rendered = PgRenderer.render_list(&plan).unwrap();
    assert!(rendered.sql.contains("(u.id > $1 OR u.id IS NULL)"));
    assert!(rendered.sql.ends_with("LIMIT $2"));
    assert!(!rendered.sql.contains("OFFSET"));
}

#[test]
fn custom_operator_uses_only_validated_field_and_bind_placeholders() {
    let operator = CustomOperator::new("multiple_of", "mod({field}, {0}) = 0", 1).unwrap();
    let policy = QueryPolicy::default();
    let field = FieldRef::<User, i64, ScalarKind>::new(FieldId(0));
    let expression = FilterExpr::custom(field, operator, [2_i64]).unwrap();
    let query = QueryBuilder::<User>::new(&policy)
        .filter(expression)
        .build()
        .unwrap();
    let plan =
        QueryPlan::new(&query, &ServerScope::unrestricted(), QueryTarget::TypedPage).unwrap();
    let rendered = PgRenderer.render_list(&plan).unwrap();
    assert!(rendered.sql.contains("mod(u.id, $1) = 0"));
}