mirror of https://github.com/apache/druid.git
SQL: Add STRING_FORMAT function. (#7327)
This commit is contained in:
parent
28b4e8586d
commit
8c104a115c
|
@ -1023,6 +1023,36 @@ interface Function
|
|||
}
|
||||
}
|
||||
|
||||
class StringFormatFunc implements Function
|
||||
{
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "format";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExprEval apply(List<Expr> args, Expr.ObjectBinding bindings)
|
||||
{
|
||||
if (args.size() < 1) {
|
||||
throw new IAE("Function[%s] needs 1 or more arguments", name());
|
||||
}
|
||||
|
||||
final String formatString = NullHandling.nullToEmptyIfNeeded(args.get(0).eval(bindings).asString());
|
||||
|
||||
if (formatString == null) {
|
||||
return ExprEval.of(null);
|
||||
}
|
||||
|
||||
final Object[] formatArgs = new Object[args.size() - 1];
|
||||
for (int i = 1; i < args.size(); i++) {
|
||||
formatArgs[i - 1] = args.get(i).eval(bindings).value();
|
||||
}
|
||||
|
||||
return ExprEval.of(StringUtils.nonStrictFormat(formatString, formatArgs));
|
||||
}
|
||||
}
|
||||
|
||||
class StrposFunc implements Function
|
||||
{
|
||||
@Override
|
||||
|
|
|
@ -67,6 +67,7 @@ The following built-in functions are available.
|
|||
|name|description|
|
||||
|----|-----------|
|
||||
|concat|concatenate a list of strings|
|
||||
|format|format(pattern[, args...]) returns a string formatted in the manner of Java's [String.format](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#format-java.lang.String-java.lang.Object...-).|
|
||||
|like|like(expr, pattern[, escape]) is equivalent to SQL `expr LIKE pattern`|
|
||||
|lookup|lookup(expr, lookup-name) looks up expr in a registered [query-time lookup](../querying/lookups.html)|
|
||||
|parse_long|parse_long(string[, radix]) parses a string as a long with the given radix, or 10 (decimal) if a radix is not provided.|
|
||||
|
|
|
@ -174,6 +174,7 @@ String functions accept strings, and return a type appropriate to the function.
|
|||
|`x \|\| y`|Concat strings x and y.|
|
||||
|`CONCAT(expr, expr...)`|Concats a list of expressions.|
|
||||
|`TEXTCAT(expr, expr)`|Two argument version of CONCAT.|
|
||||
|`FORMAT(pattern[, args...])`|Returns a string formatted in the manner of Java's [String.format](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#format-java.lang.String-java.lang.Object...-).|
|
||||
|`LENGTH(expr)`|Length of expr in UTF-16 code units.|
|
||||
|`CHAR_LENGTH(expr)`|Synonym for `LENGTH`.|
|
||||
|`CHARACTER_LENGTH(expr)`|Synonym for `LENGTH`.|
|
||||
|
|
|
@ -27,9 +27,11 @@ import org.apache.calcite.sql.SqlFunctionCategory;
|
|||
import org.apache.calcite.sql.SqlKind;
|
||||
import org.apache.calcite.sql.type.OperandTypes;
|
||||
import org.apache.calcite.sql.type.ReturnTypes;
|
||||
import org.apache.calcite.sql.type.SqlOperandTypeChecker;
|
||||
import org.apache.calcite.sql.type.SqlReturnTypeInference;
|
||||
import org.apache.calcite.sql.type.SqlTypeFamily;
|
||||
import org.apache.calcite.sql.type.SqlTypeName;
|
||||
import org.apache.druid.java.util.common.ISE;
|
||||
import org.apache.druid.sql.calcite.planner.Calcites;
|
||||
import org.apache.druid.sql.calcite.planner.PlannerContext;
|
||||
import org.apache.druid.sql.calcite.table.RowSignature;
|
||||
|
@ -116,8 +118,9 @@ public class OperatorConversions
|
|||
private SqlFunctionCategory functionCategory = SqlFunctionCategory.USER_DEFINED_FUNCTION;
|
||||
|
||||
// For operand type checking
|
||||
private SqlOperandTypeChecker operandTypeChecker;
|
||||
private List<SqlTypeFamily> operandTypes;
|
||||
private int requiredOperands = Integer.MAX_VALUE;
|
||||
private Integer requiredOperands = null;
|
||||
|
||||
private OperatorBuilder(final String name)
|
||||
{
|
||||
|
@ -158,6 +161,12 @@ public class OperatorConversions
|
|||
return this;
|
||||
}
|
||||
|
||||
public OperatorBuilder operandTypeChecker(final SqlOperandTypeChecker operandTypeChecker)
|
||||
{
|
||||
this.operandTypeChecker = operandTypeChecker;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OperatorBuilder operandTypes(final SqlTypeFamily... operandTypes)
|
||||
{
|
||||
this.operandTypes = Arrays.asList(operandTypes);
|
||||
|
@ -172,15 +181,25 @@ public class OperatorConversions
|
|||
|
||||
public SqlFunction build()
|
||||
{
|
||||
final SqlOperandTypeChecker theOperandTypeChecker;
|
||||
|
||||
if (operandTypeChecker == null) {
|
||||
theOperandTypeChecker = OperandTypes.family(
|
||||
Preconditions.checkNotNull(operandTypes, "operandTypes"),
|
||||
i -> requiredOperands == null || i + 1 > requiredOperands
|
||||
);
|
||||
} else if (operandTypes == null && requiredOperands == null) {
|
||||
theOperandTypeChecker = operandTypeChecker;
|
||||
} else {
|
||||
throw new ISE("Cannot have both 'operandTypeChecker' and 'operandTypes' / 'requiredOperands'");
|
||||
}
|
||||
|
||||
return new SqlFunction(
|
||||
name,
|
||||
kind,
|
||||
Preconditions.checkNotNull(returnTypeInference, "returnTypeInference"),
|
||||
null,
|
||||
OperandTypes.family(
|
||||
Preconditions.checkNotNull(operandTypes, "operandTypes"),
|
||||
i -> i + 1 > requiredOperands
|
||||
),
|
||||
theOperandTypeChecker,
|
||||
functionCategory
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.druid.sql.calcite.expression.builtin;
|
||||
|
||||
import org.apache.calcite.rel.type.RelDataType;
|
||||
import org.apache.calcite.rex.RexNode;
|
||||
import org.apache.calcite.sql.SqlCallBinding;
|
||||
import org.apache.calcite.sql.SqlFunction;
|
||||
import org.apache.calcite.sql.SqlFunctionCategory;
|
||||
import org.apache.calcite.sql.SqlOperandCountRange;
|
||||
import org.apache.calcite.sql.SqlOperator;
|
||||
import org.apache.calcite.sql.type.SqlOperandCountRanges;
|
||||
import org.apache.calcite.sql.type.SqlOperandTypeChecker;
|
||||
import org.apache.calcite.sql.type.SqlTypeName;
|
||||
import org.apache.druid.java.util.common.StringUtils;
|
||||
import org.apache.druid.sql.calcite.expression.DruidExpression;
|
||||
import org.apache.druid.sql.calcite.expression.OperatorConversions;
|
||||
import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.planner.PlannerContext;
|
||||
import org.apache.druid.sql.calcite.table.RowSignature;
|
||||
|
||||
public class StringFormatOperatorConversion implements SqlOperatorConversion
|
||||
{
|
||||
private static final SqlFunction SQL_FUNCTION = OperatorConversions
|
||||
.operatorBuilder("STRING_FORMAT")
|
||||
.operandTypeChecker(new StringFormatOperandTypeChecker())
|
||||
.functionCategory(SqlFunctionCategory.STRING)
|
||||
.returnType(SqlTypeName.VARCHAR)
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public SqlOperator calciteOperator()
|
||||
{
|
||||
return SQL_FUNCTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DruidExpression toDruidExpression(
|
||||
final PlannerContext plannerContext,
|
||||
final RowSignature rowSignature,
|
||||
final RexNode rexNode
|
||||
)
|
||||
{
|
||||
return OperatorConversions.convertCall(plannerContext, rowSignature, rexNode, "format");
|
||||
}
|
||||
|
||||
private static class StringFormatOperandTypeChecker implements SqlOperandTypeChecker
|
||||
{
|
||||
@Override
|
||||
public boolean checkOperandTypes(SqlCallBinding callBinding, boolean throwOnFailure)
|
||||
{
|
||||
final RelDataType firstArgType = callBinding.getOperandType(0);
|
||||
if (SqlTypeName.CHAR_TYPES.contains(firstArgType.getSqlTypeName())) {
|
||||
return true;
|
||||
} else {
|
||||
if (throwOnFailure) {
|
||||
throw callBinding.newValidationSignatureError();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlOperandCountRange getOperandCountRange()
|
||||
{
|
||||
return SqlOperandCountRanges.from(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAllowedSignatures(SqlOperator op, String opName)
|
||||
{
|
||||
return StringUtils.format("%s(CHARACTER, [ANY, ...])", opName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consistency getConsistency()
|
||||
{
|
||||
return Consistency.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOptional(int i)
|
||||
{
|
||||
return i > 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -61,6 +61,7 @@ import org.apache.druid.sql.calcite.expression.builtin.PositionOperatorConversio
|
|||
import org.apache.druid.sql.calcite.expression.builtin.RTrimOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.ReinterpretOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.StringFormatOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.SubstringOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.TextcatOperatorConversion;
|
||||
|
@ -176,6 +177,7 @@ public class DruidOperatorTable implements SqlOperatorTable
|
|||
.add(new RegexpExtractOperatorConversion())
|
||||
.add(new RTrimOperatorConversion())
|
||||
.add(new ParseLongOperatorConversion())
|
||||
.add(new StringFormatOperatorConversion())
|
||||
.add(new StrposOperatorConversion())
|
||||
.add(new SubstringOperatorConversion())
|
||||
.add(new AliasedOperatorConversion(new SubstringOperatorConversion(), "SUBSTR"))
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.apache.druid.segment.column.ValueType;
|
|||
import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.ParseLongOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.StringFormatOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.TimeExtractOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.TimeFloorOperatorConversion;
|
||||
|
@ -169,6 +170,53 @@ public class ExpressionsTest extends CalciteTestBase
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringFormat()
|
||||
{
|
||||
testExpression(
|
||||
rexBuilder.makeCall(
|
||||
new StringFormatOperatorConversion().calciteOperator(),
|
||||
rexBuilder.makeLiteral("%x"),
|
||||
inputRef("b")
|
||||
),
|
||||
DruidExpression.fromExpression("format('%x',\"b\")"),
|
||||
"19"
|
||||
);
|
||||
|
||||
testExpression(
|
||||
rexBuilder.makeCall(
|
||||
new StringFormatOperatorConversion().calciteOperator(),
|
||||
rexBuilder.makeLiteral("%s %,d"),
|
||||
inputRef("s"),
|
||||
integerLiteral(1234)
|
||||
),
|
||||
DruidExpression.fromExpression("format('%s %,d',\"s\",1234)"),
|
||||
"foo 1,234"
|
||||
);
|
||||
|
||||
testExpression(
|
||||
rexBuilder.makeCall(
|
||||
new StringFormatOperatorConversion().calciteOperator(),
|
||||
rexBuilder.makeLiteral("%s %,d"),
|
||||
inputRef("s")
|
||||
),
|
||||
DruidExpression.fromExpression("format('%s %,d',\"s\")"),
|
||||
"%s %,d; foo"
|
||||
);
|
||||
|
||||
testExpression(
|
||||
rexBuilder.makeCall(
|
||||
new StringFormatOperatorConversion().calciteOperator(),
|
||||
rexBuilder.makeLiteral("%s %,d"),
|
||||
inputRef("s"),
|
||||
integerLiteral(1234),
|
||||
integerLiteral(6789)
|
||||
),
|
||||
DruidExpression.fromExpression("format('%s %,d',\"s\",1234,6789)"),
|
||||
"foo 1,234"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrpos()
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue