Skip to content

Instantly share code, notes, and snippets.

@junosuarez
Created September 16, 2016 02:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save junosuarez/7f8174e59102c3e30f5f84d19c49dfd7 to your computer and use it in GitHub Desktop.
Save junosuarez/7f8174e59102c3e30f5f84d19c49dfd7 to your computer and use it in GitHub Desktop.
debugging graphql-ruby
  1. traverse each top-level thing, depth-first. for fragments, build a stack of "spread" references to be executed later 1a. so first it gets the query operation, then the F0, F1, etc 1b. each top-level object (operation, fragment) has a tree of nodes with "selections"
  2. when it starts processing fragments, it hits a lazy reducer which enumerates all of the schema types / interfaces (FragmentWithTypeStrategy) 2a. when it enters visiting a FragmentDefinition, it pushes it on the fragments stack
  3. when it gets to a Field under a fragment, the Node:Field.enter visitor looks up that field from the parent irep node or else creates a new irep node. it pushes it on the @nodes stack, and pushes empty parent directives? is that right? at this point, the Field AST node has directives as child AST nodes, which get traversed next
  4. when it visits the Directive AST node, it makes an irep node and pushes it on the @parent_nodes stack
  5. Arguments are children of the Directive AST, which is visited next. things unwind. back to the Field. @parent_directives stack has the directive
  6. in Nodes::Field.leave, field_node.directives (which has not yet been copied from the AST, so it's an empty set) are merged with @parent_directives stack, which is then popped.
  7. back on leave FragmentDefinition F0, F0 doesn't depend on any fragments, so it's stored as an "independent fragment" for faster resolution at the final linking stage
  8. now we're on our third Document child, Fragment F1
  9. now we're in F1 > flavor field. the parent_node is the F1 node. we get a new irep node for this field, separate from F0 > flavor's. which i guess makes sense because the same fragment could appear in different places and be used multiple times. so this really is an intermediate representation, and still needs one more pass to get the optimized query AST.
  10. it's kinda weird that the irep directives dont include their arguments, except i guess by reference back to the AST directive node. they dont have them in children, unlike other places. although the irep Field node also doesnt include arguments. it does include children and spreads.
  11. so the ast nodes have "selections" and "arguments" - different types of child trees - while the irep nodes contain children (fields, i think, in every case) and spreads which need to be linked to fragments. spreads is, in fact, an array with references to Fragment irep nodes.
  12. okay so now we're all the way back up to the root with Nodes::Document.leave. this is the part that's now gonna unwind the independent_fragments stack and link up all the spreads. as it links, it merges (using deep_merge defined in rewrite.rb). (at the end of the deep merge theres a neat thing where it checks to see if it was merging a fragment, and, if so, checking for dependencies and pushing it back on the independent_fragments if, in fact, it is independent. this way we'll eventually walk all (used) fragments)
  13. deep merge child! put a breakpoint here to get all the way back to this point. the fragment node and the "parent node" are actually at the same level in the graph hierarchy, so we merge each of the fragments children in with the parent node. the directives argument for the deep_merge_child method is the directives for the fragment, not for the child.
  14. so at the end of the first deep_merge, F1 has been merged into the Query tree, creating all the fields that werent already there
  15. so now we're at F0, and we're going to resolve/merge the spread back into the Query. deep_merge time.
  16. this time, rather than #duping the field irep node from the Fragment, it gets the reference to the field that's already on the parent, the one we duped last time.
  17. no children in this field, so the last thing there is to do is merge in the new directives.
  18. HERES WERE THE BUG IS! we're merging in extra_directives - which are the directives from the fragment level - but we're not merging in the directives that actually belong to the other node.
  19. in addition to child_node.directives.merge(extra_directives), (or maybe instead of?), we need to child_node.directives.merge(node.directives)
  20. so this explains the behavior we were seeing: the field will keep the first directive that's seen. if the field is defined in the query object, that's the one. if it's only defined in fragments, it'll get that one, which means in reverse-lexical order, since the fragments are resolved as the stack is unwinding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment