gql: resolvers
The circumstances under which I have time are frightening, but I have time to do some learning and programming. So, there we go.
In the previous post, I suggested a GraphQL syntax for Racket that would look like this:
(define-type Starship
(fields
(name -> String)))
(define-enum Episode
NEWHOPE EMPIRE JEDI)
(define-type Human
(fields
(name -> String)
(appearsIn -> (listof Episode))
(starship -> (listof Starship))
))
(define-type Query
(fields
(human ([id ID!]) -> Human)
))
Before I think about a syntax for resolvers, I need to things. 1) I need some data, and 2) I need to write some functions that do the resolving by hand, so I can see what patterns emerge, as those become either functional or syntactic abstractions.
I’ll take them in reverse order.
a resolver for ‘appearsIn’
I’m working from this page on the GraphQL site, which suggests that a resolver for appearsIn
might look like this:
Human: {
appearsIn(obj) {
return obj.appearsIn // returns [ 4, 5, 6 ]
}
}
At first glance, it might be that a resolver for appearsIn
could be:
(define (appearsIn o a c i)
(define Q (select episode
#:from people_episode
#:where (= human_id ,(hash-ref o 'id))))
;; If you want to see the SQL generated...
;; (printf "Q: ~a~n" Q)
(define result (query (hash-ref c 'conn) Q))
;; This gives me a set of rows from the people_episode table.
;; Then, I want to turn that into a list of enum elements.
(for/list ([row (rows-result-rows result)])
(ndx->enum Episode (vector-ref row 0))))
In use, it looks like this:
> (appearsIn (make-hash '((id . 1)))
NULL
(make-hash `((conn . ,conn)))
NULL)
Q: #<sql-statement: SELECT episode FROM people_episode WHERE (human_id = ?) 1>
'(NEWHOPE EMPIRE JEDI)
> (appearsIn (make-hash '((id . 2)))
NULL
(make-hash `((conn . ,conn)))
NULL)
Q: #<sql-statement: SELECT episode FROM people_episode WHERE (hum
You can see that I included a printf
statement to see the SQL being generated. This says that Luke was in all three movies, and the Ewok was only in the third movie. I forget the Ewok’s name… Nugget? Wikkit? I have no memory at this point…
However, my suspicion is that this is wrong. Specifically, I think I want to resolve from the top down, and do things in a lazy manner (from a “lazy evaluation” perspective). The resolver for a human
should return a minimal object, with promises on all of its fields. Then, when it is time to do something with the fields, those promises are forced. The reason for this is not all fields are always used in a query, so there is no sense in doing the work if the user doesn’t want the data.
The example above might be the code that could be promised, but it isn’t actually the resolver. The resolver should be a trivial resolver that is automatically forced when needed.
A good, short exploration, helping me see that GraphQL implementations really are about lazy performance in the face of large data.
the data
To continue exploring, I set up a small SQLite database. I wrote it to a temporary file, but you might want to do it differently. For now, it’s hardcoded to /tmp
on a Linux machine.
The reason for using an SQL database as the “backend” was to force me to think about GraphQL resolvers. That is, if I had just stored things in memory as hash tables for this exploration, there would be less work to do. By storing the data in an SQL table, I’m forced to think about how GraphQL queries are processed, and how they are actualized by resolvers (or something lower down the chain) in the event that you’re operating over a traditional/relational dataset, instead of a no-SQL dataset.
The code is in a Github Gist. I need to dump the code into a repository at this point, as the exploration went beyond a 1-hour exploration into something that I might continue poking at.