sift_query_core/query/plan/
construct.rs1use 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}