🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@bbakerman
Copy link
Member

This is related to #4034 but supersedes it. This is more performant than @xuorig original PR.

This iterates the list of type extensions (for a type kind) only once to check if fields are unique

Before this iterated ALL type extensions (for a type kind) multiple times (once per type extension)

@bbakerman bbakerman added this to the 25.x breaking changes milestone Aug 4, 2025
forEachBut(extension, extensions,
otherTypeExt -> checkForFieldRedefinition(errors, otherTypeExt, otherTypeExt.getFieldDefinitions(), fieldDefinitions));

//
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 code here is inside a loop already - so

  • for every type extentions
    • do this check where we loop every type extension

This created a N*M loop

extensions,
ObjectTypeDefinition::getFieldDefinitions
);

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 code is OUTSIDE the extentions loop and the new checkForTypeExtensionFieldUniqueness loops extensions only once for field unique names so its order N

errors,
extensions,
InterfaceTypeDefinition::getFieldDefinitions
);
Copy link
Member Author

Choose a reason for hiding this comment

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

same but for interfaces

errors,
extensions,
InputObjectTypeDefinition::getInputValueDefinitions
);
Copy link
Member Author

Choose a reason for hiding this comment

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

some but for input types

@github-actions
Copy link
Contributor

github-actions bot commented Aug 4, 2025

Test Results

  325 files   -    650    325 suites   - 650   3m 31s ⏱️ - 7m 5s
5 020 tests  -    345  5 014 ✅  -    346  6 💤 + 1  0 ❌ ±0 
5 109 runs   - 10 215  5 103 ✅  - 10 204  6 💤  - 11  0 ❌ ±0 

Results for commit 1001ed4. ± Comparison against base commit 0b5c762.

This pull request removes 517 and adds 148 tests. Note that renamed tests count towards both.
	?

	, expected: combo-\"\\\b\f\n\r\t, #4]
                __schema { types { fields { args { type { name fields { name }}}}}}
                __schema { types { fields { type { name fields { name }}}}}
                __schema { types { inputFields { type { inputFields { name }}}}}
                __schema { types { interfaces { fields { type { interfaces { name } } } } } }
                __schema { types { name} }
                __type(name : "t") { name }
                a1: __schema { types { name} }
                a1: __type(name : "t") { name }
…
graphql.ScalarsBooleanTest ‑ parseValue throws exception for invalid input <java.lang.Object@1121ea0a>
graphql.ScalarsBooleanTest ‑ serialize throws exception for invalid input <java.lang.Object@8364f0e>
graphql.ScalarsIDTest ‑ parseValue allows any object via String.valueOf <java.lang.Object@75a5ff46>
graphql.ScalarsIDTest ‑ serialize allows any object via String.valueOf <java.lang.Object@2278792c>
graphql.ScalarsIntTest ‑ parseValue throws exception for invalid input <java.lang.Object@d65690b>
graphql.ScalarsIntTest ‑ serialize throws exception for invalid input <java.lang.Object@354c3e18>
graphql.analysis.QueryTraverserTest ‑ builder doesn't allow null arguments [document: Document{definitions=[OperationDefinition{name='null', operation=QUERY, variableDefinitions=[], directives=graphql.language.NodeUtil$DirectivesHolder@5721cd1b, selectionSet=SelectionSet{selections=[Field{name='foo', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@e647858, selectionSet=null}]}}]}, operationName: foo, root: Field{name='null', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@511da368, selectionSet=null}, rootParentType: null, fragmentsByName: [:], #3]
graphql.analysis.QueryTraverserTest ‑ builder doesn't allow null arguments [document: Document{definitions=[OperationDefinition{name='null', operation=QUERY, variableDefinitions=[], directives=graphql.language.NodeUtil$DirectivesHolder@682fbf03, selectionSet=SelectionSet{selections=[Field{name='foo', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@6c926fd9, selectionSet=null}]}}]}, operationName: foo, root: Field{name='null', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@17baf1d6, selectionSet=null}, rootParentType: null, fragmentsByName: null, #1]
graphql.analysis.QueryTraverserTest ‑ builder doesn't allow null arguments [document: Document{definitions=[OperationDefinition{name='null', operation=QUERY, variableDefinitions=[], directives=graphql.language.NodeUtil$DirectivesHolder@6ceca8ff, selectionSet=SelectionSet{selections=[Field{name='foo', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@42aa3ded, selectionSet=null}]}}]}, operationName: null, root: Field{name='null', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@4e8faebb, selectionSet=null}, rootParentType: null, fragmentsByName: null, #0]
graphql.analysis.QueryTraverserTest ‑ builder doesn't allow null arguments [document: Document{definitions=[OperationDefinition{name='null', operation=QUERY, variableDefinitions=[], directives=graphql.language.NodeUtil$DirectivesHolder@7d048304, selectionSet=SelectionSet{selections=[Field{name='foo', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@68c6fc00, selectionSet=null}]}}]}, operationName: foo, root: Field{name='null', alias='null', arguments=[], directives=graphql.language.NodeUtil$DirectivesHolder@1755ba66, selectionSet=null}, rootParentType: Mock for type 'GraphQLObjectType', fragmentsByName: null, #2]
…
This pull request skips 1 test.
graphql.schema.fetching.LambdaFetchingSupportTest ‑ different class loaders induce certain behaviours

