This is an automated email from the ASF dual-hosted git repository.
gian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 6c18434028b CONCAT flattening, filter decomposition. (#15634)
6c18434028b is described below
commit 6c18434028b46aba9a4c25f5834005d468f9ac6f
Author: Gian Merlino <[email protected]>
AuthorDate: Thu Jan 11 11:18:50 2024 -0800
CONCAT flattening, filter decomposition. (#15634)
* CONCAT flattening, filter decomposition.
Flattening: CONCAT(CONCAT(x, y), z) is flattened to CONCAT(x, y, z). This
is especially useful for the || operator, which is a binary operator and
leads to non-flat CONCAT calls.
Filter decomposition: transforms CONCAT(x, '-', y) = 'a-b' into
x = 'a' AND y = 'b'.
* One more test.
* Fix two tests.
* Adjustments from review.
* Fix empty string problem, add tests.
---
.../builtin/ConcatOperatorConversion.java | 2 +-
.../builtin/TextcatOperatorConversion.java | 2 +-
.../sql/calcite/planner/CalciteRulesManager.java | 111 ++++---
.../calcite/rule/FilterDecomposeConcatRule.java | 296 +++++++++++++++++
.../druid/sql/calcite/rule/FlattenConcatRule.java | 133 ++++++++
.../calcite/CalciteMultiValueStringQueryTest.java | 4 +-
.../apache/druid/sql/calcite/CalciteQueryTest.java | 207 +++++++++++-
.../rule/FilterDecomposeConcatRuleTest.java | 369 +++++++++++++++++++++
8 files changed, 1066 insertions(+), 58 deletions(-)
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ConcatOperatorConversion.java
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ConcatOperatorConversion.java
index 8ba2ca6dcb8..39428dc20d7 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ConcatOperatorConversion.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ConcatOperatorConversion.java
@@ -28,7 +28,7 @@ import
org.apache.druid.sql.calcite.expression.OperatorConversions;
public class ConcatOperatorConversion extends DirectOperatorConversion
{
- private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ public static final SqlFunction SQL_FUNCTION = OperatorConversions
.operatorBuilder("CONCAT")
.operandTypeChecker(OperandTypes.SAME_VARIADIC)
.returnTypeCascadeNullable(SqlTypeName.VARCHAR)
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TextcatOperatorConversion.java
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TextcatOperatorConversion.java
index 40a13479b8c..acdbc1029ca 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TextcatOperatorConversion.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TextcatOperatorConversion.java
@@ -28,7 +28,7 @@ import
org.apache.druid.sql.calcite.expression.OperatorConversions;
public class TextcatOperatorConversion extends DirectOperatorConversion
{
- private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ public static final SqlFunction SQL_FUNCTION = OperatorConversions
.operatorBuilder("textcat")
.operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
.requiredOperandCount(2)
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
index 06c73fa9d3b..78d8ca7d098 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
@@ -31,6 +31,7 @@ import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.plan.hep.HepProgram;
import org.apache.calcite.plan.hep.HepProgramBuilder;
import org.apache.calcite.plan.volcano.AbstractConverter;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.RelFactories;
import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider;
@@ -55,13 +56,16 @@ import org.apache.druid.sql.calcite.rule.DruidRules;
import org.apache.druid.sql.calcite.rule.DruidTableScanRule;
import org.apache.druid.sql.calcite.rule.ExtensionCalciteRuleProvider;
import org.apache.druid.sql.calcite.rule.FilterDecomposeCoalesceRule;
+import org.apache.druid.sql.calcite.rule.FilterDecomposeConcatRule;
import org.apache.druid.sql.calcite.rule.FilterJoinExcludePushToChildRule;
+import org.apache.druid.sql.calcite.rule.FlattenConcatRule;
import org.apache.druid.sql.calcite.rule.ProjectAggregatePruneUnusedCallRule;
import org.apache.druid.sql.calcite.rule.SortCollapseRule;
import org.apache.druid.sql.calcite.rule.logical.DruidLogicalRules;
import org.apache.druid.sql.calcite.run.EngineFeature;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -88,7 +92,7 @@ public class CalciteRulesManager
* 3) {@link CoreRules#JOIN_COMMUTE}, {@link JoinPushThroughJoinRule#RIGHT},
{@link JoinPushThroughJoinRule#LEFT},
* and {@link CoreRules#FILTER_INTO_JOIN}, which are part of {@link
#FANCY_JOIN_RULES}.
* 4) {@link CoreRules#PROJECT_FILTER_TRANSPOSE} because PartialDruidQuery
would like to have the Project on top of the Filter -
- * this rule could create a lot of non-usefull plans.
+ * this rule could create a lot of non-useful plans.
*/
private static final List<RelOptRule> BASE_RULES =
ImmutableList.of(
@@ -228,50 +232,87 @@ public class CalciteRulesManager
public List<Program> programs(final PlannerContext plannerContext)
{
final boolean isDebug = plannerContext.queryContext().isDebug();
-
- // Program that pre-processes the tree before letting the full-on
VolcanoPlanner loose.
- final List<Program> prePrograms = new ArrayList<>();
- prePrograms.add(new LoggingProgram("Start", isDebug));
- prePrograms.add(Programs.subQuery(DefaultRelMetadataProvider.INSTANCE));
- prePrograms.add(new LoggingProgram("Finished subquery program", isDebug));
- prePrograms.add(DecorrelateAndTrimFieldsProgram.INSTANCE);
- prePrograms.add(new LoggingProgram("Finished decorrelate and trim fields
program", isDebug));
- prePrograms.add(buildCoalesceProgram());
- prePrograms.add(new LoggingProgram("Finished coalesce program", isDebug));
- prePrograms.add(buildReductionProgram(plannerContext));
- prePrograms.add(new LoggingProgram("Finished expression reduction
program", isDebug));
-
- final Program preProgram = Programs.sequence(prePrograms.toArray(new
Program[0]));
+ final Program druidPreProgram = buildPreProgram(plannerContext, true);
+ final Program bindablePreProgram = buildPreProgram(plannerContext, false);
return ImmutableList.of(
Programs.sequence(
- preProgram,
+ druidPreProgram,
Programs.ofRules(druidConventionRuleSet(plannerContext)),
new LoggingProgram("After Druid volcano planner program", isDebug)
),
Programs.sequence(
- preProgram,
+ bindablePreProgram,
Programs.ofRules(bindableConventionRuleSet(plannerContext)),
new LoggingProgram("After bindable volcano planner program",
isDebug)
),
Programs.sequence(
- preProgram,
+ druidPreProgram,
Programs.ofRules(logicalConventionRuleSet(plannerContext)),
new LoggingProgram("After logical volcano planner program",
isDebug)
)
);
}
- private Program buildReductionProgram(final PlannerContext plannerContext)
+ /**
+ * Build the program that runs prior to the cost-based {@link
VolcanoPlanner}.
+ *
+ * @param plannerContext planner context
+ * @param isDruid whether this is a Druid program
+ */
+ private Program buildPreProgram(final PlannerContext plannerContext, final
boolean isDruid)
+ {
+ final boolean isDebug = plannerContext.queryContext().isDebug();
+
+ // Program that pre-processes the tree before letting the full-on
VolcanoPlanner loose.
+ final List<Program> prePrograms = new ArrayList<>();
+ prePrograms.add(new LoggingProgram("Start", isDebug));
+ prePrograms.add(Programs.subQuery(DefaultRelMetadataProvider.INSTANCE));
+ prePrograms.add(new LoggingProgram("Finished subquery program", isDebug));
+ prePrograms.add(DecorrelateAndTrimFieldsProgram.INSTANCE);
+ prePrograms.add(new LoggingProgram("Finished decorrelate and trim fields
program", isDebug));
+ prePrograms.add(buildReductionProgram(plannerContext, isDruid));
+ prePrograms.add(new LoggingProgram("Finished expression reduction
program", isDebug));
+
+ return Programs.sequence(prePrograms.toArray(new Program[0]));
+ }
+
+ /**
+ * Builds an expression reduction program using {@link #REDUCTION_RULES}
(built-in to Calcite) plus some
+ * Druid-specific rules.
+ */
+ private Program buildReductionProgram(final PlannerContext plannerContext,
final boolean isDruid)
{
- List<RelOptRule> hepRules = new ArrayList<RelOptRule>(REDUCTION_RULES);
+ final List<RelOptRule> hepRules = new ArrayList<>();
+
+ if (isDruid) {
+ // Must run before REDUCTION_RULES, since otherwise
ReduceExpressionsRule#pushPredicateIntoCase may
+ // make it impossible to convert to COALESCE.
+ hepRules.add(new CaseToCoalesceRule());
+ hepRules.add(new CoalesceLookupRule());
+
+ // Flatten calls to CONCAT, which happen easily with the || operator
since it only accepts two arguments.
+ hepRules.add(new FlattenConcatRule());
+
+ // Decompose filters on COALESCE to promote more usage of indexes.
+ hepRules.add(new FilterDecomposeCoalesceRule());
+ }
+
+ // Calcite's builtin reduction rules.
+ hepRules.addAll(REDUCTION_RULES);
+
+ if (isDruid) {
+ // Decompose filters on CONCAT to promote more usage of indexes. Runs
after REDUCTION_RULES because
+ // this rule benefits from reduction of effectively-literal calls to
actual literals.
+ hepRules.add(new FilterDecomposeConcatRule());
+ }
+
// Apply CoreRules#FILTER_INTO_JOIN early to avoid exploring less optimal
plans.
- if (plannerContext.getJoinAlgorithm().requiresSubquery()) {
+ if (isDruid && plannerContext.getJoinAlgorithm().requiresSubquery()) {
hepRules.add(CoreRules.FILTER_INTO_JOIN);
}
- return buildHepProgram(
- hepRules
- );
+
+ return buildHepProgram(hepRules);
}
private static class LoggingProgram implements Program
@@ -372,7 +413,13 @@ public class CalciteRulesManager
return rules.build();
}
- private static Program buildHepProgram(final Iterable<? extends RelOptRule>
rules)
+ /**
+ * Build a {@link HepProgram} to apply rules mechanically as part of {@link
#buildPreProgram}. Rules are applied
+ * one-by-one.
+ *
+ * @param rules rules to apply
+ */
+ private static Program buildHepProgram(final Collection<RelOptRule> rules)
{
final HepProgramBuilder builder = HepProgram.builder();
builder.addMatchLimit(CalciteRulesManager.HEP_DEFAULT_MATCH_LIMIT);
@@ -382,20 +429,6 @@ public class CalciteRulesManager
return Programs.of(builder.build(), true,
DefaultRelMetadataProvider.INSTANCE);
}
- /**
- * Program that performs various manipulations related to COALESCE.
- */
- private static Program buildCoalesceProgram()
- {
- return buildHepProgram(
- ImmutableList.of(
- new CaseToCoalesceRule(),
- new CoalesceLookupRule(),
- new FilterDecomposeCoalesceRule()
- )
- );
- }
-
/**
* Based on Calcite's Programs.DecorrelateProgram and
Programs.TrimFieldsProgram, which are private and only
* accessible through Programs.standard (which we don't want, since it also
adds Enumerable rules).
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/FilterDecomposeConcatRule.java
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/FilterDecomposeConcatRule.java
new file mode 100644
index 00000000000..1a28392a5b4
--- /dev/null
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/FilterDecomposeConcatRule.java
@@ -0,0 +1,296 @@
+/*
+ * 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.rule;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multiset;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.rules.SubstitutionRule;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.common.config.NullHandling;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Transform calls like [CONCAT(x, '-', y) = 'a-b'] => [x = 'a' AND y = 'b'].
+ */
+public class FilterDecomposeConcatRule extends RelOptRule implements
SubstitutionRule
+{
+ public FilterDecomposeConcatRule()
+ {
+ super(operand(Filter.class, any()));
+ }
+
+ @Override
+ public void onMatch(RelOptRuleCall call)
+ {
+ final Filter oldFilter = call.rel(0);
+ final DecomposeConcatShuttle shuttle = new DecomposeConcatShuttle(
+ oldFilter.getCluster().getRexBuilder());
+ final RexNode newCondition = oldFilter.getCondition().accept(shuttle);
+
+ //noinspection ObjectEquality
+ if (newCondition != oldFilter.getCondition()) {
+ call.transformTo(
+ call.builder()
+ .push(oldFilter.getInput())
+ .filter(newCondition).build()
+ );
+
+ call.getPlanner().prune(oldFilter);
+ }
+ }
+
+ /**
+ * Shuttle that decomposes predicates on top of CONCAT calls.
+ */
+ static class DecomposeConcatShuttle extends RexShuttle
+ {
+ private final RexBuilder rexBuilder;
+
+ DecomposeConcatShuttle(final RexBuilder rexBuilder)
+ {
+ this.rexBuilder = rexBuilder;
+ }
+
+ @Override
+ public RexNode visitCall(final RexCall call)
+ {
+ final RexNode newCall;
+ final boolean negate;
+
+ if (call.isA(SqlKind.EQUALS) || call.isA(SqlKind.NOT_EQUALS)) {
+ // Convert: [CONCAT(x, '-', y) = 'a-b'] => [x = 'a' AND y = 'b']
+ // Convert: [CONCAT(x, '-', y) <> 'a-b'] => [NOT (x = 'a' AND y = 'b')]
+ negate = call.isA(SqlKind.NOT_EQUALS);
+ final RexNode lhs = call.getOperands().get(0);
+ final RexNode rhs = call.getOperands().get(1);
+
+ if (FlattenConcatRule.isNonTrivialStringConcat(lhs) &&
RexUtil.isLiteral(rhs, true)) {
+ newCall = tryDecomposeConcatEquals((RexCall) lhs, rhs, rexBuilder);
+ } else if (FlattenConcatRule.isNonTrivialStringConcat(rhs) &&
RexUtil.isLiteral(lhs, true)) {
+ newCall = tryDecomposeConcatEquals((RexCall) rhs, lhs, rexBuilder);
+ } else {
+ newCall = null;
+ }
+ } else if ((call.isA(SqlKind.IS_NULL) || call.isA(SqlKind.IS_NOT_NULL))
+ &&
FlattenConcatRule.isNonTrivialStringConcat(Iterables.getOnlyElement(call.getOperands())))
{
+ negate = call.isA(SqlKind.IS_NOT_NULL);
+ final RexCall concatCall = (RexCall)
Iterables.getOnlyElement(call.getOperands());
+ if (NullHandling.sqlCompatible()) {
+ // Convert: [CONCAT(x, '-', y) IS NULL] => [x IS NULL OR y IS
NULL]
+ newCall = RexUtil.composeDisjunction(
+ rexBuilder,
+ Iterables.transform(
+ concatCall.getOperands(),
+ operand -> rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL,
operand)
+ )
+ );
+ } else {
+ // Treat [CONCAT(x, '-', y) IS NULL] as [CONCAT(x, '-', y) = '']
+ newCall = tryDecomposeConcatEquals(concatCall,
rexBuilder.makeLiteral(""), rexBuilder);
+ }
+ } else {
+ negate = false;
+ newCall = null;
+ }
+
+ if (newCall != null) {
+ // Found a CONCAT comparison to decompose.
+ return negate ? rexBuilder.makeCall(SqlStdOperatorTable.NOT, newCall)
: newCall;
+ } else {
+ // Didn't find anything interesting. Visit children of original call.
+ return super.visitCall(call);
+ }
+ }
+ }
+
+ /**
+ * Convert [CONCAT(x, '-', y) = 'a-b'] => [x = 'a' AND y = 'b'].
+ *
+ * @param concatCall the call to concat, i.e. CONCAT(x, '-', y)
+ * @param matchRexNode the literal being matched, i.e. 'a-b'
+ * @param rexBuilder rex builder
+ */
+ @Nullable
+ private static RexNode tryDecomposeConcatEquals(
+ final RexCall concatCall,
+ final RexNode matchRexNode,
+ final RexBuilder rexBuilder
+ )
+ {
+ final String matchValue = getAsString(matchRexNode);
+ if (matchValue == null) {
+ return null;
+ }
+
+ // We can decompose if all nonliterals are separated by literals, and if
each literal appears in the matchValue
+ // string exactly the number of times that it appears in the call to
CONCAT. (In this case, the concatenation can
+ // be unambiguously reversed.)
+ final StringBuilder regexBuilder = new StringBuilder();
+ final List<RexNode> nonLiterals = new ArrayList<>();
+ final Multiset<String> literalCounter = HashMultiset.create();
+ boolean expectLiteral = false; // If true, next operand must be a literal.
+ for (int i = 0; i < concatCall.getOperands().size(); i++) {
+ final RexNode operand = concatCall.getOperands().get(i);
+ if (RexUtil.isLiteral(operand, true)) {
+ final String operandValue = getAsString(operand);
+ if (operandValue == null || operandValue.isEmpty()) {
+ return null;
+ }
+
+ regexBuilder.append(Pattern.quote(operandValue));
+ literalCounter.add(operandValue);
+ expectLiteral = false;
+ } else {
+ if (expectLiteral) {
+ return null;
+ }
+
+ nonLiterals.add(operand);
+ regexBuilder.append("(.*)");
+ expectLiteral = true;
+ }
+ }
+
+ // Verify, using literalCounter, that each literal appears in the
matchValue the correct number of times.
+ for (Multiset.Entry<String> entry : literalCounter.entrySet()) {
+ final int occurrences = countOccurrences(matchValue, entry.getElement());
+ if (occurrences > entry.getCount()) {
+ // If occurrences > entry.getCount(), the match is ambiguous; consider
concat(x, 'x', y) = '2x3x4'
+ return null;
+ } else if (occurrences < entry.getCount()) {
+ return impossibleMatch(nonLiterals, rexBuilder);
+ }
+ }
+
+ // Apply the regex to the matchValue to get the expected value of each
non-literal.
+ final Pattern regex = Pattern.compile(regexBuilder.toString(),
Pattern.DOTALL);
+ final Matcher matcher = regex.matcher(matchValue);
+ if (matcher.matches()) {
+ final List<RexNode> conditions = new ArrayList<>(nonLiterals.size());
+ for (int i = 0; i < nonLiterals.size(); i++) {
+ final RexNode operand = nonLiterals.get(i);
+ conditions.add(
+ rexBuilder.makeCall(
+ SqlStdOperatorTable.EQUALS,
+ operand,
+ rexBuilder.makeLiteral(matcher.group(i + 1))
+ )
+ );
+ }
+
+ return RexUtil.composeConjunction(rexBuilder, conditions);
+ } else {
+ return impossibleMatch(nonLiterals, rexBuilder);
+ }
+ }
+
+ /**
+ * Generate an expression for the case where matching is impossible.
+ *
+ * This expression might be FALSE and might be UNKNOWN depending on whether
any of the inputs are null. Use the
+ * construct "x IS NULL AND UNKNOWN" for each arg x to CONCAT, which is
FALSE if x is not null and UNKNOWN is x
+ * is null. Then OR them all together, so the entire expression is FALSE if
all args are not null, and UNKNOWN if any arg is null.
+ *
+ * @param nonLiterals non-literal arguments to CONCAT
+ */
+ private static RexNode impossibleMatch(final List<RexNode> nonLiterals,
final RexBuilder rexBuilder)
+ {
+ if (NullHandling.sqlCompatible()) {
+ // This expression might be FALSE and might be UNKNOWN depending on
whether any of the inputs are null. Use the
+ // construct "x IS NULL AND UNKNOWN" for each arg x to CONCAT, which is
FALSE if x is not null and UNKNOWN if
+ // x is null. Then OR them all together, so the entire expression is
FALSE if all args are not null, and
+ // UNKNOWN if any arg is null.
+ final RexLiteral unknown =
+
rexBuilder.makeNullLiteral(rexBuilder.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN));
+ return RexUtil.composeDisjunction(
+ rexBuilder,
+ Iterables.transform(
+ nonLiterals,
+ operand -> rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, operand),
+ unknown
+ )
+ )
+ );
+ } else {
+ return rexBuilder.makeLiteral(false);
+ }
+ }
+
+ /**
+ * Given a literal (which may be wrapped in a cast), remove the cast call
(if any) and read it as a string.
+ * Returns null if the rex can't be read as a string.
+ */
+ @Nullable
+ private static String getAsString(final RexNode rexNode)
+ {
+ if (!SqlTypeFamily.STRING.contains(rexNode.getType())) {
+ // We don't expect this to happen, since this method is used when
reading from RexNodes that are expected
+ // to be strings. But if it does (CONCAT operator that accepts
non-strings?), return null so we skip the
+ // optimization.
+ return null;
+ }
+
+ // Get matchValue from the matchLiteral (remove cast call if any, then
read as string).
+ final RexNode matchLiteral = RexUtil.removeCast(rexNode);
+ if (SqlTypeFamily.STRING.contains(matchLiteral.getType())) {
+ return RexLiteral.stringValue(matchLiteral);
+ } else if (SqlTypeFamily.NUMERIC.contains(matchLiteral.getType())) {
+ return String.valueOf(RexLiteral.value(matchLiteral));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Count the number of occurrences of substring in string. Considers
overlapping occurrences as multiple occurrences;
+ * for example the string "--" is counted as appearing twice in "---".
+ */
+ private static int countOccurrences(final String string, final String
substring)
+ {
+ int count = 0;
+ int i = -1;
+
+ while ((i = string.indexOf(substring, i + 1)) >= 0) {
+ count++;
+ }
+
+ return count;
+ }
+}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/FlattenConcatRule.java
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/FlattenConcatRule.java
new file mode 100644
index 00000000000..589c590b107
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rule/FlattenConcatRule.java
@@ -0,0 +1,133 @@
+/*
+ * 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.rule;
+
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.rules.SubstitutionRule;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.math.expr.Function;
+import
org.apache.druid.sql.calcite.expression.builtin.ConcatOperatorConversion;
+import
org.apache.druid.sql.calcite.expression.builtin.TextcatOperatorConversion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Flattens calls to CONCAT. Useful because otherwise [a || b || c] would get
planned as [CONCAT(CONCAT(a, b), c)].
+ */
+public class FlattenConcatRule extends RelOptRule implements SubstitutionRule
+{
+ public FlattenConcatRule()
+ {
+ super(operand(RelNode.class, any()));
+ }
+
+ @Override
+ public void onMatch(RelOptRuleCall call)
+ {
+ final RelNode oldNode = call.rel(0);
+ final FlattenConcatShuttle shuttle = new
FlattenConcatShuttle(oldNode.getCluster().getRexBuilder());
+ final RelNode newNode = oldNode.accept(shuttle);
+
+ //noinspection ObjectEquality
+ if (newNode != oldNode) {
+ call.transformTo(newNode);
+ call.getPlanner().prune(oldNode);
+ }
+ }
+
+ private static class FlattenConcatShuttle extends RexShuttle
+ {
+ private final RexBuilder rexBuilder;
+
+ public FlattenConcatShuttle(RexBuilder rexBuilder)
+ {
+ this.rexBuilder = rexBuilder;
+ }
+
+ @Override
+ public RexNode visitCall(RexCall call)
+ {
+ if (isNonTrivialStringConcat(call)) {
+ final List<RexNode> newOperands = new ArrayList<>();
+ for (final RexNode operand : call.getOperands()) {
+ if (isNonTrivialStringConcat(operand)) {
+ // Recursively flatten. We only flatten non-trivial CONCAT calls,
because trivial ones (which do not
+ // reference any inputs) are reduced to constants by
ReduceExpressionsRule.
+ final RexNode visitedOperand = visitCall((RexCall) operand);
+
+ if (isStringConcat(visitedOperand)) {
+ newOperands.addAll(((RexCall) visitedOperand).getOperands());
+ } else {
+ newOperands.add(visitedOperand);
+ }
+ } else if (RexUtil.isNullLiteral(operand, true) &&
NullHandling.sqlCompatible()) {
+ return rexBuilder.makeNullLiteral(call.getType());
+ } else {
+ newOperands.add(operand);
+ }
+ }
+
+ if (!newOperands.equals(call.getOperands())) {
+ return rexBuilder.makeCall(ConcatOperatorConversion.SQL_FUNCTION,
newOperands);
+ } else {
+ return call;
+ }
+ } else {
+ return super.visitCall(call);
+ }
+ }
+ }
+
+ /**
+ * Whether a rex is a string concatenation operator. All of these end up
being converted to
+ * {@link Function.ConcatFunc}.
+ */
+ static boolean isStringConcat(final RexNode rexNode)
+ {
+ if (SqlTypeFamily.STRING.contains(rexNode.getType()) && rexNode instanceof
RexCall) {
+ final SqlOperator operator = ((RexCall) rexNode).getOperator();
+ return ConcatOperatorConversion.SQL_FUNCTION.equals(operator)
+ || TextcatOperatorConversion.SQL_FUNCTION.equals(operator)
+ || SqlStdOperatorTable.CONCAT.equals(operator);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Whether a rex is a string concatenation involving at least one an input
field.
+ */
+ static boolean isNonTrivialStringConcat(final RexNode rexNode)
+ {
+ return isStringConcat(rexNode) &&
!RelOptUtil.InputFinder.bits(rexNode).isEmpty();
+ }
+}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java
index 7226811dba0..97f41c425e4 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java
@@ -161,7 +161,7 @@ public class CalciteMultiValueStringQueryTest extends
BaseCalciteQueryTest
new DefaultDimensionSpec("v0", "_d0",
ColumnType.STRING)
)
)
- .setDimFilter(equality("v0", "bfoo",
ColumnType.STRING))
+ .setDimFilter(equality("dim3", "b", ColumnType.STRING))
.setAggregatorSpecs(aggregators(new
LongSumAggregatorFactory("a0", "cnt")))
.setLimitSpec(new DefaultLimitSpec(
ImmutableList.of(new OrderByColumnSpec(
@@ -248,7 +248,7 @@ public class CalciteMultiValueStringQueryTest extends
BaseCalciteQueryTest
.dataSource(CalciteTests.DATASOURCE3)
.eternityInterval()
.virtualColumns(expressionVirtualColumn("v0",
"concat(\"dim3\",'foo')", ColumnType.STRING))
- .filters(equality("v0", "bfoo", ColumnType.STRING))
+ .filters(equality("dim3", "b", ColumnType.STRING))
.columns(ImmutableList.of("v0"))
.context(QUERY_CONTEXT_DEFAULT)
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index 2f795f54ed7..232228f5654 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -11886,16 +11886,21 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
new Object[]{"abc-abc_abc"}
)
);
+ }
+ @Test
+ public void testConcat2()
+ {
+ // Tests flattening CONCAT, and tests reduction of concat('x', 'y') => 'xy'
testQuery(
- "SELECT CONCAt(dim1, CONCAt(dim2,'x'), m2, 9999, dim1) as dimX FROM
foo",
+ "SELECT CONCAt(dim1, CONCAt(dim2,concat('x', 'y')), m2, 9999, dim1) as
dimX FROM foo",
ImmutableList.of(
newScanQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(querySegmentSpec(Filtration.eternity()))
.virtualColumns(expressionVirtualColumn(
"v0",
-
"concat(\"dim1\",concat(\"dim2\",'x'),\"m2\",9999,\"dim1\")",
+ "concat(\"dim1\",\"dim2\",'xy',\"m2\",9999,\"dim1\")",
ColumnType.STRING
))
.columns("v0")
@@ -11904,12 +11909,12 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
.build()
),
ImmutableList.of(
- new Object[]{"ax1.09999"},
- new Object[]{NullHandling.sqlCompatible() ? null :
"10.1x2.0999910.1"}, // dim2 is null
- new Object[]{"2x3.099992"},
- new Object[]{"1ax4.099991"},
- new Object[]{"defabcx5.09999def"},
- new Object[]{NullHandling.sqlCompatible() ? null :
"abcx6.09999abc"} // dim2 is null
+ new Object[]{"axy1.09999"},
+ new Object[]{NullHandling.sqlCompatible() ? null :
"10.1xy2.0999910.1"}, // dim2 is null
+ new Object[]{"2xy3.099992"},
+ new Object[]{"1axy4.099991"},
+ new Object[]{"defabcxy5.09999def"},
+ new Object[]{NullHandling.sqlCompatible() ? null :
"abcxy6.09999abc"} // dim2 is null
)
);
}
@@ -11942,10 +11947,14 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
new Object[]{"def-def_def"}
)
);
+ }
- final List<Object[]> secondResults;
+ @Test
+ public void testConcatGroup2()
+ {
+ final List<Object[]> results;
if (useDefault) {
- secondResults = ImmutableList.of(
+ results = ImmutableList.of(
new Object[]{"10.1x2.0999910.1"},
new Object[]{"1ax4.099991"},
new Object[]{"2x3.099992"},
@@ -11954,7 +11963,7 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
new Object[]{"defabcx5.09999def"}
);
} else {
- secondResults = ImmutableList.of(
+ results = ImmutableList.of(
new Object[]{null},
new Object[]{"1ax4.099991"},
new Object[]{"2x3.099992"},
@@ -11962,6 +11971,7 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
new Object[]{"defabcx5.09999def"}
);
}
+
testQuery(
"SELECT CONCAT(dim1, CONCAT(dim2,'x'), m2, 9999, dim1) as dimX FROM
foo GROUP BY 1",
ImmutableList.of(
@@ -11970,7 +11980,7 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
.setInterval(querySegmentSpec(Filtration.eternity()))
.setVirtualColumns(expressionVirtualColumn(
"v0",
-
"concat(\"dim1\",concat(\"dim2\",'x'),\"m2\",9999,\"dim1\")",
+ "concat(\"dim1\",\"dim2\",'x',\"m2\",9999,\"dim1\")",
ColumnType.STRING
))
.setDimensions(dimensions(new DefaultDimensionSpec("v0",
"d0")))
@@ -11979,7 +11989,172 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
.build()
),
- secondResults
+ results
+ );
+ }
+
+ @Test
+ public void testConcatDecomposeAlwaysFalseOrUnknown()
+ {
+ testQuery(
+ "SELECT CONCAT(dim1, 'x', dim2) as dimX\n"
+ + "FROM foo\n"
+ + "WHERE CONCAT(dim1, 'x', dim2) IN ('1a', '3x4')",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0",
"concat(\"dim1\",'x',\"dim2\")", ColumnType.STRING))
+ .filters(and(
+ equality("dim1", "3", ColumnType.STRING),
+ equality("dim2", "4", ColumnType.STRING)
+ ))
+ .columns("v0")
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of()
+ );
+ }
+
+ @Test
+ public void testConcatDecomposeAlwaysFalseOrUnknownNegated()
+ {
+ testQuery(
+ "SELECT CONCAT(dim1, 'x', dim2) as dimX\n"
+ + "FROM foo\n"
+ + "WHERE CONCAT(dim1, 'x', dim2) NOT IN ('1a', '3x4', '4x5')\n",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn(
+ "v0",
+ "concat(\"dim1\",'x',\"dim2\")",
+ ColumnType.STRING
+ ))
+ .filters(
+ NullHandling.sqlCompatible()
+ ? and(
+ or(
+ not(equality("dim1", "3", ColumnType.STRING)),
+ not(equality("dim2", "4", ColumnType.STRING))
+ ),
+ or(
+ not(equality("dim1", "4", ColumnType.STRING)),
+ not(equality("dim2", "5", ColumnType.STRING))
+ ),
+ notNull("dim1"),
+ notNull("dim2")
+ )
+ : and(
+ or(
+ not(equality("dim1", "3", ColumnType.STRING)),
+ not(equality("dim2", "4", ColumnType.STRING))
+ ),
+ or(
+ not(equality("dim1", "4", ColumnType.STRING)),
+ not(equality("dim2", "5", ColumnType.STRING))
+ )
+ )
+ )
+ .columns("v0")
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ NullHandling.sqlCompatible()
+ ? ImmutableList.of(
+ new Object[]{"xa"},
+ new Object[]{"2x"},
+ new Object[]{"1xa"},
+ new Object[]{"defxabc"}
+ )
+ : ImmutableList.of(
+ new Object[]{"xa"},
+ new Object[]{"10.1x"},
+ new Object[]{"2x"},
+ new Object[]{"1xa"},
+ new Object[]{"defxabc"},
+ new Object[]{"abcx"}
+ )
+ );
+ }
+
+ @Test
+ public void testConcatDecomposeIsNull()
+ {
+ testQuery(
+ "SELECT dim1, dim2, CONCAT(dim1, 'x', dim2) as dimX\n"
+ + "FROM foo\n"
+ + "WHERE CONCAT(dim1, 'x', dim2) IS NULL",
+ ImmutableList.of(
+ NullHandling.sqlCompatible()
+ ? newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn(
+ "v0",
+ "concat(\"dim1\",'x',\"dim2\")",
+ ColumnType.STRING
+ ))
+ .filters(or(isNull("dim1"), isNull("dim2")))
+ .columns("dim1", "dim2", "v0")
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ : Druids.newScanQueryBuilder()
+ .dataSource(
+ InlineDataSource.fromIterable(
+ ImmutableList.of(),
+ RowSignature.builder()
+ .add("dim1", ColumnType.STRING)
+ .add("dim2", ColumnType.STRING)
+ .add("dimX", ColumnType.STRING)
+ .build()
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .columns("dim1", "dim2", "dimX")
+ .resultFormat(ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .legacy(false)
+ .build()
+
+ ),
+ NullHandling.sqlCompatible()
+ ? ImmutableList.of(
+ new Object[]{"10.1", null, null},
+ new Object[]{"abc", null, null}
+ )
+ : ImmutableList.of()
+ );
+ }
+
+ @Test
+ public void testConcatDoubleBarsDecompose()
+ {
+ testQuery(
+ "SELECT dim1 || LOWER('x') || dim2 || 'z' as dimX\n"
+ + "FROM foo\n"
+ + "WHERE dim1 || LOWER('x') || dim2 || 'z' IN ('1xaz', '3x4z')",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0",
"concat(\"dim1\",'x',\"dim2\",'z')", ColumnType.STRING))
+ .filters(or(
+ and(equality("dim1", "1", ColumnType.STRING),
equality("dim2", "a", ColumnType.STRING)),
+ and(equality("dim1", "3", ColumnType.STRING),
equality("dim2", "4", ColumnType.STRING))
+ ))
+ .columns("v0")
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"1xaz"}
+ )
);
}
@@ -13816,8 +13991,10 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
cannotVectorize();
skipVectorize();
testQuery(
- // TODO(gianm): '||' used to be CONCAT('|', '|'), but for some reason
this is no longer being reduced
- "SELECT STRING_AGG(DISTINCT CONCAT(dim1, dim2), ','),
STRING_AGG(DISTINCT CONCAT(dim1, dim2), '||') FROM foo",
+ "SELECT\n"
+ + " STRING_AGG(DISTINCT CONCAT(dim1, dim2), ','),\n"
+ + " STRING_AGG(DISTINCT CONCAT(dim1, dim2), CONCAT('|', '|'))\n"
+ + "FROM foo",
ImmutableList.of(
Druids.newTimeseriesQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/rule/FilterDecomposeConcatRuleTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/rule/FilterDecomposeConcatRuleTest.java
new file mode 100644
index 00000000000..601d02c1ca8
--- /dev/null
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/rule/FilterDecomposeConcatRuleTest.java
@@ -0,0 +1,369 @@
+/*
+ * 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.rule;
+
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.common.config.NullHandling;
+import
org.apache.druid.sql.calcite.expression.builtin.ConcatOperatorConversion;
+import org.apache.druid.sql.calcite.planner.DruidTypeSystem;
+import org.apache.druid.testing.InitializedNullHandlingTest;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+public class FilterDecomposeConcatRuleTest extends InitializedNullHandlingTest
+{
+ private final RelDataTypeFactory typeFactory = DruidTypeSystem.TYPE_FACTORY;
+ private final RexBuilder rexBuilder = new RexBuilder(typeFactory);
+ private final RexShuttle shuttle = new
FilterDecomposeConcatRule.DecomposeConcatShuttle(rexBuilder);
+
+ @Test
+ public void test_notConcat()
+ {
+ final RexNode call =
+ equals(
+ rexBuilder.makeCall(SqlStdOperatorTable.LOWER, inputRef(0)),
+ literal("2")
+ );
+
+ Assert.assertEquals(call, shuttle.apply(call));
+ }
+
+ @Test
+ public void test_oneInput()
+ {
+ final RexNode concatCall =
+ concat(literal("it's "), inputRef(0));
+
+ Assert.assertEquals(
+ and(equals(inputRef(0), literal("2"))),
+ shuttle.apply(equals(concatCall, literal("it's 2")))
+ );
+ }
+
+ @Test
+ public void test_oneInput_lhsLiteral()
+ {
+ final RexNode concatCall =
+ concat(literal("it's "), inputRef(0));
+
+ Assert.assertEquals(
+ and(equals(inputRef(0), literal("2"))),
+ shuttle.apply(equals(literal("it's 2"), concatCall))
+ );
+ }
+
+ @Test
+ public void test_oneInput_noLiteral()
+ {
+ final RexNode concatCall = concat(inputRef(0));
+
+ Assert.assertEquals(
+ and(equals(inputRef(0), literal("it's 2"))),
+ shuttle.apply(equals(literal("it's 2"), concatCall))
+ );
+ }
+
+ @Test
+ public void test_twoInputs()
+ {
+ final RexNode concatCall =
+ concat(inputRef(0), literal("x"), inputRef(1));
+
+ Assert.assertEquals(
+ and(equals(inputRef(0), literal("2")), equals(inputRef(1),
literal("3"))),
+ shuttle.apply(equals(concatCall, literal("2x3")))
+ );
+ }
+
+ @Test
+ public void test_twoInputs_castNumberInputRef()
+ {
+ // CAST(x AS VARCHAR) when x is BIGINT
+ final RexNode numericInputRef = rexBuilder.makeCast(
+
typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.VARCHAR),
true),
+ rexBuilder.makeInputRef(
+
typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.BIGINT),
true),
+ 0
+ )
+ );
+
+ final RexNode concatCall =
+ concat(numericInputRef, literal("x"), inputRef(1));
+
+ Assert.assertEquals(
+ and(
+ equals(
+ numericInputRef,
+ literal("2")
+ ),
+ equals(
+ inputRef(1),
+ literal("3")
+ )
+ ),
+ shuttle.apply(equals(concatCall, literal("2x3")))
+ );
+ }
+
+ @Test
+ public void test_twoInputs_notEquals()
+ {
+ final RexNode call =
+ notEquals(
+ concat(inputRef(0), literal("x"), inputRef(1)),
+ literal("2x3")
+ );
+
+ Assert.assertEquals(
+ rexBuilder.makeCall(
+ SqlStdOperatorTable.NOT,
+ and(equals(inputRef(0), literal("2")), equals(inputRef(1),
literal("3")))
+ ),
+ shuttle.apply(call)
+ );
+ }
+
+ @Test
+ public void test_twoInputs_castNumberLiteral()
+ {
+ final RexNode three = rexBuilder.makeCast(
+ typeFactory.createSqlType(SqlTypeName.VARCHAR),
+ rexBuilder.makeExactLiteral(BigDecimal.valueOf(3L))
+ );
+
+ final RexNode concatCall =
+ concat(inputRef(0), three, inputRef(1), literal("4"));
+
+ Assert.assertEquals(
+ and(equals(inputRef(0), literal("x")), equals(inputRef(1),
literal("y"))),
+ shuttle.apply(equals(concatCall, literal("x3y4")))
+ );
+ }
+
+ @Test
+ public void test_twoInputs_noLiteral()
+ {
+ final RexNode call = equals(concat(inputRef(0), inputRef(1)),
literal("2x3"));
+ Assert.assertEquals(call, shuttle.apply(call));
+ }
+
+ @Test
+ public void test_twoInputs_isNull()
+ {
+ final RexNode call =
+ isNull(concat(inputRef(0), literal("x"), inputRef(1)));
+
+ Assert.assertEquals(
+ NullHandling.sqlCompatible()
+ ? or(isNull(inputRef(0)), isNull(inputRef(1)))
+ : rexBuilder.makeLiteral(false),
+ shuttle.apply(call)
+ );
+ }
+
+ @Test
+ public void test_twoInputs_isNotNull()
+ {
+ final RexNode call =
+ notNull(concat(inputRef(0), literal("x"), inputRef(1)));
+
+ Assert.assertEquals(
+ rexBuilder.makeCall(
+ SqlStdOperatorTable.NOT,
+ NullHandling.sqlCompatible()
+ ? or(isNull(inputRef(0)), isNull(inputRef(1)))
+ : rexBuilder.makeLiteral(false)
+ ),
+ shuttle.apply(call)
+ );
+ }
+
+ @Test
+ public void test_twoInputs_tooManyXes()
+ {
+ final RexNode call =
+ equals(
+ concat(inputRef(0), literal("x"), inputRef(1)),
+ literal("2xx3") // ambiguous match
+ );
+
+ Assert.assertEquals(call, shuttle.apply(call));
+ }
+
+ @Test
+ public void test_twoInputs_notEnoughXes()
+ {
+ final RexNode call =
+ equals(
+ concat(inputRef(0), literal("x"), inputRef(1)),
+ literal("2z3") // doesn't match concat pattern
+ );
+
+ final RexLiteral unknown =
rexBuilder.makeNullLiteral(typeFactory.createSqlType(SqlTypeName.BOOLEAN));
+ Assert.assertEquals(
+ NullHandling.sqlCompatible()
+ ? or(
+ and(isNull(inputRef(0)), unknown),
+ and(isNull(inputRef(1)), unknown)
+ )
+ : rexBuilder.makeLiteral(false),
+ shuttle.apply(call)
+ );
+ }
+
+ @Test
+ public void test_twoInputs_delimitersWrongOrder()
+ {
+ final RexNode call =
+ equals(
+ concat(literal("z"), inputRef(0), literal("x"), inputRef(1)),
+ literal("x2z3") // doesn't match concat pattern
+ );
+
+ final RexLiteral unknown =
rexBuilder.makeNullLiteral(typeFactory.createSqlType(SqlTypeName.BOOLEAN));
+ Assert.assertEquals(
+ NullHandling.sqlCompatible()
+ ? or(
+ and(isNull(inputRef(0)), unknown),
+ and(isNull(inputRef(1)), unknown)
+ )
+ : rexBuilder.makeLiteral(false),
+ shuttle.apply(call)
+ );
+ }
+
+ @Test
+ public void test_twoInputs_emptyDelimiter()
+ {
+ final RexNode call =
+ equals(
+ concat(inputRef(0), literal(""), inputRef(1)),
+ literal("23") // must be recognized as ambiguous
+ );
+
+ Assert.assertEquals(call, shuttle.apply(call));
+ }
+
+ @Test
+ public void test_twoInputs_ambiguousOverlappingDeliminters()
+ {
+ final RexNode call =
+ equals(
+ concat(inputRef(0), literal("--"), inputRef(1)),
+ literal("2---3") // must be recognized as ambiguous
+ );
+
+ Assert.assertEquals(call, shuttle.apply(call));
+ }
+
+ @Test
+ public void test_twoInputs_impossibleOverlappingDelimiters()
+ {
+ final RexNode call =
+ equals(
+ concat(inputRef(0), literal("--"), inputRef(1), literal("--")),
+ literal("2---3") // must be recognized as impossible
+ );
+
+ final RexLiteral unknown =
rexBuilder.makeNullLiteral(typeFactory.createSqlType(SqlTypeName.BOOLEAN));
+ Assert.assertEquals(
+ NullHandling.sqlCompatible()
+ ? or(
+ and(isNull(inputRef(0)), unknown),
+ and(isNull(inputRef(1)), unknown)
+ )
+ : rexBuilder.makeLiteral(false),
+ shuttle.apply(call)
+ );
+ }
+
+ @Test
+ public void test_twoInputs_backToBackLiterals()
+ {
+ final RexNode concatCall =
+ concat(inputRef(0), literal("x"), literal("y"), inputRef(1));
+
+ Assert.assertEquals(
+ and(equals(inputRef(0), literal("2")), equals(inputRef(1),
literal("3"))),
+ shuttle.apply(equals(concatCall, literal("2xy3")))
+ );
+ }
+
+ private RexNode concat(RexNode... args)
+ {
+ return rexBuilder.makeCall(ConcatOperatorConversion.SQL_FUNCTION, args);
+ }
+
+ private RexNode inputRef(int i)
+ {
+ return rexBuilder.makeInputRef(
+ typeFactory.createTypeWithNullability(
+ typeFactory.createSqlType(SqlTypeName.VARCHAR),
+ true
+ ),
+ i
+ );
+ }
+
+ private RexNode or(RexNode... args)
+ {
+ return RexUtil.composeDisjunction(rexBuilder, Arrays.asList(args));
+ }
+
+ private RexNode and(RexNode... args)
+ {
+ return RexUtil.composeConjunction(rexBuilder, Arrays.asList(args));
+ }
+
+ private RexNode equals(RexNode arg, RexNode value)
+ {
+ return rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, arg, value);
+ }
+
+ private RexNode notEquals(RexNode arg, RexNode value)
+ {
+ return rexBuilder.makeCall(SqlStdOperatorTable.NOT_EQUALS, arg, value);
+ }
+
+ private RexNode isNull(RexNode arg)
+ {
+ return rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, arg);
+ }
+
+ private RexNode notNull(RexNode arg)
+ {
+ return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, arg);
+ }
+
+ private RexNode literal(String s)
+ {
+ return rexBuilder.makeLiteral(s);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]