qbrs_core/select/dyn_select.rs
1//! `DynSelect`: `Select` with its join-topology-tracking `Scope` erased.
2
3use std::marker::PhantomData;
4
5use super::{Select, SelectBody, Selection};
6use crate::dialect::Dialect;
7use crate::expr::Value;
8use crate::render::{QuerySink, SelectItem};
9
10/// The one unavoidable escape hatch in this design: a single static type
11/// cannot mean "this table is joined" in one branch of an `if` and "it
12/// isn't" in another. Only the join skeleton is erased — every column
13/// reference was already checked against a concrete `Scope` before
14/// `.erase()`, and predicates keep the same closed `ExprKind`/`Value`
15/// representation used everywhere else, with no `Box<dyn _>` involved.
16///
17/// Composition happens *before* erasure, which is why `DynSelect` offers no
18/// further `.filter()`/`.join()`. It exists only to let two fully-built
19/// branches with different join topology unify into one value.
20pub struct DynSelect<D, Output> {
21 body: SelectBody,
22 selection: Vec<SelectItem>,
23 _marker: PhantomData<fn() -> (D, Output)>,
24}
25
26impl<D, Scope, Sel> Select<D, Scope, Sel> {
27 /// Erases `Scope`. `Sel::items()` runs here, while `Scope`/`Idx` are
28 /// still known; the resulting `Vec<SelectItem>` and the plain-Rust
29 /// `Output` type are all `DynSelect` needs afterwards.
30 pub fn erase<Idx>(self) -> DynSelect<D, Sel::Output>
31 where
32 Sel: Selection<Scope, Idx>,
33 {
34 DynSelect {
35 body: self.body,
36 selection: self.selection.items(),
37 _marker: PhantomData,
38 }
39 }
40}
41
42/// Never implemented: it exists so `DynSelect::filter` can name a bound
43/// that always fails, and say why. Without the method, `.filter(..)` on an
44/// erased query resolves to `Iterator::filter` and the error talks about
45/// iterators.
46#[diagnostic::on_unimplemented(
47 message = "an erased query can't be filtered",
48 label = "add `.filter(..)` before `.erase()` — erasure gives up the scope a condition is checked against",
49 note = "`.erase()` is for unifying two fully-built branches with different joins; compose the query first"
50)]
51pub trait CannotFilterAfterErase {}
52
53impl<D, Output> DynSelect<D, Output> {
54 /// Always a compile error — see `CannotFilterAfterErase`. Present so
55 /// the error is that one, rather than `Iterator::filter`'s.
56 #[doc(hidden)]
57 pub fn filter<T: CannotFilterAfterErase>(self, _cond: T) -> Self {
58 self
59 }
60
61 /// `LIMIT`/`OFFSET` survive erasure because they reference nothing: a
62 /// row count needs no proof that a table is joined. `order_by` doesn't
63 /// follow them here — a sort key is a column reference, and the scope
64 /// that would justify it is exactly what `.erase()` gave up.
65 pub fn limit(mut self, n: impl super::IntoRowCount) -> Self {
66 self.body.limit = Some(n.into_row_count());
67 self
68 }
69
70 pub fn offset(mut self, n: impl super::IntoRowCount) -> Self {
71 self.body.offset = Some(n.into_row_count());
72 self
73 }
74}
75
76impl<D: Dialect, Output> DynSelect<D, Output> {
77 /// The same total `Select::count_sql` renders. Paging is the reason
78 /// `LIMIT`/`OFFSET` survive erasure, and a page needs a total.
79 pub fn count_sql(&self, _dialect: D) -> (String, Vec<Value>) {
80 self.body.count_sql::<D>(&self.selection)
81 }
82
83 pub fn to_sql(&self, _dialect: D) -> (String, Vec<Value>) {
84 let mut sink = QuerySink::<D>::new();
85 self.body.render_into::<D>(&self.selection, &mut sink);
86 sink.finish()
87 }
88}