Skip to main content

qbrs_core/
scope.rs

1//! Type-level FROM/JOIN scope tracking.
2//!
3//! This is the load-bearing mechanism for the whole crate: a flat cons-list
4//! of joined tables, with a frunk-style indexed `Find` trait to look up
5//! whether/how a table appears in it.
6
7use std::marker::PhantomData;
8
9/// Marker trait for anything that represents a table in a query's scope.
10/// `NAME` is what the SQL renderer prints, kept as a trait const rather than
11/// derived from the Rust type name so the derive macro can freely rename
12/// tables.
13///
14/// **Known limitation**: no schema qualification — a table is rendered
15/// bare, so `search_path` decides which one it is.
16pub trait Table: 'static {
17    const NAME: &'static str;
18}
19
20/// A table that exists in the schema, as opposed to one a query brings into
21/// being. `.from()` and the joins take these; a CTE's pseudo-table is
22/// reached through its `Cte` binding instead, so selecting from a CTE that
23/// was never bound cannot be written.
24#[diagnostic::on_unimplemented(
25    message = "`{Self}` isn't a schema table",
26    label = "a `with!{{}}` pseudo-table is entered through its `cte::with(..)` binding",
27    note = "pass the `cte::with(..)` binding itself to `.from(..)`/`.inner_join(..)` — binding a CTE is what puts it in scope"
28)]
29pub trait BaseTable: Table + private::Sealed {}
30
31mod private {
32    /// The gate itself. Re-exported `#[doc(hidden)]` below, because
33    /// `#[derive(Table)]` expands in the caller's crate and has to emit the
34    /// impl there; `with!` deliberately emits neither, which is what keeps
35    /// a CTE's pseudo-table out of `.from(..)` except through its binding.
36    pub trait Sealed {}
37}
38
39#[doc(hidden)]
40pub use private::Sealed as BaseTableSealed;
41
42/// Marker trait for the two nullability states a table can have in a
43/// query's scope, depending on how it was joined.
44pub trait Nullability: 'static {}
45
46/// The table is guaranteed present (INNER/CROSS join, or the FROM table).
47pub struct NotNull;
48impl Nullability for NotNull {}
49
50/// The table may be absent (LEFT/RIGHT/FULL join introduced this
51/// possibility for it).
52pub struct MaybeNull;
53impl Nullability for MaybeNull {}
54
55/// Empty scope: no tables joined yet.
56pub struct Nil;
57
58/// A non-empty scope: `Head` joined with nullability tracked in
59/// `TableSlot`, followed by the rest of the scope in `Tail`. Most recently
60/// joined table first, which is the order a hand-written `Scope` alias has
61/// to be spelled in.
62///
63/// **Known limitation**: a table appears at most once, and uniqueness is by
64/// marker type, not by SQL name. Joining one marker twice is reported later,
65/// as an inference ambiguity the first time a column of it is referenced;
66/// two *different* markers that share a `Table::NAME` are not caught at all
67/// and render `FROM "t" JOIN "t"`. Two schemas must not name the same table.
68pub struct Cons<Head, Tail>(PhantomData<(Head, Tail)>);
69
70/// One occurrence of a table in the scope list: table `T`, with the
71/// nullability `N` its join gave it.
72pub struct TableSlot<T: Table, N: Nullability>(PhantomData<(T, N)>);
73
74/// Index marker: "found at the head of the list".
75pub struct Here;
76
77/// Index marker: "found further down the list, at index `I`".
78pub struct There<I>(PhantomData<I>);
79
80/// The 1-based position an index names, which is what SQL's `ORDER BY <n>`
81/// on a set operation takes. The index is already computed by `row::Field`;
82/// this reads it, so a set operation can be ordered by a column rather than
83/// by a number nothing checks.
84pub trait Position {
85    const POSITION: u32;
86}
87
88impl Position for Here {
89    const POSITION: u32 = 1;
90}
91
92impl<I: Position> Position for There<I> {
93    const POSITION: u32 = I::POSITION + 1;
94}
95
96/// The seals for the scope proofs. Each carries the *same parameters* as
97/// the trait it seals and is implemented only for the honest combinations,
98/// which is what a seal on `Self` alone cannot do: `Find<T, _>` and
99/// `row::Field<K, _>` take the caller's own marker as a bare parameter, so
100/// the orphan rule licenses a schema crate to write
101/// `impl Find<orders::Table, Here> for <a scope without orders>` — and with
102/// it, the compile error this crate exists to produce.
103///
104/// A private *type* in an associated position is not enough, and was the
105/// mistake this replaces: `type Proof: Sealed` can be satisfied from
106/// outside by projecting the same type out of an honest impl
107/// (`<Nil as Superset<Nil, Nil>>::Proof`), because path privacy doesn't
108/// reach through projection. A private *trait* has nothing to project.
109pub(crate) mod proof {
110    pub trait FoundAt<T, Index> {}
111
112    pub trait SupersetOf<Req, Idxs> {}
113}
114
115/// Proof that table `T` appears somewhere in a scope list, found at
116/// compile-time-inferred position `Index`. `Index` is never spelled out by
117/// callers — it's inferred, exactly like frunk's `Plucker` — and it's what
118/// keeps the two impls below structurally distinct rather than overlapping.
119#[diagnostic::on_unimplemented(
120    message = "`{T}` is not available in this query's scope",
121    label = "add `.join(<table>, ..)` (or `.from(..)`) for `{T}` before referencing its columns here",
122    note = "columns can only be referenced once their table has been joined into the current FROM/JOIN scope — and in a generic helper give each table its own `Idx` parameter, since one shared index matches no scope"
123)]
124pub trait Find<T: Table, Index>: proof::FoundAt<T, Index> {
125    /// The nullability `T` has in this scope (derived from how it was
126    /// joined, not asserted manually).
127    type Nullability: Nullability;
128}
129
130impl<T: Table, N: Nullability, Tail> proof::FoundAt<T, Here> for Cons<TableSlot<T, N>, Tail> {}
131
132impl<T: Table, N: Nullability, Tail> Find<T, Here> for Cons<TableSlot<T, N>, Tail> {
133    type Nullability = N;
134}
135
136impl<T: Table, Head, Tail, I> proof::FoundAt<T, There<I>> for Cons<Head, Tail> where Tail: Find<T, I>
137{}
138
139#[diagnostic::do_not_recommend]
140impl<T: Table, Head, Tail, I> Find<T, There<I>> for Cons<Head, Tail>
141where
142    Tail: Find<T, I>,
143{
144    type Nullability = <Tail as Find<T, I>>::Nullability;
145}
146
147/// Concatenate two type-level lists. Used to union the `Req` (tables
148/// referenced) of two expressions when they're combined via `.and()`/`.or()`
149/// or a binary operator like `.eq()`.
150pub trait Concat<Other> {
151    type Output;
152}
153
154impl<Other> Concat<Other> for Nil {
155    type Output = Other;
156}
157
158impl<Head, Tail: Concat<Other>, Other> Concat<Other> for Cons<Head, Tail> {
159    type Output = Cons<Head, Tail::Output>;
160}
161
162/// Proof that scope `Self` contains every table named in `Req`, regardless
163/// of each table's nullability. This is the bound used at the point an
164/// already-built `Expr<Req, _>` is inserted into a query (`.filter()`,
165/// `.join()`, `.select()`), and is what lets expressions be built as
166/// portable, scope-agnostic values instead of via a scope-bound cursor
167/// closure.
168///
169/// `Idxs` mirrors `Find`'s own `Index` parameter: it holds the per-element
170/// lookup indices, and callers never name it explicitly. It has to live in
171/// the trait's parameter list — an index constrained only by the `where`
172/// clause isn't accepted, since Rust has no existential quantification over
173/// impl generics.
174#[diagnostic::on_unimplemented(
175    message = "this expression references a table that isn't in scope here",
176    label = "requires {Req}, but the current query scope doesn't contain all of it",
177    note = "in a generic helper, `Idxs` has to be a type parameter of its own — one shared index matches no scope, however right the tables look"
178)]
179pub trait Superset<Req, Idxs>: proof::SupersetOf<Req, Idxs> {}
180
181impl<S> proof::SupersetOf<Nil, Nil> for S {}
182impl<S> Superset<Nil, Nil> for S {}
183
184impl<S, Head: Table, Tail, IdxHead, IdxsTail>
185    proof::SupersetOf<Cons<Head, Tail>, Cons<IdxHead, IdxsTail>> for S
186where
187    S: Find<Head, IdxHead> + Superset<Tail, IdxsTail>,
188{
189}
190
191impl<S, Head: Table, Tail, IdxHead, IdxsTail> Superset<Cons<Head, Tail>, Cons<IdxHead, IdxsTail>>
192    for S
193where
194    S: Find<Head, IdxHead> + Superset<Tail, IdxsTail>,
195{
196}
197
198/// A scope's tables without their nullability: the `Req` list an expression
199/// carries. `Select::correlated`'s `EXISTS` needs it to say "this condition
200/// references everything the outer query had in scope", so a subquery can't
201/// be filtered onto a query that never joined those tables.
202pub trait ScopeTables {
203    type Tables;
204}
205
206impl ScopeTables for Nil {
207    type Tables = Nil;
208}
209
210impl<T: Table, N: Nullability, Tail: ScopeTables> ScopeTables for Cons<TableSlot<T, N>, Tail> {
211    type Tables = Cons<T, Tail::Tables>;
212}
213
214/// Flip every table already in a scope to `MaybeNull`. Used by RIGHT/FULL
215/// JOIN, which must retroactively make every previously-joined table
216/// nullable (mirrors Drizzle's `AppendToNullabilityMap`), before adding the
217/// newly-joined table's own slot.
218pub trait MapNullable {
219    type Output;
220}
221
222impl MapNullable for Nil {
223    type Output = Nil;
224}
225
226impl<T: Table, N: Nullability, Tail: MapNullable> MapNullable for Cons<TableSlot<T, N>, Tail> {
227    type Output = Cons<TableSlot<T, MaybeNull>, Tail::Output>;
228}
229
230pub(crate) mod wrap {
231    /// Sealed carrying the nullability, for the reason `proof` explains:
232    /// what a join does to a column is the join's to decide, and `Self` is
233    /// the caller's own type wherever a schema declares one — so an open
234    /// impl lets a schema say a `LEFT JOIN` leaves its column NOT NULL, and
235    /// the row then decodes a NULL into a non-`Option`.
236    pub trait Sealed<N> {}
237}
238
239/// Idempotently wrap a SQL type as nullable-or-not depending on a
240/// `Nullability` marker, without double-wrapping an already-`Nullable<T>`
241/// column. Column accessors compose this with `Find::Nullability` so that
242/// NULL-ability is *derived* from join shape rather than requiring a manual
243/// `.nullable()` assertion (diesel's documented wart).
244pub trait WrapNullable<N: Nullability>: wrap::Sealed<N> {
245    type Output;
246}
247
248/// Wraps a base SQL type as nullable. A distinct outer shape from any base
249/// type, so it can have its own `WrapNullable<MaybeNull>` impl without
250/// overlapping the per-base-type impls below.
251pub struct Nullable<T>(PhantomData<T>);
252
253// `NotNull` never changes the type, for *any* `T` (including `Nullable<T>`
254// itself) — a single blanket impl is coherence-safe here because there is
255// no second impl competing for the `NotNull` slot.
256impl<T> wrap::Sealed<NotNull> for T {}
257
258impl<T> WrapNullable<NotNull> for T {
259    type Output = T;
260}
261
262// Idempotent: wrapping an already-nullable type as `MaybeNull` again is a
263// no-op, not `Nullable<Nullable<T>>`.
264impl<T> wrap::Sealed<MaybeNull> for Nullable<T> {}
265
266impl<T> WrapNullable<MaybeNull> for Nullable<T> {
267    type Output = Nullable<T>;
268}
269
270// IMPORTANT: there must be NO blanket `impl<T> WrapNullable<MaybeNull> for T`
271// alongside the `Nullable<T>` impl above — the two overlap, since coherence
272// treats a blanket impl over a fully generic `T` as potentially covering
273// `Nullable<_>`. Each concrete base SQL type (Integer, Text, Bool, ...) gets
274// its own individual, non-generic `WrapNullable<MaybeNull>` impl instead,
275// generated by `expr`'s `sql_leaf_type!` macro over the closed base-type
276// list.