Skip to content

Commit 90c4903

Browse files
not-napoleonelasticsearchmachine
authored andcommitted
ESQL - date nanos range bug? (elastic#125345)
Fixes elastic#125439 We were incorrectly formatting nanosecond dates when building lucene queries. We had missed this in our testing because none of the CSV tests were running against Lucene. This happened because the date nanos test data includes multivalue fields. Our warning behavior for multivalue fields is inconsistent between queries run in Lucene and queries run in pure ES|QL without pushdown. Our warning tests, however, require that the specified warnings be present in all execution paths. When we first built the date nanos CSV tests, we worked around this by always using an MV function to unpack the multivalue fields. But we forgot that using an MV function prevents the entire query from being pushed down to Lucene, and thus that path wasn't being tested. In this PR, I've duplicated many of the tests to have a version that doesn't use the MV function, and uses warningRegex instead of warning. The regex version does not fail if the header is absent, so it's safe to use in both modes. Rewriting the tests this way revealed several situations in which this bug can manifest, all of which are fixed in this PR. I cannot be confidant that there aren't more paths that can trigger this bug and aren't covered by these tests, but I haven't found any yet. I've left some trace level logging that I found helpful while debugging this. --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent fc9affe commit 90c4903

File tree

8 files changed

+533
-6
lines changed

8 files changed

+533
-6
lines changed

docs/changelog/125345.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 125345
2+
summary: ESQL - date nanos range bug?
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 125439

x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec

+458
Large diffs are not rendered by default.

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

+4
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,10 @@ public enum Cap {
550550
* support date diff function on date nanos type, and mixed nanos/millis
551551
*/
552552
DATE_NANOS_DATE_DIFF(),
553+
/**
554+
* Indicates that https://github.com/elastic/elasticsearch/issues/125439 (incorrect lucene push down for date nanos) is fixed
555+
*/
556+
FIX_DATE_NANOS_LUCENE_PUSHDOWN_BUG(),
553557
/**
554558
* DATE_PARSE supports reading timezones
555559
*/

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/Range.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import org.apache.lucene.util.BytesRef;
1010
import org.elasticsearch.common.io.stream.StreamOutput;
11+
import org.elasticsearch.logging.LogManager;
12+
import org.elasticsearch.logging.Logger;
1113
import org.elasticsearch.xpack.esql.capabilities.TranslationAware;
1214
import org.elasticsearch.xpack.esql.core.expression.Expression;
1315
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
@@ -31,18 +33,23 @@
3133

3234
import static java.util.Arrays.asList;
3335
import static org.elasticsearch.xpack.esql.core.expression.Foldables.valueOf;
36+
import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
37+
import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS;
3438
import static org.elasticsearch.xpack.esql.core.type.DataType.IP;
3539
import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
3640
import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
3741
import static org.elasticsearch.xpack.esql.core.util.DateUtils.asDateTime;
3842
import static org.elasticsearch.xpack.esql.core.util.NumericUtils.unsignedLongAsNumber;
43+
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_NANOS_FORMATTER;
3944
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER;
4045
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToString;
4146
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.ipToString;
47+
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.nanoTimeToString;
4248
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.versionToString;
4349

4450
// BETWEEN or range - is a mix of gt(e) AND lt(e)
4551
public class Range extends ScalarFunction implements TranslationAware.SingleValueTranslationAware {
52+
private static final Logger logger = LogManager.getLogger(Range.class);
4653

4754
private final Expression value, lower, upper;
4855
private final boolean includeLower, includeUpper;
@@ -210,12 +217,19 @@ private RangeQuery translate(TranslatorHandler handler) {
210217
String format = null;
211218

212219
DataType dataType = value.dataType();
213-
if (DataType.isDateTime(dataType) && DataType.isDateTime(lower.dataType()) && DataType.isDateTime(upper.dataType())) {
220+
logger.trace("Translating Range into lucene query. dataType is [{}] upper is [{}] lower is [{}]", dataType, lower, upper);
221+
if (dataType == DataType.DATETIME && lower.dataType() == DATETIME && upper.dataType() == DATETIME) {
214222
l = dateTimeToString((Long) l);
215223
u = dateTimeToString((Long) u);
216224
format = DEFAULT_DATE_TIME_FORMATTER.pattern();
217225
}
218226

227+
if (dataType == DATE_NANOS && lower.dataType() == DATE_NANOS && upper.dataType() == DATE_NANOS) {
228+
l = nanoTimeToString((Long) l);
229+
u = nanoTimeToString((Long) u);
230+
format = DEFAULT_DATE_NANOS_FORMATTER.pattern();
231+
}
232+
219233
if (dataType == IP) {
220234
if (l instanceof BytesRef bytesRef) {
221235
l = ipToString(bytesRef);

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/EsqlBinaryComparison.java

+30-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import org.elasticsearch.common.io.stream.Writeable;
1414
import org.elasticsearch.common.time.DateFormatter;
1515
import org.elasticsearch.compute.operator.EvalOperator;
16+
import org.elasticsearch.logging.LogManager;
17+
import org.elasticsearch.logging.Logger;
1618
import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
1719
import org.elasticsearch.xpack.esql.capabilities.TranslationAware;
1820
import org.elasticsearch.xpack.esql.core.QlIllegalArgumentException;
@@ -50,14 +52,17 @@
5052

5153
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
5254
import static org.elasticsearch.xpack.esql.core.expression.Foldables.valueOf;
55+
import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
56+
import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS;
5357
import static org.elasticsearch.xpack.esql.core.type.DataType.IP;
5458
import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
5559
import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
5660
import static org.elasticsearch.xpack.esql.core.util.NumericUtils.unsignedLongAsNumber;
61+
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_NANOS_FORMATTER;
5762
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER;
5863
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.HOUR_MINUTE_SECOND;
5964
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.commonType;
60-
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToString;
65+
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateWithTypeToString;
6166
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.ipToString;
6267
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.versionToString;
6368

@@ -66,6 +71,8 @@ public abstract class EsqlBinaryComparison extends BinaryComparison
6671
EvaluatorMapper,
6772
TranslationAware.SingleValueTranslationAware {
6873

74+
private static final Logger logger = LogManager.getLogger(EsqlBinaryComparison.class);
75+
6976
private final Map<DataType, EsqlArithmeticOperation.BinaryEvaluator> evaluatorMap;
7077

7178
private final BinaryComparisonOperation functionType;
@@ -375,14 +382,29 @@ private Query translate(TranslatorHandler handler) {
375382
String format = null;
376383
boolean isDateLiteralComparison = false;
377384

385+
logger.trace(
386+
"Translating binary comparison with right: [{}<{}>], left: [{}<{}>], attribute: [{}<{}>]",
387+
right(),
388+
right().dataType(),
389+
left(),
390+
left().dataType(),
391+
attribute,
392+
attribute.dataType()
393+
);
394+
378395
// TODO: This type coersion layer is copied directly from the QL counterpart code. It's probably not necessary or desireable
379396
// in the ESQL version. We should instead do the type conversions using our casting functions.
380397
// for a date constant comparison, we need to use a format for the date, to make sure that the format is the same
381398
// no matter the timezone provided by the user
382399
if (value instanceof ZonedDateTime || value instanceof OffsetTime) {
383400
DateFormatter formatter;
384401
if (value instanceof ZonedDateTime) {
385-
formatter = DEFAULT_DATE_TIME_FORMATTER;
402+
// NB: we check the data type of right here because value is the RHS value
403+
formatter = switch (right().dataType()) {
404+
case DATETIME -> DEFAULT_DATE_TIME_FORMATTER;
405+
case DATE_NANOS -> DEFAULT_DATE_NANOS_FORMATTER;
406+
default -> throw new EsqlIllegalArgumentException("Found date value in non-date type comparison");
407+
};
386408
// RangeQueryBuilder accepts an Object as its parameter, but it will call .toString() on the ZonedDateTime instance
387409
// which can have a slightly different format depending on the ZoneId used to create the ZonedDateTime
388410
// Since RangeQueryBuilder can handle date as String as well, we'll format it as String and provide the format as well.
@@ -408,10 +430,14 @@ private Query translate(TranslatorHandler handler) {
408430
}
409431

410432
ZoneId zoneId = null;
411-
if (DataType.isDateTime(attribute.dataType())) {
433+
if (attribute.dataType() == DATETIME) {
412434
zoneId = zoneId();
413-
value = dateTimeToString((Long) value);
435+
value = dateWithTypeToString((Long) value, right().dataType());
414436
format = DEFAULT_DATE_TIME_FORMATTER.pattern();
437+
} else if (attribute.dataType() == DATE_NANOS) {
438+
zoneId = zoneId();
439+
value = dateWithTypeToString((Long) value, right().dataType());
440+
format = DEFAULT_DATE_NANOS_FORMATTER.pattern();
415441
}
416442
if (this instanceof GreaterThan) {
417443
return new RangeQuery(source(), name, value, false, null, false, format, zoneId);

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/In.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.elasticsearch.compute.data.Block;
1616
import org.elasticsearch.compute.data.Vector;
1717
import org.elasticsearch.compute.operator.EvalOperator;
18+
import org.elasticsearch.logging.LogManager;
19+
import org.elasticsearch.logging.Logger;
1820
import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
1921
import org.elasticsearch.xpack.esql.capabilities.TranslationAware;
2022
import org.elasticsearch.xpack.esql.core.expression.Expression;
@@ -115,6 +117,7 @@
115117
*/
116118
public class In extends EsqlScalarFunction implements TranslationAware.SingleValueTranslationAware {
117119
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "In", In::new);
120+
private static final Logger logger = LogManager.getLogger(In.class);
118121

119122
private final Expression value;
120123
private final List<Expression> list;
@@ -468,6 +471,7 @@ public Query asQuery(TranslatorHandler handler) {
468471
}
469472

470473
private Query translate(TranslatorHandler handler) {
474+
logger.trace("Attempting to generate lucene query for IN expression");
471475
TypedAttribute attribute = LucenePushdownPredicates.checkIsPushableAttribute(value());
472476

473477
Set<Object> terms = new LinkedHashSet<>();
@@ -501,7 +505,7 @@ private Query translate(TranslatorHandler handler) {
501505
}
502506

503507
private static boolean needsTypeSpecificValueHandling(DataType fieldType) {
504-
return DataType.isDateTime(fieldType) || fieldType == IP || fieldType == VERSION || fieldType == UNSIGNED_LONG;
508+
return fieldType == DATETIME || fieldType == DATE_NANOS || fieldType == IP || fieldType == VERSION || fieldType == UNSIGNED_LONG;
505509
}
506510

507511
private static Query or(Source source, Query left, Query right) {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import org.elasticsearch.index.query.QueryBuilders;
4141
import org.elasticsearch.index.query.SearchExecutionContext;
4242
import org.elasticsearch.index.search.NestedHelper;
43+
import org.elasticsearch.logging.LogManager;
44+
import org.elasticsearch.logging.Logger;
4345
import org.elasticsearch.search.fetch.StoredFieldsSpec;
4446
import org.elasticsearch.search.internal.AliasFilter;
4547
import org.elasticsearch.search.lookup.SearchLookup;
@@ -76,6 +78,8 @@
7678
import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.NONE;
7779

7880
public class EsPhysicalOperationProviders extends AbstractPhysicalOperationProviders {
81+
private static final Logger logger = LogManager.getLogger(EsPhysicalOperationProviders.class);
82+
7983
/**
8084
* Context of each shard we're operating against.
8185
*/
@@ -180,6 +184,7 @@ public Function<org.elasticsearch.compute.lucene.ShardContext, Query> querySuppl
180184
@Override
181185
public final PhysicalOperation sourcePhysicalOperation(EsQueryExec esQueryExec, LocalExecutionPlannerContext context) {
182186
final LuceneOperator.Factory luceneFactory;
187+
logger.trace("Query Exec is {}", esQueryExec);
183188

184189
List<Sort> sorts = esQueryExec.sorts();
185190
assert esQueryExec.estimatedRowSize() != null : "estimated row size not initialized";

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java

+10
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,16 @@ public static long dateNanosToLong(String dateNano, DateFormatter formatter) {
557557
return DateUtils.toLong(parsed);
558558
}
559559

560+
public static String dateWithTypeToString(long dateTime, DataType type) {
561+
if (type == DATETIME) {
562+
return dateTimeToString(dateTime);
563+
}
564+
if (type == DATE_NANOS) {
565+
return nanoTimeToString(dateTime);
566+
}
567+
throw new IllegalArgumentException("Unsupported data type [" + type + "]");
568+
}
569+
560570
public static String dateTimeToString(long dateTime) {
561571
return DEFAULT_DATE_TIME_FORMATTER.formatMillis(dateTime);
562572
}

0 commit comments

Comments
 (0)