diff --git a/docs/changelog/133309.yaml b/docs/changelog/133309.yaml
new file mode 100644
index 0000000000000..80fa837eb9f05
--- /dev/null
+++ b/docs/changelog/133309.yaml
@@ -0,0 +1,5 @@
+pr: 133309
+summary: support DATE_RANGE field type
+area: ES|QL
+type: enhancement
+issues: []
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BlockLoader.java b/server/src/main/java/org/elasticsearch/index/mapper/BlockLoader.java
index 5f2bd15abaa34..215b10e22c71e 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/BlockLoader.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/BlockLoader.java
@@ -496,6 +496,8 @@ interface BlockFactory {
SortedSetOrdinalsBuilder sortedSetOrdinalsBuilder(SortedSetDocValues ordinals, int count);
AggregateMetricDoubleBuilder aggregateMetricDoubleBuilder(int count);
+
+ DateRangeBuilder dateRangeBuilder(int count);
}
/**
@@ -623,4 +625,10 @@ interface AggregateMetricDoubleBuilder extends Builder {
IntBuilder count();
}
+
+ interface DateRangeBuilder extends Builder {
+ LongBuilder from();
+
+ LongBuilder to();
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java
index ee891eb6a6c1a..87c53e33523a6 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java
@@ -9,10 +9,13 @@
package org.elasticsearch.index.mapper;
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.TransportVersion;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.network.InetAddresses;
@@ -61,6 +64,8 @@ public class RangeFieldMapper extends FieldMapper {
public static final boolean DEFAULT_INCLUDE_UPPER = true;
public static final boolean DEFAULT_INCLUDE_LOWER = true;
+ public static final TransportVersion ESQL_DATE_RANGE_CREATED_VERSION = TransportVersion.fromName("esql_date_range_created_version");
+
public static class Defaults {
public static final DateFormatter DATE_FORMATTER = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER;
public static final Locale LOCALE = DateFieldMapper.DEFAULT_LOCALE;
@@ -342,6 +347,96 @@ public Query rangeQuery(
context
);
}
+
+ public static class DateRangeDocValuesLoader extends BlockDocValuesReader.DocValuesBlockLoader {
+ private final String fieldName;
+
+ public DateRangeDocValuesLoader(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ @Override
+ public Builder builder(BlockFactory factory, int expectedCount) {
+ return factory.longsFromDocValues(expectedCount);
+ }
+
+ @Override
+ public AllReader reader(LeafReaderContext context) throws IOException {
+ var docValues = context.reader().getBinaryDocValues(fieldName);
+ return new DateRangeDocValuesReader(docValues);
+ }
+ }
+
+ @Override
+ public BlockLoader blockLoader(BlockLoaderContext blContext) {
+ if (hasDocValues()) {
+ return new DateRangeDocValuesLoader(name());
+ }
+ throw new IllegalStateException("Cannot load blocks without doc values");
+ }
+ }
+
+ public static class DateRangeDocValuesReader extends BlockDocValuesReader {
+ private final BytesRef spare = new BytesRef();
+
+ private final BinaryDocValues numericDocValues;
+
+ public DateRangeDocValuesReader(BinaryDocValues numericDocValues) {
+ this.numericDocValues = numericDocValues;
+ }
+
+ private int docId = -1;
+
+ @Override
+ protected int docId() {
+ return docId;
+ }
+
+ @Override
+ public String toString() {
+ return "BlockDocValuesReader.DateRangeDocValuesReader";
+ }
+
+ @Override
+ public BlockLoader.Block read(BlockLoader.BlockFactory factory, BlockLoader.Docs docs, int offset, boolean nullsFiltered)
+ throws IOException {
+ try (BlockLoader.DateRangeBuilder builder = factory.dateRangeBuilder(docs.count() - offset)) {
+ int lastDoc = -1;
+ for (int i = offset; i < docs.count(); i++) {
+ int doc = docs.get(i);
+ if (doc < lastDoc) {
+ throw new IllegalStateException("docs within same block must be in order");
+ }
+ if (false == numericDocValues.advanceExact(doc)) {
+ builder.appendNull();
+ } else {
+ BytesRef ref = numericDocValues.binaryValue();
+ var ranges = BinaryRangeUtil.decodeLongRanges(ref);
+ for (var range : ranges) {
+ lastDoc = doc;
+ this.docId = doc;
+ builder.from().appendLong((long) range.from);
+ builder.to().appendLong((long) range.to);
+ }
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ @Override
+ public void read(int doc, BlockLoader.StoredFields storedFields, BlockLoader.Builder builder) throws IOException {
+ var blockBuilder = (BlockLoader.DateRangeBuilder) builder;
+ this.docId = doc;
+ if (false == numericDocValues.advanceExact(doc)) {
+ blockBuilder.appendNull();
+ } else {
+ var range = BinaryRangeUtil.decodeLongRanges(numericDocValues.binaryValue());
+ assert range.size() == 1 : "stored fields should only have a single range";
+ blockBuilder.from().appendLong((long) range.getFirst().from);
+ blockBuilder.to().appendLong((long) range.getFirst().to);
+ }
+ }
}
private final RangeType type;
diff --git a/server/src/main/resources/transport/definitions/referable/esql_date_range_created_version.csv b/server/src/main/resources/transport/definitions/referable/esql_date_range_created_version.csv
new file mode 100644
index 0000000000000..9a797d9f368cf
--- /dev/null
+++ b/server/src/main/resources/transport/definitions/referable/esql_date_range_created_version.csv
@@ -0,0 +1 @@
+9192000
diff --git a/server/src/main/resources/transport/upper_bounds/9.3.csv b/server/src/main/resources/transport/upper_bounds/9.3.csv
index 03e1a18b61d0a..a39e58f4029bb 100644
--- a/server/src/main/resources/transport/upper_bounds/9.3.csv
+++ b/server/src/main/resources/transport/upper_bounds/9.3.csv
@@ -1 +1 @@
-add_cross_cluster_api_key_signature,9191000
+esql_date_range_created_version,9192000
diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/TestBlock.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/TestBlock.java
index 7f4dc19d8f433..465c618fae610 100644
--- a/test/framework/src/main/java/org/elasticsearch/index/mapper/TestBlock.java
+++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/TestBlock.java
@@ -414,9 +414,15 @@ public SortedSetOrdinalBuilder appendOrd(int value) {
return new SortedSetOrdinalBuilder();
}
+ @Override
public BlockLoader.AggregateMetricDoubleBuilder aggregateMetricDoubleBuilder(int expectedSize) {
return new AggregateMetricDoubleBlockBuilder(expectedSize);
}
+
+ @Override
+ public BlockLoader.DateRangeBuilder dateRangeBuilder(int expectedSize) {
+ return new DateRangeBuilder(expectedSize);
+ }
};
}
@@ -624,4 +630,68 @@ public void close() {
}
}
+
+ public static class DateRangeBuilder implements BlockLoader.DateRangeBuilder {
+ private final LongBuilder from;
+ private final LongBuilder to;
+
+ DateRangeBuilder(int expectedSize) {
+ from = new LongBuilder(expectedSize);
+ to = new LongBuilder(expectedSize);
+ }
+
+ @Override
+ public BlockLoader.LongBuilder from() {
+ return from;
+ }
+
+ @Override
+ public BlockLoader.LongBuilder to() {
+ return to;
+ }
+
+ @Override
+ public BlockLoader.Block build() {
+ var fromBlock = from.build();
+ var toBlock = to.build();
+ assert fromBlock.size() == toBlock.size();
+ var values = new ArrayList<>(fromBlock.size());
+ for (int i = 0; i < fromBlock.size(); i++) {
+ values.add(List.of(fromBlock.values.get(i), toBlock.values.get(i)));
+ }
+ return new TestBlock(values);
+ }
+
+ @Override
+ public BlockLoader.Builder appendNull() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BlockLoader.Builder beginPositionEntry() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BlockLoader.Builder endPositionEntry() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ private static class LongBuilder extends TestBlock.Builder implements BlockLoader.LongBuilder {
+ private LongBuilder(int expectedSize) {
+ super(expectedSize);
+ }
+
+ @Override
+ public BlockLoader.LongBuilder appendLong(long value) {
+ add(value);
+ return this;
+ }
+ }
+ };
}
diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java
index 56bbf69c76e55..6a20478028d85 100644
--- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java
+++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java
@@ -21,6 +21,7 @@
import static org.elasticsearch.xpack.esql.core.type.DataType.AGGREGATE_METRIC_DOUBLE;
import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN;
import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
+import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_RANGE;
import static org.elasticsearch.xpack.esql.core.type.DataType.DENSE_VECTOR;
import static org.elasticsearch.xpack.esql.core.type.DataType.IP;
import static org.elasticsearch.xpack.esql.core.type.DataType.NULL;
@@ -85,10 +86,10 @@ public static TypeResolution isRepresentableExceptCountersDenseVectorAndAggregat
) {
return isType(
e,
- dt -> isRepresentable(dt) && dt != DENSE_VECTOR && dt != AGGREGATE_METRIC_DOUBLE,
+ dt -> isRepresentable(dt) && dt != DENSE_VECTOR && dt != AGGREGATE_METRIC_DOUBLE && dt != DATE_RANGE,
operationName,
paramOrd,
- "any type except counter types, dense_vector, or aggregate_metric_double"
+ "any type except counter types, dense_vector, aggregate_metric_double or date_range"
);
}
@@ -99,10 +100,14 @@ public static TypeResolution isRepresentableExceptCountersSpatialDenseVectorAndA
) {
return isType(
e,
- (t) -> isSpatialOrGrid(t) == false && DataType.isRepresentable(t) && t != DENSE_VECTOR && t != AGGREGATE_METRIC_DOUBLE,
+ (t) -> isSpatialOrGrid(t) == false
+ && DataType.isRepresentable(t)
+ && t != DENSE_VECTOR
+ && t != AGGREGATE_METRIC_DOUBLE
+ && t != DATE_RANGE,
operationName,
paramOrd,
- "any type except counter, spatial types, dense_vector, or aggregate_metric_double"
+ "any type except counter, spatial types, dense_vector, aggregate_metric_double or date_range"
);
}
diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java
index b7a3204c43692..7335d3965e87c 100644
--- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java
+++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java
@@ -33,6 +33,7 @@
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toMap;
+import static org.elasticsearch.index.mapper.RangeFieldMapper.ESQL_DATE_RANGE_CREATED_VERSION;
/**
* This enum represents data types the ES|QL query processing layer is able to
@@ -270,6 +271,7 @@ public enum DataType implements Writeable {
* Nanosecond precision date, stored as a 64-bit signed number.
*/
DATE_NANOS(builder().esType("date_nanos").estimatedSize(Long.BYTES).docValues().supportedOnAllNodes()),
+ DATE_RANGE(builder().esType("date_range").estimatedSize(2 * Long.BYTES).docValues().supportedOn(ESQL_DATE_RANGE_CREATED_VERSION)),
/**
* IP addresses. IPv4 address are always
* embedded
@@ -982,5 +984,6 @@ private static class DataTypesTransportVersions {
public static final TransportVersion ESQL_AGGREGATE_METRIC_DOUBLE_CREATED_VERSION = TransportVersion.fromName(
"esql_aggregate_metric_double_created_version"
);
+
}
}
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockFactory.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockFactory.java
index 53cb0a72f86fc..16217976e30d2 100644
--- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockFactory.java
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockFactory.java
@@ -481,6 +481,22 @@ public final AggregateMetricDoubleBlock newAggregateMetricDoubleBlock(
return new AggregateMetricDoubleArrayBlock(min, max, sum, count);
}
+ public DateRangeBlockBuilder newDateRangeBlockBuilder(int estimatedSize) {
+ return new DateRangeBlockBuilder(estimatedSize, this);
+ }
+
+ public DateRangeBlock newConstantDateRangeBlock(DateRangeBlockBuilder.DateRangeLiteral value, int positions) {
+ var fromBuilder = newConstantLongBlockWith(value.from(), positions);
+ var toBuilder = newConstantLongBlockWith(value.to(), positions);
+ return new DateRangeArrayBlock(fromBuilder, toBuilder);
+ }
+
+ public DateRangeBlock newDateRangeBlock(long[] fromValues, long[] toValues, int positions) {
+ var from = newLongArrayVector(fromValues, positions).asBlock();
+ var to = newLongArrayVector(toValues, positions).asBlock();
+ return new DateRangeArrayBlock(from, to);
+ }
+
/**
* Returns the maximum number of bytes that a Block should be backed by a primitive array before switching to using BigArrays.
*/
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockUtils.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockUtils.java
index fa5f139c5c6a1..65d76cb0b2df9 100644
--- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockUtils.java
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockUtils.java
@@ -224,6 +224,7 @@ public static void appendValue(Block.Builder builder, Object val, ElementType ty
case DOUBLE -> ((DoubleBlock.Builder) builder).appendDouble((Double) val);
case BOOLEAN -> ((BooleanBlock.Builder) builder).appendBoolean((Boolean) val);
case AGGREGATE_METRIC_DOUBLE -> ((AggregateMetricDoubleBlockBuilder) builder).appendLiteral((AggregateMetricDoubleLiteral) val);
+ case DATE_RANGE -> ((DateRangeBlockBuilder) builder).appendDateRange((DateRangeBlockBuilder.DateRangeLiteral) val);
default -> throw new UnsupportedOperationException("unsupported element type [" + type + "]");
}
}
@@ -253,6 +254,7 @@ private static Block constantBlock(BlockFactory blockFactory, ElementType type,
case BOOLEAN -> blockFactory.newConstantBooleanBlockWith((boolean) val, size);
case AGGREGATE_METRIC_DOUBLE -> blockFactory.newConstantAggregateMetricDoubleBlock((AggregateMetricDoubleLiteral) val, size);
case FLOAT -> blockFactory.newConstantFloatBlockWith((float) val, size);
+ case DATE_RANGE -> blockFactory.newConstantDateRangeBlock((DateRangeBlockBuilder.DateRangeLiteral) val, size);
default -> throw new UnsupportedOperationException("unsupported element type [" + type + "]");
};
}
@@ -306,6 +308,12 @@ yield new AggregateMetricDoubleLiteral(
aggBlock.countBlock().getInt(offset)
);
}
+ case DATE_RANGE -> {
+ DateRangeBlock b = (DateRangeBlock) block;
+ LongBlock fromBlock = b.getFromBlock();
+ LongBlock toBlock = b.getToBlock();
+ yield new DateRangeBlockBuilder.DateRangeLiteral(fromBlock.getLong(offset), toBlock.getLong(offset));
+ }
case UNKNOWN -> throw new IllegalArgumentException("can't read values from [" + block + "]");
};
}
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ConstantNullBlock.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ConstantNullBlock.java
index 22f4809100014..31cd60b18a227 100644
--- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ConstantNullBlock.java
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ConstantNullBlock.java
@@ -26,7 +26,8 @@ public final class ConstantNullBlock extends AbstractNonThreadSafeRefCounted
FloatBlock,
DoubleBlock,
BytesRefBlock,
- AggregateMetricDoubleBlock {
+ AggregateMetricDoubleBlock,
+ DateRangeBlock {
private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(ConstantNullBlock.class);
private final int positionCount;
@@ -118,6 +119,16 @@ public ConstantNullBlock expand() {
return this;
}
+ @Override
+ public LongBlock getFromBlock() {
+ return this;
+ }
+
+ @Override
+ public LongBlock getToBlock() {
+ return this;
+ }
+
@Override
public long ramBytesUsed() {
return BASE_RAM_BYTES_USED;
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeArrayBlock.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeArrayBlock.java
new file mode 100644
index 0000000000000..7852d33240685
--- /dev/null
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeArrayBlock.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.compute.data;
+
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.unit.ByteSizeValue;
+import org.elasticsearch.core.ReleasableIterator;
+import org.elasticsearch.core.Releasables;
+
+import java.io.IOException;
+
+public final class DateRangeArrayBlock extends AbstractNonThreadSafeRefCounted implements DateRangeBlock {
+ private final LongBlock fromBlock;
+ private final LongBlock toBlock;
+
+ public DateRangeArrayBlock(LongBlock fromBlock, LongBlock toBlock) {
+ this.fromBlock = fromBlock;
+ this.toBlock = toBlock;
+ }
+
+ @Override
+ public LongBlock getFromBlock() {
+ return fromBlock;
+ }
+
+ @Override
+ public LongBlock getToBlock() {
+ return toBlock;
+ }
+
+ @Override
+ protected void closeInternal() {
+ Releasables.close(fromBlock, toBlock);
+ }
+
+ @Override
+ public Vector asVector() {
+ return null;
+ }
+
+ @Override
+ public int getTotalValueCount() {
+ return fromBlock.getTotalValueCount() + toBlock.getTotalValueCount();
+ }
+
+ @Override
+ public int getPositionCount() {
+ return fromBlock.getPositionCount();
+ }
+
+ @Override
+ public int getFirstValueIndex(int position) {
+ return fromBlock.getFirstValueIndex(position);
+ }
+
+ @Override
+ public int getValueCount(int position) {
+ return Math.max(fromBlock.getValueCount(position), toBlock.getValueCount(position));
+ }
+
+ @Override
+ public ElementType elementType() {
+ return ElementType.DATE_RANGE;
+ }
+
+ @Override
+ public BlockFactory blockFactory() {
+ return fromBlock.blockFactory();
+ }
+
+ @Override
+ public void allowPassingToDifferentDriver() {
+ fromBlock.allowPassingToDifferentDriver();
+ toBlock.allowPassingToDifferentDriver();
+ }
+
+ @Override
+ public boolean isNull(int position) {
+ return fromBlock.isNull(position) || toBlock.isNull(position);
+ }
+
+ @Override
+ public boolean mayHaveNulls() {
+ return fromBlock.mayHaveNulls() || toBlock.mayHaveNulls();
+ }
+
+ @Override
+ public boolean areAllValuesNull() {
+ return fromBlock.areAllValuesNull() && toBlock.areAllValuesNull();
+ }
+
+ @Override
+ public boolean mayHaveMultivaluedFields() {
+ return fromBlock.mayHaveMultivaluedFields() || toBlock.mayHaveMultivaluedFields();
+ }
+
+ @Override
+ public boolean doesHaveMultivaluedFields() {
+ return fromBlock.doesHaveMultivaluedFields() || toBlock.doesHaveMultivaluedFields();
+ }
+
+ @Override
+ public DateRangeBlock filter(int... positions) {
+ DateRangeBlock result = null;
+ LongBlock newFromBlock = null;
+ LongBlock newToBlock = null;
+ try {
+ newFromBlock = fromBlock.filter(positions);
+ newToBlock = toBlock.filter(positions);
+ result = new DateRangeArrayBlock(newFromBlock, newToBlock);
+ return result;
+ } finally {
+ if (result == null) {
+ Releasables.close(newFromBlock, newToBlock);
+ }
+ }
+ }
+
+ @Override
+ public DateRangeBlock keepMask(BooleanVector mask) {
+ DateRangeBlock result = null;
+ LongBlock newFromBlock = null;
+ LongBlock newToBlock = null;
+ try {
+ newFromBlock = fromBlock.keepMask(mask);
+ newToBlock = toBlock.keepMask(mask);
+ result = new DateRangeArrayBlock(newFromBlock, newToBlock);
+ return result;
+ } finally {
+ if (result == null) {
+ Releasables.close(newFromBlock, newToBlock);
+ }
+ }
+ }
+
+ @Override
+ public ReleasableIterator extends DateRangeBlock> lookup(IntBlock positions, ByteSizeValue targetBlockSize) {
+ // TODO: support
+ throw new UnsupportedOperationException("can't lookup values from DateRangeBlock");
+ }
+
+ @Override
+ public MvOrdering mvOrdering() {
+ // TODO: determine based on sub-blocks
+ return MvOrdering.UNORDERED;
+ }
+
+ @Override
+ public DateRangeBlock expand() {
+ this.incRef();
+ return this;
+ }
+
+ @Override
+ public Block deepCopy(BlockFactory blockFactory) {
+ DateRangeBlock ret = null;
+ LongBlock newFromBlock = null;
+ LongBlock newToBlock = null;
+ try {
+ newFromBlock = fromBlock.deepCopy(blockFactory);
+ newToBlock = toBlock.deepCopy(blockFactory);
+ ret = new DateRangeArrayBlock(newFromBlock, newToBlock);
+ return ret;
+ } finally {
+ if (ret == null) {
+ Releasables.close(newFromBlock, newToBlock);
+ }
+ }
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ fromBlock.writeTo(out);
+ toBlock.writeTo(out);
+ }
+
+ public static Block readFrom(StreamInput in) throws IOException {
+ boolean success = false;
+ LongBlock from = null;
+ LongBlock to = null;
+ BlockStreamInput blockStreamInput = (BlockStreamInput) in;
+ try {
+ from = LongBlock.readFrom(blockStreamInput);
+ to = LongBlock.readFrom(blockStreamInput);
+ var result = new DateRangeArrayBlock(from, to);
+ success = true;
+ return result;
+ } finally {
+ if (success == false) {
+ Releasables.close(from, to);
+ }
+ }
+ }
+
+ @Override
+ public long ramBytesUsed() {
+ return fromBlock.ramBytesUsed() + toBlock.ramBytesUsed();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof DateRangeArrayBlock that) {
+ return DateRangeBlock.equals(this, that);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return DateRangeBlock.hash(this);
+ }
+}
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeBlock.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeBlock.java
new file mode 100644
index 0000000000000..58fb2902e4c2a
--- /dev/null
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeBlock.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.compute.data;
+
+import org.elasticsearch.common.unit.ByteSizeValue;
+import org.elasticsearch.core.ReleasableIterator;
+
+/**
+ * Block that stores aggregate_metric_double values.
+ */
+public sealed interface DateRangeBlock extends Block permits DateRangeArrayBlock, ConstantNullBlock {
+ @Override
+ DateRangeBlock filter(int... positions);
+
+ @Override
+ DateRangeBlock keepMask(BooleanVector mask);
+
+ @Override
+ ReleasableIterator extends DateRangeBlock> lookup(IntBlock positions, ByteSizeValue targetBlockSize);
+
+ @Override
+ DateRangeBlock expand();
+
+ /**
+ * Returns {@code true} if the given blocks are equal to each other, otherwise {@code false}.
+ * Two blocks are considered equal if they have the same position count, and contain the same
+ * values (including absent null values) in the same order. This definition ensures that the
+ * equals method works properly across different implementations of the AggregateMetricDoubleBlock interface.
+ */
+ static boolean equals(DateRangeBlock lhs, DateRangeBlock rhs) {
+ if (lhs == rhs) {
+ return true;
+ }
+ if (lhs.getPositionCount() != rhs.getPositionCount()) {
+ return false;
+ }
+ return LongBlock.equals(lhs.getFromBlock(), rhs.getFromBlock()) && LongBlock.equals(lhs.getToBlock(), rhs.getToBlock());
+ }
+
+ static int hash(DateRangeBlock block) {
+ final int positions = block.getPositionCount();
+ int ret = 1;
+ for (int pos = 0; pos < positions; pos++) {
+ if (block.isNull(pos)) {
+ ret = 31 * ret - 1;
+ } else {
+ final int valueCount = block.getValueCount(pos);
+ ret = 31 * ret + valueCount;
+ final int firstValueIdx = block.getFirstValueIndex(pos);
+ for (int valueIndex = 0; valueIndex < valueCount; valueIndex++) {
+ ret *= 31;
+ ret += extractHashFrom(block.getFromBlock(), firstValueIdx, valueIndex);
+ ret *= 31;
+ ret += extractHashFrom(block.getToBlock(), firstValueIdx, valueIndex);
+ }
+ }
+ }
+ return ret;
+ }
+
+ private static int extractHashFrom(LongBlock b, int firstValueIdx, int valueIndex) {
+ return b.isNull(firstValueIdx + valueIndex) ? -1 : Long.hashCode(b.getLong(firstValueIdx + valueIndex));
+ }
+
+ // TODO: those DateRange-specific sub-block getters, together with the AggregateMetricBuilder specific getters
+ // Should probably be refactored into some "composite block" interface, to avoid the need to implement all of
+ // them in ConstantArrayBlock. Something like `Block getSubBlock(int index)` would be enough.
+ // I think that was the original intent of CompositeBlock - not sure why it was abandoned.
+ LongBlock getFromBlock();
+
+ LongBlock getToBlock();
+}
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeBlockBuilder.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeBlockBuilder.java
new file mode 100644
index 0000000000000..0525d0cef4ec1
--- /dev/null
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DateRangeBlockBuilder.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.compute.data;
+
+import org.elasticsearch.TransportVersion;
+import org.elasticsearch.common.io.stream.GenericNamedWriteable;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.index.mapper.BlockLoader;
+
+import java.io.IOException;
+
+import static org.elasticsearch.index.mapper.RangeFieldMapper.ESQL_DATE_RANGE_CREATED_VERSION;
+
+public class DateRangeBlockBuilder extends AbstractBlockBuilder implements BlockLoader.DateRangeBuilder {
+
+ private LongBlockBuilder fromBuilder;
+ private LongBlockBuilder toBuilder;
+
+ public DateRangeBlockBuilder(int estimatedSize, BlockFactory blockFactory) {
+ super(blockFactory);
+ fromBuilder = null;
+ toBuilder = null;
+ try {
+ fromBuilder = new LongBlockBuilder(estimatedSize, blockFactory);
+ toBuilder = new LongBlockBuilder(estimatedSize, blockFactory);
+ } finally {
+ if (toBuilder == null) {
+ Releasables.closeWhileHandlingException(fromBuilder);
+ }
+ }
+ }
+
+ @Override
+ protected int valuesLength() {
+ throw new UnsupportedOperationException("Not available on date_range");
+ }
+
+ @Override
+ protected void growValuesArray(int newSize) {
+ throw new UnsupportedOperationException("Not available on date_range");
+ }
+
+ @Override
+ protected int elementSize() {
+ throw new UnsupportedOperationException("Not available on date_range");
+ }
+
+ @Override
+ public long estimatedBytes() {
+ return fromBuilder.estimatedBytes() + toBuilder.estimatedBytes();
+ }
+
+ @Override
+ public DateRangeBlockBuilder copyFrom(Block b, int beginInclusive, int endExclusive) {
+ Block fromBlock;
+ Block toBlock;
+ if (b.areAllValuesNull()) {
+ fromBlock = b;
+ toBlock = b;
+ } else {
+ var block = (DateRangeArrayBlock) b;
+ fromBlock = block.getFromBlock();
+ toBlock = block.getToBlock();
+ }
+ fromBuilder.copyFrom(fromBlock, beginInclusive, endExclusive);
+ toBuilder.copyFrom(toBlock, beginInclusive, endExclusive);
+ return this;
+ }
+
+ public DateRangeBlockBuilder copyFrom(DateRangeBlock block, int pos) {
+ if (block.isNull(pos)) {
+ appendNull();
+ return this;
+ }
+
+ if (block.getFromBlock().isNull(pos)) {
+ from().appendNull();
+ } else {
+ from().appendLong(block.getFromBlock().getLong(pos));
+ }
+
+ if (block.getToBlock().isNull(pos)) {
+ to().appendNull();
+ } else {
+ to().appendLong(block.getToBlock().getLong(pos));
+ }
+ return this;
+ }
+
+ @Override
+ public DateRangeBlockBuilder appendNull() {
+ fromBuilder.appendNull();
+ toBuilder.appendNull();
+ return this;
+ }
+
+ public DateRangeBlockBuilder appendDateRange(DateRangeLiteral lit) {
+ fromBuilder.appendLong(lit.from);
+ toBuilder.appendLong(lit.to);
+ return this;
+ }
+
+ @Override
+ public DateRangeBlockBuilder mvOrdering(Block.MvOrdering mvOrdering) {
+ fromBuilder.mvOrdering(mvOrdering);
+ toBuilder.mvOrdering(mvOrdering);
+ return this;
+ }
+
+ @Override
+ public DateRangeBlock build() {
+ LongBlock fromBlock = null;
+ LongBlock toBlock = null;
+ boolean success = false;
+ try {
+ finish();
+ fromBlock = fromBuilder.build();
+ toBlock = toBuilder.build();
+ var block = new DateRangeArrayBlock(fromBlock, toBlock);
+ success = true;
+ return block;
+ } finally {
+ if (success == false) {
+ Releasables.closeExpectNoException(fromBlock, toBlock);
+ }
+ }
+ }
+
+ @Override
+ protected void extraClose() {
+ Releasables.closeExpectNoException(fromBuilder, toBuilder);
+ }
+
+ @Override
+ public BlockLoader.LongBuilder from() {
+ return fromBuilder;
+ }
+
+ @Override
+ public BlockLoader.LongBuilder to() {
+ return toBuilder;
+ }
+
+ public record DateRangeLiteral(Long from, Long to) implements GenericNamedWriteable {
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
+ GenericNamedWriteable.class,
+ "DateRangeLiteral",
+ DateRangeLiteral::new
+ );
+
+ public DateRangeLiteral(StreamInput in) throws IOException {
+ this(in.readOptionalLong(), in.readOptionalLong());
+ }
+
+ @Override
+ public String getWriteableName() {
+ return "DateRangeLiteral";
+ }
+
+ @Override
+ public TransportVersion getMinimalSupportedVersion() {
+ return ESQL_DATE_RANGE_CREATED_VERSION;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeOptionalLong(from);
+ out.writeOptionalLong(to);
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ElementType.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ElementType.java
index 5202231067787..5e7cf73edbe7f 100644
--- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ElementType.java
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ElementType.java
@@ -64,7 +64,9 @@ public enum ElementType {
"AggregateMetricDouble",
BlockFactory::newAggregateMetricDoubleBlockBuilder,
AggregateMetricDoubleArrayBlock::readFrom
- );
+ ),
+
+ DATE_RANGE(11, "DateRange", BlockFactory::newDateRangeBlockBuilder, DateRangeArrayBlock::readFrom);
private static final TransportVersion ESQL_SERIALIZE_BLOCK_TYPE_CODE = TransportVersion.fromName("esql_serialize_block_type_code");
@@ -113,6 +115,8 @@ public static ElementType fromJava(Class> type) {
elementType = BOOLEAN;
} else if (type == AggregateMetricDoubleBlockBuilder.AggregateMetricDoubleLiteral.class) {
elementType = AGGREGATE_METRIC_DOUBLE;
+ } else if (type == DateRangeBlockBuilder.DateRangeLiteral.class) {
+ elementType = DATE_RANGE;
} else if (type == null || type == Void.class) {
elementType = NULL;
} else {
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/DelegatingBlockLoaderFactory.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/DelegatingBlockLoaderFactory.java
index 9e5f045b965ee..78e0ce9f9468e 100644
--- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/DelegatingBlockLoaderFactory.java
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/DelegatingBlockLoaderFactory.java
@@ -137,4 +137,9 @@ public BlockLoader.SortedSetOrdinalsBuilder sortedSetOrdinalsBuilder(SortedSetDo
public BlockLoader.AggregateMetricDoubleBuilder aggregateMetricDoubleBuilder(int count) {
return factory.newAggregateMetricDoubleBlockBuilder(count);
}
+
+ @Override
+ public BlockLoader.DateRangeBuilder dateRangeBuilder(int expectedCount) {
+ return factory.newDateRangeBlockBuilder(expectedCount);
+ }
}
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java
index f300a400e60af..db86cd1d17157 100644
--- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java
@@ -188,6 +188,7 @@ public static IntFunction