Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
05a4561
abstract function
not-napoleon May 7, 2025
5f8e350
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon May 8, 2025
0c0b5bd
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Jun 12, 2025
9d79b2f
stub in field attribute
not-napoleon Jun 17, 2025
2f9e7d2
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Jul 2, 2025
c11fa12
begin wiring up the time series types
not-napoleon Jul 7, 2025
28a3aea
stub in abstract methods
not-napoleon Jul 7, 2025
001047d
explicit metadata type for some benchmarks
not-napoleon Jul 8, 2025
3397960
explicit metadata type
not-napoleon Jul 8, 2025
038f743
stuff
not-napoleon Jul 10, 2025
d726c75
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Jul 16, 2025
37849ed
explicit default for a bunch of constructors
not-napoleon Jul 16, 2025
cf48733
fix the other constructor
not-napoleon Jul 16, 2025
f246efa
multi-type fields
not-napoleon Jul 16, 2025
3703533
text-field subclass
not-napoleon Jul 17, 2025
47df3b4
keyword fields
not-napoleon Jul 17, 2025
5250dce
date fields
not-napoleon Jul 17, 2025
c7dfe13
equals and hashcode
not-napoleon Jul 17, 2025
ec5dfd4
fix a couple of tests
not-napoleon Jul 17, 2025
2f638e7
Remove unnecessary check
not-napoleon Jul 18, 2025
1a16702
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Jul 21, 2025
e7b7e7c
revert unexpected docs changes
not-napoleon Jul 22, 2025
1b1afe5
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Jul 24, 2025
89c2054
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Aug 14, 2025
3b4dc17
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Aug 15, 2025
9ab5c0f
Merge branch 'main' into esql-dimension-aware-attributes
not-napoleon Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ public void setup() {
var fields = 10_000;
var mapping = LinkedHashMap.<String, EsField>newLinkedHashMap(fields);
for (int i = 0; i < fields; i++) {
mapping.put("field" + i, new EsField("field-" + i, TEXT, emptyMap(), true));
// We're creating a standard index, so none of these fields should be marked as dimensions.
mapping.put("field" + i, new EsField("field-" + i, TEXT, emptyMap(), true, EsField.TimeSeriesFieldType.NONE));
}

var esIndex = new EsIndex("test", mapping, Map.of("test", IndexMode.STANDARD));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) {
FieldAttribute timestamp = new FieldAttribute(
Source.EMPTY,
"timestamp",
new EsField("timestamp", DataType.DATETIME, Map.of(), true)
new EsField("timestamp", DataType.DATETIME, Map.of(), true, EsField.TimeSeriesFieldType.NONE)
);
yield EvalMapper.toEvaluator(
FOLD_CONTEXT,
Expand Down Expand Up @@ -321,19 +321,35 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) {
}

private static FieldAttribute longField() {
return new FieldAttribute(Source.EMPTY, "long", new EsField("long", DataType.LONG, Map.of(), true));
return new FieldAttribute(
Source.EMPTY,
"long",
new EsField("long", DataType.LONG, Map.of(), true, EsField.TimeSeriesFieldType.NONE)
);
}

private static FieldAttribute doubleField() {
return new FieldAttribute(Source.EMPTY, "double", new EsField("double", DataType.DOUBLE, Map.of(), true));
return new FieldAttribute(
Source.EMPTY,
"double",
new EsField("double", DataType.DOUBLE, Map.of(), true, EsField.TimeSeriesFieldType.NONE)
);
}

private static FieldAttribute intField() {
return new FieldAttribute(Source.EMPTY, "int", new EsField("int", DataType.INTEGER, Map.of(), true));
return new FieldAttribute(
Source.EMPTY,
"int",
new EsField("int", DataType.INTEGER, Map.of(), true, EsField.TimeSeriesFieldType.NONE)
);
}

private static FieldAttribute keywordField() {
return new FieldAttribute(Source.EMPTY, "keyword", new EsField("keyword", DataType.KEYWORD, Map.of(), true));
return new FieldAttribute(
Source.EMPTY,
"keyword",
new EsField("keyword", DataType.KEYWORD, Map.of(), true, EsField.TimeSeriesFieldType.NONE)
);
}

private static Configuration configuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,9 @@ public static boolean dataTypeEquals(List<Attribute> left, List<Attribute> right
}
return true;
}

