diff --git a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/ExecutionTreeBuilder.java b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/ExecutionTreeBuilder.java index 527c789b..0b06a2b9 100644 --- a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/ExecutionTreeBuilder.java +++ b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/ExecutionTreeBuilder.java @@ -70,7 +70,6 @@ public QueryNode build() { executionContext.getExpressionContext(), EDS.name()); if (entitiesRequest.getIncludeNonLiveEntities() && areFiltersOnlyOnEds) { ExecutionTreeUtils.removeDuplicateSelectionAttributes(executionContext, EDS.name()); - QueryNode rootNode = new DataFetcherNode(EDS.name(), entitiesRequest.getFilter()); // if the filter by and order by are from the same source, pagination can be pushed down to // EDS @@ -84,12 +83,16 @@ public QueryNode build() { entitiesRequest.getOrderByList(), entitiesRequest.getFetchTotal()); executionContext.setSortAndPaginationNodeAdded(true); + rootNode.acceptVisitor(new ExecutionContextBuilderVisitor(executionContext)); + QueryNode executionTree = buildExecutionTree(executionContext, rootNode); + if (LOG.isDebugEnabled()) { + LOG.debug("Execution Tree:{}", executionTree.acceptVisitor(new PrintVisitor())); + } + return executionTree; } rootNode.acceptVisitor(new ExecutionContextBuilderVisitor(executionContext)); - - QueryNode executionTree = buildExecutionTree(executionContext, rootNode); - + QueryNode executionTree = buildExecutionTreeWithJoinNode(executionContext, rootNode); if (LOG.isDebugEnabled()) { LOG.debug("Execution Tree:{}", executionTree.acceptVisitor(new PrintVisitor())); } @@ -223,6 +226,37 @@ QueryNode buildExecutionTree(EntityExecutionContext executionContext, QueryNode metricSourcesForOrderBy.forEach(executionContext::removePendingMetricAggregationSources); } + return buildPaginationAndSelectionsNode(executionContext, rootNode); + } + + @VisibleForTesting + QueryNode buildExecutionTreeWithJoinNode( + EntityExecutionContext executionContext, QueryNode filterTree) { + QueryNode rootNode = filterTree; + // Select attributes from sources in order by but not part of the filter tree + Set attrSourcesForOrderBy = executionContext.getPendingSelectionSourcesForOrderBy(); + if (!attrSourcesForOrderBy.isEmpty()) { + rootNode = + new JoinNode.Builder(filterTree).setAttrSelectionSources(attrSourcesForOrderBy).build(); + attrSourcesForOrderBy.forEach(executionContext::removePendingSelectionSource); + } + + // Select agg attributes from sources in order by + Set metricSourcesForOrderBy = + executionContext.getPendingMetricAggregationSourcesForOrderBy(); + if (!metricSourcesForOrderBy.isEmpty()) { + rootNode = + new JoinNode.Builder(rootNode) + .setAggMetricSelectionSources(metricSourcesForOrderBy) + .build(); + metricSourcesForOrderBy.forEach(executionContext::removePendingMetricAggregationSources); + } + + return buildPaginationAndSelectionsNode(executionContext, rootNode); + } + + private QueryNode buildPaginationAndSelectionsNode( + EntityExecutionContext executionContext, QueryNode rootNode) { // Try adding SortAndPaginateNode rootNode = checkAndAddSortAndPaginationNode(rootNode, executionContext); diff --git a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/JoinNode.java b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/JoinNode.java new file mode 100644 index 00000000..4f28a7fe --- /dev/null +++ b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/JoinNode.java @@ -0,0 +1,74 @@ +package org.hypertrace.gateway.service.entity.query; + +import java.util.Collections; +import java.util.Set; +import org.hypertrace.gateway.service.entity.query.visitor.Visitor; + +public class JoinNode implements QueryNode { + private final Set attrSelectionSources; + private final Set aggMetricSelectionSources; + + private final QueryNode childNode; + + private JoinNode( + QueryNode childNode, + Set attrSelectionSources, + Set aggMetricSelectionSources) { + this.attrSelectionSources = attrSelectionSources; + this.aggMetricSelectionSources = aggMetricSelectionSources; + this.childNode = childNode; + } + + public Set getAttrSelectionSources() { + return attrSelectionSources; + } + + public Set getAggMetricSelectionSources() { + return aggMetricSelectionSources; + } + + public QueryNode getChildNode() { + return childNode; + } + + @Override + public R acceptVisitor(Visitor v) { + return v.visit(this); + } + + @Override + public String toString() { + return "SelectionNode{" + + "attrSelectionSources=" + + attrSelectionSources + + ", aggMetricSelectionSources=" + + aggMetricSelectionSources + + ", childNode=" + + childNode + + '}'; + } + + public static class Builder { + private final QueryNode childNode; + private Set attrSelectionSources = Collections.emptySet(); + private Set aggMetricSelectionSources = Collections.emptySet(); + + public Builder(QueryNode childNode) { + this.childNode = childNode; + } + + public JoinNode.Builder setAttrSelectionSources(Set attrSelectionSources) { + this.attrSelectionSources = attrSelectionSources; + return this; + } + + public JoinNode.Builder setAggMetricSelectionSources(Set aggMetricSelectionSources) { + this.aggMetricSelectionSources = aggMetricSelectionSources; + return this; + } + + public JoinNode build() { + return new JoinNode(childNode, attrSelectionSources, aggMetricSelectionSources); + } + } +} diff --git a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionContextBuilderVisitor.java b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionContextBuilderVisitor.java index 7f3609d6..a850412a 100644 --- a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionContextBuilderVisitor.java +++ b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionContextBuilderVisitor.java @@ -7,6 +7,7 @@ import org.hypertrace.gateway.service.entity.query.AndNode; import org.hypertrace.gateway.service.entity.query.DataFetcherNode; import org.hypertrace.gateway.service.entity.query.EntityExecutionContext; +import org.hypertrace.gateway.service.entity.query.JoinNode; import org.hypertrace.gateway.service.entity.query.NoOpNode; import org.hypertrace.gateway.service.entity.query.OrNode; import org.hypertrace.gateway.service.entity.query.PaginateOnlyNode; @@ -119,6 +120,11 @@ public Void visit(PaginateOnlyNode paginateOnlyNode) { return paginateOnlyNode.getChildNode().acceptVisitor(this); } + @Override + public Void visit(JoinNode joinNode) { + return joinNode.getChildNode().acceptVisitor(this); + } + private Set getRedundantPendingSelectionSources( Set fetchedAttributes, Set pendingAttributeSelectionSources, diff --git a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitor.java b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitor.java index d9baca90..52398b97 100644 --- a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitor.java +++ b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitor.java @@ -10,6 +10,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -29,6 +30,7 @@ import org.hypertrace.gateway.service.entity.query.AndNode; import org.hypertrace.gateway.service.entity.query.DataFetcherNode; import org.hypertrace.gateway.service.entity.query.EntityExecutionContext; +import org.hypertrace.gateway.service.entity.query.JoinNode; import org.hypertrace.gateway.service.entity.query.NoOpNode; import org.hypertrace.gateway.service.entity.query.OrNode; import org.hypertrace.gateway.service.entity.query.PaginateOnlyNode; @@ -93,6 +95,25 @@ protected static EntityResponse intersect(List entityResponses) return new EntityResponse(entityFetcherResponse, entityFetcherResponse.size()); } + private static EntityFetcherResponse mergeEntities( + EntityFetcherResponse rootEntityResponse, EntityFetcherResponse otherResponse) { + return new EntityFetcherResponse( + rootEntityResponse.getEntityKeyBuilderMap().entrySet().stream() + .collect( + Collectors.toUnmodifiableMap( + Entry::getKey, + entry -> { + Map entityKeyBuilderMap = + otherResponse.getEntityKeyBuilderMap(); + if (entityKeyBuilderMap.containsKey(entry.getKey())) { + return entry + .getValue() + .mergeFrom(entityKeyBuilderMap.get(entry.getKey()).build()); + } + return entry.getValue(); + }))); + } + private static EntityFetcherResponse unionEntities(List builders) { return new EntityFetcherResponse( builders.stream() @@ -209,6 +230,92 @@ public EntityResponse visit(DataFetcherNode dataFetcherNode) { } } + @Override + public EntityResponse visit(JoinNode joinNode) { + EntityResponse childNodeResponse = joinNode.getChildNode().acceptVisitor(this); + EntityFetcherResponse childEntityFetcherResponse = childNodeResponse.getEntityFetcherResponse(); + // If the result was empty when the filter is non-empty, it means no entities matched the filter + // and hence no need to do any more follow up calls. + if (childEntityFetcherResponse.isEmpty() + && !Filter.getDefaultInstance().equals(executionContext.getEntitiesRequest().getFilter())) { + LOG.debug("No results matched the filter so not fetching aggregate/timeseries metrics."); + return childNodeResponse; + } + + List resultMapList = new ArrayList<>(); + resultMapList.addAll( + joinNode.getAttrSelectionSources().parallelStream() + .map( + source -> { + EntitiesRequest request = + EntitiesRequest.newBuilder(executionContext.getEntitiesRequest()) + .clearSelection() + .clearTimeAggregation() + .clearFilter() + // TODO: Should we push order by, limit and offet down to the data source? + // If we want to push the order by down, we would also have to divide + // order by into sourceToOrderBySelectionExpressionMap, + // sourceToOrderByMetricExpressionMap, sourceToOrderByTimeAggregationMap + .clearOrderBy() + .clearLimit() + .clearOffset() + .addAllSelection( + executionContext + .getExpressionContext() + .getSourceToSelectionExpressionMap() + .get(source)) + .setFilter(addSourceFilters(executionContext, source)) + .build(); + IEntityFetcher entityFetcher = queryHandlerRegistry.getEntityFetcher(source); + EntitiesRequestContext context = + new EntitiesRequestContext( + executionContext.getEntitiesRequestContext().getGrpcContext(), + request.getStartTimeMillis(), + request.getEndTimeMillis(), + request.getEntityType(), + executionContext.getTimestampAttributeId()); + return entityFetcher.getEntities(context, request); + }) + .collect(Collectors.toList())); + resultMapList.addAll( + joinNode.getAggMetricSelectionSources().parallelStream() + .map( + source -> { + EntitiesRequest request = + EntitiesRequest.newBuilder(executionContext.getEntitiesRequest()) + .clearSelection() + .clearTimeAggregation() + .clearFilter() + .clearOrderBy() + .clearOffset() + .clearLimit() + .addAllSelection( + executionContext + .getExpressionContext() + .getSourceToMetricExpressionMap() + .get(source)) + .setFilter(addSourceFilters(executionContext, source)) + .build(); + IEntityFetcher entityFetcher = queryHandlerRegistry.getEntityFetcher(source); + EntitiesRequestContext context = + new EntitiesRequestContext( + executionContext.getEntitiesRequestContext().getGrpcContext(), + request.getStartTimeMillis(), + request.getEndTimeMillis(), + request.getEntityType(), + executionContext.getTimestampAttributeId()); + return entityFetcher.getEntities(context, request); + }) + .collect(Collectors.toList())); + + EntityFetcherResponse response = + resultMapList.stream() + .reduce(new EntityFetcherResponse(), (r1, r2) -> unionEntities(Arrays.asList(r1, r2))); + + return new EntityResponse( + mergeEntities(childEntityFetcherResponse, response), childNodeResponse.getTotal()); + } + @Override public EntityResponse visit(AndNode andNode) { return intersect( @@ -357,6 +464,10 @@ public EntityResponse visit(SelectionNode selectionNode) { } } + private Filter addSourceFilters(EntityExecutionContext executionContext, String source) { + return addSourceFilters(executionContext, source, null); + } + private Filter addSourceFilters( EntityExecutionContext executionContext, String source, Filter filter) { Optional sourceFilterOptional = @@ -365,6 +476,10 @@ private Filter addSourceFilters( .getExpressionContext() .getSourceToFilterMap() .get(AttributeSource.valueOf(source))); + if (filter == null) { + return sourceFilterOptional.orElse(Filter.getDefaultInstance()); + } + return sourceFilterOptional .map( sourceFilter -> diff --git a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/FilterOptimizingVisitor.java b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/FilterOptimizingVisitor.java index 3aaf404e..36fcb81f 100644 --- a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/FilterOptimizingVisitor.java +++ b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/FilterOptimizingVisitor.java @@ -8,6 +8,7 @@ import java.util.stream.Stream; import org.hypertrace.gateway.service.entity.query.AndNode; import org.hypertrace.gateway.service.entity.query.DataFetcherNode; +import org.hypertrace.gateway.service.entity.query.JoinNode; import org.hypertrace.gateway.service.entity.query.NoOpNode; import org.hypertrace.gateway.service.entity.query.OrNode; import org.hypertrace.gateway.service.entity.query.PaginateOnlyNode; @@ -194,4 +195,9 @@ public QueryNode visit(PaginateOnlyNode paginateOnlyNode) { return new PaginateOnlyNode( childNode, paginateOnlyNode.getLimit(), paginateOnlyNode.getOffset()); } + + @Override + public QueryNode visit(JoinNode joinNode) { + return joinNode; + } } diff --git a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/PrintVisitor.java b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/PrintVisitor.java index dfa9a4a2..36d849a4 100644 --- a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/PrintVisitor.java +++ b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/PrintVisitor.java @@ -3,6 +3,7 @@ import java.util.stream.Collectors; import org.hypertrace.gateway.service.entity.query.AndNode; import org.hypertrace.gateway.service.entity.query.DataFetcherNode; +import org.hypertrace.gateway.service.entity.query.JoinNode; import org.hypertrace.gateway.service.entity.query.NoOpNode; import org.hypertrace.gateway.service.entity.query.OrNode; import org.hypertrace.gateway.service.entity.query.PaginateOnlyNode; @@ -66,4 +67,9 @@ public String visit(PaginateOnlyNode paginateOnlyNode) { + ") --> \n" + paginateOnlyNode.getChildNode().acceptVisitor(this); } + + @Override + public String visit(JoinNode joinNode) { + return "JOIN_NODE(" + joinNode + ") --> \n" + joinNode.getChildNode().acceptVisitor(this); + } } diff --git a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/Visitor.java b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/Visitor.java index c41d1141..abf79cc0 100644 --- a/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/Visitor.java +++ b/gateway-service-impl/src/main/java/org/hypertrace/gateway/service/entity/query/visitor/Visitor.java @@ -2,6 +2,7 @@ import org.hypertrace.gateway.service.entity.query.AndNode; import org.hypertrace.gateway.service.entity.query.DataFetcherNode; +import org.hypertrace.gateway.service.entity.query.JoinNode; import org.hypertrace.gateway.service.entity.query.NoOpNode; import org.hypertrace.gateway.service.entity.query.OrNode; import org.hypertrace.gateway.service.entity.query.PaginateOnlyNode; @@ -28,4 +29,6 @@ public interface Visitor { R visit(NoOpNode noOpNode); R visit(PaginateOnlyNode paginateOnlyNode); + + R visit(JoinNode joinNode); } diff --git a/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/common/EntitiesRequestAndResponseUtils.java b/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/common/EntitiesRequestAndResponseUtils.java index ab550e91..154606ab 100644 --- a/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/common/EntitiesRequestAndResponseUtils.java +++ b/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/common/EntitiesRequestAndResponseUtils.java @@ -105,6 +105,10 @@ public static Value getStringValue(String value) { return Value.newBuilder().setString(value).setValueType(ValueType.STRING).build(); } + public static Value getLongValue(long value) { + return Value.newBuilder().setLong(value).setValueType(ValueType.LONG).build(); + } + public static AggregatedMetricValue getAggregatedMetricValue( FunctionType functionType, double value) { return AggregatedMetricValue.newBuilder() diff --git a/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitorTest.java b/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitorTest.java index c921f8bd..25c0eccb 100644 --- a/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitorTest.java +++ b/gateway-service-impl/src/test/java/org/hypertrace/gateway/service/entity/query/visitor/ExecutionVisitorTest.java @@ -8,6 +8,7 @@ import static org.hypertrace.gateway.service.common.EntitiesRequestAndResponseUtils.compareEntityResponses; import static org.hypertrace.gateway.service.common.EntitiesRequestAndResponseUtils.generateEQFilter; import static org.hypertrace.gateway.service.common.EntitiesRequestAndResponseUtils.getAggregatedMetricValue; +import static org.hypertrace.gateway.service.common.EntitiesRequestAndResponseUtils.getLongValue; import static org.hypertrace.gateway.service.common.EntitiesRequestAndResponseUtils.getStringValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -43,6 +44,7 @@ import org.hypertrace.gateway.service.entity.EntityQueryHandlerRegistry; import org.hypertrace.gateway.service.entity.query.DataFetcherNode; import org.hypertrace.gateway.service.entity.query.EntityExecutionContext; +import org.hypertrace.gateway.service.entity.query.JoinNode; import org.hypertrace.gateway.service.entity.query.NoOpNode; import org.hypertrace.gateway.service.entity.query.PaginateOnlyNode; import org.hypertrace.gateway.service.entity.query.SelectionNode; @@ -58,6 +60,7 @@ import org.hypertrace.gateway.service.v1.common.Operator; import org.hypertrace.gateway.service.v1.common.OrderByExpression; import org.hypertrace.gateway.service.v1.common.Period; +import org.hypertrace.gateway.service.v1.common.SortOrder; import org.hypertrace.gateway.service.v1.common.TimeAggregation; import org.hypertrace.gateway.service.v1.common.Value; import org.hypertrace.gateway.service.v1.common.ValueType; @@ -380,6 +383,99 @@ public void testConstructFilterFromChildNodesNonEmptyResultsMultipleEntityIdExpr new HashSet<>(filter.getChildFilterList())); } + @Test + public void test_visit_JoinNode() { + List orderByExpressions = + List.of( + buildOrderByExpression( + SortOrder.DESC, + buildAggregateExpression( + API_NUM_CALLS_ATTR, FunctionType.COUNT, "numCalls", List.of()))); + int limit = 10; + int offset = 0; + long startTime = 0; + long endTime = 10; + String tenantId = "TENANT_ID"; + AttributeScope entityType = AttributeScope.API; + Expression selectionExpression = buildExpression(API_NAME_ATTR); + EntitiesRequest entitiesRequest = + EntitiesRequest.newBuilder() + .setEntityType(entityType.name()) + .setStartTimeMillis(startTime) + .setEndTimeMillis(endTime) + .addSelection(orderByExpressions.get(0).getExpression()) + .addSelection(selectionExpression) + .setFilter(generateEQFilter(API_DISCOVERY_STATE, "DISCOVERED")) + .addAllOrderBy(orderByExpressions) + .setIncludeNonLiveEntities(true) + .setLimit(limit) + .setOffset(offset) + .build(); + EntitiesRequestContext entitiesRequestContext = + new EntitiesRequestContext( + forTenantId(tenantId), startTime, endTime, entityType.name(), "API.startTime"); + Map entityKeyBuilderResponseMap = + Map.of( + EntityKey.of("entity-id-0"), + Entity.newBuilder().putAttribute("API.name", getStringValue("entity-0")), + EntityKey.of("entity-id-1"), + Entity.newBuilder().putAttribute("API.name", getStringValue("entity-1")), + EntityKey.of("entity-id-2"), + Entity.newBuilder().putAttribute("API.name", getStringValue("entity-2"))); + + EntityFetcherResponse entityFetcherResponse = + new EntityFetcherResponse(entityKeyBuilderResponseMap); + + when(expressionContext.getSourceToMetricExpressionMap()) + .thenReturn(Map.of("QS", List.of(orderByExpressions.get(0).getExpression()))); + when(expressionContext.getSourceToSelectionExpressionMap()) + .thenReturn(Map.of("EDS", List.of(selectionExpression))); + when(expressionContext.getSourceToSelectionExpressionMap()) + .thenReturn(Map.of("EDS", List.of(selectionExpression))); + when(executionContext.getEntitiesRequest()).thenReturn(entitiesRequest); + when(executionContext.getTenantId()).thenReturn(tenantId); + when(executionContext.getEntitiesRequestContext()).thenReturn(entitiesRequestContext); + when(executionContext.getTimestampAttributeId()).thenReturn("API.startTime"); + + Map queryServiceEntityKeyBuilderResponseMap = + Map.of( + EntityKey.of("entity-id-0"), + Entity.newBuilder().putAttribute("API.numCalls", getLongValue(10)), + EntityKey.of("entity-id-1"), + Entity.newBuilder().putAttribute("API.numCalls", getLongValue(50)), + EntityKey.of("entity-id-3"), + Entity.newBuilder().putAttribute("API.numCalls", getLongValue(100))); + + when(queryServiceEntityFetcher.getEntities(eq(entitiesRequestContext), any())) + .thenReturn(new EntityFetcherResponse(queryServiceEntityKeyBuilderResponseMap)); + + NoOpNode mockChildNode = mock(NoOpNode.class); + when(mockChildNode.acceptVisitor(any())) + .thenReturn(new EntityResponse(entityFetcherResponse, 100)); + JoinNode joinNode = + new JoinNode.Builder(mockChildNode) + .setAttrSelectionSources(Collections.emptySet()) + .setAggMetricSelectionSources(Set.of(QS_SOURCE)) + .build(); + + Map mergedEntityKeyBuilderResponseMap = + Map.of( + EntityKey.of("entity-id-0"), + Entity.newBuilder() + .putAttribute("API.name", getStringValue("entity-0")) + .putAttribute("API.numCalls", getLongValue(10)), + EntityKey.of("entity-id-1"), + Entity.newBuilder() + .putAttribute("API.name", getStringValue("entity-1")) + .putAttribute("API.numCalls", getLongValue(50)), + EntityKey.of("entity-id-2"), + Entity.newBuilder().putAttribute("API.name", getStringValue("entity-2"))); + + compareEntityResponses( + new EntityResponse(new EntityFetcherResponse(mergedEntityKeyBuilderResponseMap), 100L), + executionVisitor.visit(joinNode)); + } + @Test public void test_visitDataFetcherNodeQs() { List orderByExpressions = List.of(buildOrderByExpression(API_ID_ATTR));