Skip to main content

postrust_graphql/
hasura.rs

1//! The response envelope Hasura clients expect.
2//!
3//! async-graphql answers in the shape the GraphQL specification describes:
4//! `data` is always present, errors carry `locations` and a `path` that is a
5//! list of segments. Hasura answers in a narrower shape of its own, and client
6//! code branches on it:
7//!
8//! ```json
9//! {"errors": [{"message": "...",
10//!              "extensions": {"path": "$.selectionSet.author", "code": "validation-failed"}}]}
11//! ```
12//!
13//! Two differences matter to a client. A failed request has no `data` key at
14//! all, rather than `"data": null` -- code written as `if (body.data)` reads
15//! the two the same way, but code written as `if ('data' in body)` does not.
16//! And `extensions.code` is the machine-readable half: it is what a client
17//! switches on to tell a permission failure from a constraint violation, and
18//! the message text is only for a human.
19//!
20//! The status code is 200 for almost all of this: a GraphQL error is a value
21//! in the response body, not a transport failure. Of the 468 cases in Hasura's
22//! own corpus that this server is measured against, 465 answer 200 --
23//! including every constraint violation, every refusal to authenticate, and
24//! every permission failure at `/v1/graphql`.
25//!
26//! The exception belongs to the *endpoint*, not to the error. All three cases
27//! that answer 400 were sent to `/v1alpha1/graphql`, the address Hasura served
28//! before `/v1`, and all three are a write refused by the `check` of an insert
29//! or update permission. The same refusal at `/v1/graphql` is a 200. Five
30//! cases prove the second half: identical bodies, identical codes, different
31//! status, and the only thing that differs is where they were sent.
32//!
33//! This was read out of the corpus's declared statuses once and read wrongly:
34//! taking it for a rule about the error meant answering 400 at `/v1/graphql`,
35//! which is three cases' worth of wrong. [`status_for`] takes the endpoint.
36
37use async_graphql::{Response, ServerError};
38use serde_json::{json, Map, Value};
39use std::collections::HashMap;
40
41/// Hasura's `extensions.code` for a server error.
42///
43/// The code is inferred from the error's own extensions where the resolver
44/// set one, and otherwise from the message. Guessing from text is not
45/// something to be proud of, but the alternative -- omitting the code -- is
46/// worse: a client that switches on it would fall through to its default
47/// branch for every error this server produces.
48fn code_for(error: &ServerError) -> &'static str {
49    if let Some(extensions) = &error.extensions {
50        if let Some(Value::String(code)) = extensions.get("code").map(value_of) {
51            return match code.as_str() {
52                "validation-failed" => "validation-failed",
53                "bad-request" => "bad-request",
54                // Nothing authenticated the request, or the role it claimed is
55                // not one it may claim. Distinct from `permission-error`,
56                // which is a rule refusing an authenticated caller.
57                "access-denied" => "access-denied",
58                // A token was offered and did not verify.
59                "invalid-jwt" => "invalid-jwt",
60                // A token that verified and named no role to be.
61                "jwt-missing-role-claims" => "jwt-missing-role-claims",
62                // What Hasura answers when a permission needs a session
63                // variable the caller does not carry: the variable was looked
64                // for and is not there.
65                "not-found" => "not-found",
66                "permission-error" => "permission-error",
67                "constraint-violation" => "constraint-violation",
68                "data-exception" => "data-exception",
69                "not-supported" => "not-supported",
70                "parse-failed" => "parse-failed",
71                _ => "unexpected",
72            };
73        }
74    }
75
76    let message = error.message.to_ascii_lowercase();
77    // Validation is tested first: a rejected enum value says "enumeration type
78    // ... does not contain the value", which mentions no constraint but reads
79    // as one to a later test that only looks for the word.
80    if message.contains("invalid value for argument")
81        || message.contains("does not contain the value")
82        || message.contains("is not defined by operation")
83        // A role granted only reads has no mutation root, so a document naming
84        // a mutation names an operation this schema does not have. That is the
85        // document being wrong about the schema, which is what validation is.
86        // async-graphql's wording, classified here rather than there.
87        || message.contains("not configured for mutations")
88        || message.contains("not configured for subscriptions")
89    {
90        "validation-failed"
91    } else if message.contains("permission") || message.contains("not allowed") {
92        "permission-error"
93    } else if message.contains("violates") || message.contains("constraint") {
94        "constraint-violation"
95    } else if message.contains("unknown argument")
96        || message.contains("cannot query field")
97        || message.contains("expected")
98        || message.contains("unknown field")
99        // Everything the variable rules say. A document that names a variable
100        // it never declared is refused before a resolver runs, so calling it a
101        // database error sends a client looking in the wrong place.
102        || message.starts_with("variable \"")
103    {
104        "validation-failed"
105    } else {
106        "postgres-error"
107    }
108}
109
110fn value_of(value: &async_graphql::Value) -> Value {
111    serde_json::to_value(value).unwrap_or(Value::Null)
112}
113
114/// Render an error's path the way Hasura spells it.
115///
116/// Hasura writes a JSONPath-ish string rooted at the operation --
117/// `$.selectionSet.author.args.where` -- where the specification writes a list
118/// of segments. A list index stays an index; everything else is a field name.
119fn path_for(error: &ServerError) -> String {
120    use async_graphql::PathSegment;
121
122    // Written where the error was raised, when whoever raised it knew where it
123    // was. That is the only way to reach an *argument*: `$.selectionSet.
124    // insert_author.args.objects[0].bio` names a place inside a value the
125    // client sent, and a response path only ever names a field of the answer.
126    if let Some(Value::String(path)) = error
127        .extensions
128        .as_ref()
129        .and_then(|extensions| extensions.get("path"))
130        .map(value_of)
131    {
132        return path;
133    }
134
135    if error.path.is_empty() {
136        return "$".to_string();
137    }
138    // Otherwise the response path, in Hasura's spelling: a field of a field is
139    // written with the selection set that holds it between them, so `author`
140    // then `articles` is `$.selectionSet.author.selectionSet.articles` rather
141    // than `$.selectionSet.author.articles`.
142    //
143    // Which row of a list the error came from is dropped. A response path has
144    // it and Hasura's selection paths never do -- an index appears in one of
145    // its paths only inside an argument, where it names a row the client
146    // *sent*. Naming a field is what a selection path is for.
147    let mut rendered = String::from("$");
148    for segment in &error.path {
149        if let PathSegment::Field(name) = segment {
150            rendered.push_str(".selectionSet.");
151            rendered.push_str(name);
152        }
153    }
154    rendered
155}
156
157/// Convert an async-graphql response into Hasura's envelope.
158pub fn envelope(response: Response) -> Value {
159    if !response.errors.is_empty() {
160        let errors: Vec<Value> = response
161            .errors
162            .iter()
163            .map(|error| {
164                json!({
165                    "message": error.message,
166                    "extensions": {
167                        "path": path_for(error),
168                        "code": code_for(error),
169                    }
170                })
171            })
172            .collect();
173        // No `data` key. Hasura omits it entirely on failure.
174        return json!({ "errors": errors });
175    }
176
177    let mut body = Map::new();
178    body.insert("data".to_string(), value_of(&response.data));
179    Value::Object(body)
180}
181
182/// Which address the request came in on.
183///
184/// The only thing that decides a status here, so it is passed rather than
185/// guessed. `/api/graphql` is this server's own name for the endpoint and
186/// follows `/v1`, which is what a client pointed at it expects.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum Endpoint {
189    /// `/v1/graphql` and `/api/graphql`: every answer is a 200.
190    Current,
191    /// `/v1alpha1/graphql`: a check refused by a permission is a 400.
192    Legacy,
193}
194
195/// The HTTP status a body is answered with.
196///
197/// 200, unless a write refused by a permission's `check` reached the legacy
198/// endpoint. See the note at the top of this module: the status belongs to the
199/// address, and the same refusal at `/v1/graphql` is a 200.
200pub fn status_for(body: &Value, endpoint: Endpoint) -> u16 {
201    if endpoint == Endpoint::Current {
202        return 200;
203    }
204    let refused = body
205        .get("errors")
206        .and_then(Value::as_array)
207        .is_some_and(|errors| {
208            errors.iter().any(|error| {
209                error.get("message").and_then(Value::as_str) == Some(crate::role::CHECK_FAILED)
210            })
211        });
212    match refused {
213        true => 400,
214        false => 200,
215    }
216}
217
218/// A request refused before it was read.
219///
220/// Nothing authenticated the caller, or the role it named is not one it may
221/// name. The document is never parsed, so there is no path into it to report
222/// and no operation to blame -- which is why Hasura answers `$` here and this
223/// does too. Still a 200: a GraphQL error is a value in the body.
224pub fn denied(message: &str) -> Value {
225    refused(message, "access-denied")
226}
227
228/// A request whose token was offered and could not be verified.
229///
230/// Refused rather than answered as the anonymous role: a bad signature, an
231/// expired token or a claim out of place is a credential the client believes
232/// in, and serving it as a stranger hides that it stopped working. REST says
233/// 401; Hasura says `invalid-jwt`, in the same shape as [`denied`].
234//
235// Hidden: public only so that `postrust-server`, which authenticates, can
236// call it. It is not an API anyone else should build on.
237#[doc(hidden)]
238pub fn invalid_jwt(reason: &str) -> Value {
239    refused(&format!("Could not verify JWT: {reason}"), "invalid-jwt")
240}
241
242/// A request whose token verified and named no role, where there is no
243/// anonymous role to fall back to.
244///
245/// REST says 401. Hasura refuses a token without its role claims as
246/// `jwt-missing-role-claims`, naming the claim, and so does this.
247//
248// Hidden for the reason [`invalid_jwt`] is.
249#[doc(hidden)]
250pub fn missing_role_claim(claim: &str) -> Value {
251    refused(
252        &format!("JWT claim does not contain {claim}"),
253        "jwt-missing-role-claims",
254    )
255}
256
257fn refused(message: &str, code: &str) -> Value {
258    let mut error = ServerError::new(message, None);
259    let mut extensions = async_graphql::ErrorExtensionValues::default();
260    extensions.set("code", code);
261    error.extensions = Some(extensions);
262
263    let mut response = Response::new(async_graphql::Value::Null);
264    response.errors = vec![error];
265    envelope(response)
266}
267
268/// Whether the request is an Apollo persisted-query handshake.
269///
270/// A client with automatic persisted queries sends the hash alone, and sends
271/// the document only when the server says it has not seen that hash. Neither
272/// server implements the protocol; the difference is that Hasura says so --
273/// `PersistedQueryNotSupported` -- and this used to run whatever document was
274/// beside the hash, which is the answer that looks like the handshake worked.
275/// A client would then send hash-only requests and get nothing back.
276pub fn persisted_query(request: &async_graphql::Request) -> bool {
277    request.extensions.contains_key("persistedQuery")
278}
279
280/// Hasura's refusal for one, which is the whole answer.
281pub fn not_supported(message: &str) -> Value {
282    let mut error = ServerError::new(message, None);
283    let mut extensions = async_graphql::ErrorExtensionValues::default();
284    extensions.set("code", "not-supported");
285    extensions.set("path", "$");
286    error.extensions = Some(extensions);
287
288    let mut response = Response::new(async_graphql::Value::Null);
289    response.errors = vec![error];
290    envelope(response)
291}
292
293/// A refusal about the request itself rather than about what it asked for.
294///
295/// `bad-request` at `$`: nothing in the document is wrong, so there is no part
296/// of it to point at. A header Hasura reads and cannot parse is the case that
297/// needs it.
298pub fn malformed(message: &str) -> Value {
299    let mut error = ServerError::new(message, None);
300    let mut extensions = async_graphql::ErrorExtensionValues::default();
301    extensions.set("code", "bad-request");
302    extensions.set("path", "$");
303    error.extensions = Some(extensions);
304
305    let mut response = Response::new(async_graphql::Value::Null);
306    response.errors = vec![error];
307    envelope(response)
308}
309
310/// The answer while there is no schema to answer from: start-up could not
311/// build one, and no reload since has.
312///
313/// `unexpected` at `$`, Hasura's code for a fault of the server's rather than
314/// of the request. The request may be fine, and will be answered once a
315/// migration fixes what stopped the schema from being built.
316pub fn unavailable(message: &str) -> Value {
317    let mut error = ServerError::new(message, None);
318    let mut extensions = async_graphql::ErrorExtensionValues::default();
319    extensions.set("code", "unexpected");
320    extensions.set("path", "$");
321    error.extensions = Some(extensions);
322
323    let mut response = Response::new(async_graphql::Value::Null);
324    response.errors = vec![error];
325    envelope(response)
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use async_graphql::{PathSegment, ServerError};
332    use pretty_assertions::assert_eq;
333
334    fn error_at(message: &str, path: Vec<PathSegment>) -> ServerError {
335        let mut error = ServerError::new(message, None);
336        error.path = path;
337        error
338    }
339
340    #[test]
341    fn only_a_refused_write_is_answered_with_a_failure_status() {
342        // Hasura's own inconsistency, reproduced because a client may branch
343        // on it: this one error is 400 and every other refusal is 200.
344        let refused = json!({"errors": [{"message": crate::role::CHECK_FAILED,
345                                         "extensions": {"code": "permission-error"}}]});
346        assert_eq!(status_for(&refused, Endpoint::Legacy), 400);
347        // The same refusal at the address a Hasura client actually uses.
348        assert_eq!(status_for(&refused, Endpoint::Current), 200);
349
350        // The same code, a different error: four cases in the corpus.
351        let denied = json!({"errors": [{"message": "not allowed",
352                                        "extensions": {"code": "permission-error"}}]});
353        assert_eq!(status_for(&denied, Endpoint::Legacy), 200);
354        assert_eq!(
355            status_for(&json!({"data": {"article": []}}), Endpoint::Legacy),
356            200
357        );
358    }
359
360    #[test]
361    fn a_role_may_be_refused_the_schema_without_being_refused_its_rows() {
362        let asked = |query: &str| {
363            let doc = async_graphql::parser::parse_query(query).unwrap();
364            introspection_refusal(&doc, "reader").map(|e| e.message)
365        };
366        let refusal = Some("introspection is disabled for role: \"reader\"".to_string());
367        assert_eq!(asked("{ __schema { types { name } } }"), refusal);
368        assert_eq!(asked("{ __type(name: \"article\") { name } }"), refusal);
369        // Not introspection in the sense that matters: every client library
370        // sends it, and Hasura answers it.
371        assert_eq!(asked("{ article { __typename id } }"), None);
372        assert_eq!(asked("{ article { id } }"), None);
373    }
374
375    #[test]
376    fn a_failed_response_has_no_data_key() {
377        let mut response = Response::new(async_graphql::Value::Null);
378        response.errors = vec![error_at("no", vec![])];
379        let body = envelope(response);
380        assert!(body.get("data").is_none());
381        assert!(body.get("errors").is_some());
382    }
383
384    #[test]
385    fn a_successful_response_has_no_errors_key() {
386        let body = envelope(Response::new(async_graphql::Value::Null));
387        assert!(body.get("errors").is_none());
388        assert!(body.get("data").is_some());
389    }
390
391    #[test]
392    fn path_is_rooted_at_the_selection_set() {
393        let error = error_at(
394            "boom",
395            vec![
396                PathSegment::Field("insert_author".to_string()),
397                PathSegment::Index(0),
398                PathSegment::Field("name".to_string()),
399            ],
400        );
401        assert_eq!(
402            path_for(&error),
403            "$.selectionSet.insert_author.selectionSet.name"
404        );
405    }
406
407    /// A path written where the error was raised wins over the response path,
408    /// because it can name a place the response path cannot: inside an
409    /// argument the client sent.
410    /// A hash beside the document is a handshake, and neither server speaks
411    /// it. Saying so is the difference between a client that falls back and
412    /// one that sends hash-only requests into silence.
413    #[test]
414    fn a_persisted_query_is_refused_as_unsupported() {
415        let plain = async_graphql::Request::new("{ author { id } }");
416        assert!(!persisted_query(&plain));
417
418        let mut carried = async_graphql::Request::new("{ author { id } }");
419        carried.extensions.0.insert(
420            "persistedQuery".to_string(),
421            async_graphql::Value::Object(Default::default()),
422        );
423        assert!(persisted_query(&carried));
424
425        assert_eq!(
426            not_supported("PersistedQueryNotSupported"),
427            json!({"errors": [{
428                "message": "PersistedQueryNotSupported",
429                "extensions": {"path": "$", "code": "not-supported"}
430            }]})
431        );
432    }
433
434    #[test]
435    fn a_path_given_with_the_error_is_the_one_answered() {
436        let mut error = error_at(
437            "boom",
438            vec![PathSegment::Field("insert_author".to_string())],
439        );
440        let mut extensions = async_graphql::ErrorExtensionValues::default();
441        extensions.set("code", "permission-error");
442        extensions.set("path", "$.selectionSet.insert_author.args.objects");
443        error.extensions = Some(extensions);
444        assert_eq!(
445            path_for(&error),
446            "$.selectionSet.insert_author.args.objects"
447        );
448    }
449
450    #[test]
451    fn an_error_with_no_path_is_rooted_at_the_document() {
452        assert_eq!(path_for(&error_at("boom", vec![])), "$");
453    }
454
455    #[test]
456    fn a_validation_message_is_coded_as_one() {
457        let error = ServerError::new("Unknown argument \"filter\" on field \"author\"", None);
458        assert_eq!(code_for(&error), "validation-failed");
459    }
460
461    #[test]
462    fn a_constraint_message_is_coded_as_one() {
463        let error = ServerError::new("duplicate key value violates unique constraint", None);
464        assert_eq!(code_for(&error), "constraint-violation");
465    }
466}
467
468/// Drop variable definitions the document never uses.
469///
470/// The specification's "All Variables Used" rule makes
471/// `query ($tags: jsonb) { article(where: {tags: {_contains: "latest"}}) { id } }`
472/// an invalid document, and async-graphql refuses it. Hasura executes it. A
473/// client that has been sending that query for years -- because a filter was
474/// edited and the declaration was left behind -- gets an answer from the server
475/// it is migrating off and an error from this one, which is the kind of
476/// difference a migration discovers in production.
477///
478/// So the document is parsed here, the unused declarations are removed, and
479/// what validation sees has nothing to complain about. Every other rule still
480/// runs: this makes one specific refusal go away, not validation in general.
481/// A document that does not parse is handed on untouched, so the parse error is
482/// reported by the executor with its own position rather than by this.
483pub fn allow_unused_variables(request: async_graphql::Request) -> async_graphql::Request {
484    prepare(None, request, None).unwrap_or_else(|(request, _)| request)
485}
486
487/// Ready a request for execution, and refuse it where async-graphql would not.
488///
489/// Two passes over the same parsed document, because parsing it twice to do
490/// them separately would be the only reason to separate them:
491///
492/// - variable declarations nothing uses are dropped, since Hasura executes
493///   such a document and the specification does not;
494/// - variables used where their declared type does not fit are refused, since
495///   the specification says so and async-graphql does not.
496///
497/// `schema` is what the second pass needs -- the types a variable is being
498/// used against. Without one only the first pass runs.
499///
500/// `Err` carries the request back beside the errors, so the caller can answer
501/// with them rather than executing.
502// The error carries the request back, which is the whole point of it: the
503// caller answers with the errors instead of executing, and needs the request
504// to do anything else with. Boxing it would hide that.
505#[allow(clippy::result_large_err)]
506pub fn prepare(
507    schema: Option<&async_graphql::dynamic::Schema>,
508    request: async_graphql::Request,
509    // The role introspection is withheld from, where it is withheld from this
510    // one. Checked here because this is where the document is already parsed,
511    // and because refusing it is the whole answer: a schema a role may not
512    // read is not one it may read a little of.
513    introspection_disabled_for: Option<&str>,
514) -> Result<async_graphql::Request, (async_graphql::Request, Vec<ServerError>)> {
515    let mut request = rewrite_document(schema, request);
516    // Parsed again rather than threaded through: `set_parsed_query` takes the
517    // document by value and gives nothing back, and re-parsing a document that
518    // has already parsed once is not where the time goes.
519    //
520    // A document that does not parse is answered here rather than left to
521    // async-graphql, which reports it the way a compiler does -- the offending
522    // line, a caret under the column, and what it expected instead. Hasura
523    // says the document is not a query and names the document, and a client
524    // showing that to a user is showing text it already ships. Nothing further
525    // can run either way, so this is the whole of the answer.
526    let doc = match async_graphql::parser::parse_query(&request.query) {
527        Ok(doc) => doc,
528        Err(_) => {
529            return Err((
530                request,
531                vec![coded(
532                    "not a valid graphql query".to_string(),
533                    async_graphql::Pos::default(),
534                    "$.query",
535                )],
536            ))
537        }
538    };
539    if let Some(schema) = schema {
540        if let Some(role) = introspection_disabled_for {
541            if let Some(error) = introspection_refusal(&doc, role) {
542                return Err((request, vec![error]));
543            }
544        }
545        let mut errors = variable_errors(&doc, schema.registry(), &request.variables);
546        // The first refusal is the whole answer. Hasura validates a request
547        // as a parser reads one -- it stops where it cannot go on -- and in
548        // 468 replayed cases it never answered with a second error. A query
549        // naming two fields that do not exist is refused for the first of
550        // them, and so is one whose `on_conflict` carries an unknown key
551        // beside a missing required one.
552        //
553        // Reporting all of them is the more useful answer and the wrong one
554        // here: a client written against Hasura reads `errors[0]`, and the
555        // rest of the list is text it will not show. The walk still runs to
556        // the end, because it is cheap and because stopping it would put the
557        // stopping condition in every arm.
558        errors.truncate(1);
559        if !errors.is_empty() {
560            return Err((request, errors));
561        }
562    }
563    let _ = &mut request;
564    Ok(request)
565}
566
567/// Whether a document asks the schema about itself, and the refusal if so.
568///
569/// `__typename` is not introspection in the sense that matters: it names the
570/// type of a row a client is already reading, every client library sends it,
571/// and Hasura answers it for a role introspection is disabled for. `__schema`
572/// and `__type` are the two that read the schema as data.
573///
574/// The message and the code are Hasura's.
575fn introspection_refusal(
576    doc: &async_graphql::parser::types::ExecutableDocument,
577    role: &str,
578) -> Option<ServerError> {
579    use async_graphql::parser::types::{OperationType, Selection};
580
581    let operations: Vec<_> = match &doc.operations {
582        async_graphql::parser::types::DocumentOperations::Single(one) => vec![one],
583        async_graphql::parser::types::DocumentOperations::Multiple(many) => many.values().collect(),
584    };
585
586    let asks = operations.iter().any(|operation| {
587        operation.node.ty == OperationType::Query
588            && operation
589                .node
590                .selection_set
591                .node
592                .items
593                .iter()
594                .any(|item| match &item.node {
595                    Selection::Field(field) => {
596                        matches!(field.node.name.node.as_str(), "__schema" | "__type")
597                    }
598                    _ => false,
599                })
600    });
601
602    match asks {
603        false => None,
604        true => {
605            let mut error = ServerError::new(
606                format!("introspection is disabled for role: \"{}\"", role),
607                None,
608            );
609            let mut extensions = async_graphql::ErrorExtensionValues::default();
610            extensions.set("code", "not-supported");
611            error.extensions = Some(extensions);
612            Some(error)
613        }
614    }
615}
616
617/// The edits made to a document before it is validated.
618///
619/// A declaration nothing uses is dropped, and a value written as a string
620/// where a number or a boolean is expected becomes one. Both are things
621/// Hasura accepts and the specification does not, and both are edits to the
622/// same parsed document -- which is why they are one pass: `set_parsed_query`
623/// takes the document by value, so a second pass would have to re-parse the
624/// source text and would undo the first.
625fn rewrite_document(
626    schema: Option<&async_graphql::dynamic::Schema>,
627    mut request: async_graphql::Request,
628) -> async_graphql::Request {
629    use async_graphql::parser::types::{
630        DocumentOperations, ExecutableDocument, Selection, SelectionSet,
631    };
632    use async_graphql::Name;
633    // The executable `Value`, which has a `Variable` case; `async_graphql::Value`
634    // is the constant one a variable has already been substituted into.
635    use async_graphql_value::Value;
636
637    let Ok(mut doc): Result<ExecutableDocument, _> =
638        async_graphql::parser::parse_query(&request.query)
639    else {
640        return request;
641    };
642
643    fn from_value(value: &Value, used: &mut std::collections::HashSet<Name>) {
644        match value {
645            Value::Variable(name) => {
646                used.insert(name.clone());
647            }
648            Value::List(items) => items.iter().for_each(|item| from_value(item, used)),
649            Value::Object(fields) => fields.values().for_each(|field| from_value(field, used)),
650            _ => {}
651        }
652    }
653
654    fn from_selection_set(set: &SelectionSet, used: &mut std::collections::HashSet<Name>) {
655        for selection in &set.items {
656            let (directives, arguments, nested) = match &selection.node {
657                Selection::Field(field) => (
658                    &field.node.directives,
659                    Some(&field.node.arguments),
660                    Some(&field.node.selection_set),
661                ),
662                Selection::FragmentSpread(spread) => (&spread.node.directives, None, None),
663                Selection::InlineFragment(fragment) => (
664                    &fragment.node.directives,
665                    None,
666                    Some(&fragment.node.selection_set),
667                ),
668            };
669            for directive in directives {
670                for (_, value) in &directive.node.arguments {
671                    from_value(&value.node, used);
672                }
673            }
674            for (_, value) in arguments.into_iter().flatten() {
675                from_value(&value.node, used);
676            }
677            if let Some(nested) = nested {
678                from_selection_set(&nested.node, used);
679            }
680        }
681    }
682
683    // Every variable named anywhere in the document, rather than per operation.
684    // Over-counting only keeps a declaration that would have been kept before,
685    // and a variable declared by one operation and used by another is exactly
686    // the case this is here to stop refusing.
687    let mut used = std::collections::HashSet::new();
688    for (_, operation) in doc.operations.iter() {
689        for directive in &operation.node.directives {
690            for (_, value) in &directive.node.arguments {
691                from_value(&value.node, &mut used);
692            }
693        }
694        from_selection_set(&operation.node.selection_set.node, &mut used);
695    }
696    for fragment in doc.fragments.values() {
697        from_selection_set(&fragment.node.selection_set.node, &mut used);
698    }
699
700    let mut edited = false;
701    {
702        let mut prune = |operation: &mut async_graphql::Positioned<
703            async_graphql::parser::types::OperationDefinition,
704        >| {
705            let before = operation.node.variable_definitions.len();
706            operation
707                .node
708                .variable_definitions
709                .retain(|definition| used.contains(&definition.node.name.node));
710            edited |= operation.node.variable_definitions.len() != before;
711        };
712        match &mut doc.operations {
713            DocumentOperations::Single(operation) => prune(operation),
714            DocumentOperations::Multiple(operations) => operations.values_mut().for_each(prune),
715        }
716    }
717
718    // A value written as a string where a number or a boolean is expected.
719    // `insert_test_types(objects: [{c1_smallint: "32767", c20_boolean:
720    // "true"}])` is a mutation Hasura performs: a column's value is read the
721    // way PostgreSQL reads a literal, which takes either spelling, while the
722    // schema still introspects as `Int`. So does `article(offset: "1")`.
723    //
724    // `limit` is the exception, and the corpus is explicit about it: `limit:
725    // "3"` is refused in the same breath that `offset: "1"` is answered. It is
726    // the one Int in the schema that is the engine's own rather than a
727    // column's, and it is the only place a string is not a number.
728    if let Some(schema) = schema {
729        let registry = schema.registry();
730        {
731            let mut coerce = |operation: &mut async_graphql::Positioned<
732                async_graphql::parser::types::OperationDefinition,
733            >| {
734                use async_graphql::parser::types::OperationType;
735                let root = match operation.node.ty {
736                    OperationType::Query => Some(registry.query_type.as_str()),
737                    OperationType::Mutation => registry.mutation_type.as_deref(),
738                    OperationType::Subscription => registry.subscription_type.as_deref(),
739                };
740                if let Some(root) = root {
741                    edited |= coerce_selection_set(
742                        registry,
743                        &mut operation.node.selection_set.node,
744                        root,
745                    );
746                }
747            };
748            match &mut doc.operations {
749                DocumentOperations::Single(operation) => coerce(operation),
750                DocumentOperations::Multiple(operations) => {
751                    operations.values_mut().for_each(coerce)
752                }
753            }
754        }
755        // A fragment names the type it is on, so it is walked on its own
756        // rather than from wherever it is spread -- which also means a cyclic
757        // spread cannot walk forever.
758        for fragment in doc.fragments.values_mut() {
759            let on = fragment.node.type_condition.node.on.node.to_string();
760            edited |= coerce_selection_set(registry, &mut fragment.node.selection_set.node, &on);
761        }
762    }
763
764    if edited {
765        request.set_parsed_query(doc);
766    }
767    request
768}
769
770/// Coerce the written values of one selection set, and of everything under it.
771///
772/// Type-directed, the same walk [`Usage`] makes: a field names its arguments,
773/// an argument names an input object, an input object names its fields, and at
774/// the leaves sits the type a written value has to be. Returns whether
775/// anything changed.
776fn coerce_selection_set(
777    registry: &async_graphql::registry::Registry,
778    set: &mut async_graphql::parser::types::SelectionSet,
779    type_name: &str,
780) -> bool {
781    use async_graphql::parser::types::Selection;
782    use async_graphql::registry::MetaTypeName;
783
784    let mut edited = false;
785    for selection in &mut set.items {
786        match &mut selection.node {
787            Selection::Field(field) => {
788                let meta = registry
789                    .types
790                    .get(type_name)
791                    .and_then(|ty| ty.field_by_name(field.node.name.node.as_str()));
792                for (name, value) in &mut field.node.arguments {
793                    // The engine's own Int, which is strict where a column's
794                    // is not.
795                    if name.node.as_str() == "limit" {
796                        continue;
797                    }
798                    let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str()))
799                    else {
800                        continue;
801                    };
802                    let ty = argument.ty.clone();
803                    edited |= coerce_value(registry, &mut value.node, &ty);
804                }
805                if let Some(meta) = meta {
806                    let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
807                    edited |=
808                        coerce_selection_set(registry, &mut field.node.selection_set.node, &inner);
809                }
810            }
811            Selection::InlineFragment(fragment) => {
812                let inner = fragment
813                    .node
814                    .type_condition
815                    .as_ref()
816                    .map(|condition| condition.node.on.node.to_string())
817                    .unwrap_or_else(|| type_name.to_string());
818                edited |=
819                    coerce_selection_set(registry, &mut fragment.node.selection_set.node, &inner);
820            }
821            Selection::FragmentSpread(_) => {}
822        }
823    }
824    edited
825}
826
827/// One written value, against the type of the place it was written in.
828fn coerce_value(
829    registry: &async_graphql::registry::Registry,
830    value: &mut async_graphql_value::Value,
831    expected: &str,
832) -> bool {
833    use async_graphql::registry::{MetaType, MetaTypeName};
834    use async_graphql_value::Value;
835
836    match value {
837        Value::List(items) => {
838            // A single value may be written where a list is expected, so the
839            // item type stands in for either.
840            let inner = match MetaTypeName::create(expected).unwrap_non_null() {
841                MetaTypeName::List(inner) => inner.to_string(),
842                _ => expected.to_string(),
843            };
844            let mut edited = false;
845            for item in items {
846                edited |= coerce_value(registry, item, &inner);
847            }
848            edited
849        }
850        Value::Object(fields) => {
851            let name = MetaTypeName::concrete_typename(expected);
852            let Some(MetaType::InputObject { input_fields, .. }) = registry.types.get(name) else {
853                return false;
854            };
855            let types: Vec<(async_graphql::Name, String)> = fields
856                .keys()
857                .filter_map(|key| {
858                    input_fields
859                        .get(key.as_str())
860                        .map(|field| (key.clone(), field.ty.clone()))
861                })
862                .collect();
863            let mut edited = false;
864            for (key, ty) in types {
865                if let Some(item) = fields.get_mut(&key) {
866                    edited |= coerce_value(registry, item, &ty);
867                }
868            }
869            edited
870        }
871        Value::String(text) => {
872            let Some(coerced) = as_written(text, MetaTypeName::concrete_typename(expected)) else {
873                return false;
874            };
875            *value = coerced;
876            true
877        }
878        _ => false,
879    }
880}
881
882/// A string read as the type it was written where, if it can be.
883///
884/// Only the three GraphQL scalars that are strict about it: everything else in
885/// this schema is a scalar of its own, which takes a string already.
886fn as_written(text: &str, expected: &str) -> Option<async_graphql_value::Value> {
887    use async_graphql_value::{Number, Value};
888    match expected {
889        "Int" => text.parse::<i64>().ok().map(|n| Value::Number(n.into())),
890        "Float" => text
891            .parse::<f64>()
892            .ok()
893            .and_then(Number::from_f64)
894            .map(Value::Number),
895        "Boolean" => match text {
896            "true" => Some(Value::Boolean(true)),
897            "false" => Some(Value::Boolean(false)),
898            _ => None,
899        },
900        _ => None,
901    }
902}
903
904#[cfg(test)]
905mod unused_variable_tests {
906    use super::*;
907
908    fn declarations(query: &str) -> Vec<String> {
909        let request = allow_unused_variables(async_graphql::Request::new(query));
910        // A request whose declarations were all used is handed on with nothing
911        // parsed, so what the executor sees is the source text.
912        let mut request = request;
913        let doc = request.parsed_query().expect("the query parses");
914        doc.operations
915            .iter()
916            .flat_map(|(_, operation)| operation.node.variable_definitions.iter())
917            .map(|definition| definition.node.name.node.to_string())
918            .collect()
919    }
920
921    #[test]
922    fn a_variable_nothing_names_is_dropped() {
923        assert_eq!(
924            declarations("query ($tags: jsonb) { article(where: {id: {_eq: 1}}) { id } }"),
925            Vec::<String>::new()
926        );
927    }
928
929    #[test]
930    fn a_variable_an_argument_names_is_kept() {
931        assert_eq!(
932            declarations("query ($id: Int) { article(where: {id: {_eq: $id}}) { id } }"),
933            vec!["id".to_string()]
934        );
935    }
936
937    #[test]
938    fn a_variable_named_inside_a_list_is_kept() {
939        assert_eq!(
940            declarations("query ($id: Int) { article(where: {_or: [{id: {_eq: $id}}]}) { id } }"),
941            vec!["id".to_string()]
942        );
943    }
944
945    #[test]
946    fn a_variable_only_a_fragment_names_is_kept() {
947        assert_eq!(
948            declarations(
949                "query ($n: Int) { author { ...rows } } \
950                 fragment rows on author { articles(limit: $n) { id } }"
951            ),
952            vec!["n".to_string()]
953        );
954    }
955
956    #[test]
957    fn a_document_that_does_not_parse_is_left_alone() {
958        let request = allow_unused_variables(async_graphql::Request::new("query ("));
959        assert_eq!(request.query, "query (");
960    }
961}
962
963/// Refuse a variable used where its declared type does not fit.
964///
965/// The specification's "All Variable Usages Are Allowed" rule: `query
966/// ($limit: String) { author(limit: $limit) }` is invalid, because `limit` is
967/// an `Int` and a `String` is not one. async-graphql carries exactly this rule
968/// and it does not fire -- verified against 7.0.17 with a static schema, a
969/// dynamic one, and a built-in directive, none of which report -- so the check
970/// is made here instead. It is the one place where being lax is worse than
971/// being wrong: the client is answered with data when what it wrote cannot
972/// mean what it thinks, and it finds out from the rows.
973///
974/// The messages are Hasura's, since a client that shows them to a developer is
975/// showing text it already ships.
976fn variable_errors(
977    doc: &async_graphql::parser::types::ExecutableDocument,
978    registry: &async_graphql::registry::Registry,
979    variables: &async_graphql::Variables,
980) -> Vec<ServerError> {
981    use async_graphql::parser::types::{DocumentOperations, OperationType};
982
983    let mut errors: Vec<ServerError> = Vec::new();
984
985    let operations: Vec<
986        &async_graphql::Positioned<async_graphql::parser::types::OperationDefinition>,
987    > = match &doc.operations {
988        DocumentOperations::Single(operation) => vec![operation],
989        DocumentOperations::Multiple(operations) => operations.values().collect(),
990    };
991
992    for operation in operations {
993        let root = match operation.node.ty {
994            OperationType::Query => Some(registry.query_type.as_str()),
995            OperationType::Mutation => registry.mutation_type.as_deref(),
996            OperationType::Subscription => registry.subscription_type.as_deref(),
997        };
998        let Some(root) = root else {
999            // A mutation sent to a schema that has no mutation root. Hasura
1000            // answers this rather than "the field you named is not there",
1001            // because there is no type for it to have not been in.
1002            if operation.node.ty == OperationType::Mutation {
1003                errors.push(coded("no mutations exist".to_string(), operation.pos, "$"));
1004            }
1005            continue;
1006        };
1007
1008        // What each variable was declared as, and what a null in it means.
1009        let mut declared: HashMap<&str, String> = HashMap::new();
1010        for definition in &operation.node.variable_definitions {
1011            let name = definition.node.name.node.as_str();
1012            let written = definition.node.var_type.node.to_string();
1013            // A nullable declaration with a default behaves as a non-null
1014            // one: the default stands in wherever the variable is left out.
1015            // A default *of* null does not -- `$author: author_insert_input =
1016            // null` still cannot be used where a non-null is expected, because
1017            // what it stands in with is a null.
1018            let defaulted = definition
1019                .node
1020                .default_value
1021                .as_ref()
1022                .is_some_and(|value| !matches!(value.node, async_graphql::Value::Null));
1023            let effective = match (definition.node.var_type.node.nullable, defaulted) {
1024                (true, true) => format!("{}!", written),
1025                _ => written.clone(),
1026            };
1027            declared.insert(name, effective);
1028
1029            // An explicit null for a non-null variable. The default does not
1030            // save it: a default stands in for a variable that was not given,
1031            // not for one that was given as null.
1032            let given = variables.get(&async_graphql::Name::new(name));
1033            if !definition.node.var_type.node.nullable
1034                && matches!(given, Some(async_graphql::Value::Null))
1035            {
1036                errors.push(coded(
1037                    format!("null value found for non-nullable type: \"{}\"", written),
1038                    definition.pos,
1039                    "$",
1040                ));
1041            }
1042        }
1043        // Walked even with nothing declared: the second thing this looks for
1044        // is a null written straight into a comparison, which needs no
1045        // variable to be written.
1046        let mut scope = Usage {
1047            registry,
1048            fragments: &doc.fragments,
1049            declared: &declared,
1050            variables,
1051            errors: &mut errors,
1052            spreading: Vec::new(),
1053            expansions: 0,
1054        };
1055        scope.selection_set(&operation.node.selection_set.node, root, "$");
1056    }
1057
1058    errors
1059}
1060
1061/// One walk of a document, checking every variable against where it is used.
1062struct Usage<'a> {
1063    registry: &'a async_graphql::registry::Registry,
1064    fragments: &'a std::collections::HashMap<
1065        async_graphql::Name,
1066        async_graphql::Positioned<async_graphql::parser::types::FragmentDefinition>,
1067    >,
1068    declared: &'a HashMap<&'a str, String>,
1069    /// What the request gave for each variable, which is the only way to tell
1070    /// a variable standing for a null from one standing for a value.
1071    variables: &'a async_graphql::Variables,
1072    errors: &'a mut Vec<ServerError>,
1073    /// Fragments already walked, so a cycle terminates. async-graphql refuses
1074    /// cyclic fragments too, but this runs first.
1075    /// The fragments currently being spread, outermost first.
1076    ///
1077    /// A stack rather than a set, because what a cycle is named after is the
1078    /// run of fragments from the first occurrence to the repeat -- and because
1079    /// a fragment spread twice in sibling positions is not a cycle and its
1080    /// contents should be checked at both.
1081    spreading: Vec<String>,
1082    /// How many spreads have been expanded. A document whose fragments each
1083    /// spread the next twice doubles the work per level without ever
1084    /// repeating a name, so the stack alone does not bound this.
1085    expansions: usize,
1086}
1087
1088impl Usage<'_> {
1089    /// `path` is where this selection set is, in Hasura's spelling: `$` at the
1090    /// root, and `$.selectionSet.author` for the set inside that field. A
1091    /// fragment carries its spread site's path, since that is where the fields
1092    /// it contributes actually are.
1093    fn selection_set(
1094        &mut self,
1095        set: &async_graphql::parser::types::SelectionSet,
1096        type_name: &str,
1097        path: &str,
1098    ) {
1099        use async_graphql::parser::types::Selection;
1100        use async_graphql::registry::MetaTypeName;
1101
1102        for selection in &set.items {
1103            match &selection.node {
1104                Selection::Field(field) => {
1105                    let named = field.node.name.node.as_str();
1106                    let parent = self.registry.types.get(type_name);
1107                    let meta = parent.and_then(|ty| ty.field_by_name(named));
1108                    let here = format!("{}.selectionSet.{}", path, field.node.name.node);
1109                    // A field the type does not have. async-graphql reports
1110                    // this too, and in its own words -- `Unknown field "x" on
1111                    // type "y". Did you mean ...` -- so saying it here is what
1112                    // makes the answer Hasura's: this walk runs first and a
1113                    // document with an error in it never reaches validation.
1114                    //
1115                    // Only where the parent type is one this registry knows.
1116                    // Not knowing it is not evidence that the field is absent.
1117                    if meta.is_none() && parent.is_some() && !named.starts_with("__") {
1118                        self.errors.push(coded(
1119                            format!("field '{}' not found in type: '{}'", named, type_name),
1120                            field.pos,
1121                            &here,
1122                        ));
1123                        continue;
1124                    }
1125                    for (name, value) in &field.node.arguments {
1126                        let Some(argument) =
1127                            meta.and_then(|meta| meta.args.get(name.node.as_str()))
1128                        else {
1129                            // An argument the field does not take. Reported
1130                            // against the field rather than against the
1131                            // argument -- there is no such argument to point
1132                            // at, which is the complaint.
1133                            if meta.is_some() {
1134                                self.errors.push(coded(
1135                                    format!("'{}' has no argument named '{}'", named, name.node),
1136                                    name.pos,
1137                                    &here,
1138                                ));
1139                            }
1140                            continue;
1141                        };
1142                        // A location with a default of its own accepts a
1143                        // nullable variable where it says non-null, since
1144                        // leaving the variable out is then the same as not
1145                        // writing the argument.
1146                        let expected = relax(&argument.ty, argument.default_value.is_some());
1147                        let at = format!("{}.args.{}", here, name.node);
1148                        self.rows_asked_for(
1149                            name.node.as_str(),
1150                            &value.node,
1151                            &expected,
1152                            value.pos,
1153                            &at,
1154                        );
1155                        self.object_expected(&value.node, &expected, value.pos, &at);
1156                        self.value(&value.node, &expected, value.pos, &at);
1157                    }
1158                    self.one_operator_per_column(&field.node.arguments, &here);
1159                    self.directives(&field.node.directives, &here);
1160                    if let Some(meta) = meta {
1161                        let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
1162                        self.selection_set(&field.node.selection_set.node, &inner, &here);
1163                    }
1164                }
1165                Selection::InlineFragment(fragment) => {
1166                    let inner = fragment
1167                        .node
1168                        .type_condition
1169                        .as_ref()
1170                        .map(|condition| condition.node.on.node.to_string())
1171                        .unwrap_or_else(|| type_name.to_string());
1172                    self.directives(&fragment.node.directives, path);
1173                    self.selection_set(&fragment.node.selection_set.node, &inner, path);
1174                }
1175                Selection::FragmentSpread(spread) => {
1176                    let name = spread.node.fragment_name.node.to_string();
1177                    self.directives(&spread.node.directives, path);
1178                    // A fragment that spreads its way back to itself. Nothing
1179                    // can be checked inside it -- there is no inside, the
1180                    // document does not describe a finite selection -- so this
1181                    // is reported and the walk stops here. async-graphql finds
1182                    // it too, as a recursion depth, which says that something
1183                    // went too deep rather than what it was.
1184                    if let Some(began) = self.spreading.iter().position(|held| held == &name) {
1185                        let cycle: Vec<&str> =
1186                            self.spreading[began..].iter().map(String::as_str).collect();
1187                        self.errors.push(coded(
1188                            format!("the fragment definition(s) {} form a cycle", listed(&cycle)),
1189                            spread.pos,
1190                            // The selection set the offending spread is in,
1191                            // rather than the spread itself: by then the
1192                            // spread has no selection set of its own to name.
1193                            &format!("{}.selectionSet", path),
1194                        ));
1195                        continue;
1196                    }
1197                    self.expansions += 1;
1198                    if self.expansions > SPREAD_BUDGET {
1199                        continue;
1200                    }
1201                    if let Some(fragment) = self.fragments.get(&async_graphql::Name::new(&name)) {
1202                        let on = fragment.node.type_condition.node.on.node.to_string();
1203                        // A spread is a step in Hasura's path, named after the
1204                        // fragment: `...selectionSet.author.selectionSet.
1205                        // authorFragment.selectionSet.articles`.
1206                        let here = format!("{}.selectionSet.{}", path, name);
1207                        self.spreading.push(name);
1208                        self.selection_set(&fragment.node.selection_set.node, &on, &here);
1209                        self.spreading.pop();
1210                    }
1211                }
1212            }
1213        }
1214    }
1215
1216    fn directives(
1217        &mut self,
1218        directives: &[async_graphql::Positioned<async_graphql::parser::types::Directive>],
1219        path: &str,
1220    ) {
1221        for directive in directives {
1222            let meta = self
1223                .registry
1224                .directives
1225                .get(directive.node.name.node.as_str());
1226            for (name, value) in &directive.node.arguments {
1227                let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str())) else {
1228                    continue;
1229                };
1230                let expected = relax(&argument.ty, argument.default_value.is_some());
1231                let at = format!("{}.args.{}", path, name.node);
1232                self.value(&value.node, &expected, value.pos, &at);
1233            }
1234        }
1235    }
1236
1237    /// Whether a written value is a null, however it was written.
1238    ///
1239    /// A literal one, or a variable the request gave a null for. A variable
1240    /// the request left out is not: an absent variable makes the field itself
1241    /// absent, which is a query with no such comparison rather than one
1242    /// comparing against nothing.
1243    fn stands_for_null(&self, value: &async_graphql_value::Value) -> bool {
1244        use async_graphql_value::Value;
1245        match value {
1246            Value::Null => true,
1247            Value::Variable(name) => matches!(
1248                self.variables.get(&async_graphql::Name::new(name)),
1249                Some(async_graphql::Value::Null)
1250            ),
1251            _ => false,
1252        }
1253    }
1254
1255    /// One written value, against the type of the place it was written in.
1256    fn value(
1257        &mut self,
1258        value: &async_graphql_value::Value,
1259        expected: &str,
1260        pos: async_graphql::Pos,
1261        path: &str,
1262    ) {
1263        use async_graphql::registry::{MetaType, MetaTypeName};
1264        use async_graphql_value::Value;
1265
1266        match value {
1267            Value::Variable(name) => {
1268                let Some(declared) = self.declared.get(name.as_str()) else {
1269                    // Undefined, which async-graphql reports itself.
1270                    return;
1271                };
1272                if !MetaTypeName::create(expected).is_subtype(&MetaTypeName::create(declared)) {
1273                    self.errors.push(coded(
1274                        format!(
1275                            "variable '{}' is declared as '{}', but used where '{}' is expected",
1276                            name, declared, expected
1277                        ),
1278                        pos,
1279                        path,
1280                    ));
1281                    return;
1282                }
1283                // A variable of the right type may still carry a value the
1284                // type does not have: `$color: colors_enum` given
1285                // `"not_a_real_color"` is declared correctly and is still not
1286                // one of the colours.
1287                if let Some(given) = self.variables.get(&async_graphql::Name::new(name)) {
1288                    let given: async_graphql_value::Value = given.clone().into();
1289                    self.enumerated(&given, expected, pos, path, true);
1290                    self.label_path(&given, expected, pos, path);
1291                    self.raster_hex(&given, expected, pos, path);
1292                }
1293            }
1294            Value::Enum(_) | Value::String(_) => {
1295                self.enumerated(value, expected, pos, path, false);
1296                self.label_path(value, expected, pos, path);
1297                self.raster_hex(value, expected, pos, path);
1298            }
1299            Value::Number(_) => self.integral(value, expected, pos, path),
1300            Value::List(items) => {
1301                // A list may be written where one value is expected, in which
1302                // case each item is checked against that same type -- which is
1303                // what list input coercion means.
1304                let inner = match MetaTypeName::create(expected).unwrap_non_null() {
1305                    MetaTypeName::List(inner) => inner.to_string(),
1306                    _ => expected.to_string(),
1307                };
1308                for (index, item) in items.iter().enumerate() {
1309                    self.value(item, &inner, pos, &format!("{}[{}]", path, index));
1310                }
1311            }
1312            Value::Object(fields) => {
1313                let name = MetaTypeName::concrete_typename(expected);
1314                let Some(MetaType::InputObject { input_fields, .. }) =
1315                    self.registry.types.get(name)
1316                else {
1317                    return;
1318                };
1319                // A comparison against null. `where: {id: {_eq: null}}` reads
1320                // as `id = NULL`, which is never true -- so a client that
1321                // wrote it meant something the query cannot mean, and gets
1322                // every row or no rows depending on which. Hasura refuses it,
1323                // and the operand is a nullable `Int` in the schema either
1324                // server publishes, so this is the only place it can be
1325                // refused.
1326                let comparison = name.ends_with("_comparison_exp");
1327                // One object written where a list was expected is that list's
1328                // first item -- input coercion says so, and the path says so
1329                // too: `insert_author(objects: {location: $x})` is refused at
1330                // `args.objects[0].location`, not at `args.objects.location`.
1331                let base = match MetaTypeName::create(expected).unwrap_non_null() {
1332                    MetaTypeName::List(_) => format!("{}[0]", path),
1333                    _ => path.to_string(),
1334                };
1335                for (key, item) in fields {
1336                    let at = format!("{}.{}", base, key.as_str());
1337                    let Some(field) = input_fields.get(key.as_str()) else {
1338                        // A key the input object does not have, in the same
1339                        // words a missing selection field gets: Hasura writes
1340                        // `field 'action' not found in type:
1341                        // 'article_on_conflict'` for both.
1342                        self.errors.push(coded(
1343                            format!("field '{}' not found in type: '{}'", key.as_str(), name),
1344                            pos,
1345                            &at,
1346                        ));
1347                        continue;
1348                    };
1349                    if comparison && self.stands_for_null(item) {
1350                        self.errors.push(coded(
1351                            format!(
1352                                "unexpected null value for type '{}'",
1353                                MetaTypeName::concrete_typename(&field.ty)
1354                            ),
1355                            pos,
1356                            &at,
1357                        ));
1358                        continue;
1359                    }
1360                    let ty = relax(&field.ty, field.default_value.is_some());
1361                    self.value(item, &ty, pos, &at);
1362                }
1363            }
1364            _ => {}
1365        }
1366    }
1367}
1368
1369impl Usage<'_> {
1370    /// `null` written where an object is required.
1371    ///
1372    /// `update_author(where: null)` says nothing about which rows, and `where`
1373    /// is non-null in the schema either server publishes. async-graphql calls
1374    /// it `Invalid value for argument "where", expected type
1375    /// "author_bool_exp"`, which names the argument and not what was wrong
1376    /// with it; Hasura names the type it wanted and the thing it got.
1377    ///
1378    /// Only a *required* input object is refused here. A nullable one written
1379    /// as `null` is the same as leaving it out, which is a request this server
1380    /// has no business turning into an error.
1381    fn object_expected(
1382        &mut self,
1383        value: &async_graphql_value::Value,
1384        expected: &str,
1385        pos: async_graphql::Pos,
1386        path: &str,
1387    ) {
1388        use async_graphql::registry::{MetaType, MetaTypeName};
1389
1390        if !matches!(MetaTypeName::create(expected), MetaTypeName::NonNull(_)) {
1391            return;
1392        }
1393        if !self.stands_for_null(value) {
1394            return;
1395        }
1396        let name = MetaTypeName::concrete_typename(expected);
1397        if !matches!(
1398            self.registry.types.get(name),
1399            Some(MetaType::InputObject { .. })
1400        ) {
1401            return;
1402        }
1403        self.errors.push(coded(
1404            format!("expected an object for type '{}', but found null", name),
1405            pos,
1406            path,
1407        ));
1408    }
1409
1410    /// A column named by two of an update's operators.
1411    ///
1412    /// `_set: {id: 1}` beside `_inc: {id: 2}` asks for one column to be two
1413    /// things in one statement, and the order they would be applied in is not
1414    /// something the request says. PostgreSQL refuses the statement -- a
1415    /// column may appear once in a `SET` list -- and by then the answer names
1416    /// the column alone. Hasura names every column written twice, and reports
1417    /// it against the arguments rather than against any one of them, since no
1418    /// single operator is the wrong one.
1419    ///
1420    /// The operators are Hasura's own seven. A duplicate *within* one of them
1421    /// is not this: an object cannot have a key twice.
1422    fn one_operator_per_column(
1423        &mut self,
1424        arguments: &[(
1425            async_graphql::Positioned<async_graphql::Name>,
1426            async_graphql::Positioned<async_graphql_value::Value>,
1427        )],
1428        path: &str,
1429    ) {
1430        use async_graphql_value::Value;
1431
1432        const OPERATORS: [&str; 7] = [
1433            "_set",
1434            "_inc",
1435            "_append",
1436            "_prepend",
1437            "_delete_key",
1438            "_delete_elem",
1439            "_delete_at_path",
1440        ];
1441
1442        let mut seen: Vec<&str> = Vec::new();
1443        let mut twice: Vec<&str> = Vec::new();
1444        for (name, value) in arguments {
1445            if !OPERATORS.contains(&name.node.as_str()) {
1446                continue;
1447            }
1448            let Value::Object(columns) = &value.node else {
1449                continue;
1450            };
1451            for column in columns.keys() {
1452                let column = column.as_str();
1453                match seen.contains(&column) {
1454                    true if !twice.contains(&column) => twice.push(column),
1455                    true => {}
1456                    false => seen.push(column),
1457                }
1458            }
1459        }
1460        if twice.is_empty() {
1461            return;
1462        }
1463        // Haskell's `show` for a list of strings, which is what the message is
1464        // rendered by: single quotes are what Hasura's own output has.
1465        let listed = twice
1466            .iter()
1467            .map(|column| format!("'{}'", column))
1468            .collect::<Vec<String>>()
1469            .join(", ");
1470        self.errors.push(coded(
1471            format!("Column found in multiple operators: [{}].", listed),
1472            arguments
1473                .first()
1474                .map(|(name, _)| name.pos)
1475                .unwrap_or_default(),
1476            &format!("{}.args", path),
1477        ));
1478    }
1479
1480    /// A number written where an `Int` is expected, against what an `Int` is.
1481    ///
1482    /// GraphQL's `Int` is a signed 32-bit integer, and `2147483648` is not
1483    /// one. PostgreSQL says `integer out of range` when the statement runs;
1484    /// Hasura says so while reading the request, and names the value it could
1485    /// not read. `int8` columns are a `bigint` scalar rather than an `Int`, so
1486    /// nothing that legitimately holds a large number is caught here.
1487    fn integral(
1488        &mut self,
1489        value: &async_graphql_value::Value,
1490        expected: &str,
1491        pos: async_graphql::Pos,
1492        path: &str,
1493    ) {
1494        use async_graphql::registry::MetaTypeName;
1495        use async_graphql_value::Value;
1496
1497        if MetaTypeName::concrete_typename(expected) != "Int" {
1498            return;
1499        }
1500        let Value::Number(number) = value else {
1501            return;
1502        };
1503        if number.as_i64().is_some_and(|n| i32::try_from(n).is_ok()) {
1504            return;
1505        }
1506        let Some(shown) = number.as_f64().map(shown_as_haskell_would) else {
1507            return;
1508        };
1509        self.errors.push(coded_as(
1510            "parse-failed",
1511            format!(
1512                "The value {} lies outside the bounds or is not an integer. \
1513                 Maybe it is a float, or is there integer overflow?",
1514                shown
1515            ),
1516            pos,
1517            path,
1518        ));
1519    }
1520
1521    /// How many rows a query asked for, which `Int` alone does not say.
1522    ///
1523    /// `limit` is typed `Int` in the schema either server publishes and is not
1524    /// any `Int`: a page of -1 rows is not a page, and Hasura refuses it
1525    /// before the query is built rather than letting `LIMIT must not be
1526    /// negative` come back from the database. A string is refused there too,
1527    /// even one that reads as a number.
1528    ///
1529    /// `offset` is *not* the same rule, and that asymmetry is Hasura's rather
1530    /// than an omission here: its own corpus sends `offset: "1"` and expects
1531    /// rows back, and sends `offset: -1` and expects PostgreSQL's own
1532    /// `OFFSET must not be negative`. So this is about the argument named
1533    /// `limit` and nothing else.
1534    fn rows_asked_for(
1535        &mut self,
1536        argument: &str,
1537        value: &async_graphql_value::Value,
1538        expected: &str,
1539        pos: async_graphql::Pos,
1540        path: &str,
1541    ) {
1542        use async_graphql::registry::MetaTypeName;
1543        use async_graphql_value::Value;
1544
1545        if argument != "limit" || MetaTypeName::concrete_typename(expected) != "Int" {
1546            return;
1547        }
1548        // Through a variable, what matters is the value it stands for.
1549        let value = match value {
1550            Value::Variable(name) => {
1551                match self.variables.get(&async_graphql::Name::new(name.as_str())) {
1552                    Some(given) => std::borrow::Cow::Owned(given.clone().into()),
1553                    None => return,
1554                }
1555            }
1556            other => std::borrow::Cow::Borrowed(other),
1557        };
1558
1559        let found = match value.as_ref() {
1560            Value::String(_) => "a string",
1561            Value::Number(number) => match number.as_i64() {
1562                // A number that is not an `Int` at all is left to the rule
1563                // about `Int`: that is the more specific complaint, and both
1564                // rules firing would answer one mistake twice.
1565                None => return,
1566                Some(written) if i32::try_from(written).is_err() => return,
1567                // What is left is a number of rows, and a negative one is not.
1568                Some(written) if written >= 0 => return,
1569                Some(_) => "an integer",
1570            },
1571            _ => return,
1572        };
1573        self.errors.push(coded(
1574            format!(
1575                "expected a non-negative 32-bit integer for type 'Int', but found {}",
1576                found
1577            ),
1578            pos,
1579            path,
1580        ));
1581    }
1582
1583    /// A value written where a `raster` is expected, as hexadecimal.
1584    ///
1585    /// A raster travels as the hex of its well-known binary, and PostGIS
1586    /// answers `rt_raster_from_wkb: wkb size (14) < min size (61)` for
1587    /// something that is not one -- a complaint about the bytes it managed to
1588    /// decode rather than about the text it was given. Hasura reads the text
1589    /// first and says it is not hexadecimal.
1590    ///
1591    /// Only what is certainly not hexadecimal is refused: a character outside
1592    /// `0-9A-Fa-f`, or an odd number of them, since a byte takes two. A
1593    /// well-formed hex string that is too short to be a raster still goes to
1594    /// PostGIS and comes back in PostGIS's words, which is what happens today
1595    /// and is not something the corpus has an opinion about.
1596    fn raster_hex(
1597        &mut self,
1598        value: &async_graphql_value::Value,
1599        expected: &str,
1600        pos: async_graphql::Pos,
1601        path: &str,
1602    ) {
1603        use async_graphql::registry::MetaTypeName;
1604        use async_graphql_value::Value;
1605
1606        if MetaTypeName::concrete_typename(expected) != "raster" {
1607            return;
1608        }
1609        let Value::String(written) = value else {
1610            return;
1611        };
1612        if written.len() % 2 == 0 && written.chars().all(|c| c.is_ascii_hexdigit()) {
1613            return;
1614        }
1615        self.errors.push(coded_as(
1616            "parse-failed",
1617            "invalid hexadecimal representation of raster well known binary format".to_string(),
1618            pos,
1619            path,
1620        ));
1621    }
1622
1623    /// A value written where an `ltree` is expected, as a label path.
1624    ///
1625    /// PostgreSQL refuses `Tree.Collections.` with `ltree syntax error` and
1626    /// nothing else; Hasura reads the path before sending it and says what a
1627    /// path is. Reading it here is also what puts the refusal at
1628    /// `...where.path._ancestor` rather than at the request as a whole.
1629    ///
1630    /// Only an *empty label* is refused, and the narrowness is the point.
1631    /// PostgreSQL's idea of a label character comes from the database's
1632    /// locale -- `a-b`, `a_b`, `1.2` and `Ünï` are all paths on the image the
1633    /// harness runs, and in C locale the last of those is not. Refusing a
1634    /// character this server merely doubts would turn a working query into an
1635    /// error, so what is refused here is the one thing no locale accepts: a
1636    /// label with nothing in it. Everything else is still PostgreSQL's to
1637    /// judge, in PostgreSQL's words.
1638    fn label_path(
1639        &mut self,
1640        value: &async_graphql_value::Value,
1641        expected: &str,
1642        pos: async_graphql::Pos,
1643        path: &str,
1644    ) {
1645        use async_graphql::registry::MetaTypeName;
1646        use async_graphql_value::Value;
1647
1648        if MetaTypeName::concrete_typename(expected) != "ltree" {
1649            return;
1650        }
1651        let Value::String(written) = value else {
1652            return;
1653        };
1654        // The empty string is the empty path, which is a path: "zero or more
1655        // labels" is what the message this raises says, and PostgreSQL agrees.
1656        if written.is_empty() || written.split('.').all(|label| !label.is_empty()) {
1657            return;
1658        }
1659        self.errors.push(coded_as(
1660            "parse-failed",
1661            "Expecting label path: a sequence of zero or more labels separated by \
1662             dots, for example L1.L2.L3"
1663                .to_string(),
1664            pos,
1665            path,
1666        ));
1667    }
1668
1669    /// A value written where an enum is expected, against that enum's members.
1670    ///
1671    /// async-graphql reports this too, in its own words and against the
1672    /// argument rather than against the place inside it. Hasura lists what it
1673    /// would have accepted, which is the half a client can act on.
1674    fn enumerated(
1675        &mut self,
1676        value: &async_graphql_value::Value,
1677        expected: &str,
1678        pos: async_graphql::Pos,
1679        path: &str,
1680        // Whether the value arrived through a variable. JSON has no enum, so a
1681        // variable carries one as a string and the question is which member it
1682        // names. Written into the document, a string is not an enum value at
1683        // all -- `_eq: "red"` is a mistake about the language rather than
1684        // about the colours, and Hasura says which mistake it was.
1685        from_variable: bool,
1686    ) {
1687        use async_graphql::registry::{MetaType, MetaTypeName};
1688        use async_graphql_value::Value;
1689
1690        let name = MetaTypeName::concrete_typename(expected);
1691        let Some(MetaType::Enum { enum_values, .. }) = self.registry.types.get(name) else {
1692            return;
1693        };
1694        let written = match value {
1695            Value::Enum(written) => written.as_str(),
1696            Value::String(written) if from_variable => written.as_str(),
1697            Value::String(_) => {
1698                self.errors.push(coded(
1699                    format!(
1700                        "expected an enum value for type '{}', but found a string",
1701                        name
1702                    ),
1703                    pos,
1704                    path,
1705                ));
1706                return;
1707            }
1708            _ => return,
1709        };
1710        if enum_values.contains_key(written) {
1711            return;
1712        }
1713        // Alphabetical, which is the order Hasura lists them in and not
1714        // necessarily the order they were declared.
1715        let mut allowed: Vec<&str> = enum_values.keys().map(String::as_str).collect();
1716        allowed.sort_unstable();
1717        let listed = allowed
1718            .iter()
1719            .map(|value| format!("'{}'", value))
1720            .collect::<Vec<_>>()
1721            .join(", ");
1722        self.errors.push(coded(
1723            format!(
1724                "expected one of the values [{}] for type '{}', but found '{}'",
1725                listed, name, written
1726            ),
1727            pos,
1728            path,
1729        ));
1730    }
1731}
1732
1733/// How many fragment spreads one document may expand before the walk gives up.
1734///
1735/// Not a limit anyone should reach: it is here because fragments that spread
1736/// other fragments twice each double the work per level without any name
1737/// repeating, so the cycle check alone does not bound the walk. Reaching it
1738/// stops the checking rather than refusing the document -- async-graphql
1739/// validates it afterwards either way.
1740const SPREAD_BUDGET: usize = 1000;
1741
1742/// Names in a sentence, the way Hasura writes them: `a`, `a and b`,
1743/// `a, b and c`.
1744fn listed(names: &[&str]) -> String {
1745    match names {
1746        [] => String::new(),
1747        [one] => one.to_string(),
1748        [rest @ .., last] => format!("{} and {}", rest.join(", "), last),
1749    }
1750}
1751
1752/// A number as Haskell's `show` renders a `Double`, which is how the value in
1753/// Hasura's message is spelled.
1754///
1755/// `2147483648` comes back as `2.147483648e9` rather than as its digits, and
1756/// the message quotes it, so matching the message means matching the
1757/// rendering. Haskell writes a `Double` in fixed notation while its magnitude
1758/// is in `[0.1, 10^7)` and in scientific notation otherwise, always with a
1759/// decimal point in the mantissa. Rust's own two formats are the same two
1760/// spellings and both give the shortest digits that read back exactly, so the
1761/// choice between them is the whole of what this adds.
1762fn shown_as_haskell_would(value: f64) -> String {
1763    if !value.is_finite() {
1764        return value.to_string();
1765    }
1766    let magnitude = value.abs();
1767    let with_a_point = |rendered: String| match rendered.contains('.') {
1768        true => rendered,
1769        false => format!("{}.0", rendered),
1770    };
1771    if magnitude != 0.0 && (magnitude < 0.1 || magnitude >= 1e7) {
1772        let rendered = format!("{:e}", value);
1773        let (mantissa, exponent) = match rendered.split_once('e') {
1774            Some(halves) => halves,
1775            None => return with_a_point(rendered),
1776        };
1777        return format!("{}e{}", with_a_point(mantissa.to_string()), exponent);
1778    }
1779    with_a_point(format!("{}", value))
1780}
1781
1782/// A location type as it accepts a variable, given whether it has a default.
1783///
1784/// A non-null location with a default takes a nullable variable: leaving that
1785/// variable out is the same as not writing the argument, and the default then
1786/// stands in. Without a default it does not.
1787fn relax(ty: &str, has_default: bool) -> String {
1788    match has_default {
1789        true => ty.strip_suffix('!').unwrap_or(ty).to_string(),
1790        false => ty.to_string(),
1791    }
1792}
1793
1794/// A validation failure, coded as one, at the place it was found.
1795///
1796/// The path is carried on the error rather than derived from it: this walk
1797/// happens before execution, so there is no response path to read, and what
1798/// has to be named is a place inside the request -- an argument, a key of an
1799/// input object, an item of a list.
1800fn coded(message: String, pos: async_graphql::Pos, path: &str) -> ServerError {
1801    coded_as("validation-failed", message, pos, path)
1802}
1803
1804/// The same, for a rule whose failure Hasura codes as something else.
1805fn coded_as(
1806    code: &'static str,
1807    message: String,
1808    pos: async_graphql::Pos,
1809    path: &str,
1810) -> ServerError {
1811    let mut error = ServerError::new(message, Some(pos));
1812    let mut extensions = async_graphql::ErrorExtensionValues::default();
1813    extensions.set("code", code);
1814    extensions.set("path", path);
1815    error.extensions = Some(extensions);
1816    error
1817}
1818
1819#[cfg(test)]
1820mod variable_position_tests {
1821    use super::*;
1822    use async_graphql::dynamic::*;
1823
1824    /// A schema with the shapes the rule has to reason about: a scalar
1825    /// argument, a nested input object, a list, a non-null location, and a
1826    /// non-null location that has a default.
1827    fn schema() -> async_graphql::dynamic::Schema {
1828        let filter = InputObject::new("author_bool_exp")
1829            .field(InputValue::new("id", TypeRef::named("Int")))
1830            .field(InputValue::new(
1831                "name",
1832                TypeRef::named("String_comparison_exp"),
1833            ));
1834        let comparison = InputObject::new("String_comparison_exp")
1835            .field(InputValue::new("_eq", TypeRef::named("String")))
1836            .field(InputValue::new("_in", TypeRef::named_nn_list("String")));
1837        // A tree, for the rules that are about a scalar's own shape rather
1838        // than about the type it is written under.
1839        let ltree = InputObject::new("ltree_comparison_exp")
1840            .field(InputValue::new("_ancestor", TypeRef::named("ltree")))
1841            .field(InputValue::new(
1842                "_ancestor_any",
1843                TypeRef::named_list("ltree"),
1844            ));
1845        let kind = Enum::new("kind_enum").item("leaf").item("branch");
1846        let kind_comparison = InputObject::new("kind_enum_comparison_exp")
1847            .field(InputValue::new("_eq", TypeRef::named("kind_enum")));
1848        let raster_comparison = InputObject::new("raster_comparison_exp").field(InputValue::new(
1849            "_st_intersects_rast",
1850            TypeRef::named("raster"),
1851        ));
1852        let tree_filter = InputObject::new("tree_bool_exp")
1853            .field(InputValue::new(
1854                "rast",
1855                TypeRef::named("raster_comparison_exp"),
1856            ))
1857            .field(InputValue::new(
1858                "path",
1859                TypeRef::named("ltree_comparison_exp"),
1860            ))
1861            .field(InputValue::new(
1862                "kind",
1863                TypeRef::named("kind_enum_comparison_exp"),
1864            ));
1865        let insert = InputObject::new("author_insert_input")
1866            .field(InputValue::new("name", TypeRef::named("String")))
1867            .field(InputValue::new("count", TypeRef::named("Int")));
1868        let query = Object::new("query_root")
1869            .field(
1870                Field::new("author", TypeRef::named_nn_list_nn("author"), |_| {
1871                    FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1872                })
1873                .argument(InputValue::new("limit", TypeRef::named("Int")))
1874                .argument(InputValue::new("where", TypeRef::named("author_bool_exp"))),
1875            )
1876            .field(
1877                Field::new("tree", TypeRef::named_nn_list_nn("tree"), |_| {
1878                    FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1879                })
1880                .argument(InputValue::new("where", TypeRef::named("tree_bool_exp"))),
1881            );
1882        let mutation = Object::new("mutation_root")
1883            .field(
1884                Field::new("insert_author_one", TypeRef::named("author"), |_| {
1885                    FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1886                })
1887                .argument(InputValue::new(
1888                    "object",
1889                    TypeRef::named_nn("author_insert_input"),
1890                )),
1891            )
1892            .field(
1893                Field::new("insert_author", TypeRef::named("author"), |_| {
1894                    FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1895                })
1896                .argument(InputValue::new(
1897                    "objects",
1898                    TypeRef::named_nn_list_nn("author_insert_input"),
1899                ))
1900                .argument(
1901                    InputValue::new("update_columns", TypeRef::named_nn_list_nn("String"))
1902                        .default_value(async_graphql::Value::List(Vec::new())),
1903                ),
1904            )
1905            .field(
1906                Field::new(
1907                    "update_author",
1908                    TypeRef::named("author_mutation_response"),
1909                    |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1910                )
1911                .argument(InputValue::new(
1912                    "where",
1913                    TypeRef::named_nn("author_bool_exp"),
1914                ))
1915                .argument(InputValue::new("_set", TypeRef::named("author_set_input")))
1916                .argument(InputValue::new("_inc", TypeRef::named("author_inc_input"))),
1917            );
1918        let author = Object::new("author").field(Field::new("id", TypeRef::named("Int"), |_| {
1919            FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1920        }));
1921        let tree = Object::new("tree")
1922            .field(Field::new("id", TypeRef::named("Int"), |_| {
1923                FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1924            }))
1925            .field(Field::new("path", TypeRef::named("ltree"), |_| {
1926                FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1927            }));
1928        // What an update writes, and the one required `where` in the fixture.
1929        let set = InputObject::new("author_set_input")
1930            .field(InputValue::new("name", TypeRef::named("String")))
1931            .field(InputValue::new("count", TypeRef::named("Int")));
1932        let inc = InputObject::new("author_inc_input")
1933            .field(InputValue::new("count", TypeRef::named("Int")));
1934        let response = Object::new("author_mutation_response").field(Field::new(
1935            "affected_rows",
1936            TypeRef::named_nn("Int"),
1937            |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1938        ));
1939        Schema::build("query_root", Some("mutation_root"), None)
1940            .register(Scalar::new("ltree"))
1941            .register(Scalar::new("raster"))
1942            .register(raster_comparison)
1943            .register(kind)
1944            .register(kind_comparison)
1945            .register(filter)
1946            .register(comparison)
1947            .register(ltree)
1948            .register(tree_filter)
1949            .register(insert)
1950            .register(set)
1951            .register(inc)
1952            .register(response)
1953            .register(author)
1954            .register(tree)
1955            .register(query)
1956            .register(mutation)
1957            .finish()
1958            .expect("the test schema builds")
1959    }
1960
1961    fn refusals(query: &str, variables: &str) -> Vec<String> {
1962        let request =
1963            async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1964                serde_json::from_str(variables).expect("the variables are JSON"),
1965            ));
1966        match prepare(Some(&schema()), request, None) {
1967            Ok(_) => Vec::new(),
1968            Err((_, errors)) => errors.into_iter().map(|e| e.message).collect(),
1969        }
1970    }
1971
1972    /// The same, answering with where each refusal says it happened.
1973    fn refusals_with_paths(query: &str, variables: &str) -> Vec<String> {
1974        let request =
1975            async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1976                serde_json::from_str(variables).expect("the variables are JSON"),
1977            ));
1978        match prepare(Some(&schema()), request, None) {
1979            Ok(_) => Vec::new(),
1980            Err((_, errors)) => errors.iter().map(path_for).collect(),
1981        }
1982    }
1983
1984    /// `where: {name: {_eq: null}}` reads as `name = NULL`, which is never
1985    /// true. Hasura refuses it, and so does this.
1986    #[test]
1987    fn a_null_written_into_a_comparison_is_refused() {
1988        assert_eq!(
1989            refusals("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
1990            vec!["unexpected null value for type 'String'"]
1991        );
1992    }
1993
1994    #[test]
1995    fn a_variable_standing_for_a_null_is_refused_there_too() {
1996        assert_eq!(
1997            refusals(
1998                "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
1999                r#"{"n": null}"#
2000            ),
2001            vec!["unexpected null value for type 'String'"]
2002        );
2003    }
2004
2005    /// An absent variable makes the comparison itself absent, which is a
2006    /// query with no such filter rather than one filtering on nothing.
2007    /// Where a refusal says it happened, which is a place in the request and
2008    /// not in the answer.
2009    #[test]
2010    fn a_refusal_names_the_argument_it_was_found_in() {
2011        let paths =
2012            |query: &str, variables: &str| -> Vec<String> { refusals_with_paths(query, variables) };
2013        // A key of an input object, under the argument that carried it.
2014        assert_eq!(
2015            paths(
2016                "mutation ($n: Int!) { insert_author_one(object: {name: $n}) { id } }",
2017                r#"{"n": 1}"#
2018            ),
2019            vec!["$.selectionSet.insert_author_one.args.object.name"]
2020        );
2021        // A comparison against a null names the operator it was written in.
2022        assert_eq!(
2023            paths("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
2024            vec!["$.selectionSet.author.args.where.name._eq"]
2025        );
2026        // One object written where a list was expected is that list's first
2027        // item, and the path says so.
2028        assert_eq!(
2029            paths(
2030                "mutation ($n: Int!) { insert_author(objects: {name: $n}) { id } }",
2031                r#"{"n": 1}"#
2032            ),
2033            vec!["$.selectionSet.insert_author.args.objects[0].name"]
2034        );
2035        // And an actual list is indexed by where the value really is.
2036        assert_eq!(
2037            paths(
2038                "mutation ($n: Int!) { insert_author(objects: [{name: \"a\"}, \
2039                 {name: $n}]) { id } }",
2040                r#"{"n": 1}"#
2041            ),
2042            vec!["$.selectionSet.insert_author.args.objects[1].name"]
2043        );
2044    }
2045
2046    /// `where: null` says nothing about which rows, and an update's `where`
2047    /// is required. Hasura names the type it wanted and what it got instead.
2048    #[test]
2049    fn a_null_written_where_an_object_is_required_is_refused_as_one() {
2050        assert_eq!(
2051            refusals(
2052                "mutation { update_author(where: null, _set: {name: \"a\"}) \
2053                 { affected_rows } }",
2054                "{}"
2055            ),
2056            vec!["expected an object for type 'author_bool_exp', but found null"]
2057        );
2058        assert_eq!(
2059            refusals_with_paths(
2060                "mutation { update_author(where: null, _set: {name: \"a\"}) \
2061                 { affected_rows } }",
2062                "{}"
2063            ),
2064            vec!["$.selectionSet.update_author.args.where"]
2065        );
2066        // A *variable* carrying the null is a different complaint, and
2067        // already had Hasura's own words for it: the fault is in the
2068        // variable's declaration, which is where it is reported.
2069        assert_eq!(
2070            refusals(
2071                "mutation ($w: author_bool_exp!) { update_author(where: $w, \
2072                 _set: {name: \"a\"}) { affected_rows } }",
2073                r#"{"w": null}"#
2074            ),
2075            vec!["null value found for non-nullable type: \"author_bool_exp!\""]
2076        );
2077    }
2078
2079    /// A nullable argument written as `null` is the same as leaving it out,
2080    /// which is not something to refuse.
2081    #[test]
2082    fn a_null_written_where_an_object_is_optional_is_left_alone() {
2083        assert_eq!(
2084            refusals("{ author(where: null) { id } }", "{}"),
2085            Vec::<String>::new()
2086        );
2087    }
2088
2089    /// One column named by two of an update's operators asks for it to be two
2090    /// things at once, and the order is not something the request says.
2091    #[test]
2092    fn a_column_written_by_two_operators_is_refused_before_the_statement() {
2093        assert_eq!(
2094            refusals(
2095                "mutation { update_author(_set: {name: \"a\", count: 1}, \
2096                 _inc: {count: 2}, where: {}) { affected_rows } }",
2097                "{}"
2098            ),
2099            vec!["Column found in multiple operators: ['count']."]
2100        );
2101        assert_eq!(
2102            refusals_with_paths(
2103                "mutation { update_author(_set: {count: 1}, _inc: {count: 2}, \
2104                 where: {}) { affected_rows } }",
2105                "{}"
2106            ),
2107            vec!["$.selectionSet.update_author.args"]
2108        );
2109        // Two operators naming different columns is an ordinary update.
2110        assert_eq!(
2111            refusals(
2112                "mutation { update_author(_set: {name: \"a\"}, _inc: {count: 2}, \
2113                 where: {}) { affected_rows } }",
2114                "{}"
2115            ),
2116            Vec::<String>::new()
2117        );
2118    }
2119
2120    /// Two things wrong with one request is one refusal. Hasura stops at the
2121    /// first, and a client written against it reads `errors[0]`.
2122    #[test]
2123    fn a_request_is_refused_once_however_many_faults_it_has() {
2124        assert_eq!(
2125            refusals("{ author { nope } tree { alsoNope } }", "{}"),
2126            vec!["field 'nope' not found in type: 'author'"]
2127        );
2128        // The second fault is a different kind from the first, and is still
2129        // not answered: what is reported is where the walk stopped, not one
2130        // error of each sort.
2131        assert_eq!(
2132            refusals(
2133                "mutation { insert_author(objects: [{nope: 1}],                  unknownArg: 2) { id } }",
2134                "{}"
2135            ),
2136            vec!["field 'nope' not found in type: 'author_insert_input'"]
2137        );
2138    }
2139
2140    /// A raster travels as the hex of its well-known binary. What is
2141    /// certainly not hex is refused here; the rest is still PostGIS's.
2142    #[test]
2143    fn a_raster_that_is_not_hexadecimal_is_refused_as_that() {
2144        let invalid = "invalid hexadecimal representation of raster well known binary format";
2145        assert_eq!(
2146            refusals(
2147                "query ($r: raster) { tree(where: {rast: {_st_intersects_rast: $r}}) { path } }",
2148                r#"{"r": "this is invalid raster value"}"#
2149            ),
2150            vec![invalid]
2151        );
2152        assert_eq!(
2153            refusals_with_paths(
2154                "{ tree(where: {rast: {_st_intersects_rast: \"zz\"}}) { path } }",
2155                "{}"
2156            ),
2157            vec!["$.selectionSet.tree.args.where.rast._st_intersects_rast"]
2158        );
2159        // A byte takes two characters.
2160        assert_eq!(
2161            refusals(
2162                "{ tree(where: {rast: {_st_intersects_rast: \"abc\"}}) { path } }",
2163                "{}"
2164            ),
2165            vec![invalid]
2166        );
2167        // Hex that is too short to be a raster is PostGIS's to refuse, in
2168        // PostGIS's words.
2169        assert!(refusals(
2170            "{ tree(where: {rast: {_st_intersects_rast: \"0100\"}}) { path } }",
2171            "{}"
2172        )
2173        .is_empty());
2174    }
2175
2176    /// A fragment that spreads its way back to itself describes no finite
2177    /// selection. async-graphql notices that something went too deep; Hasura
2178    /// says which fragments went round.
2179    #[test]
2180    fn fragments_that_spread_each_other_are_named_as_a_cycle() {
2181        let cycle = "query { author { ...a } } \
2182                     fragment a on author { id ...b } \
2183                     fragment b on author { id ...a }";
2184        assert_eq!(
2185            refusals(cycle, "{}"),
2186            vec!["the fragment definition(s) a and b form a cycle"]
2187        );
2188        // The selection set the offending spread is in, with each spread a
2189        // step of its own.
2190        assert_eq!(
2191            refusals_with_paths(cycle, "{}"),
2192            vec!["$.selectionSet.author.selectionSet.a.selectionSet.b.selectionSet"]
2193        );
2194        // A fragment that spreads itself is a cycle of one.
2195        assert_eq!(
2196            refusals(
2197                "query { author { ...a } } fragment a on author { id ...a }",
2198                "{}"
2199            ),
2200            vec!["the fragment definition(s) a form a cycle"]
2201        );
2202        // The same fragment twice in sibling positions is not a cycle.
2203        assert!(refusals(
2204            "query { author { ...a } articles: author { ...a } } \
2205             fragment a on author { id }",
2206            "{}"
2207        )
2208        .is_empty());
2209    }
2210
2211    /// GraphQL's `Int` is a signed 32-bit integer, and a number that is not
2212    /// one is refused while the request is read rather than by the database.
2213    #[test]
2214    fn a_number_that_is_not_an_int_is_refused_as_one() {
2215        let bounds = |value: &str| {
2216            format!(
2217                "The value {} lies outside the bounds or is not an integer. \
2218                 Maybe it is a float, or is there integer overflow?",
2219                value
2220            )
2221        };
2222        assert_eq!(
2223            refusals(
2224                "mutation { insert_author_one(object: {count: 2147483648}) { id } }",
2225                "{}"
2226            ),
2227            vec![bounds("2.147483648e9")]
2228        );
2229        assert_eq!(
2230            refusals_with_paths(
2231                "mutation { insert_author(objects: [{count: 2147483648}]) { id } }",
2232                "{}"
2233            ),
2234            vec!["$.selectionSet.insert_author.args.objects[0].count"]
2235        );
2236        // The edges of the type are inside it.
2237        for good in ["2147483647", "-2147483648", "0"] {
2238            assert!(
2239                refusals(
2240                    &format!(
2241                        "mutation {{ insert_author_one(object: {{count: {}}}) {{ id }} }}",
2242                        good
2243                    ),
2244                    "{}"
2245                )
2246                .is_empty(),
2247                "{} was refused",
2248                good
2249            );
2250        }
2251    }
2252
2253    /// Haskell writes a `Double` in fixed notation while it is neither very
2254    /// large nor very small, and Hasura quotes what Haskell wrote.
2255    #[test]
2256    fn a_number_is_shown_the_way_the_message_spells_it() {
2257        assert_eq!(shown_as_haskell_would(2147483648.0), "2.147483648e9");
2258        assert_eq!(shown_as_haskell_would(1.5), "1.5");
2259        assert_eq!(shown_as_haskell_would(1.0), "1.0");
2260        assert_eq!(shown_as_haskell_would(0.0), "0.0");
2261        assert_eq!(shown_as_haskell_would(-2147483649.0), "-2.147483649e9");
2262        assert_eq!(shown_as_haskell_would(1e-3), "1.0e-3");
2263        // The boundaries of the fixed range, which are Haskell's and not
2264        // Rust's.
2265        assert_eq!(shown_as_haskell_would(9999999.0), "9999999.0");
2266        assert_eq!(shown_as_haskell_would(10000000.0), "1.0e7");
2267        assert_eq!(shown_as_haskell_would(0.1), "0.1");
2268    }
2269
2270    /// `limit` is typed `Int` and is not any `Int`, and `offset` is -- which
2271    /// is Hasura's asymmetry, not an omission.
2272    #[test]
2273    fn a_limit_is_a_number_of_rows_and_not_any_integer() {
2274        let refused = |q: &str| refusals(q, "{}");
2275        assert_eq!(
2276            refused("{ author(limit: -1) { id } }"),
2277            vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2278        );
2279        assert_eq!(
2280            refused("{ author(limit: \"3\") { id } }"),
2281            vec!["expected a non-negative 32-bit integer for type 'Int', but found a string"]
2282        );
2283        assert_eq!(
2284            refusals_with_paths("{ author(limit: -1) { id } }", "{}"),
2285            vec!["$.selectionSet.author.args.limit"]
2286        );
2287        // A variable is the value it stands for.
2288        assert_eq!(
2289            refusals(
2290                "query ($n: Int) { author(limit: $n) { id } }",
2291                r#"{"n": -1}"#
2292            ),
2293            vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2294        );
2295        // Zero is a number of rows, and so is the largest `Int` there is.
2296        assert!(refused("{ author(limit: 0) { id } }").is_empty());
2297        assert!(refused("{ author(limit: 2147483647) { id } }").is_empty());
2298        assert!(refused("{ author(limit: null) { id } }").is_empty());
2299        // Beyond that it is not a number of rows because it is not an `Int`,
2300        // and it is answered as that rather than as both.
2301        assert_eq!(
2302            refused("{ author(limit: 4294967295) { id } }"),
2303            vec![
2304                "The value 4.294967295e9 lies outside the bounds or is not an \
2305                  integer. Maybe it is a float, or is there integer overflow?"
2306            ]
2307        );
2308    }
2309
2310    /// A quoted enum value is a mistake about the language; an unquoted one
2311    /// that names nothing is a mistake about the members. Hasura says which.
2312    #[test]
2313    fn a_quoted_enum_value_is_not_a_member_that_is_missing() {
2314        assert_eq!(
2315            refusals("{ tree(where: {kind: {_eq: \"leaf\"}}) { path } }", "{}"),
2316            vec!["expected an enum value for type 'kind_enum', but found a string"]
2317        );
2318        assert_eq!(
2319            refusals("{ tree(where: {kind: {_eq: twig}}) { path } }", "{}"),
2320            vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2321        );
2322        // Through a variable, a string is how JSON carries an enum, so the
2323        // question is which member it names.
2324        assert_eq!(
2325            refusals(
2326                "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2327                r#"{"k": "twig"}"#
2328            ),
2329            vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2330        );
2331        assert!(refusals(
2332            "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2333            r#"{"k": "leaf"}"#
2334        )
2335        .is_empty());
2336    }
2337
2338    /// A document that does not parse is not a query, and that is all the
2339    /// answer there is: async-graphql reports it the way a compiler does, and
2340    /// Hasura names the document.
2341    #[test]
2342    fn a_document_that_does_not_parse_is_not_a_query() {
2343        assert_eq!(
2344            refusals("{ author(where: {name: {_eq: \"unclosed}}) { id } }", "{}"),
2345            vec!["not a valid graphql query"]
2346        );
2347        assert_eq!(
2348            refusals_with_paths("query { author(", "{}"),
2349            vec!["$.query"]
2350        );
2351        // The rule is only about parsing. A document that parses and is wrong
2352        // about the schema is answered by the rules that know the schema.
2353        assert_eq!(
2354            refusals("{ nothing_here { id } }", "{}"),
2355            vec!["field 'nothing_here' not found in type: 'query_root'"]
2356        );
2357    }
2358
2359    /// An `ltree` with an empty label is refused before it is sent, in the
2360    /// words Hasura uses -- and nothing else is, because what a label may
2361    /// contain is the database's locale to decide.
2362    #[test]
2363    fn a_label_path_with_an_empty_label_is_refused() {
2364        let empty = "Expecting label path: a sequence of zero or more labels \
2365                     separated by dots, for example L1.L2.L3";
2366        assert_eq!(
2367            refusals(
2368                "{ tree(where: {path: {_ancestor: \"a.b.\"}}) { path } }",
2369                "{}"
2370            ),
2371            vec![empty]
2372        );
2373        assert_eq!(
2374            refusals(
2375                "{ tree(where: {path: {_ancestor: \".a\"}}) { path } }",
2376                "{}"
2377            ),
2378            vec![empty]
2379        );
2380        assert_eq!(
2381            refusals(
2382                "{ tree(where: {path: {_ancestor: \"a..b\"}}) { path } }",
2383                "{}"
2384            ),
2385            vec![empty]
2386        );
2387        // The list form says which item, which is what its path is for.
2388        assert_eq!(
2389            refusals_with_paths(
2390                "{ tree(where: {path: {_ancestor_any: [\"a\", \"b.\"]}}) { path } }",
2391                "{}"
2392            ),
2393            vec!["$.selectionSet.tree.args.where.path._ancestor_any[1]"]
2394        );
2395        // Everything PostgreSQL accepts is still sent to it, including the
2396        // characters a stricter reading would have refused.
2397        for good in ["a", "a.b", "", "a-b", "a_b", "1.2", "Ünï", "a@b"] {
2398            assert!(
2399                refusals(
2400                    &format!(
2401                        "{{ tree(where: {{path: {{_ancestor: \"{}\"}}}}) {{ path }} }}",
2402                        good
2403                    ),
2404                    "{}"
2405                )
2406                .is_empty(),
2407                "{} was refused",
2408                good
2409            );
2410        }
2411    }
2412
2413    #[test]
2414    fn a_variable_that_was_not_given_is_not_a_null() {
2415        assert!(refusals(
2416            "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
2417            "{}"
2418        )
2419        .is_empty());
2420    }
2421
2422    /// Only comparisons. A column set to null is a column set to null.
2423    #[test]
2424    fn a_null_written_anywhere_else_is_left_alone() {
2425        assert!(refusals(
2426            "mutation { insert_author_one(object: {name: null}) { id } }",
2427            "{}"
2428        )
2429        .is_empty());
2430    }
2431
2432    #[test]
2433    fn a_variable_of_the_wrong_type_is_refused() {
2434        assert_eq!(
2435            refusals("query ($s: String) { author(limit: $s) { id } }", "{}"),
2436            vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2437        );
2438    }
2439
2440    #[test]
2441    fn the_check_reaches_inside_an_input_object() {
2442        assert_eq!(
2443            refusals(
2444                "query ($s: String) { author(where: {id: $s}) { id } }",
2445                "{}"
2446            ),
2447            vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2448        );
2449    }
2450
2451    #[test]
2452    fn the_check_reaches_inside_a_list() {
2453        assert_eq!(
2454            refusals(
2455                "mutation ($n: Int) { insert_author(objects: [{name: $n}]) { id } }",
2456                "{}"
2457            ),
2458            vec!["variable 'n' is declared as 'Int', but used where 'String' is expected"]
2459        );
2460    }
2461
2462    #[test]
2463    fn a_nullable_variable_cannot_fill_a_non_null_place() {
2464        assert_eq!(
2465            refusals(
2466                "mutation ($a: author_insert_input) { insert_author_one(object: $a) { id } }",
2467                "{}"
2468            ),
2469            vec![
2470                "variable 'a' is declared as 'author_insert_input', but used where \
2471                 'author_insert_input!' is expected"
2472            ]
2473        );
2474    }
2475
2476    #[test]
2477    fn a_default_lets_it() {
2478        // The default stands in wherever the variable is left out, so the
2479        // place can never actually see a null.
2480        assert!(refusals(
2481            "mutation ($a: author_insert_input = {name: \"x\"}) \
2482             { insert_author_one(object: $a) { id } }",
2483            "{}"
2484        )
2485        .is_empty());
2486    }
2487
2488    #[test]
2489    fn a_default_of_null_does_not() {
2490        assert_eq!(
2491            refusals(
2492                "mutation ($a: author_insert_input = null) \
2493                 { insert_author_one(object: $a) { id } }",
2494                "{}"
2495            ),
2496            vec![
2497                "variable 'a' is declared as 'author_insert_input', but used where \
2498                 'author_insert_input!' is expected"
2499            ]
2500        );
2501    }
2502
2503    #[test]
2504    fn a_default_on_the_place_lets_it_too() {
2505        // `update_columns` is non-null with a default: not writing the
2506        // argument is allowed, so a variable that might be absent is too.
2507        assert!(refusals(
2508            "mutation ($c: [String!]) { insert_author(objects: [], update_columns: $c) { id } }",
2509            "{}"
2510        )
2511        .is_empty());
2512    }
2513
2514    #[test]
2515    fn a_non_null_variable_fits_a_nullable_place() {
2516        assert!(refusals(
2517            "query ($n: Int!) { author(limit: $n) { id } }",
2518            "{\"n\": 1}"
2519        )
2520        .is_empty());
2521    }
2522
2523    #[test]
2524    fn a_non_null_variable_given_null_is_refused() {
2525        assert_eq!(
2526            refusals(
2527                "query ($n: Int! = 1) { author(limit: $n) { id } }",
2528                "{\"n\": null}"
2529            ),
2530            vec!["null value found for non-nullable type: \"Int!\""]
2531        );
2532    }
2533
2534    #[test]
2535    fn a_nullable_variable_given_null_is_not() {
2536        assert!(refusals(
2537            "query ($n: Int) { author(limit: $n) { id } }",
2538            "{\"n\": null}"
2539        )
2540        .is_empty());
2541    }
2542
2543    #[test]
2544    fn a_variable_used_through_a_fragment_is_checked() {
2545        assert_eq!(
2546            refusals(
2547                "query ($s: String) { author { ...rows } } \
2548                 fragment rows on author { id }",
2549                "{}"
2550            ),
2551            Vec::<String>::new()
2552        );
2553        assert_eq!(
2554            refusals(
2555                "query ($s: String) { ...roots } \
2556                 fragment roots on query_root { author(limit: $s) { id } }",
2557                "{}"
2558            ),
2559            vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2560        );
2561    }
2562}
2563
2564#[cfg(test)]
2565mod coercion_tests {
2566    use super::*;
2567    use async_graphql::dynamic::*;
2568
2569    /// The three places a value is written: a field argument the engine owns,
2570    /// a field argument a column owns, and a column inside an input object.
2571    fn schema() -> async_graphql::dynamic::Schema {
2572        let insert = InputObject::new("test_types_insert_input")
2573            .field(InputValue::new("c1_smallint", TypeRef::named("Int")))
2574            .field(InputValue::new("c6_real", TypeRef::named("Float")))
2575            .field(InputValue::new("c20_boolean", TypeRef::named("Boolean")))
2576            .field(InputValue::new("c13_text", TypeRef::named("String")));
2577        let row = Object::new("test_types").field(Field::new(
2578            "c1_smallint",
2579            TypeRef::named("Int"),
2580            |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
2581        ));
2582        let query = Object::new("query_root").field(
2583            Field::new(
2584                "test_types",
2585                TypeRef::named_nn_list_nn("test_types"),
2586                |_| FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) }),
2587            )
2588            .argument(InputValue::new("limit", TypeRef::named("Int")))
2589            .argument(InputValue::new("offset", TypeRef::named("Int"))),
2590        );
2591        let mutation = Object::new("mutation_root").field(
2592            Field::new("insert_test_types", TypeRef::named("test_types"), |_| {
2593                FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
2594            })
2595            .argument(InputValue::new(
2596                "objects",
2597                TypeRef::named_nn_list_nn("test_types_insert_input"),
2598            )),
2599        );
2600        Schema::build("query_root", Some("mutation_root"), None)
2601            .register(insert)
2602            .register(row)
2603            .register(query)
2604            .register(mutation)
2605            .finish()
2606            .expect("the schema builds")
2607    }
2608
2609    fn rewritten(query: &str) -> String {
2610        let schema = schema();
2611        let request = prepare(Some(&schema), async_graphql::Request::new(query), None)
2612            .unwrap_or_else(|(request, _)| request);
2613        let mut request = request;
2614        format!("{:?}", request.parsed_query().expect("the query parses"))
2615    }
2616
2617    #[test]
2618    fn an_offset_written_as_a_string_becomes_a_number() {
2619        let printed = rewritten("{ test_types(offset: \"1\") { c1_smallint } }");
2620        assert!(printed.contains("Number(1)"), "{}", printed);
2621    }
2622
2623    /// The corpus refuses this one in the same breath as it answers the
2624    /// offset above, so it is left exactly as written.
2625    #[test]
2626    fn a_limit_written_as_a_string_is_left_alone() {
2627        let printed = rewritten("{ test_types(limit: \"3\") { c1_smallint } }");
2628        assert!(printed.contains("String(\"3\")"), "{}", printed);
2629    }
2630
2631    #[test]
2632    fn a_column_written_as_a_string_becomes_what_the_column_is() {
2633        let printed = rewritten(
2634            "mutation { insert_test_types(objects: [{ \
2635             c1_smallint: \"32767\", c6_real: \"0.5\", c20_boolean: \"true\" }]) \
2636             { c1_smallint } }",
2637        );
2638        assert!(printed.contains("Number(32767)"), "{}", printed);
2639        assert!(printed.contains("Number(0.5)"), "{}", printed);
2640        assert!(printed.contains("Boolean(true)"), "{}", printed);
2641    }
2642
2643    /// A text column keeps its digits. This is the whole reason the walk is
2644    /// type-directed rather than a sweep over every string in the document.
2645    #[test]
2646    fn a_string_written_where_a_string_belongs_stays_one() {
2647        let printed = rewritten(
2648            "mutation { insert_test_types(objects: [{ c13_text: \"32767\" }]) { c1_smallint } }",
2649        );
2650        assert!(printed.contains("String(\"32767\")"), "{}", printed);
2651    }
2652}