{"record":{"id":"0cb39054da86a21d","repo":"prestodb/presto","slug":"pinot-unsupported-expression-0cb390","errorCode":"PINOT_UNSUPPORTED_EXPRESSION","errorMessage":"Unsupported function in pinot aggregation: ","messagePattern":"Unsupported function in pinot aggregation: ","errorType":"error_code","errorClass":"PinotException","httpStatus":null,"severity":"warning","filePath":"presto-pinot-toolkit/src/main/java/com/facebook/presto/pinot/query/PinotAggregationProjectConverter.java","lineNumber":78,"sourceCode":"    }\n\n    public PinotAggregationProjectConverter(TypeManager typeManager, FunctionMetadataManager functionMetadataManager, StandardFunctionResolution standardFunctionResolution, ConnectorSession session, VariableReferenceExpression arrayVariableHint)\n    {\n        super(typeManager, functionMetadataManager, standardFunctionResolution, session);\n        this.arrayVariableHint = arrayVariableHint;\n    }\n\n    @Override\n    public PinotExpression visitCall(\n            CallExpression call,\n            Map<VariableReferenceExpression, PinotQueryGeneratorContext.Selection> context)\n    {\n        FunctionHandle functionHandle = call.getFunctionHandle();\n        if (standardFunctionResolution.isCastFunction(functionHandle)) {\n            return handleCast(call, context);\n        }\n        if (standardFunctionResolution.isNotFunction(functionHandle) || standardFunctionResolution.isBetweenFunction(functionHandle)) {\n            throw new PinotException(PINOT_UNSUPPORTED_EXPRESSION, Optional.empty(), \"Unsupported function in pinot aggregation: \" + functionHandle);\n        }\n\n        FunctionMetadata functionMetadata = functionMetadataManager.getFunctionMetadata(functionHandle);\n        Optional<OperatorType> operatorTypeOptional = functionMetadata.getOperatorType();\n        if (operatorTypeOptional.isPresent()) {\n            OperatorType operatorType = operatorTypeOptional.get();\n            if (operatorType.isArithmeticOperator()) {\n                return handleArithmeticExpression(call, operatorType, context);\n            }\n            if (operatorType.isComparisonOperator()) {\n                throw new PinotException(PINOT_UNSUPPORTED_EXPRESSION, Optional.empty(), \"Comparison operator not supported: \" + call);\n            }\n        }\n        return handleFunction(call, context);\n    }\n\n    @Override\n    public PinotExpression visitConstant(","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/prestodb/presto/blob/55bb57d202de3b926896fa966c2c4a44c779634e/presto-pinot-toolkit/src/main/java/com/facebook/presto/pinot/query/PinotAggregationProjectConverter.java#L60-L96","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rewrite the query to move the unsupported function into an outer projection and keep only supported expressions in the aggregation/inner projection","Register/implement the equivalent function in Pinot so pushdown is valid","Cast inputs so the expression reduces to a supported operator (e.g. use || or a supported date function)","If the function should be pushable, add handling in PinotAggregationProjectConverter/PinotExpressionFormatter"],"exampleFix":"// before\nSELECT concat(region, '_x'), count(*) FROM pinot_table GROUP BY 1\n// after\nSELECT region || '_x', count(*) FROM pinot_table GROUP BY 1\n-- or keep concat outside the pushed-down aggregation","handlingStrategy":"try-catch","validationCode":"// pre-check: keep pushed-down projections to supported functions only\nList<String> allowed = List.of(\"cast\", \"not\", \"between\", \"and\", \"or\", \"+\", \"-\", \"*\", \"/\", \"%\", \"=\", \"<\", \">\", \"<=\", \">=\", \"<>\");\n// during query construction, reject scalar function calls (concat, udfs) in grouped/projected expressions\nif (callOutsideAllowedSet(expr, allowed)) {\n  log.warn(\"Expression will not push down to pinot: \" + expr);\n}","typeGuard":null,"tryCatchPattern":"try {\n  result = session.execute(query);\n} catch (PinotException e) {\n  if (PinotErrorCode.PINOT_UNSUPPORTED_EXPRESSION.toErrorCodeObject().equals(e.getErrorCode())) {\n    log.warn(\"Rewriting query to avoid pushdown: %s\", e.getMessage());\n    result = session.execute(restrictPushdown(query)); // wrap unsupported fn in an outer projection\n  } else throw e;\n}","preventionTips":["Use only arithmetic/comparison/cast operators in aggregation projections","Move string/date formatting functions into outer SELECT layers","Avoid UDFs in queries intended for full pushdown","Test new dashboards against pinot to catch pushdown rejections early"],"tags":["pinot","pushdown","unsupported-expression","aggregation"],"backgroundTag":"pushdown-unsupported-expression","analyzedSha":"55bb57d202de3b926896fa966c2c4a44c779634e","analyzedAt":"2026-09-04T12:50:26.162Z","contentChangedAt":"2026-09-04T12:50:26.162Z","schemaVersion":2},"datasetVersion":"2026-09-11T21:17:09.523Z"}