This is an automated email from the ASF dual-hosted git repository.

cwylie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 6d4c8abe312 refactor: add VirtualColumns support for getting 
VirtualColumn dependency tree structure (#19281)
6d4c8abe312 is described below

commit 6d4c8abe3121628438d945b2439888f91783cc22
Author: Clint Wylie <[email protected]>
AuthorDate: Tue Apr 14 16:36:44 2026 -0700

    refactor: add VirtualColumns support for getting VirtualColumn dependency 
tree structure (#19281)
    
    changes:
    * add class `VirtualColumns.Node` capturing a `VirtualColumn` and its 
transitive `VirtualColumn` dependencies
    * add `VirtualColumns.getNode` method which takes a virtual column name and 
returns a `VirtualColumns.Node` from a memoized map supplier
    * modified `VirtualColumns.findEquivalent` to take a `VirtualColumns.Node` 
as an argument, replacing the previous two-arg `findEquivalent(VirtualColumns, 
VirtualColumn)`, which iterates `node.getDependencies()` directly instead of 
calling `virtualColumn.requiredColumns()` + `virtualColumns.getVirtualColumn()` 
+ null-checking, which simplifies both the implementation and all call sites
    * removed `ShardVirtualColumnCacheEntry` from `FilterSegmentPruner`, the 
shard equivalence cache now uses `VirtualColumns.Node` as the key instead of 
allocating a new tree-structure per call
    * `Projections` updated to use `getNode()` + `findEquivalent(Node)`
    * `SegmentGenerationStageSpec` method 
`addRequiredVirtualColumns(VirtualColumns, VirtualColumn, Map)` replaced by 
`addRequiredFromNode(Node, Map)` which walks `getDependencies()` of the node 
rather than manually calling `requiredColumns()` + `getVirtualColumn`
---
 .../destination/SegmentGenerationStageSpec.java    |  36 +++---
 .../druid/query/filter/FilterSegmentPruner.java    |  58 ++--------
 .../org/apache/druid/segment/VirtualColumn.java    |   2 +-
 .../org/apache/druid/segment/VirtualColumns.java   | 126 ++++++++++++++++++---
 .../druid/segment/projections/Projections.java     |   9 +-
 .../apache/druid/timeline/partition/ShardSpec.java |   2 +-
 .../apache/druid/segment/VirtualColumnsTest.java   | 113 ++++++++++++++++--
 7 files changed, 244 insertions(+), 102 deletions(-)

diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/SegmentGenerationStageSpec.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/SegmentGenerationStageSpec.java
index 4d00d19f3fc..d760d262a51 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/SegmentGenerationStageSpec.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/SegmentGenerationStageSpec.java
@@ -138,26 +138,26 @@ public class SegmentGenerationStageSpec implements 
TerminalStageSpec
   {
     final Map<String, VirtualColumn> clusterByVirtualColumns = new 
LinkedHashMap<>();
     if (query instanceof GroupByQuery groupByQuery) {
-      final Map<String, VirtualColumn> outputToVc = new LinkedHashMap<>();
+      final Map<String, VirtualColumns.Node> outputToVc = new 
LinkedHashMap<>();
       for (DimensionSpec spec : groupByQuery.getDimensions()) {
-        final VirtualColumn vc = 
groupByQuery.getVirtualColumns().getVirtualColumn(spec.getDimension());
+        final VirtualColumns.Node vc = 
groupByQuery.getVirtualColumns().getNode(spec.getDimension());
         if (vc != null) {
           outputToVc.put(spec.getOutputName(), vc);
         }
       }
       for (KeyColumn column : queryClusterBy.getColumns()) {
-        final VirtualColumn vc = outputToVc.get(column.columnName());
+        final VirtualColumns.Node vc = outputToVc.get(column.columnName());
         if (vc != null) {
-          clusterByVirtualColumns.put(column.columnName(), vc);
-          addRequiredVirtualColumns(groupByQuery.getVirtualColumns(), vc, 
clusterByVirtualColumns);
+          clusterByVirtualColumns.put(column.columnName(), 
vc.getVirtualColumn());
+          addRequiredFromNode(vc, clusterByVirtualColumns);
         }
       }
     } else if (query instanceof ScanQuery scanQuery) {
       for (KeyColumn column : queryClusterBy.getColumns()) {
-        final VirtualColumn vc = 
scanQuery.getVirtualColumns().getVirtualColumn(column.columnName());
+        final VirtualColumns.Node vc = 
scanQuery.getVirtualColumns().getNode(column.columnName());
         if (vc != null) {
-          clusterByVirtualColumns.put(column.columnName(), vc);
-          addRequiredVirtualColumns(scanQuery.getVirtualColumns(), vc, 
clusterByVirtualColumns);
+          clusterByVirtualColumns.put(column.columnName(), 
vc.getVirtualColumn());
+          addRequiredFromNode(vc, clusterByVirtualColumns);
         }
       }
     }
@@ -165,24 +165,16 @@ public class SegmentGenerationStageSpec implements 
TerminalStageSpec
   }
 
   /**
-   * Recursively adds any {@link VirtualColumn#requiredColumns()} which are 
also virtual columns. This handles cases
-   * where a cluster-by virtual column depends on other virtual columns, such 
as when clustering by something like
+   * Adds all transitive virtual column dependencies of {@code vc} into {@code 
collected}. This handles cases where a
+   * cluster-by virtual column depends on other virtual columns, such as when 
clustering by something like
    * {@code LOWER(JSON_VALUE(obj, '$.path'))} which creates an 
ExpressionVirtualColumn that references a
    * NestedFieldVirtualColumn.
    */
-  private static void addRequiredVirtualColumns(
-      VirtualColumns allVirtualColumns,
-      VirtualColumn vc,
-      Map<String, VirtualColumn> collected
-  )
+  private static void addRequiredFromNode(VirtualColumns.Node node, 
Map<String, VirtualColumn> collected)
   {
-    for (String requiredColumn : vc.requiredColumns()) {
-      if (!collected.containsKey(requiredColumn)) {
-        final VirtualColumn requiredVc = 
allVirtualColumns.getVirtualColumn(requiredColumn);
-        if (requiredVc != null) {
-          collected.put(requiredColumn, requiredVc);
-          addRequiredVirtualColumns(allVirtualColumns, requiredVc, collected);
-        }
+    for (VirtualColumns.Node dep : node.getDependencies()) {
+      if (collected.putIfAbsent(dep.getVirtualColumn().getOutputName(), 
dep.getVirtualColumn()) == null) {
+        addRequiredFromNode(dep, collected);
       }
     }
   }
diff --git 
a/processing/src/main/java/org/apache/druid/query/filter/FilterSegmentPruner.java
 
b/processing/src/main/java/org/apache/druid/query/filter/FilterSegmentPruner.java
index 32854ac0d32..6ef7565302b 100644
--- 
a/processing/src/main/java/org/apache/druid/query/filter/FilterSegmentPruner.java
+++ 
b/processing/src/main/java/org/apache/druid/query/filter/FilterSegmentPruner.java
@@ -47,7 +47,7 @@ public class FilterSegmentPruner implements SegmentPruner
   private final Set<String> filterFields;
   private final VirtualColumns virtualColumns;
   private final Map<String, Optional<RangeSet<String>>> rangeCache;
-  private final Map<ShardVirtualColumnCacheEntry, Optional<VirtualColumn>> 
shardEquivalenceCache;
+  private final Map<VirtualColumns.Node, Optional<VirtualColumn>> 
shardEquivalenceCache;
 
   public FilterSegmentPruner(
       DimFilter filter,
@@ -79,12 +79,9 @@ public class FilterSegmentPruner implements SegmentPruner
       final Map<String, RangeSet<String>> filterDomain = new HashMap<>();
       final List<String> dimensions = shard.getDomainDimensions();
       for (String dimension : dimensions) {
-        final VirtualColumn shardVirtualColumn = 
shard.getDomainVirtualColumns().getVirtualColumn(dimension);
-        if (shardVirtualColumn != null) {
-          final VirtualColumn queryEquivalent = getQueryEquivalent(
-              shard.getDomainVirtualColumns(),
-              shardVirtualColumn
-          );
+        final VirtualColumns.Node shardNode = 
shard.getDomainVirtualColumns().getNode(dimension);
+        if (shardNode != null) {
+          final VirtualColumn queryEquivalent = getQueryEquivalent(shardNode);
           if (queryEquivalent != null) {
             if (filterFields == null || 
filterFields.contains(queryEquivalent.getOutputName())) {
               final Optional<RangeSet<String>> optFilterRangeSet = rangeCache
@@ -93,7 +90,7 @@ public class FilterSegmentPruner implements SegmentPruner
                       d -> Optional.ofNullable(filter.getDimensionRangeSet(d))
                   );
               optFilterRangeSet.ifPresent(stringRangeSet -> filterDomain.put(
-                  shardVirtualColumn.getOutputName(),
+                  shardNode.getVirtualColumn().getOutputName(),
                   stringRangeSet
               ));
             }
@@ -168,51 +165,12 @@ public class FilterSegmentPruner implements SegmentPruner
   }
 
   @Nullable
-  private VirtualColumn getQueryEquivalent(VirtualColumns shardVirtualColumns, 
VirtualColumn shardVirtualColumn)
+  private VirtualColumn getQueryEquivalent(VirtualColumns.Node node)
   {
     final Optional<VirtualColumn> cached = 
shardEquivalenceCache.computeIfAbsent(
-        new ShardVirtualColumnCacheEntry(shardVirtualColumn, 
shardVirtualColumns),
-        virtualColumn -> 
Optional.ofNullable(virtualColumns.findEquivalent(shardVirtualColumns, 
virtualColumn.shardVirtualColumn))
+        node,
+        n -> Optional.ofNullable(virtualColumns.findEquivalent(n))
     );
     return cached.orElse(null);
   }
-
-  /**
-   * Structure to preserve the VirtualColumn 'tree' to use as a cache key so 
that we can distinguish otherwise
-   * identical {@link VirtualColumn} that depend on other virtual columns that 
have the same name but are different
-   */
-  private static final class ShardVirtualColumnCacheEntry
-  {
-    private final VirtualColumn shardVirtualColumn;
-    private final List<ShardVirtualColumnCacheEntry> dependents;
-
-    public ShardVirtualColumnCacheEntry(VirtualColumn shardVirtualColumn, 
VirtualColumns shardVirtualColumns)
-    {
-      this.shardVirtualColumn = shardVirtualColumn;
-      this.dependents = new ArrayList<>();
-      for (String required : shardVirtualColumn.requiredColumns()) {
-        final VirtualColumn dependent = 
shardVirtualColumns.getVirtualColumn(required);
-        if (dependent != null) {
-          dependents.add(new ShardVirtualColumnCacheEntry(dependent, 
shardVirtualColumns));
-        }
-      }
-    }
-
-    @Override
-    public boolean equals(Object o)
-    {
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      ShardVirtualColumnCacheEntry that = (ShardVirtualColumnCacheEntry) o;
-      return Objects.equals(shardVirtualColumn, that.shardVirtualColumn) &&
-             Objects.equals(dependents, that.dependents);
-    }
-
-    @Override
-    public int hashCode()
-    {
-      return Objects.hash(shardVirtualColumn, dependents);
-    }
-  }
 }
diff --git 
a/processing/src/main/java/org/apache/druid/segment/VirtualColumn.java 
b/processing/src/main/java/org/apache/druid/segment/VirtualColumn.java
index 5768ce4c04b..3c2fdcf5f69 100644
--- a/processing/src/main/java/org/apache/druid/segment/VirtualColumn.java
+++ b/processing/src/main/java/org/apache/druid/segment/VirtualColumn.java
@@ -540,7 +540,7 @@ public interface VirtualColumn extends Cacheable
    * virtual column, regardless of the output name. If this method returns 
null, it does not participate in equivalence
    * comparisons.
    *
-   * @see VirtualColumns#findEquivalent(VirtualColumns, VirtualColumn)
+   * @see VirtualColumns#findEquivalent(VirtualColumns.Node)
    */
   @Nullable
   default EquivalenceKey getEquivalanceKey()
diff --git 
a/processing/src/main/java/org/apache/druid/segment/VirtualColumns.java 
b/processing/src/main/java/org/apache/druid/segment/VirtualColumns.java
index 089931ac792..48d1decb559 100644
--- a/processing/src/main/java/org/apache/druid/segment/VirtualColumns.java
+++ b/processing/src/main/java/org/apache/druid/segment/VirtualColumns.java
@@ -57,6 +57,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -138,6 +139,8 @@ public class VirtualColumns implements Cacheable
   private final List<String> virtualColumnNames;
   // For equivalence
   private final Supplier<Map<VirtualColumn.EquivalenceKey, VirtualColumn>> 
equivalence;
+  // For getNode
+  private final Supplier<Map<String, Node>> dependencyNodes;
 
   // For getVirtualColumn:
   private final Map<String, VirtualColumn> withDotSupport;
@@ -170,6 +173,31 @@ public class VirtualColumns implements Cacheable
       }
       return equiv;
     });
+    this.dependencyNodes = Suppliers.memoize(() -> {
+      final Map<String, Node> nodes = 
Maps.newHashMapWithExpectedSize(virtualColumns.size());
+      for (VirtualColumn vc : virtualColumns) {
+        buildNode(vc, nodes);
+      }
+      return nodes;
+    });
+  }
+
+  private Node buildNode(VirtualColumn vc, Map<String, Node> nodes)
+  {
+    final Node existing = nodes.get(vc.getOutputName());
+    if (existing != null) {
+      return existing;
+    }
+    final List<Node> deps = new ArrayList<>();
+    for (String required : vc.requiredColumns()) {
+      final VirtualColumn dep = getVirtualColumn(required);
+      if (dep != null) {
+        deps.add(buildNode(dep, nodes));
+      }
+    }
+    final Node node = new Node(vc, deps.isEmpty() ? List.of() : 
List.copyOf(deps));
+    nodes.put(vc.getOutputName(), node);
+    return node;
   }
 
   /**
@@ -199,34 +227,48 @@ public class VirtualColumns implements Cacheable
   }
 
   /**
-   * Check if {@link #virtualColumns} contains a virtual column which is 
equivalent to some other virtual column,
-   * ignoring output name, returning it if it exists or null if there is no 
equivalent virtual column.
+   * Returns the {@link Node} for the given virtual column name, providing 
access to the virtual
+   * column and all of its transitive virtual column dependencies within this 
instance. Returns null if the
+   * column is not a virtual column in this instance.
+   */
+  @Nullable
+  public Node getNode(String columnName)
+  {
+    final VirtualColumn vc = getVirtualColumn(columnName);
+    if (vc == null) {
+      return null;
+    }
+    return dependencyNodes.get().get(vc.getOutputName());
+  }
+
+  /**
+   * Check if {@link #virtualColumns} contains a virtual column which is 
equivalent to the virtual column in the
+   * supplied {@link Node}, ignoring output name, returning it if it exists or 
null if there is no
+   * equivalent virtual column.
    * <p>
-   * If the other virtual column depends on other virtual columns (from the 
supplied {@link VirtualColumns}), this
-   * method will attempt to locate equivalent entries in {@link 
#virtualColumns} to build a map of equivalent output
-   * names. Then, we rewrite the inputs of the other virtual column using
+   * If the virtual column has virtual column dependencies (indicated by 
non-empty {@link Node#getDependencies()}),
+   * this method will attempt to locate equivalent entries in {@link 
#virtualColumns} to build a map of equivalent
+   * output names. Then, we rewrite the inputs of the other virtual column 
using
    * {@link VirtualColumn#rewriteRequiredColumns(Map)} so that differently 
named inputs are normalized prior to testing
    * for equivalence.
    */
   @Nullable
-  public VirtualColumn findEquivalent(VirtualColumns otherVirtualColumns, 
VirtualColumn otherVirtualColumn)
+  public VirtualColumn findEquivalent(Node otherNode)
   {
     // check to see if the virtual column refers to other virtual columns to 
see if we need to normalize it
     // by rewriting its inputs first to refer to the equivalent virtual columns
     final Map<String, String> equivalenceRewriteMap = new HashMap<>();
-    for (String column : otherVirtualColumn.requiredColumns()) {
-      final VirtualColumn dependent = 
otherVirtualColumns.getVirtualColumn(column);
-      if (dependent != null) {
-        final VirtualColumn equivalentDependent = 
findEquivalent(otherVirtualColumns, dependent);
-        if (equivalentDependent != null) {
-          equivalenceRewriteMap.put(dependent.getOutputName(), 
equivalentDependent.getOutputName());
-        } else {
-          // missing an equivalent dependent, that means we cannot be 
equivalent so just bail early
-          return null;
-        }
+    for (Node dep : otherNode.getDependencies()) {
+      final VirtualColumn equivalentDependent = findEquivalent(dep);
+      if (equivalentDependent != null) {
+        equivalenceRewriteMap.put(dep.getVirtualColumn().getOutputName(), 
equivalentDependent.getOutputName());
+      } else {
+        // missing an equivalent dependent, that means we cannot be equivalent 
so just bail early
+        return null;
       }
     }
 
+    final VirtualColumn otherVirtualColumn = otherNode.getVirtualColumn();
     if (!equivalenceRewriteMap.isEmpty() && 
!otherVirtualColumn.supportsRequiredRewrite()) {
       // cannot safely check for equivalence if the rewrite map is not empty 
and rewrites are not supported
       return null;
@@ -559,6 +601,58 @@ public class VirtualColumns implements Cacheable
     return virtualColumns.toString();
   }
 
+  /**
+   * A node in the virtual column dependency tree, capturing a {@link 
VirtualColumn} and all of its transitive
+   * virtual column dependencies within a {@link VirtualColumns} instance. 
Leaf virtual columns (those whose
+   * {@link VirtualColumn#requiredColumns()} contain no other virtual columns) 
have an empty {@link #getDependencies()}
+   * list.
+   *
+   * @see VirtualColumns#getNode(String)
+   */
+  public static final class Node
+  {
+    private final VirtualColumn virtualColumn;
+    private final List<Node> dependencies;
+
+    private Node(VirtualColumn virtualColumn, List<Node> dependencies)
+    {
+      this.virtualColumn = virtualColumn;
+      this.dependencies = dependencies;
+    }
+
+    public VirtualColumn getVirtualColumn()
+    {
+      return virtualColumn;
+    }
+
+    /**
+     * The virtual column nodes that this virtual column directly depends on, 
containing only dependencies
+     * that are themselves virtual columns. An empty list does not imply 
{@link VirtualColumn#requiredColumns()}
+     * is empty, as physical column inputs are not represented here.
+     */
+    public List<Node> getDependencies()
+    {
+      return dependencies;
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      Node that = (Node) o;
+      return Objects.equals(virtualColumn, that.virtualColumn) &&
+             Objects.equals(dependencies, that.dependencies);
+    }
+
+    @Override
+    public int hashCode()
+    {
+      return Objects.hash(virtualColumn, dependencies);
+    }
+  }
+
   /**
    * {@link JsonInclude} filter for {@code getVirtualColumns()}.
    *
diff --git 
a/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
 
b/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
index 0609ad926db..0415d540e24 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
@@ -35,6 +35,7 @@ import org.apache.druid.segment.AggregateProjectionMetadata;
 import org.apache.druid.segment.CursorBuildSpec;
 import org.apache.druid.segment.CursorHolder;
 import org.apache.druid.segment.VirtualColumn;
+import org.apache.druid.segment.VirtualColumns;
 import org.apache.druid.segment.column.ColumnHolder;
 import org.apache.druid.utils.CollectionUtils;
 import org.joda.time.DateTime;
@@ -405,10 +406,10 @@ public class Projections
   )
   {
     // check to see if we have an equivalent virtual column defined in the 
projection, if so we can
-    final VirtualColumn projectionEquivalent = 
projection.getVirtualColumns().findEquivalent(
-        queryCursorBuildSpec.getVirtualColumns(),
-        queryVirtualColumn
-    );
+    final VirtualColumns.Node queryNode =
+        
queryCursorBuildSpec.getVirtualColumns().getNode(queryVirtualColumn.getOutputName());
+    final VirtualColumn projectionEquivalent =
+        queryNode != null ? 
projection.getVirtualColumns().findEquivalent(queryNode) : null;
     if (projectionEquivalent != null) {
       final String remapColumnName;
       if (Objects.equals(projectionEquivalent.getOutputName(), 
projection.getTimeColumnName())) {
diff --git 
a/processing/src/main/java/org/apache/druid/timeline/partition/ShardSpec.java 
b/processing/src/main/java/org/apache/druid/timeline/partition/ShardSpec.java
index e881b32484f..ab70cf9f39d 100644
--- 
a/processing/src/main/java/org/apache/druid/timeline/partition/ShardSpec.java
+++ 
b/processing/src/main/java/org/apache/druid/timeline/partition/ShardSpec.java
@@ -150,7 +150,7 @@ public interface ShardSpec
   /**
    * If any of the columns in {@link #getDomainDimensions()} was computed with 
an expression and was not stored, the
    * {@link VirtualColumn} which computes it is stored here. This allows 
matching ranges even when the value is not
-   * stored in the shard so long as {@link 
VirtualColumns#findEquivalent(VirtualColumns, VirtualColumn)} exists.
+   * stored in the shard so long as {@link 
VirtualColumns#findEquivalent(VirtualColumns.Node)} exists.
    *
    * @return {@link VirtualColumns} associated with columns listed in {@link 
#getDomainDimensions()}.
    */
diff --git 
a/processing/src/test/java/org/apache/druid/segment/VirtualColumnsTest.java 
b/processing/src/test/java/org/apache/druid/segment/VirtualColumnsTest.java
index b6323f7de2f..8cff8c15dfb 100644
--- a/processing/src/test/java/org/apache/druid/segment/VirtualColumnsTest.java
+++ b/processing/src/test/java/org/apache/druid/segment/VirtualColumnsTest.java
@@ -458,6 +458,7 @@ public class VirtualColumnsTest extends 
InitializedNullHandlingTest
                   .withIgnoredFields(
                       "virtualColumnNames",
                       "equivalence",
+                      "dependencyNodes",
                       "withDotSupport",
                       "withoutDotSupport",
                       "hasNoDotColumns"
@@ -465,6 +466,102 @@ public class VirtualColumnsTest extends 
InitializedNullHandlingTest
                   .verify();
   }
 
+  @Test
+  public void testGetNodeNullForNonVirtualColumn()
+  {
+    final VirtualColumns virtualColumns = makeVirtualColumns();
+    Assert.assertNull(virtualColumns.getNode(REAL_COLUMN_NAME));
+    Assert.assertNull(virtualColumns.getNode("doesNotExist"));
+    Assert.assertNull(VirtualColumns.EMPTY.getNode("anything"));
+  }
+
+  @Test
+  public void testGetNodeLeaf()
+  {
+    // "expr" is "1", a constant expression with no required columns, so no VC 
deps
+    final VirtualColumns virtualColumns = makeVirtualColumns();
+    final VirtualColumns.Node node = virtualColumns.getNode("expr");
+    Assert.assertNotNull(node);
+    Assert.assertEquals(virtualColumns.getVirtualColumn("expr"), 
node.getVirtualColumn());
+    Assert.assertTrue(node.getDependencies().isEmpty());
+  }
+
+  @Test
+  public void testGetNodeWithVcDependency()
+  {
+    // "expr2" is "expr2i + real_column", depends on expr2i (a VC) and 
REAL_COLUMN_NAME (physical)
+    final VirtualColumns virtualColumns = makeVirtualColumns();
+    final VirtualColumns.Node node = virtualColumns.getNode("expr2");
+    Assert.assertNotNull(node);
+    Assert.assertEquals(virtualColumns.getVirtualColumn("expr2"), 
node.getVirtualColumn());
+    Assert.assertEquals(1, node.getDependencies().size());
+    final VirtualColumns.Node depNode = node.getDependencies().get(0);
+    Assert.assertEquals(virtualColumns.getVirtualColumn("expr2i"), 
depNode.getVirtualColumn());
+    Assert.assertTrue(depNode.getDependencies().isEmpty());
+  }
+
+  @Test
+  public void testGetNodeTransitiveDependencies()
+  {
+    // v0 = v1 + v2, v2 = 1 + v1, v1 = 1 + x (physical)
+    final ExpressionVirtualColumn v1 = new ExpressionVirtualColumn("v1", "1 + 
x", ColumnType.LONG, TestExprMacroTable.INSTANCE);
+    final ExpressionVirtualColumn v2 = new ExpressionVirtualColumn("v2", "1 + 
v1", ColumnType.LONG, TestExprMacroTable.INSTANCE);
+    final ExpressionVirtualColumn v0 = new ExpressionVirtualColumn("v0", "v1 + 
v2", ColumnType.LONG, TestExprMacroTable.INSTANCE);
+    final VirtualColumns virtualColumns = VirtualColumns.create(v0, v1, v2);
+
+    final VirtualColumns.Node v1Node = virtualColumns.getNode("v1");
+    final VirtualColumns.Node v2Node = virtualColumns.getNode("v2");
+    final VirtualColumns.Node v0Node = virtualColumns.getNode("v0");
+
+    // v1 is a leaf
+    Assert.assertNotNull(v1Node);
+    Assert.assertEquals(v1, v1Node.getVirtualColumn());
+    Assert.assertTrue(v1Node.getDependencies().isEmpty());
+
+    // v2 has v1 as a dependency
+    Assert.assertNotNull(v2Node);
+    Assert.assertEquals(v2, v2Node.getVirtualColumn());
+    Assert.assertEquals(1, v2Node.getDependencies().size());
+    Assert.assertSame(v1Node, v2Node.getDependencies().get(0));
+
+    // v0 has v1 and v2 as direct dependencies; the same node instances are 
reused
+    Assert.assertNotNull(v0Node);
+    Assert.assertEquals(v0, v0Node.getVirtualColumn());
+    Assert.assertEquals(2, v0Node.getDependencies().size());
+    Assert.assertTrue(v0Node.getDependencies().contains(v1Node));
+    Assert.assertTrue(v0Node.getDependencies().contains(v2Node));
+  }
+
+  @Test
+  public void testGetNodeStructuralEquality()
+  {
+    final NestedFieldVirtualColumn n0 = new NestedFieldVirtualColumn("obj", 
"$.a", "n0", ColumnType.STRING);
+    final ExpressionVirtualColumn e0 = new ExpressionVirtualColumn(
+        "e0", "lower(\"n0\")", ColumnType.STRING, TestExprMacroTable.INSTANCE
+    );
+    final VirtualColumns vc1 = VirtualColumns.create(n0, e0);
+
+    // Same structure, different instances, nodes must be equal
+    final NestedFieldVirtualColumn n0copy = new 
NestedFieldVirtualColumn("obj", "$.a", "n0", ColumnType.STRING);
+    final ExpressionVirtualColumn e0copy = new ExpressionVirtualColumn(
+        "e0", "lower(\"n0\")", ColumnType.STRING, TestExprMacroTable.INSTANCE
+    );
+    final VirtualColumns vc2 = VirtualColumns.create(n0copy, e0copy);
+
+    Assert.assertEquals(vc1.getNode("e0"), vc2.getNode("e0"));
+    Assert.assertEquals(vc1.getNode("e0").hashCode(), 
vc2.getNode("e0").hashCode());
+
+    // Different underlying dependency ($.b instead of $.a), even though e0's 
expression looks the same,
+    // the node must differ because the dependency subtree differs
+    final NestedFieldVirtualColumn n0different = new 
NestedFieldVirtualColumn("obj", "$.b", "n0", ColumnType.STRING);
+    final ExpressionVirtualColumn e0sameName = new ExpressionVirtualColumn(
+        "e0", "lower(\"n0\")", ColumnType.STRING, TestExprMacroTable.INSTANCE
+    );
+    final VirtualColumns vc3 = VirtualColumns.create(n0different, e0sameName);
+
+    Assert.assertNotEquals(vc1.getNode("e0"), vc3.getNode("e0"));
+  }
+
   @Test
   public void testEquivalence()
   {
@@ -496,10 +593,10 @@ public class VirtualColumnsTest extends 
InitializedNullHandlingTest
     );
     VirtualColumns otherVirtualColumns = VirtualColumns.create(v1, v2, v3);
 
-    Assert.assertEquals(v0, 
virtualColumns.findEquivalent(VirtualColumns.EMPTY, v0));
-    Assert.assertEquals(v0, virtualColumns.findEquivalent(otherVirtualColumns, 
v1));
-    Assert.assertNull(virtualColumns.findEquivalent(otherVirtualColumns, v2));
-    Assert.assertNull(virtualColumns.findEquivalent(otherVirtualColumns, v3));
+    Assert.assertEquals(v0, 
virtualColumns.findEquivalent(virtualColumns.getNode(v0.getOutputName())));
+    Assert.assertEquals(v0, 
virtualColumns.findEquivalent(otherVirtualColumns.getNode(v1.getOutputName())));
+    
Assert.assertNull(virtualColumns.findEquivalent(otherVirtualColumns.getNode(v2.getOutputName())));
+    
Assert.assertNull(virtualColumns.findEquivalent(otherVirtualColumns.getNode(v3.getOutputName())));
   }
 
   @Test
@@ -520,9 +617,9 @@ public class VirtualColumnsTest extends 
InitializedNullHandlingTest
     );
     final VirtualColumns otherVirtualColumns = VirtualColumns.create(n1, e1);
 
-    Assert.assertEquals(n0, virtualColumns.findEquivalent(otherVirtualColumns, 
n1));
+    Assert.assertEquals(n0, 
virtualColumns.findEquivalent(otherVirtualColumns.getNode(n1.getOutputName())));
 
-    Assert.assertEquals(e0, virtualColumns.findEquivalent(otherVirtualColumns, 
e1));
+    Assert.assertEquals(e0, 
virtualColumns.findEquivalent(otherVirtualColumns.getNode(e1.getOutputName())));
 
     // a different nested field path produces no equivalence, even if it has 
the same name
     final NestedFieldVirtualColumn n0different = new 
NestedFieldVirtualColumn("obj", "$.b", "n0", ColumnType.STRING);
@@ -532,8 +629,8 @@ public class VirtualColumnsTest extends 
InitializedNullHandlingTest
         TestExprMacroTable.INSTANCE
     );
     final VirtualColumns notEquivalent = VirtualColumns.create(n0different, 
e0different);
-    Assert.assertNull(virtualColumns.findEquivalent(notEquivalent, 
n0different));
-    Assert.assertNull(virtualColumns.findEquivalent(notEquivalent, 
e0different));
+    
Assert.assertNull(virtualColumns.findEquivalent(notEquivalent.getNode(n0different.getOutputName())));
+    
Assert.assertNull(virtualColumns.findEquivalent(notEquivalent.getNode(e0different.getOutputName())));
   }
 
   @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to