Skip to content

Commit 8c2c883

Browse files
chore: handle case of empty child filter (#214)
1 parent 436adec commit 8c2c883

File tree

7 files changed

+305
-7
lines changed

7 files changed

+305
-7
lines changed

query-service-impl/src/main/java/org/hypertrace/core/query/service/AbstractQueryTransformation.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.reactivex.rxjava3.core.Observable;
66
import io.reactivex.rxjava3.core.Single;
77
import java.util.List;
8+
import java.util.Optional;
89
import org.hypertrace.core.query.service.api.AttributeExpression;
910
import org.hypertrace.core.query.service.api.ColumnIdentifier;
1011
import org.hypertrace.core.query.service.api.Expression;
@@ -110,15 +111,23 @@ protected Single<OrderByExpression> transformOrderBy(OrderByExpression orderBy)
110111
}
111112

112113
protected Single<Filter> transformFilter(Filter filter) {
114+
return transformFilterInternal(filter)
115+
.map(filterOptional -> filterOptional.orElseGet(Filter::getDefaultInstance));
116+
}
117+
118+
private Single<Optional<Filter>> transformFilterInternal(Filter filter) {
113119
if (filter.equals(Filter.getDefaultInstance())) {
114-
return Single.just(filter);
120+
return Single.just(Optional.empty());
115121
}
116122

117123
Single<Expression> lhsSingle = this.transformExpression(filter.getLhs());
118124
Single<Expression> rhsSingle = this.transformExpression(filter.getRhs());
125+
// remove defaults from child filters
119126
Single<List<Filter>> childFilterListSingle =
120127
Observable.fromIterable(filter.getChildFilterList())
121-
.concatMapSingle(this::transformFilter)
128+
.concatMapSingle(this::transformFilterInternal)
129+
.filter(Optional::isPresent)
130+
.map(Optional::get)
122131
.toList();
123132
return zip(
124133
lhsSingle,
@@ -143,7 +152,7 @@ private Single<List<OrderByExpression>> transformOrderByList(
143152
* This doesn't change any functional behavior, but omits fields that aren't needed, shrinking the
144153
* object and keeping it equivalent to the source object for equality checks.
145154
*/
146-
private Filter rebuildFilterOmittingDefaults(
155+
private Optional<Filter> rebuildFilterOmittingDefaults(
147156
Filter original, Expression lhs, Expression rhs, List<Filter> childFilters) {
148157
Filter.Builder builder = original.toBuilder();
149158

@@ -159,7 +168,7 @@ private Filter rebuildFilterOmittingDefaults(
159168
builder.setRhs(rhs);
160169
}
161170

162-
return builder.clearChildFilter().addAllChildFilter(childFilters).build();
171+
return Optional.of(builder.clearChildFilter().addAllChildFilter(childFilters).build());
163172
}
164173

165174
private void debugLogIfRequestTransformed(QueryRequest original, QueryRequest transformed) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.hypertrace.core.query.service;
2+
3+
import com.google.protobuf.GeneratedMessageV3;
4+
import com.google.protobuf.InvalidProtocolBufferException;
5+
import com.google.protobuf.util.JsonFormat;
6+
import java.io.BufferedReader;
7+
import java.io.InputStream;
8+
import java.io.InputStreamReader;
9+
import java.io.Reader;
10+
import java.util.stream.Collectors;
11+
import org.hypertrace.core.grpcutils.context.RequestContext;
12+
13+
public abstract class AbstractServiceTest<TQueryServiceRequestType extends GeneratedMessageV3> {
14+
15+
private static final String QUERY_SERVICE_TEST_REQUESTS_DIR = "test-requests";
16+
17+
protected TQueryServiceRequestType readQueryServiceRequest(String fileName) {
18+
String resourceFileName = createResourceFileName(QUERY_SERVICE_TEST_REQUESTS_DIR, fileName);
19+
String requestJsonStr = readResourceFileAsString(resourceFileName);
20+
21+
GeneratedMessageV3.Builder requestBuilder = getQueryServiceRequestBuilder();
22+
try {
23+
JsonFormat.parser().merge(requestJsonStr, requestBuilder);
24+
} catch (InvalidProtocolBufferException e) {
25+
e.printStackTrace();
26+
}
27+
28+
return (TQueryServiceRequestType) requestBuilder.build();
29+
}
30+
31+
private static Reader readResourceFile(String fileName) {
32+
InputStream in = RequestContext.class.getClassLoader().getResourceAsStream(fileName);
33+
return new BufferedReader(new InputStreamReader(in));
34+
}
35+
36+
private String readResourceFileAsString(String fileName) {
37+
return ((BufferedReader) readResourceFile(fileName)).lines().collect(Collectors.joining());
38+
}
39+
40+
private String createResourceFileName(String filesBaseDir, String fileName) {
41+
return String.format("%s/%s/%s.json", filesBaseDir, getTestSuiteName(), fileName);
42+
}
43+
44+
protected abstract String getTestSuiteName();
45+
46+
protected abstract GeneratedMessageV3.Builder getQueryServiceRequestBuilder();
47+
}

query-service-impl/src/test/java/org/hypertrace/core/query/service/QueryServiceConfigTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void testQueryServiceImplConfigParser() {
3333
assertEquals("query-service", appConfig.getString("service.name"));
3434
assertEquals(8091, appConfig.getInt("service.admin.port"));
3535
assertEquals(8090, appConfig.getInt("service.port"));
36-
assertEquals(10, queryServiceConfig.getQueryRequestHandlersConfigs().size());
36+
assertEquals(11, queryServiceConfig.getQueryRequestHandlersConfigs().size());
3737
assertEquals(3, queryServiceConfig.getRequestHandlerClientConfigs().size());
3838

3939
RequestHandlerConfig handler0 = queryServiceConfig.getQueryRequestHandlersConfigs().get(0);

query-service-impl/src/test/java/org/hypertrace/core/query/service/attribubteexpression/AttributeExpressionSubpathExistsFilteringTransformationTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.hypertrace.core.query.service.QueryRequestUtil.createContainsKeyFilter;
99
import static org.hypertrace.core.query.service.QueryRequestUtil.createStringLiteralValueExpression;
1010
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertFalse;
1112
import static org.junit.jupiter.api.Assertions.assertTrue;
1213

1314
import java.util.List;
@@ -93,6 +94,55 @@ void transQueryWithComplexAttributeExpression_MultipleFilter() {
9394
.contains(createContainsKeyFilter(spanTags2.getAttributeExpression())));
9495
}
9596

97+
@Test
98+
void transQueryWithEmptyFilter() {
99+
Filter emptyFilter = Filter.getDefaultInstance();
100+
QueryRequest originalRequest = QueryRequest.newBuilder().setFilter(emptyFilter).build();
101+
102+
QueryRequest expectedTransform =
103+
this.transformation.transform(originalRequest, mockTransformationContext).blockingGet();
104+
assertFalse(expectedTransform.hasFilter());
105+
}
106+
107+
@Test
108+
void transQueryWithComplexAttributeExpression_EmptyFilter() {
109+
Expression spanTags1 = createComplexAttributeExpression("Span.tags", "FLAGS").build();
110+
Expression spanTags2 = createComplexAttributeExpression("Span.tags", "span.kind").build();
111+
112+
Filter childFilter1 =
113+
Filter.newBuilder()
114+
.setLhs(spanTags1)
115+
.setOperator(Operator.EQ)
116+
.setRhs(createStringLiteralValueExpression("0"))
117+
.build();
118+
Filter childFilter2 =
119+
Filter.newBuilder()
120+
.setLhs(spanTags2)
121+
.setOperator(Operator.EQ)
122+
.setRhs(createStringLiteralValueExpression("server"))
123+
.build();
124+
Filter emptyFilter = Filter.getDefaultInstance();
125+
126+
Filter.Builder filter =
127+
createCompositeFilter(Operator.AND, childFilter1, childFilter2, emptyFilter);
128+
QueryRequest originalRequest = QueryRequest.newBuilder().setFilter(filter).build();
129+
130+
QueryRequest expectedTransform =
131+
this.transformation.transform(originalRequest, mockTransformationContext).blockingGet();
132+
List<Filter> childFilterList = expectedTransform.getFilter().getChildFilterList();
133+
134+
assertTrue(
135+
childFilterList
136+
.get(0)
137+
.getChildFilterList()
138+
.contains(createContainsKeyFilter(spanTags1.getAttributeExpression())));
139+
assertTrue(
140+
childFilterList
141+
.get(1)
142+
.getChildFilterList()
143+
.contains(createContainsKeyFilter(spanTags2.getAttributeExpression())));
144+
}
145+
96146
@Test
97147
void transQueryWithComplexAttributeExpression_HierarchicalFilter() {
98148
Expression spanTags1 = createComplexAttributeExpression("Span.tags", "FLAGS").build();

query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/PinotBasedRequestHandlerTest.java

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import com.fasterxml.jackson.core.JsonProcessingException;
1414
import com.fasterxml.jackson.databind.ObjectMapper;
15+
import com.google.protobuf.GeneratedMessageV3;
1516
import com.typesafe.config.Config;
1617
import com.typesafe.config.ConfigFactory;
1718
import io.reactivex.rxjava3.core.Observable;
@@ -24,10 +25,13 @@
2425
import java.util.stream.Stream;
2526
import org.apache.pinot.client.ResultSet;
2627
import org.apache.pinot.client.ResultSetGroup;
28+
import org.hypertrace.core.query.service.AbstractQueryTransformation;
29+
import org.hypertrace.core.query.service.AbstractServiceTest;
2730
import org.hypertrace.core.query.service.ExecutionContext;
2831
import org.hypertrace.core.query.service.QueryCost;
2932
import org.hypertrace.core.query.service.QueryRequestBuilderUtils;
3033
import org.hypertrace.core.query.service.QueryRequestUtil;
34+
import org.hypertrace.core.query.service.QueryTransformation.QueryTransformationContext;
3135
import org.hypertrace.core.query.service.api.Expression;
3236
import org.hypertrace.core.query.service.api.Filter;
3337
import org.hypertrace.core.query.service.api.LiteralConstant;
@@ -43,8 +47,11 @@
4347
import org.junit.jupiter.params.ParameterizedTest;
4448
import org.junit.jupiter.params.provider.Arguments;
4549
import org.junit.jupiter.params.provider.MethodSource;
50+
import org.slf4j.Logger;
51+
import org.slf4j.LoggerFactory;
4652

47-
public class PinotBasedRequestHandlerTest {
53+
public class PinotBasedRequestHandlerTest extends AbstractServiceTest<QueryRequest> {
54+
private static final Logger LOGGER = LoggerFactory.getLogger(PinotBasedRequestHandlerTest.class);
4855
// Test subject
4956
private PinotBasedRequestHandler pinotBasedRequestHandler;
5057
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -1734,6 +1741,78 @@ public boolean isResultTableResultSetType(ResultSet resultSet) {
17341741
}
17351742
}
17361743

1744+
@Test
1745+
public void testEmptyChildFilterCase() throws IOException {
1746+
for (Config config : serviceConfig.getConfigList("queryRequestHandlersConfig")) {
1747+
if (!isPinotConfig(config)) {
1748+
continue;
1749+
}
1750+
1751+
if (!config.getString("name").equals("domain-service-handler")) {
1752+
continue;
1753+
}
1754+
1755+
// Mock the PinotClient
1756+
PinotClient pinotClient = mock(PinotClient.class);
1757+
PinotClientFactory factory = mock(PinotClientFactory.class);
1758+
when(factory.getPinotClient(any())).thenReturn(pinotClient);
1759+
1760+
String[][] resultTable = new String[][] {{"5"}};
1761+
List<String> columnNames = List.of("domain_id");
1762+
ResultSet resultSet = mockResultSet(1, 1, columnNames, resultTable);
1763+
ResultSetGroup resultSetGroup = mockResultSetGroup(List.of(resultSet));
1764+
1765+
PinotBasedRequestHandler handler =
1766+
new PinotBasedRequestHandler(
1767+
config.getString("name"),
1768+
config.getConfig("requestHandlerInfo"),
1769+
new ResultSetTypePredicateProvider() {
1770+
@Override
1771+
public boolean isSelectionResultSetType(ResultSet resultSet) {
1772+
return true;
1773+
}
1774+
1775+
@Override
1776+
public boolean isResultTableResultSetType(ResultSet resultSet) {
1777+
return false;
1778+
}
1779+
},
1780+
factory);
1781+
1782+
QueryRequest request = readQueryServiceRequest("request-with-empty-filter");
1783+
AbstractQueryTransformation transformation =
1784+
new AbstractQueryTransformation() {
1785+
@Override
1786+
protected Logger getLogger() {
1787+
return LOGGER;
1788+
}
1789+
};
1790+
QueryTransformationContext mockTransformationContext = mock(QueryTransformationContext.class);
1791+
QueryRequest transformedRequest =
1792+
transformation.transform(request, mockTransformationContext).blockingGet();
1793+
ExecutionContext context = new ExecutionContext("__default", transformedRequest);
1794+
QueryCost cost = handler.canHandle(transformedRequest, context);
1795+
Assertions.assertTrue(cost.getCost() > 0.0d && cost.getCost() < 1d);
1796+
1797+
// empty child filter should be ignored
1798+
String expectedQuery =
1799+
"Select DISTINCTCOUNTHLL(domain_id) FROM domainEventSpanView WHERE customer_id = ?"
1800+
+ " AND ( domain_id != ? AND ( start_time_millis >= ? AND start_time_millis < ? )"
1801+
+ " AND ( ( environment IN (?) ) ) ) limit 1";
1802+
Params params =
1803+
Params.newBuilder()
1804+
.addStringParam("__default")
1805+
.addStringParam("null")
1806+
.addLongParam(1658818870181L)
1807+
.addLongParam(1658905270181L)
1808+
.addStringParam("ui-data-validation")
1809+
.build();
1810+
when(pinotClient.executeQuery(expectedQuery, params)).thenReturn(resultSetGroup);
1811+
1812+
verifyResponseRows(handler.handleRequest(transformedRequest, context), resultTable);
1813+
}
1814+
}
1815+
17371816
@ParameterizedTest
17381817
@MethodSource("provideHandlerValue")
17391818
public void testGetTimeFilterColumn(int index, String expectedValue) {
@@ -1816,4 +1895,14 @@ private String stringify(Object obj) throws JsonProcessingException {
18161895
private boolean isPinotConfig(Config config) {
18171896
return config.getString("type").equals("pinot");
18181897
}
1898+
1899+
@Override
1900+
protected String getTestSuiteName() {
1901+
return "query";
1902+
}
1903+
1904+
@Override
1905+
protected GeneratedMessageV3.Builder getQueryServiceRequestBuilder() {
1906+
return QueryRequest.newBuilder();
1907+
}
18191908
}

query-service-impl/src/test/resources/application.conf

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,5 +382,24 @@ service.config = {
382382
}
383383
}
384384
}
385+
{
386+
name = domain-service-handler
387+
type = pinot
388+
clientConfig = broker
389+
requestHandlerInfo = {
390+
tenantColumnName: customer_id
391+
startTimeAttributeName = "EVENT.startTime"
392+
distinctCountAggFunction = DISTINCTCOUNTHLL
393+
viewDefinition = {
394+
viewName = domainEventSpanView
395+
fieldMap = {
396+
"EVENT.startTime": "start_time_millis",
397+
"DOMAIN.id": "domain_id",
398+
"DOMAIN.name": "domain_name",
399+
"EVENT.environment": "environment"
400+
}
401+
}
402+
}
403+
}
385404
]
386-
}
405+
}

0 commit comments

Comments
 (0)