fraiseql-core 2.19.1

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! Query classification — determines operation type for routing.

use super::super::{Executor, MutationRoot, QueryType};
use crate::{
    error::{FraiseQLError, Result},
    graphql::{operation_selection_error, parse_query_with_operation_name, value_json},
    runtime::{collect_variable_references, validate_variable_uses, validate_variables_used},
    schema::is_fact_table_root,
    security::SecurityContext,
};

/// A classified document: its type, its AST where the dispatch needs it, its
/// variables' declared defaults, and its variables as the per-request rules read them.
pub(in crate::runtime::executor) type Classification = (
    QueryType,
    Option<std::sync::Arc<crate::graphql::ParsedQuery>>,
    Vec<(String, serde_json::Value)>,
    Option<std::sync::Arc<CompletionTarget>>,
    DocumentVariables,
);

/// The executed operation's variable definitions and the variables it references: what
/// GraphQL § 5.8.2 and § 5.8.4 are checked from on every request.
///
/// § 5.8.2 resolves a declared type against the schema the *caller* may see, and
/// a classification is shared by every caller of the document, so the rule cannot be part
/// of it. § 5.8.4 runs after it, here too, so a document with both problems is answered as
/// it was when all three rules ran together: an undefined reference, then an unknown
/// type, then an unused variable.
#[derive(Debug, Default)]
pub(in crate::runtime::executor) struct DocumentVariables {
    operation_name: Option<String>,
    definitions:    Vec<crate::graphql::types::VariableDefinition>,
    defined:        Vec<String>,
    referenced:     Vec<String>,
}

/// A [`Classification`] as the parse cache holds it: with the document and the
/// operation name it was computed from.
///
/// The cache is keyed by a 64-bit hash, so a hit is a claim that the document is
/// the same, not a proof. The entry carries what it answers for, and a hit whose
/// source or operation name differs is treated as a miss: a key collision costs a
/// re-classification, never another document's verdict.
#[derive(Debug)]
pub(in crate::runtime::executor) struct CachedClassification {
    source: Box<str>,
    operation_name: Option<Box<str>>,
    /// The verdict, shared by every request for this document (#1562).
    pub(in crate::runtime::executor) classification: Classification,
}

impl CachedClassification {
    /// `classification`, as computed for `source` with `operation_name` selected.
    pub(in crate::runtime::executor) fn new(
        source: &str,
        operation_name: Option<&str>,
        classification: Classification,
    ) -> Self {
        Self {
            source: source.into(),
            operation_name: operation_name.map(Into::into),
            classification,
        }
    }

    /// Whether this entry was computed for `source` with `operation_name` selected.
    pub(in crate::runtime::executor) fn answers(
        &self,
        source: &str,
        operation_name: Option<&str>,
    ) -> bool {
        *self.source == *source && self.operation_name.as_deref() == operation_name
    }
}

/// What a response is completed against (§ 6.4.4, #1522): the operation's root type and
/// its root selection set, fragment spreads expanded. Computed once per document, with its
/// classification, for every operation type; `None` when the spreads cannot be expanded
/// (the document is refused elsewhere).
#[derive(Debug)]
pub(in crate::runtime::executor) struct CompletionTarget {
    /// `Query`, `Mutation` or `Subscription`.
    pub(in crate::runtime::executor) root_type:  &'static str,
    /// The root selection set, spreads expanded, and the fields it collects per set of
    /// directive outcomes.
    pub(in crate::runtime::executor) selections: crate::graphql::selection_set::CollectedFields,
}

impl Executor {
    /// Check `variables` for this request: every declared type is one `caller` may see
    /// (GraphQL § 5.8.2), then every defined variable is used (§ 5.8.4).
    ///
    /// # Errors
    ///
    /// [`FraiseQLError::Validation`](crate::error::FraiseQLError::Validation) naming the
    /// first variable that breaks either rule.
    pub(in crate::runtime::executor) fn check_variables(
        &self,
        variables: &DocumentVariables,
        caller: Option<&SecurityContext>,
    ) -> Result<()> {
        let operation_name = variables.operation_name.as_deref();
        self.validate_variable_types_for(
            caller,
            operation_name,
            &variables.definitions,
            self.ctx.config.suggestions,
        )?;
        validate_variables_used(operation_name, &variables.defined, &variables.referenced)
    }

