MergedField.java
package graphql.execution;
import com.google.common.collect.ImmutableList;
import graphql.ExperimentalApi;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import graphql.execution.incremental.DeferredExecution;
import graphql.language.Argument;
import graphql.language.Field;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import static graphql.Assert.assertNotEmpty;
import static graphql.Assert.assertNotNull;
/**
* This represents all Fields in a query which overlap and are merged into one.
* This means they all represent the same field actually when the query is executed.
* <p>
* Example query with more than one Field merged together:
*
* <pre>
* {@code
*
* query Foo {
* bar
* ...BarFragment
* }
*
* fragment BarFragment on Query {
* bar
* }
* }
* </pre>
*
* Another example:
* <pre>
* {@code
* {
* me{fistName}
* me{lastName}
* }
* }
* </pre>
*
* Here the field is merged together including the sub selections.
* <p>
* A third example with different directives:
* <pre>
* {@code
* {
* foo @someDirective
* foo @anotherDirective
* }
* }
* </pre>
* These examples make clear that you need to consider all merged fields together to have the full picture.
* <p>
* The actual logic when fields can be successfully merged together is implemented in {#graphql.validation.rules.OverlappingFieldsCanBeMerged}
*/
@PublicApi
@NullMarked
public class MergedField {
private final Field singleField;
private final ImmutableList<DeferredExecution> deferredExecutions;
private MergedField(Field field, ImmutableList<DeferredExecution> deferredExecutions) {
this.singleField = field;
this.deferredExecutions = deferredExecutions;
}
/**
* All merged fields have the same name.
* <p>
* WARNING: This is not always the key in the execution result, because of possible aliases. See {@link #getResultKey()}
*
* @return the name of the merged fields.
*/
public String getName() {
return singleField.getName();
}
/**
* Returns the key of this MergedField for the overall result.
* This is either an alias or the field name.
*
* @return the key for this MergedField.
*/
public String getResultKey() {
return singleField.getResultKey();
}
/**
* The first of the merged fields.
* <p>
* Because all fields are almost identically
* often only one of the merged fields are used.
*
* @return the fist of the merged Fields
*/
public Field getSingleField() {
return singleField;
}
/**
* All merged fields share the same arguments.
*
* @return the list of arguments
*/
public List<Argument> getArguments() {
return singleField.getArguments();
}
/**
* All merged fields
*
* @return all merged fields
*/
public List<Field> getFields() {
return ImmutableList.of(singleField);
}
/**
* @return how many fields are in this merged field
*/
public int getFieldsCount() {
return 1;
}
/**
* @return true if the field has a sub selection
*/
public boolean hasSubSelection() {
return singleField.getSelectionSet() != null;
}
/**
* @return true if this {@link MergedField} represents a single {@link Field} in the operation
*/
public boolean isSingleField() {
return true;
}
/**
* Get a list of all {@link DeferredExecution}s that this field is part of
*
* @return all defer executions.
*/
@ExperimentalApi
public List<DeferredExecution> getDeferredExecutions() {
return deferredExecutions;
}
/**
* Returns true if this field is part of a deferred execution
*
* @return true if this field is part of a deferred execution
*/
@ExperimentalApi
public boolean isDeferred() {
return !deferredExecutions.isEmpty();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MergedField that = (MergedField) o;
return this.singleField.equals(that.singleField);
}
@Override
public int hashCode() {
return Objects.hashCode(singleField);
}
@Override
public String toString() {
return "MergedField{" +
"field(s)=" + singleField +
'}';
}
/**
* This is an important method because it creates a new MergedField from the existing one without using a builder
* to save memory.
*
* @param field the new field to add to the current merged field
* @param deferredExecution the deferred execution
*
* @return a new {@link MergedField} instance
*/
MergedField newMergedFieldWith(Field field, @Nullable DeferredExecution deferredExecution) {
ImmutableList<DeferredExecution> deferredExecutions = mkDeferredExecutions(deferredExecution);
ImmutableList<Field> fields = ImmutableList.of(singleField, field);
return new MultiMergedField(fields, deferredExecutions);
}
ImmutableList<DeferredExecution> mkDeferredExecutions(@Nullable DeferredExecution deferredExecution) {
ImmutableList<DeferredExecution> deferredExecutions = this.deferredExecutions;
if (deferredExecution != null) {
deferredExecutions = ImmutableKit.addToList(deferredExecutions, deferredExecution);
}
return deferredExecutions;
}
/**
* Most of the time we have a single field inside a MergedField but when we need more than one field
* represented then this {@link MultiMergedField} is used
*/
static final class MultiMergedField extends MergedField {
private final ImmutableList<Field> fields;
MultiMergedField(ImmutableList<Field> fields, ImmutableList<DeferredExecution> deferredExecutions) {
super(fields.get(0), deferredExecutions);
this.fields = fields;
}
@Override
public List<Field> getFields() {
return fields;
}
@Override
public boolean hasSubSelection() {
for (Field field : this.fields) {
if (field.getSelectionSet() != null) {
return true;
}
}
return false;
}
@Override
public int getFieldsCount() {
return fields.size();
}
@Override
public boolean isSingleField() {
return fields.size() == 1;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MultiMergedField that = (MultiMergedField) o;
return fields.equals(that.fields);
}
@Override
public int hashCode() {
return Objects.hashCode(fields);
}
@Override
public String toString() {
return "MultiMergedField{" +
"field(s)=" + fields +
'}';
}
@Override
public void forEach(Consumer<Field> fieldConsumer) {
fields.forEach(fieldConsumer);
}
@Override
MergedField newMergedFieldWith(Field field, @Nullable DeferredExecution deferredExecution) {
ImmutableList<DeferredExecution> deferredExecutions = mkDeferredExecutions(deferredExecution);
ImmutableList<Field> fields = ImmutableKit.addToList(this.fields, field);
return new MultiMergedField(fields, deferredExecutions);
}
}
public static Builder newMergedField() {
return new Builder();
}
public static Builder newMergedField(Field field) {
return new Builder().addField(field);
}
public static Builder newMergedField(List<Field> fields) {
return new Builder().fields(fields);
}
/**
* This is an important method in that it creates a MergedField direct without the list and without a builder and hence
* saves some micro memory in not allocating a list of 1
*
* @param field the field to wrap
* @param deferredExecution the deferred execution
*
* @return a new {@link MergedField}
*/
static MergedField newSingletonMergedField(Field field, @Nullable DeferredExecution deferredExecution) {
return new MergedField(field, deferredExecution == null ? ImmutableList.of() : ImmutableList.of(deferredExecution));
}
public MergedField transform(Consumer<Builder> builderConsumer) {
Builder builder = new Builder(this);
builderConsumer.accept(builder);
return builder.build();
}
/**
* Runs a consumer for each field
*
* @param fieldConsumer the consumer to run
*/
public void forEach(Consumer<Field> fieldConsumer) {
fieldConsumer.accept(singleField);
}
public static class Builder {
/*
The builder logic is complicated by these dual singleton / list duality code,
but it prevents memory allocation and every bit counts
when the CPU is running hot and an operation has lots of fields!
*/
private ImmutableList.@Nullable Builder<Field> fields;
private @Nullable Field singleField;
private ImmutableList.@Nullable Builder<DeferredExecution> deferredExecutions;
private Builder() {
}
private Builder(MergedField existing) {
if (existing instanceof MultiMergedField) {
this.singleField = null;
this.fields = new ImmutableList.Builder<>();
this.fields.addAll(existing.getFields());
} else {
this.singleField = existing.singleField;
}
if (!existing.deferredExecutions.isEmpty()) {
this.deferredExecutions = ensureDeferredExecutionsListBuilder();
this.deferredExecutions.addAll(existing.deferredExecutions);
}
}
private ImmutableList.Builder<DeferredExecution> ensureDeferredExecutionsListBuilder() {
if (this.deferredExecutions == null) {
this.deferredExecutions = new ImmutableList.Builder<>();
}
return this.deferredExecutions;
}
private ImmutableList.Builder<Field> ensureFieldsListBuilder() {
if (this.fields == null) {
this.fields = new ImmutableList.Builder<>();
if (this.singleField != null) {
this.fields.add(this.singleField);
this.singleField = null;
}
}
return this.fields;
}
public Builder fields(List<Field> fields) {
if (singleField == null && this.fields == null && fields.size() == 1) {
// even if you present a list - if its a list of one, we dont allocate a list
singleField = fields.get(0);
return this;
} else {
this.fields = ensureFieldsListBuilder();
this.fields.addAll(fields);
}
return this;
}
public Builder addField(Field field) {
if (singleField == null && this.fields == null) {
singleField = field;
return this;
} else {
this.fields = ensureFieldsListBuilder();
}
this.fields.add(field);
return this;
}
public Builder addDeferredExecutions(List<DeferredExecution> deferredExecutions) {
if (!deferredExecutions.isEmpty()) {
this.deferredExecutions = ensureDeferredExecutionsListBuilder();
this.deferredExecutions.addAll(deferredExecutions);
}
return this;
}
@SuppressWarnings("UnusedReturnValue")
public Builder addDeferredExecution(@Nullable DeferredExecution deferredExecution) {
if (deferredExecution != null) {
this.deferredExecutions = ensureDeferredExecutionsListBuilder();
this.deferredExecutions.add(deferredExecution);
}
return this;
}
public MergedField build() {
ImmutableList<DeferredExecution> deferredExecutions;
if (this.deferredExecutions == null) {
deferredExecutions = ImmutableList.of();
} else {
deferredExecutions = this.deferredExecutions.build();
}
if (this.singleField != null && this.fields == null) {
return new MergedField(singleField, deferredExecutions);
}
ImmutableList<Field> fields = assertNotNull(this.fields, () -> "You MUST add at least one field via the builder").build();
assertNotEmpty(fields);
return new MultiMergedField(fields, deferredExecutions);
}
}
}