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

FrankChen021 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 ed4fb761619 fix: Don't report unservable projections as nonexistent 
(#20685)
ed4fb761619 is described below

commit ed4fb761619d0e4483bba7c9feb61ec1c45870d8
Author: Yahya Kisana <[email protected]>
AuthorDate: Thu Oct 22 20:13:51 2026 -0600

    fix: Don't report unservable projections as nonexistent (#20685)
    
    #20633 added a "Projection[%s] does not exist" check to
    Projections.findMatchingProjection, which runs against whatever projection
    set the caller passes. PartialQueryableIndex.getServableProjection (#20617)
    passes only the servable subset, so a useProjection name that exists in the
    segment but is not servable was reported as not existing, failing
    
PartialQueryableIndexServableBundlesTest.testUseProjectionNamingUnservedProjectionFails.
    
    Add a findMatchingProjection overload that takes a servable predicate: only
    servable projections are matching candidates, while the existence check
    still uses the segment's full projection set. getServableProjection uses it
    instead of building a filtered copy; other callers are unchanged.
---
 .../druid/segment/PartialQueryableIndex.java       |  10 +-
 .../druid/segment/projections/Projections.java     |  30 +++++
 .../PartialQueryableIndexServableBundlesTest.java  | 125 ++++++++++++++++++++-
 3 files changed, 153 insertions(+), 12 deletions(-)

diff --git 
a/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java 
b/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java
index f81d8767334..6ba91ac9c89 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java
@@ -363,16 +363,10 @@ public class PartialQueryableIndex implements 
QueryableIndex
       Predicate<String> servableBundles
   )
   {
-    final SortedSet<AggregateProjectionMetadata> servable =
-        new ObjectAVLTreeSet<>(AggregateProjectionMetadata.COMPARATOR);
-    for (AggregateProjectionMetadata projection : projections) {
-      if (servableBundles.test(projection.getSchema().getName())) {
-        servable.add(projection);
-      }
-    }
     return Projections.findMatchingProjection(
         cursorBuildSpec,
-        servable,
+        projections,
+        servableBundles,
         dataInterval,
         this::checkProjectionHasColumn,
         this::getProjectionQueryableIndex
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 66f9bead54e..069719a0856 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
@@ -72,6 +72,7 @@ import java.util.Set;
 import java.util.SortedSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 public class Projections
 {
@@ -112,6 +113,32 @@ public class Projections
       PhysicalColumnChecker physicalChecker,
       Function<String, T> getRowSelector
   )
+  {
+    return findMatchingProjection(
+        cursorBuildSpec,
+        projections,
+        projectionName -> true,
+        dataInterval,
+        physicalChecker,
+        getRowSelector
+    );
+  }
+
+  /**
+   * Like {@link #findMatchingProjection(CursorBuildSpec, SortedSet, Interval, 
PhysicalColumnChecker, Function)}, but
+   * only projections accepted by {@code servableProjections} are matching 
candidates. A projection named by
+   * {@link QueryContextParameters#USE_PROJECTION} that is in {@code 
projections} but not servable is reported as not
+   * satisfying the query rather than as not existing.
+   */
+  @Nullable
+  public static <T> QueryableProjection<T> findMatchingProjection(
+      CursorBuildSpec cursorBuildSpec,
+      SortedSet<AggregateProjectionMetadata> projections,
+      Predicate<String> servableProjections,
+      Interval dataInterval,
+      PhysicalColumnChecker physicalChecker,
+      Function<String, T> getRowSelector
+  )
   {
     if 
(cursorBuildSpec.getQueryContext().getOrDefault(QueryContextParameters.NO_PROJECTIONS))
 {
       return null;
@@ -123,6 +150,9 @@ public class Projections
         if (name != null && !name.equals(spec.getSchema().getName())) {
           continue;
         }
+        if (!servableProjections.test(spec.getSchema().getName())) {
+          continue;
+        }
         final ProjectionMatch match = matchAggregateProjection(
             spec.getSchema(),
             cursorBuildSpec,
diff --git 
a/processing/src/test/java/org/apache/druid/segment/PartialQueryableIndexServableBundlesTest.java
 
b/processing/src/test/java/org/apache/druid/segment/PartialQueryableIndexServableBundlesTest.java
index dd9cbb110a2..e448569d905 100644
--- 
a/processing/src/test/java/org/apache/druid/segment/PartialQueryableIndexServableBundlesTest.java
+++ 
b/processing/src/test/java/org/apache/druid/segment/PartialQueryableIndexServableBundlesTest.java
@@ -31,8 +31,8 @@ import org.apache.druid.data.input.impl.StringDimensionSchema;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.query.QueryContext;
-import org.apache.druid.query.QueryContexts;
 import org.apache.druid.query.aggregation.CountAggregatorFactory;
+import org.apache.druid.query.context.QueryContextParameters;
 import org.apache.druid.segment.column.ColumnType;
 import org.apache.druid.segment.column.RowSignature;
 import org.apache.druid.segment.data.CompressionStrategy;
@@ -53,7 +53,6 @@ import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.Callable;
@@ -108,6 +107,14 @@ class PartialQueryableIndexServableBundlesTest extends 
PartialQueryableIndexCurs
                      .setPhysicalColumns(Set.of("dim1"))
                      .build();
 
+  // group by dim1, metric1 + count, which only BIG_PROJECTION can answer
+  private static final CursorBuildSpec COUNT_BY_DIM1_METRIC1 =
+      CursorBuildSpec.builder()
+                     .setGroupingColumns(List.of("dim1", "metric1"))
+                     .setAggregators(List.of(new 
CountAggregatorFactory("_count")))
+                     .setPhysicalColumns(Set.of("dim1", "metric1"))
+                     .build();
+
   @RegisterExtension
   public static final TemporaryFolderExtension SHARED_TEMPORARY_FOLDER = 
TemporaryFolderExtension.classScoped();
 
@@ -272,7 +279,8 @@ class PartialQueryableIndexServableBundlesTest extends 
PartialQueryableIndexCurs
       final CursorBuildSpec spec = CursorBuildSpec.builder(COUNT_BY_DIM1)
                                                   .setQueryContext(
                                                       QueryContext.of(
-                                                          
Map.of(QueryContexts.USE_PROJECTION, SMALL_PROJECTION)
+                                                          
QueryContextParameters.USE_PROJECTION,
+                                                          SMALL_PROJECTION
                                                       )
                                                   )
                                                   .build();
@@ -284,7 +292,116 @@ class PartialQueryableIndexServableBundlesTest extends 
PartialQueryableIndexCurs
           DruidException.class,
           () -> opened.index().planCursorPrefetch(spec, BIG_PROJECTION::equals)
       );
-      Assertions.assertTrue(e.getMessage().contains("does not satisfy query"), 
e.getMessage());
+      Assertions.assertEquals(
+          "Projection[" + SMALL_PROJECTION + "] specified, but does not 
satisfy query",
+          e.getMessage()
+      );
+    }
+  }
+
+  @Test
+  void testUseProjectionNamingMissingProjectionFails() throws IOException
+  {
+    try (IndexAndMapper opened = openIndex(new 
CountingRangeReader(segmentDir), "use_missing_projection")) {
+      final CursorBuildSpec spec = CursorBuildSpec.builder(COUNT_BY_DIM1)
+                                                  .setQueryContext(
+                                                      QueryContext.of(
+                                                          
QueryContextParameters.USE_PROJECTION,
+                                                          "does_not_exist"
+                                                      )
+                                                  )
+                                                  .build();
+      final DruidException e = Assertions.assertThrows(
+          DruidException.class,
+          () -> opened.index().planCursorPrefetch(spec, BIG_PROJECTION::equals)
+      );
+      Assertions.assertEquals("Projection[does_not_exist] does not exist", 
e.getMessage());
+    }
+  }
+
+  @Test
+  void testUseProjectionNamingServedProjectionSucceeds() throws IOException
+  {
+    try (IndexAndMapper opened = openIndex(new 
CountingRangeReader(segmentDir), "use_served_projection")) {
+      final CursorBuildSpec spec = CursorBuildSpec.builder(COUNT_BY_DIM1)
+                                                  .setQueryContext(
+                                                      QueryContext.of(
+                                                          
QueryContextParameters.USE_PROJECTION,
+                                                          BIG_PROJECTION
+                                                      )
+                                                  )
+                                                  .build();
+      // the big projection is named and servable, so it's used even though 
the small one is also servable and
+      // would otherwise be preferred
+      final PartialQueryableIndex.CursorPrefetchPlan plan =
+          opened.index().planCursorPrefetch(spec, name -> !BASE.equals(name));
+      Assertions.assertEquals(BIG_PROJECTION, 
plan.matchedProjection().getName());
+    }
+  }
+
+  @Test
+  void testUseProjectionNamingServedProjectionThatDoesNotFitFails() throws 
IOException
+  {
+    try (IndexAndMapper opened = openIndex(new 
CountingRangeReader(segmentDir), "use_served_no_fit")) {
+      final CursorBuildSpec spec = 
CursorBuildSpec.builder(COUNT_BY_DIM1_METRIC1)
+                                                  .setQueryContext(
+                                                      QueryContext.of(
+                                                          
QueryContextParameters.USE_PROJECTION,
+                                                          SMALL_PROJECTION
+                                                      )
+                                                  )
+                                                  .build();
+      final DruidException e = Assertions.assertThrows(
+          DruidException.class,
+          () -> opened.index().planCursorPrefetch(spec, 
SMALL_PROJECTION::equals)
+      );
+      Assertions.assertEquals(
+          "Projection[" + SMALL_PROJECTION + "] specified, but does not 
satisfy query",
+          e.getMessage()
+      );
+    }
+  }
+
+  @Test
+  void testForceProjectionsWithNothingServableFails() throws IOException
+  {
+    try (IndexAndMapper opened = openIndex(new 
CountingRangeReader(segmentDir), "force_nothing_servable")) {
+      final CursorBuildSpec spec = CursorBuildSpec.builder(COUNT_BY_DIM1)
+                                                  .setQueryContext(
+                                                      
QueryContext.of(QueryContextParameters.FORCE_PROJECTIONS, true)
+                                                  )
+                                                  .build();
+      Assertions.assertEquals(
+          SMALL_PROJECTION,
+          opened.index().planCursorPrefetch(spec, 
null).matchedProjection().getName()
+      );
+      final DruidException e = Assertions.assertThrows(
+          DruidException.class,
+          () -> opened.index().planCursorPrefetch(spec, bundle -> false)
+      );
+      Assertions.assertEquals("Force projections specified, but none satisfy 
query", e.getMessage());
+    }
+  }
+
+  @Test
+  void testNoProjectionsWithNothingServableSkipsUseProjection() throws 
IOException
+  {
+    try (IndexAndMapper opened = openIndex(new 
CountingRangeReader(segmentDir), "no_projections_nothing_servable")) {
+      // without noProjections, naming the unservable small projection fails 
as in
+      // testUseProjectionNamingUnservedProjectionFails
+      final CursorBuildSpec spec = CursorBuildSpec.builder(COUNT_BY_DIM1)
+                                                  .setQueryContext(
+                                                      QueryContext.of(
+                                                          
QueryContextParameters.NO_PROJECTIONS,
+                                                          true,
+                                                          
QueryContextParameters.USE_PROJECTION,
+                                                          SMALL_PROJECTION
+                                                      )
+                                                  )
+                                                  .build();
+      final PartialQueryableIndex.CursorPrefetchPlan plan = 
opened.index().planCursorPrefetch(spec, bundle -> false);
+      Assertions.assertNull(plan.matchedProjection());
+      Assertions.assertTrue(plan.bundles().isEmpty(), "the base table is not 
servable either");
     }
   }
 


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

Reply via email to