gortiz commented on code in PR #19883:
URL: https://github.com/apache/pinot/pull/19883#discussion_r4293206133


##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java:
##########
@@ -2464,11 +2474,97 @@ static void validateRequest(PinotQuery pinotQuery, int 
queryResponseLimit) {
           "Value for 'LIMIT' (" + limit + ") exceeds maximum allowed value of 
" + queryResponseLimit);
     }
     QueryOptionsUtils.getNumReplicaGroupsToQuery(pinotQuery.getQueryOptions());
+    validateExprMinMax(pinotQuery);
     if (pinotQuery.getDataSource().getSubquery() != null) {
       validateRequest(pinotQuery.getDataSource().getSubquery(), 
queryResponseLimit);
     }
   }
 
+  /// Servers cannot execute EXPR_MIN/EXPR_MAX directly. The broker must 
rewrite them into parent and child
+  /// aggregations with [ExprMinMaxRewriter], and merge the parent result back 
into the child columns with
+  /// [ParentAggregationResultRewriter]. Neither rewriter is enabled by 
default.
+  ///
+  /// Without this check, a query that still has EXPR_MIN/EXPR_MAX fails on 
the servers, and a query without the
+  /// result rewriter returns the placeholder values of the child aggregations 
instead of the expected values.
+  @VisibleForTesting
+  static void validateExprMinMax(PinotQuery pinotQuery) {
+    String exprMinMax = findFunction(pinotQuery, EXPR_MIN_MAX_FUNCTIONS);
+    if (exprMinMax != null) {
+      String functionName = exprMinMax.equals(EXPR_MIN) ? "EXPR_MIN" : 
"EXPR_MAX";
+      boolean queryRewriterEnabled =
+          
CalciteSqlParser.QUERY_REWRITERS.stream().anyMatch(ExprMinMaxRewriter.class::isInstance);
+      if (!queryRewriterEnabled) {
+        throw new BadQueryRequestException(
+            functionName + " is not enabled on this broker. To enable it, add 
'" + ExprMinMaxRewriter.class.getName()
+                + "' to '" + 
Broker.CONFIG_OF_BROKER_QUERY_REWRITER_CLASS_NAMES + "' and '"
+                + ParentAggregationResultRewriter.class.getName() + "' to '"
+                + Broker.CONFIG_OF_BROKER_RESULT_REWRITER_CLASS_NAMES + "'");
+      }
+      // The rewriter only handles the top-level expressions of the select 
list, so the function is nested in another
+      // expression or has an alias.
+      throw new BadQueryRequestException(
+          functionName + " is only supported as a top-level expression in the 
select list, without an alias");

Review Comment:
   Good point. In 20b7b64 the message now says that other expressions and the 
WHERE, GROUP BY, HAVING and ORDER BY clauses aren't supported either, so a 
query like `WHERE EXPR_MIN(a, b) > 5` gets an accurate error.



##########
pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/QueryValidationTest.java:
##########
@@ -167,6 +171,51 @@ public void testReplicaGroupToQueryInvalidQuery() {
         () -> BaseSingleStageBrokerRequestHandler.validateRequest(pinotQuery, 
10));
   }
 
+  @Test
+  public void testExprMinMaxWithoutRewriters() {

Review Comment:
   Agreed, that was fragile. In 20b7b64 an `@AfterMethod` resets both factories 
to their defaults after every test, and the per-test `finally` blocks are gone. 
Now no test depends on the order of the others.



##########
pinot-core/src/main/java/org/apache/pinot/core/transport/InstanceRequestHandler.java:
##########
@@ -123,20 +124,31 @@ protected void channelRead0(ChannelHandlerContext ctx, 
ByteBuf msg) {
     
_serverMetrics.addMeteredGlobalValue(ServerMeter.NETTY_CONNECTION_BYTES_RECEIVED,
 requestSize);
     byte[] requestBytes = new byte[requestSize];
     msg.readBytes(requestBytes);
-    ServerQueryRequest queryRequest;
+    InstanceRequest instanceRequest = new InstanceRequest();
     try {
-      InstanceRequest instanceRequest = new InstanceRequest();
       THREAD_LOCAL_T_DESERIALIZER.get().deserialize(instanceRequest, 
requestBytes);
-      queryRequest = new ServerQueryRequest(instanceRequest, _serverMetrics, 
queryArrivalTimeMs);
-      _serverMetrics.addMeteredTableValue(queryRequest.getTableNameWithType(), 
ServerMeter.QUERIES_ON_TABLE, 1);
-      
queryRequest.getTimerContext().startNewPhaseTimer(ServerQueryPhase.REQUEST_DESERIALIZATION,
 queryArrivalTimeMs)
-          .stopAndRecord();
     } catch (Exception e) {
       // Do not send error response because request id is unknown.
       LOGGER.error("Failed to deserialize instance request: {}", 
BytesUtils.toHexString(requestBytes), e);
       
_serverMetrics.addMeteredGlobalValue(ServerMeter.REQUEST_DESERIALIZATION_EXCEPTIONS,
 1);
       return;
     }
+    ServerQueryRequest queryRequest;
+    try {
+      queryRequest = new ServerQueryRequest(instanceRequest, _serverMetrics, 
queryArrivalTimeMs);
+    } catch (Exception e) {
+      // The request id is known, so reply with the error. Without a response, 
the broker waits until the query times
+      // out and reports the server as not responded, which hides the actual 
error (e.g. an unsupported function).
+      long requestId = instanceRequest.getRequestId();
+      String tableNameWithType = getTableNameWithType(instanceRequest);
+      LOGGER.error("Failed to compile query request: {} on table: {}", 
requestId, tableNameWithType, e);
+      sendErrorResponse(ctx, requestId, tableNameWithType, queryArrivalTimeMs,

Review Comment:
   Yes, that shift is intended: `REQUEST_DESERIALIZATION_EXCEPTIONS` now counts 
only Thrift failures, and a compile failure is a query execution error. I added 
a release notes section to the PR description and the `release-notes` label so 
operators can update their alerts.



##########
pinot-core/src/test/java/org/apache/pinot/core/transport/InstanceRequestHandlerTest.java:
##########
@@ -144,6 +156,39 @@ public void testWriteFailureClosesChannel()
     verify(ctx).close();
   }
 
+  @Test
+  public void testRepliesWithErrorWhenQueryCannotBeCompiled()
+      throws Exception {
+    PinotConfiguration config = new PinotConfiguration();
+    QueryScheduler queryScheduler = createQueryScheduler(config, new 
CountDownLatch(0));
+    InstanceRequestHandler handler =
+        new InstanceRequestHandler("server01", config, queryScheduler, 
mock(AccessControl.class),
+            ThreadAccountantUtils.getNoOpAccountant());
+
+    // EXPR_MIN cannot be executed without the broker-side ExprMinMaxRewriter, 
so the server fails to build the
+    // aggregation function while it compiles the request.
+    BrokerRequest brokerRequest = new BrokerRequest();
+    brokerRequest.setPinotQuery(CalciteSqlParser.compileToPinotQuery("SELECT 
EXPR_MIN(a, b) FROM testTable_OFFLINE"));
+    InstanceRequest instanceRequest = new InstanceRequest(123L, brokerRequest);
+    byte[] requestBytes = new TSerializer(new 
TCompactProtocol.Factory()).serialize(instanceRequest);
+
+    ChannelHandlerContext ctx = mock(ChannelHandlerContext.class);
+    when(ctx.writeAndFlush(any())).thenReturn(mock(ChannelFuture.class));
+    handler.channelRead0(ctx, Unpooled.wrappedBuffer(requestBytes));
+
+    ArgumentCaptor<ByteBuf> responseCaptor = 
ArgumentCaptor.forClass(ByteBuf.class);
+    verify(ctx).writeAndFlush(responseCaptor.capture());
+    ByteBuf response = responseCaptor.getValue();
+    byte[] responseBytes = new byte[response.readableBytes()];
+    response.readBytes(responseBytes);
+    DataTable dataTable = DataTableFactory.getDataTable(responseBytes);
+    
assertEquals(dataTable.getMetadata().get(MetadataKey.REQUEST_ID.getName()), 
"123");
+    Map<Integer, String> exceptions = dataTable.getExceptions();
+    assertEquals(exceptions.size(), 1);
+    String message = exceptions.get(QueryErrorCode.QUERY_EXECUTION.getId());
+    assertTrue(message.contains("EXPRMIN"), message);

Review Comment:
   Fixed in 20b7b64. The test now asserts the `AggregationFunctionFactory` 
message ("Aggregation function: EXPRMIN is only supported in selection without 
alias"), so it only passes for the unsupported-function failure.



##########
pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/QueryValidationTest.java:
##########
@@ -167,6 +171,51 @@ public void testReplicaGroupToQueryInvalidQuery() {
         () -> BaseSingleStageBrokerRequestHandler.validateRequest(pinotQuery, 
10));
   }
 
+  @Test
+  public void testExprMinMaxWithoutRewriters() {

Review Comment:
   Added in 20b7b64: WHERE, HAVING and ORDER BY cases, next to the alias and 
nested cases, all with the full message.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to