    /// [`check_variables`](Self::check_variables) for a surface that runs as no caller and
    /// so resolves § 5.8.2 against the whole schema: `plan_query`, which the server mounts
    /// only behind the admin token.
    ///
    /// # Errors
    ///
    /// As [`check_variables`](Self::check_variables).
    pub(in crate::runtime::executor) fn check_variables_against_the_whole_schema(
        &self,
        variables: &DocumentVariables,
    ) -> Result<()> {
        let operation_name = variables.operation_name.as_deref();
        crate::runtime::validate_variable_types(
            &self.ctx.schema,
            &self.ctx.schema,
            operation_name,
            &variables.definitions,
            self.ctx.config.suggestions,
        )?;
        validate_variables_used(operation_name, &variables.defined, &variables.referenced)
    }

    /// Classify a GraphQL query into its operation type for routing.
    ///
    /// This is the first phase of query execution. It determines which handler
    /// to invoke based on the query structure and conventions:
    ///
    /// - **Introspection** (`__schema`, `__type`) → Uses pre-built responses (zero-cost)
    /// - **Federation** (`_service`, `_entities`) → Fed-specific logic
    /// - **Relay node** (`node(id: "...")`) → Global ID lookup
    /// - **Mutations** (`mutation { ... }`) → Write operations
    /// - **Aggregates** (root field ends with `_aggregate`) → Analytics queries
    /// - **Windows** (root field ends with `_window`) → Time-series queries
    /// - **Regular** (default) → Standard field selections
    ///
    /// # Errors
    ///
    /// Returns `FraiseQLError::Parse` if the query string is malformed GraphQL.
    ///
    /// # Example
    ///
    /// ```text
    /// // Illustrative — classify_query() is internal.
    /// // Use executor.execute(query, None).await? for the public API.
    ///
    /// // Regular query → Regular
    /// // Mutation       → Mutation  → execute_mutation_query()
    /// // __schema       → Introspection
    /// // _entities      → Federation
    /// ```
    pub(in crate::runtime::executor) fn classify_query(
        &self,
        query: &str,
        operation_name: Option<&str>,
    ) -> Result<QueryType> {
        self.classify_query_with_parse(query, operation_name).map(|(qt, ..)| qt)
    }

    /// Classify a query and simultaneously return the parsed AST for `Regular`
    /// queries, avoiding a redundant parse in the multi-root pipeline path.
    ///
    /// Returns `(QueryType, Some(ParsedQuery), defaults)` for `Regular` queries and
    /// `(QueryType, None, defaults)` for all other types (introspection, federation,
    /// etc.). `defaults` are the operation's declared variable defaults, decoded: a
    /// property of the document, applied to each request's variables at dispatch
    /// (§ 6.4.1, #1504).
    ///
    /// # Errors
    ///
    /// Returns [`FraiseQLError::Parse`] if the query string is malformed GraphQL.
    pub(in crate::runtime::executor) fn classify_query_with_parse(
        &self,
        query: &str,
        operation_name: Option<&str>,
    ) -> Result<Classification> {
        // Parse the query once; the AST is the canonical source of truth.
        // Substring scans on the raw string produce false-positives on aliases,
        // comments, and string argument values (e.g. `{ search(q: "_service") }`
        // would be mis-routed as a federation query by a text scan).
        //
        // GraphQL § 6.1 — the request selects which operation runs. Passing the
        // name here rather than taking the document's first operation is what
        // keeps a two-operation document from answering with the wrong one.
        let parsed = parse_query_with_operation_name(query, operation_name)
            .map_err(|e| operation_selection_error(&e))?;
        let defaults = value_json::variable_defaults(&parsed.variables)?;
        let root_type = match parsed.operation_type.as_str() {
            "mutation" => "Mutation",
            "subscription" => "Subscription",
            _ => "Query",
        };
        let completion = crate::graphql::selection_set::resolve(
            &parsed.selections,
            &parsed.fragments,
            self.max_query_depth(),
        )
        .ok()
        .map(|selections| {
            std::sync::Arc::new(CompletionTarget {
                root_type,
                selections: crate::graphql::selection_set::CollectedFields::new(selections),
            })
        });
        let variables = Self::document_variables(&parsed, self.ctx.config.suggestions)?;
        let (query_type, parsed) = self.classify_parsed(parsed)?;
        Ok((query_type, parsed.map(std::sync::Arc::new), defaults, completion, variables))
    }

