Skip to content

Instantly share code, notes, and snippets.

@ruslantalpa
Last active October 16, 2015 14:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ruslantalpa/d14853b27ab2999130d6 to your computer and use it in GitHub Desktop.
Save ruslantalpa/d14853b27ab2999130d6 to your computer and use it in GitHub Desktop.
Thoughts on SQL backend/resolver for GraphQL server

We want to get back this result

{
	project: {
		id: 1,
		name: "First Project",
		description: "Create a cool website",
		client: {
			id: 1,
			name: "First Client"
		},
		tasks: [
			{
				id: 1,
				name: "Task One"
			},
			{
				id: 2,
				name: "Task Two"
			}
		]
	}
}

The GraphQL query looks like this, and in the comment we have the sql being generated/defined by the resolver for each field

query ProjectOne {
	project: projects (id: 1) {     -- SELECT * FROM projects WHERE id = 1
		id,                         -- SELECT id FROM projects
		name,                       -- SELECT name FROM projects 
		description,                -- SELECT description FROM projects
		client: clients {           -- SELECT * FROM clients
			id,                     -- SELECT id FROM clients
			name                    -- SELECT name FROM clients
		},
		tasks (completed: false): { -- SELECT * FROM tasks WHERE completed = false
			id,                     -- SELECT id FROM tasks
			name                    -- SELECT name FROM tasks
		}
	}
}

When the SQL execution engine walks this GraphQL AST, the result of executing each node is a value defined by an ADT for representing a limited part of SQL, so at this point, resolvers are pure functions. After the first step (execution of each resolver), the result basically is a "Tree Query" where Query is something like: data Query = Select {fields::[String], from::[String], where_::[Condition], ...} | .... The second part that is to fold this tree based on the fact that the Sql ADT is part of Monoid typeclass and by that i mean that the follwing queries

SELECT * FROM projects WHERE id = 1
SELECT id FROM projects
SELECT name FROM projects 
SELECT description FROM projects

collapse into

 SELECT id, name, description FROM projects WHERE id = 1

The same is true for clients/tasks So after the second step (actually 2.1) We have a tree like this

SELECT id, name, description FROM projects WHERE id = 1
	SELECT id, name FROM clients
	SELECT id, name FROM tasks WHERE completed = 1

Based on relations between projects/clients/tasks (not sure how that is specified by the schema or resolver know that beforehand, but the db can used for introspection to determine the foreign keys for relations), this tree of queries can be folded/collapsed into an end result query

SELECT array_to_json(array_agg(row_to_json(t)))::CHARACTER VARYING AS json
FROM (
	WITH clients AS (
	  SELECT clients.id, clients.name FROM clients
	)
	SELECT
	    projects.id, 
	    projects.name, 
	    projects.decription,
	    row_to_json(clients.*) AS client,
	    (
	        SELECT array_to_json(array_agg(row_to_json(tasks)))
	        FROM (
	            SELECT tasks.id, tasks.name
	            FROM tasks
	            WHERE tasks.project_id = projects.id
	        ) tasks
	    ) AS tasks
	FROM projects, clients
	WHERE projects.client_id = clients.id
) t;

This idea above seems limited to the fact that The SQL backend is not allowing general SQL being used in resolvers and also, this backend is tied to a single database for which we know all the relations and i am not sure if FB intended each resolver to be independent of each other (i.e. one resolver to be SQL and another be a pure function )

@ruslantalpa
Copy link
Author

So my understanding so far of how the "execution" part is working in graphql-js is that each resolver returns basically a scalar/array/object which obviously have a json representation. So the "interpreter" walks the ast and executes each resolver and collects these objects in a tree and at the end it does a json encode.
What this (probably) means in Haskell is that each resolver needs to return a value or type "a" with the constraint that "a" has a "ToJSON" instance from Aeson. So since these values can come from 2 places (pure and IO) then the return type of the resolver could be something like "(ToJSON a) => IO a"

@ruslantalpa
Copy link
Author

Also note this discussion to see how a database like postgresql can return a Aeson.Value after executing a query.
nikita-volkov/hasql-postgres#25

@ruslantalpa
Copy link
Author

At the same time if resolvers are only of type IO a, then when dealing with sql, each one would have to actually execute a query which does not allow "building" a single query like described above ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment