This is an automated email from the ASF dual-hosted git repository.
zhenchen 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 6f15426e81 [CALCITE-5913] Support to get functional dependency
metadata in RelMetadataQuery
6f15426e81 is described below
commit 6f15426e81b5450d62f4c169ffac4cd8f27cf886
Author: Zhen Chen <[email protected]>
AuthorDate: Fri Aug 1 07:05:58 2025 +0800
[CALCITE-5913] Support to get functional dependency metadata in
RelMetadataQuery
---
.../calcite/rel/metadata/BuiltInMetadata.java | 23 +-
.../rel/metadata/DefaultRelMetadataProvider.java | 3 +-
.../rel/metadata/RelMdFunctionalDependency.java | 275 +++++++++++++++++++++
.../calcite/rel/metadata/RelMetadataQuery.java | 19 ++
.../org/apache/calcite/util/BuiltInMethod.java | 5 +-
.../apache/calcite/sql/test/SqlAdvisorTest.java | 1 +
.../org/apache/calcite/test/RelMetadataTest.java | 191 ++++++++++++++
.../test/catalog/MockCatalogReaderSimple.java | 15 ++
8 files changed, 529 insertions(+), 3 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java
b/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java
index 7fe071ba45..0f289aecbf 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java
@@ -901,10 +901,31 @@ default RelDataTypeFactory getTypeFactory() {
}
}
+ /** Metadata about the functional dependency of columns. */
+ public interface FunctionalDependency extends Metadata {
+ MetadataDef<FunctionalDependency> DEF =
+ MetadataDef.of(FunctionalDependency.class,
FunctionalDependency.Handler.class,
+ BuiltInMethod.FUNCTIONAL_DEPENDENCY.method);
+
+ /**
+ * Returns whether column is functionally dependent on column.
+ */
+ @Nullable Boolean determines(int key, int column);
+
+ /** Handler API. */
+ interface Handler extends MetadataHandler<FunctionalDependency> {
+ @Nullable Boolean determines(RelNode r, RelMetadataQuery mq, int key,
int column);
+
+ @Override default MetadataDef<FunctionalDependency> getDef() {
+ return DEF;
+ }
+ }
+ }
+
/** The built-in forms of metadata. */
interface All extends Selectivity, UniqueKeys, RowCount, DistinctRowCount,
PercentageOriginalRows, ColumnUniqueness, ColumnOrigin, Predicates,
Collation, Distribution, Size, Parallelism, Memory, AllPredicates,
- ExpressionLineage, TableReferences, NodeTypes {
+ ExpressionLineage, TableReferences, NodeTypes, FunctionalDependency {
}
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/DefaultRelMetadataProvider.java
b/core/src/main/java/org/apache/calcite/rel/metadata/DefaultRelMetadataProvider.java
index c874d36340..d47e5aac23 100644
---
a/core/src/main/java/org/apache/calcite/rel/metadata/DefaultRelMetadataProvider.java
+++
b/core/src/main/java/org/apache/calcite/rel/metadata/DefaultRelMetadataProvider.java
@@ -63,6 +63,7 @@ protected DefaultRelMetadataProvider() {
RelMdExplainVisibility.SOURCE,
RelMdPredicates.SOURCE,
RelMdAllPredicates.SOURCE,
- RelMdCollation.SOURCE));
+ RelMdCollation.SOURCE,
+ RelMdFunctionalDependency.SOURCE));
}
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdFunctionalDependency.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdFunctionalDependency.java
new file mode 100644
index 0000000000..02aaa51d68
--- /dev/null
+++
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdFunctionalDependency.java
@@ -0,0 +1,275 @@
+/*
+ * 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.metadata;
+
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelOptUtil;
+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.Calc;
+import org.apache.calcite.rel.core.Correlate;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.SetOp;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexProgram;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.List;
+
+/**
+ * Default implementation of
+ * {@link RelMetadataQuery#determines(RelNode, int, int)}
+ * for the standard logical algebra.
+ *
+ * <p>The goal of this provider is to determine whether
+ * key is functionally dependent on column.
+ *
+ * <p>If the functional dependency cannot be determined, we return false.
+ */
+public class RelMdFunctionalDependency
+ implements MetadataHandler<BuiltInMetadata.FunctionalDependency> {
+ public static final RelMetadataProvider SOURCE =
+ ReflectiveRelMetadataProvider.reflectiveSource(
+ new RelMdFunctionalDependency(),
BuiltInMetadata.FunctionalDependency.Handler.class);
+
+ //~ Constructors -----------------------------------------------------------
+
+ protected RelMdFunctionalDependency() {}
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override public MetadataDef<BuiltInMetadata.FunctionalDependency> getDef() {
+ return BuiltInMetadata.FunctionalDependency.DEF;
+ }
+
+ public @Nullable Boolean determines(RelNode rel, RelMetadataQuery mq,
+ int key, int column) {
+ return determinesImpl2(rel, mq, key, column);
+ }
+
+ public @Nullable Boolean determines(SetOp rel, RelMetadataQuery mq,
+ int key, int column) {
+ return determinesImpl2(rel, mq, key, column);
+ }
+
+ public @Nullable Boolean determines(Join rel, RelMetadataQuery mq,
+ int key, int column) {
+ return determinesImpl2(rel, mq, key, column);
+ }
+
+ public @Nullable Boolean determines(Correlate rel, RelMetadataQuery mq,
+ int key, int column) {
+ return determinesImpl2(rel, mq, key, column);
+ }
+
+ public @Nullable Boolean determines(Aggregate rel, RelMetadataQuery mq,
+ int key, int column) {
+ return determinesImpl(rel, mq, key, column);
+ }
+
+ public @Nullable Boolean determines(Calc rel, RelMetadataQuery mq,
+ int key, int column) {
+ return determinesImpl(rel, mq, key, column);
+ }
+
+ public @Nullable Boolean determines(Project rel, RelMetadataQuery mq,
+ int key, int column) {
+ return determinesImpl(rel, mq, key, column);
+ }
+
+ /**
+ * Checks if a column is functionally determined by a key column through
expression analysis.
+ *
+ * @param rel The input relation
+ * @param mq Metadata query instance
+ * @param key Index of the determinant expression
+ * @param column Index of the dependent expression
+ * @return TRUE if column is determined by key,
+ * FALSE if not determined,
+ * NULL if undetermined
+ */
+ private static @Nullable Boolean determinesImpl(RelNode rel,
RelMetadataQuery mq,
+ int key, int column) {
+ if (preCheck(rel, key, column)) {
+ return true;
+ }
+
+ ImmutableBitSet keyInputIndices = null;
+ ImmutableBitSet columnInputIndices = null;
+ if (rel instanceof Project || rel instanceof Calc) {
+ List<RexNode> exprs = null;
+ if (rel instanceof Project) {
+ Project project = (Project) rel;
+ exprs = project.getProjects();
+ } else {
+ Calc calc = (Calc) rel;
+ final RexProgram program = calc.getProgram();
+ exprs = program.expandList(program.getProjectList());
+ }
+
+ // TODO: Supports dependency analysis for all types of expressions
+ if (!(exprs.get(column) instanceof RexInputRef)) {
+ return false;
+ }
+
+ RexNode keyExpr = exprs.get(key);
+ RexNode columnExpr = exprs.get(column);
+
+ // Identical expressions imply functional dependency
+ if (keyExpr.equals(columnExpr)) {
+ return true;
+ }
+
+ keyInputIndices = extractDeterministicRefs(keyExpr);
+ columnInputIndices = extractDeterministicRefs(columnExpr);
+ } else if (rel instanceof Aggregate) {
+ Aggregate aggregate = (Aggregate) rel;
+
+ int groupByCnt = aggregate.getGroupCount();
+ if (key < groupByCnt && column >= groupByCnt) {
+ return false;
+ }
+
+ keyInputIndices = extractDeterministicRefs(aggregate, key);
+ columnInputIndices = extractDeterministicRefs(aggregate, column);
+ } else {
+ throw new UnsupportedOperationException("Unsupported RelNode type: "
+ + rel.getClass().getSimpleName());
+ }
+
+ // Early return if invalid cases
+ if (keyInputIndices.isEmpty()
+ || columnInputIndices.isEmpty()) {
+ return false;
+ }
+
+ // Currently only supports multiple (keyInputIndices) to one
(columnInputIndices)
+ // dependency detection
+ for (Integer keyRef : keyInputIndices) {
+ if (Boolean.FALSE.equals(
+ mq.determines(rel.getInput(0), keyRef,
+ columnInputIndices.nextSetBit(0)))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * determinesImpl2is similar to determinesImpl, but it doesn't need to
handle the
+ * mapping between output and input columns.
+ */
+ private static @Nullable Boolean determinesImpl2(RelNode rel,
RelMetadataQuery mq,
+ int key, int column) {
+ if (preCheck(rel, key, column)) {
+ return true;
+ }
+
+ if (rel instanceof TableScan) {
+ TableScan tableScan = (TableScan) rel;
+ RelOptTable table = tableScan.getTable();
+ List<ImmutableBitSet> keys = table.getKeys();
+ return keys != null
+ && keys.size() == 1
+ && keys.get(0).equals(ImmutableBitSet.of(column));
+ } else if (rel instanceof Join) {
+ Join join = (Join) rel;
+ // TODO Considering column mapping based on equality conditions in join
+ int leftFieldCnt = join.getLeft().getRowType().getFieldCount();
+ if (key < leftFieldCnt && column < leftFieldCnt) {
+ return mq.determines(join.getLeft(), key, column);
+ } else if (key >= leftFieldCnt && column >= leftFieldCnt) {
+ return mq.determines(join.getRight(), key - leftFieldCnt, column -
leftFieldCnt);
+ }
+ return false;
+ } else if (rel instanceof Correlate) {
+ // TODO Support Correlate.
+ return false;
+ } else if (rel instanceof SetOp) {
+ // TODO Support SetOp
+ return false;
+ }
+
+ return mq.determines(rel.getInput(0), key, column);
+ }
+
+ private static Boolean preCheck(RelNode rel, int key, int column) {
+ verifyIndex(rel, key, column);
+
+ // Equal index values indicate the same expression reference
+ if (key == column) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void verifyIndex(RelNode rel, int... indices) {
+ for (int index : indices) {
+ if (index < 0 || index >= rel.getRowType().getFieldCount()) {
+ throw new IndexOutOfBoundsException(
+ "Column index " + index + " is out of bounds. "
+ + "Valid range is [0, " + rel.getRowType().getFieldCount() +
")");
+ }
+ }
+ }
+
+ /**
+ * Extracts input indices referenced by an output column in an Aggregate.
+ * For group-by columns, returns the column index itself since they directly
+ * reference input columns. For aggregate function columns, returns the input
+ * column indices used by the aggregate call.
+ *
+ * @param aggregate The Aggregate relational expression to analyze
+ * @param index Index of the output column in the Aggregate (0-based)
+ * @return ImmutableBitSet of input column indices referenced by the output
column.
+ * For group-by columns, returns a singleton set of the column index.
+ * For aggregate columns, returns the argument indices of the
aggregate call.
+ */
+ private static ImmutableBitSet extractDeterministicRefs(Aggregate aggregate,
int index) {
+ int groupByCnt = aggregate.getGroupCount();
+ if (index < groupByCnt) {
+ return ImmutableBitSet.of(index);
+ }
+
+ List<AggregateCall> aggCalls = aggregate.getAggCallList();
+ AggregateCall call = aggCalls.get(index - groupByCnt);
+ return ImmutableBitSet.of(call.getArgList());
+ }
+
+ /**
+ * Extracts input indices referenced by a deterministic RexNode expression.
+ *
+ * @param rex The expression to analyze
+ * @return referenced input indices if deterministic
+ */
+ private static ImmutableBitSet extractDeterministicRefs(RexNode rex) {
+ if (rex instanceof RexCall && !RexUtil.isDeterministic(rex)) {
+ return ImmutableBitSet.of();
+ }
+ return RelOptUtil.InputFinder.bits(rex);
+ }
+}
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
index bf8e09b2b2..1caefacdf8 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
@@ -108,6 +108,7 @@ public class RelMetadataQuery extends RelMetadataQueryBase {
private BuiltInMetadata.Size.Handler sizeHandler;
private BuiltInMetadata.UniqueKeys.Handler uniqueKeysHandler;
private BuiltInMetadata.LowerBoundCost.Handler lowerBoundCostHandler;
+ private BuiltInMetadata.FunctionalDependency.Handler
functionalDependencyHandler;
/**
* Creates the instance with {@link JaninoRelMetadataProvider} instance
@@ -154,6 +155,8 @@ public RelMetadataQuery(MetadataHandlerProvider provider) {
this.sizeHandler = provider.handler(BuiltInMetadata.Size.Handler.class);
this.uniqueKeysHandler =
provider.handler(BuiltInMetadata.UniqueKeys.Handler.class);
this.lowerBoundCostHandler =
provider.handler(BuiltInMetadata.LowerBoundCost.Handler.class);
+ this.functionalDependencyHandler =
+ provider.handler(BuiltInMetadata.FunctionalDependency.Handler.class);
}
/** Creates and initializes the instance that will serve as a prototype for
@@ -187,6 +190,8 @@ private RelMetadataQuery(@SuppressWarnings("unused")
boolean dummy) {
this.sizeHandler = initialHandler(BuiltInMetadata.Size.Handler.class);
this.uniqueKeysHandler =
initialHandler(BuiltInMetadata.UniqueKeys.Handler.class);
this.lowerBoundCostHandler =
initialHandler(BuiltInMetadata.LowerBoundCost.Handler.class);
+ this.functionalDependencyHandler =
+ initialHandler(BuiltInMetadata.FunctionalDependency.Handler.class);
}
private RelMetadataQuery(
@@ -218,6 +223,7 @@ private RelMetadataQuery(
this.sizeHandler = prototype.sizeHandler;
this.uniqueKeysHandler = prototype.uniqueKeysHandler;
this.lowerBoundCostHandler = prototype.lowerBoundCostHandler;
+ this.functionalDependencyHandler = prototype.functionalDependencyHandler;
}
//~ Methods ----------------------------------------------------------------
@@ -985,4 +991,17 @@ public Boolean isVisibleInExplain(RelNode rel,
}
}
}
+
+ /**
+ * Determines whether key is functionally dependent on column.
+ */
+ public @Nullable Boolean determines(RelNode rel, int key, int column) {
+ for (;;) {
+ try {
+ return functionalDependencyHandler.determines(rel, this, key, column);
+ } catch (MetadataHandlerProvider.NoHandler e) {
+ functionalDependencyHandler =
revise(BuiltInMetadata.FunctionalDependency.Handler.class);
+ }
+ }
+ }
}
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index a76617c194..bd8ce7f2b7 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -63,6 +63,7 @@
import org.apache.calcite.rel.metadata.BuiltInMetadata.Distribution;
import org.apache.calcite.rel.metadata.BuiltInMetadata.ExplainVisibility;
import org.apache.calcite.rel.metadata.BuiltInMetadata.ExpressionLineage;
+import org.apache.calcite.rel.metadata.BuiltInMetadata.FunctionalDependency;
import org.apache.calcite.rel.metadata.BuiltInMetadata.LowerBoundCost;
import org.apache.calcite.rel.metadata.BuiltInMetadata.MaxRowCount;
import org.apache.calcite.rel.metadata.BuiltInMetadata.Measure;
@@ -967,7 +968,9 @@ public enum BuiltInMethod {
VARIANT_CAST(VariantValue.class, "cast", RuntimeTypeInformation.class),
TYPEOF(VariantValue.class, "getTypeString", VariantValue.class),
VARIANT_ITEM(SqlFunctions.class, "item", VariantValue.class, Object.class),
- VARIANTNULL(VariantNull.class, "getInstance");
+ VARIANTNULL(VariantNull.class, "getInstance"),
+ FUNCTIONAL_DEPENDENCY(FunctionalDependency.class, "determines",
+ int.class, int.class);
@SuppressWarnings("ImmutableEnumChecker")
public final Method method;
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
index c4da7b79bd..28e0420b1e 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
@@ -93,6 +93,7 @@ class SqlAdvisorTest extends SqlValidatorTestCase {
"TABLE(CATALOG.SALES.DEPT)",
"TABLE(CATALOG.SALES.DEPTNULLABLES)",
"TABLE(CATALOG.SALES.DEPT_SINGLE)",
+ "TABLE(CATALOG.SALES.DOUBLE_PK)",
"TABLE(CATALOG.SALES.DEPT_NESTED)",
"TABLE(CATALOG.SALES.DEPT_NESTED_EXPANDED)",
"TABLE(CATALOG.SALES.BONUS)",
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 5a5cfcf4f5..4e3733efa2 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
@@ -27,6 +27,7 @@
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptPredicateList;
import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.plan.hep.HepPlanner;
import org.apache.calcite.plan.hep.HepProgram;
@@ -294,6 +295,196 @@ final RelMetadataFixture sql(String sql) {
/ (DEPT_SIZE + EMP_SIZE)));
}
+ @Test void textFunctionDependencySimple() {
+ final String sql = "select empno, deptno, deptno + 1, rand() as r"
+ + " from emp where deptno < 20";
+
+ // Plan is
+ // LogicalProject(EMPNO=[$0], DEPTNO=[$7], EXPR$2=[+($7, 1)], R=[RAND()])
+ // LogicalFilter(condition=[<($7, 20)])
+ // LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ final RelNode relNode = sql(sql).toRel();
+ final RelMetadataQuery mq = relNode.getCluster().getMetadataQuery();
+ Project project = (Project) relNode;
+ int empNo = 0;
+ int deptNo = 1;
+ int deptNoPlus1 = 2;
+ int r = 3;
+
+ assertThat(mq.determines(project, deptNoPlus1, deptNo), is(Boolean.TRUE));
+ assertThat(mq.determines(project, deptNo, deptNoPlus1), is(Boolean.FALSE));
+ assertThat(mq.determines(project, deptNo, empNo), is(Boolean.TRUE));
+ assertThat(mq.determines(project, empNo, deptNoPlus1), is(Boolean.FALSE));
+ assertThat(mq.determines(project, deptNoPlus1, empNo), is(Boolean.TRUE));
+ assertThat(mq.determines(project, r, empNo), is(Boolean.FALSE));
+ }
+
+ @Test void textFunctionDependencyJoin() {
+ final String sql = "SELECT e.job, e.deptno, e.deptno + 1, d.d1, d.d2\n"
+ + "FROM emp e JOIN (SELECT deptno AS d1, deptno AS d2 FROM dept) d\n"
+ + "ON e.deptno = d.d1";
+
+ // Plan is
+ // LogicalProject(JOB=[$2], DEPTNO=[$7], EXPR$2=[+($7, 1)], D1=[$9],
D2=[$10])
+ // LogicalJoin(condition=[=($7, $9)], joinType=[inner])
+ // LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ // LogicalProject(D1=[$0], D2=[$0])
+ // LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ final RelNode relNode = sql(sql).toRel();
+ final RelMetadataQuery mq = relNode.getCluster().getMetadataQuery();
+ Project project = (Project) relNode;
+ int eJob = 0;
+ int eDeptNo = 1;
+ int eDeptNoPlus1 = 2;
+ int dD1 = 3;
+ int dD2 = 4;
+
+ assertThat(mq.determines(project, eJob, eDeptNo), is(Boolean.FALSE));
+ assertThat(mq.determines(project, eDeptNoPlus1, eDeptNo),
is(Boolean.TRUE));
+ assertThat(mq.determines(project, eDeptNoPlus1, dD1), is(Boolean.FALSE));
+ assertThat(mq.determines(project, dD2, dD1), is(Boolean.TRUE));
+ }
+
+ @Test void textFunctionDependencyDoulbePK() {
+ // Table double_pk with pk (id1, id2)
+ final String sql = "select id1, id2, sum(age) as z"
+ + " from double_pk group by id1, id2";
+
+ // Plan is
+ // LogicalAggregate(group=[{0, 1}], Z=[SUM($2)])
+ // LogicalProject(ID1=[$0], ID2=[$1], AGE=[$3])
+ // LogicalTableScan(table=[[CATALOG, SALES, DOUBLE_PK]])
+ final RelNode relNode = sql(sql).toRel();
+ System.out.println(RelOptUtil.toString(relNode));
+ final RelMetadataQuery mq = relNode.getCluster().getMetadataQuery();
+ Aggregate aggregate = (Aggregate) relNode;
+ int id1 = 0;
+ int id2 = 1;
+ int z = 2;
+
+ assertThat(mq.determines(aggregate, id2, id1), is(Boolean.FALSE));
+ assertThat(mq.determines(aggregate, id1, id2), is(Boolean.FALSE));
+ assertThat(mq.determines(aggregate, z, id1), is(Boolean.FALSE));
+ assertThat(mq.determines(aggregate, z, id2), is(Boolean.FALSE));
+ }
+
+ @Test void testFunctionDependencyComplex() {
+ final String sql = "SELECT deptno, sal1Sum, sal2Sum\n"
+ + "FROM (\n"
+ + " SELECT deptno,"
+ + " SUM(sal1) AS sal1Sum,"
+ + " SUM(sal2) AS sal2Sum,"
+ + " job\n"
+ + " FROM (\n"
+ + " SELECT deptno,"
+ + " sal AS sal1,"
+ + " sal AS sal2,"
+ + " job\n"
+ + " FROM emp\n"
+ + " ) t\n"
+ + " GROUP BY deptno, job\n"
+ + ") t2\n"
+ + "ORDER BY sal1Sum, job, sal2Sum + sal1Sum + 1";
+
+ // Plan is
+ // LogicalSort(sort0=[$1], sort1=[$3], sort2=[$4], dir0=[ASC], dir1=[ASC],
dir2=[ASC])
+ // LogicalProject(DEPTNO=[$0], SAL1SUM=[$2], SAL2SUM=[$3], JOB=[$1],
+ // EXPR$4=[+(+($3, $2), 1)])
+ // LogicalAggregate(group=[{0, 1}], SAL1SUM=[SUM($2)],
SAL2SUM=[SUM($3)])
+ // LogicalProject(DEPTNO=[$7], JOB=[$2], SAL1=[$5], SAL2=[$5])
+ // LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+
+ final RelNode relNode = sql(sql).toRel();
+ final RelMetadataQuery mq = relNode.getCluster().getMetadataQuery();
+
+ // check sort
+ final Sort sort = (Sort) relNode;
+ List<RelFieldCollation> collations =
sort.getCollation().getFieldCollations();
+ int sal1Sum = collations.get(0).getFieldIndex();
+ int job = collations.get(1).getFieldIndex();
+ int sal2SumPlusSal1SumPlus1 = collations.get(2).getFieldIndex();
+
+ assertThat(mq.determines(sort, sal1Sum, sal1Sum), is(Boolean.TRUE));
+ assertThat(mq.determines(sort, job, sal1Sum), is(Boolean.FALSE));
+ assertThat(mq.determines(sort, sal2SumPlusSal1SumPlus1, sal1Sum),
is(Boolean.TRUE));
+ assertThat(mq.determines(sort, sal1Sum, sal2SumPlusSal1SumPlus1),
is(Boolean.FALSE));
+ assertThat(mq.determines(sort, sal2SumPlusSal1SumPlus1,
sal2SumPlusSal1SumPlus1),
+ is(Boolean.TRUE));
+ assertThat(mq.determines(sort, sal2SumPlusSal1SumPlus1, job),
is(Boolean.FALSE));
+
+ // check aggregate
+ Aggregate aggregate = (Aggregate) relNode.getInput(0).getInput(0);
+ int deptNoGroupByKey = 0;
+ int jobGroupByKey = 1;
+ int sal1SumAggCall = 2;
+ int sal2SumAggCall = 3;
+
+ assertThat(mq.determines(aggregate, jobGroupByKey, deptNoGroupByKey),
is(Boolean.FALSE));
+ assertThat(mq.determines(aggregate, jobGroupByKey, sal1SumAggCall),
is(Boolean.FALSE));
+ assertThat(mq.determines(aggregate, sal2SumAggCall, sal1SumAggCall),
is(Boolean.TRUE));
+ }
+
+ @Test void testFunctionDependencyCalc() {
+ final String sql = "SELECT deptno, sal1Sum, sal2Sum\n"
+ + "FROM (\n"
+ + " SELECT deptno,"
+ + " SUM(sal1) AS sal1Sum,"
+ + " SUM(sal2) AS sal2Sum,"
+ + " job\n"
+ + " FROM (\n"
+ + " SELECT deptno,"
+ + " sal AS sal1,"
+ + " sal AS sal2,"
+ + " job\n"
+ + " FROM emp\n"
+ + " ) t\n"
+ + " GROUP BY deptno, job\n"
+ + ") t2\n"
+ + "ORDER BY sal1Sum, job, sal2Sum + sal1Sum + 1";
+
+ // Plan is
+ // LogicalSort(sort0=[$1], sort1=[$3], sort2=[$4], dir0=[ASC], dir1=[ASC],
dir2=[ASC])
+ // LogicalCalc(expr#0..3=[{inputs}], expr#4=[+($t3, $t2)], expr#5=[1],
expr#6=[+($t4, $t5)],
+ // DEPTNO=[$t0], SAL1SUM=[$t2], SAL2SUM=[$t3], JOB=[$t1],
EXPR$4=[$t6])
+ // LogicalAggregate(group=[{0, 1}], SAL1SUM=[SUM($2)],
SAL2SUM=[SUM($3)])
+ // LogicalCalc(expr#0..8=[{inputs}], DEPTNO=[$t7], JOB=[$t2],
SAL1=[$t5], SAL2=[$t5])
+ // LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+
+ RelNode relNode = sql(sql).toRel();
+ final HepProgram program = new HepProgramBuilder().
+ addRuleInstance(CoreRules.PROJECT_TO_CALC).build();
+ final HepPlanner planner = new HepPlanner(program);
+ planner.setRoot(relNode);
+ relNode = planner.findBestExp();
+ System.out.println(RelOptUtil.toString(relNode));
+ final RelMetadataQuery mq = relNode.getCluster().getMetadataQuery();
+
+ // check sort
+ final Sort sort = (Sort) relNode;
+ List<RelFieldCollation> collations =
sort.getCollation().getFieldCollations();
+ int sal1Sum = collations.get(0).getFieldIndex();
+ int job = collations.get(1).getFieldIndex();
+ int sal2SumPlusSal1SumPlus1 = collations.get(2).getFieldIndex();
+
+ assertThat(mq.determines(sort, sal1Sum, sal1Sum), is(Boolean.TRUE));
+ assertThat(mq.determines(sort, job, sal1Sum), is(Boolean.FALSE));
+ assertThat(mq.determines(sort, sal2SumPlusSal1SumPlus1, sal1Sum),
is(Boolean.TRUE));
+ assertThat(mq.determines(sort, sal1Sum, sal2SumPlusSal1SumPlus1),
is(Boolean.FALSE));
+ assertThat(mq.determines(sort, sal2SumPlusSal1SumPlus1,
sal2SumPlusSal1SumPlus1),
+ is(Boolean.TRUE));
+ assertThat(mq.determines(sort, sal2SumPlusSal1SumPlus1, job),
is(Boolean.FALSE));
+ // check aggregate
+ Aggregate aggregate = (Aggregate) relNode.getInput(0).getInput(0);
+ int deptNoGroupByKey = 0;
+ int jobGroupByKey = 1;
+ int sal1SumAggCall = 2;
+ int sal2SumAggCall = 3;
+
+ assertThat(mq.determines(aggregate, jobGroupByKey, deptNoGroupByKey),
is(Boolean.FALSE));
+ assertThat(mq.determines(aggregate, jobGroupByKey, sal1SumAggCall),
is(Boolean.FALSE));
+ assertThat(mq.determines(aggregate, sal2SumAggCall, sal1SumAggCall),
is(Boolean.TRUE));
+ }
+
// ----------------------------------------------------------------------
// Tests for getColumnOrigins
// ----------------------------------------------------------------------
diff --git
a/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java
b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java
index e34173e250..8723266ea0 100644
---
a/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java
+++
b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java
@@ -28,6 +28,7 @@
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql2rel.InitializerExpressionFactory;
import org.apache.calcite.sql2rel.NullInitializerExpressionFactory;
+import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.ImmutableIntList;
import org.apache.calcite.util.Litmus;
import org.apache.calcite.util.Util;
@@ -451,6 +452,17 @@ private void registerStructTypeTables(Fixture fixture) {
registerTable(struct10View);
}
+ private void registerTableDoublePK(MockSchema salesSchema, Fixture fixture) {
+ final MockTable doublePK =
+ MockTable.create(this, salesSchema, "DOUBLE_PK", false, 14);
+ doublePK.addColumn("ID1", fixture.intType, true);
+ doublePK.addColumn("ID2", fixture.varchar20Type);
+ doublePK.addColumn("NAME", fixture.varchar20Type);
+ doublePK.addColumn("AGE", fixture.intType);
+ doublePK.keyList.add(ImmutableBitSet.of(0, 1));
+ registerTable(doublePK);
+ }
+
@Override public MockCatalogReaderSimple init() {
final Fixture fixture = new Fixture(typeFactory);
@@ -552,6 +564,9 @@ private void registerStructTypeTables(Fixture fixture) {
registerStructTypeTables(fixture);
registerTablesWithRollUp(salesSchema, fixture);
+
+ registerTableDoublePK(salesSchema, fixture);
+
return this;
}
}