Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e105a49
Optimize assertValidName by replacing regex with character-by-charact…
Dec 11, 2025
cadb12a
Phase 1: Add FastBuilder scaffolding with scalar type support
Dec 6, 2025
0f81a9c
Phase 2: Add directive support with type reference resolution
Dec 6, 2025
e39bf20
Phase 3: Add enumeration type support
Dec 6, 2025
e473ca8
Phase 4: Add input object type support with type reference resolution
Dec 6, 2025
d09a763
Phase 5: Add applied directive support with type reference resolution
Dec 6, 2025
798c7e1
Phase 6: Object Types - FastBuilder implementation
Dec 6, 2025
ea5d3cc
Phase 7: Interface Types - FastBuilder implementation
Dec 6, 2025
4472f7c
Phase 8: Union Types - FastBuilder implementation
Dec 6, 2025
e12c074
Phase 9: Validation and Edge Cases - FastBuilder implementation
Dec 6, 2025
b7e9e46
Add FastSchemaGenerator and JMH benchmark infrastructure
Dec 20, 2025
388ab73
Address review comment, change a name
Jan 11, 2026
62815b1
Tighter type-bounds for GraphQLSchema.additionalTypes.
Jan 20, 2026
1c54eae
Remove detached-type detection: all types are additionalTypes
Jan 20, 2026
46e2e9b
Merge remote-tracking branch 'origin/master' into fastbuilder
andimarek Jan 22, 2026
7cf12cd
Remove phase separator comments from FastBuilder tests
andimarek Jan 22, 2026
c446304
Fix FastBuilder Javadoc comments
andimarek Jan 22, 2026
16a8fc2
Change FastBuilder.addType to accept GraphQLNamedType
andimarek Jan 22, 2026
d430e66
Fixes
Jan 23, 2026
a15baf6
Refactor built-in directive handling
Jan 25, 2026
eb8e24c
Fix withSchemaAppliedDirectives(Collection) not scanning for type ref…
andimarek Jan 28, 2026
86da667
Merge remote-tracking branch 'upstream/master' into fastbuilder
Jan 29, 2026
719f91f
Remove cruft
Jan 29, 2026
14a31e4
Improve FastBuilder documentation and add @NullMarked
andimarek Jan 29, 2026
cd32b5c
Remove redundant checks in assertValidName, mark param @Nullable
andimarek Jan 29, 2026
131f169
Promote FastSchemaGenerator to @ExperimentalApi with safer defaults
Jan 30, 2026
23c59ae
Add type resolver validation to FastBuilder
Jan 30, 2026
5bcba4a
Merge remote-tracking branch 'upstream/master' into fastbuilder
Jan 30, 2026
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
Prev Previous commit
Next Next commit
Phase 9: Validation and Edge Cases - FastBuilder implementation
- Fixed additionalTypes not being set in FastBuilder path:
  - Added additionalTypes parameter to FastBuilder private constructor
  - Added buildAdditionalTypes() method to compute additionalTypes from typeMap
  - Validation now properly traverses all types in the schema
- Added comprehensive tests for validation and edge cases:
  - withValidation(false) skips validation
  - withValidation(true) runs validation and catches errors
  - Circular type reference resolution
  - Deeply nested type reference resolution (NonNull + List + NonNull)
  - Complex schema with interfaces, unions, input types
  - Null handling for types/directives
  - Built-in directives are added automatically

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
Raymie Stata and claude committed Jan 20, 2026
commit e12c0742709177e45a34e1ddb008259da7d74693
144 changes: 53 additions & 91 deletions src/main/java/graphql/schema/GraphQLSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import graphql.AssertException;
import graphql.Directives;
import graphql.DirectivesUtil;
import graphql.ExperimentalApi;
import graphql.Internal;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import graphql.introspection.Introspection;
import graphql.language.SchemaDefinition;
import graphql.language.SchemaExtensionDefinition;
import graphql.schema.impl.FindDetachedTypes;
import graphql.schema.impl.GraphQLTypeCollectingVisitor;
import graphql.schema.impl.SchemaUtil;
import graphql.schema.validation.InvalidSchemaException;
Expand All @@ -30,7 +32,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;

import static graphql.Assert.assertNotNull;
Expand Down Expand Up @@ -166,34 +167,48 @@ public GraphQLSchema(BuilderWithoutTypes builder) {
}

