Skip to content

Commit 61b8e0b

Browse files
committed
[Rollup] Validate timezone in range queries (#30338)
When validating the search request, we make sure any date_histogram aggregations have timezones that match the jobs. But we didn't do any such validation on range queries. While it wouldn't produce incorrect results, it would be confusing to the user as no documents would match the aggregation (because we add a filter clause on the timezone for the agg). Now the user gets an exception up front, and some helpful text about why the range query didnt match, and which timezones are acceptable
1 parent 0f229b3 commit 61b8e0b

File tree

3 files changed

+65
-25
lines changed

3 files changed

+65
-25
lines changed

docs/CHANGELOG.asciidoc

+5
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ analysis module. ({pull}30397[#30397])
105105

106106
{ref-64}/breaking_64_api_changes.html#copy-source-settings-on-resize[Allow copying source settings on index resize operations] ({pull}30255[#30255])
107107

108+
Rollup::
109+
* Validate timezone in range queries to ensure they match the selected job when
110+
searching ({pull}30338[#30338])
111+
108112
[float]
109113
=== Bug Fixes
110114

@@ -221,6 +225,7 @@ Engine::
221225

222226
Ingest::
223227
* Don't allow referencing the pattern bank name in the pattern bank {pull}29295[#29295] (issue: {issue}29257[#29257])
228+
224229
[float]
225230
=== Regressions
226231
Fail snapshot operations early when creating or deleting a snapshot on a repository that has been

x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@
5656
import org.elasticsearch.xpack.core.rollup.RollupField;
5757
import org.elasticsearch.xpack.core.rollup.action.RollupJobCaps;
5858
import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction;
59+
import org.elasticsearch.xpack.core.rollup.job.DateHistoGroupConfig;
5960
import org.elasticsearch.xpack.rollup.Rollup;
6061
import org.elasticsearch.xpack.rollup.RollupJobIdentifierUtils;
6162
import org.elasticsearch.xpack.rollup.RollupRequestTranslator;
6263
import org.elasticsearch.xpack.rollup.RollupResponseTranslator;
64+
import org.joda.time.DateTimeZone;
6365

6466
import java.io.IOException;
6567
import java.util.ArrayList;
@@ -282,6 +284,7 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set<RollupJobCaps> jobCap
282284
? ((RangeQueryBuilder)builder).fieldName()
283285
: ((TermQueryBuilder)builder).fieldName();
284286

287+
List<String> incorrectTimeZones = new ArrayList<>();
285288
List<String> rewrittenFieldName = jobCaps.stream()
286289
// We only care about job caps that have the query's target field
287290
.filter(caps -> caps.getFieldCaps().keySet().contains(fieldName))
@@ -291,6 +294,24 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set<RollupJobCaps> jobCap
291294
// For now, we only allow filtering on grouping fields
292295
.filter(agg -> {
293296
String type = (String)agg.get(RollupField.AGG);
297+
298+
// If the cap is for a date_histo, and the query is a range, the timezones need to match
299+
if (type.equals(DateHistogramAggregationBuilder.NAME) && builder instanceof RangeQueryBuilder) {
300+
String timeZone = ((RangeQueryBuilder)builder).timeZone();
301+
302+
// Many range queries don't include the timezone because the default is UTC, but the query
303+
// builder will return null so we need to set it here
304+
if (timeZone == null) {
305+
timeZone = DateTimeZone.UTC.toString();
306+
}
307+
boolean matchingTZ = ((String)agg.get(DateHistoGroupConfig.TIME_ZONE.getPreferredName()))
308+
.equalsIgnoreCase(timeZone);
309+
if (matchingTZ == false) {
310+
incorrectTimeZones.add((String)agg.get(DateHistoGroupConfig.TIME_ZONE.getPreferredName()));
311+
}
312+
return matchingTZ;
313+
}
314+
// Otherwise just make sure it's one of the three groups
294315
return type.equals(TermsAggregationBuilder.NAME)
295316
|| type.equals(DateHistogramAggregationBuilder.NAME)
296317
|| type.equals(HistogramAggregationBuilder.NAME);
@@ -309,8 +330,14 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set<RollupJobCaps> jobCap
309330
.collect(ArrayList::new, List::addAll, List::addAll);
310331

311332
if (rewrittenFieldName.isEmpty()) {
312-
throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName()
333+
if (incorrectTimeZones.isEmpty()) {
334+
throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName()
313335
+ "] query is not available in selected rollup indices, cannot query.");
336+
} else {
337+
throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName()
338+
+ "] query was found in rollup indices, but requested timezone is not compatible. Options include: "
339+
+ incorrectTimeZones);
340+
}
314341
}
315342

316343
if (rewrittenFieldName.size() > 1) {

x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java

+32-24
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,38 @@ public void testRange() {
121121
RollupJobCaps cap = new RollupJobCaps(job.build());
122122
Set<RollupJobCaps> caps = new HashSet<>();
123123
caps.add(cap);
124-
QueryBuilder rewritten = null;
125-
try {
126-
rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1), caps);
127-
} catch (Exception e) {
128-
fail("Should not have thrown exception when parsing query.");
129-
}
124+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("UTC"), caps);
130125
assertThat(rewritten, instanceOf(RangeQueryBuilder.class));
131126
assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp"));
132127
}
133128

129+
public void testRangeNullTimeZone() {
130+
RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo");
131+
GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig();
132+
group.setDateHisto(new DateHistoGroupConfig.Builder().setField("foo").setInterval(new DateHistogramInterval("1h")).build());
133+
job.setGroupConfig(group.build());
134+
RollupJobCaps cap = new RollupJobCaps(job.build());
135+
Set<RollupJobCaps> caps = new HashSet<>();
136+
caps.add(cap);
137+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1), caps);
138+
assertThat(rewritten, instanceOf(RangeQueryBuilder.class));
139+
assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp"));
140+
}
141+
142+
public void testRangeWrongTZ() {
143+
RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo");
144+
GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig();
145+
group.setDateHisto(new DateHistoGroupConfig.Builder().setField("foo").setInterval(new DateHistogramInterval("1h")).build());
146+
job.setGroupConfig(group.build());
147+
RollupJobCaps cap = new RollupJobCaps(job.build());
148+
Set<RollupJobCaps> caps = new HashSet<>();
149+
caps.add(cap);
150+
Exception e = expectThrows(IllegalArgumentException.class,
151+
() -> TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("EST"), caps));
152+
assertThat(e.getMessage(), equalTo("Field [foo] in [range] query was found in rollup indices, but requested timezone is not " +
153+
"compatible. Options include: [UTC]"));
154+
}
155+
134156
public void testTerms() {
135157
RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo");
136158
GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig();
@@ -139,12 +161,7 @@ public void testTerms() {
139161
RollupJobCaps cap = new RollupJobCaps(job.build());
140162
Set<RollupJobCaps> caps = new HashSet<>();
141163
caps.add(cap);
142-
QueryBuilder rewritten = null;
143-
try {
144-
rewritten = TransportRollupSearchAction.rewriteQuery(new TermQueryBuilder("foo", "bar"), caps);
145-
} catch (Exception e) {
146-
fail("Should not have thrown exception when parsing query.");
147-
}
164+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new TermQueryBuilder("foo", "bar"), caps);
148165
assertThat(rewritten, instanceOf(TermQueryBuilder.class));
149166
assertThat(((TermQueryBuilder)rewritten).fieldName(), equalTo("foo.terms.value"));
150167
}
@@ -160,12 +177,7 @@ public void testCompounds() {
160177

161178
BoolQueryBuilder builder = new BoolQueryBuilder();
162179
builder.must(getQueryBuilder(2));
163-
QueryBuilder rewritten = null;
164-
try {
165-
rewritten = TransportRollupSearchAction.rewriteQuery(builder, caps);
166-
} catch (Exception e) {
167-
fail("Should not have thrown exception when parsing query.");
168-
}
180+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(builder, caps);
169181
assertThat(rewritten, instanceOf(BoolQueryBuilder.class));
170182
assertThat(((BoolQueryBuilder)rewritten).must().size(), equalTo(1));
171183
}
@@ -178,12 +190,8 @@ public void testMatchAll() {
178190
RollupJobCaps cap = new RollupJobCaps(job.build());
179191
Set<RollupJobCaps> caps = new HashSet<>();
180192
caps.add(cap);
181-
try {
182-
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new MatchAllQueryBuilder(), caps);
183-
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
184-
} catch (Exception e) {
185-
fail("Should not have thrown exception when parsing query.");
186-
}
193+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new MatchAllQueryBuilder(), caps);
194+
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
187195
}
188196

189197
public void testAmbiguousResolution() {

0 commit comments

Comments
 (0)