Skip to main content

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}