X Tutup
Skip to content

This adds support for QueryAppliedDirective on operations and documents#4297

Open
bbakerman wants to merge 18 commits intomasterfrom
extract-out-operation-directives
Open

This adds support for QueryAppliedDirective on operations and documents#4297
bbakerman wants to merge 18 commits intomasterfrom
extract-out-operation-directives

Conversation

@bbakerman
Copy link
Member

@bbakerman bbakerman commented Mar 6, 2026

See #4295

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 11 5706 (+4 🟢) 5650 (+4 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 17 5706 (+4 🟢) 5649 (+4 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 21 5706 (+4 🟢) 5649 (+4 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 25 5706 (+4 🟢) 5649 (+4 🟢) 0 (±0) 0 (±0) 57 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 22856 (+16 🟢) 22629 (+16 🟢) 0 (±0) 0 (±0) 227 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 28773 3122 90.2% ±0.0%
Branches 8354 1508 84.7% ±0.0%
Methods 7698 1223 86.3% ±0.0%

Changed Class Coverage (7 classes)

Class Line Branch Method
g.e.d.DirectivesResolver ±0.0% +8.3% 🟢 ±0.0%
g.e.d.OperationDirectivesResolver +100.0% 🟢 +100.0% 🟢 +100.0% 🟢
g.e.Execution +0.3% 🟢 ±0.0% ±0.0%
g.e.ExecutionContext +0.1% 🟢 ±0.0% +0.2% 🟢
g.n.ExecutableNormalizedOperation +0.8% 🟢 ±0.0% +1.0% 🟢
g.n.ExecutableNormalizedOperationFactory -8.0% 🔴 ±0.0% -12.5% 🔴
g.v.OperationValidator ±0.0% +0.2% 🟢 ±0.0%
ExecutableNormalizedOperationFactory — method details
Method Line Branch
createExecutableNormalizedOperation 0.0% (-100.0% 🔴)

Full HTML report: build artifact jacoco-html-report

Updated: 2026-03-09 23:31:31 UTC

return directives.stream().map(this::toAppliedDirective).collect(ImmutableList.toImmutableList());
}

public QueryAppliedDirective toAppliedDirective(GraphQLDirective directive) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods used to be in QueryDirectivesImpl but I moved them here to be common and used now in two places


@Internal
@NullMarked
public class OperationDirectivesResolver {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new extracting class is Internal - exposed later via ExecutionContext and ENO

@bbakerman bbakerman changed the title WIP This adds support for QueryAppliedDirective on operations and documents This adds support for QueryAppliedDirective on operations and documents Mar 6, 2026
// preconditions
assertNotNull(executionId, "You must provide a query identifier");
return new ExecutionContext(this);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved build down to the bottom for code OCD reasons

commonIntegrationAsserts(resolveDirectives)

when:
def normalizedOperation = executionContext.getNormalizedQueryTree().get()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here the ENO has the same values

.inputValueWithState(argument.getArgumentValue())
.build();
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved methods so they can be re-used

.responseMapFactory().getOr(ResponseMapFactory.DEFAULT);

Supplier<Map<OperationDefinition, ImmutableList<QueryAppliedDirective>>> operationDirectives = FpKit.interThreadMemoize(() ->
operationDirectivesResolver.resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a memoizing supplier for performance reasons. if you never ask for the directives - no work is done - only the allocation of a interThreadMemoize object

this.executionInput = builder.executionInput;
this.dataLoaderDispatcherStrategy = builder.dataLoaderDispatcherStrategy;
this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables));
this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rejigged this code to a maker method


private Supplier<ExecutableNormalizedOperation> mkExecutableNormalizedOperation() {
return FpKit.interThreadMemoize(() -> {
Options options = Options.defaultOptions().graphQLContext(graphQLContext).locale(locale);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed a bug here - graphql context and locale was never respected before

assertNotNull(executionId, "You must provide a query identifier");
return new ExecutionContext(this);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move down

Copy link
Member

@andimarek andimarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import

import java.util.function.Consumer;
import java.util.function.Supplier;

import static graphql.normalized.ExecutableNormalizedOperationFactory.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import not correct :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@andimarek
Copy link
Member

andimarek commented Mar 9, 2026

Coverage Drop Analysis

The "Per-Class Coverage Gate" CI check is failing due to two classes showing coverage regressions. Here's a deep analysis of each:


1. QueryDirectivesImpl — Line: 97.3% (was 97.6%), Branch: 75.0% (was 80.0%), Method: 93.8% (was 94.4%)

Root cause: Code extraction shifted coverage ratios — no actual coverage was lost.

This PR moves two private methods (toAppliedDirective and toAppliedArgument, 18 source lines) from QueryDirectivesImpl to DirectivesResolver as public methods. The call site at line 83 changes from toAppliedDirective(resolvedDirective) to directivesResolver.toAppliedDirective(resolvedDirective).

These extracted methods were fully covered by existing tests. Removing well-covered lines from a class while the same 2 uncovered lines remain causes the coverage percentage to drop even though no test coverage was actually lost.

The math in detail

Baseline from test-baseline.json:

  • Line: 82 covered, 2 missed → total 84 → 82/84 = 97.6%
  • Branch: 8 covered, 2 missed → total 10 → 8/10 = 80.0%
  • Method: 17 covered, 1 missed → total 18 → 17/18 = 94.4%

What was removed — the two methods contained (as counted by JaCoCo):

  • ~10 executable lines, all covered
  • 2 branches (the for loop's empty-vs-nonempty paths in toAppliedDirective), all covered
  • 2 methods, all covered

After the PR — subtract the removed covered items from both numerator and denominator:

Metric Before Removed (all covered) After Reported
Line 82 / 84 = 97.6% 10 covered lines (82−10) / (84−10) = 72/74 = 97.3% 97.3% ✓
Branch 8 / 10 = 80.0% 2 covered branches (8−2) / (10−2) = 6/8 = 75.0% 75.0% ✓
Method 17 / 18 = 94.4% 2 covered methods (17−2) / (18−2) = 15/16 = 93.75% 93.8% ✓

Why does this happen? If a class has coverage C/T and you remove R items that were all covered:

New coverage = (C − R) / (T − R)

This is less than C/T whenever R/R > C/T, i.e. whenever 100% > C/T — which is true for any class not already at 100%. Removing perfectly-covered code from a not-perfectly-covered class always lowers the percentage, even though nothing became less tested. The 2 missed lines/branches now represent a larger fraction of a smaller total (2/74 instead of 2/84, 2/8 instead of 2/10).

This is a mathematical artifact of the ratio, not a real regression. The extracted code is now tested via DirectivesResolver and the new OperationDirectivesResolverTest.


2. ExecutableNormalizedOperationFactory (outer class) — Line: 84.0% (was 92.0%), Method: 75.0% (was 87.5%)

Root cause: The 4-arg convenience overload of createExecutableNormalizedOperation(schema, operationDefinition, fragments, coercedVars) (line 311-320) lost its only caller.

Previously, ExecutionContext constructed the normalized query tree by calling this 4-arg overload:

ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(
    graphQLSchema, operationDefinition, fragmentsByName, coercedVariables);

This PR changes ExecutionContext.mkExecutableNormalizedOperation() to call the 5-arg overload instead, passing Options with graphQLContext and locale:

Options options = Options.defaultOptions().graphQLContext(graphQLContext).locale(locale);
return createExecutableNormalizedOperation(
    graphQLSchema, operationDefinition, fragmentsByName, coercedVariables, options);

Integration tests that execute full queries go through ExecutionContext, which was the only path exercising the 4-arg (schema, OperationDefinition, fragments, coercedVars) overload. Direct unit tests in ExecutableNormalizedOperationFactoryTest use the (schema, Document, operationName, coercedVars, options) overload instead. This matches the numbers exactly:

Baseline: 23 covered, 2 missed lines (92.0%) / 7 covered, 1 missed method (87.5%)

Line:   (23−2) / (25+0) = 21/25 = 84.0%   ← 2 lines in the 4-arg method body lost coverage
Method: (7−1)  / (8+0)  = 6/8   = 75.0%   ← the 4-arg method itself is now uncovered

This is a real but trivial gap — the uncovered method is a one-line delegator to the 5-arg version. It could be fixed by adding a test that calls the 4-arg overload, or the baseline can be updated since the method is a trivial convenience wrapper.


Summary

Class Verdict Action needed
QueryDirectivesImpl False positive (refactor shifted ratios) Update baseline
ExecutableNormalizedOperationFactory Real but trivial (convenience overload lost its only caller) Update baseline or add a trivial test

https://claude.ai/code/session_01XVBWUo2VM4B82ssZm1f2mu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

X Tutup