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;
   }
 }

Reply via email to