Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions modules/core/src/main/scala/compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -445,34 +445,36 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) {
val fragRefs: Map[String, (Set[String], Set[String])] =
frags.map { frag => (frag.name, collectQueryRefs(frag.child)) }.toMap

@tailrec
def checkCycle(pendingFrags: Set[String], seen: Set[String]): Option[Set[String]] = {
if (pendingFrags.isEmpty) Some(seen)
else {
val hd = pendingFrags.head
if (seen.contains(hd)) None
// Find a fragment which is reachable from itself, if any.
//
// `path` is the set of fragments on the current traversal path, and `done` the set of
// fragments whose references have already been explored in full without discovering a
// cycle. A fragment is added to `done` only once its references have been explored, and
// the `path` check precedes the `done` check, so that a fragment which is merely
// reachable by more than one path isn't mistaken for a cycle.
def findCycle: Option[String] = {
def visit(
frag: String,
path: Set[String],
done: Set[String]): Either[String, Set[String]] =
if (path.contains(frag)) Left(frag)
else if (done.contains(frag)) Right(done)
else
checkCycle(
fragRefs.get(hd).map(_._2).getOrElse(Set.empty) ++ pendingFrags.tail,
seen + hd)
}
}
fragRefs
.get(frag)
.map(_._2)
.getOrElse(Set.empty)
.foldLeft(Right(done): Either[String, Set[String]]) { (acc, ref) =>
acc.flatMap(visit(ref, path + frag, _))
}
.map(_ + frag)

def findCycle: Option[String] = {
@tailrec
def loop(pendingFrags: Set[String]): Either[Set[String], String] = {
if (pendingFrags.isEmpty) Left(Set.empty[String])
else {
val hd = pendingFrags.head
checkCycle(Set(hd), Set.empty[String]) match {
case None => Right(hd)
case Some(seen) => loop(pendingFrags.tail.diff(seen))
}
uniqueFrags
.foldLeft(Right(Set.empty[String]): Either[String, Set[String]]) { (acc, frag) =>
acc.flatMap(visit(frag, Set.empty[String], _))
}
}

if (uniqueFrags.isEmpty) None
else loop(uniqueFrags).toOption
.left
.toOption
}

findCycle match {
Expand Down
61 changes: 61 additions & 0 deletions modules/core/src/test/scala/compiler/FragmentSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,67 @@ final class FragmentSuite extends CatsEffectSuite {
assertIO(res, expected)
}

test("fragment shared by multiple paths (not a cycle)") {
val query = """
query withFragments {
user(id: 1) {
...userFields
}
}

fragment userFields on User {
friends {
...nameFields
}
mutualFriends {
...mutualFriendFields
}
}

fragment mutualFriendFields on User {
...nameFields
}

fragment nameFields on User {
id
name
}
"""

val expected = json"""
{
"data" : {
"user" : {
"friends" : [
{
"id" : "2",
"name" : "Bob"
},
{
"id" : "3",
"name" : "Carol"
}
],
"mutualFriends" : [
{
"id" : "2",
"name" : "Bob"
},
{
"id" : "3",
"name" : "Carol"
}
]
}
}
}
"""

val res = FragmentMapping.compileAndRun(query)

assertIO(res, expected)
}

test("fragment recursion (1)") {
val query = """
query withFragments {
Expand Down
Loading