    /// The executed operation's [`DocumentVariables`], refusing a reference to a variable it
    /// does not define (GraphQL § 5.8.3).
    ///
    /// Run immediately after the parse and *before* the routing branches, because this is
    /// the only point every operation type shares while the AST still exists:
    /// `classify_query_with_parse` returns the parsed query only for `Regular`, so a check in
    /// `execute_dispatch` would have nothing to read for a mutation, aggregate, window, node
    /// or federation document. What it reads is kept for the two rules that run per request
    /// ([`check_variables`](Self::check_variables)).
    ///
    /// Not in `QueryMatcher::match_query`: `field_selection_to_query` re-serialises each
    /// root of a multi-root query into a synthetic single-root string that carries no
    /// variable *definitions* while faithfully re-emitting `$name` references, so a check
    /// there would reject every multi-root query that uses a variable.
    fn document_variables(
        parsed: &crate::graphql::ParsedQuery,
        suggestions: bool,
    ) -> Result<DocumentVariables> {
        let operation_name = parsed.operation_name.as_deref();
        let defined: Vec<String> = parsed.variables.iter().map(|v| v.name.clone()).collect();
        let referenced = collect_variable_references(parsed)?;
        validate_variable_uses(operation_name, &defined, &referenced, suggestions)?;
        Ok(DocumentVariables {
            operation_name: parsed.operation_name.clone(),
            definitions: parsed.variables.clone(),
            defined,
            referenced,
        })
    }

