Skip to content
Merged
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
72 changes: 56 additions & 16 deletions src/main/java/graphql/schema/GraphQLCodeRegistry.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graphql.schema;

import graphql.Assert;
import graphql.Internal;
import graphql.PublicApi;
import graphql.schema.visibility.GraphqlFieldVisibility;

Expand Down Expand Up @@ -189,7 +190,7 @@ public static class Builder {
private final Map<String, TypeResolver> typeResolverMap = new HashMap<>();
private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY;
private DataFetcherFactory<?> defaultDataFetcherFactory = env -> PropertyDataFetcher.fetching(env.getFieldDefinition().getName());

private boolean changed = false;

private Builder() {
}
Expand All @@ -202,6 +203,38 @@ private Builder(GraphQLCodeRegistry codeRegistry) {
this.defaultDataFetcherFactory = codeRegistry.defaultDataFetcherFactory;
}

/**

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This allows us to mark a code registry and know if it changes since we marked it.

* A helper method to track if the builder changes from the point
* at which this method was called.
*
* @return this builder for fluent code
*/
@Internal
public Builder trackChanges() {
changed = false;
return this;
}

/**
* @return true if the builder has changed since {@link #trackChanges()} was called
*/
@Internal
public boolean hasChanged() {
return changed;
}

private Builder markChanged() {
changed = true;
return this;
}

private Builder markChanged(boolean condition) {
if (condition) {
changed = true;
}
return this;
}

/**
* Returns a data fetcher associated with a field within a container type
*
Expand Down Expand Up @@ -309,7 +342,7 @@ public Builder systemDataFetcher(FieldCoordinates coordinates, DataFetcher<?> da
assertNotNull(coordinates);
coordinates.assertValidNames();
systemDataFetcherMap.put(coordinates.getFieldName(), DataFetcherFactories.useDataFetcher(dataFetcher));
return this;
return markChanged();
}

/**
Expand All @@ -329,7 +362,7 @@ public Builder dataFetcher(FieldCoordinates coordinates, DataFetcherFactory<?> d
} else {
dataFetcherMap.put(coordinates, dataFetcherFactory);
}
return this;
return markChanged();
}

/**
Expand All @@ -347,6 +380,7 @@ public Builder dataFetcherIfAbsent(FieldCoordinates coordinates, DataFetcher<?>
} else {
dataFetcher(coordinates, dataFetcher);
}
return markChanged();
}
return this;
}
Expand All @@ -362,7 +396,7 @@ public Builder dataFetcherIfAbsent(FieldCoordinates coordinates, DataFetcher<?>
public Builder dataFetchers(String parentTypeName, Map<String, DataFetcher<?>> fieldDataFetchers) {
assertNotNull(fieldDataFetchers);
fieldDataFetchers.forEach((fieldName, dataFetcher) -> dataFetcher(coordinates(parentTypeName, fieldName), dataFetcher));
return this;
return markChanged(!fieldDataFetchers.isEmpty());
}

/**
Expand All @@ -375,57 +409,63 @@ public Builder dataFetchers(String parentTypeName, Map<String, DataFetcher<?>> f
*/
public Builder defaultDataFetcher(DataFetcherFactory<?> defaultDataFetcherFactory) {
this.defaultDataFetcherFactory = Assert.assertNotNull(defaultDataFetcherFactory);
return this;
return markChanged();
}

public Builder dataFetchers(GraphQLCodeRegistry codeRegistry) {
this.dataFetcherMap.putAll(codeRegistry.dataFetcherMap);
return this;
return markChanged(!codeRegistry.dataFetcherMap.isEmpty());
}

public Builder typeResolver(GraphQLInterfaceType interfaceType, TypeResolver typeResolver) {
typeResolverMap.put(interfaceType.getName(), typeResolver);
return this;
return markChanged();
}

public Builder typeResolverIfAbsent(GraphQLInterfaceType interfaceType, TypeResolver typeResolver) {
typeResolverMap.putIfAbsent(interfaceType.getName(), typeResolver);
if (!typeResolverMap.containsKey(interfaceType.getName())) {
typeResolverMap.put(interfaceType.getName(), typeResolver);
return markChanged();
}
return this;
}

public Builder typeResolver(GraphQLUnionType unionType, TypeResolver typeResolver) {
typeResolverMap.put(unionType.getName(), typeResolver);
return this;
return markChanged();
}

public Builder typeResolverIfAbsent(GraphQLUnionType unionType, TypeResolver typeResolver) {
typeResolverMap.putIfAbsent(unionType.getName(), typeResolver);
return this;
if (!typeResolverMap.containsKey(unionType.getName())) {
typeResolverMap.put(unionType.getName(), typeResolver);
return markChanged();
}
return markChanged();
}

public Builder typeResolver(String typeName, TypeResolver typeResolver) {
typeResolverMap.put(assertValidName(typeName), typeResolver);
return this;
return markChanged();
}

public Builder typeResolvers(GraphQLCodeRegistry codeRegistry) {
this.typeResolverMap.putAll(codeRegistry.typeResolverMap);
return this;
return markChanged(!codeRegistry.typeResolverMap.isEmpty());
}

public Builder fieldVisibility(GraphqlFieldVisibility fieldVisibility) {
this.fieldVisibility = assertNotNull(fieldVisibility);
return this;
return markChanged();
}

public Builder clearDataFetchers() {
dataFetcherMap.clear();
return this;
return markChanged();
}

public Builder clearTypeResolvers() {
typeResolverMap.clear();
return this;
return markChanged();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

All the mutative methods mark the builder as changed

}

public GraphQLCodeRegistry build() {
Expand Down
28 changes: 16 additions & 12 deletions src/main/java/graphql/schema/SchemaTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ private Object transformImpl(final GraphQLSchema schema, GraphQLSchemaElement sc
final Map<String, GraphQLTypeReference> typeReferences = new LinkedHashMap<>();

// first pass - general transformation
traverseAndTransform(dummyRoot, changedTypes, typeReferences, visitor, codeRegistry);
boolean schemaChanged = traverseAndTransform(dummyRoot, changedTypes, typeReferences, visitor, codeRegistry);

// if we have changed any named elements AND we have type references referring to them then
// we need to make a second pass to replace these type references to the new names
Expand All @@ -152,9 +152,13 @@ private Object transformImpl(final GraphQLSchema schema, GraphQLSchemaElement sc
}

if (schema != null) {
GraphQLSchema graphQLSchema = dummyRoot.rebuildSchema(codeRegistry);
if (postTransformation != null) {
graphQLSchema = graphQLSchema.transform(postTransformation);

GraphQLSchema graphQLSchema = schema;
if (schemaChanged || codeRegistry.hasChanged()) {
graphQLSchema = dummyRoot.rebuildSchema(codeRegistry);
if (postTransformation != null) {
graphQLSchema = graphQLSchema.transform(postTransformation);
}

Copy link
Copy Markdown
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 BIG change - if the visitor didnt change anything and the code registry didn't change - then nothing has changed - its the same schema - so we return it direct

}
return graphQLSchema;
} else {
Expand All @@ -177,7 +181,7 @@ public TraversalControl visitGraphQLTypeReference(GraphQLTypeReference typeRef,
traverseAndTransform(dummyRoot, new HashMap<>(), new HashMap<>(), typeRefVisitor, codeRegistry);
}

private void traverseAndTransform(DummyRoot dummyRoot, Map<String, GraphQLNamedType> changedTypes, Map<String, GraphQLTypeReference> typeReferences, GraphQLTypeVisitor visitor, GraphQLCodeRegistry.Builder codeRegistry) {
private boolean traverseAndTransform(DummyRoot dummyRoot, Map<String, GraphQLNamedType> changedTypes, Map<String, GraphQLTypeReference> typeReferences, GraphQLTypeVisitor visitor, GraphQLCodeRegistry.Builder codeRegistry) {
List<NodeZipper<GraphQLSchemaElement>> zippers = new LinkedList<>();
Map<GraphQLSchemaElement, NodeZipper<GraphQLSchemaElement>> zipperByNodeAfterTraversing = new LinkedHashMap<>();
Map<GraphQLSchemaElement, NodeZipper<GraphQLSchemaElement>> zipperByOriginalNode = new LinkedHashMap<>();
Expand Down Expand Up @@ -268,16 +272,16 @@ public TraversalControl backRef(TraverserContext<GraphQLSchemaElement> context)

List<List<GraphQLSchemaElement>> stronglyConnectedTopologicallySorted = getStronglyConnectedComponentsTopologicallySorted(reverseDependencies, typeRefReverseDependencies);

zipUpToDummyRoot(zippers, stronglyConnectedTopologicallySorted, breadcrumbsByZipper, zipperByNodeAfterTraversing);
return zipUpToDummyRoot(zippers, stronglyConnectedTopologicallySorted, breadcrumbsByZipper, zipperByNodeAfterTraversing);
}


private void zipUpToDummyRoot(List<NodeZipper<GraphQLSchemaElement>> zippers,
List<List<GraphQLSchemaElement>> stronglyConnectedTopologicallySorted,
Map<NodeZipper<GraphQLSchemaElement>, List<List<Breadcrumb<GraphQLSchemaElement>>>> breadcrumbsByZipper,
Map<GraphQLSchemaElement, NodeZipper<GraphQLSchemaElement>> nodeToZipper) {
private boolean zipUpToDummyRoot(List<NodeZipper<GraphQLSchemaElement>> zippers,
List<List<GraphQLSchemaElement>> stronglyConnectedTopologicallySorted,
Map<NodeZipper<GraphQLSchemaElement>, List<List<Breadcrumb<GraphQLSchemaElement>>>> breadcrumbsByZipper,
Map<GraphQLSchemaElement, NodeZipper<GraphQLSchemaElement>> nodeToZipper) {
if (zippers.size() == 0) {
return;
return false;
}
Set<NodeZipper<GraphQLSchemaElement>> curZippers = new LinkedHashSet<>(zippers);

Expand Down Expand Up @@ -331,8 +335,8 @@ private void zipUpToDummyRoot(List<NodeZipper<GraphQLSchemaElement>> zippers,
breadcrumbsByZipper.put(newZipper, breadcrumbsForOriginalParent);

}

}
return true;
}

private Map<NodeZipper<GraphQLSchemaElement>, List<Breadcrumb<GraphQLSchemaElement>>> zipperWithSameParent(GraphQLSchemaElement parent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,35 @@ public SchemaDirectiveWiringSchemaGeneratorPostProcessing(TypeDefinitionRegistry

@Override
public GraphQLSchema process(GraphQLSchema originalSchema) {
GraphQLSchema newSchema = SchemaTransformer.transformSchema(originalSchema, new Visitor());
return newSchema.transform(builder -> {
// they could have changed the code registry so rebuild it
GraphQLCodeRegistry codeRegistry = this.codeRegistryBuilder.build();
builder.codeRegistry(codeRegistry);
});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We have two schema transformers here - one for a visitor that might not have done anything

And one because they MAY have changed the code registry.

Another way to fix this would be to have a way to change a schema's code registry safely (since its really a object swap in not a tree rebuild - but thats not this does

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This turned up in JProfiler as a heavily invoked bit of code in schema builds

codeRegistryBuilder.trackChanges();
Visitor visitor = new Visitor();
GraphQLSchema newSchema = SchemaTransformer.transformSchema(originalSchema, visitor);
if (visitor.schemaChanged() || codeRegistryBuilder.hasChanged()) {
return newSchema.transform(builder -> {
// they could have changed the code registry so rebuild it
GraphQLCodeRegistry codeRegistry = this.codeRegistryBuilder.build();
builder.codeRegistry(codeRegistry);
});
}
return newSchema;
}

public class Visitor extends GraphQLTypeVisitorStub {

private boolean schemaChanged = false;

public boolean schemaChanged() {
return schemaChanged;
}

private SchemaGeneratorDirectiveHelper.Parameters mkBehaviourParams() {
return new SchemaGeneratorDirectiveHelper.Parameters(typeRegistry, runtimeWiring, directiveBehaviourContext, codeRegistryBuilder);
}

private TraversalControl changOrContinue(GraphQLSchemaElement node, GraphQLSchemaElement newNode, TraverserContext<GraphQLSchemaElement> context) {
if (node != newNode) {
TreeTransformerUtil.changeNode(context, newNode);
schemaChanged = true;
}
return CONTINUE;
}
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/graphql/util/DefaultTraverserContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ public DefaultTraverserContext(T curNode,
if (parent == null || parent.isRootContext()) {
this.breadcrumbs = ImmutableKit.emptyList();
} else {
List<Breadcrumb<T>> breadcrumbs = new ArrayList<>(parent.getBreadcrumbs().size() + 1);
breadcrumbs.add(new Breadcrumb<>(this.parent.thisNode(), this.location));
breadcrumbs.addAll(parent.getBreadcrumbs());
this.breadcrumbs = ImmutableList.copyOf(breadcrumbs);
this.breadcrumbs = ImmutableList.<Breadcrumb<T>>builderWithExpectedSize(parent.getBreadcrumbs().size() + 1)
.add(new Breadcrumb<>(this.parent.thisNode(), this.location))
.addAll(parent.getBreadcrumbs())
.build();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is more efficient in memory allocation and faster to boot

}
}

Expand Down
46 changes: 46 additions & 0 deletions src/test/groovy/graphql/schema/GraphQLCodeRegistryTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,50 @@ class GraphQLCodeRegistryTest extends Specification {
codeRegistry.getDataFetcher(userCoords, userFieldDef) == dfUser

}

def "builder can track changes - internal methods"() {
DataFetcher<?> dfSystem = { env -> "system" }
DataFetcher<?> dfUser = { env -> "user" }
def systemFieldDef = newFieldDefinition().name("__system").type(GraphQLInt).build()
def userFieldDef = newFieldDefinition().name("field").type(GraphQLInt).build()
def systemCoords = FieldCoordinates.systemCoordinates(systemFieldDef.name)
def userCoords = FieldCoordinates.coordinates("User", userFieldDef.name)

when:
def codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
then:
!codeRegistry.hasChanged()

when:
codeRegistry.dataFetcher(systemCoords, dfSystem)
codeRegistry.dataFetcher(userCoords, dfUser)
then:
codeRegistry.hasChanged()

when:
codeRegistry.trackChanges()
then:
!codeRegistry.hasChanged()

when:
codeRegistry.clearDataFetchers()
then:
codeRegistry.hasChanged()

when:
def newCodeRegistry = GraphQLCodeRegistry.newCodeRegistry(codeRegistry.build())
then:
!newCodeRegistry.hasChanged()

when:
newCodeRegistry = GraphQLCodeRegistry.newCodeRegistry(codeRegistry.build())
then:
!newCodeRegistry.hasChanged()

when:
newCodeRegistry.dataFetcher(systemCoords, dfSystem as DataFetcher)
newCodeRegistry.dataFetcher(userCoords, dfUser as DataFetcher)
then:
newCodeRegistry.hasChanged()
}
}
19 changes: 19 additions & 0 deletions src/test/groovy/graphql/schema/SchemaTransformerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -744,4 +744,23 @@ type Query {
}
'''
}

def "if nothing changes in the schema transformer, we return the same object"() {

def schema = TestUtil.schema("type Query { f : String }")

def fieldChanger = new GraphQLTypeVisitorStub() {

@Override
TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node,
TraverserContext<GraphQLSchemaElement> context) {
return TraversalControl.CONTINUE
}
}

when:
def newSchema = SchemaTransformer.transformSchema(schema, fieldChanger)
then:
newSchema === schema
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class SchemaGeneratorDirectiveHelperTest extends Specification {
})
.build()

def assertCallHierarchy(elementHierarchy, astHierarchy, String name, List<String> l) {
static def assertCallHierarchy(elementHierarchy, astHierarchy, String name, List<String> l) {
assert elementHierarchy[name] == l, "unexpected elementHierarchy"
assert astHierarchy[name] == l, "unexpected astHierarchy"
true
Expand Down
Loading