♻️ This comment has been updated with latest results.

@dondonz dondonz added the performance work that is primarily targeted as performance improvements label Aug 4, 2025
@bbakerman
Copy link
Member Author

I just looked into graphql-js to see how they do it and they dont do "extends type" validation at "node -> fields creation time"

function buildFieldMap(
nodes: ReadonlyArray<
| InterfaceTypeDefinitionNode
| InterfaceTypeExtensionNode
| ObjectTypeDefinitionNode
| ObjectTypeExtensionNode
>,
): GraphQLFieldConfigMap<unknown, unknown> {
const fieldConfigMap = Object.create(null);
for (const node of nodes) {
const nodeFields = node.fields ?? [];

  for (const field of nodeFields) {
    fieldConfigMap[field.name.value] = {
      // Note: While this could make assertions to get the correctly typed
      // value, that would throw immediately while type system validation
      // with validateSchema() will produce more actionable results.
      type: getWrappedType(field.type),
      description: field.description?.value,
      args: buildArgumentMap(field.arguments),
      deprecationReason: getDeprecationReason(field),
      astNode: field,
    };
  }
}
return fieldConfigMap;

}
So they build and invalid type (via extends) and then do validation on the runtime schema - eg field redefinition later

This might be faster but its likely less precise - eg which extend type led to the error say. Maybe this is not true.

The spec is not precise here

https://spec.graphql.org/October2021/#sec-Object-Extensions

Object type extensions have the potential to be invalid if incorrectly defined.

The named type must already be defined and must be an Object type.
The fields of an Object type extension must have unique names; no two fields may share the same name.
Any fields of an Object type extension must not be already defined on the original Object type.
Any non-repeatable directives provided must not already apply to the original Object type.
Any interfaces provided must not be already implemented by the original Object type.
The resulting extended object type must be a super-set of all interfaces it implements.
The fields of an Object type extension must have unique names; no two fields may share the same name.

Does this mean unique with a single extend type node or with all extend type nodes? I guess its all nodes since this is how we are treating it today (via our repeated looping)

…tensions - for completeness did enums as well
expect:

!result.isEmpty()
result.size() == 4
Copy link
Member

Choose a reason for hiding this comment

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

I was curious what the extra error was: it is a field redefinition error, not a NonUniqueNameError like the others in this list.

'Query' extension type [@8:13] tried to redefine field 'fieldB' [@10:17]

Copy link
Member

Choose a reason for hiding this comment

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

This is fine to have another error, it is correct anyway

Copy link
Member

@dondonz dondonz left a comment

Choose a reason for hiding this comment

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

Nice work

…ion-uniqueness-perf

# Conflicts:
#	src/main/java/graphql/schema/idl/SchemaTypeExtensionsChecker.java
@bbakerman bbakerman merged commit 6267d65 into master Aug 12, 2025
2 checks passed
@dondonz dondonz deleted the xuorig-field-extension-uniqueness-perf branch August 16, 2025 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance work that is primarily targeted as performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants