Skip to main content

sift_query_core/query/plan/
construct.rs

1use super::*;
2
3impl<E: SearchEntity> QueryPlan<E> {
4    pub fn new(
5        query: &UserQuery<E>,
6        scope: &ServerScope<E>,
7        target: QueryTarget,
8    ) -> Result<Self, PlanError> {
9        Self::new_with_scopes(query, scope, &GroupScope::unrestricted(), target)
10    }
11
12    pub fn new_with_scopes(
13        query: &UserQuery<E>,
14        scope: &ServerScope<E>,
15        group_scope: &GroupScope<E>,
16        target: QueryTarget,
17    ) -> Result<Self, PlanError> {
18        if query.inner.schema_fingerprint != E::META.schema_fingerprint
19            || scope.schema_fingerprint != E::META.schema_fingerprint
20            || group_scope.schema_fingerprint != E::META.schema_fingerprint
21        {
22            return Err(PlanError::SchemaMismatch);
23        }
24        if !E::META.is_grouped() && group_scope.predicate.root().is_some() {
25            return Err(ParamError::InvalidValue {
26                field: "group scope".to_owned(),
27                reason: "HAVING scope requires a grouped entity".to_owned(),
28            }
29            .into());
30        }
31        if matches!(
32            target,
33            QueryTarget::TypedPage | QueryTarget::TypedList | QueryTarget::First
34        ) && query.inner.projection.is_dynamic()
35        {
36            return Err(ParamError::ProjectionNotAllowed.into());
37        }
38        if target == QueryTarget::First
39            && (query.inner.paging.explicit_page
40                || query.inner.paging.explicit_size
41                || query.inner.paging.explicit_offset
42                || query.inner.paging.explicit_cursor)
43        {
44            return Err(ParamError::PagingNotAllowed.into());
45        }
46
47        let projection = match target {
48            QueryTarget::MapPage => match &query.inner.projection {
49                Projection::All => E::META
50                    .fields
51                    .iter()
52                    .filter(|field| {
53                        field.selectable
54                            && field.result_alias.is_some()
55                            && scope.field_access.allows(field.id)
56                    })
57                    .map(|field| field.id)
58                    .collect(),
59                Projection::Fields(fields) => fields.clone(),
60            },
61            _ => E::META
62                .fields
63                .iter()
64                .filter(|field| field.result_alias.is_some())
65                .map(|field| field.id)
66                .collect(),
67        };
68
69        if target == QueryTarget::MapPage && projection.is_empty() {
70            return Err(ParamError::EmptyProjection.into());
71        }
72        if matches!(target, QueryTarget::MapPage) {
73            if let Projection::Fields(fields) = &query.inner.projection {
74                Self::validate_fields_with_access(
75                    E::META,
76                    &scope.field_access,
77                    fields.iter().copied(),
78                )?;
79            }
80        } else if matches!(
81            target,
82            QueryTarget::TypedPage | QueryTarget::TypedList | QueryTarget::First
83        ) {
84            Self::validate_fields_with_access(
85                E::META,
86                &scope.field_access,
87                projection.iter().copied(),
88            )?;
89        }
90
91        if let PagingMode::Cursor(cursor) = &query.inner.paging.mode {
92            if E::META.is_grouped() {
93                return Err(ParamError::InvalidValue {
94                    field: "cursor".to_owned(),
95                    reason: "cursor pagination does not support grouped entities in M2".to_owned(),
96                }
97                .into());
98            }
99            if cursor
100                .as_ref()
101                .is_some_and(|cursor| cursor.values().len() != query.inner.order.len())
102            {
103                return Err(ParamError::InvalidValue {
104                    field: "cursor".to_owned(),
105                    reason: "cursor value count does not match ordering".to_owned(),
106                }
107                .into());
108            }
109            if let Some(cursor) = cursor {
110                for (order, value) in query.inner.order.iter().zip(cursor.values()) {
111                    if E::cursor_key_non_null(order.field)
112                        && matches!(value, crate::CursorValue::Null)
113                    {
114                        return Err(ParamError::InvalidValue {
115                            field: "cursor".to_owned(),
116                            reason: "non-null cursor key cannot contain NULL".to_owned(),
117                        }
118                        .into());
119                    }
120                }
121            }
122            for order in &query.inner.order {
123                if E::META
124                    .field(order.field)
125                    .is_none_or(|field| !field.value_type.supports_cursor())
126                {
127                    return Err(ParamError::InvalidValue {
128                        field: "cursor".to_owned(),
129                        reason: "ordering type is not eligible for cursor pagination".to_owned(),
130                    }
131                    .into());
132                }
133                if !projection.contains(&order.field) {
134                    let name = E::META
135                        .field(order.field)
136                        .map_or("<unknown>", |field| field.public_name);
137                    return Err(ParamError::NotSelectable(name.to_owned()).into());
138                }
139            }
140        }
141
142        if E::META.distinct
143            && matches!(
144                target,
145                QueryTarget::TypedPage
146                    | QueryTarget::MapPage
147                    | QueryTarget::TypedList
148                    | QueryTarget::First
149            )
150        {
151            for order in &query.inner.order {
152                if !projection.contains(&order.field) {
153                    let name = E::META
154                        .field(order.field)
155                        .map_or("<unknown>", |field| field.public_name);
156                    return Err(ParamError::DistinctOrderNotProjected(name.to_owned()).into());
157                }
158            }
159        }
160
161        let (row_predicate, group_predicate) = query.inner.predicate.partition()?;
162        Self::validate_predicate_with_access(E::META, &scope.field_access, &row_predicate)?;
163        Self::validate_predicate_with_access(E::META, &scope.field_access, &group_predicate)?;
164        for order in &query.inner.order {
165            Self::validate_fields_with_access(E::META, &scope.field_access, [order.field])?;
166        }
167        let paging = if target == QueryTarget::First {
168            Paging {
169                size: 1,
170                ..Paging::default()
171            }
172        } else {
173            query.inner.paging.clone()
174        };
175        let plan = Self {
176            meta: E::META,
177            server_predicate: scope.predicate.clone(),
178            server_group_predicate: group_scope.predicate.clone(),
179            row_predicate,
180            group_predicate,
181            projection,
182            projection_aliases: None,
183            order: query.inner.order.clone(),
184            paging,
185            target,
186            distinct: E::META.distinct,
187            force_empty: query.inner.force_empty,
188            field_access: scope.field_access.clone(),
189            policy_fingerprint: query.inner.policy_fingerprint,
190            _entity: PhantomData,
191        };
192        for order in &plan.order {
193            plan.condition_order_value(order.field)?;
194        }
195        Ok(plan)
196    }
197
198    pub fn condition_order_value(
199        &self,
200        field: FieldId,
201    ) -> Result<Option<&crate::ScalarValue>, ParamError> {
202        let Some(config) = E::condition_order(field) else {
203            return Ok(None);
204        };
205        let invalid = || {
206            ParamError::InvalidValue { field:"orderBy".to_owned(),reason:"computed ordering requires exactly one matching scalar query condition and an offset/page query".to_owned() }
207        };
208        if self.meta.is_grouped()
209            || self.distinct
210            || matches!(self.paging.mode, PagingMode::Cursor(_))
211        {
212            return Err(invalid());
213        }
214        let mut conditions = self
215            .row_predicate
216            .nodes()
217            .iter()
218            .filter_map(|node| match node {
219                crate::PredicateNode::Condition(condition)
220                    if condition.field == config.source
221                        && condition.operator.short_name() == config.operator =>
222                {
223                    Some(condition)
224                }
225                _ => None,
226            });
227        let condition = conditions.next().ok_or_else(invalid)?;
228        if conditions.next().is_some() || condition.values.len() != 1 || condition.ignore_case {
229            return Err(invalid());
230        }
231        Ok(condition.values.first())
232    }
233
234    pub fn for_projection<P: SearchProjection<E>>(
235        query: &UserQuery<E>,
236        scope: &ServerScope<E>,
237    ) -> Result<Self, PlanError> {
238        if query.inner.projection.is_dynamic() {
239            return Err(ParamError::ProjectionNotAllowed.into());
240        }
241        if P::FIELDS.is_empty() {
242            return Err(ParamError::EmptyProjection.into());
243        }
244        let mut seen = std::collections::BTreeSet::new();
245        let mut projection = Vec::with_capacity(P::FIELDS.len());
246        let mut aliases = Vec::with_capacity(P::FIELDS.len());
247        for item in P::FIELDS {
248            let field = E::META
249                .field(item.source)
250                .ok_or_else(|| ParamError::UnknownField(format!("#{}", item.source.0)))?;
251            if field.result_alias.is_none() {
252                return Err(ParamError::NotSelectable(field.public_name.to_owned()).into());
253            }
254            if !seen.insert(item.source) {
255                return Err(ParamError::DuplicateParameter(field.public_name.to_owned()).into());
256            }
257            projection.push(item.source);
258            aliases.push(item.result_alias);
259        }
260        let mut plan = Self::new(query, scope, QueryTarget::Count)?;
261        plan.target = QueryTarget::StaticPage;
262        plan.projection = projection;
263        plan.projection_aliases = Some(aliases);
264        plan.validate_field_access(plan.projection.iter().copied())?;
265        if matches!(plan.paging.mode, PagingMode::Cursor(_)) {
266            for order in &plan.order {
267                if !plan.projection.contains(&order.field) {
268                    let name = E::META
269                        .field(order.field)
270                        .map_or("<unknown>", |field| field.public_name);
271                    return Err(ParamError::NotSelectable(name.to_owned()).into());
272                }
273            }
274        }
275        if plan.distinct {
276            for order in &plan.order {
277                if !plan.projection.contains(&order.field) {
278                    let name = E::META
279                        .field(order.field)
280                        .map_or("<unknown>", |field| field.public_name);
281                    return Err(ParamError::DistinctOrderNotProjected(name.to_owned()).into());
282                }
283            }
284        }
285        Ok(plan)
286    }
287
288    #[must_use]
289    pub const fn shape_fingerprint_seed(&self) -> u64 {
290        self.meta.schema_fingerprint ^ self.policy_fingerprint.rotate_left(17)
291    }
292
293    #[must_use]
294    pub fn field_access(&self) -> &FieldAccessPolicy<E> {
295        &self.field_access
296    }
297
298    pub fn validate_field_access(
299        &self,
300        fields: impl IntoIterator<Item = FieldId>,
301    ) -> Result<(), ParamError> {
302        Self::validate_fields_with_access(self.meta, &self.field_access, fields)
303    }
304
305    pub fn validate_predicate_access(&self, predicate: &PredicateArena) -> Result<(), ParamError> {
306        Self::validate_predicate_with_access(self.meta, &self.field_access, predicate)
307    }
308
309    fn validate_fields_with_access(
310        meta: &'static crate::EntityMeta,
311        access: &FieldAccessPolicy<E>,
312        fields: impl IntoIterator<Item = FieldId>,
313    ) -> Result<(), ParamError> {
314        for field_id in fields {
315            let field = meta
316                .field(field_id)
317                .ok_or_else(|| ParamError::UnknownField(format!("#{}", field_id.0)))?;
318            if !access.allows(field_id) {
319                return Err(ParamError::FieldAccessDenied(field.public_name.to_owned()));
320            }
321        }
322        Ok(())
323    }
324
325    fn validate_predicate_with_access(
326        meta: &'static crate::EntityMeta,
327        access: &FieldAccessPolicy<E>,
328        predicate: &PredicateArena,
329    ) -> Result<(), ParamError> {
330        let fields = predicate.nodes().iter().filter_map(|node| match node {
331            crate::PredicateNode::Condition(condition) => Some(condition.field),
332            crate::PredicateNode::And(_) | crate::PredicateNode::Or(_) => None,
333        });
334        Self::validate_fields_with_access(meta, access, fields)
335    }
336}