shiftedProjects =
+ RexUtil.shift(rightProject.getProjects(), newLeft.getRowType().getFieldCount());
+ for (int i = 0; i < shiftedProjects.size(); i++) {
+ newProjectExprs.add(
+ rightHoist.matchIndicator != null && rightHoist.isConstant[i]
+ ? nullGateOuterJoin(shiftedProjects.get(i), rightHoist.matchIndicator, rexBuilder)
+ : shiftedProjects.get(i)
+ );
}
newRight = right.withPartialQuery(PartialDruidQuery.create(rightScan));
- conditionAnalysis = conditionAnalysis.pushThroughRightProject(rightProject);
+ conditionAnalysis = rightHoist.pushedAnalysis;
} else {
// Leave right as-is. Write input refs that do nothing.
for (int i = 0; i < right.getRowType().getFieldCount(); i++) {
@@ -225,19 +230,6 @@ public void onMatch(RelOptRuleCall call)
call.transformTo(relBuilder.build());
}
- private static RexNode makeNullableIfLiteral(final RexNode rexNode, final RexBuilder rexBuilder)
- {
- if (rexNode.isA(SqlKind.LITERAL)) {
- return rexBuilder.makeLiteral(
- RexLiteral.value(rexNode),
- rexBuilder.getTypeFactory().createTypeWithNullability(rexNode.getType(), true),
- true
- );
- } else {
- return rexNode;
- }
- }
-
/**
* Returns whether we can handle the join condition. In case, some conditions in an AND expression are not supported,
* they are extracted into a post-join filter instead.
@@ -582,6 +574,90 @@ private static boolean isRightInputRef(final RexNode rexNode, final int numLeftF
return rexNode.isA(SqlKind.INPUT_REF) && ((RexInputRef) rexNode).getIndex() >= numLeftFields;
}
+ /**
+ * Plan for hoisting a right-side {@link Project} above {@link Join}. Computes the pushed-down condition
+ * analysis, flags each projection expression as constant (no input refs) or not, and (if necessary)
+ * selects an equi-condition's right column to use as a match indicator for {@link #nullGateOuterJoin}.
+ *
+ * Returns null if the right project cannot be hoisted.
+ */
+ @Nullable
+ private static RightHoistPlan planRightHoist(
+ final Join join,
+ final Project rightProject,
+ final ConditionAnalysis analysis
+ )
+ {
+ final List projects = rightProject.getProjects();
+ final boolean[] isConstant = new boolean[projects.size()];
+ boolean hasConstant = false;
+
+ for (int i = 0; i < projects.size(); i++) {
+ isConstant[i] = !RexUtil.containsInputRef(projects.get(i));
+ hasConstant = hasConstant || isConstant[i];
+ }
+
+ final ConditionAnalysis pushed = analysis.pushThroughRightProject(rightProject);
+ if (!hasConstant || !join.getJoinType().generatesNullsOnRight()) {
+ // No null-gating needed: either there are no constants to gate, or the join type does not
+ // null-extend the right side.
+ return new RightHoistPlan(pushed, null, isConstant);
+ }
+
+ if (join.getJoinType() != JoinRelType.LEFT) {
+ // Only LEFT is safely gateable with the matchIndicator scheme. FULL (and any future
+ // null-extending-on-right join types) are skipped. See the Javadoc above for details.
+ return null;
+ }
+
+ RexNode indicator = null;
+ for (RexEquality eq : pushed.equalitySubConditions) {
+ // Exclude IS_NOT_DISTINCT_FROM, because it allows NULL = NULL matches, which would mean
+ // the rhs could potentially be NULL even when the join matches.
+ if (eq.kind == SqlKind.EQUALS) {
+ indicator = eq.right;
+ break;
+ }
+ }
+
+ return indicator == null ? null : new RightHoistPlan(pushed, indicator, isConstant);
+ }
+
+ /**
+ * Wrap a right-side constant expression in a CASE keyed on {@code matchIndicator}, so it evaluates to NULL
+ * for non-matching rows of an outer join. Non-constant expressions don't need this because their input refs
+ * are already NULL-extended by the join.
+ */
+ private static RexNode nullGateOuterJoin(
+ final RexNode rexNode,
+ final RexNode matchIndicator,
+ final RexBuilder rexBuilder
+ )
+ {
+ final RelDataType nullableType =
+ rexBuilder.getTypeFactory().createTypeWithNullability(rexNode.getType(), true);
+ return rexBuilder.makeCall(
+ SqlStdOperatorTable.CASE,
+ rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, matchIndicator),
+ rexBuilder.ensureType(nullableType, rexNode, false),
+ rexBuilder.makeNullLiteral(nullableType)
+ );
+ }
+
+ /**
+ * Return value from {@link #planRightHoist(Join, Project, ConditionAnalysis)}.
+ *
+ * @param pushedAnalysis modified condition analysis for the hoisted plan
+ * @param matchIndicator indicator field that can be used to determine if a join happened
+ * @param isConstant whether each field (by index) is a constant
+ */
+ private record RightHoistPlan(
+ ConditionAnalysis pushedAnalysis,
+ @Nullable RexNode matchIndicator,
+ boolean[] isConstant
+ )
+ {
+ }
/**
* Like {@link org.apache.druid.segment.join.Equality} but uses {@link RexNode} instead of
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidStripUnionArmCastRule.java b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidStripUnionArmCastRule.java
new file mode 100644
index 000000000000..d8d3b4c5c846
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidStripUnionArmCastRule.java
@@ -0,0 +1,161 @@
+/*
+ * 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.rel.RelNode;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.core.Union;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.rules.SubstitutionRule;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Strips numeric-to-numeric {@code CAST} expressions so {@link DruidUnionDataSourceRule} is able
+ * to generate unions in more plans.
+ *
+ * Example: for {@code SELECT dim1, dim2, m1 FROM foo2 UNION ALL SELECT dim1, dim2, m1 FROM foo}
+ * where {@code foo2.m1} is {@code BIGINT} and {@code foo.m1} is {@code FLOAT}, Calcite produces:
+ *
+ * LogicalUnion(all=[true])
+ * LogicalProject(dim1=[$1], dim2=[$2], m1=[CAST($5):FLOAT])
+ * LogicalTableScan(table=[[druid, foo2]])
+ * LogicalProject(dim1=[$1], dim2=[$2], m1=[$5])
+ * LogicalTableScan(table=[[druid, foo]])
+ *
+ * This rule rewrites it to:
+ *
+ * LogicalUnion(all=[true])
+ * LogicalProject(dim1=[$1], dim2=[$2], m1=[$5]) // CAST stripped
+ * LogicalTableScan(table=[[druid, foo2]])
+ * LogicalProject(dim1=[$1], dim2=[$2], m1=[$5])
+ * LogicalTableScan(table=[[druid, foo]])
+ *
+ *
+ * See {@code union_datasource.iq} for test cases that fail without this rule.
+ */
+public class DruidStripUnionArmCastRule extends RelOptRule implements SubstitutionRule
+{
+ private static final DruidStripUnionArmCastRule INSTANCE = new DruidStripUnionArmCastRule();
+
+ private DruidStripUnionArmCastRule()
+ {
+ super(operand(Union.class, any()));
+ }
+
+ public static DruidStripUnionArmCastRule instance()
+ {
+ return INSTANCE;
+ }
+
+ @Override
+ public void onMatch(final RelOptRuleCall call)
+ {
+ final Union union = call.rel(0);
+ final List newInputs = new ArrayList<>(union.getInputs().size());
+ boolean anyChanged = false;
+ for (final RelNode input : union.getInputs()) {
+ final RelNode arm = input.stripped();
+ if (armHasStrippableCast(arm)) {
+ newInputs.add(stripArmCasts((Project) arm));
+ anyChanged = true;
+ } else {
+ newInputs.add(input);
+ }
+ }
+ if (anyChanged) {
+ final RelNode transformed = union.copy(union.getTraitSet(), newInputs, union.all);
+ if (!transformed.getRowType().equals(union.getRowType())) {
+ return;
+ }
+ call.transformTo(transformed);
+ }
+ }
+
+ /**
+ * Returns true when {@code arm} is a {@link Project} directly atop a {@link TableScan} and at
+ * least one of its projections is a Druid-absorbable numeric {@code CAST}.
+ */
+ private static boolean armHasStrippableCast(final RelNode arm)
+ {
+ if (!(arm instanceof Project project)) {
+ return false;
+ }
+ if (!(project.getInput().stripped() instanceof TableScan)) {
+ return false;
+ }
+ for (final RexNode expr : project.getProjects()) {
+ if (isStrippableCast(expr)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a new {@link LogicalProject} with strippable CAST replaced by their underlying {@link RexInputRef}.
+ */
+ private static Project stripArmCasts(final Project project)
+ {
+ final List newExprs = new ArrayList<>(project.getProjects().size());
+ for (final RexNode expr : project.getProjects()) {
+ if (isStrippableCast(expr)) {
+ newExprs.add(((RexCall) expr).getOperands().get(0));
+ } else {
+ newExprs.add(expr);
+ }
+ }
+ // Using LogicalProject.create so Calcite re-derives the row type from the new expressions.
+ return LogicalProject.create(
+ project.getInput(),
+ project.getHints(),
+ newExprs,
+ project.getRowType().getFieldNames(),
+ project.getVariablesSet()
+ );
+ }
+
+ /**
+ * Returns true when {@code expr} is a {@code CAST} of a {@link RexInputRef} between numeric types.
+ */
+ private static boolean isStrippableCast(final RexNode expr)
+ {
+ if (!expr.isA(SqlKind.CAST)) {
+ return false;
+ }
+ final RexCall castCall = (RexCall) expr;
+ if (castCall.getOperands().size() != 1 || !(castCall.getOperands().get(0) instanceof RexInputRef)) {
+ return false;
+ }
+ final RelDataType fromType = castCall.getOperands().get(0).getType();
+ final RelDataType toType = castCall.getType();
+ return SqlTypeUtil.isNumeric(fromType) && SqlTypeUtil.isNumeric(toType);
+ }
+}
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
index b4a4b16b8a28..c87d503221ab 100644
--- 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
@@ -117,6 +117,17 @@ public RexNode visitCall(final RexCall call)
operand -> rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, operand)
)
);
+ } else if (call.isA(SqlKind.SEARCH)
+ && FlattenConcatRule.isNonTrivialStringConcat(call.getOperands().get(0))) {
+ // Expand SEARCH to OR(EQUALS, ...) so the EQUALS branch above decomposes each CONCAT comparison.
+ final RexNode expanded = RexUtil.expandSearch(rexBuilder, null, call);
+ final RexNode rewritten = expanded.accept(this);
+ //noinspection ObjectEquality
+ if (rewritten != expanded) {
+ return rewritten;
+ }
+ negate = false;
+ newCall = null;
} else {
negate = false;
newCall = null;
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/rule/FixIncorrectInExpansionTypes.java b/sql/src/main/java/org/apache/druid/sql/calcite/rule/FixIncorrectInExpansionTypes.java
deleted file mode 100644
index 9c049ac89dab..000000000000
--- a/sql/src/main/java/org/apache/druid/sql/calcite/rule/FixIncorrectInExpansionTypes.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.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.SqlKind;
-import org.apache.calcite.sql.type.SqlTypeName;
-
-/**
- * Rewrites comparisions to avoid bug FIXME.
- *
- * Rewrites RexCall::VARCHAR = RexLiteral::CHAR to RexCall::VARCHAR =
- * RexLiteral::VARCHAR
- *
- * needed until CALCITE-6435 is fixed & released.
- */
-public class FixIncorrectInExpansionTypes extends RelOptRule implements SubstitutionRule
-{
- public FixIncorrectInExpansionTypes()
- {
- super(operand(RelNode.class, any()));
- }
-
- @Override
- public void onMatch(RelOptRuleCall call)
- {
- final RelNode oldNode = call.rel(0);
- final RewriteShuttle shuttle = new RewriteShuttle(oldNode.getCluster().getRexBuilder());
- final RelNode newNode = oldNode.accept(shuttle);
-
- // noinspection ObjectEquality
- if (newNode != oldNode) {
- call.transformTo(newNode);
- call.getPlanner().prune(oldNode);
- }
- }
-
- private static class RewriteShuttle extends RexShuttle
- {
- private final RexBuilder rexBuilder;
-
- public RewriteShuttle(RexBuilder rexBuilder)
- {
- this.rexBuilder = rexBuilder;
- }
-
- @Override
- public RexNode visitCall(RexCall call)
- {
- RexNode newNode = super.visitCall(call);
- if (newNode.getKind() == SqlKind.EQUALS || newNode.getKind() == SqlKind.NOT_EQUALS) {
- RexCall newCall = (RexCall) newNode;
- RexNode op0 = newCall.getOperands().get(0);
- RexNode op1 = newCall.getOperands().get(1);
- if (RexUtil.isLiteral(op1, false)) {
-
- if (op1.getType().getSqlTypeName() == SqlTypeName.CHAR
- && op0.getType().getSqlTypeName() == SqlTypeName.VARCHAR) {
-
- RexNode newLiteral = rexBuilder.ensureType(op0.getType(), op1, true);
- return rexBuilder.makeCall(
- newCall.getOperator(),
- op0,
- newLiteral
- );
- }
- }
- }
- return newNode;
- }
- }
-}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/rule/InlineValuesSubQueryRule.java b/sql/src/main/java/org/apache/druid/sql/calcite/rule/InlineValuesSubQueryRule.java
new file mode 100644
index 000000000000..290e979667c7
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rule/InlineValuesSubQueryRule.java
@@ -0,0 +1,256 @@
+/*
+ * 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.ImmutableList;
+import com.google.common.collect.ImmutableRangeSet;
+import com.google.common.collect.Range;
+import com.google.common.collect.TreeRangeSet;
+import org.apache.calcite.plan.RelOptLattice;
+import org.apache.calcite.plan.RelOptMaterialization;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelHomogeneousShuttle;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.Values;
+import org.apache.calcite.rel.rules.SubQueryRemoveRule;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexSubQuery;
+import org.apache.calcite.rex.RexUnknownAs;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.tools.Program;
+import org.apache.calcite.util.Sarg;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.sql.calcite.planner.DruidRexExecutor;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.utils.CollectionUtils;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Rewrites {@code [NOT] IN (SELECT col FROM (VALUES ...))} into {@code SEARCH(, Sarg[...])}
+ * before Calcite's {@link SubQueryRemoveRule#rewriteIn} runs. When that rule applies along with an UNNEST,
+ * the subquery ends up being too complex for us to plan. See {@code qaUnnest/mv_sql_subquery_with_where.09.all.iq}
+ * for a test case that would fail without this rule.
+ */
+public class InlineValuesSubQueryRule implements Program
+{
+ private final PlannerContext plannerContext;
+
+ public InlineValuesSubQueryRule(final PlannerContext plannerContext)
+ {
+ this.plannerContext = plannerContext;
+ }
+
+ @Override
+ public RelNode run(
+ final RelOptPlanner planner,
+ final RelNode rel,
+ final RelTraitSet requiredOutputTraits,
+ final List materializations,
+ final List lattices
+ )
+ {
+ return rel.accept(new InlineValuesRelShuttle(plannerContext));
+ }
+
+ private static class InlineValuesRelShuttle extends RelHomogeneousShuttle
+ {
+ private final PlannerContext plannerContext;
+
+ InlineValuesRelShuttle(final PlannerContext plannerContext)
+ {
+ this.plannerContext = plannerContext;
+ }
+
+ @Override
+ public RelNode visit(RelNode other)
+ {
+ final RelNode visited = super.visit(other);
+ if (visited instanceof Filter filter && RexUtil.SubQueryFinder.containsSubQuery(filter)) {
+ final RexNode condition = filter.getCondition();
+ final RexBuilder rexBuilder = filter.getCluster().getRexBuilder();
+ final RexNode newCondition = condition.accept(new InlineValuesRexShuttle(rexBuilder, plannerContext));
+ if (newCondition != condition) {
+ return filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+ }
+ }
+ return visited;
+ }
+ }
+
+ private static class InlineValuesRexShuttle extends RexShuttle
+ {
+ private final RexBuilder rexBuilder;
+ private final int inSubQueryThreshold;
+ private final DruidRexExecutor executor;
+
+ InlineValuesRexShuttle(final RexBuilder rexBuilder, final PlannerContext plannerContext)
+ {
+ this.rexBuilder = rexBuilder;
+ this.inSubQueryThreshold = plannerContext.queryContext().getInSubQueryThreshold();
+ this.executor = new DruidRexExecutor(plannerContext);
+ }
+
+ @Override
+ public RexNode visitSubQuery(final RexSubQuery subQuery)
+ {
+ if (subQuery.getKind() == SqlKind.IN) {
+ final RexNode inlined = tryInlineIn(subQuery);
+ if (inlined != null) {
+ return inlined;
+ }
+ }
+ return super.visitSubQuery(subQuery);
+ }
+
+ @Override
+ public RexNode visitCall(final RexCall call)
+ {
+ // NOT IN appears as NOT(IN(...))
+ if (call.getKind() == SqlKind.NOT && call.getOperands().size() == 1) {
+ final RexNode operand = call.getOperands().get(0);
+ if (operand instanceof RexSubQuery && operand.getKind() == SqlKind.IN) {
+ final RexNode inlined = tryInlineIn((RexSubQuery) operand);
+ if (inlined != null) {
+ return rexBuilder.makeCall(SqlStdOperatorTable.NOT, inlined);
+ }
+ }
+ }
+ return super.visitCall(call);
+ }
+
+ @Nullable
+ private RexNode tryInlineIn(final RexSubQuery subQuery)
+ {
+ if (subQuery.getOperands().size() != 1) {
+ return null;
+ }
+
+ // Accept a bare Values, or a Project-over-Values. For Project, we evaluate the projection
+ // expression on each Values tuple via the executor.
+ final Values valuesRel;
+ final RexNode projExpr;
+ if (subQuery.rel instanceof Values) {
+ valuesRel = (Values) subQuery.rel;
+ projExpr = null;
+ } else if (subQuery.rel instanceof Project
+ && ((Project) subQuery.rel).getProjects().size() == 1
+ && ((Project) subQuery.rel).getInput() instanceof Values) {
+ valuesRel = (Values) ((Project) subQuery.rel).getInput();
+ projExpr = CollectionUtils.getOnlyElement(
+ ((Project) subQuery.rel).getProjects(),
+ xs -> DruidException.defensive("Expected 1 project, got[%s]", xs)
+ );
+ } else {
+ return null;
+ }
+
+ if (valuesRel.getRowType().getFieldCount() != 1) {
+ return null;
+ }
+
+ final ImmutableList> tuples = valuesRel.getTuples();
+ if (tuples.isEmpty() || tuples.size() >= inSubQueryThreshold) {
+ return null;
+ }
+
+ final RelDataType targetType =
+ projExpr != null ? projExpr.getType() : valuesRel.getRowType().getFieldList().getFirst().getType();
+
+ final List literals = new ArrayList<>(tuples.size());
+ for (final ImmutableList tuple : tuples) {
+ if (tuple.size() != 1) {
+ return null;
+ }
+ final RexLiteral raw = tuple.getFirst();
+ final RexLiteral literal;
+ if (projExpr == null) {
+ literal = raw;
+ } else {
+ // Substitute the row's literal for $0 in the projection, then evaluate.
+ final RexNode substituted = projExpr.accept(new RexShuttle()
+ {
+ @Override
+ public RexNode visitInputRef(final RexInputRef ref)
+ {
+ return ref.getIndex() == 0 ? raw : ref;
+ }
+ });
+ final List reducedValues = new ArrayList<>(1);
+ executor.reduce(rexBuilder, ImmutableList.of(substituted), reducedValues);
+ final RexNode reduced = CollectionUtils.getOnlyElement(
+ reducedValues,
+ xs -> DruidException.defensive("Expected 1 value, got[%s]", xs)
+ );
+ if (!(reduced instanceof RexLiteral)) {
+ return null;
+ }
+ literal = (RexLiteral) reduced;
+ }
+ if (literal.isNull()) {
+ // Bounce out if we have an IN/NOT IN with NULL in the list. Let other rules try to handle it.
+ return null;
+ }
+ literals.add(literal);
+ }
+
+ final RexNode lhs = CollectionUtils.getOnlyElement(
+ subQuery.getOperands(),
+ xs -> DruidException.defensive("Expected 1 operator, got[%s]", xs)
+ );
+
+ return buildSearch(lhs, literals, targetType);
+ }
+
+ /**
+ * Build a SEARCH(lhs, Sarg[v1, v2, ...]) expression from the extracted literals. Literals
+ * must be non-null; the caller is responsible for bailing out if any NULL is present in
+ * the VALUES list.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private RexNode buildSearch(
+ final RexNode lhs,
+ final List literals,
+ final RelDataType targetType
+ )
+ {
+ final TreeRangeSet rangeSet = TreeRangeSet.create();
+ for (final RexLiteral literal : literals) {
+ rangeSet.add(Range.singleton(literal.getValueAs(Comparable.class)));
+ }
+
+ final Sarg sarg = Sarg.of(RexUnknownAs.UNKNOWN, ImmutableRangeSet.copyOf(rangeSet));
+ return RexUtil.sargRef(rexBuilder, lhs, sarg, targetType, RexUnknownAs.UNKNOWN);
+ }
+ }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/table/RowSignatures.java b/sql/src/main/java/org/apache/druid/sql/calcite/table/RowSignatures.java
index 9c345dafad0c..444f4d2b9c31 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/table/RowSignatures.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/table/RowSignatures.java
@@ -31,6 +31,7 @@
import org.apache.calcite.sql.type.SqlOperandCountRanges;
import org.apache.calcite.sql.type.SqlSingleOperandTypeChecker;
import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
@@ -58,6 +59,12 @@ private RowSignatures()
// No instantiation.
}
+ /**
+ * Creates a {@link RowSignature} from a Calcite row type with a list of field names. The provided list
+ * of field names is used instead of the field names from the Calcite row type. Care is required when
+ * selecting the field names, because field names in Druid are the primary way of identifying a field,
+ * whereas field names in Calcite are just labels (and may not be unique).
+ */
public static RowSignature fromRelDataType(final List rowOrder, final RelDataType rowType)
{
if (rowOrder.size() != rowType.getFieldCount()) {
@@ -75,6 +82,15 @@ public static RowSignature fromRelDataType(final List rowOrder, final Re
return rowSignatureBuilder.build();
}
+ /**
+ * Creates a {@link RowSignature} from a Calcite row type, adjusting field names if needed
+ * such that they are unique.
+ */
+ public static RowSignature fromRelDataTypeWithUniqueFields(final RelDataType rowType)
+ {
+ return fromRelDataType(SqlValidatorUtil.uniquify(rowType.getFieldNames()), rowType);
+ }
+
/**
* Return the "natural" {@link StringComparator} for an extraction from a row signature. This will be a
* lexicographic comparator for String types and a numeric comparator for Number types.
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java
index 2ca95310e1cc..936d37c32c9a 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java
@@ -1515,7 +1515,7 @@ public void testArrayContainsFilterWithDynamicParameter()
Druids.ScanQueryBuilder builder = newScanQueryBuilder()
.dataSource(CalciteTests.DATASOURCE3)
.intervals(querySegmentSpec(Filtration.eternity()))
- .filters(expressionFilter("array_contains(array(1,null),array((\"dbl1\" > 1)))"))
+ .filters(expressionFilter("array_contains(array(1,null),array((\"dbl1\" > 1.0)))"))
.columns("dim3")
.columnTypes(ColumnType.STRING)
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
@@ -5141,7 +5141,7 @@ public void testUnnestVirtualWithColumns1()
.intervals(querySegmentSpec(Filtration.eternity()))
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
.filters(or(
- range("m1", ColumnType.LONG, null, "10", false, false),
+ range("m1", ColumnType.FLOAT, null, "10.0", false, false),
equality("j0.unnest", "b", ColumnType.STRING)
))
.context(QUERY_CONTEXT_UNNEST)
@@ -5722,7 +5722,7 @@ public void testUnnestWithINFiltersWithLeftRewrite()
.dataSource(UnnestDataSource.create(
FilteredDataSource.create(
new TableDataSource(CalciteTests.DATASOURCE3),
- range("m1", ColumnType.LONG, null, 10L, false, true)
+ range("m1", ColumnType.FLOAT, null, 10.0, false, true)
),
nestedExpressionVirtualColumn("j0.unnest", "\"dim3\"", ColumnType.STRING),
in("j0.unnest", ImmutableSet.of("a", "b"))
@@ -5924,8 +5924,8 @@ public void testUnnestWithMultipleAndFiltersOnSelectedColumns()
FilteredDataSource.create(
new TableDataSource(CalciteTests.DATASOURCE3),
and(
- range("m1", ColumnType.LONG, null, 10L, false, true),
- range("m2", ColumnType.LONG, null, 10L, false, true)
+ range("m1", ColumnType.FLOAT, null, 10.0, false, true),
+ range("m2", ColumnType.DOUBLE, null, 10.0, false, true)
)
),
nestedExpressionVirtualColumn("j0.unnest", "\"dim3\"", ColumnType.STRING),
@@ -5964,7 +5964,7 @@ public void testUnnestWithMultipleOrFiltersOnSelectedColumns()
.filters(
or(
equality("j0.unnest", "b", ColumnType.STRING),
- range("m1", ColumnType.LONG, null, 2L, false, true)
+ range("m1", ColumnType.FLOAT, null, 2.0, false, true)
)
)
.columns("j0.unnest")
@@ -6082,8 +6082,8 @@ public void testUnnestWithMultipleOrFiltersOnSelectedNonUnnestedColumns()
FilteredDataSource.create(
new TableDataSource(CalciteTests.DATASOURCE3),
or(
- range("m1", ColumnType.LONG, null, 2L, false, true),
- range("m2", ColumnType.LONG, null, 2L, false, true)
+ range("m1", ColumnType.FLOAT, null, 2.0, false, true),
+ range("m2", ColumnType.DOUBLE, null, 2.0, false, true)
)
),
nestedExpressionVirtualColumn("j0.unnest", "\"dim3\"", ColumnType.STRING),
@@ -6122,7 +6122,7 @@ public void testUnnestWithMultipleOrFiltersOnSelectedVirtualColumns()
.filters(
or(
in("j0.unnest", ImmutableSet.of("a", "aa")),
- range("m1", ColumnType.LONG, null, 2L, false, true)
+ range("m1", ColumnType.FLOAT, null, 2.0, false, true)
)
)
.columns("j0.unnest")
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java
index d89d618bda13..4ff935a6fbdf 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java
@@ -880,9 +880,9 @@ public void testGroupByInsertAddNonDefinedColumnIntoNonSealedCatalogTable()
dimensions(
new DefaultDimensionSpec("v0", "d0", ColumnType.LONG),
new DefaultDimensionSpec("b", "d1", ColumnType.STRING),
- new DefaultDimensionSpec("c", "d3", ColumnType.LONG),
- new DefaultDimensionSpec("d", "d4", ColumnType.LONG),
- new DefaultDimensionSpec("e", "d5", ColumnType.STRING)
+ new DefaultDimensionSpec("c", "d2", ColumnType.LONG),
+ new DefaultDimensionSpec("d", "d3", ColumnType.LONG),
+ new DefaultDimensionSpec("e", "d4", ColumnType.STRING)
)
)
.setAggregatorSpecs(
@@ -902,7 +902,7 @@ public void testGroupByInsertAddNonDefinedColumnIntoNonSealedCatalogTable()
)
.setPostAggregatorSpecs(
expressionPostAgg("p0", "1", ColumnType.LONG),
- expressionPostAgg("p1", "CAST(\"d3\", 'DOUBLE')", ColumnType.DOUBLE)
+ expressionPostAgg("p1", "CAST(\"d2\", 'DOUBLE')", ColumnType.DOUBLE)
)
.setContext(CalciteIngestionDmlTest.PARTITIONED_BY_ALL_TIME_QUERY_CONTEXT)
.build()
@@ -1123,9 +1123,9 @@ public void testGroupByInsertWithSourceIntoCatalogTable()
dimensions(
new DefaultDimensionSpec("v0", "d0", ColumnType.LONG),
new DefaultDimensionSpec("b", "d1", ColumnType.STRING),
- new DefaultDimensionSpec("c", "d3", ColumnType.LONG),
- new DefaultDimensionSpec("d", "d4", ColumnType.LONG),
- new DefaultDimensionSpec("e", "d5", ColumnType.STRING)
+ new DefaultDimensionSpec("c", "d2", ColumnType.LONG),
+ new DefaultDimensionSpec("d", "d3", ColumnType.LONG),
+ new DefaultDimensionSpec("e", "d4", ColumnType.STRING)
)
)
.setAggregatorSpecs(
@@ -1145,7 +1145,7 @@ public void testGroupByInsertWithSourceIntoCatalogTable()
)
.setPostAggregatorSpecs(
expressionPostAgg("p0", "1", ColumnType.LONG),
- expressionPostAgg("p1", "CAST(\"d3\", 'DOUBLE')", ColumnType.DOUBLE)
+ expressionPostAgg("p1", "CAST(\"d2\", 'DOUBLE')", ColumnType.DOUBLE)
)
.setContext(CalciteIngestionDmlTest.PARTITIONED_BY_ALL_TIME_QUERY_CONTEXT)
.build()
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java
index f5ace8e84ebb..d78aa5d24de3 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java
@@ -3569,8 +3569,8 @@ public void testLeftJoinRightTableCanBeEmpty()
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
.filters(range(
"m1",
- ColumnType.LONG,
- 2L,
+ ColumnType.FLOAT,
+ 2.0,
null,
true,
false
@@ -4754,10 +4754,10 @@ public void testJoinWithEquiAndNonEquiCondition(Map queryContext
JoinType.INNER
)
)
- .virtualColumns(expressionVirtualColumn("v0", "(\"m1\" + \"j0.m1\")", ColumnType.DOUBLE))
+ .virtualColumns(expressionVirtualColumn("v0", "(\"m1\" + \"j0.m1\")", ColumnType.FLOAT))
.intervals(querySegmentSpec(Filtration.eternity()))
.filters(
- equality("v0", 6.0, ColumnType.DOUBLE)
+ equality("v0", 6.0, ColumnType.FLOAT)
)
.columns("m1", "j0.m1")
.columnTypes(ColumnType.FLOAT, ColumnType.FLOAT)
@@ -4970,8 +4970,8 @@ public void testNestedGroupByOnInlineDataSourceWithFilter(Map qu
.dataSource(CalciteTests.DATASOURCE1)
.intervals(querySegmentSpec(Intervals.of(
"2001-01-02T00:00:00.000Z/146140482-04-24T15:36:27.903Z")))
- .columns("dim1", "m2")
- .columnTypes(ColumnType.STRING, ColumnType.DOUBLE)
+ .columns("dim1")
+ .columnTypes(ColumnType.STRING)
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
.context(queryContext)
.build()
@@ -5665,11 +5665,11 @@ public void testPlanWithInFilterMoreThanInSubQueryThreshold()
new Object[]{29064L}
),
RowSignature.builder()
- .add("ROW_VALUE", ColumnType.LONG)
+ .add("EXPR$0", ColumnType.LONG)
.build()
),
"j0.",
- "(\"l1\" == \"j0.ROW_VALUE\")",
+ "(\"l1\" == \"j0.EXPR$0\")",
JoinType.INNER,
null,
ExprMacroTable.nil(),
@@ -5690,7 +5690,7 @@ public void testPlanWithInFilterMoreThanInSubQueryThreshold()
);
}
- @NotYetSupported({Modes.SORT_REMOVE_TROUBLE, Modes.DD_SORT_REMOVE_TROUBLE})
+ @NotYetSupported(Modes.SORT_REMOVE_TROUBLE)
@MethodSource("provideQueryContexts")
@ParameterizedTest(name = "{0}")
public void testRegressionFilteredAggregatorsSubqueryJoins(Map queryContext)
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteLookupFunctionQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteLookupFunctionQueryTest.java
index f4b65da38850..e4055f84513f 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteLookupFunctionQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteLookupFunctionQueryTest.java
@@ -518,9 +518,8 @@ public void testFilterNotInAndIsNotNull()
buildFilterTestExpectedQuery(
expressionVirtualColumn("v0", LOOKUP_EXPRESSION, ColumnType.STRING),
and(
- not(equality("v0", "x6", ColumnType.STRING)),
- not(equality("v0", "nonexistent", ColumnType.STRING)),
- notNull("v0")
+ notNull("v0"),
+ not(in("v0", ImmutableList.of("nonexistent", "x6")))
)
),
ImmutableList.of(new Object[]{"xabc", 1L})
@@ -574,11 +573,8 @@ public void testFilterNotInOrIsNull()
buildFilterTestExpectedQuery(
expressionVirtualColumn("v0", LOOKUP_EXPRESSION, ColumnType.STRING),
or(
- and(
- not(equality("v0", "x6", ColumnType.STRING)),
- not(equality("v0", "nonexistent", ColumnType.STRING))
- ),
- isNull("v0")
+ isNull("v0"),
+ not(in("v0", ImmutableList.of("nonexistent", "x6")))
)
),
ImmutableList.of(
@@ -617,7 +613,7 @@ public void testFilterNotIn()
QUERY_CONTEXT,
buildFilterTestExpectedQuery(
expressionVirtualColumn("v0", LOOKUP_EXPRESSION, ColumnType.STRING),
- and(not(equality("v0", "x6", ColumnType.STRING)), not(equality("v0", "nonexistent", ColumnType.STRING)))
+ not(in("v0", ImmutableList.of("nonexistent", "x6")))
),
ImmutableList.of(new Object[]{"xabc", 1L})
);
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 6e2bb2cddeee..f46408da189e 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
@@ -1072,9 +1072,6 @@ public void testStringToMVOfConstant()
@Test
public void testStringToMVOfConstantGroupedBy()
{
- // Cannot vectorize due to usage of expressions.
- cannotVectorize();
-
testBuilder()
.sql("SELECT m1, STRING_TO_MV('a,b', ',') AS mv FROM druid.numfoo GROUP BY 1, 2")
.expectedQuery(
@@ -1082,30 +1079,23 @@ public void testStringToMVOfConstantGroupedBy()
.setDataSource(CalciteTests.DATASOURCE3)
.setInterval(querySegmentSpec(Filtration.eternity()))
.setGranularity(Granularities.ALL)
- .setVirtualColumns(
- expressionVirtualColumn("v0", "string_to_array('a,b',',')", ColumnType.STRING)
- )
.setDimensions(dimensions(
- new DefaultDimensionSpec("m1", "d0", ColumnType.FLOAT),
- new DefaultDimensionSpec("v0", "d1", ColumnType.STRING)
+ new DefaultDimensionSpec("m1", "d0", ColumnType.FLOAT)
))
+ .setPostAggregatorSpecs(
+ expressionPostAgg("p0", "string_to_array('a,b',',')", ColumnType.STRING)
+ )
.setContext(QUERY_CONTEXT_DEFAULT)
.build()
)
.expectedResults(
ImmutableList.of(
- new Object[]{1.0f, "a"},
- new Object[]{1.0f, "b"},
- new Object[]{2.0f, "a"},
- new Object[]{2.0f, "b"},
- new Object[]{3.0f, "a"},
- new Object[]{3.0f, "b"},
- new Object[]{4.0f, "a"},
- new Object[]{4.0f, "b"},
- new Object[]{5.0f, "a"},
- new Object[]{5.0f, "b"},
- new Object[]{6.0f, "a"},
- new Object[]{6.0f, "b"}
+ new Object[]{1.0f, "[\"a\",\"b\"]"},
+ new Object[]{2.0f, "[\"a\",\"b\"]"},
+ new Object[]{3.0f, "[\"a\",\"b\"]"},
+ new Object[]{4.0f, "[\"a\",\"b\"]"},
+ new Object[]{5.0f, "[\"a\",\"b\"]"},
+ new Object[]{6.0f, "[\"a\",\"b\"]"}
)
)
.run();
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
index 842dc283fc6e..0665304fa0f8 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
@@ -7249,7 +7249,7 @@ public void testNvlJsonValueDoubleMissingColumn()
),
expressionVirtualColumn("v2", "notnull(nvl(\"v1\",1.0))", ColumnType.LONG)
)
- .filters(range("v0", ColumnType.LONG, 0.0, null, true, false))
+ .filters(range("v0", ColumnType.DOUBLE, 0.0, null, true, false))
.limit(1)
.columns("v1", "v0", "v2")
.columnTypes(ColumnType.DOUBLE, ColumnType.DOUBLE, ColumnType.LONG)
@@ -7281,7 +7281,7 @@ public void testNvlJsonValueDoubleSometimesMissing()
.virtualColumns(
new NestedFieldVirtualColumn("nest", "$.y", "v0", ColumnType.DOUBLE),
expressionVirtualColumn("v1", "nvl(\"v0\",1.0)", ColumnType.DOUBLE),
- expressionVirtualColumn("v2", "(nvl(\"v0\",1.0) > 0)", ColumnType.LONG),
+ expressionVirtualColumn("v2", "(nvl(\"v0\",1.0) > 0.0)", ColumnType.LONG),
expressionVirtualColumn("v3", "(nvl(\"v0\",1.0) == 1.0)", ColumnType.LONG)
)
.columns("v0", "v1", "v2", "v3")
@@ -7325,7 +7325,7 @@ public void testNvlJsonValueDoubleSometimesMissingRangeFilter()
new NestedFieldVirtualColumn("nest", "$.y", "v1", ColumnType.DOUBLE),
expressionVirtualColumn("v2", "notnull(nvl(\"v1\",1.0))", ColumnType.LONG)
)
- .filters(range("v0", ColumnType.LONG, 0.0, null, true, false))
+ .filters(range("v0", ColumnType.DOUBLE, 0.0, null, true, false))
.columns("v1", "v0", "v2")
.columnTypes(ColumnType.DOUBLE, ColumnType.DOUBLE, ColumnType.LONG)
.build()
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 fcb944852453..848b54604b71 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
@@ -182,9 +182,14 @@ public void testInformationSchemaTables()
testQuery(
"SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, IS_JOINABLE, IS_BROADCAST\n"
+ "FROM INFORMATION_SCHEMA.TABLES\n"
- + "WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW')",
+ + "WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW')\n"
+ + "ORDER BY TABLE_SCHEMA, TABLE_NAME",
ImmutableList.of(),
ImmutableList.