Skip to main content

sift_query_core/query/plan/
mod.rs

1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use crate::ParamParser;
5use crate::{
6    Cursor, FieldAccessPolicy, FieldId, ParamError, PlanError, PredicateArena, QueryPolicy,
7    RawParams, SearchEntity, SearchProjection,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum SortOrder {
12    Asc,
13    Desc,
14}
15
16impl SortOrder {
17    pub fn parse(value: &str) -> Option<Self> {
18        match value {
19            "asc" => Some(Self::Asc),
20            "desc" => Some(Self::Desc),
21            _ => None,
22        }
23    }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum NullOrder {
28    First,
29    Last,
30}
31
32impl From<SortOrder> for NullOrder {
33    fn from(value: SortOrder) -> Self {
34        match value {
35            SortOrder::Asc => Self::Last,
36            SortOrder::Desc => Self::First,
37        }
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub struct OrderBy {
43    pub field: FieldId,
44    pub direction: SortOrder,
45    pub nulls: NullOrder,
46}
47
48impl From<crate::OrderMeta> for OrderBy {
49    fn from(value: crate::OrderMeta) -> Self {
50        Self {
51            field: value.field,
52            direction: value.direction,
53            nulls: value.direction.into(),
54        }
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Projection {
60    All,
61    Fields(Vec<FieldId>),
62}
63
64impl Projection {
65    #[must_use]
66    pub const fn is_dynamic(&self) -> bool {
67        matches!(self, Self::Fields(_))
68    }
69}
70
71#[derive(Debug, Clone, PartialEq)]
72pub enum PagingMode {
73    Page { page: u64 },
74    Offset,
75    Cursor(Option<Cursor>),
76}
77
78#[derive(Debug, Clone, PartialEq)]
79#[allow(clippy::struct_excessive_bools)]
80pub struct Paging {
81    pub mode: PagingMode,
82    pub size: u32,
83    pub offset: u64,
84    pub explicit_page: bool,
85    pub explicit_size: bool,
86    pub explicit_offset: bool,
87    pub explicit_cursor: bool,
88}
89
90impl Default for Paging {
91    fn default() -> Self {
92        Self {
93            mode: PagingMode::Page { page: 1 },
94            size: 15,
95            offset: 0,
96            explicit_page: false,
97            explicit_size: false,
98            explicit_offset: false,
99            explicit_cursor: false,
100        }
101    }
102}
103
104/// Validated, entity-bound user query.
105#[derive(Debug)]
106struct UserQueryData {
107    predicate: PredicateArena,
108    projection: Projection,
109    order: Vec<OrderBy>,
110    paging: Paging,
111    schema_fingerprint: u64,
112    policy_fingerprint: u64,
113    force_empty: bool,
114}
115
116#[derive(Debug)]
117pub struct UserQuery<E> {
118    inner: Arc<UserQueryData>,
119    _entity: PhantomData<fn() -> E>,
120}
121
122impl<E> Clone for UserQuery<E> {
123    fn clone(&self) -> Self {
124        Self {
125            inner: Arc::clone(&self.inner),
126            _entity: PhantomData,
127        }
128    }
129}
130
131impl<E> UserQuery<E> {
132    pub(crate) fn from_parts(
133        predicate: PredicateArena,
134        projection: Projection,
135        order: Vec<OrderBy>,
136        paging: Paging,
137        schema_fingerprint: u64,
138        policy_fingerprint: u64,
139        force_empty: bool,
140    ) -> Self {
141        Self {
142            inner: Arc::new(UserQueryData {
143                predicate,
144                projection,
145                order,
146                paging,
147                schema_fingerprint,
148                policy_fingerprint,
149                force_empty,
150            }),
151            _entity: PhantomData,
152        }
153    }
154
155    #[must_use]
156    pub fn predicate(&self) -> &PredicateArena {
157        &self.inner.predicate
158    }
159
160    #[must_use]
161    pub fn projection(&self) -> &Projection {
162        &self.inner.projection
163    }
164
165    #[must_use]
166    pub fn order(&self) -> &[OrderBy] {
167        &self.inner.order
168    }
169
170    #[must_use]
171    pub fn paging(&self) -> Paging {
172        self.inner.paging.clone()
173    }
174
175    #[must_use]
176    pub fn is_forced_empty(&self) -> bool {
177        self.inner.force_empty
178    }
179}
180
181/// Service-controlled row-level predicate.
182#[derive(Debug, Clone)]
183pub struct ServerScope<E: SearchEntity> {
184    pub(crate) predicate: PredicateArena,
185    pub(crate) schema_fingerprint: u64,
186    pub(crate) field_access: FieldAccessPolicy<E>,
187    pub(crate) _entity: PhantomData<fn() -> E>,
188}
189
190/// Service-controlled predicate evaluated after `GROUP BY`.
191#[derive(Debug, Clone)]
192pub struct GroupScope<E> {
193    pub(crate) predicate: PredicateArena,
194    pub(crate) schema_fingerprint: u64,
195    pub(crate) _entity: PhantomData<fn() -> E>,
196}
197
198impl<E: SearchEntity> GroupScope<E> {
199    #[must_use]
200    pub fn unrestricted() -> Self {
201        Self {
202            predicate: PredicateArena::default(),
203            schema_fingerprint: E::META.schema_fingerprint,
204            _entity: PhantomData,
205        }
206    }
207
208    #[must_use]
209    pub fn predicate(&self) -> &PredicateArena {
210        &self.predicate
211    }
212}
213
214impl<E: SearchEntity> ServerScope<E> {
215    #[must_use]
216    pub fn unrestricted() -> Self {
217        Self {
218            predicate: PredicateArena::default(),
219            schema_fingerprint: E::META.schema_fingerprint,
220            field_access: FieldAccessPolicy::unrestricted(),
221            _entity: PhantomData,
222        }
223    }
224
225    /// Attaches a server-owned field policy to this row scope.
226    pub fn with_field_access(
227        mut self,
228        field_access: FieldAccessPolicy<E>,
229    ) -> Result<Self, ParamError> {
230        if field_access.schema_fingerprint() != E::META.schema_fingerprint {
231            return Err(ParamError::UnknownField(
232                "field access entity schema mismatch".to_owned(),
233            ));
234        }
235        self.field_access = field_access;
236        Ok(self)
237    }
238
239    #[must_use]
240    pub fn field_access(&self) -> &FieldAccessPolicy<E> {
241        &self.field_access
242    }
243
244    #[must_use]
245    pub fn predicate(&self) -> &PredicateArena {
246        &self.predicate
247    }
248}
249
250pub trait QueryInput<E: SearchEntity>: private::Sealed<E> {
251    fn into_user_query(self, policy: &QueryPolicy) -> Result<UserQuery<E>, ParamError>;
252}
253
254impl<E: SearchEntity> QueryInput<E> for &RawParams {
255    fn into_user_query(self, policy: &QueryPolicy) -> Result<UserQuery<E>, ParamError> {
256        ParamParser::new(policy)?.parse(self)
257    }
258}
259
260impl<E: SearchEntity> QueryInput<E> for &UserQuery<E> {
261    fn into_user_query(self, policy: &QueryPolicy) -> Result<UserQuery<E>, ParamError> {
262        if self.inner.schema_fingerprint != E::META.schema_fingerprint {
263            return Err(ParamError::UnknownField(
264                "entity schema mismatch".to_owned(),
265            ));
266        }
267        if self.inner.policy_fingerprint != policy.fingerprint() {
268            return Err(ParamError::ConflictingParameters(
269                "query policy fingerprint mismatch".to_owned(),
270            ));
271        }
272        Ok(self.clone())
273    }
274}
275
276mod private {
277    use super::*;
278
279    pub trait Sealed<E: SearchEntity> {}
280    impl<E: SearchEntity> Sealed<E> for &RawParams {}
281    impl<E: SearchEntity> Sealed<E> for &UserQuery<E> {}
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum QueryTarget {
286    TypedPage,
287    MapPage,
288    TypedList,
289    First,
290    Count,
291    Sum,
292    StaticPage,
293}
294
295/// Immutable input to a backend renderer.
296#[derive(Debug)]
297pub struct QueryPlan<E: SearchEntity> {
298    pub meta: &'static crate::EntityMeta,
299    pub server_predicate: PredicateArena,
300    pub server_group_predicate: PredicateArena,
301    pub row_predicate: PredicateArena,
302    pub group_predicate: PredicateArena,
303    pub projection: Vec<FieldId>,
304    pub projection_aliases: Option<Vec<&'static str>>,
305    pub order: Vec<OrderBy>,
306    pub paging: Paging,
307    pub target: QueryTarget,
308    pub distinct: bool,
309    pub force_empty: bool,
310    field_access: FieldAccessPolicy<E>,
311    policy_fingerprint: u64,
312    _entity: PhantomData<fn() -> E>,
313}
314
315mod construct;
316#[cfg(test)]
317mod tests;