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]