This is an automated email from the ASF dual-hosted git repository.
mihaibudiu 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 4062e2b1c0 [CALCITE-7793] RelMdDistinctRowCount passes an Aggregate
predicate to the Aggregate's input without translating column references
4062e2b1c0 is described below
commit 4062e2b1c05088488dc98c749a1ae4951f14f355
Author: Mihai Budiu <[email protected]>
AuthorDate: Thu Sep 17 14:43:50 2026 -0700
[CALCITE-7793] RelMdDistinctRowCount passes an Aggregate predicate to the
Aggregate's input without translating column references
Signed-off-by: Mihai Budiu <[email protected]>
---
.../rel/metadata/RelMdDistinctRowCount.java | 15 +++++
.../org/apache/calcite/test/RelMetadataTest.java | 70 ++++++++++++++++++++++
core/src/test/resources/sql/sub-query.iq | 31 ++++++++++
3 files changed, 116 insertions(+)
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdDistinctRowCount.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdDistinctRowCount.java
index 7058dcbb4d..f70fa5dfef 100644
---
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdDistinctRowCount.java
+++
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdDistinctRowCount.java
@@ -191,6 +191,21 @@ protected RelMdDistinctRowCount() {}
RexNode childPreds =
RexUtil.composeConjunction(rexBuilder, pushable, true);
+ if (childPreds != null) {
+ // Convert aggregate-output references to the corresponding input fields.
+ final int[] adjustments = new int[rel.getGroupCount()];
+ int j = 0;
+ for (int i : rel.getGroupSet()) {
+ adjustments[j] = i - j;
+ j++;
+ }
+ childPreds =
+ childPreds.accept(
+ new RelOptUtil.RexInputConverter(rexBuilder,
+ rel.getRowType().getFieldList(),
+ rel.getInput().getRowType().getFieldList(), adjustments));
+ }
+
// set the bits as they correspond to the child input
ImmutableBitSet.Builder childKey = ImmutableBitSet.builder();
RelMdUtil.setAggChildKeys(groupKey, rel, childKey);
diff --git a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
index a1780aa319..b6625ab0b3 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
@@ -5414,6 +5414,76 @@ static <K, V> List<String> toSortedStringList(Map<K, V>
map) {
.assertThatDistinctRowCount(bitSetOf(0), is(1d));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7793">[CALCITE-7793]
+ * RelMdDistinctRowCount passes an Aggregate predicate to the Aggregate's
+ * input without translating column references</a>. */
+ @Test void testAggregateDistinctRowCountPredicateIsTranslated() {
+ // select id, sum(sal) from t where id > 1 group by id having id > 1
+ //
+ // LogicalFilter(condition=[>($0, 1)]) <- Aggregate output ordinals;
+ // LogicalAggregate(group=[{1}], sumsal=[SUM($0)])
+ // LogicalFilter(condition=[>($1, 1)]) <- Aggregate input ordinals;
+ // LogicalValues(tuples=[[{ 10, 1 }, { 20, 2 }, { 30, 3 }]])
+ // The HAVING has the same condition as the WHERE:
+ // Thus the Aggregate's output $0 is the same as its input $1:
+ // the two conditions restrict the key column in the same way.
+ //
+ // The plan is built by hand because SqlToRel produces a different plan.
+ final RelNode query = fixture()
+ .withRelFn(b -> {
+ b.values(new String[]{"sal", "id"}, 10, 1, 20, 2, 30, 3);
+ b.filter(b.greaterThan(b.field(1), b.literal(1)));
+ b.aggregate(b.groupKey(1), b.sum(false, "sumsal", b.field(0)));
+ return b.filter(b.greaterThan(b.field(0), b.literal(1))).build();
+ })
+ .toRel();
+
+ // The two plans compared below differ only by the HAVING.
+ final RelNode aggregate = query.getInput(0);
+ assertThat(aggregate, instanceOf(Aggregate.class));
+
+ // A HAVING that restricts nothing new does not change the estimated group
count.
+ final RelMetadataQuery mq = query.getCluster().getMetadataQuery();
+ assertThat(mq.getDistinctRowCount(query, bitSetOf(0), null),
+ is(mq.getDistinctRowCount(aggregate, bitSetOf(0), null)));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7793">[CALCITE-7793]
+ * RelMdDistinctRowCount passes an Aggregate predicate to the Aggregate's
+ * input without translating column references</a>. */
+ @Test void testAggregateDistinctRowCountStructPredicate() {
+ // select r, sum(sal) from
+ // (select sal, cast(row(id) as row(d integer)) as r from t)
+ // group by r
+ // having (r).d = 2
+ //
+ // LogicalFilter(condition=[=($0.d, 2)]) <- Aggregate output
ordinals;
+ // LogicalAggregate(group=[{1}], sumsal=[SUM($0)])
+ // LogicalProject(sal=[$0], r=[ROW($1)]) <- Aggregate input ordinals;
+ // LogicalValues(tuples=[[{ 10, 1 }, { 20, 2 }, { 30, 3 }]])
+ //
+ // The plan is built by hand because SqlToRel produces a different plan.
+ fixture()
+ .withRelFn(b -> {
+ b.values(new String[]{"sal", "id"}, 10, 1, 20, 2, 30, 3);
+ b.project(
+ ImmutableList.of(b.field(0),
+ b.getRexBuilder().makeCall(
+ b.getTypeFactory().builder()
+ .add("d", b.field(1).getType()).build(),
+ SqlStdOperatorTable.ROW, ImmutableList.of(b.field(1)))),
+ ImmutableList.of("sal", "r"));
+ b.aggregate(b.groupKey(1), b.sum(false, "sumsal", b.field(0)));
+ return b.filter(
+ b.equals(b.getRexBuilder().makeFieldAccess(b.field(0), 0),
+ b.literal(2))).build();
+ })
+ // Without the fix the next statement throws.
+ .assertThatDistinctRowCount(bitSetOf(0), notNullValue(Double.class));
+ }
+
@Test void testAggregateDistinctRowCountLosslessCast() {
final String sql =
"select name, sal, cast(sal as varchar(11)) "
diff --git a/core/src/test/resources/sql/sub-query.iq
b/core/src/test/resources/sql/sub-query.iq
index 4bc3fae31a..4a71d6a388 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -10200,3 +10200,34 @@ from (select array(select ROW(ROW(1, 2), 3) from
(values (0))) as a) as t;
(1 row)
!ok
+
+!set trimfields false
+
+# [CALCITE-7793] RelMdDistinctRowCount passes an Aggregate predicate to the
+# Aggregate's input without translating column references.
+# PostgreSQL 14.13 returns the same empty result for this query.
+with t(n, r) as (
+ values (1, cast(row(10) as row(d integer))),
+ (2, cast(row(20) as row(d integer)))
+)
+select e.n
+from t e
+where e.n > (
+ select avg(e2.n)
+ from t e2
+ where (e2.r).d = (e.r).d
+ and exists (
+ select 1
+ from (values (1, cast(row(10) as row(d integer)))) as b(k, br)
+ where b.k = e2.n
+ and (b.br).d = (e.r).d
+ )
+);
++---+
+| N |
++---+
++---+
+(0 rows)
+
+!ok
+!set trimfields true