This is an automated email from the ASF dual-hosted git repository.
tjbanghart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 9c82eb4b68 [CALCITE-1440] Add Combine RelNode for converting multiple
SQL statements to unified RelNode Tree
9c82eb4b68 is described below
commit 9c82eb4b68f2ae2dc4e410749f7419fcdabdea96
Author: TJ Banghart <[email protected]>
AuthorDate: Mon Sep 15 16:05:38 2025 -0700
[CALCITE-1440] Add Combine RelNode for converting multiple SQL statements
to unified RelNode Tree
---
.../java/org/apache/calcite/rel/core/Combine.java | 105 +++++++
.../org/apache/calcite/rel/core/RelFactories.java | 34 ++-
.../rel/metadata/RelMdPercentageOriginalRows.java | 48 +++
.../apache/calcite/rel/metadata/RelMdRowCount.java | 15 +
.../java/org/apache/calcite/tools/RelBuilder.java | 43 ++-
.../org/apache/calcite/test/RelBuilderTest.java | 338 +++++++++++++++++++++
.../GeneratedMetadata_CumulativeCostHandler.java | 2 +
...atedMetadata_PercentageOriginalRowsHandler.java | 2 +
.../janino/GeneratedMetadata_RowCountHandler.java | 2 +
9 files changed, 577 insertions(+), 12 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Combine.java
b/core/src/main/java/org/apache/calcite/rel/core/Combine.java
new file mode 100644
index 0000000000..286facf2c8
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/core/Combine.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.rel.core;
+
+import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * A relational operator that combines multiple relational expressions into a
single root.
+ * This is used for multi-root optimization in the VolcanoPlanner.
+ */
+public class Combine extends AbstractRelNode {
+ protected final ImmutableList<RelNode> inputs;
+
+ /** Creates a Combine. */
+ public static Combine create(RelOptCluster cluster, RelTraitSet traitSet,
List<RelNode> inputs) {
+ return new Combine(cluster, traitSet, inputs);
+ }
+
+
+ public Combine(RelOptCluster cluster, RelTraitSet traitSet, List<RelNode>
inputs) {
+ super(cluster, traitSet);
+ this.inputs = ImmutableList.copyOf(inputs);
+ }
+
+ @Override public List<RelNode> getInputs() {
+ return inputs;
+ }
+
+ @Override public RelWriter explainTerms(RelWriter pw) {
+ super.explainTerms(pw);
+ for (Ord<RelNode> ord : Ord.zip(inputs)) {
+ pw.input("input#" + ord.i, ord.e);
+ }
+ return pw;
+ }
+
+ @Override protected RelDataType deriveRowType() {
+ // Combine represents multiple independent result sets that are not merged.
+ // Each input maintains its own row type and is accessed independently.
+ //
+ // We use a struct type where each field represents one of the input
queries.
+ // This allows metadata and optimization rules to understand the structure
+ // while making it clear that results are not unified into a single stream.
+
+ RelDataTypeFactory typeFactory = getCluster().getTypeFactory();
+ RelDataTypeFactory.Builder builder = typeFactory.builder();
+
+ for (int i = 0; i < inputs.size(); i++) {
+ RelNode input = inputs.get(i);
+ // Create a field for each input with its row type
+ // Field names are "QUERY_0", "QUERY_1", etc.
+ builder.add("QUERY_" + i, input.getRowType());
+ }
+
+ return builder.build();
+ }
+
+ @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
RelMetadataQuery mq) {
+ // The self cost of Combine is minimal - it's just a structural operator
+ // that binds multiple queries together for optimization purposes.
+ // The real cost comes from executing all the inputs (handled by
getCumulativeCost).
+
+ // We add a tiny cost to represent the overhead of managing multiple
result sets
+ double rowCount = 0;
+ for (RelNode input : inputs) {
+ Double inputRows = mq.getRowCount(input);
+ rowCount += inputRows;
+ }
+
+ // Very small CPU cost for result set management
+ // No I/O cost since Combine doesn't read data itself
+ return planner.getCostFactory().makeCost(
+ rowCount,
+ rowCount * 0.01, // minimal CPU cost
+ 0); // no I/O
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java
b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java
index a8e53d9b74..9844219ecb 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java
@@ -130,6 +130,9 @@ public class RelFactories {
public static final RepeatUnionFactory DEFAULT_REPEAT_UNION_FACTORY =
new RepeatUnionFactoryImpl();
+ public static final CombineFactory DEFAULT_COMBINE_FACTORY =
+ new CombineFactoryImpl();
+
public static final Struct DEFAULT_STRUCT =
new Struct(DEFAULT_FILTER_FACTORY,
DEFAULT_PROJECT_FACTORY,
@@ -148,7 +151,8 @@ public class RelFactories {
DEFAULT_SAMPLE_FACTORY,
DEFAULT_MATCH_FACTORY,
DEFAULT_SPOOL_FACTORY,
- DEFAULT_REPEAT_UNION_FACTORY);
+ DEFAULT_REPEAT_UNION_FACTORY,
+ DEFAULT_COMBINE_FACTORY);
/** A {@link RelBuilderFactory} that creates a {@link RelBuilder} that will
* create logical relational expressions for everything. */
@@ -704,6 +708,25 @@ private static class RepeatUnionFactoryImpl implements
RepeatUnionFactory {
}
}
+ /**
+ * Can create a {@link Combine} of the appropriate type for a rule's calling
+ * convention.
+ */
+ public interface CombineFactory {
+ /** Creates a {@link Combine}. */
+ RelNode createCombine(RelOptCluster cluster, List<RelNode> inputs);
+ }
+
+ /**
+ * Implementation of {@link CombineFactory} that returns a
+ * {@link Combine}.
+ */
+ private static class CombineFactoryImpl implements CombineFactory {
+ @Override public RelNode createCombine(RelOptCluster cluster,
List<RelNode> inputs) {
+ return Combine.create(cluster, cluster.traitSet(), inputs);
+ }
+ }
+
/** Immutable record that contains an instance of each factory. */
public static class Struct {
public final FilterFactory filterFactory;
@@ -724,6 +747,7 @@ public static class Struct {
public final SampleFactory sampleFactory;
public final SpoolFactory spoolFactory;
public final RepeatUnionFactory repeatUnionFactory;
+ public final CombineFactory combineFactory;
private Struct(FilterFactory filterFactory,
ProjectFactory projectFactory,
@@ -742,7 +766,8 @@ private Struct(FilterFactory filterFactory,
SampleFactory sampleFactory,
MatchFactory matchFactory,
SpoolFactory spoolFactory,
- RepeatUnionFactory repeatUnionFactory) {
+ RepeatUnionFactory repeatUnionFactory,
+ CombineFactory combineFactory) {
this.filterFactory = requireNonNull(filterFactory, "filterFactory");
this.projectFactory = requireNonNull(projectFactory, "projectFactory");
this.aggregateFactory = requireNonNull(aggregateFactory,
"aggregateFactory");
@@ -762,6 +787,7 @@ private Struct(FilterFactory filterFactory,
this.matchFactory = requireNonNull(matchFactory, "matchFactory");
this.spoolFactory = requireNonNull(spoolFactory, "spoolFactory");
this.repeatUnionFactory = requireNonNull(repeatUnionFactory,
"repeatUnionFactory");
+ this.combineFactory = requireNonNull(combineFactory, "combineFactory");
}
public static Struct fromContext(Context context) {
@@ -805,7 +831,9 @@ public static Struct fromContext(Context context) {
context.maybeUnwrap(SpoolFactory.class)
.orElse(DEFAULT_SPOOL_FACTORY),
context.maybeUnwrap(RepeatUnionFactory.class)
- .orElse(DEFAULT_REPEAT_UNION_FACTORY));
+ .orElse(DEFAULT_REPEAT_UNION_FACTORY),
+ context.maybeUnwrap(CombineFactory.class)
+ .orElse(DEFAULT_COMBINE_FACTORY));
}
}
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPercentageOriginalRows.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPercentageOriginalRows.java
index 4e530f8809..e2ec6d94b8 100644
---
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPercentageOriginalRows.java
+++
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPercentageOriginalRows.java
@@ -20,6 +20,7 @@
import org.apache.calcite.plan.RelOptCost;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.Combine;
import org.apache.calcite.rel.core.Join;
import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.core.Union;
@@ -123,6 +124,32 @@ public Double getPercentageOriginalRows(Union rel,
RelMetadataQuery mq) {
return left * right;
}
+ public @Nullable Double getPercentageOriginalRows(Combine rel,
RelMetadataQuery mq) {
+ // For Combine, we return the weighted average of the percentage
+ // original rows across all inputs, weighted by row count
+ double totalRows = 0;
+ double weightedPercentage = 0;
+
+ for (RelNode input : rel.getInputs()) {
+ Double rowCount = mq.getRowCount(input);
+ if (rowCount == null) {
+ return null;
+ }
+ Double percentage = mq.getPercentageOriginalRows(input);
+ if (percentage == null) {
+ return null;
+ }
+ totalRows += rowCount;
+ weightedPercentage += rowCount * percentage;
+ }
+
+ if (totalRows == 0) {
+ return 1.0; // No rows, so 100% original
+ }
+
+ return weightedPercentage / totalRows;
+ }
+
// Catch-all rule when none of the others apply.
public @Nullable Double getPercentageOriginalRows(RelNode rel,
RelMetadataQuery mq) {
if (rel.getInputs().size() > 1) {
@@ -182,6 +209,27 @@ public Double getPercentageOriginalRows(Union rel,
RelMetadataQuery mq) {
return mq.getNonCumulativeCost(rel);
}
+ public @Nullable RelOptCost getCumulativeCost(Combine rel, RelMetadataQuery
mq) {
+ // For Combine, the cumulative cost is the sum of all input costs
+ // since all inputs need to be executed (unlike Union where rows flow
through)
+ RelOptCost cost = mq.getNonCumulativeCost(rel);
+ if (cost == null) {
+ return null;
+ }
+
+ // Add the cumulative cost of each input
+ // All inputs contribute to the total cost since they all must be executed
+ for (RelNode input : rel.getInputs()) {
+ RelOptCost inputCost = mq.getCumulativeCost(input);
+ if (inputCost == null) {
+ return null;
+ }
+ cost = cost.plus(inputCost);
+ }
+
+ return cost;
+ }
+
// Ditto for getNonCumulativeCost
public @Nullable RelOptCost getNonCumulativeCost(RelNode rel,
RelMetadataQuery mq) {
return rel.computeSelfCost(rel.getCluster().getPlanner(), mq);
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
index eeb8cc9adc..637e8777cc 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
@@ -23,6 +23,7 @@
import org.apache.calcite.rel.SingleRel;
import org.apache.calcite.rel.core.Aggregate;
import org.apache.calcite.rel.core.Calc;
+import org.apache.calcite.rel.core.Combine;
import org.apache.calcite.rel.core.Exchange;
import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.core.Intersect;
@@ -87,6 +88,20 @@ public class RelMdRowCount
return Util.first(v, 1e6d); // if set is empty, estimate large
}
+ public @Nullable Double getRowCount(Combine rel, RelMetadataQuery mq) {
+ // For Combine, the row count is the sum of all input row counts
+ // since each input represents a separate query that will be executed
+ double totalRowCount = 0.0;
+ for (RelNode input : rel.getInputs()) {
+ Double inputRowCount = mq.getRowCount(input);
+ if (inputRowCount == null) {
+ return null;
+ }
+ totalRowCount += inputRowCount;
+ }
+ return totalRowCount;
+ }
+
public @Nullable Double getRowCount(Union rel, RelMetadataQuery mq) {
double rowCount = 0.0;
for (RelNode input : rel.getInputs()) {
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index 90df447ebc..2aca3fc58a 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -36,6 +36,7 @@
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Aggregate;
import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.Combine;
import org.apache.calcite.rel.core.Correlate;
import org.apache.calcite.rel.core.CorrelationId;
import org.apache.calcite.rel.core.Filter;
@@ -1185,8 +1186,8 @@ public RexNode cast(RexNode expr, SqlTypeName typeName,
int precision) {
return cast(SqlParserPos.ZERO, expr, typeName, precision);
}
- /** Creates an expression that casts an expression to a type with a given
name
- * and precision or length. */
+ /** Creates an expression that casts an expression to a type with a given
name
+ * and precision or length. */
public RexNode cast(SqlParserPos pos, RexNode expr, SqlTypeName typeName,
int precision) {
final RelDataType type =
cluster.getTypeFactory().createSqlType(typeName, precision);
@@ -2360,11 +2361,11 @@ public RelBuilder uncollect(List<String> itemAliases,
boolean withOrdinality) {
stack.push(
new Frame(
new Uncollect(
- cluster,
- cluster.traitSetOf(Convention.NONE),
- frame.rel,
- withOrdinality,
- requireNonNull(itemAliases, "itemAliases"))));
+ cluster,
+ cluster.traitSetOf(Convention.NONE),
+ frame.rel,
+ withOrdinality,
+ requireNonNull(itemAliases, "itemAliases"))));
return this;
}
@@ -2840,8 +2841,7 @@ private RelBuilder rewriteAggregateWithDuplicateGroupSets(
for (Multiset.Entry<ImmutableBitSet> entry : groupSets.entrySet()) {
int groupId = entry.getCount() - 1;
for (int i = 0; i <= groupId; i++) {
- groupIdToGroupSets.computeIfAbsent(i,
- k -> Sets.newTreeSet(ImmutableBitSet.COMPARATOR))
+ groupIdToGroupSets.computeIfAbsent(i, k ->
Sets.newTreeSet(ImmutableBitSet.COMPARATOR))
.add(entry.getElement());
}
}
@@ -5398,4 +5398,29 @@ private interface RegisterAgg {
RexInputRef registerAgg(SqlAggFunction op, List<RexNode> operands,
RelDataType type, @Nullable String name);
}
+
+ /** Creates a {@link Combine} of the top {@code n} relational expressions
+ * on the stack. */
+ public RelBuilder combine(int n) {
+ final List<RelNode> inputs = new ArrayList<>();
+ for (int i = 0; i < n; i++) {
+ inputs.add(0, peek(i));
+ }
+ return push(struct.combineFactory.createCombine(cluster, inputs));
+ }
+
+ /** Creates a {@link Combine} of all relational expressions on the stack. */
+ public RelBuilder combine() {
+ return combine(size());
+ }
+
+ /** Creates a {@link Combine} of the given relational expressions. */
+ public RelBuilder combine(RelNode... inputs) {
+ return push(struct.combineFactory.createCombine(cluster,
Arrays.asList(inputs)));
+ }
+
+ /** Creates a {@link Combine} of the given relational expressions. */
+ public RelBuilder combine(Iterable<? extends RelNode> inputs) {
+ return push(struct.combineFactory.createCombine(cluster,
ImmutableList.copyOf(inputs)));
+ }
}
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index eb4f719991..f1dc0baac9 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -23,6 +23,7 @@
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.plan.Contexts;
import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.RelOptCost;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelTraitDef;
import org.apache.calcite.plan.RelTraitSet;
@@ -31,9 +32,11 @@
import org.apache.calcite.rel.RelFieldCollation;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.Combine;
import org.apache.calcite.rel.core.Correlate;
import org.apache.calcite.rel.core.CorrelationId;
import org.apache.calcite.rel.core.Exchange;
+import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.core.JoinRelType;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.core.Sort;
@@ -124,6 +127,7 @@
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
+import static org.apache.calcite.test.Matchers.containsStringLinux;
import static org.apache.calcite.test.Matchers.hasExpandedTree;
import static org.apache.calcite.test.Matchers.hasFieldNames;
import static org.apache.calcite.test.Matchers.hasHints;
@@ -136,6 +140,8 @@
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.hasToString;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -5585,6 +5591,289 @@ private static RelNode
buildCorrelateWithJoin(JoinRelType type, RelBuilder build
assertThat(root, hasTree(expected));
}
+
+ @Test void testCombine() {
+ // Create two separate queries
+ final RelBuilder builder = RelBuilder.create(config().build());
+ RelNode scan1 = builder.scan("EMP")
+ .filter(builder.equals(builder.field("DEPTNO"), builder.literal(10)))
+ .project(builder.field("ENAME"))
+ .build();
+ RelNode scan2 = builder.scan("EMP")
+ .filter(builder.equals(builder.field("DEPTNO"), builder.literal(20)))
+ .project(builder.field("SAL"))
+ .build();
+
+ // Test combine with varargs
+ RelNode combine1 = builder.combine(scan1, scan2).build();
+ assertThat(combine1, instanceOf(Combine.class));
+ assertThat(combine1.getInputs(), hasSize(2));
+
+ // Test combine with Iterable
+ RelNode combine2 = builder.combine(Arrays.asList(scan1, scan2)).build();
+ assertThat(combine2, instanceOf(Combine.class));
+ assertThat(combine2.getInputs(), hasSize(2));
+
+ // Test combine with stack
+ builder.push(scan1)
+ .push(scan2);
+ RelNode combine3 = builder.combine(2).build();
+ assertThat(combine3, instanceOf(Combine.class));
+ assertThat(((Combine) combine3).getInputs(), hasSize(2));
+
+ // Test combine with all stack
+ builder.clear();
+ builder.push(scan1)
+ .push(scan2);
+ RelNode combine4 = builder.combine().build();
+ assertThat(combine4, instanceOf(Combine.class));
+ assertThat(combine4.getInputs(), hasSize(2));
+ }
+
+ @Test void testCombineWithSharedSubexpression() {
+ // Test that demonstrates how Combine can optimize queries with shared
subexpressions
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // Create a common subexpression - employees with salary > 1000
+ builder.scan("EMP")
+ .filter(
+ builder.call(SqlStdOperatorTable.GREATER_THAN,
+ builder.field("SAL"), builder.literal(1000)));
+ RelNode commonSubexpr = builder.build();
+
+ // Query 1: Count high earners by department
+ RelNode query1 = builder.push(commonSubexpr)
+ .aggregate(builder.groupKey("DEPTNO"), builder.count())
+ .build();
+
+ // Query 2: Get names of high earners
+ RelNode query2 = builder.push(commonSubexpr)
+ .project(builder.field("ENAME"), builder.field("SAL"))
+ .build();
+
+ // Combine both queries - planner can recognize shared subexpression
+ RelNode combined = builder.combine(query1, query2).build();
+
+ assertThat(combined, instanceOf(Combine.class));
+ assertThat(combined.getInputs(), hasSize(2));
+ // Both queries share the same filter as their base
+ assertThat(combined.getInputs().get(0).getInput(0),
instanceOf(Filter.class));
+ assertThat(combined.getInputs().get(1).getInput(0),
instanceOf(Filter.class));
+ }
+
+ @Test void testCombineDifferentRowTypes() {
+ // Test that Combine doesn't require inputs to have the same row type.
+ // Contrast to Union which requires row types to unify to the same row
type, element-wise.
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // Query 1: Returns (ENAME: VARCHAR)
+ RelNode query1 = builder.scan("EMP")
+ .project(builder.field("ENAME"))
+ .build();
+
+ // Query 2: Returns (DEPTNO: INTEGER, AVG_SAL: DECIMAL)
+ RelNode query2 = builder.scan("EMP")
+ .aggregate(builder.groupKey("DEPTNO"),
+ builder.avg(false, "AVG_SAL", builder.field("SAL")))
+ .build();
+
+ // Query 3: Returns just count (COUNT: BIGINT)
+ RelNode query3 = builder.scan("DEPT")
+ .aggregate(builder.groupKey(), builder.count())
+ .build();
+
+ // Combine can handle different row types
+ RelNode combined = builder.combine(query1, query2, query3).build();
+
+ assertThat(combined, instanceOf(Combine.class));
+ assertThat(combined.getInputs(), hasSize(3));
+ // Verify each has different row type
+ assertThat(combined.getInputs().get(0).getRowType().getFieldCount(),
is(1));
+ assertThat(combined.getInputs().get(1).getRowType().getFieldCount(),
is(2));
+ assertThat(combined.getInputs().get(2).getRowType().getFieldCount(),
is(1));
+ }
+
+ @Test void testCombineMetadata() {
+ // Test metadata methods for Combine operator
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // Create queries with known characteristics
+ RelNode query1 = builder.scan("EMP")
+ .filter(builder.equals(builder.field("DEPTNO"), builder.literal(10)))
+ .build();
+
+ RelNode query2 = builder.scan("DEPT")
+ .project(builder.field("DNAME"))
+ .build();
+
+ RelNode combined = builder.combine(query1, query2).build();
+
+ // Test that Combine has proper metadata
+ assertThat(combined, instanceOf(Combine.class));
+
+ RelMetadataQuery mq = combined.getCluster().getMetadataQuery();
+
+ // Test row count - should be sum of inputs
+ Double rowCount = mq.getRowCount(combined);
+ assertThat(rowCount, notNullValue());
+
+ // Test cost computation
+ RelOptCost selfCost =
+ combined.computeSelfCost(combined.getCluster().getPlanner(), mq);
+ assertThat(selfCost, notNullValue());
+ assertThat(selfCost.getCpu(), greaterThan(0.0));
+
+ // Test cumulative cost includes all inputs
+ RelOptCost cumulativeCost = mq.getCumulativeCost(combined);
+ assertThat(cumulativeCost, notNullValue());
+
+ // Cumulative cost should be greater than self cost
+ assertThat(cumulativeCost.isLt(selfCost), is(false));
+ }
+
+ @Test void testCombineCumulativeCost() {
+ // Comprehensive test for getCumulativeCost with Combine operator
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // Create three queries with different complexities
+ // Query 1: Simple scan
+ RelNode scan = builder.scan("EMP").build();
+
+ // Query 2: Scan with filter
+ RelNode filtered = builder.scan("EMP")
+ .filter(
+ builder.call(SqlStdOperatorTable.GREATER_THAN,
+ builder.field("SAL"), builder.literal(1000)))
+ .build();
+
+ // Query 3: Scan with aggregation
+ RelNode aggregated = builder.scan("EMP")
+ .aggregate(builder.groupKey("DEPTNO"),
+ builder.count(false, "CNT"),
+ builder.sum(false, "SUM_SAL", builder.field("SAL")))
+ .build();
+
+ // Create Combine with all three queries
+ RelNode combined = builder.combine(scan, filtered, aggregated).build();
+
+ RelMetadataQuery mq = combined.getCluster().getMetadataQuery();
+
+ // Get individual cumulative costs
+ RelOptCost scanCost = mq.getCumulativeCost(scan);
+ RelOptCost filteredCost = mq.getCumulativeCost(filtered);
+ RelOptCost aggregatedCost = mq.getCumulativeCost(aggregated);
+
+ assertThat(scanCost, notNullValue());
+ assertThat(filteredCost, notNullValue());
+ assertThat(aggregatedCost, notNullValue());
+
+ // Get Combine's costs
+ RelOptCost combineSelfCost =
+ combined.computeSelfCost(combined.getCluster().getPlanner(), mq);
+ RelOptCost combineCumulativeCost = mq.getCumulativeCost(combined);
+
+ assertThat(combineSelfCost, notNullValue());
+ assertThat(combineCumulativeCost, notNullValue());
+
+ // Verify cumulative cost is sum of all input costs plus self cost
+ RelOptCost expectedTotal = combineSelfCost
+ .plus(scanCost)
+ .plus(filteredCost)
+ .plus(aggregatedCost);
+
+ // The cumulative cost should equal the sum
+ assertThat(combineCumulativeCost.getRows(),
+ is(expectedTotal.getRows()));
+ assertThat(combineCumulativeCost.getCpu(),
+ is(expectedTotal.getCpu()));
+ assertThat(combineCumulativeCost.getIo(),
+ is(expectedTotal.getIo()));
+
+ // Verify relationships between costs
+ // Filtered should cost more than simple scan
+ assertThat(filteredCost.isLt(scanCost), is(false));
+
+ // Aggregated should cost more than simple scan
+ assertThat(aggregatedCost.isLt(scanCost), is(false));
+
+ // Combined cumulative cost should be greater than any individual cost
+ assertThat(combineCumulativeCost.isLt(scanCost), is(false));
+ assertThat(combineCumulativeCost.isLt(filteredCost), is(false));
+ assertThat(combineCumulativeCost.isLt(aggregatedCost), is(false));
+ }
+
+ @Test void testCombineCumulativeCostWithSharedInputs() {
+ // Test cumulative cost when Combine has shared subexpressions
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // Create a base scan that will be shared
+ RelNode baseScan = builder.scan("EMP").build();
+ RelOptCost baseScanCost =
+ baseScan.computeSelfCost(baseScan.getCluster().getPlanner(),
+ baseScan.getCluster().getMetadataQuery());
+ assertThat(baseScanCost, notNullValue());
+
+ // Create two queries that use the same base scan
+ RelNode query1 = builder.push(baseScan)
+ .filter(builder.equals(builder.field("DEPTNO"), builder.literal(10)))
+ .project(builder.field("ENAME"))
+ .build();
+
+ RelNode query2 = builder.push(baseScan)
+ .filter(builder.equals(builder.field("DEPTNO"), builder.literal(20)))
+ .aggregate(builder.groupKey(), builder.count())
+ .build();
+
+ // Combine the queries
+ RelNode combined = builder.combine(query1, query2).build();
+
+ RelMetadataQuery mq = combined.getCluster().getMetadataQuery();
+
+ // Get costs
+ RelOptCost query1Cost = mq.getCumulativeCost(query1).minus(baseScanCost);
+ RelOptCost query2Cost = mq.getCumulativeCost(query2).minus(baseScanCost);
+ RelOptCost combinedCost = mq.getCumulativeCost(combined);
+
+ assertThat(query1Cost, notNullValue());
+ assertThat(query2Cost, notNullValue());
+ assertThat(combinedCost, notNullValue());
+
+ // Even with shared base scan, Combine's cumulative cost
+ // should include full cost of each branch
+ RelOptCost combineSelfCost =
+ combined.computeSelfCost(combined.getCluster().getPlanner(), mq);
+ RelOptCost expectedTotal = combineSelfCost
+ .plus(baseScanCost.multiplyBy(2)) // base scan counted twice since
used in both queries
+ .plus(query1Cost)
+ .plus(query2Cost);
+
+ assertThat(combinedCost.getRows(), is(expectedTotal.getRows()));
+ assertThat(combinedCost.getCpu(), is(expectedTotal.getCpu()));
+ assertThat(combinedCost.getIo(), is(expectedTotal.getIo()));
+ }
+
+ @Test void testCombineCumulativeCostEmpty() {
+ // Test edge case with empty inputs
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // Create an empty values relation
+ RelNode emptyValues = builder.values(new String[]{"x"}, 0).build();
+
+ // Create a normal query
+ RelNode normalQuery = builder.scan("DEPT").build();
+
+ // Combine empty and normal
+ RelNode combined = builder.combine(emptyValues, normalQuery).build();
+
+ RelMetadataQuery mq = combined.getCluster().getMetadataQuery();
+
+ // Should still compute costs correctly even with empty input
+ RelOptCost combinedCost = mq.getCumulativeCost(combined);
+ assertThat(combinedCost, notNullValue());
+ assertThat(combinedCost.getRows(), greaterThan(-1.0));
+ assertThat(combinedCost.getCpu(), greaterThan(-1.0));
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-4415">[CALCITE-4415]
* SqlStdOperatorTable.NOT_LIKE has a wrong implementor</a>. */
@@ -5609,6 +5898,55 @@ private static RelNode
buildCorrelateWithJoin(JoinRelType type, RelBuilder build
"empid=150; name=Sebastian");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-1440">[CALCITE-1440]
+ * Combine operator - combines multiple SQL queries into a single plan</a>.
*/
+ @Test void testCombineExplain() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // Query 1: Simple filter on department 10
+ RelNode query1 = builder
+ .scan("EMP")
+ .filter(builder.equals(builder.field("DEPTNO"), builder.literal(10)))
+ .project(builder.field("ENAME"), builder.field("SAL"))
+ .build();
+
+ // Query 2: Aggregate to get average salary by department
+ RelNode query2 = builder
+ .scan("EMP")
+ .aggregate(builder.groupKey("DEPTNO"),
+ builder.avg(false, "AVG_SAL", builder.field("SAL")))
+ .build();
+
+ // Query 3: Find all departments
+ RelNode query3 = builder
+ .scan("DEPT")
+ .project(builder.field("DEPTNO"), builder.field("DNAME"))
+ .build();
+
+ // Combine all three queries
+ RelNode combined = builder.combine(query1, query2, query3).build();
+
+ // Test that the combine node is of the correct type
+ assertThat(combined, instanceOf(Combine.class));
+ Combine combineNode = (Combine) combined;
+ assertThat(combineNode.getInputs(), hasSize(3));
+
+ String expectedPlan =
+ "Combine\n"
+ + " LogicalProject(ENAME=[$1], SAL=[$5])\n"
+ + " LogicalFilter(condition=[=($7, 10)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalAggregate(group=[{7}], AVG_SAL=[AVG($5)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalProject(DEPTNO=[$0], DNAME=[$1])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n";
+
+ // Test that explain() works correctly for Combine
+ String explanation = combined.explain();
+ assertThat(explanation, containsStringLinux(expectedPlan));
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-6688">[CALCITE-6688]
* Allow operators of SqlKind.SYMMETRICAL to be reversed</a>. */
diff --git
a/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_CumulativeCostHandler.java
b/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_CumulativeCostHandler.java
index 0f76154ec7..d77b827cb9 100644
---
a/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_CumulativeCostHandler.java
+++
b/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_CumulativeCostHandler.java
@@ -62,6 +62,8 @@ private org.apache.calcite.plan.RelOptCost getCumulativeCost_(
org.apache.calcite.rel.metadata.RelMetadataQuery mq) {
if (r instanceof
org.apache.calcite.adapter.enumerable.EnumerableInterpreter) {
return
provider0.getCumulativeCost((org.apache.calcite.adapter.enumerable.EnumerableInterpreter)
r, mq);
+ } else if (r instanceof org.apache.calcite.rel.core.Combine) {
+ return provider0.getCumulativeCost((org.apache.calcite.rel.core.Combine)
r, mq);
} else if (r instanceof org.apache.calcite.rel.RelNode) {
return provider0.getCumulativeCost((org.apache.calcite.rel.RelNode) r,
mq);
} else {
diff --git
a/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_PercentageOriginalRowsHandler.java
b/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_PercentageOriginalRowsHandler.java
index d83e952a92..d52c75d355 100644
---
a/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_PercentageOriginalRowsHandler.java
+++
b/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_PercentageOriginalRowsHandler.java
@@ -62,6 +62,8 @@ private java.lang.Double getPercentageOriginalRows_(
org.apache.calcite.rel.metadata.RelMetadataQuery mq) {
if (r instanceof org.apache.calcite.rel.core.Aggregate) {
return
provider0.getPercentageOriginalRows((org.apache.calcite.rel.core.Aggregate) r,
mq);
+ } else if (r instanceof org.apache.calcite.rel.core.Combine) {
+ return
provider0.getPercentageOriginalRows((org.apache.calcite.rel.core.Combine) r,
mq);
} else if (r instanceof org.apache.calcite.rel.core.Join) {
return
provider0.getPercentageOriginalRows((org.apache.calcite.rel.core.Join) r, mq);
} else if (r instanceof org.apache.calcite.rel.core.TableScan) {
diff --git
a/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_RowCountHandler.java
b/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_RowCountHandler.java
index cef8019f14..9e6cc7205c 100644
---
a/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_RowCountHandler.java
+++
b/core/src/test/resources/org/apache/calcite/rel/metadata/janino/GeneratedMetadata_RowCountHandler.java
@@ -68,6 +68,8 @@ private java.lang.Double getRowCount_(
return provider0.getRowCount((org.apache.calcite.rel.core.Aggregate) r,
mq);
} else if (r instanceof org.apache.calcite.rel.core.Calc) {
return provider0.getRowCount((org.apache.calcite.rel.core.Calc) r, mq);
+ } else if (r instanceof org.apache.calcite.rel.core.Combine) {
+ return provider0.getRowCount((org.apache.calcite.rel.core.Combine) r,
mq);
} else if (r instanceof org.apache.calcite.rel.core.Exchange) {
return provider0.getRowCount((org.apache.calcite.rel.core.Exchange) r,
mq);
} else if (r instanceof org.apache.calcite.rel.core.Filter) {