/**
* @return true if the attribute represents a TSDB dimension type
*/
public abstract boolean isDimension();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should actually be ternary, resp. return Boolean (capital B) so that attributes can return null to say "I don't know if I refer to a dimension" rather than claiming not to.

Otherwise 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the behavior associated with the null version? The intention here is that we want to only allow filtering by dimensions, and enable automatic grouping by all dimensions. If a field returns null here, we would then need to decide if "null" behaved as "this is a dimension" or "this is not a dimension" for these cases.

Unless you're proposing that we fail (or null-with-warning) any query where there is ambiguity about the dimensions? I'm not sure I agree with that.

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ protected String label() {
return "e";
}

@Override
public boolean isDimension() {
return false;
}

@Override
public boolean resolved() {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ protected String label() {
return "f";
}

@Override
public boolean isDimension() {
return field.getTimeSeriesFieldType() == EsField.TimeSeriesFieldType.DIMENSION;
}

public EsField field() {
return field;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ protected String label() {
return "m";
}

@Override
public boolean isDimension() {
return false;
}

@Override
protected NodeInfo<? extends Expression> info() {
return NodeInfo.create(this, MetadataAttribute::new, name(), dataType(), nullable(), id(), synthetic(), searchable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,9 @@ protected NodeInfo<ReferenceAttribute> info() {
protected String label() {
return "r";
}

@Override
public boolean isDimension() {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: it might get tricky when dealing with renames; if you have a FROM idx | RENAME dimension_field AS foo, then the reference attribute for foo will claim not to be a dimension - so downstream plan nodes may need to resolve aliases to correctly determine if they're looking at a dimension or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding from @kkrik-es is that, at least for the initial version, that's an acceptable restriction. I think ReferenceAttributes are used for the output of EVAL and similar commands, and at least for the initial version of this we do not want to treat those as dimensions.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ protected String label() {
return UNRESOLVED_PREFIX;
}

@Override
public boolean isDimension() {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: maybe this should throw UOE, because we can't determine this on an unresolved attr.

}

@Override
public String nodeString() {
return toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
*/
public class DateEsField extends EsField {

public static DateEsField dateEsField(String name, Map<String, EsField> properties, boolean hasDocValues) {
return new DateEsField(name, DataType.DATETIME, properties, hasDocValues, TimeSeriesFieldType.UNKNOWN);
public static DateEsField dateEsField(String name, Map<String, EsField> properties, boolean hasDocValues, TimeSeriesFieldType tsType) {
return new DateEsField(name, DataType.DATETIME, properties, hasDocValues, tsType);
}

private DateEsField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.elasticsearch.xpack.esql.core.type;

import org.elasticsearch.TransportVersions;
import org.elasticsearch.action.fieldcaps.IndexFieldCapabilities;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
Expand All @@ -31,10 +32,36 @@ public class EsField implements Writeable {
* roles within the ESQL query processing pipeline.
*/
public enum TimeSeriesFieldType implements Writeable {
UNKNOWN(0),
NONE(1),
METRIC(2),
DIMENSION(3);
UNKNOWN(0) {
@Override
public TimeSeriesFieldType merge(TimeSeriesFieldType other) {
return other;
}
},
NONE(1) {
@Override
public TimeSeriesFieldType merge(TimeSeriesFieldType other) {
return other;
}
},
METRIC(2) {
@Override
public TimeSeriesFieldType merge(TimeSeriesFieldType other) {
if (other != DIMENSION) {
return METRIC;
}
throw new IllegalStateException("Time Series Metadata conflict. Cannot merge [" + other + "] with [METRIC].");
}
},
DIMENSION(3) {
@Override
public TimeSeriesFieldType merge(TimeSeriesFieldType other) {
if (other != METRIC) {
return DIMENSION;
}
throw new IllegalStateException("Time Series Metadata conflict. Cannot merge [" + other + "] with [DIMENSION].");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm, we do not allow mixing dimensions and metrics, but we do allow NONE with dimensions. I think the GAUGE type is optional - should we treat it similarly to NONE?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main concern here is allowing for a migration from a label to a dimension. Labels would have neither a metric nor a dimension type, and would show up as NONE here, but if a later mapping marks them as dimensions, we should pick that up.

}
};

private final int id;

Expand All @@ -57,6 +84,19 @@ public static TimeSeriesFieldType readFromStream(StreamInput in) throws IOExcept
default -> throw new IOException("Unexpected value for TimeSeriesFieldType: " + id);
};
}

public static TimeSeriesFieldType fromIndexFieldCapabilities(IndexFieldCapabilities capabilities) {
if (capabilities.isDimension()) {
assert capabilities.metricType() == null;
return DIMENSION;
}
if (capabilities.metricType() != null) {
return METRIC;
}
return NONE;
}

public abstract TimeSeriesFieldType merge(TimeSeriesFieldType other);
}

private static Map<String, Reader<? extends EsField>> readers = Map.ofEntries(
Expand All @@ -83,13 +123,8 @@ public static Reader<? extends EsField> getReader(String name) {
private final Map<String, EsField> properties;
private final String name;
private final boolean isAlias;
// Because the subclasses all reimplement serialization, this needs to be writeable from subclass constructors
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Comment was outdated - I ended up not reimplementing the serialization in the sub classes, but forgot to delete this in the previous PR)

private final TimeSeriesFieldType timeSeriesFieldType;

public EsField(String name, DataType esDataType, Map<String, EsField> properties, boolean aggregatable) {
this(name, esDataType, properties, aggregatable, false, TimeSeriesFieldType.UNKNOWN);
}

public EsField(
String name,
DataType esDataType,
Expand All @@ -100,10 +135,6 @@ public EsField(
this(name, esDataType, properties, aggregatable, false, timeSeriesFieldType);
}

public EsField(String name, DataType esDataType, Map<String, EsField> properties, boolean aggregatable, boolean isAlias) {
this(name, esDataType, properties, aggregatable, isAlias, TimeSeriesFieldType.UNKNOWN);
}

public EsField(
String name,
DataType esDataType,
Expand Down Expand Up @@ -247,6 +278,10 @@ public Exact getExactInfo() {
return Exact.EXACT_FIELD;
}

public TimeSeriesFieldType getTimeSeriesFieldType() {
return timeSeriesFieldType;
}

@Override
public String toString() {
return name + "@" + esDataType.typeName() + "=" + properties;
Expand All @@ -265,12 +300,13 @@ public boolean equals(Object o) {
&& isAlias == field.isAlias
&& esDataType == field.esDataType
&& Objects.equals(name, field.name)
&& Objects.equals(properties, field.properties);
&& Objects.equals(properties, field.properties)
&& Objects.equals(timeSeriesFieldType, field.timeSeriesFieldType);
}

@Override
public int hashCode() {
return Objects.hash(esDataType, aggregatable, properties, name, isAlias);
return Objects.hash(esDataType, aggregatable, properties, name, isAlias, timeSeriesFieldType);
}

public static final class Exact {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

Expand All @@ -26,23 +25,16 @@ public class KeywordEsField extends EsField {
private final int precision;
private final boolean normalized;

public KeywordEsField(String name) {
this(name, Collections.emptyMap(), true, Short.MAX_VALUE, false);
}

public KeywordEsField(String name, Map<String, EsField> properties, boolean hasDocValues, int precision, boolean normalized) {
this(name, properties, hasDocValues, precision, normalized, false);
}

public KeywordEsField(
String name,
Map<String, EsField> properties,
boolean hasDocValues,
int precision,
boolean normalized,
boolean isAlias
boolean isAlias,
TimeSeriesFieldType timeSeriesFieldType
) {
this(name, KEYWORD, properties, hasDocValues, precision, normalized, isAlias, TimeSeriesFieldType.UNKNOWN);
this(name, KEYWORD, properties, hasDocValues, precision, normalized, isAlias, timeSeriesFieldType);
}

protected KeywordEsField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ public class MultiTypeEsField extends EsField {

private final Map<String, Expression> indexToConversionExpressions;

public MultiTypeEsField(String name, DataType dataType, boolean aggregatable, Map<String, Expression> indexToConversionExpressions) {
super(name, dataType, Map.of(), aggregatable);
this.indexToConversionExpressions = indexToConversionExpressions;
}

public MultiTypeEsField(
String name,
DataType dataType,
Expand Down Expand Up @@ -99,7 +94,13 @@ public static MultiTypeEsField resolveFrom(
indexToConversionExpressions.put(indexName, convertExpr);
}
}
return new MultiTypeEsField(invalidMappedField.getName(), resolvedDataType, false, indexToConversionExpressions);
return new MultiTypeEsField(
invalidMappedField.getName(),
resolvedDataType,
false,
indexToConversionExpressions,
invalidMappedField.getTimeSeriesFieldType()
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.elasticsearch.common.io.stream.StreamInput;

import java.io.IOException;
import java.util.Collections;

/**
* This class is used as a marker for fields that may be unmapped, where an unmapped field is a field which exists in the _source but is not
Expand All @@ -17,7 +18,7 @@
*/
public class PotentiallyUnmappedKeywordEsField extends KeywordEsField {
public PotentiallyUnmappedKeywordEsField(String name) {
super(name);
super(name, Collections.emptyMap(), true, Short.MAX_VALUE, false, false, TimeSeriesFieldType.UNKNOWN);
}

public PotentiallyUnmappedKeywordEsField(StreamInput in) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@
*/
public class TextEsField extends EsField {

public TextEsField(String name, Map<String, EsField> properties, boolean hasDocValues) {
this(name, properties, hasDocValues, false);
}

public TextEsField(String name, Map<String, EsField> properties, boolean hasDocValues, boolean isAlias) {
super(name, TEXT, properties, hasDocValues, isAlias, TimeSeriesFieldType.UNKNOWN);
}

public TextEsField(
String name,
Map<String, EsField> properties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ public static FieldAttribute fieldAttribute() {
}

public static FieldAttribute fieldAttribute(String name, DataType type) {
return new FieldAttribute(EMPTY, name, new EsField(name, type, emptyMap(), randomBoolean()));
return new FieldAttribute(EMPTY, name, new EsField(name, type, emptyMap(), randomBoolean(), EsField.TimeSeriesFieldType.NONE));
}

public static FieldAttribute getFieldAttribute(String name) {
return getFieldAttribute(name, INTEGER);
}

public static FieldAttribute getFieldAttribute(String name, DataType dataType) {
return new FieldAttribute(EMPTY, name, new EsField(name + "f", dataType, emptyMap(), true));
return new FieldAttribute(EMPTY, name, new EsField(name + "f", dataType, emptyMap(), true, EsField.TimeSeriesFieldType.NONE));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,15 @@ public static FieldAttribute getFieldAttribute(String name) {
}

public static FieldAttribute getFieldAttribute(String name, DataType dataType) {
return new FieldAttribute(EMPTY, name, new EsField(name + "f", dataType, emptyMap(), true));
return new FieldAttribute(EMPTY, name, new EsField(name + "f", dataType, emptyMap(), true, EsField.TimeSeriesFieldType.NONE));
}

public static FieldAttribute fieldAttribute() {
return fieldAttribute(randomAlphaOfLength(10), randomFrom(DataType.types()));
}

public static FieldAttribute fieldAttribute(String name, DataType type) {
return new FieldAttribute(EMPTY, name, new EsField(name, type, emptyMap(), randomBoolean()));
return new FieldAttribute(EMPTY, name, new EsField(name, type, emptyMap(), randomBoolean(), EsField.TimeSeriesFieldType.NONE));
}

public static Literal of(Object value) {
Expand Down
Loading