    /// The routing half of [`classify_query_with_parse`](Self::classify_query_with_parse).
    fn classify_parsed(
        &self,
        parsed: crate::graphql::ParsedQuery,
    ) -> Result<(QueryType, Option<crate::graphql::ParsedQuery>)> {
        let root_field = &parsed.root_field;

        // A fact-table root is executed alone: its runner answers the document's first
        // root only, so any root beside it was silently dropped. Refused, naming the roots,
        // whichever comes first.
        if parsed.operation_type != "mutation" && parsed.selections.len() > 1 {
            if let Some(fact_root) =
                parsed.selections.iter().find(|root| is_fact_table_root(&root.name))
            {
                let others: Vec<String> = parsed
                    .selections
                    .iter()
                    .filter(|root| !std::ptr::eq(*root, fact_root))
                    .map(|root| format!("`{}`", root.response_key()))
                    .collect();
                return Err(FraiseQLError::Validation {
                    message: format!(
                        "`{}` must be the only root of its document; it was sent with {}: send \
                         each in its own request",
                        fact_root.response_key(),
                        others.join(", ")
                    ),
                    path:    None,
                });
            }
        }

        // Introspection (highest priority): `__schema` or `__type`.
        // These are meta-fields defined by the GraphQL spec — always a root query.
        //
        // The AST travels with introspection so the dispatch can project the
        // pre-built response onto the client's selection set (§ 6.3). It is
        // memoised in the parse cache alongside the classification, so carrying
        // it costs one clone per distinct query string, not one per request.
        if root_field == "__schema" || root_field == "__type" {
            return Ok((QueryType::Introspection, Some(parsed)));
        }

        // Root `__typename` meta-field (GraphQL spec §"Type Name Introspection"):
        // a single-root selection consisting solely of `__typename` (optionally
        // aliased) resolves to the operation's root type name without a DB
        // round-trip. Placed before the mutation branch so `mutation { __typename }`
        // and `subscription { __typename }` resolve correctly instead of being
        // routed as a (missing) mutation field. The `len() == 1` guard is
        // load-bearing: mixed roots like `{ __typename users { id } }` fall through
        // to `Regular` and are resolved by the multi-root pipeline.
        if root_field == "__typename" && parsed.selections.len() == 1 {
            return Ok((
                QueryType::TypeName {
                    selection:      Box::new(parsed.selections[0].clone()),
                    operation_type: parsed.operation_type.clone(),
                },
                None,
            ));
        }

        // Federation (Apollo Federation v1/v2 entry-points).
        if root_field == "_service" || root_field == "_entities" {
            return Ok((QueryType::Federation(root_field.clone()), None));
        }

        // Relay global node lookup: root field is exactly "node" on a query.
        //
        // Named fragment spreads are expanded here, using the same routine as the
        // query matcher and the mutation branch below. This branch used to lift
        // `sel.nested_fields` out of every child whose name starts with `"..."`,
        // which is right for an inline fragment (whose nested fields hold the real
        // selections) and silently *dropped* a named spread, whose pseudo-field
        // always has an empty `nested_fields` (#827). Relay Modern issues
        // `node(id: $id) { id ...Container_data }` for every lookup, so that was
        // the canonical shape.
        //
        // `@skip`/`@include` are evaluated later in `execute_node_query`, where
        // the request variables are available — the same split as mutations, and
        // what keeps this classification cacheable by query string alone.
        if parsed.operation_type == "query" && root_field == "node" {
            let raw = parsed.selections.first().map_or(&[][..], |s| s.nested_fields.as_slice());
            let resolved = crate::graphql::selection_set::resolve(
                raw,
                &parsed.fragments,
                self.max_query_depth(),
            )?;

            // Flatten inline fragments: `node { ... on Booking { id startDate } }`.
            // Only `"...on "` entries carry their selections in `nested_fields`;
            // after expansion no bare `"...Name"` spread survives. Lifting the
            // children out discards the inline fragment itself, so its own
            // directives travel with them.
            let mut selections = Vec::with_capacity(resolved.len());
            for sel in resolved {
                if sel.name.starts_with("...on ") {
                    for mut child in sel.nested_fields {
                        child.inherit_directives(&sel.directives);
                        selections.push(child);
                    }
                } else {
                    selections.push(sel);
                }
            }
            return Ok((QueryType::NodeQuery { selections }, None));
        }

        // Mutations are routed by operation type. Carry the full result selection
        // set (inline fragments intact) so the projector can subset and recurse
        // exactly like the query path. Named fragment spreads are resolved here —
        // the same as the query matcher — using the document's fragment
        // definitions; `@skip`/`@include` directives are evaluated later in
        // `execute_mutation_impl`, where the request variables are available.
        //
        // *Every* root is carried, not just the first: the spec requires all of a
        // mutation's root fields to execute, serially, in document order (#759).
        if parsed.operation_type == "mutation" {
            let roots = parsed
                .selections
                .iter()
                .map(|root| {
                    Ok(MutationRoot {
                        response_key: root.response_key().to_string(),
                        name:         root.name.clone(),
                        selections:   crate::graphql::selection_set::resolve(
                            &root.nested_fields,
                            &parsed.fragments,
                            self.max_query_depth(),
                        )?,
                        arguments:    root.arguments.clone(),
                    })
                })
                .collect::<Result<Vec<_>>>()?;
            return Ok((QueryType::Mutation { roots }, None));
        }

        // Aggregate and window queries (root field ends with `_aggregate` / `_window`). Their
        // planners read the request — `groupBy`, `limit`, `offset`, … — from the root field's
        // arguments, inline or as variables (#1609); the parsed document goes with them.
        if is_fact_table_root(root_field) {
            let query_type = if root_field.ends_with("_aggregate") {
                QueryType::Aggregate(root_field.clone())
            } else {
                QueryType::Window(root_field.clone())
            };
            return Ok((query_type, Some(parsed)));
        }

        // Regular query — return the already-parsed AST to avoid re-parsing in
        // the multi-root pipeline path.
        Ok((QueryType::Regular, Some(parsed)))
    }
}