diff --git a/.gitignore b/.gitignore index b7a3b059..58c95541 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,6 @@ test-output *.patch *.log.gz *.code-workspace -.idea/*.xml -.idea/libraries/ -.idea/dictionaries/ -.idea/codeStyles/ -.idea/.name +.idea/ # Local config to handle using Java 8 vs java 11. .java-version \ No newline at end of file diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/ConfigUtils.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/ConfigUtils.java new file mode 100644 index 00000000..5b66c9cb --- /dev/null +++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/ConfigUtils.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.query.service; + +import java.util.Optional; +import java.util.function.Supplier; + +public class ConfigUtils { + + public static Optional optionallyGet(Supplier strictGet) { + try { + return Optional.ofNullable(strictGet.get()); + } catch (Throwable t) { + return Optional.empty(); + } + } +} diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/QueryFunctionConstants.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/QueryFunctionConstants.java new file mode 100644 index 00000000..c606f919 --- /dev/null +++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/QueryFunctionConstants.java @@ -0,0 +1,16 @@ +package org.hypertrace.core.query.service; + + +/** + * Canonical query function names received in requests. These should be converted to data-store + * specific names when handling a request. + */ +public interface QueryFunctionConstants { + String QUERY_FUNCTION_SUM = "SUM"; + String QUERY_FUNCTION_AVG = "AVG"; + String QUERY_FUNCTION_MIN = "MIN"; + String QUERY_FUNCTION_MAX = "MAX"; + String QUERY_FUNCTION_COUNT = "COUNT"; + String QUERY_FUNCTION_PERCENTILE = "PERCENTILE"; + String QUERY_FUNCTION_DISTINCTCOUNT = "DISTINCTCOUNT"; +} diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/PinotBasedRequestHandler.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/PinotBasedRequestHandler.java index 1084823f..d10859f2 100644 --- a/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/PinotBasedRequestHandler.java +++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/PinotBasedRequestHandler.java @@ -1,5 +1,7 @@ package org.hypertrace.core.query.service.pinot; +import static org.hypertrace.core.query.service.ConfigUtils.optionallyGet; + import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; @@ -15,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -34,6 +37,7 @@ import org.hypertrace.core.query.service.api.Row.Builder; import org.hypertrace.core.query.service.api.Value; import org.hypertrace.core.query.service.pinot.PinotClientFactory.PinotClient; +import org.hypertrace.core.query.service.pinot.converters.PinotFunctionConverter; import org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,13 +54,7 @@ public class PinotBasedRequestHandler implements RequestHandler customPercentileFunction = + optionallyGet(() -> config.getString(PERCENTILE_AGGREGATION_FUNCTION_CONFIG)); + + customPercentileFunction.ifPresent( + function -> + LOG.info( + "Using {} function for percentile aggregations of handler: {}", function, name)); + PinotFunctionConverter functionConverter = + customPercentileFunction + .map(PinotFunctionConverter::new) + .orElseGet(PinotFunctionConverter::new); this.request2PinotSqlConverter = - new QueryRequestToPinotSQLConverter(viewDefinition, this.percentileAggFunction); + new QueryRequestToPinotSQLConverter(viewDefinition, functionConverter); if (config.hasPath(SLOW_QUERY_THRESHOLD_MS_CONFIG)) { this.slowQueryThreshold = config.getInt(SLOW_QUERY_THRESHOLD_MS_CONFIG); diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverter.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverter.java index 74d1a0cc..cff13922 100644 --- a/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverter.java +++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverter.java @@ -12,7 +12,6 @@ import org.hypertrace.core.query.service.ExecutionContext; import org.hypertrace.core.query.service.api.Expression; import org.hypertrace.core.query.service.api.Filter; -import org.hypertrace.core.query.service.api.Function; import org.hypertrace.core.query.service.api.LiteralConstant; import org.hypertrace.core.query.service.api.Operator; import org.hypertrace.core.query.service.api.OrderByExpression; @@ -20,6 +19,7 @@ import org.hypertrace.core.query.service.api.SortOrder; import org.hypertrace.core.query.service.api.Value; import org.hypertrace.core.query.service.api.ValueType; +import org.hypertrace.core.query.service.pinot.converters.PinotFunctionConverter; import org.hypertrace.core.query.service.pinot.converters.StringToValueConverter; import org.hypertrace.core.query.service.pinot.converters.ToValueConverter; import org.slf4j.Logger; @@ -37,19 +37,21 @@ class QueryRequestToPinotSQLConverter { private static final String MAP_VALUE = "mapValue"; private static final int MAP_KEY_INDEX = 0; private static final int MAP_VALUE_INDEX = 1; - private static final String PERCENTILE_PREFIX = "PERCENTILE"; private final ViewDefinition viewDefinition; - private final String percentileAggFunction; + private final PinotFunctionConverter functionConverter; private final Joiner joiner = Joiner.on(", ").skipNulls(); - QueryRequestToPinotSQLConverter(ViewDefinition viewDefinition, String percentileAggFunc) { + QueryRequestToPinotSQLConverter( + ViewDefinition viewDefinition, PinotFunctionConverter functionConverter) { this.viewDefinition = viewDefinition; - this.percentileAggFunction = percentileAggFunc; + this.functionConverter = functionConverter; } Entry toSQL( - ExecutionContext executionContext, QueryRequest request, LinkedHashSet allSelections) { + ExecutionContext executionContext, + QueryRequest request, + LinkedHashSet allSelections) { Params.Builder paramsBuilder = Params.newBuilder(); StringBuilder pqlBuilder = new StringBuilder("Select "); String delim = ""; @@ -137,7 +139,8 @@ private String convertFilter2String(Filter filter, Params.Builder paramsBuilder) switch (filter.getOperator()) { case LIKE: // The like operation in PQL looks like `regexp_like(lhs, rhs)` - Expression rhs = handleValueConversionForLiteralExpression(filter.getLhs(), filter.getRhs()); + Expression rhs = + handleValueConversionForLiteralExpression(filter.getLhs(), filter.getRhs()); builder.append(operator); builder.append("("); builder.append(convertExpression2String(filter.getLhs(), paramsBuilder)); @@ -203,14 +206,18 @@ private Expression handleValueConversionForLiteralExpression(Expression lhs, Exp ToValueConverter converter = getValueConverter(rhs.getLiteral().getValue().getValueType()); if (converter != null) { try { - Value value = converter.convert(rhs.getLiteral().getValue().getString(), + Value value = + converter.convert( + rhs.getLiteral().getValue().getString(), viewDefinition.getColumnType(lhsColumnName)); - newRhs = Expression.newBuilder() - .setLiteral(LiteralConstant.newBuilder().setValue(value)) - .build(); + newRhs = + Expression.newBuilder() + .setLiteral(LiteralConstant.newBuilder().setValue(value)) + .build(); } catch (Exception e) { throw new IllegalArgumentException( - String.format("Invalid string input:{ %s } for bytes column:{ %s }", + String.format( + "Invalid string input:{ %s } for bytes column:{ %s }", rhs.getLiteral().getValue().getString(), viewDefinition.getPhysicalColumnNames(lhsColumnName).get(0))); } @@ -279,22 +286,9 @@ private String convertExpression2String(Expression expression, Params.Builder pa case LITERAL: return convertLiteralToString(expression.getLiteral(), paramsBuilder); case FUNCTION: - Function function = expression.getFunction(); - String functionName = function.getFunctionName(); - // For COUNT(column_name), Pinot sql format converts it to COUNT(*) and even only works with - // COUNT(*) for ORDER BY - if (functionName.equalsIgnoreCase("COUNT")) { - return functionName + "(*)"; - } else if (functionName.startsWith(PERCENTILE_PREFIX) && !PERCENTILE_PREFIX.equals(functionName)) { - functionName = functionName.replaceFirst(PERCENTILE_PREFIX, percentileAggFunction); - } - List argumentsList = function.getArgumentsList(); - String[] args = new String[argumentsList.size()]; - for (int i = 0; i < argumentsList.size(); i++) { - Expression expr = argumentsList.get(i); - args[i] = convertExpression2String(expr, paramsBuilder); - } - return functionName + "(" + joiner.join(args) + ")"; + return this.functionConverter.convert( + expression.getFunction(), + argExpression -> convertExpression2String(argExpression, paramsBuilder)); case ORDERBY: OrderByExpression orderBy = expression.getOrderBy(); return convertExpression2String(orderBy.getExpression(), paramsBuilder); diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/converters/PinotFunctionConverter.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/converters/PinotFunctionConverter.java new file mode 100644 index 00000000..0388e017 --- /dev/null +++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/pinot/converters/PinotFunctionConverter.java @@ -0,0 +1,130 @@ +package org.hypertrace.core.query.service.pinot.converters; + +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_COUNT; +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_PERCENTILE; + +import java.util.Optional; +import java.util.stream.Collectors; +import org.hypertrace.core.query.service.api.Expression; +import org.hypertrace.core.query.service.api.Function; +import org.hypertrace.core.query.service.api.LiteralConstant; +import org.hypertrace.core.query.service.api.Value; +import org.hypertrace.core.query.service.api.ValueType; + +public class PinotFunctionConverter { + /** + * Computing PERCENTILE in Pinot is resource intensive. T-Digest calculation is much faster and + * reasonably accurate, hence use that as the default. + */ + private static final String DEFAULT_PERCENTILE_AGGREGATION_FUNCTION = "PERCENTILETDIGEST"; + + private final String percentileAggFunction; + + public PinotFunctionConverter(String configuredPercentileFunction) { + this.percentileAggFunction = + Optional.ofNullable(configuredPercentileFunction) + .orElse(DEFAULT_PERCENTILE_AGGREGATION_FUNCTION); + } + + public PinotFunctionConverter() { + this.percentileAggFunction = DEFAULT_PERCENTILE_AGGREGATION_FUNCTION; + } + + public String convert( + Function function, java.util.function.Function argumentConverter) { + switch (function.getFunctionName().toUpperCase()) { + case QUERY_FUNCTION_COUNT: + return this.convertCount(); + case QUERY_FUNCTION_PERCENTILE: + return this.functionToString(this.toPinotPercentile(function), argumentConverter); + default: + // TODO remove once pinot-specific logic removed from gateway - this normalization reverts + // that logic + if (this.isHardcodedPercentile(function)) { + return this.convert(this.normalizeHardcodedPercentile(function), argumentConverter); + } + return this.functionToString(function, argumentConverter); + } + } + + private String functionToString( + Function function, java.util.function.Function argumentConverter) { + String argumentString = + function.getArgumentsList().stream() + .map(argumentConverter::apply) + .collect(Collectors.joining(",")); + + return function.getFunctionName() + "(" + argumentString + ")"; + } + + private String convertCount() { + return "COUNT(*)"; + } + + private Function toPinotPercentile(Function function) { + int percentileValue = + this.getPercentileValueFromFunction(function) + .orElseThrow( + () -> + new UnsupportedOperationException( + String.format( + "%s must include an integer convertible value as its first argument. Got: %s", + QUERY_FUNCTION_PERCENTILE, function.getArguments(0)))); + return Function.newBuilder(function) + .removeArguments(0) + .setFunctionName(this.percentileAggFunction + percentileValue) + .build(); + } + + private boolean isHardcodedPercentile(Function function) { + String functionName = function.getFunctionName().toUpperCase(); + return functionName.startsWith(QUERY_FUNCTION_PERCENTILE) + && this.getPercentileValueFromName(functionName).isPresent(); + } + + private Function normalizeHardcodedPercentile(Function function) { + // Verified in isHardcodedPercentile + int percentileValue = this.getPercentileValueFromName(function.getFunctionName()).orElseThrow(); + return Function.newBuilder(function) + .setFunctionName(QUERY_FUNCTION_PERCENTILE) + .addArguments(0, this.literalInt(percentileValue)) + .build(); + } + + private Optional getPercentileValueFromName(String functionName) { + try { + return Optional.of( + Integer.parseInt(functionName.substring(QUERY_FUNCTION_PERCENTILE.length()))); + } catch (Throwable t) { + return Optional.empty(); + } + } + + private Optional getPercentileValueFromFunction(Function percentileFunction) { + return Optional.of(percentileFunction) + .filter(function -> function.getArgumentsCount() > 0) + .map(function -> function.getArguments(0)) + .map(Expression::getLiteral) + .map(LiteralConstant::getValue) + .flatMap(this::intFromValue); + } + + Expression literalInt(int value) { + return Expression.newBuilder() + .setLiteral( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.INT).setInt(value))) + .build(); + } + + Optional intFromValue(Value value) { + switch (value.getValueType()) { + case INT: + return Optional.of(value.getInt()); + case LONG: + return Optional.of(Math.toIntExact(value.getLong())); + default: + return Optional.empty(); + } + } +} diff --git a/query-service-impl/src/test/java/org/hypertrace/core/query/service/ConfigUtilsTest.java b/query-service-impl/src/test/java/org/hypertrace/core/query/service/ConfigUtilsTest.java new file mode 100644 index 00000000..95622b25 --- /dev/null +++ b/query-service-impl/src/test/java/org/hypertrace/core/query/service/ConfigUtilsTest.java @@ -0,0 +1,25 @@ +package org.hypertrace.core.query.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class ConfigUtilsTest { + + @Test + void optionallyGetReturnsEmptyIfThrows() { + assertEquals( + Optional.empty(), + ConfigUtils.optionallyGet( + () -> { + throw new RuntimeException(); + })); + } + + @Test + void optionallyGetReturnsValueIfProvided() { + assertEquals(Optional.of("value"), ConfigUtils.optionallyGet(() -> "value")); + assertEquals(Optional.of(10), ConfigUtils.optionallyGet(() -> 10)); + } +} diff --git a/query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverterTest.java b/query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverterTest.java index 701fcf79..90118021 100644 --- a/query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverterTest.java +++ b/query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/QueryRequestToPinotSQLConverterTest.java @@ -27,6 +27,7 @@ import org.hypertrace.core.query.service.api.Value; import org.hypertrace.core.query.service.api.ValueType; import org.hypertrace.core.query.service.pinot.PinotClientFactory.PinotClient; +import org.hypertrace.core.query.service.pinot.converters.PinotFunctionConverter; import org.hypertrace.core.query.service.util.QueryRequestUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -1093,7 +1094,7 @@ private QueryRequest buildSimpleQueryWithFilter(Filter filter) { private void assertPQLQuery(QueryRequest queryRequest, String expectedQuery) { QueryRequestToPinotSQLConverter converter = - new QueryRequestToPinotSQLConverter(viewDefinition, "PERCENTILETDIGEST"); + new QueryRequestToPinotSQLConverter(viewDefinition, new PinotFunctionConverter()); Entry statementToParam = converter.toSQL(new ExecutionContext("__default", queryRequest), queryRequest, createSelectionsFromQueryRequest(queryRequest)); @@ -1108,7 +1109,7 @@ private void assertPQLQuery(QueryRequest queryRequest, String expectedQuery) { private void assertExceptionOnPQLQuery(QueryRequest queryRequest, Class className, String expectedMessage) { QueryRequestToPinotSQLConverter converter = - new QueryRequestToPinotSQLConverter(viewDefinition, "PERCENTILETDIGEST"); + new QueryRequestToPinotSQLConverter(viewDefinition, new PinotFunctionConverter()); Throwable exception = Assertions.assertThrows(className, () -> converter .toSQL(new ExecutionContext("__default", queryRequest), queryRequest, diff --git a/query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/converters/PinotFunctionConverterTest.java b/query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/converters/PinotFunctionConverterTest.java new file mode 100644 index 00000000..3a2f9004 --- /dev/null +++ b/query-service-impl/src/test/java/org/hypertrace/core/query/service/pinot/converters/PinotFunctionConverterTest.java @@ -0,0 +1,170 @@ +package org.hypertrace.core.query.service.pinot.converters; + +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_AVG; +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_COUNT; +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_DISTINCTCOUNT; +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_MAX; +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_MIN; +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_PERCENTILE; +import static org.hypertrace.core.query.service.QueryFunctionConstants.QUERY_FUNCTION_SUM; +import static org.hypertrace.core.query.service.QueryRequestBuilderUtils.createColumnExpression; +import static org.hypertrace.core.query.service.QueryRequestBuilderUtils.createIntLiteralValueExpression; +import static org.hypertrace.core.query.service.QueryRequestBuilderUtils.createLongLiteralValueExpression; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.stream.Collectors; +import org.hypertrace.core.query.service.api.Expression; +import org.hypertrace.core.query.service.api.Expression.Builder; +import org.hypertrace.core.query.service.api.Function; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PinotFunctionConverterTest { + + @Mock java.util.function.Function mockArgumentConverter; + + @Test + void convertsCountStar() { + String expected = "COUNT(*)"; + Function countFunction = buildFunction(QUERY_FUNCTION_COUNT, createColumnExpression("foo")); + + assertEquals( + expected, new PinotFunctionConverter().convert(countFunction, this.mockArgumentConverter)); + + // Should not be case sensitive + Function lowerCaseCountFunction = buildFunction("count", createColumnExpression("foo")); + assertEquals( + expected, + new PinotFunctionConverter().convert(lowerCaseCountFunction, this.mockArgumentConverter)); + } + + @Test + void convertsPercentileWithValueArg() { + String expected = "PERCENTILETDIGEST90(fooOut)"; + Expression columnExpression = createColumnExpression("fooIn").build(); + Function intPercentileFunction = + buildFunction( + QUERY_FUNCTION_PERCENTILE, + createIntLiteralValueExpression(90).toBuilder(), + columnExpression.toBuilder()); + when(this.mockArgumentConverter.apply(columnExpression)).thenReturn("fooOut"); + + assertEquals( + expected, + new PinotFunctionConverter().convert(intPercentileFunction, this.mockArgumentConverter)); + + // Should not be case sensitive + Function lowerCasePercentileFunction = + intPercentileFunction.toBuilder().setFunctionName("percentile").build(); + assertEquals( + expected, + new PinotFunctionConverter() + .convert(lowerCasePercentileFunction, this.mockArgumentConverter)); + + // Should work with longs + Function longPercentileFunction = + intPercentileFunction.toBuilder() + .setArguments(0, createLongLiteralValueExpression(90)) + .build(); + assertEquals( + expected, + new PinotFunctionConverter().convert(longPercentileFunction, this.mockArgumentConverter)); + } + + @Test + void acceptsCustomPercentileFunctions() { + String expected = "CUSTOMPERCENTILE90(foo)"; + Function percentileFunction = + buildFunction( + QUERY_FUNCTION_PERCENTILE, + createIntLiteralValueExpression(90).toBuilder(), + createColumnExpression("foo")); + when(this.mockArgumentConverter.apply(any(Expression.class))).thenReturn("foo"); + + assertEquals( + expected, + new PinotFunctionConverter("CUSTOMPERCENTILE") + .convert(percentileFunction, this.mockArgumentConverter)); + } + + @Test + void errorsOnInvalidPercentileValueArg() { + Function percentileFunctionWithoutValue = + buildFunction(QUERY_FUNCTION_PERCENTILE, createColumnExpression("foo")); + + assertThrows( + UnsupportedOperationException.class, + () -> + new PinotFunctionConverter() + .convert(percentileFunctionWithoutValue, this.mockArgumentConverter)); + } + + @Test + void backwardsCompatibleWithPercentileValueFunction() { + String expected = "PERCENTILETDIGEST90(foo)"; + Function percentileFunction = buildFunction("PERCENTILE90", createColumnExpression("foo")); + when(this.mockArgumentConverter.apply(any(Expression.class))).thenReturn("foo"); + + assertEquals( + expected, + new PinotFunctionConverter().convert(percentileFunction, this.mockArgumentConverter)); + } + + @Test + void convertsBasicFunctions() { + PinotFunctionConverter converter = new PinotFunctionConverter(); + + when(this.mockArgumentConverter.apply(any(Expression.class))).thenReturn("foo"); + assertEquals( + "SUM(foo)", + converter.convert( + buildFunction(QUERY_FUNCTION_SUM, createColumnExpression("foo")), + this.mockArgumentConverter)); + + assertEquals( + "AVG(foo)", + converter.convert( + buildFunction(QUERY_FUNCTION_AVG, createColumnExpression("foo")), + this.mockArgumentConverter)); + assertEquals( + "DISTINCTCOUNT(foo)", + converter.convert( + buildFunction(QUERY_FUNCTION_DISTINCTCOUNT, createColumnExpression("foo")), + this.mockArgumentConverter)); + assertEquals( + "MAX(foo)", + converter.convert( + buildFunction(QUERY_FUNCTION_MAX, createColumnExpression("foo")), + this.mockArgumentConverter)); + assertEquals( + "MIN(foo)", + converter.convert( + buildFunction(QUERY_FUNCTION_MIN, createColumnExpression("foo")), + this.mockArgumentConverter)); + } + + @Test + void convertsUnknownFunctions() { + when(this.mockArgumentConverter.apply(any(Expression.class))).thenReturn("foo"); + assertEquals( + "UNKNOWN(foo)", + new PinotFunctionConverter() + .convert( + buildFunction("UNKNOWN", createColumnExpression("foo")), + this.mockArgumentConverter)); + } + + private Function buildFunction(String name, Builder... arguments) { + return Function.newBuilder() + .setFunctionName(name) + .addAllArguments(Arrays.stream(arguments).map(Builder::build).collect(Collectors.toList())) + .build(); + } +}