/**
* Constructor for FastBuilder - assembles the schema from precomputed fields.
* Performs NO traversals, NO reference replacement, and NO validation.
* Private constructor for FastBuilder that copies data from the builder
* and converts mutable collections to immutable ones.
*/
@Internal
private GraphQLSchema(FastBuilder fastBuilder,
ImmutableMap<String, GraphQLNamedType> typeMap,
ImmutableList<GraphQLDirective> directives,
ImmutableList<GraphQLDirective> schemaDirectives,
ImmutableList<GraphQLAppliedDirective> schemaAppliedDirectives,
ImmutableMap<String, ImmutableList<GraphQLObjectType>> interfaceNameToObjectTypes,
ImmutableMap<String, ImmutableList<String>> interfaceNameToObjectTypeNames,
GraphQLCodeRegistry codeRegistry) {
private GraphQLSchema(FastBuilder fastBuilder) {
// Build immutable collections from FastBuilder's mutable state
ImmutableMap<String, GraphQLNamedType> finalTypeMap = ImmutableMap.copyOf(fastBuilder.typeMap);
ImmutableList<GraphQLDirective> finalDirectives = ImmutableList.copyOf(fastBuilder.directiveMap.values());

// Get interface-to-object-type-names map from collector (already sorted)
ImmutableMap<String, ImmutableList<String>> finalInterfaceNameMap =
fastBuilder.shallowTypeRefCollector.getInterfaceNameToObjectTypeNames();

// Build interface-to-object-types map by looking up types in typeMap
ImmutableMap.Builder<String, ImmutableList<GraphQLObjectType>> interfaceMapBuilder = ImmutableMap.builder();
for (Map.Entry<String, ImmutableList<String>> entry : finalInterfaceNameMap.entrySet()) {
ImmutableList<GraphQLObjectType> objectTypes = map(entry.getValue(),
name -> (GraphQLObjectType) finalTypeMap.get(name));
interfaceMapBuilder.put(entry.getKey(), objectTypes);
}
ImmutableMap<String, ImmutableList<GraphQLObjectType>> finalInterfaceMap = interfaceMapBuilder.build();

// Initialize all fields
this.queryType = fastBuilder.queryType;
this.mutationType = fastBuilder.mutationType;
this.subscriptionType = fastBuilder.subscriptionType;
this.introspectionSchemaType = fastBuilder.introspectionSchemaType;
this.additionalTypes = ImmutableSet.of(); // Not used in FastBuilder path
this.additionalTypes = ImmutableSet.copyOf(FindDetachedTypes.findDetachedTypes(
finalTypeMap, fastBuilder.queryType, fastBuilder.mutationType, fastBuilder.subscriptionType, finalDirectives));
this.introspectionSchemaField = Introspection.buildSchemaField(fastBuilder.introspectionSchemaType);
this.introspectionTypeField = Introspection.buildTypeField(fastBuilder.introspectionSchemaType);
this.directiveDefinitionsHolder = new DirectivesUtil.DirectivesHolder(directives, emptyList());
this.schemaAppliedDirectivesHolder = new DirectivesUtil.DirectivesHolder(schemaDirectives, schemaAppliedDirectives);
this.directiveDefinitionsHolder = new DirectivesUtil.DirectivesHolder(finalDirectives, emptyList());
this.schemaAppliedDirectivesHolder = new DirectivesUtil.DirectivesHolder(
ImmutableList.copyOf(fastBuilder.schemaDirectives),
ImmutableList.copyOf(fastBuilder.schemaAppliedDirectives));
this.definition = fastBuilder.definition;
this.extensionDefinitions = nonNullCopyOf(fastBuilder.extensionDefinitions);
this.description = fastBuilder.description;
this.codeRegistry = codeRegistry;
this.typeMap = typeMap;
this.interfaceNameToObjectTypes = interfaceNameToObjectTypes;
this.interfaceNameToObjectTypeNames = interfaceNameToObjectTypeNames;
this.codeRegistry = fastBuilder.codeRegistryBuilder.build();
this.typeMap = finalTypeMap;
this.interfaceNameToObjectTypes = finalInterfaceMap;
this.interfaceNameToObjectTypeNames = finalInterfaceNameMap;
}

private static GraphQLDirective[] schemaDirectivesArray(GraphQLSchema existingSchema) {
Expand Down Expand Up @@ -1042,26 +1057,24 @@ private GraphQLSchema validateSchema(GraphQLSchema graphQLSchema) {

/**
* A high-performance schema builder that avoids all full-schema traversals performed by
* {@link GraphQLSchema.Builder#build()}. It is intended for constructing schemas, especially
* large or deeply nested schemas.
* <p>
* FastBuilder is an "expert mode" builder with stricter assumptions:
* {@link GraphQLSchema.Builder#build()}. This builder is both significantly faster and
* allocates significantly less memory than the standard GraphQLSchema.Builder - however
* it's subject to limitations listed below. It is intended for constructing large
* schemas, especially deeply nested ones.
*
* <p> Use FastBuilder when:
* <ul>
* <li>No clearing/resetting of types or directives</li>
* <li>All named types must be explicitly provided via {@link #additionalType(GraphQLType)}</li>
* <li>Incremental work only: when a type or directive is added, only that node is examined</li>
* <li>No schema traversals except shallow scans for local type references</li>
* <li>Validation is optional, off by default</li>
* <li>Building large schemas (500+ types) where construction time and memory are measurable</li>
* <li>All types are known without traversal and can be added explicitly with addAdditionalType(s)</li>
* <li>There's no need to clear/reset the builder state midstream.</li>
* <li>The code registry builder is complete and available when FastBuilder is constructed</li>
* </ul>
* <p>
* Performance is gained by eliminating:
* <ol>
* <li>Full traversal for code-registry wiring</li>
* <li>Full traversal for type-reference replacement</li>
* <li>Full traversal for validation (if disabled)</li>
* </ol>
* FastBuilder also can optionally skip schema validation, which can save time and
* memory for large schemas that have been previously validated (eg, in build tool chains).
*
* @see GraphQLSchema.Builder for standard schema construction
*/
@PublicApi
@ExperimentalApi
@NullMarked
public static final class FastBuilder {
// Fields consumed by the private constructor
Expand All @@ -1073,7 +1086,6 @@ public static final class FastBuilder {
private final Map<String, GraphQLDirective> directiveMap = new LinkedHashMap<>();
private final List<GraphQLDirective> schemaDirectives = new ArrayList<>();
private final List<GraphQLAppliedDirective> schemaAppliedDirectives = new ArrayList<>();
private final Map<String, List<GraphQLObjectType>> interfacesToImplementations = new LinkedHashMap<>();
private @Nullable String description;
private @Nullable SchemaDefinition definition;
private @Nullable List<SchemaExtensionDefinition> extensionDefinitions;
Expand Down Expand Up @@ -1147,18 +1159,9 @@ public FastBuilder additionalType(GraphQLType type) {
// Insert into typeMap
typeMap.put(name, namedType);

// Shallow scan via ShallowTypeRefCollector
// Shallow scan via ShallowTypeRefCollector (also tracks interface implementations)
shallowTypeRefCollector.handleTypeDef(namedType);

// For object types, update interface→implementations map
if (namedType instanceof GraphQLObjectType) {
GraphQLObjectType objectType = (GraphQLObjectType) namedType;
for (GraphQLNamedOutputType iface : objectType.getInterfaces()) {
String interfaceName = iface.getName();
interfacesToImplementations.computeIfAbsent(interfaceName, k -> new ArrayList<>()).add(objectType);
}
}

// For interface types, wire type resolver if present
if (namedType instanceof GraphQLInterfaceType) {
GraphQLInterfaceType interfaceType = (GraphQLInterfaceType) namedType;
Expand Down Expand Up @@ -1352,29 +1355,17 @@ public GraphQLSchema build() {
// Step 2: Add built-in directives if missing
addBuiltInDirectivesIfMissing();

// Step 3: Build final immutable objects
ImmutableMap<String, GraphQLNamedType> finalTypeMap = buildSortedImmutableTypeMap();
ImmutableList<GraphQLDirective> finalDirectives = buildImmutableDirectives();
ImmutableList<GraphQLDirective> finalSchemaDirectives = ImmutableList.copyOf(schemaDirectives);
ImmutableList<GraphQLAppliedDirective> finalSchemaAppliedDirectives = ImmutableList.copyOf(schemaAppliedDirectives);
ImmutableMap<String, ImmutableList<GraphQLObjectType>> finalInterfaceMap = buildImmutableInterfaceMap();
ImmutableMap<String, ImmutableList<String>> finalInterfaceNameMap = buildInterfaceNameMap(finalInterfaceMap);
GraphQLCodeRegistry finalCodeRegistry = codeRegistryBuilder.build();

// Step 4: Create schema via private constructor
GraphQLSchema schema = new GraphQLSchema(this, finalTypeMap, finalDirectives,
finalSchemaDirectives, finalSchemaAppliedDirectives,
finalInterfaceMap, finalInterfaceNameMap, finalCodeRegistry);

// Step 5: Optional validation
// Step 3: Create schema via private constructor
GraphQLSchema schema = new GraphQLSchema(this);

// Step 4: Optional validation
if (validationEnabled) {
Collection<SchemaValidationError> errors = new SchemaValidator().validateSchema(schema);
if (!errors.isEmpty()) {
throw new InvalidSchemaException(errors);
}
}

// Step 6: Return
return schema;
}

Expand All @@ -1392,34 +1383,5 @@ private void addDirectiveIfMissing(GraphQLDirective directive) {
directiveMap.put(directive.getName(), directive);
}
}

private ImmutableMap<String, GraphQLNamedType> buildSortedImmutableTypeMap() {
TreeMap<String, GraphQLNamedType> sorted = new TreeMap<>(typeMap);
return ImmutableMap.copyOf(sorted);
}

private ImmutableList<GraphQLDirective> buildImmutableDirectives() {
return ImmutableList.copyOf(directiveMap.values());
}

private ImmutableMap<String, ImmutableList<GraphQLObjectType>> buildImmutableInterfaceMap() {
ImmutableMap.Builder<String, ImmutableList<GraphQLObjectType>> builder = ImmutableMap.builder();
for (Map.Entry<String, List<GraphQLObjectType>> entry : interfacesToImplementations.entrySet()) {
ImmutableList<GraphQLObjectType> sortedObjectTypes = ImmutableList.copyOf(
sortTypes(byNameAsc(), entry.getValue()));
builder.put(entry.getKey(), sortedObjectTypes);
}
return builder.build();
}

private ImmutableMap<String, ImmutableList<String>> buildInterfaceNameMap(
ImmutableMap<String, ImmutableList<GraphQLObjectType>> interfaceMap) {
ImmutableMap.Builder<String, ImmutableList<String>> builder = ImmutableMap.builder();
for (Map.Entry<String, ImmutableList<GraphQLObjectType>> entry : interfaceMap.entrySet()) {
ImmutableList<String> objectTypeNames = map(entry.getValue(), GraphQLObjectType::getName);
builder.put(entry.getKey(), objectTypeNames);
}
return builder.build();
}
}
}
30 changes: 29 additions & 1 deletion src/main/java/graphql/schema/ShallowTypeRefCollector.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package graphql.schema;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import graphql.AssertException;
import graphql.Internal;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

/**
* Collects type-refs found in type- and directive-definitions for later replacement with actual types.
* This class performs shallow scans (no recursive traversal from one type-def to another) and
* collects replacement targets that need their type references resolved.
* Also tracks interface-to-implementation relationships.
*/
@Internal
public class ShallowTypeRefCollector {
Expand All @@ -22,6 +27,9 @@ public class ShallowTypeRefCollector {
// GraphQLAppliedDirectiveArgument
private final List<Object> replaceTargets = new ArrayList<>();

// Track interface implementations: interface name -> sorted set of implementing object type names
private final Map<String, TreeSet<String>> interfaceToObjectTypeNames = new LinkedHashMap<>();

/**
* Scan a type definition for type references.
* Called on GraphQL{Object|Input|Scalar|Union|etc}Type - NOT on wrappers or type-refs.
Expand Down Expand Up @@ -58,7 +66,13 @@ private void handleObjectType(GraphQLObjectType objectType) {
// Scan applied directives on field
scanAppliedDirectives(field.getAppliedDirectives());
}
// Scan interfaces for type references
// Track interface implementations and scan for type references
for (GraphQLNamedOutputType iface : objectType.getInterfaces()) {
String interfaceName = iface.getName();
interfaceToObjectTypeNames
.computeIfAbsent(interfaceName, k -> new TreeSet<>())
.add(objectType.getName());
}
if (hasInterfaceTypeReferences(objectType.getInterfaces())) {
replaceTargets.add(new ObjectInterfaceReplaceTarget(objectType));
}
Expand Down Expand Up @@ -390,4 +404,18 @@ private GraphQLInputType resolveInputType(GraphQLInputType type, Map<String, Gra
// Already a concrete type, return as-is
return type;
}

/**
* Returns an immutable map of interface names to sorted lists of implementing object type names.
* The object type names are maintained in sorted order as they're added.
*
* @return immutable map from interface name to list of object type names
*/
public ImmutableMap<String, ImmutableList<String>> getInterfaceNameToObjectTypeNames() {
ImmutableMap.Builder<String, ImmutableList<String>> builder = ImmutableMap.builder();
for (Map.Entry<String, TreeSet<String>> entry : interfaceToObjectTypeNames.entrySet()) {
builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
}
return builder.build();
}
}
Loading