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