@@ -230,3 +230,239 @@ let ``Planning must work with unions``() =
230230 " Person" ,
231231 [ ( " name" , upcast UNamed, upcast StringType)
232232 ( " age" , upcast UNamed, upcast IntType) ] ])
233+
234+ [<Fact>]
235+ let ``Planning must handle inline fragment with non - matching type condition in unions`` () =
236+ // ═══════════════════════════════════════════════════════════════════════════
237+ // REGRESSION TEST for Planning_ResolveDeferred_Bug
238+ // ═══════════════════════════════════════════════════════════════════════════
239+ //
240+ // GraphQL SCENARIO:
241+ // =================
242+ // In GraphQL, inline fragments with type conditions are used to query fields
243+ // specific to certain types in a union or interface:
244+ //
245+ // query {
246+ // items { # Union of [Animal, Person]
247+ // ... on Animal { } # ✓ Valid – Animal is in union
248+ // ... on Person { } # ✓ Valid – Person is in union
249+ // ... on Robot { } # ✓ Valid by spec! Robot is not in union
250+ // }
251+ // }
252+ //
253+ // THE PROBLEM:
254+ // ============
255+ // When an inline fragment's type condition does NOT match any type in the union,
256+ // the GraphQL spec says this is VALID – the fragment simply never matches.
257+ //
258+ // Example: Querying for Robot fields on a Person|Animal union
259+ // Expected: Empty result for Robot (no runtime error during planning)
260+ // Bug: Runtime error "Expected an Abstraction!" during query planning phase
261+ //
262+ // WHY IT MATTERS:
263+ // ===============
264+ // This commonly happens in real-world scenarios:
265+ // - Generic queries across multiple schema types
266+ // - Schema evolution (type removed from union, old queries still reference it)
267+ // - Client doesn't know exact union composition
268+ //
269+ // According to GraphQL spec, this must NOT fail – it should gracefully
270+ // produce an empty result set for non-matching fragments.
271+ // ═══════════════════════════════════════════════════════════════════════════
272+
273+ // Create a third type that is NOT part of UNamed union
274+ let Robot =
275+ Define.Object(
276+ name = " Robot" ,
277+ fields =
278+ [ Define.Field( " modelNumber" , StringType, fun _ ( robot : string ) -> robot)
279+ Define.Field( " name" , StringType, fun _ _ -> " Robot" ) ])
280+
281+ let Query = Define.Object( " Query" , [ Define.Field( " names" , ListOf UNamed, fun _ () -> []) ])
282+ let schema = Schema( query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; Robot ] })
283+ let schemaProcessor = Executor( schema)
284+
285+ // GraphQL Query:
286+ // UNamed union = Person | Animal (Robot is NOT in this union)
287+ // The "... on Robot" fragment below will never match any objects
288+ let query = """ query Example {
289+ names {
290+ ... on Animal {
291+ name
292+ species
293+ }
294+ ... on Person {
295+ name
296+ age
297+ }
298+ ... on Robot {
299+ modelNumber
300+ }
301+ }
302+ }"""
303+
304+ // TEST ASSERTION:
305+ // This must succeed per GraphQL spec – non-matching fragments are valid
306+ // Bug would cause: "Expected an Abstraction!" runtime error during planning
307+ let plan = schemaProcessor.CreateExecutionPlanOrFail( query)
308+
309+ // Verify the execution plan structure
310+ equals 1 plan.Fields.Length
311+ let listInfo = plan.Fields.Head
312+ let UNamedList : ListOfDef < Named , Named list > = ListOf UNamed
313+ listInfo.Identifier |> equals " names"
314+ listInfo.ReturnDef |> equals ( upcast UNamedList)
315+ let ( ResolveCollection ( info )) = listInfo.Kind
316+ info.ParentDef |> equals ( upcast UNamedList)
317+ info.ReturnDef |> equals ( upcast UNamed)
318+
319+ // Must successfully extract abstraction info
320+ // Bug would fail here with wrong execution info kind
321+ let ( ResolveAbstraction ( innerFields )) = info.Kind
322+
323+ // Result: Only Animal and Person fields (Robot is filtered out)
324+ // This is correct GraphQL behavior – non-matching fragments produce no fields
325+ innerFields
326+ |> Map.map ( fun typeName fields -> fields |> List.map ( fun i -> ( i.Identifier, i.ParentDef, i.ReturnDef)))
327+ |> equals ( Map.ofList [ " Animal" ,
328+ [ ( " name" , upcast UNamed, upcast StringType)
329+ ( " species" , upcast UNamed, upcast StringType) ]
330+ " Person" ,
331+ [ ( " name" , upcast UNamed, upcast StringType)
332+ ( " age" , upcast UNamed, upcast IntType) ] ])
333+
334+ [<Fact>]
335+ let ``Planning must handle nested inline fragments with non - matching type conditions`` () =
336+ // REGRESSION TEST for Planning_ResolveDeferred_Bug (nested scenario)
337+ //
338+ // GraphQL SCENARIO:
339+ // =================
340+ // Same issue as above, but with nested structure:
341+ //
342+ // query {
343+ // container {
344+ // nested { # Union of [Animal, Person]
345+ // ... on Robot { } # ✓ Valid – just never matches
346+ // }
347+ // }
348+ // }
349+ //
350+ // Tests that deeply nested queries with non-matching fragments work correctly.
351+ // This is common in production with complex schema hierarchies.
352+
353+ // Define Robot type (not part of UNamed union)
354+ let RobotType =
355+ Define.Object(
356+ name = " Robot" ,
357+ fields =
358+ [ Define.Field( " modelNumber" , StringType, fun _ ( robot : string ) -> robot)
359+ Define.Field( " name" , StringType, fun _ _ -> " Robot" ) ])
360+
361+ // Container type with nested union list – creates deeper nesting
362+ let ContainerType =
363+ Define.Object< unit>(
364+ name = " Container" ,
365+ fields = [ Define.Field( " nested" , ListOf UNamed, fun _ () -> []) ])
366+
367+ let Query =
368+ Define.Object(
369+ " Query" ,
370+ [ Define.Field( " container" , ContainerType, fun _ () -> ()) ])
371+
372+ let schema = Schema( query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; RobotType ] })
373+ let schemaProcessor = Executor( schema)
374+
375+ // Nested query with non-matching fragment
376+ let query = """ query Example {
377+ container {
378+ nested {
379+ ... on Animal {
380+ name
381+ species
382+ }
383+ ... on Person {
384+ name
385+ age
386+ }
387+ ... on Robot {
388+ modelNumber
389+ }
390+ }
391+ }
392+ }"""
393+
394+ // Must succeed – nested non-matching fragments are valid per GraphQL spec
395+ let plan = schemaProcessor.CreateExecutionPlanOrFail( query)
396+
397+ // Verify the plan structure is correct
398+ equals 1 plan.Fields.Length
399+ plan.Fields.Head.Identifier |> equals " container"
400+
401+ [<Fact>]
402+ let ``Planning must return ResolveAbstraction even when all fragments are non - matching`` () =
403+ // REGRESSION TEST for Planning_ResolveDeferred_Bug (extreme case)
404+ //
405+ // GraphQL SCENARIO – EDGE CASE:
406+ // ==============================
407+ // What if ALL inline fragments in a query don't match the union?
408+ //
409+ // query {
410+ // items { # Union of [Animal, Person]
411+ // ... on Robot { } # Doesn't match
412+ // }
413+ // }
414+ //
415+ // THE PROBLEM:
416+ // ============
417+ // With only non-matching fragments, the planner has zero fields to plan.
418+ // This is the MOST EXTREME case of the bug.
419+ //
420+ // Expected GraphQL behavior: Valid query, returns empty result set
421+ // Bug behavior: Runtime crash during planning with "Expected an Abstraction!"
422+ //
423+ // WHY THIS HAPPENS:
424+ // =================
425+ // In real-world scenarios:
426+ // - Client queries for types that were removed from union
427+ // - Conditional fragments based on client-side logic
428+ // - Generic queries against multiple schema versions
429+ //
430+ // Per GraphQL spec: This MUST work – it's just a query that matches nothing.
431+
432+ // Robot is NOT in UNamed union
433+ let RobotType =
434+ Define.Object(
435+ name = " Robot" ,
436+ fields = [ Define.Field( " modelNumber" , StringType, fun _ ( robot : string ) -> robot) ])
437+
438+ let Query = Define.Object( " Query" , [ Define.Field( " names" , ListOf UNamed, fun _ () -> []) ])
439+ let schema = Schema( query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; RobotType ] })
440+ let schemaProcessor = Executor( schema)
441+
442+ // GraphQL Query – ONLY non-matching fragment!
443+ // UNamed union = Person | Animal (NOT Robot)
444+ // This query will match zero objects at runtime
445+ let query = """ query Example {
446+ names {
447+ ... on Robot {
448+ modelNumber
449+ }
450+ }
451+ }"""
452+
453+ // TEST ASSERTION:
454+ // Must succeed per GraphQL spec – empty result is valid, not an error
455+ // Bug would cause: Runtime crash "Expected an Abstraction!" during planning
456+ let plan = schemaProcessor.CreateExecutionPlanOrFail( query)
457+
458+ // Verify the plan was created successfully
459+ equals 1 plan.Fields.Length
460+ let listInfo = plan.Fields.Head
461+ let ( ResolveCollection ( info )) = listInfo.Kind
462+
463+ // Must successfully extract abstraction info
464+ let ( ResolveAbstraction ( innerFields )) = info.Kind
465+
466+ // Result: Empty map – no matching types
467+ // This is CORRECT per GraphQL spec – valid query, just matches nothing
468+ innerFields |> Map.isEmpty |> equals true
0 commit comments