-
Notifications
You must be signed in to change notification settings - Fork 73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create GraphQL to SQL compiler #61
Comments
As mentioned on Slack, I've decided first to make GraphQL-to-LINQ interpreter as this is more flexible approach, and can be potentially used for more scenarios and solve problems such as selects over many-to-many joints for free. Downside of it is performance cost - another level of indirection that requires additional computations and may result in slower code. Right now I've managed to refactor GraphQL query: {
users(id: 1) {
id
info {
firstName
lastName
}
contacts {
firstName
}
}
} This would produce following execution plan:
By applying that execution plan we would expect following LINQ query: users.Where(u => u.Id = 1).Select(u => // ResolveCollection: users
new User { // SelectFields: users
Id = u.Id, // ResolveValue: id
Info = new UserInfo { // SelectFields: info
FirstName = u.FirstName // ResolveValue: firstName
LastName = u.LastName }, // ResolveValue: lastName
Contacts = u.Contacts.Select(i => // ResolveCollection: contacts
new UserInfo { // SelectFields: contacts
FirstName = i.FirstName // ResolveValue: firstName
})
}) Therefore:
LINQ queries should also respect arguments, at least the ones defined in Relay specification:
All of those arguments work on |
Motivation
Right now we can execute any kind of query. However one of the common scenarios would be to create a reply by making SQL query to a database and then returning the computation result back to requester. Here we have a problem - since we are defining resolution of per-field basis, any nested calls will result in N+1 SELECT problems, eg. given a GraphQL query:
and schema defined like:
we would end up with making a one query to retrieve user, and then for each user retrieved this way, one additional query to get his pets. This is a common problem in O/R mapping and something we'd like to avoid.
Possible solution
Because we have already implemented planning phase, we have a convenient representation of GraphQL queries in form of
ExecutionPlan
data structure attached to context. What we can do from here is to create a compiler, which would translate GraphQL query into SQL queries.Currently, beside data about things like field name, alias, output type, arguments or containing object data, we can also represent a structure of the query itself by using
ExecutionPlanKind
discriminated union, ie. GraphQL query above would look close to the following:We could translate those into their SQL query equivalent.
How?
ResolveValue
This can be translated directly to matching SQL table column. Given an execution plan info structure the most straightforward translation would look like:
info.ParentDef.Name + "." + info.Definition.Name + " AS " + info.Identifier
. Remark: this is not actual code, we would need some table aliasing etc.SelectFields
We have two cases here:
SelectFields
we can use selection with JOIN clause between inner and outer SelectFields (becauseSelectFields [ SelectFields [ .. ] ]
is always 1-1 relation. Example:GraphQL query:
into SQL:
ResolveCollection
Since this kind always describes 1-many relationship, realizing it with joins may not be a good idea i.e.: given
customers.orders.products
- where we have 10 customers, each of them has 50 orders and each order has 20 products, we would end up with a SQL query returning 10 000 rows, where actual number of entities we care about is 80.Solution:
Make a separate sub select - as SQL command can take more than one SELECT statement, once we find
ResolveCollection
we can translate its subtree into separate SELECT FROM statement and join data for it directly from the result set. Remark: this will require and additional field to correlate rows from a separate selects.Example:
GraphQL query:
into SQL queries:
ResolveAbstraction
This kind is used for both GraphQL interfaces and unions and carry the typeMap info (
Map<string, ExecutionPlanInfo list>
), which describes a list of fields to be resolved for each type target abstraction refers to. We basically need to use two cases here:Union
Union should be pretty easy to translate. Example - given type
union Pet = Dog | Cat
we could assume there are some Dog and Cat tables in the SQL schema, we could translate following GraphQL query:into SQL looking like that:
Then just use TypeName as discriminator for creating result objects.
Interface
Dunno yet. They are basically unrepresentable in SQL.
Security issues
One of the most obvious problems is to apply some constrains on the queries, so that not everyone would be able to traverse through the whole database. Two possibilities I had in mind:
Tree cutting
Given an exectuion plan tree, we could introduce some operators to split it in two before executing. ie given user query:
split it into twp:
Once first query will be executed, the result can be authorized and then (if auth was successful) the second query would be executed. We could add some methods to the API to make execution plan tree cutting easier.
Resolve value exceptions
The second option - more costly but also easier to achieve - would be to simply apply authorization of field resolution level, i.e:
This won't save from retrieving that data from the database, but it won't return it outside.
Upgrade F# compiler dependency
Other thing that could be taken into account withing that task is to think about switching
FieldDef.Resolve
from function to expression - this could give us possibility to interpret the body of resolution function. This will be the breaking change for F# compiler < 4.0 - we could raise that requirement and mark function passed as parameter withReflectedDefinition
attribute to make it work in transparent way.The text was updated successfully, but these errors were encountered: