Skip to main content

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}