sift-query-core 0.1.0

Backend-independent query model and strict parameter parser for SiftQuery
Documentation
use super::*;

impl<E: SearchEntity> QueryPlan<E> {
    pub fn new(
        query: &UserQuery<E>,
        scope: &ServerScope<E>,
        target: QueryTarget,
    ) -> Result<Self, PlanError> {
        Self::new_with_scopes(query, scope, &GroupScope::unrestricted(), target)
    }

    pub fn new_with_scopes(
        query: &UserQuery<E>,
        scope: &ServerScope<E>,
        group_scope: &GroupScope<E>,
        target: QueryTarget,
    ) -> Result<Self, PlanError> {
        if query.inner.schema_fingerprint != E::META.schema_fingerprint
            || scope.schema_fingerprint != E::META.schema_fingerprint
            || group_scope.schema_fingerprint != E::META.schema_fingerprint
        {
            return Err(PlanError::SchemaMismatch);
        }
        if !E::META.is_grouped() && group_scope.predicate.root().is_some() {
            return Err(ParamError::InvalidValue {
                field: "group scope".to_owned(),
                reason: "HAVING scope requires a grouped entity".to_owned(),
            }
            .into());
        }
        if matches!(
            target,
            QueryTarget::TypedPage | QueryTarget::TypedList | QueryTarget::First
        ) && query.inner.projection.is_dynamic()
        {
            return Err(ParamError::ProjectionNotAllowed.into());
        }
        if target == QueryTarget::First
            && (query.inner.paging.explicit_page
                || query.inner.paging.explicit_size
                || query.inner.paging.explicit_offset
                || query.inner.paging.explicit_cursor)
        {
            return Err(ParamError::PagingNotAllowed.into());
        }

        let projection = match target {
            QueryTarget::MapPage => match &query.inner.projection {
                Projection::All => E::META
                    .fields
                    .iter()
                    .filter(|field| {
                        field.selectable
                            && field.result_alias.is_some()
                            && scope.field_access.allows(field.id)
                    })
                    .map(|field| field.id)
                    .collect(),
                Projection::Fields(fields) => fields.clone(),
            },
            _ => E::META
                .fields
                .iter()
                .filter(|field| field.result_alias.is_some())
                .map(|field| field.id)
                .collect(),
        };

        if target == QueryTarget::MapPage && projection.is_empty() {
            return Err(ParamError::EmptyProjection.into());
        }
        if matches!(target, QueryTarget::MapPage) {
            if let Projection::Fields(fields) = &query.inner.projection {
                Self::validate_fields_with_access(
                    E::META,
                    &scope.field_access,
                    fields.iter().copied(),
                )?;
            }
        } else if matches!(
            target,
            QueryTarget::TypedPage | QueryTarget::TypedList | QueryTarget::First
        ) {
            Self::validate_fields_with_access(
                E::META,
                &scope.field_access,
                projection.iter().copied(),
            )?;
        }

        if let PagingMode::Cursor(cursor) = &query.inner.paging.mode {
            if E::META.is_grouped() {
                return Err(ParamError::InvalidValue {
                    field: "cursor".to_owned(),
                    reason: "cursor pagination does not support grouped entities in M2".to_owned(),
                }
                .into());
            }
            if cursor
                .as_ref()
                .is_some_and(|cursor| cursor.values().len() != query.inner.order.len())
            {
                return Err(ParamError::InvalidValue {
                    field: "cursor".to_owned(),
                    reason: "cursor value count does not match ordering".to_owned(),
                }
                .into());
            }
            if let Some(cursor) = cursor {
                for (order, value) in query.inner.order.iter().zip(cursor.values()) {
                    if E::cursor_key_non_null(order.field)
                        && matches!(value, crate::CursorValue::Null)
                    {
                        return Err(ParamError::InvalidValue {
                            field: "cursor".to_owned(),
                            reason: "non-null cursor key cannot contain NULL".to_owned(),
                        }
                        .into());
                    }
                }
            }
            for order in &query.inner.order {
                if E::META
                    .field(order.field)
                    .is_none_or(|field| !field.value_type.supports_cursor())
                {
                    return Err(ParamError::InvalidValue {
                        field: "cursor".to_owned(),
                        reason: "ordering type is not eligible for cursor pagination".to_owned(),
                    }
                    .into());
                }
                if !projection.contains(&order.field) {
                    let name = E::META
                        .field(order.field)
                        .map_or("<unknown>", |field| field.public_name);
                    return Err(ParamError::NotSelectable(name.to_owned()).into());
                }
            }
        }

        if E::META.distinct
            && matches!(
                target,
                QueryTarget::TypedPage
                    | QueryTarget::MapPage
                    | QueryTarget::TypedList
                    | QueryTarget::First
            )
        {
            for order in &query.inner.order {
                if !projection.contains(&order.field) {
                    let name = E::META
                        .field(order.field)
                        .map_or("<unknown>", |field| field.public_name);
                    return Err(ParamError::DistinctOrderNotProjected(name.to_owned()).into());
                }
            }
        }

        let (row_predicate, group_predicate) = query.inner.predicate.partition()?;
        Self::validate_predicate_with_access(E::META, &scope.field_access, &row_predicate)?;
        Self::validate_predicate_with_access(E::META, &scope.field_access, &group_predicate)?;
        for order in &query.inner.order {
            Self::validate_fields_with_access(E::META, &scope.field_access, [order.field])?;
        }
        let paging = if target == QueryTarget::First {
            Paging {
                size: 1,
                ..Paging::default()
            }
        } else {
            query.inner.paging.clone()
        };
        let plan = Self {
            meta: E::META,
            server_predicate: scope.predicate.clone(),
            server_group_predicate: group_scope.predicate.clone(),
            row_predicate,
            group_predicate,
            projection,
            projection_aliases: None,
            order: query.inner.order.clone(),
            paging,
            target,
            distinct: E::META.distinct,
            force_empty: query.inner.force_empty,
            field_access: scope.field_access.clone(),
            policy_fingerprint: query.inner.policy_fingerprint,
            _entity: PhantomData,
        };
        for order in &plan.order {
            plan.condition_order_value(order.field)?;
        }
        Ok(plan)
    }

