Skip to content

Commit 283529f

Browse files
authored
[Backport] Allow casted literal values in SQL functions accepting literals (Part 2) (#15322)
Backport of #15316 to 28.0.0.
1 parent 80dc45e commit 283529f

9 files changed

Lines changed: 438 additions & 10 deletions

File tree

codestyle/druid-forbidden-apis.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ java.util.Random#<init>() @ Use ThreadLocalRandom.current() or the constructor w
4545
java.lang.Math#random() @ Use ThreadLocalRandom.current()
4646
java.util.regex.Pattern#matches(java.lang.String,java.lang.CharSequence) @ Use String.startsWith(), endsWith(), contains(), or compile and cache a Pattern explicitly
4747
org.apache.calcite.sql.type.OperandTypes#LITERAL @ LITERAL type checker throws when literals with CAST are passed. Use org.apache.druid.sql.calcite.expression.DefaultOperandTypeChecker instead.
48+
org.apache.calcite.sql.type.OperandTypes#BOOLEAN_LITERAL @ Create a type checker like org.apache.calcite.sql.type.POSITIVE_INTEGER_LITERAL and use that instead
49+
org.apache.calcite.sql.type.OperandTypes#ARRAY_BOOLEAN_LITERAL @ Create a type checker like org.apache.calcite.sql.type.POSITIVE_INTEGER_LITERAL and use that instead
50+
org.apache.calcite.sql.type.OperandTypes#POSITIVE_INTEGER_LITERAL @ Use org.apache.calcite.sql.type.POSITIVE_INTEGER_LITERAL instead
51+
org.apache.calcite.sql.type.OperandTypes#UNIT_INTERVAL_NUMERIC_LITERAL @ Create a type checker like org.apache.calcite.sql.type.POSITIVE_INTEGER_LITERAL and use that instead
52+
org.apache.calcite.sql.type.OperandTypes#NUMERIC_UNIT_INTERVAL_NUMERIC_LITERAL @ Create a type checker like org.apache.calcite.sql.type.POSITIVE_INTEGER_LITERAL and use that instead
53+
org.apache.calcite.sql.type.OperandTypes#NULLABLE_LITERAL @ Create an instance of org.apache.calcite.sql.type.CastedLiteralOperandTypeChecker that allows nulls and use that instead
4854
org.apache.commons.io.FileUtils#getTempDirectory() @ Use org.junit.rules.TemporaryFolder for tests instead
4955
org.apache.commons.io.FileUtils#deleteDirectory(java.io.File) @ Use org.apache.druid.java.util.common.FileUtils#deleteDirectory()
5056
org.apache.commons.io.FileUtils#forceMkdir(java.io.File) @ Use org.apache.druid.java.util.common.FileUtils.mkdirp instead

extensions-contrib/compressed-bigdecimal/src/main/java/org/apache/druid/compressedbigdecimal/CompressedBigDecimalSqlAggregatorBase.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.calcite.sql.SqlAggFunction;
2727
import org.apache.calcite.sql.SqlFunctionCategory;
2828
import org.apache.calcite.sql.SqlKind;
29+
import org.apache.calcite.sql.type.CastedLiteralOperandTypeCheckers;
2930
import org.apache.calcite.sql.type.OperandTypes;
3031
import org.apache.calcite.sql.type.ReturnTypes;
3132
import org.apache.calcite.sql.type.SqlTypeFamily;
@@ -156,25 +157,25 @@ private CompressedBigDecimalSqlAggFunction(String name)
156157
OperandTypes.sequence(
157158
"'" + name + "(column, size)'",
158159
OperandTypes.ANY,
159-
OperandTypes.POSITIVE_INTEGER_LITERAL
160+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL
160161
),
161162
OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.EXACT_NUMERIC)
162163
),
163164
OperandTypes.and(
164165
OperandTypes.sequence(
165166
"'" + name + "(column, size, scale)'",
166167
OperandTypes.ANY,
167-
OperandTypes.POSITIVE_INTEGER_LITERAL,
168-
OperandTypes.POSITIVE_INTEGER_LITERAL
168+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL,
169+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL
169170
),
170171
OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.EXACT_NUMERIC, SqlTypeFamily.EXACT_NUMERIC)
171172
),
172173
OperandTypes.and(
173174
OperandTypes.sequence(
174175
"'" + name + "(column, size, scale, strictNumberParsing)'",
175176
OperandTypes.ANY,
176-
OperandTypes.POSITIVE_INTEGER_LITERAL,
177-
OperandTypes.POSITIVE_INTEGER_LITERAL,
177+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL,
178+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL,
178179
OperandTypes.BOOLEAN
179180
),
180181
OperandTypes.family(
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
//CHECKSTYLE.OFF: PackageName - Must be in Calcite
21+
22+
package org.apache.calcite.sql.type;
23+
24+
import org.apache.calcite.sql.SqlCallBinding;
25+
import org.apache.calcite.sql.SqlNode;
26+
import org.apache.calcite.sql.SqlOperator;
27+
import org.apache.calcite.sql.SqlUtil;
28+
import org.apache.calcite.util.Static;
29+
import org.apache.calcite.util.Util;
30+
31+
/**
32+
* Like {@link LiteralOperandTypeChecker}, but also allows casted literals.
33+
*
34+
* "Casted literals" are like `CAST(100 AS INTEGER)`. While it doesn't make sense to cast a literal that the user
35+
* themselves enter, it is important to add a broader validation to allow these literals because Calcite's JDBC driver
36+
* doesn't allow the wildcards (?)to work without a cast, and there's no workaround it.
37+
* <p>
38+
* This makes sure that the functions using the literal operand type checker can be workaround the JDBC's restriction,
39+
* without being marked as invalid SQL input
40+
*/
41+
42+
public class CastedLiteralOperandTypeChecker implements SqlSingleOperandTypeChecker
43+
{
44+
public static SqlSingleOperandTypeChecker LITERAL = new CastedLiteralOperandTypeChecker(false);
45+
46+
private final boolean allowNull;
47+
48+
CastedLiteralOperandTypeChecker(boolean allowNull)
49+
{
50+
this.allowNull = allowNull;
51+
}
52+
53+
@Override
54+
public boolean checkSingleOperandType(
55+
SqlCallBinding callBinding,
56+
SqlNode node,
57+
int iFormalOperand,
58+
boolean throwOnFailure
59+
)
60+
{
61+
Util.discard(iFormalOperand);
62+
63+
if (SqlUtil.isNullLiteral(node, true)) {
64+
if (allowNull) {
65+
return true;
66+
}
67+
if (throwOnFailure) {
68+
throw callBinding.newError(
69+
Static.RESOURCE.argumentMustNotBeNull(
70+
callBinding.getOperator().getName()));
71+
}
72+
return false;
73+
}
74+
// The following line of code is the only difference between the OperandTypes.LITERAL and this type checker
75+
if (!SqlUtil.isLiteral(node, true) && !SqlUtil.isLiteralChain(node)) {
76+
if (throwOnFailure) {
77+
throw callBinding.newError(
78+
Static.RESOURCE.argumentMustBeLiteral(
79+
callBinding.getOperator().getName()));
80+
}
81+
return false;
82+
}
83+
84+
return true;
85+
}
86+
87+
@Override
88+
public String getAllowedSignatures(SqlOperator op, String opName)
89+
{
90+
return "<LITERAL>";
91+
}
92+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
//CHECKSTYLE.OFF: PackageName - Must be in Calcite
21+
22+
package org.apache.calcite.sql.type;
23+
24+
import com.google.common.collect.ImmutableList;
25+
import org.apache.calcite.sql.SqlCall;
26+
import org.apache.calcite.sql.SqlCallBinding;
27+
import org.apache.calcite.sql.SqlLiteral;
28+
import org.apache.calcite.sql.SqlNode;
29+
import org.apache.calcite.sql.parser.SqlParserPos;
30+
import org.apache.calcite.util.Static;
31+
import org.apache.druid.error.DruidException;
32+
33+
import java.math.BigDecimal;
34+
35+
public class CastedLiteralOperandTypeCheckers
36+
{
37+
public static final SqlSingleOperandTypeChecker LITERAL = new CastedLiteralOperandTypeChecker(false);
38+
39+
/**
40+
* Blatantly copied from {@link OperandTypes#POSITIVE_INTEGER_LITERAL}, however the reference to the {@link #LITERAL}
41+
* is the one which accepts casted literals
42+
*/
43+
public static final SqlSingleOperandTypeChecker POSITIVE_INTEGER_LITERAL =
44+
new FamilyOperandTypeChecker(
45+
ImmutableList.of(SqlTypeFamily.INTEGER),
46+
i -> false
47+
)
48+
{
49+
@Override
50+
public boolean checkSingleOperandType(
51+
SqlCallBinding callBinding,
52+
SqlNode operand,
53+
int iFormalOperand,
54+
SqlTypeFamily family,
55+
boolean throwOnFailure
56+
)
57+
{
58+
// This LITERAL refers to the above implementation, the one which allows casted literals
59+
if (!LITERAL.checkSingleOperandType(
60+
callBinding,
61+
operand,
62+
iFormalOperand,
63+
throwOnFailure
64+
)) {
65+
return false;
66+
}
67+
68+
if (!super.checkSingleOperandType(
69+
callBinding,
70+
operand,
71+
iFormalOperand,
72+
family,
73+
throwOnFailure
74+
)) {
75+
return false;
76+
}
77+
78+
final SqlLiteral arg = fetchPrimitiveLiteralFromCasts(operand);
79+
final BigDecimal value = arg.getValueAs(BigDecimal.class);
80+
if (value.compareTo(BigDecimal.ZERO) < 0
81+
|| hasFractionalPart(value)) {
82+
if (throwOnFailure) {
83+
throw callBinding.newError(
84+
Static.RESOURCE.argumentMustBePositiveInteger(
85+
callBinding.getOperator().getName()));
86+
}
87+
return false;
88+
}
89+
if (value.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE)) > 0) {
90+
if (throwOnFailure) {
91+
throw callBinding.newError(
92+
Static.RESOURCE.numberLiteralOutOfRange(value.toString()));
93+
}
94+
return false;
95+
}
96+
return true;
97+
}
98+
99+
/** Returns whether a number has any fractional part.
100+
*
101+
* @see BigDecimal#longValueExact() */
102+
private boolean hasFractionalPart(BigDecimal bd)
103+
{
104+
return bd.precision() - bd.scale() <= 0;
105+
}
106+
};
107+
108+
public static boolean isLiteral(SqlNode node, boolean allowCast)
109+
{
110+
assert node != null;
111+
if (node instanceof SqlLiteral) {
112+
return true;
113+
}
114+
if (!allowCast) {
115+
return false;
116+
}
117+
switch (node.getKind()) {
118+
case CAST:
119+
// "CAST(e AS type)" is literal if "e" is literal
120+
return isLiteral(((SqlCall) node).operand(0), true);
121+
case MAP_VALUE_CONSTRUCTOR:
122+
case ARRAY_VALUE_CONSTRUCTOR:
123+
return ((SqlCall) node).getOperandList().stream()
124+
.allMatch(o -> isLiteral(o, true));
125+
case DEFAULT:
126+
return true; // DEFAULT is always NULL
127+
default:
128+
return false;
129+
}
130+
}
131+
132+
/**
133+
* Fetches primitive literals from the casts, including NULL literal.
134+
* It throws if the entered node isn't a primitive literal, which can be cast multiple times.
135+
*
136+
* Therefore, it would fail on the following types:
137+
* 1. Nodes that are not of the form CAST(....(CAST LITERAL AS TYPE).....)
138+
* 2. ARRAY and MAP literals. This won't be required since we are only using this method in the type checker for
139+
* primitive types
140+
*/
141+
private static SqlLiteral fetchPrimitiveLiteralFromCasts(SqlNode node)
142+
{
143+
if (node == null) {
144+
throw DruidException.defensive("'node' cannot be null");
145+
}
146+
if (node instanceof SqlLiteral) {
147+
return (SqlLiteral) node;
148+
}
149+
150+
switch (node.getKind()) {
151+
case CAST:
152+
return fetchPrimitiveLiteralFromCasts(((SqlCall) node).operand(0));
153+
case DEFAULT:
154+
return SqlLiteral.createNull(SqlParserPos.ZERO);
155+
default:
156+
throw DruidException.defensive("Expected a literal or a cast on the literal. Found [%s] instead", node.getKind());
157+
}
158+
}
159+
}

sql/src/main/java/org/apache/druid/sql/calcite/aggregation/builtin/ArrayConcatSqlAggregator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.calcite.sql.SqlAggFunction;
2727
import org.apache.calcite.sql.SqlFunctionCategory;
2828
import org.apache.calcite.sql.SqlKind;
29+
import org.apache.calcite.sql.type.CastedLiteralOperandTypeCheckers;
2930
import org.apache.calcite.sql.type.InferTypes;
3031
import org.apache.calcite.sql.type.OperandTypes;
3132
import org.apache.calcite.sql.type.ReturnTypes;
@@ -156,7 +157,7 @@ private static class ArrayConcatAggFunction extends SqlAggFunction
156157
OperandTypes.sequence(
157158
StringUtils.format("'%s(expr, maxSizeBytes)'", NAME),
158159
OperandTypes.ARRAY,
159-
OperandTypes.POSITIVE_INTEGER_LITERAL
160+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL
160161
)
161162
),
162163
SqlFunctionCategory.USER_DEFINED_FUNCTION,

