|
36 | 36 | import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; |
37 | 37 | import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; |
38 | 38 | import org.elasticsearch.xpack.esql.core.expression.FieldAttribute.FieldName; |
39 | | -import org.elasticsearch.xpack.esql.core.util.Holder; |
40 | 39 |
|
41 | 40 | import java.io.IOException; |
42 | 41 | import java.util.LinkedHashMap; |
@@ -232,74 +231,99 @@ public long count(FieldName field, BytesRef value) { |
232 | 231 |
|
233 | 232 | @Override |
234 | 233 | public Object min(FieldName field) { |
235 | | - var stat = cache.computeIfAbsent(field.string(), this::makeFieldStats); |
236 | | - // Consolidate min for indexed date fields only, skip the others and mixed-typed fields. |
237 | | - MappedFieldType fieldType = stat.config.fieldType; |
238 | | - boolean hasDocValueSkipper = fieldType instanceof DateFieldType dft && dft.hasDocValuesSkipper(); |
239 | | - if (fieldType == null |
240 | | - || (hasDocValueSkipper == false && stat.config.indexed == false) |
241 | | - || fieldType instanceof DateFieldType == false) { |
| 234 | + final var stat = cache.computeIfAbsent(field.string(), this::makeFieldStats); |
| 235 | + final MappedFieldType fieldType = stat.config.fieldType; |
| 236 | + if (fieldType instanceof DateFieldType == false) { |
242 | 237 | return null; |
243 | 238 | } |
244 | 239 | if (stat.min == null) { |
245 | | - var min = new long[] { Long.MAX_VALUE }; |
246 | | - Holder<Boolean> foundMinValue = new Holder<>(false); |
247 | | - doWithContexts(r -> { |
248 | | - long minValue = Long.MAX_VALUE; |
249 | | - if (hasDocValueSkipper) { |
250 | | - minValue = DocValuesSkipper.globalMinValue(new IndexSearcher(r), field.string()); |
251 | | - } else { |
252 | | - byte[] minPackedValue = PointValues.getMinPackedValue(r, field.string()); |
253 | | - if (minPackedValue != null && minPackedValue.length == 8) { |
254 | | - minValue = NumericUtils.sortableBytesToLong(minPackedValue, 0); |
| 240 | + Long result = null; |
| 241 | + try { |
| 242 | + for (final SearchExecutionContext context : contexts) { |
| 243 | + if (context.isFieldMapped(field.string()) == false) { |
| 244 | + continue; |
| 245 | + } |
| 246 | + final MappedFieldType ctxFieldType = context.getFieldType(field.string()); |
| 247 | + boolean ctxHasSkipper = ctxFieldType.indexType().hasDocValuesSkipper(); |
| 248 | + for (final LeafReaderContext leafContext : context.searcher().getLeafContexts()) { |
| 249 | + final Long minValue = ctxHasSkipper |
| 250 | + ? docValuesSkipperMinValue(leafContext, field.string()) |
| 251 | + : pointMinValue(leafContext, field.string()); |
| 252 | + result = nullableMin(result, minValue); |
255 | 253 | } |
256 | 254 | } |
257 | | - if (minValue <= min[0]) { |
258 | | - min[0] = minValue; |
259 | | - foundMinValue.set(true); |
260 | | - } |
261 | | - return true; |
262 | | - }, true); |
263 | | - stat.min = foundMinValue.get() ? min[0] : null; |
| 255 | + } catch (IOException ex) { |
| 256 | + throw new EsqlIllegalArgumentException("Cannot access data storage", ex); |
| 257 | + } |
| 258 | + stat.min = result; |
264 | 259 | } |
265 | 260 | return stat.min; |
266 | 261 | } |
267 | 262 |
|
268 | 263 | @Override |
269 | 264 | public Object max(FieldName field) { |
270 | | - var stat = cache.computeIfAbsent(field.string(), this::makeFieldStats); |
271 | | - // Consolidate max for indexed date fields only, skip the others and mixed-typed fields. |
272 | | - MappedFieldType fieldType = stat.config.fieldType; |
273 | | - boolean hasDocValueSkipper = fieldType instanceof DateFieldType dft && dft.hasDocValuesSkipper(); |
274 | | - if (fieldType == null |
275 | | - || (hasDocValueSkipper == false && stat.config.indexed == false) |
276 | | - || fieldType instanceof DateFieldType == false) { |
| 265 | + final var stat = cache.computeIfAbsent(field.string(), this::makeFieldStats); |
| 266 | + final MappedFieldType fieldType = stat.config.fieldType; |
| 267 | + if (fieldType instanceof DateFieldType == false) { |
277 | 268 | return null; |
278 | 269 | } |
279 | 270 | if (stat.max == null) { |
280 | | - var max = new long[] { Long.MIN_VALUE }; |
281 | | - Holder<Boolean> foundMaxValue = new Holder<>(false); |
282 | | - doWithContexts(r -> { |
283 | | - long maxValue = Long.MIN_VALUE; |
284 | | - if (hasDocValueSkipper) { |
285 | | - maxValue = DocValuesSkipper.globalMaxValue(new IndexSearcher(r), field.string()); |
286 | | - } else { |
287 | | - byte[] maxPackedValue = PointValues.getMaxPackedValue(r, field.string()); |
288 | | - if (maxPackedValue != null && maxPackedValue.length == 8) { |
289 | | - maxValue = NumericUtils.sortableBytesToLong(maxPackedValue, 0); |
| 271 | + Long result = null; |
| 272 | + try { |
| 273 | + for (final SearchExecutionContext context : contexts) { |
| 274 | + if (context.isFieldMapped(field.string()) == false) { |
| 275 | + continue; |
| 276 | + } |
| 277 | + final MappedFieldType ctxFieldType = context.getFieldType(field.string()); |
| 278 | + boolean ctxHasSkipper = ctxFieldType.indexType().hasDocValuesSkipper(); |
| 279 | + for (final LeafReaderContext leafContext : context.searcher().getLeafContexts()) { |
| 280 | + final Long maxValue = ctxHasSkipper |
| 281 | + ? docValuesSkipperMaxValue(leafContext, field.string()) |
| 282 | + : pointMaxValue(leafContext, field.string()); |
| 283 | + result = nullableMax(result, maxValue); |
290 | 284 | } |
291 | 285 | } |
292 | | - if (maxValue >= max[0]) { |
293 | | - max[0] = maxValue; |
294 | | - foundMaxValue.set(true); |
295 | | - } |
296 | | - return true; |
297 | | - }, true); |
298 | | - stat.max = foundMaxValue.get() ? max[0] : null; |
| 286 | + } catch (IOException ex) { |
| 287 | + throw new EsqlIllegalArgumentException("Cannot access data storage", ex); |
| 288 | + } |
| 289 | + stat.max = result; |
299 | 290 | } |
300 | 291 | return stat.max; |
301 | 292 | } |
302 | 293 |
|
| 294 | + private static Long nullableMin(final Long a, final Long b) { |
| 295 | + if (a == null) return b; |
| 296 | + if (b == null) return a; |
| 297 | + return Math.min(a, b); |
| 298 | + } |
| 299 | + |
| 300 | + private static Long nullableMax(final Long a, final Long b) { |
| 301 | + if (a == null) return b; |
| 302 | + if (b == null) return a; |
| 303 | + return Math.max(a, b); |
| 304 | + } |
| 305 | + |
| 306 | + // TODO: replace these helpers with a unified Lucene min/max API once https://github.com/apache/lucene/issues/15740 is resolved |
| 307 | + private static Long docValuesSkipperMinValue(final LeafReaderContext leafContext, final String field) throws IOException { |
| 308 | + long value = DocValuesSkipper.globalMinValue(new IndexSearcher(leafContext.reader()), field); |
| 309 | + return (value == Long.MAX_VALUE || value == Long.MIN_VALUE) ? null : value; |
| 310 | + } |
| 311 | + |
| 312 | + private static Long docValuesSkipperMaxValue(final LeafReaderContext leafContext, final String field) throws IOException { |
| 313 | + long value = DocValuesSkipper.globalMaxValue(new IndexSearcher(leafContext.reader()), field); |
| 314 | + return (value == Long.MAX_VALUE || value == Long.MIN_VALUE) ? null : value; |
| 315 | + } |
| 316 | + |
| 317 | + private static Long pointMinValue(final LeafReaderContext leafContext, final String field) throws IOException { |
| 318 | + final byte[] minPackedValue = PointValues.getMinPackedValue(leafContext.reader(), field); |
| 319 | + return (minPackedValue != null && minPackedValue.length == 8) ? NumericUtils.sortableBytesToLong(minPackedValue, 0) : null; |
| 320 | + } |
| 321 | + |
| 322 | + private static Long pointMaxValue(final LeafReaderContext leafContext, final String field) throws IOException { |
| 323 | + final byte[] maxPackedValue = PointValues.getMaxPackedValue(leafContext.reader(), field); |
| 324 | + return (maxPackedValue != null && maxPackedValue.length == 8) ? NumericUtils.sortableBytesToLong(maxPackedValue, 0) : null; |
| 325 | + } |
| 326 | + |
303 | 327 | @Override |
304 | 328 | public boolean isSingleValue(FieldName field) { |
305 | 329 | String fieldName = field.string(); |
|
0 commit comments