icydb_core/db/query/intent/
mod.rs1use crate::{
2 db::query::{
3 ReadConsistency,
4 plan::{
5 DeleteLimitSpec, ExecutablePlan, ExplainPlan, LogicalPlan, OrderDirection, OrderSpec,
6 PageSpec, PlanError, ProjectionSpec,
7 planner::{PlannerError, plan_access},
8 validate::validate_access_plan,
9 validate::validate_order,
10 },
11 predicate::{Predicate, SchemaInfo, ValidateError, normalize},
12 },
13 error::InternalError,
14 traits::EntityKind,
15};
16use std::marker::PhantomData;
17use thiserror::Error as ThisError;
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum QueryMode {
28 Load(LoadSpec),
29 Delete(DeleteSpec),
30}
31
32impl QueryMode {
33 #[must_use]
35 pub const fn is_load(&self) -> bool {
36 match self {
37 Self::Load(_) => true,
38 Self::Delete(_) => false,
39 }
40 }
41
42 #[must_use]
44 pub const fn is_delete(&self) -> bool {
45 match self {
46 Self::Delete(_) => true,
47 Self::Load(_) => false,
48 }
49 }
50}
51
52#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub struct LoadSpec {
60 pub limit: Option<u32>,
61 pub offset: u64,
62}
63
64impl LoadSpec {
65 #[must_use]
67 pub const fn new() -> Self {
68 Self {
69 limit: None,
70 offset: 0,
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
82pub struct DeleteSpec {
83 pub limit: Option<u32>,
84}
85
86impl DeleteSpec {
87 #[must_use]
89 pub const fn new() -> Self {
90 Self { limit: None }
91 }
92}
93
94#[derive(Debug)]
106pub struct Query<E: EntityKind> {
107 mode: QueryMode,
108 predicate: Option<Predicate>,
109 order: Option<OrderSpec>,
110 projection: ProjectionSpec,
111 consistency: ReadConsistency,
112 _marker: PhantomData<E>,
113}
114
115impl<E: EntityKind> Query<E> {
116 #[must_use]
120 pub const fn new(consistency: ReadConsistency) -> Self {
121 Self {
122 mode: QueryMode::Load(LoadSpec::new()),
123 predicate: None,
124 order: None,
125 projection: ProjectionSpec::All,
126 consistency,
127 _marker: PhantomData,
128 }
129 }
130
131 #[must_use]
133 pub const fn mode(&self) -> QueryMode {
134 self.mode
135 }
136
137 #[must_use]
139 pub fn filter(mut self, predicate: Predicate) -> Self {
140 self.predicate = match self.predicate.take() {
141 Some(existing) => Some(Predicate::And(vec![existing, predicate])),
142 None => Some(predicate),
143 };
144 self
145 }
146
147 #[must_use]
149 pub fn order_by(mut self, field: &'static str) -> Self {
150 self.order = Some(push_order(self.order, field, OrderDirection::Asc));
151 self
152 }
153
154 #[must_use]
156 pub fn order_by_desc(mut self, field: &'static str) -> Self {
157 self.order = Some(push_order(self.order, field, OrderDirection::Desc));
158 self
159 }
160
161 #[must_use]
163 pub const fn delete(mut self) -> Self {
164 if self.mode.is_load() {
165 self.mode = QueryMode::Delete(DeleteSpec::new());
166 }
167 self
168 }
169
170 #[must_use]
174 pub const fn limit(mut self, limit: u32) -> Self {
175 match self.mode {
176 QueryMode::Load(mut spec) => {
177 spec.limit = Some(limit);
178 self.mode = QueryMode::Load(spec);
179 }
180 QueryMode::Delete(mut spec) => {
181 spec.limit = Some(limit);
182 self.mode = QueryMode::Delete(spec);
183 }
184 }
185 self
186 }
187
188 #[must_use]
190 pub const fn offset(mut self, offset: u64) -> Self {
191 if let QueryMode::Load(mut spec) = self.mode {
192 spec.offset = offset;
193 self.mode = QueryMode::Load(spec);
194 }
195 self
196 }
197
198 pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
200 let plan = self.build_plan::<E>()?;
201
202 Ok(plan.explain())
203 }
204
205 pub fn plan(&self) -> Result<ExecutablePlan<E>, QueryError> {
207 let plan = self.build_plan::<E>()?;
208
209 Ok(ExecutablePlan::new(plan))
210 }
211
212 fn build_plan<T: EntityKind>(&self) -> Result<LogicalPlan, QueryError> {
214 let model = T::MODEL;
216 let schema_info = SchemaInfo::from_entity_model(model)?;
217 self.validate_intent()?;
218
219 if let Some(order) = &self.order {
220 validate_order(&schema_info, order)?;
221 }
222
223 let normalized_predicate = self.predicate.as_ref().map(normalize);
225 let access_plan = plan_access::<T>(&schema_info, normalized_predicate.as_ref())?;
226
227 validate_access_plan(&schema_info, model, &access_plan)?;
228
229 let plan = LogicalPlan {
231 mode: self.mode,
232 access: access_plan,
233 predicate: normalized_predicate,
234 order: self.order.clone(),
235 delete_limit: match self.mode {
236 QueryMode::Delete(spec) => spec.limit.map(|max_rows| DeleteLimitSpec { max_rows }),
237 QueryMode::Load(_) => None,
238 },
239 page: match self.mode {
240 QueryMode::Load(spec) => {
241 if spec.limit.is_some() || spec.offset > 0 {
242 Some(PageSpec {
243 limit: spec.limit,
244 offset: spec.offset,
245 })
246 } else {
247 None
248 }
249 }
250 QueryMode::Delete(_) => None,
251 },
252 projection: self.projection.clone(),
253 consistency: self.consistency,
254 };
255
256 Ok(plan)
257 }
258
259 const fn validate_intent(&self) -> Result<(), IntentError> {
261 match self.mode {
262 QueryMode::Load(_) => {}
263 QueryMode::Delete(spec) => {
264 if spec.limit.is_some() && self.order.is_none() {
265 return Err(IntentError::DeleteLimitRequiresOrder);
266 }
267 }
268 }
269
270 Ok(())
271 }
272}
273
274#[derive(Debug, ThisError)]
279pub enum QueryError {
280 #[error("{0}")]
281 Validate(#[from] ValidateError),
282
283 #[error("{0}")]
284 Plan(#[from] PlanError),
285
286 #[error("{0}")]
287 Intent(#[from] IntentError),
288
289 #[error("{0}")]
290 Execute(#[from] InternalError),
291}
292
293impl From<PlannerError> for QueryError {
294 fn from(err: PlannerError) -> Self {
295 match err {
296 PlannerError::Plan(err) => Self::Plan(err),
297 PlannerError::Internal(err) => Self::Execute(err),
298 }
299 }
300}
301
302#[derive(Clone, Copy, Debug, ThisError)]
307pub enum IntentError {
308 #[error("delete limit requires an explicit ordering")]
309 DeleteLimitRequiresOrder,
310}
311
312fn push_order(
314 order: Option<OrderSpec>,
315 field: &'static str,
316 direction: OrderDirection,
317) -> OrderSpec {
318 match order {
319 Some(mut spec) => {
320 spec.fields.push((field.to_string(), direction));
321 spec
322 }
323 None => OrderSpec {
324 fields: vec![(field.to_string(), direction)],
325 },
326 }
327}
328
329#[cfg(test)]
330mod tests;