sql/src/main/java/org/apache/druid/sql/calcite/aggregation/builtin/ArraySqlAggregator.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.calcite.sql.SqlFunctionCategory;
2929
import org.apache.calcite.sql.SqlKind;
3030
import org.apache.calcite.sql.SqlOperatorBinding;
31+
import org.apache.calcite.sql.type.CastedLiteralOperandTypeCheckers;
3132
import org.apache.calcite.sql.type.InferTypes;
3233
import org.apache.calcite.sql.type.OperandTypes;
3334
import org.apache.calcite.sql.type.SqlReturnTypeInference;
@@ -179,7 +180,11 @@ private static class ArrayAggFunction extends SqlAggFunction
179180
OperandTypes.or(
180181
OperandTypes.ANY,
181182
OperandTypes.and(
182-
OperandTypes.sequence(StringUtils.format("'%s(expr, maxSizeBytes)'", NAME), OperandTypes.ANY, OperandTypes.POSITIVE_INTEGER_LITERAL),
183+
OperandTypes.sequence(
184+
StringUtils.format("'%s(expr, maxSizeBytes)'", NAME),
185+
OperandTypes.ANY,
186+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL
187+
),
183188
OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.NUMERIC)
184189
)
185190
),

sql/src/main/java/org/apache/druid/sql/calcite/aggregation/builtin/StringSqlAggregator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.apache.calcite.sql.SqlFunctionCategory;
3030
import org.apache.calcite.sql.SqlKind;
3131
import org.apache.calcite.sql.SqlOperatorBinding;
32+
import org.apache.calcite.sql.type.CastedLiteralOperandTypeCheckers;
3233
import org.apache.calcite.sql.type.InferTypes;
3334
import org.apache.calcite.sql.type.OperandTypes;
3435
import org.apache.calcite.sql.type.SqlReturnTypeInference;
@@ -251,7 +252,7 @@ private static class StringAggFunction extends SqlAggFunction
251252
StringUtils.format("'%s(expr, separator, maxSizeBytes)'", name),
252253
OperandTypes.ANY,
253254
OperandTypes.STRING,
254-
OperandTypes.POSITIVE_INTEGER_LITERAL
255+
CastedLiteralOperandTypeCheckers.POSITIVE_INTEGER_LITERAL
255256
),
256257
OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.STRING, SqlTypeFamily.NUMERIC)
257258
)

0 commit comments

Comments
 (0)