Skip to content

Support for generating (limited) recursive queries #9

@warpfork

Description

@warpfork

The setup

First off, just to clear the air -- GraphQL doesn't really support recursion. This is a conscious design decision by the GraphQL developers, and it's one I largely understand and agree with. Recursive queries are a great way to accidentally generate unbounded load on your servers.

However, that's not to say some finite amount of recursion isn't useful. It's possible to come up with situations where a query should use some type A which contains SomeField A[]. The recursion might be limited at some depth -- possibly 1, possibly 2; any finite number -- even 1 is significant.

So, as formulated nicely in an upstream issue in GraphQL, this sort of query for example is sometimes useful:

{
  messages {
    ...CommentsRecursive
  }
}

fragment CommentsRecursive on Message {
  comments {
    ...CommentFields
    comments {
      ...CommentFields
      comments {
        ...CommentFields
      }
    }
  }
}

fragment CommentFields on Comment {
  id
  content
}

I've been experimenting with this, and it seems like it's not a huge problem to do it on the server side with neelance/graphql-go, happily (it just terminates recursion when it runs out of data, and implementing that is Our Problem, but that's fine).

This client library, however, currently gets hung up if we use a single type that contains itself, like this:

type Foobar struct {
    Children []Foobar
}

The current hangup isn't because the library blocks recursive queries, but rather because it's a tad too enthusiastic about recursing... forever. This is the relevant part of the stack trace resulting:

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

goroutine 7 [running]:
... some frames elided ....
theproject/vendor/github.com/shurcooL/graphql.writeQuery(0x15b3be0, 0xc42017f180, 0x15be320, 0x139e100, 0x0)
        /theproject/vendor/github.com/shurcooL/graphql/query.go:103 +0x93 fp=0xc4402005c0 sp=0xc4402004c0 pc=0x1303dd3
theproject/vendor/github.com/shurcooL/graphql.writeQuery(0x15b3be0, 0xc42017f180, 0x15be320, 0x13ba360, 0x133be00)
        /theproject/vendor/github.com/shurcooL/graphql/query.go:123 +0x11f fp=0xc4402006c0 sp=0xc4402005c0 pc=0x1303e5f
theproject/vendor/github.com/shurcooL/graphql.writeQuery(0x15b3be0, 0xc42017f180, 0x15be320, 0x1358c40, 0x0)
        /theproject/vendor/github.com/shurcooL/graphql/query.go:100 +0x3fa fp=0xc4402007c0 sp=0xc4402006c0 pc=0x130413a
theproject/vendor/github.com/shurcooL/graphql.writeQuery(0x15b3be0, 0xc42017f180, 0x15be320, 0x13ac720, 0x133be00)
        /theproject/vendor/github.com/shurcooL/graphql/query.go:123 +0x11f fp=0xc4402008c0 sp=0xc4402007c0 pc=0x1303e5f
... loop continues between 100 and 123 ...

Basically, the writeQuery function as currently written is perfectly happy to keep looking up the type of the Foobar struct, looking at the Children field and generating a little more query, then recursing to look up the type of the field, which is of course still Foobar...

(not-so-great) Workarounds

Once solution to this issue is "don't have recursive queries".

If you can get away with that, great. If trying to shoehorn that in to a situation where the types really are correctly represented as recursive, ehm.

We tried doing a couple of copypasta types -- e.g. just a T1, T2, T3, etc, where T1 has a field that's a slice of T2, etc -- but this approach does not come out very elegant: those almost-identical types become visible to more other code in the project than we'd like.

Potential Solutions

The GraphQL upstream opinion seems to be that finite and explicit recursion is fine -- just pick a depth. I'd propose we teach this library how to do that. The main question is how to communicate it to the library, since the structs and tags are where all the communication currently goes on.

My rough proposal would be adding a map to the writeQuery function where it keeps track of where it's visited, and increments the map values on each visit. This map could be keyed by a tuple of {t reflect.Type, fieldOffset int}, which marks the recursion edge itself. This would also correspond nicely with adding more info to the tag on the struct field itself for how much we want to limit recursion.

How to add another parameter to the field tags is a little more interesting. Since the entire quoted section of the tag is current graphQL snippet, perhaps the clearest thing to do would actually be to add a second tag, like so: SomeField TypeX graphql:"... on foobar",graphql-recursion:3` I'm definitely open to other designs on that though.

There are some limits to this approach, namely that the tags approach leaves us sort of stuck declaring the recursion depths statically at compile time. But that's roughly consistent with the library's approach as a whole, and seems to be a pretty acceptable trade to me.

WDYT?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions