prestodb/presto · warning · PinotException

PINOT_UNSUPPORTED_EXPRESSION

PINOT_UNSUPPORTED_EXPRESSION

Error message

Unsupported function in pinot aggregation: 

What it means

During aggregation pushdown, PinotAggregationProjectConverter.visitCall converts call expressions to Pinot SQL. Casts, NOT, and BETWEEN have dedicated handling; any other non-operator function encountered in an aggregation projection is rejected with PINOT_UNSUPPORTED_EXPRESSION because Pinot cannot execute it server-side.

Source

Thrown at presto-pinot-toolkit/src/main/java/com/facebook/presto/pinot/query/PinotAggregationProjectConverter.java:78

    }

    public PinotAggregationProjectConverter(TypeManager typeManager, FunctionMetadataManager functionMetadataManager, StandardFunctionResolution standardFunctionResolution, ConnectorSession session, VariableReferenceExpression arrayVariableHint)
    {
        super(typeManager, functionMetadataManager, standardFunctionResolution, session);
        this.arrayVariableHint = arrayVariableHint;
    }

    @Override
    public PinotExpression visitCall(
            CallExpression call,
            Map<VariableReferenceExpression, PinotQueryGeneratorContext.Selection> context)
    {
        FunctionHandle functionHandle = call.getFunctionHandle();
        if (standardFunctionResolution.isCastFunction(functionHandle)) {
            return handleCast(call, context);
        }
        if (standardFunctionResolution.isNotFunction(functionHandle) || standardFunctionResolution.isBetweenFunction(functionHandle)) {
            throw new PinotException(PINOT_UNSUPPORTED_EXPRESSION, Optional.empty(), "Unsupported function in pinot aggregation: " + functionHandle);
        }

        FunctionMetadata functionMetadata = functionMetadataManager.getFunctionMetadata(functionHandle);
        Optional<OperatorType> operatorTypeOptional = functionMetadata.getOperatorType();
        if (operatorTypeOptional.isPresent()) {
            OperatorType operatorType = operatorTypeOptional.get();
            if (operatorType.isArithmeticOperator()) {
                return handleArithmeticExpression(call, operatorType, context);
            }
            if (operatorType.isComparisonOperator()) {
                throw new PinotException(PINOT_UNSUPPORTED_EXPRESSION, Optional.empty(), "Comparison operator not supported: " + call);
            }
        }
        return handleFunction(call, context);
    }

    @Override
    public PinotExpression visitConstant(

View on GitHub (pinned to 55bb57d202)

Solutions

  1. Rewrite the query to move the unsupported function into an outer projection and keep only supported expressions in the aggregation/inner projection
  2. Register/implement the equivalent function in Pinot so pushdown is valid
  3. Cast inputs so the expression reduces to a supported operator (e.g. use || or a supported date function)
  4. If the function should be pushable, add handling in PinotAggregationProjectConverter/PinotExpressionFormatter

Example fix

// before
SELECT concat(region, '_x'), count(*) FROM pinot_table GROUP BY 1
// after
SELECT region || '_x', count(*) FROM pinot_table GROUP BY 1
-- or keep concat outside the pushed-down aggregation
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check: keep pushed-down projections to supported functions only
List<String> allowed = List.of("cast", "not", "between", "and", "or", "+", "-", "*", "/", "%", "=", "<", ">", "<=", ">=", "<>");
// during query construction, reject scalar function calls (concat, udfs) in grouped/projected expressions
if (callOutsideAllowedSet(expr, allowed)) {
  log.warn("Expression will not push down to pinot: " + expr);
}

Try / catch

try {
  result = session.execute(query);
} catch (PinotException e) {
  if (PinotErrorCode.PINOT_UNSUPPORTED_EXPRESSION.toErrorCodeObject().equals(e.getErrorCode())) {
    log.warn("Rewriting query to avoid pushdown: %s", e.getMessage());
    result = session.execute(restrictPushdown(query)); // wrap unsupported fn in an outer projection
  } else throw e;
}

Prevention

When it happens

Trigger: visitCall sees a Call expression inside an aggregation whose function is neither a cast nor NOT/BETWEEN and whose FunctionMetadata has no OperatorType — e.g. user-defined scalar functions, string functions like concat/substr used in the projection, lambda expressions, or try_cast.

Common situations: Writing aggregations with computed columns using functions Pinot doesn't support (e.g. concat(), date_format() in the projection); UDFs registered in Presto but not Pinot; planner pushing projections down that should stay in Presto.

Related errors


AI-assisted analysis of prestodb/presto@55bb57d202 (2026-09-04). Data as JSON: /api/errors/0cb39054da86a21d. Report an issue: GitHub.