fraiseql_db/traits/composed_read.rs
1//! The call shape of a read that composes embedded levels into one statement.
2//!
3//! A REST `?select=posts(id,comments(id))` used to be answered by a parent read and one
4//! sub-read per parent row per level. [`ComposedLevel`] is that request as **one**
5//! statement: each embedded level is a correlated subquery joined `LATERAL`, carrying its
6//! own filter, ordering and page, so the database returns the whole tree in a single
7//! round trip and under a single snapshot.
8//!
9//! Every value here is already resolved. Which rows a level may read (its security
10//! predicate), which keys it may return, and in what order are decided by the engine
11//! before this type is built; an adapter renders what it is given and decides nothing.
12//!
13//! # Where an embedded level's rows come from
14//!
15//! Either from its own view, correlated to the parent row ([`EmbedSource::Correlated`]),
16//! or from the parent row itself ([`EmbedSource::Materialised`]): the objects a view has
17//! already embedded in its document, read one by one as the level's rows. The second is
18//! how a GraphQL selection into a nested type is gated — its type's predicate filters the
19//! elements the parent's view materialised, exactly as it filters rows of the type's own
20//! view — without a join the schema may not be able to express.
21//!
22//! # What a composed read returns
23//!
24//! Every row, at every level, is the object
25//!
26//! ```text
27//! { "d": <the level's document>, "e": { <output_key>: <embedded value>, … } }
28//! ```
29//!
30//! rather than the document with the embeds merged into it. A relationship may share its
31//! name with a stored key, and the engine projects the document before it attaches the
32//! embeds; keeping the two apart means neither can overwrite the other on the way.
33
34use fraiseql_error::FraiseQLError;
35
36use crate::{CursorValue, OrderByClause, ScalarFieldType, WhereClause};
37
38/// The refusal a composed read of `view` gets from an adapter that cannot compose.
39///
40/// One constructor for both places that refuse — the trait's default
41/// `execute_composed_with_session` and the engine, which refuses from
42/// `supports_composed_reads()` before it would call it — so the two cannot word it
43/// differently. `Unsupported` is a `501`.
44#[must_use]
45pub fn composed_read_unsupported(view: &str) -> FraiseQLError {
46 FraiseQLError::Unsupported {
47 message: format!(
48 "Embedding related resources into a read of '{view}' needs a composed read, which \
49 this database adapter does not implement"
50 ),
51 }
52}
53
54/// The key under which a composed row carries its level's document.
55pub const COMPOSED_DOCUMENT_KEY: &str = "d";
56
57/// The key under which a composed row carries its embedded values.
58pub const COMPOSED_EMBEDS_KEY: &str = "e";
59
60/// One level of a composed read: a relation, filtered, ordered and paged, with the levels
61/// embedded beneath it.
62#[derive(Debug, Clone, PartialEq)]
63pub struct ComposedLevel {
64 /// The view this level reads.
65 pub view: String,
66 /// The `SELECT` expression for this level's document, in place of `data`.
67 ///
68 /// Only the root carries one, and only for a value the stored document does not
69 /// hold — a `nearest` distance (#959). The expression is evaluated against this
70 /// level's own relation.
71 pub projection: Option<String>,
72 /// Security conditions AND-ed with the client filter, exactly as a flat read of the
73 /// same query would compose them. The correlation to the parent is **not** in here:
74 /// it is a column reference, not a value, and the renderer adds it.
75 pub where_clause: Option<WhereClause>,
76 /// This level's ordering, or `None` for the relation's own order.
77 pub order_by: Option<Vec<OrderByClause>>,
78 /// This level's page size.
79 pub limit: Option<u32>,
80 /// This level's page offset.
81 pub offset: Option<u32>,
82 /// A keyset page in place of an offset one: how a Relay connection pages. Only the
83 /// root can carry one, and not with an `offset`; the renderer refuses anything else.
84 pub keyset: Option<ComposedKeyset>,
85 /// What this level's document is allowed to carry.
86 pub keys: LevelKeys,
87 /// The levels embedded into each row of this one.
88 pub embeds: Vec<ComposedEmbed>,
89}
90
91/// A root level paged by keyset, as a Relay connection pages.
92///
93/// The rows past `cursor` on `cursor_column`, in `order_by` then `cursor_column`, forward
94/// or backward, `limit` of them — `RelayDatabaseAdapter::execute_relay_page`'s page. A
95/// backward page is read descending and returned in ascending `cursor_column` order, as
96/// the relay page is, so a connection answers the same page whichever of the two reads it.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ComposedKeyset {
99 /// The native column the page is keyed on (`relay_cursor_column`).
100 pub cursor_column: String,
101 /// The cursor the page starts past: `after` when forward, `before` when backward.
102 pub cursor: Option<CursorValue>,
103 /// `true` for `first`/`after`, `false` for `last`/`before`.
104 pub forward: bool,
105}
106
107/// Which keys of a stored document a composed level returns.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum LevelKeys {
110 /// The whole document. The root's shape: it is projected afterwards exactly as a flat
111 /// read of the same query is, so it reads what a flat read reads.
112 Whole,
113 /// The whole document, less these keys.
114 ///
115 /// The root's shape when some of its stored keys are embedded in their own place:
116 /// the embedded value is the gated one, and the stored one — which the level's
117 /// predicate never filtered — does not leave the database.
118 Without(Vec<String>),
119 /// Only these keys, and these keys as `null`.
120 ///
121 /// An embedded level's shape. A key the caller may not see never leaves the
122 /// database: `masked` keys are written as SQL `NULL`, and a key in neither list is
123 /// not returned at all.
124 Only {
125 /// Stored keys returned with their values, when the document has them.
126 kept: Vec<String>,
127 /// Stored keys returned as `null` whatever the document holds.
128 masked: Vec<String>,
129 },
130}
131
132/// How an embedded level is attached to each row of its parent.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum EmbedShape {
135 /// A JSON array of the related rows, `[]` when there are none.
136 Many,
137 /// The one related row, or `null`.
138 ///
139 /// The join key is unique on the target — a to-one relationship whose key is not is
140 /// refused when the schema loads — so there is at most one row to return.
141 One,
142 /// The number of related rows. The level's page, ordering, keys and embeds do not
143 /// apply to a count.
144 Count,
145}
146
147/// An embedded level and where its rows come from.
148#[derive(Debug, Clone, PartialEq)]
149pub struct ComposedEmbed {
150 /// The key the embedded value is written under in the parent row's `embeds` object.
151 pub output_key: String,
152 /// Array, object or count.
153 pub shape: EmbedShape,
154 /// The level's rows: its own view's, correlated, or its parent document's.
155 pub source: EmbedSource,
156 /// The embedded level itself.
157 pub level: ComposedLevel,
158}
159
160/// Where an embedded level's rows come from.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum EmbedSource {
163 /// Rows of the level's view whose `target_key` equals the parent row's `parent_key`.
164 Correlated {
165 /// The stored path on the **embedded** row that must equal the parent's key.
166 target_key: Vec<String>,
167 /// The stored path on the **parent** row it is compared with.
168 parent_key: Vec<String>,
169 /// The declared scalar type both keys are compared as — the cast a filter on the
170 /// embedded key would take, so the correlation matches the rows a predicate
171 /// `target_key = <the parent's value>` matched when it was a flat sub-read.
172 key_type: ScalarFieldType,
173 },
174 /// The value the parent row's own document holds under the first of `keys` it has:
175 /// each object element of it for [`EmbedShape::Many`], the object itself for
176 /// [`EmbedShape::One`]. The level's view is not read.
177 ///
178 /// The level's predicate is evaluated against each element as if it were a row of the
179 /// level's view, so it may only read the element's document: a native-column condition
180 /// has no column to read here, and is refused rather than rendered. A count, an
181 /// ordering and a projection are refused too — the elements keep the order the parent
182 /// stored them in.
183 Materialised {
184 /// The parent's stored spellings of the key, in the order they are tried.
185 keys: Vec<String>,
186 },
187}