    pub fn condition_order_value(
        &self,
        field: FieldId,
    ) -> Result<Option<&crate::ScalarValue>, ParamError> {
        let Some(config) = E::condition_order(field) else {
            return Ok(None);
        };
        let invalid = || {
            ParamError::InvalidValue { field:"orderBy".to_owned(),reason:"computed ordering requires exactly one matching scalar query condition and an offset/page query".to_owned() }
        };
        if self.meta.is_grouped()
            || self.distinct
            || matches!(self.paging.mode, PagingMode::Cursor(_))
        {
            return Err(invalid());
        }
        let mut conditions = self
            .row_predicate
            .nodes()
            .iter()
            .filter_map(|node| match node {
                crate::PredicateNode::Condition(condition)
                    if condition.field == config.source
                        && condition.operator.short_name() == config.operator =>
                {
                    Some(condition)
                }
                _ => None,
            });
        let condition = conditions.next().ok_or_else(invalid)?;
        if conditions.next().is_some() || condition.values.len() != 1 || condition.ignore_case {
            return Err(invalid());
        }
        Ok(condition.values.first())
    }

    pub fn for_projection<P: SearchProjection<E>>(
        query: &UserQuery<E>,
        scope: &ServerScope<E>,
    ) -> Result<Self, PlanError> {
        if query.inner.projection.is_dynamic() {
            return Err(ParamError::ProjectionNotAllowed.into());
        }
        if P::FIELDS.is_empty() {
            return Err(ParamError::EmptyProjection.into());
        }
        let mut seen = std::collections::BTreeSet::new();
        let mut projection = Vec::with_capacity(P::FIELDS.len());
        let mut aliases = Vec::with_capacity(P::FIELDS.len());
        for item in P::FIELDS {
            let field = E::META
                .field(item.source)
                .ok_or_else(|| ParamError::UnknownField(format!("#{}", item.source.0)))?;
            if field.result_alias.is_none() {
                return Err(ParamError::NotSelectable(field.public_name.to_owned()).into());
            }
            if !seen.insert(item.source) {
                return Err(ParamError::DuplicateParameter(field.public_name.to_owned()).into());
            }
            projection.push(item.source);
            aliases.push(item.result_alias);
        }
        let mut plan = Self::new(query, scope, QueryTarget::Count)?;
        plan.target = QueryTarget::StaticPage;
        plan.projection = projection;
        plan.projection_aliases = Some(aliases);
        plan.validate_field_access(plan.projection.iter().copied())?;
        if matches!(plan.paging.mode, PagingMode::Cursor(_)) {
            for order in &plan.order {
                if !plan.projection.contains(&order.field) {
                    let name = E::META
                        .field(order.field)
                        .map_or("<unknown>", |field| field.public_name);
                    return Err(ParamError::NotSelectable(name.to_owned()).into());
                }
            }
        }
        if plan.distinct {
            for order in &plan.order {
                if !plan.projection.contains(&order.field) {
                    let name = E::META
                        .field(order.field)
                        .map_or("<unknown>", |field| field.public_name);
                    return Err(ParamError::DistinctOrderNotProjected(name.to_owned()).into());
                }
            }
        }
        Ok(plan)
    }

    #[must_use]
    pub const fn shape_fingerprint_seed(&self) -> u64 {
        self.meta.schema_fingerprint ^ self.policy_fingerprint.rotate_left(17)
    }

    #[must_use]
    pub fn field_access(&self) -> &FieldAccessPolicy<E> {
        &self.field_access
    }

    pub fn validate_field_access(
        &self,
        fields: impl IntoIterator<Item = FieldId>,
    ) -> Result<(), ParamError> {
        Self::validate_fields_with_access(self.meta, &self.field_access, fields)
    }

    pub fn validate_predicate_access(&self, predicate: &PredicateArena) -> Result<(), ParamError> {
        Self::validate_predicate_with_access(self.meta, &self.field_access, predicate)
    }

    fn validate_fields_with_access(
        meta: &'static crate::EntityMeta,
        access: &FieldAccessPolicy<E>,
        fields: impl IntoIterator<Item = FieldId>,
    ) -> Result<(), ParamError> {
        for field_id in fields {
            let field = meta
                .field(field_id)
                .ok_or_else(|| ParamError::UnknownField(format!("#{}", field_id.0)))?;
            if !access.allows(field_id) {
                return Err(ParamError::FieldAccessDenied(field.public_name.to_owned()));
            }
        }
        Ok(())
    }

    fn validate_predicate_with_access(
        meta: &'static crate::EntityMeta,
        access: &FieldAccessPolicy<E>,
        predicate: &PredicateArena,
    ) -> Result<(), ParamError> {
        let fields = predicate.nodes().iter().filter_map(|node| match node {
            crate::PredicateNode::Condition(condition) => Some(condition.field),
            crate::PredicateNode::And(_) | crate::PredicateNode::Or(_) => None,
        });
        Self::validate_fields_with_access(meta, access, fields)
    }
}