From 4dcf459a20109389f462f8a726a62f48520ec7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesse=20Tu=C4=9Flu?= Date: Tue, 19 May 2026 22:58:02 -0700 Subject: [PATCH 1/2] feat: add realtimeSegmentsMode query context param --- docs/querying/query-context-reference.md | 3 +- .../org/apache/druid/query/QueryContext.java | 29 ++++++- .../org/apache/druid/query/QueryContexts.java | 44 ++++++++++ .../apache/druid/query/QueryContextTest.java | 61 ++++++++++++++ .../druid/client/CachingClusteredClient.java | 18 +++- .../client/CachingClusteredClientTest.java | 83 +++++++++++++++++-- 6 files changed, 225 insertions(+), 13 deletions(-) diff --git a/docs/querying/query-context-reference.md b/docs/querying/query-context-reference.md index 511e8b13b699..fdefe98e7d18 100644 --- a/docs/querying/query-context-reference.md +++ b/docs/querying/query-context-reference.md @@ -71,7 +71,8 @@ Unless otherwise noted, the following parameters apply to all query types, and t |`setProcessingThreadNames`|`true`| Whether processing thread names will be set to `queryType_dataSource_intervals` while processing a query. This aids in interpreting thread dumps, and is on by default. Query overhead can be reduced slightly by setting this to `false`. This has a tiny effect in most scenarios, but can be meaningful in high-QPS, low-per-segment-processing-time scenarios. | |`sqlPlannerBloat`|`1000`|Calcite parameter which controls whether to merge two Project operators when inlining expressions causes complexity to increase. Implemented as a workaround to exception `There are not enough rules to produce a node with desired properties: convention=DRUID, sort=[]` thrown after rejecting the merge of two projects.| |`cloneQueryMode`|`excludeClones`| Indicates whether clone Historicals should be queried by brokers. Clone servers are created by the `cloneServers` Coordinator dynamic configuration. Possible values are `excludeClones`, `includeClones` and `preferClones`. `excludeClones` means that clone Historicals are not queried by the broker. `preferClones` indicates that when given a choice between the clone Historical and the original Historical which is being cloned, the broker chooses the clones. Historicals which are not involved in the cloning process will still be queried. `includeClones` means that broker queries any Historical without regarding clone status. This parameter only affects native queries. MSQ does not query Historicals directly.| -|`realtimeSegmentsOnly` |`false`| When set to true, only query realtime segments. Historical segments are excluded. | +|`realtimeSegmentsMode` |`include`| Controls which segments are queried, classified by whether a historical replica exists. `include` queries all segments. `exclusive` queries only segments served solely by realtime servers; any segment with at least one historical replica (including segments mid-handoff) is excluded. `exclude` is the inverse: segments served solely by realtime servers are skipped, but segments mid-handoff that have both a realtime and a historical replica are still queried. | +|`realtimeSegmentsOnly` |`false`| **Deprecated.** Use `realtimeSegmentsMode=exclusive` instead. When set to `true`, equivalent to `realtimeSegmentsMode=exclusive`. | ## Parameters by query type diff --git a/processing/src/main/java/org/apache/druid/query/QueryContext.java b/processing/src/main/java/org/apache/druid/query/QueryContext.java index 39f325d10893..73aadae15368 100644 --- a/processing/src/main/java/org/apache/druid/query/QueryContext.java +++ b/processing/src/main/java/org/apache/druid/query/QueryContext.java @@ -27,6 +27,7 @@ import org.apache.druid.java.util.common.HumanReadableBytes; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.java.util.common.granularity.Granularity; +import org.apache.druid.query.QueryContexts.RealtimeSegmentsMode; import org.apache.druid.query.QueryContexts.Vectorize; import org.apache.druid.query.filter.InDimFilter; import org.apache.druid.query.filter.TypedInFilter; @@ -781,8 +782,34 @@ public boolean isPrePlanned() return getBoolean(QueryContexts.CTX_PREPLANNED, QueryContexts.DEFAULT_PREPLANNED); } + /** + * Returns the realtime segments mode for this query. If {@code realtimeSegmentsMode} is absent + * or null, falls back to the deprecated {@code realtimeSegmentsOnly} boolean: {@code true} maps + * to {@link RealtimeSegmentsMode#EXCLUSIVE}; otherwise returns {@link RealtimeSegmentsMode#INCLUDE}. + */ + public RealtimeSegmentsMode getRealtimeSegmentsMode() + { + RealtimeSegmentsMode mode = getEnum( + QueryContexts.REALTIME_SEGMENTS_MODE, + RealtimeSegmentsMode.class, + null + ); + if (mode != null) { + return mode; + } + // Backward-compat: honour the deprecated realtimeSegmentsOnly flag. + if (getBoolean(QueryContexts.REALTIME_SEGMENTS_ONLY, QueryContexts.DEFAULT_REALTIME_SEGMENTS_ONLY)) { + return RealtimeSegmentsMode.EXCLUSIVE; + } + return QueryContexts.DEFAULT_REALTIME_SEGMENTS_MODE; + } + + /** + * @deprecated Use {@link #getRealtimeSegmentsMode()} instead. + */ + @Deprecated public boolean isRealtimeSegmentsOnly() { - return getBoolean(QueryContexts.REALTIME_SEGMENTS_ONLY, QueryContexts.DEFAULT_REALTIME_SEGMENTS_ONLY); + return getRealtimeSegmentsMode() == RealtimeSegmentsMode.EXCLUSIVE; } } diff --git a/processing/src/main/java/org/apache/druid/query/QueryContexts.java b/processing/src/main/java/org/apache/druid/query/QueryContexts.java index 1cb8aa24cf4f..b3d7556d09a6 100644 --- a/processing/src/main/java/org/apache/druid/query/QueryContexts.java +++ b/processing/src/main/java/org/apache/druid/query/QueryContexts.java @@ -146,9 +146,20 @@ public class QueryContexts public static final String NATIVE_QUERY_SQL_PLANNING_MODE_COUPLED = "COUPLED"; public static final String NATIVE_QUERY_SQL_PLANNING_MODE_DECOUPLED = "DECOUPLED"; + /** + * @deprecated Use {@link #REALTIME_SEGMENTS_MODE} instead. + */ + @Deprecated public static final String REALTIME_SEGMENTS_ONLY = "realtimeSegmentsOnly"; + /** + * @deprecated Use {@link #DEFAULT_REALTIME_SEGMENTS_MODE} instead. + */ + @Deprecated public static final boolean DEFAULT_REALTIME_SEGMENTS_ONLY = false; + public static final String REALTIME_SEGMENTS_MODE = "realtimeSegmentsMode"; + public static final RealtimeSegmentsMode DEFAULT_REALTIME_SEGMENTS_MODE = RealtimeSegmentsMode.INCLUDE; + public static final String CTX_PREPLANNED = "prePlanned"; public static final boolean DEFAULT_PREPLANNED = true; @@ -233,6 +244,39 @@ public String toString() } } + /** + * Classifies segments by whether a historical replica exists + * (see {@link org.apache.druid.client.selector.ServerSelector#isRealtimeSegment()}: a segment is + * "realtime" only when it has realtime servers and zero historical servers). + */ + public enum RealtimeSegmentsMode + { + /** Include all segments (default). */ + INCLUDE, + /** Include only segments served solely by realtime servers; any segment with a historical replica + * (including segments mid-handoff) is excluded. */ + EXCLUSIVE, + /** Exclude segments served solely by realtime servers; segments mid-handoff with both realtime + * and historical replicas are still included. */ + EXCLUDE; + + @JsonCreator + public static RealtimeSegmentsMode fromString(String str) + { + if (str == null) { + return null; + } + return RealtimeSegmentsMode.valueOf(StringUtils.toUpperCase(str)); + } + + @Override + @JsonValue + public String toString() + { + return StringUtils.toLowerCase(name()); + } + } + private QueryContexts() { } diff --git a/processing/src/test/java/org/apache/druid/query/QueryContextTest.java b/processing/src/test/java/org/apache/druid/query/QueryContextTest.java index 0cb931e50d29..c04d9707df5c 100644 --- a/processing/src/test/java/org/apache/druid/query/QueryContextTest.java +++ b/processing/src/test/java/org/apache/druid/query/QueryContextTest.java @@ -430,6 +430,7 @@ public void testNonLegacyIsNotLegacyContext() } @Test + @SuppressWarnings("deprecation") public void testIsRealtimeSegmentsOnly() { assertFalse(QueryContext.empty().isRealtimeSegmentsOnly()); @@ -440,6 +441,66 @@ public void testIsRealtimeSegmentsOnly() ); } + @Test + public void testGetRealtimeSegmentsMode() + { + assertEquals( + QueryContexts.RealtimeSegmentsMode.INCLUDE, + QueryContext.empty().getRealtimeSegmentsMode() + ); + assertEquals( + QueryContexts.RealtimeSegmentsMode.EXCLUSIVE, + QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "exclusive")) + .getRealtimeSegmentsMode() + ); + assertEquals( + QueryContexts.RealtimeSegmentsMode.EXCLUDE, + QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "exclude")) + .getRealtimeSegmentsMode() + ); + assertEquals( + QueryContexts.RealtimeSegmentsMode.INCLUDE, + QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "include")) + .getRealtimeSegmentsMode() + ); + } + + @Test + public void testGetRealtimeSegmentsModeBackwardCompat() + { + // realtimeSegmentsOnly=true maps to EXCLUSIVE + assertEquals( + QueryContexts.RealtimeSegmentsMode.EXCLUSIVE, + QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_ONLY, true)) + .getRealtimeSegmentsMode() + ); + // realtimeSegmentsOnly=false maps to INCLUDE (default) + assertEquals( + QueryContexts.RealtimeSegmentsMode.INCLUDE, + QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_ONLY, false)) + .getRealtimeSegmentsMode() + ); + // realtimeSegmentsMode takes precedence over realtimeSegmentsOnly + assertEquals( + QueryContexts.RealtimeSegmentsMode.EXCLUDE, + QueryContext.of(ImmutableMap.of( + QueryContexts.REALTIME_SEGMENTS_ONLY, true, + QueryContexts.REALTIME_SEGMENTS_MODE, "exclude" + )).getRealtimeSegmentsMode() + ); + } + + @Test + public void testGetRealtimeSegmentsModeInvalidValue() + { + BadQueryContextException e = assertThrows( + BadQueryContextException.class, + () -> QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "badvalue")) + .getRealtimeSegmentsMode() + ); + assertTrue(e.getMessage().contains("realtimeSegmentsMode")); + } + @Test public void testSerialization() throws Exception { diff --git a/server/src/main/java/org/apache/druid/client/CachingClusteredClient.java b/server/src/main/java/org/apache/druid/client/CachingClusteredClient.java index eb0a5a83997f..9305b1b88e9f 100644 --- a/server/src/main/java/org/apache/druid/client/CachingClusteredClient.java +++ b/server/src/main/java/org/apache/druid/client/CachingClusteredClient.java @@ -61,6 +61,7 @@ import org.apache.druid.query.Query; import org.apache.druid.query.QueryContext; import org.apache.druid.query.QueryContexts; +import org.apache.druid.query.QueryContexts.RealtimeSegmentsMode; import org.apache.druid.query.QueryMetrics; import org.apache.druid.query.QueryPlus; import org.apache.druid.query.QueryRunner; @@ -444,7 +445,7 @@ private Set computeSegmentsToQuery( final Set segments = new LinkedHashSet<>(); final SegmentPruner segmentPruner = ev.getSegmentPruner(); - boolean isRealtimeSegmentOnly = query.context().isRealtimeSegmentsOnly(); + RealtimeSegmentsMode realtimeSegmentsMode = query.context().getRealtimeSegmentsMode(); // Filter unneeded chunks based on partition dimension for (TimelineObjectHolder holder : serversLookup) { final Collection> filteredChunks; @@ -458,8 +459,19 @@ private Set computeSegmentsToQuery( } for (PartitionChunk chunk : filteredChunks) { ServerSelector server = chunk.getObject(); - if (isRealtimeSegmentOnly && !server.isRealtimeSegment()) { - continue; // Skip historical segments when only realtime segments are requested + switch (realtimeSegmentsMode) { + case EXCLUSIVE: + if (!server.isRealtimeSegment()) { + continue; + } + break; + case EXCLUDE: + if (server.isRealtimeSegment()) { + continue; + } + break; + case INCLUDE: + break; } final SegmentDescriptor segment = new SegmentDescriptor( holder.getInterval(), diff --git a/server/src/test/java/org/apache/druid/client/CachingClusteredClientTest.java b/server/src/test/java/org/apache/druid/client/CachingClusteredClientTest.java index 4f76b7b52c17..94c259314dea 100644 --- a/server/src/test/java/org/apache/druid/client/CachingClusteredClientTest.java +++ b/server/src/test/java/org/apache/druid/client/CachingClusteredClientTest.java @@ -3132,27 +3132,94 @@ public void testRealtimeSegmentsQueryContext() selector.addServerAndUpdateSegment(new QueryableDruidServer(servers[0], null), dataSegment); timeline.add(interval, "ver", new SingleElementPartitionChunk<>(selector)); - final TimeBoundaryQuery query = Druids.newTimeBoundaryQueryBuilder() + // include (default): historical segment is included + final TimeBoundaryQuery queryInclude = Druids.newTimeBoundaryQueryBuilder() .dataSource(DATA_SOURCE) .intervals(new MultipleIntervalSegmentSpec(ImmutableList.of(queryInterval))) - .context(ImmutableMap.of("realtimeSegmentsOnly", false)) + .context(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "include")) .randomQueryId() .build(); - final TimeBoundaryQuery query2 = Druids.newTimeBoundaryQueryBuilder() + // exclusive: only realtime segments — historical segment is excluded + final TimeBoundaryQuery queryExclusive = Druids.newTimeBoundaryQueryBuilder() + .dataSource(DATA_SOURCE) + .intervals(new MultipleIntervalSegmentSpec(ImmutableList.of(queryInterval))) + .context(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "exclusive")) + .randomQueryId() + .build(); + + // backward compat: realtimeSegmentsOnly=true maps to EXCLUSIVE + final TimeBoundaryQuery queryLegacyTrue = Druids.newTimeBoundaryQueryBuilder() .dataSource(DATA_SOURCE) .intervals(new MultipleIntervalSegmentSpec(ImmutableList.of(queryInterval))) - .context(ImmutableMap.of("realtimeSegmentsOnly", true)) + .context(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_ONLY, true)) .randomQueryId() .build(); final ResponseContext responseContext = initializeResponseContext(); - getDefaultQueryRunner().run(QueryPlus.wrap(query), responseContext); - getDefaultQueryRunner().run(QueryPlus.wrap(query2), responseContext); + getDefaultQueryRunner().run(QueryPlus.wrap(queryInclude), responseContext); + getDefaultQueryRunner().run(QueryPlus.wrap(queryExclusive), responseContext); + getDefaultQueryRunner().run(QueryPlus.wrap(queryLegacyTrue), responseContext); + + final Map remainingResponseMap = (Map) responseContext.get(ResponseContext.Keys.REMAINING_RESPONSES_FROM_QUERY_SERVERS); + Assert.assertEquals(1, remainingResponseMap.get(queryInclude.getId()).intValue()); + Assert.assertEquals(0, remainingResponseMap.get(queryExclusive.getId()).intValue()); + Assert.assertEquals(0, remainingResponseMap.get(queryLegacyTrue.getId()).intValue()); + } + + @Test + public void testRealtimeSegmentsModeExclude() + { + final Interval interval = Intervals.of("2016-01-01/2016-01-02"); + final Interval queryInterval = Intervals.of("2016-01-01T14:00:00/2016-01-02T14:00:00"); + final DataSegment dataSegment = new DataSegment( + "dataSource", + interval, + "ver", + ImmutableMap.of("type", "hdfs", "path", "/tmp"), + ImmutableList.of("product"), + ImmutableList.of("visited_sum"), + NoneShardSpec.instance(), + 9, + 12334 + ); + + // selector backed only by a realtime server — isRealtimeSegment() == true + final DruidServer realtimeServer = new DruidServer( + "rt1", "rt1", null, 10, null, ServerType.REALTIME, DruidServer.DEFAULT_TIER, 0 + ); + final ServerSelector realtimeSelector = new ServerSelector( + dataSegment, + new HighestPriorityTierSelectorStrategy(new RandomServerSelectorStrategy()), + HistoricalFilter.IDENTITY_FILTER + ); + realtimeSelector.addServerAndUpdateSegment(new QueryableDruidServer(realtimeServer, null), dataSegment); + timeline.add(interval, "ver", new SingleElementPartitionChunk<>(realtimeSelector)); + + // exclude: realtime-only segment is skipped + final TimeBoundaryQuery queryExclude = Druids.newTimeBoundaryQueryBuilder() + .dataSource(DATA_SOURCE) + .intervals(new MultipleIntervalSegmentSpec(ImmutableList.of(queryInterval))) + .context(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "exclude")) + .randomQueryId() + .build(); + + // include: realtime-only segment is included + final TimeBoundaryQuery queryInclude = Druids.newTimeBoundaryQueryBuilder() + .dataSource(DATA_SOURCE) + .intervals(new MultipleIntervalSegmentSpec(ImmutableList.of(queryInterval))) + .context(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "include")) + .randomQueryId() + .build(); + + final ResponseContext responseContext = initializeResponseContext(); + getDefaultQueryRunner().run(QueryPlus.wrap(queryExclude), responseContext); + getDefaultQueryRunner().run(QueryPlus.wrap(queryInclude), responseContext); + final Map remainingResponseMap = (Map) responseContext.get(ResponseContext.Keys.REMAINING_RESPONSES_FROM_QUERY_SERVERS); - Assert.assertEquals(1, remainingResponseMap.get(query.getId()).intValue()); - Assert.assertEquals(0, remainingResponseMap.get(query2.getId()).intValue()); + Assert.assertEquals(0, remainingResponseMap.get(queryExclude.getId()).intValue()); + Assert.assertEquals(1, remainingResponseMap.get(queryInclude.getId()).intValue()); } @SuppressWarnings("unchecked") From 2c6c0e69f2424826b9317ad0561ed66a02fb059a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesse=20Tu=C4=9Flu?= Date: Wed, 20 May 2026 11:28:33 -0700 Subject: [PATCH 2/2] more changes --- docs/querying/query-context-reference.md | 4 ++-- .../org/apache/druid/query/QueryContext.java | 22 +++++++++++++++---- .../org/apache/druid/query/QueryContexts.java | 8 +++---- .../apache/druid/query/QueryContextTest.java | 21 +++++++++++++----- .../query-context-completions.ts | 12 ++++++++-- 5 files changed, 49 insertions(+), 18 deletions(-) diff --git a/docs/querying/query-context-reference.md b/docs/querying/query-context-reference.md index fdefe98e7d18..c485c0231c06 100644 --- a/docs/querying/query-context-reference.md +++ b/docs/querying/query-context-reference.md @@ -71,8 +71,8 @@ Unless otherwise noted, the following parameters apply to all query types, and t |`setProcessingThreadNames`|`true`| Whether processing thread names will be set to `queryType_dataSource_intervals` while processing a query. This aids in interpreting thread dumps, and is on by default. Query overhead can be reduced slightly by setting this to `false`. This has a tiny effect in most scenarios, but can be meaningful in high-QPS, low-per-segment-processing-time scenarios. | |`sqlPlannerBloat`|`1000`|Calcite parameter which controls whether to merge two Project operators when inlining expressions causes complexity to increase. Implemented as a workaround to exception `There are not enough rules to produce a node with desired properties: convention=DRUID, sort=[]` thrown after rejecting the merge of two projects.| |`cloneQueryMode`|`excludeClones`| Indicates whether clone Historicals should be queried by brokers. Clone servers are created by the `cloneServers` Coordinator dynamic configuration. Possible values are `excludeClones`, `includeClones` and `preferClones`. `excludeClones` means that clone Historicals are not queried by the broker. `preferClones` indicates that when given a choice between the clone Historical and the original Historical which is being cloned, the broker chooses the clones. Historicals which are not involved in the cloning process will still be queried. `includeClones` means that broker queries any Historical without regarding clone status. This parameter only affects native queries. MSQ does not query Historicals directly.| -|`realtimeSegmentsMode` |`include`| Controls which segments are queried, classified by whether a historical replica exists. `include` queries all segments. `exclusive` queries only segments served solely by realtime servers; any segment with at least one historical replica (including segments mid-handoff) is excluded. `exclude` is the inverse: segments served solely by realtime servers are skipped, but segments mid-handoff that have both a realtime and a historical replica are still queried. | -|`realtimeSegmentsOnly` |`false`| **Deprecated.** Use `realtimeSegmentsMode=exclusive` instead. When set to `true`, equivalent to `realtimeSegmentsMode=exclusive`. | +|`realtimeSegmentsMode` |`include`| Controls whether realtime segments are queried. `include` queries all segments, including realtime. `exclude` skips realtime segments. `exclusive` queries only realtime segments. | +|`realtimeSegmentsOnly` |`false`| **Deprecated.** Use `realtimeSegmentsMode=exclusive` instead. When set to `true`, this is equivalent to `realtimeSegmentsMode=exclusive`. When set to `false`, this is equivalent to `realtimeSegmentsMode=include`.| ## Parameters by query type diff --git a/processing/src/main/java/org/apache/druid/query/QueryContext.java b/processing/src/main/java/org/apache/druid/query/QueryContext.java index 73aadae15368..c29296260023 100644 --- a/processing/src/main/java/org/apache/druid/query/QueryContext.java +++ b/processing/src/main/java/org/apache/druid/query/QueryContext.java @@ -783,9 +783,10 @@ public boolean isPrePlanned() } /** - * Returns the realtime segments mode for this query. If {@code realtimeSegmentsMode} is absent + * Returns the realtime segments mode for this query. If {@link QueryContexts#REALTIME_SEGMENTS_MODE} is absent * or null, falls back to the deprecated {@code realtimeSegmentsOnly} boolean: {@code true} maps * to {@link RealtimeSegmentsMode#EXCLUSIVE}; otherwise returns {@link RealtimeSegmentsMode#INCLUDE}. + * Throws {@link BadQueryContextException} if both fields are set simultaneously. */ public RealtimeSegmentsMode getRealtimeSegmentsMode() { @@ -794,12 +795,25 @@ public RealtimeSegmentsMode getRealtimeSegmentsMode() RealtimeSegmentsMode.class, null ); + boolean hasDeprecatedFlag = get(QueryContexts.REALTIME_SEGMENTS_ONLY) != null; + if (mode != null && hasDeprecatedFlag) { + throw new BadQueryContextException( + StringUtils.format( + "Cannot set both [%s] and deprecated [%s]; use [%s] only.", + QueryContexts.REALTIME_SEGMENTS_MODE, + QueryContexts.REALTIME_SEGMENTS_ONLY, + QueryContexts.REALTIME_SEGMENTS_MODE + ) + ); + } if (mode != null) { return mode; } - // Backward-compat: honour the deprecated realtimeSegmentsOnly flag. - if (getBoolean(QueryContexts.REALTIME_SEGMENTS_ONLY, QueryContexts.DEFAULT_REALTIME_SEGMENTS_ONLY)) { - return RealtimeSegmentsMode.EXCLUSIVE; + if (hasDeprecatedFlag) { + // Backward-compat: honour the deprecated realtimeSegmentsOnly flag. + return getBoolean(QueryContexts.REALTIME_SEGMENTS_ONLY, QueryContexts.DEFAULT_REALTIME_SEGMENTS_ONLY) + ? RealtimeSegmentsMode.EXCLUSIVE + : QueryContexts.DEFAULT_REALTIME_SEGMENTS_MODE; } return QueryContexts.DEFAULT_REALTIME_SEGMENTS_MODE; } diff --git a/processing/src/main/java/org/apache/druid/query/QueryContexts.java b/processing/src/main/java/org/apache/druid/query/QueryContexts.java index b3d7556d09a6..44dffc9a427f 100644 --- a/processing/src/main/java/org/apache/druid/query/QueryContexts.java +++ b/processing/src/main/java/org/apache/druid/query/QueryContexts.java @@ -251,13 +251,11 @@ public String toString() */ public enum RealtimeSegmentsMode { - /** Include all segments (default). */ + /** Query all segments, including realtime (default). */ INCLUDE, - /** Include only segments served solely by realtime servers; any segment with a historical replica - * (including segments mid-handoff) is excluded. */ + /** Query only realtime segments. */ EXCLUSIVE, - /** Exclude segments served solely by realtime servers; segments mid-handoff with both realtime - * and historical replicas are still included. */ + /** Skip realtime segments; query only historical. */ EXCLUDE; @JsonCreator diff --git a/processing/src/test/java/org/apache/druid/query/QueryContextTest.java b/processing/src/test/java/org/apache/druid/query/QueryContextTest.java index c04d9707df5c..d5550bc28dcf 100644 --- a/processing/src/test/java/org/apache/druid/query/QueryContextTest.java +++ b/processing/src/test/java/org/apache/druid/query/QueryContextTest.java @@ -480,14 +480,22 @@ public void testGetRealtimeSegmentsModeBackwardCompat() QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_ONLY, false)) .getRealtimeSegmentsMode() ); - // realtimeSegmentsMode takes precedence over realtimeSegmentsOnly - assertEquals( - QueryContexts.RealtimeSegmentsMode.EXCLUDE, - QueryContext.of(ImmutableMap.of( + } + + @Test + public void testGetRealtimeSegmentsModeConflictThrows() + { + BadQueryContextException e = assertThrows( + BadQueryContextException.class, + () -> QueryContext.of(ImmutableMap.of( QueryContexts.REALTIME_SEGMENTS_ONLY, true, QueryContexts.REALTIME_SEGMENTS_MODE, "exclude" )).getRealtimeSegmentsMode() ); + assertEquals( + "Cannot set both [realtimeSegmentsMode] and deprecated [realtimeSegmentsOnly]; use [realtimeSegmentsMode] only.", + e.getMessage() + ); } @Test @@ -498,7 +506,10 @@ public void testGetRealtimeSegmentsModeInvalidValue() () -> QueryContext.of(ImmutableMap.of(QueryContexts.REALTIME_SEGMENTS_MODE, "badvalue")) .getRealtimeSegmentsMode() ); - assertTrue(e.getMessage().contains("realtimeSegmentsMode")); + assertEquals( + "Expected key [realtimeSegmentsMode] to be referring to one of the values [INCLUDE,EXCLUSIVE,EXCLUDE] of enum [RealtimeSegmentsMode], but got [badvalue]", + e.getMessage() + ); } @Test diff --git a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts index 9e3cb8dca8dd..d7e67b7f492d 100644 --- a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts +++ b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts @@ -73,8 +73,8 @@ export const QUERY_CONTEXT_COMPLETIONS: JsonCompletionRule[] = [ documentation: 'Enable vectorized query execution', }, { - value: 'realtimeSegmentsOnly', - documentation: 'Whether to query only realtime segments', + value: 'realtimeSegmentsMode', + documentation: 'Controls whether realtime segments are queried', }, { value: 'vectorSize', @@ -152,4 +152,12 @@ export const QUERY_CONTEXT_COMPLETIONS: JsonCompletionRule[] = [ { value: 'force', documentation: 'Force vectorized execution' }, ], }, + { + path: '$.realtimeSegmentsMode', + completions: [ + { value: 'include', documentation: 'Query all segments, including realtime (default)' }, + { value: 'exclude', documentation: 'Skip realtime segments; query only historical' }, + { value: 'exclusive', documentation: 'Query only realtime segments' }, + ], + }, ];