Skip to main content

icydb_core/db/query/fluent/load/
terminals.rs

1use crate::{
2    db::{
3        query::fluent::load::FluentLoadQuery,
4        query::{
5            api::ResponseCardinalityExt,
6            builder::aggregate::{exists, first, last, max, min},
7            explain::{ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor},
8            intent::QueryError,
9        },
10        response::EntityResponse,
11    },
12    traits::{EntityKind, EntityValue},
13    types::{Decimal, Id},
14    value::Value,
15};
16
17type MinMaxByIds<E> = Option<(Id<E>, Id<E>)>;
18
19impl<E> FluentLoadQuery<'_, E>
20where
21    E: EntityKind,
22{
23    // ------------------------------------------------------------------
24    // Execution (single semantic boundary)
25    // ------------------------------------------------------------------
26
27    /// Execute this query using the session's policy settings.
28    pub fn execute(&self) -> Result<EntityResponse<E>, QueryError>
29    where
30        E: EntityValue,
31    {
32        self.ensure_non_paged_mode_ready()?;
33
34        self.session.execute_query(self.query())
35    }
36
37    // ------------------------------------------------------------------
38    // Execution terminals — semantic only
39    // ------------------------------------------------------------------
40
41    /// Execute and return whether the result set is empty.
42    pub fn is_empty(&self) -> Result<bool, QueryError>
43    where
44        E: EntityValue,
45    {
46        Ok(!self.exists()?)
47    }
48
49    /// Execute and return whether at least one matching row exists.
50    pub fn exists(&self) -> Result<bool, QueryError>
51    where
52        E: EntityValue,
53    {
54        self.ensure_non_paged_mode_ready()?;
55
56        self.session
57            .execute_load_query_with(self.query(), |load, plan| load.aggregate_exists(plan))
58    }
59
60    /// Explain scalar `exists()` routing without executing the terminal.
61    pub fn explain_exists(&self) -> Result<ExplainAggregateTerminalPlan, QueryError>
62    where
63        E: EntityValue,
64    {
65        self.ensure_non_paged_mode_ready()?;
66
67        crate::db::DbSession::<E::Canister>::explain_load_query_terminal_with(
68            self.query(),
69            exists(),
70        )
71    }
72
73    /// Explain scalar load execution shape without executing the query.
74    pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError>
75    where
76        E: EntityValue,
77    {
78        self.query().explain_execution()
79    }
80
81    /// Explain scalar load execution shape as deterministic text.
82    pub fn explain_execution_text(&self) -> Result<String, QueryError>
83    where
84        E: EntityValue,
85    {
86        self.query().explain_execution_text()
87    }
88
89    /// Explain scalar load execution shape as canonical JSON.
90    pub fn explain_execution_json(&self) -> Result<String, QueryError>
91    where
92        E: EntityValue,
93    {
94        self.query().explain_execution_json()
95    }
96
97    /// Explain scalar load execution shape as verbose text with diagnostics.
98    pub fn explain_execution_verbose(&self) -> Result<String, QueryError>
99    where
100        E: EntityValue,
101    {
102        self.query().explain_execution_verbose()
103    }
104
105    /// Execute and return the number of matching rows.
106    pub fn count(&self) -> Result<u32, QueryError>
107    where
108        E: EntityValue,
109    {
110        self.ensure_non_paged_mode_ready()?;
111
112        self.session
113            .execute_load_query_with(self.query(), |load, plan| load.aggregate_count(plan))
114    }
115
116    /// Execute and return the total persisted payload bytes for the effective
117    /// result window.
118    pub fn bytes(&self) -> Result<u64, QueryError>
119    where
120        E: EntityValue,
121    {
122        self.ensure_non_paged_mode_ready()?;
123
124        self.session
125            .execute_load_query_with(self.query(), |load, plan| load.bytes(plan))
126    }
127
128    /// Execute and return the smallest matching identifier, if any.
129    pub fn min(&self) -> Result<Option<Id<E>>, QueryError>
130    where
131        E: EntityValue,
132    {
133        self.ensure_non_paged_mode_ready()?;
134
135        self.session
136            .execute_load_query_with(self.query(), |load, plan| load.aggregate_min(plan))
137    }
138
139    /// Explain scalar `min()` routing without executing the terminal.
140    pub fn explain_min(&self) -> Result<ExplainAggregateTerminalPlan, QueryError>
141    where
142        E: EntityValue,
143    {
144        self.ensure_non_paged_mode_ready()?;
145
146        crate::db::DbSession::<E::Canister>::explain_load_query_terminal_with(self.query(), min())
147    }
148
149    /// Execute and return the id of the row with the smallest value for `field`.
150    ///
151    /// Ties are deterministic: equal field values resolve by primary key ascending.
152    pub fn min_by(&self, field: impl AsRef<str>) -> Result<Option<Id<E>>, QueryError>
153    where
154        E: EntityValue,
155    {
156        self.ensure_non_paged_mode_ready()?;
157
158        Self::with_slot(field, |target_slot| {
159            self.session
160                .execute_load_query_with(self.query(), move |load, plan| {
161                    load.aggregate_min_by_slot(plan, target_slot)
162                })
163        })
164    }
165
166    /// Execute and return the largest matching identifier, if any.
167    pub fn max(&self) -> Result<Option<Id<E>>, QueryError>
168    where
169        E: EntityValue,
170    {
171        self.ensure_non_paged_mode_ready()?;
172
173        self.session
174            .execute_load_query_with(self.query(), |load, plan| load.aggregate_max(plan))
175    }
176
177    /// Explain scalar `max()` routing without executing the terminal.
178    pub fn explain_max(&self) -> Result<ExplainAggregateTerminalPlan, QueryError>
179    where
180        E: EntityValue,
181    {
182        self.ensure_non_paged_mode_ready()?;
183
184        crate::db::DbSession::<E::Canister>::explain_load_query_terminal_with(self.query(), max())
185    }
186
187    /// Execute and return the id of the row with the largest value for `field`.
188    ///
189    /// Ties are deterministic: equal field values resolve by primary key ascending.
190    pub fn max_by(&self, field: impl AsRef<str>) -> Result<Option<Id<E>>, QueryError>
191    where
192        E: EntityValue,
193    {
194        self.ensure_non_paged_mode_ready()?;
195
196        Self::with_slot(field, |target_slot| {
197            self.session
198                .execute_load_query_with(self.query(), move |load, plan| {
199                    load.aggregate_max_by_slot(plan, target_slot)
200                })
201        })
202    }
203
204    /// Execute and return the id at zero-based ordinal `nth` when rows are
205    /// ordered by `field` ascending, with primary-key ascending tie-breaks.
206    pub fn nth_by(&self, field: impl AsRef<str>, nth: usize) -> Result<Option<Id<E>>, QueryError>
207    where
208        E: EntityValue,
209    {
210        self.ensure_non_paged_mode_ready()?;
211
212        Self::with_slot(field, |target_slot| {
213            self.session
214                .execute_load_query_with(self.query(), move |load, plan| {
215                    load.aggregate_nth_by_slot(plan, target_slot, nth)
216                })
217        })
218    }
219
220    /// Execute and return the sum of `field` over matching rows.
221    pub fn sum_by(&self, field: impl AsRef<str>) -> Result<Option<Decimal>, QueryError>
222    where
223        E: EntityValue,
224    {
225        self.ensure_non_paged_mode_ready()?;
226
227        Self::with_slot(field, |target_slot| {
228            self.session
229                .execute_load_query_with(self.query(), move |load, plan| {
230                    load.aggregate_sum_by_slot(plan, target_slot)
231                })
232        })
233    }
234
235    /// Execute and return the sum of distinct `field` values.
236    pub fn sum_distinct_by(&self, field: impl AsRef<str>) -> Result<Option<Decimal>, QueryError>
237    where
238        E: EntityValue,
239    {
240        self.ensure_non_paged_mode_ready()?;
241
242        Self::with_slot(field, |target_slot| {
243            self.session
244                .execute_load_query_with(self.query(), move |load, plan| {
245                    load.aggregate_sum_distinct_by_slot(plan, target_slot)
246                })
247        })
248    }
249
250    /// Execute and return the average of `field` over matching rows.
251    pub fn avg_by(&self, field: impl AsRef<str>) -> Result<Option<Decimal>, QueryError>
252    where
253        E: EntityValue,
254    {
255        self.ensure_non_paged_mode_ready()?;
256
257        Self::with_slot(field, |target_slot| {
258            self.session
259                .execute_load_query_with(self.query(), move |load, plan| {
260                    load.aggregate_avg_by_slot(plan, target_slot)
261                })
262        })
263    }
264
265    /// Execute and return the median id by `field` using deterministic ordering
266    /// `(field asc, primary key asc)`.
267    ///
268    /// Even-length windows select the lower median.
269    pub fn median_by(&self, field: impl AsRef<str>) -> Result<Option<Id<E>>, QueryError>
270    where
271        E: EntityValue,
272    {
273        self.ensure_non_paged_mode_ready()?;
274
275        Self::with_slot(field, |target_slot| {
276            self.session
277                .execute_load_query_with(self.query(), move |load, plan| {
278                    load.aggregate_median_by_slot(plan, target_slot)
279                })
280        })
281    }
282
283    /// Execute and return the number of distinct values for `field` over the
284    /// effective result window.
285    pub fn count_distinct_by(&self, field: impl AsRef<str>) -> Result<u32, QueryError>
286    where
287        E: EntityValue,
288    {
289        self.ensure_non_paged_mode_ready()?;
290
291        Self::with_slot(field, |target_slot| {
292            self.session
293                .execute_load_query_with(self.query(), move |load, plan| {
294                    load.aggregate_count_distinct_by_slot(plan, target_slot)
295                })
296        })
297    }
298
299    /// Execute and return both `(min_by(field), max_by(field))` in one terminal.
300    ///
301    /// Tie handling is deterministic for both extrema: primary key ascending.
302    pub fn min_max_by(&self, field: impl AsRef<str>) -> Result<MinMaxByIds<E>, QueryError>
303    where
304        E: EntityValue,
305    {
306        self.ensure_non_paged_mode_ready()?;
307
308        Self::with_slot(field, |target_slot| {
309            self.session
310                .execute_load_query_with(self.query(), move |load, plan| {
311                    load.aggregate_min_max_by_slot(plan, target_slot)
312                })
313        })
314    }
315
316    /// Execute and return projected field values for the effective result window.
317    pub fn values_by(&self, field: impl AsRef<str>) -> Result<Vec<Value>, QueryError>
318    where
319        E: EntityValue,
320    {
321        self.ensure_non_paged_mode_ready()?;
322
323        Self::with_slot(field, |target_slot| {
324            self.session
325                .execute_load_query_with(self.query(), move |load, plan| {
326                    load.values_by_slot(plan, target_slot)
327                })
328        })
329    }
330
331    /// Execute and return the first `k` rows from the effective response window.
332    pub fn take(&self, take_count: u32) -> Result<EntityResponse<E>, QueryError>
333    where
334        E: EntityValue,
335    {
336        self.ensure_non_paged_mode_ready()?;
337
338        self.session
339            .execute_load_query_with(self.query(), |load, plan| load.take(plan, take_count))
340    }
341
342    /// Execute and return the top `k` rows by `field` under deterministic
343    /// ordering `(field desc, primary_key asc)` over the effective response
344    /// window.
345    ///
346    /// This terminal applies its own ordering and does not preserve query
347    /// `order_by(...)` row order in the returned rows. For `k = 1`, this
348    /// matches `max_by(field)` selection semantics.
349    pub fn top_k_by(
350        &self,
351        field: impl AsRef<str>,
352        take_count: u32,
353    ) -> Result<EntityResponse<E>, QueryError>
354    where
355        E: EntityValue,
356    {
357        self.ensure_non_paged_mode_ready()?;
358
359        Self::with_slot(field, |target_slot| {
360            self.session
361                .execute_load_query_with(self.query(), move |load, plan| {
362                    load.top_k_by_slot(plan, target_slot, take_count)
363                })
364        })
365    }
366
367    /// Execute and return the bottom `k` rows by `field` under deterministic
368    /// ordering `(field asc, primary_key asc)` over the effective response
369    /// window.
370    ///
371    /// This terminal applies its own ordering and does not preserve query
372    /// `order_by(...)` row order in the returned rows. For `k = 1`, this
373    /// matches `min_by(field)` selection semantics.
374    pub fn bottom_k_by(
375        &self,
376        field: impl AsRef<str>,
377        take_count: u32,
378    ) -> Result<EntityResponse<E>, QueryError>
379    where
380        E: EntityValue,
381    {
382        self.ensure_non_paged_mode_ready()?;
383
384        Self::with_slot(field, |target_slot| {
385            self.session
386                .execute_load_query_with(self.query(), move |load, plan| {
387                    load.bottom_k_by_slot(plan, target_slot, take_count)
388                })
389        })
390    }
391
392    /// Execute and return projected values for the top `k` rows by `field`
393    /// under deterministic ordering `(field desc, primary_key asc)` over the
394    /// effective response window.
395    ///
396    /// Ranking is applied before projection and does not preserve query
397    /// `order_by(...)` row order in the returned values. For `k = 1`, this
398    /// matches `max_by(field)` projected to one value.
399    pub fn top_k_by_values(
400        &self,
401        field: impl AsRef<str>,
402        take_count: u32,
403    ) -> Result<Vec<Value>, QueryError>
404    where
405        E: EntityValue,
406    {
407        self.ensure_non_paged_mode_ready()?;
408
409        Self::with_slot(field, |target_slot| {
410            self.session
411                .execute_load_query_with(self.query(), move |load, plan| {
412                    load.top_k_by_values_slot(plan, target_slot, take_count)
413                })
414        })
415    }
416
417    /// Execute and return projected values for the bottom `k` rows by `field`
418    /// under deterministic ordering `(field asc, primary_key asc)` over the
419    /// effective response window.
420    ///
421    /// Ranking is applied before projection and does not preserve query
422    /// `order_by(...)` row order in the returned values. For `k = 1`, this
423    /// matches `min_by(field)` projected to one value.
424    pub fn bottom_k_by_values(
425        &self,
426        field: impl AsRef<str>,
427        take_count: u32,
428    ) -> Result<Vec<Value>, QueryError>
429    where
430        E: EntityValue,
431    {
432        self.ensure_non_paged_mode_ready()?;
433
434        Self::with_slot(field, |target_slot| {
435            self.session
436                .execute_load_query_with(self.query(), move |load, plan| {
437                    load.bottom_k_by_values_slot(plan, target_slot, take_count)
438                })
439        })
440    }
441
442    /// Execute and return projected id/value pairs for the top `k` rows by
443    /// `field` under deterministic ordering `(field desc, primary_key asc)`
444    /// over the effective response window.
445    ///
446    /// Ranking is applied before projection and does not preserve query
447    /// `order_by(...)` row order in the returned values. For `k = 1`, this
448    /// matches `max_by(field)` projected to one `(id, value)` pair.
449    pub fn top_k_by_with_ids(
450        &self,
451        field: impl AsRef<str>,
452        take_count: u32,
453    ) -> Result<Vec<(Id<E>, Value)>, QueryError>
454    where
455        E: EntityValue,
456    {
457        self.ensure_non_paged_mode_ready()?;
458
459        Self::with_slot(field, |target_slot| {
460            self.session
461                .execute_load_query_with(self.query(), move |load, plan| {
462                    load.top_k_by_with_ids_slot(plan, target_slot, take_count)
463                })
464        })
465    }
466
467    /// Execute and return projected id/value pairs for the bottom `k` rows by
468    /// `field` under deterministic ordering `(field asc, primary_key asc)`
469    /// over the effective response window.
470    ///
471    /// Ranking is applied before projection and does not preserve query
472    /// `order_by(...)` row order in the returned values. For `k = 1`, this
473    /// matches `min_by(field)` projected to one `(id, value)` pair.
474    pub fn bottom_k_by_with_ids(
475        &self,
476        field: impl AsRef<str>,
477        take_count: u32,
478    ) -> Result<Vec<(Id<E>, Value)>, QueryError>
479    where
480        E: EntityValue,
481    {
482        self.ensure_non_paged_mode_ready()?;
483
484        Self::with_slot(field, |target_slot| {
485            self.session
486                .execute_load_query_with(self.query(), move |load, plan| {
487                    load.bottom_k_by_with_ids_slot(plan, target_slot, take_count)
488                })
489        })
490    }
491
492    /// Execute and return distinct projected field values for the effective
493    /// result window, preserving first-observed value order.
494    pub fn distinct_values_by(&self, field: impl AsRef<str>) -> Result<Vec<Value>, QueryError>
495    where
496        E: EntityValue,
497    {
498        self.ensure_non_paged_mode_ready()?;
499
500        Self::with_slot(field, |target_slot| {
501            self.session
502                .execute_load_query_with(self.query(), move |load, plan| {
503                    load.distinct_values_by_slot(plan, target_slot)
504                })
505        })
506    }
507
508    /// Execute and return projected field values paired with row ids for the
509    /// effective result window.
510    pub fn values_by_with_ids(
511        &self,
512        field: impl AsRef<str>,
513    ) -> Result<Vec<(Id<E>, Value)>, QueryError>
514    where
515        E: EntityValue,
516    {
517        self.ensure_non_paged_mode_ready()?;
518
519        Self::with_slot(field, |target_slot| {
520            self.session
521                .execute_load_query_with(self.query(), move |load, plan| {
522                    load.values_by_with_ids_slot(plan, target_slot)
523                })
524        })
525    }
526
527    /// Execute and return the first projected field value in effective response
528    /// order, if any.
529    pub fn first_value_by(&self, field: impl AsRef<str>) -> Result<Option<Value>, QueryError>
530    where
531        E: EntityValue,
532    {
533        self.ensure_non_paged_mode_ready()?;
534
535        Self::with_slot(field, |target_slot| {
536            self.session
537                .execute_load_query_with(self.query(), move |load, plan| {
538                    load.first_value_by_slot(plan, target_slot)
539                })
540        })
541    }
542
543    /// Execute and return the last projected field value in effective response
544    /// order, if any.
545    pub fn last_value_by(&self, field: impl AsRef<str>) -> Result<Option<Value>, QueryError>
546    where
547        E: EntityValue,
548    {
549        self.ensure_non_paged_mode_ready()?;
550
551        Self::with_slot(field, |target_slot| {
552            self.session
553                .execute_load_query_with(self.query(), move |load, plan| {
554                    load.last_value_by_slot(plan, target_slot)
555                })
556        })
557    }
558
559    /// Execute and return the first matching identifier in response order, if any.
560    pub fn first(&self) -> Result<Option<Id<E>>, QueryError>
561    where
562        E: EntityValue,
563    {
564        self.ensure_non_paged_mode_ready()?;
565
566        self.session
567            .execute_load_query_with(self.query(), |load, plan| load.aggregate_first(plan))
568    }
569
570    /// Explain scalar `first()` routing without executing the terminal.
571    pub fn explain_first(&self) -> Result<ExplainAggregateTerminalPlan, QueryError>
572    where
573        E: EntityValue,
574    {
575        self.ensure_non_paged_mode_ready()?;
576
577        crate::db::DbSession::<E::Canister>::explain_load_query_terminal_with(self.query(), first())
578    }
579
580    /// Execute and return the last matching identifier in response order, if any.
581    pub fn last(&self) -> Result<Option<Id<E>>, QueryError>
582    where
583        E: EntityValue,
584    {
585        self.ensure_non_paged_mode_ready()?;
586
587        self.session
588            .execute_load_query_with(self.query(), |load, plan| load.aggregate_last(plan))
589    }
590
591    /// Explain scalar `last()` routing without executing the terminal.
592    pub fn explain_last(&self) -> Result<ExplainAggregateTerminalPlan, QueryError>
593    where
594        E: EntityValue,
595    {
596        self.ensure_non_paged_mode_ready()?;
597
598        crate::db::DbSession::<E::Canister>::explain_load_query_terminal_with(self.query(), last())
599    }
600
601    /// Execute and require exactly one matching row.
602    pub fn require_one(&self) -> Result<(), QueryError>
603    where
604        E: EntityValue,
605    {
606        self.execute()?.require_one()?;
607        Ok(())
608    }
609
610    /// Execute and require at least one matching row.
611    pub fn require_some(&self) -> Result<(), QueryError>
612    where
613        E: EntityValue,
614    {
615        self.execute()?.require_some()?;
616        Ok(())
617    }
618}