Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e2bbc21
Minimal grammar change
alex-spies Jul 31, 2025
f1c087e
Basic parsing + plugging into attribute classes
alex-spies Aug 11, 2025
88bf534
First test + fix grammar
alex-spies Aug 12, 2025
6d7a097
Add statement parser tests for use in expressions
alex-spies Aug 12, 2025
63be22f
Start command tests + fixes
alex-spies Aug 13, 2025
24c89df
Fix qualifier parsing in KEEP/DROP
alex-spies Aug 13, 2025
044071d
Invalidate qualified patterns in DROP
alex-spies Aug 13, 2025
6731d9e
Add ENRICH tests + validation
alex-spies Aug 13, 2025
493453c
Add tests for EVAL
alex-spies Aug 13, 2025
8a0b539
Add tests for FORK
alex-spies Aug 13, 2025
a9028c5
More tests for more commands
alex-spies Aug 14, 2025
fdf2393
Finish adding tests for commands
alex-spies Aug 14, 2025
30a5732
Confirm qualifiers disabled in release builds
alex-spies Aug 14, 2025
81b5093
Merge remote-tracking branch 'upstream/main' into qualifiers-in-lu-join
alex-spies Aug 14, 2025
7d6014e
Add qualifier to serialization code
alex-spies Aug 18, 2025
a9be5ba
Reduce usage of c'tors without qualifiers
alex-spies Aug 18, 2025
e8f271a
Update ExchangeSinkExecSerializationTests
alex-spies Aug 18, 2025
14aefe9
Merge remote-tracking branch 'upstream/main' into qualifiers-in-lu-join
alex-spies Aug 18, 2025
15ebd88
Clean up TODO comments
alex-spies Aug 22, 2025
7a2378c
Merge remote-tracking branch 'upstream/main' into qualifiers-in-lu-join
alex-spies Aug 22, 2025
541425f
Use bracket syntax [qualifier].[name]
alex-spies Aug 22, 2025
8edaa6a
Make checkStyle happy
alex-spies Aug 22, 2025
d58b40a
Fix test
alex-spies Aug 25, 2025
91034c3
Merge remote-tracking branch 'upstream/main' into qualifiers-in-lu-join
alex-spies Aug 25, 2025
355ce70
Merge remote-tracking branch 'upstream/main' into qualifiers-in-lu-join
alex-spies Aug 26, 2025
896c378
Add parsing tests that inspect the whole AST
alex-spies Aug 26, 2025
0692571
Fix attribute serialization behavior
alex-spies Aug 27, 2025
2d1cb10
Merge remote-tracking branch 'upstream/main' into qualifiers-in-lu-join
alex-spies Aug 27, 2025
e8534e7
Add and test explicitly unqualified attributes
alex-spies Aug 27, 2025
8525e64
Fix tests
alex-spies Aug 27, 2025
6b6d68d
Merge remote-tracking branch 'upstream/main' into qualifiers-in-lu-join
alex-spies Aug 28, 2025
f8f4863
Fix tests in release mode
alex-spies Aug 28, 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 @@ -369,6 +369,7 @@ static TransportVersion def(int id) {
public static final TransportVersion SCRIPT_RESCORER = def(9_143_0_00);
public static final TransportVersion ESQL_LOOKUP_OPERATOR_EMITTED_ROWS = def(9_144_0_00);
public static final TransportVersion ALLOCATION_DECISION_NOT_PREFERRED = def(9_145_0_00);
public static final TransportVersion ESQL_QUALIFIERS_IN_ATTRIBUTES = def(9_146_0_00);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public DataType dataType() {
public Attribute toAttribute() {
if (lazyAttribute == null) {
lazyAttribute = resolved()
? new ReferenceAttribute(source(), name(), dataType(), nullable(), id(), synthetic())
? new ReferenceAttribute(source(), null, name(), dataType(), nullable(), id(), synthetic())
: new UnresolvedAttribute(source(), name());
}
return lazyAttribute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
*/
package org.elasticsearch.xpack.esql.core.expression;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.esql.core.tree.Source;
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.core.util.PlanStreamInput;
import org.elasticsearch.xpack.esql.core.util.PlanStreamOutput;

import java.io.IOException;
import java.util.List;
import java.util.Objects;

import static java.util.Collections.emptyList;
import static org.elasticsearch.TransportVersions.ESQL_QUALIFIERS_IN_ATTRIBUTES;

/**
* {@link Expression}s that can be materialized and describe properties of the derived table.
Expand All @@ -33,19 +38,40 @@ public abstract class Attribute extends NamedExpression {
*/
protected static final String SYNTHETIC_ATTRIBUTE_NAME_PREFIX = "$$";

// can the attr be null - typically used in JOINs
// can the attr be null
private final Nullability nullability;
private final String qualifier;

public Attribute(Source source, String name, @Nullable NameId id) {
this(source, name, Nullability.TRUE, id);
}

public Attribute(Source source, @Nullable String qualifier, String name, @Nullable NameId id) {
this(source, qualifier, name, Nullability.TRUE, id);
}

public Attribute(Source source, String name, Nullability nullability, @Nullable NameId id) {
this(source, name, nullability, id, false);
this(source, null, name, nullability, id);
}

public Attribute(Source source, @Nullable String qualifier, String name, Nullability nullability, @Nullable NameId id) {
this(source, qualifier, name, nullability, id, false);
}

public Attribute(Source source, String name, Nullability nullability, @Nullable NameId id, boolean synthetic) {
this(source, null, name, nullability, id, synthetic);
}

public Attribute(
Source source,
@Nullable String qualifier,
String name,
Nullability nullability,
@Nullable NameId id,
boolean synthetic
) {
super(source, name, emptyList(), id, synthetic);
this.qualifier = qualifier;
this.nullability = nullability;
}

Expand All @@ -59,6 +85,14 @@ public final Expression replaceChildren(List<Expression> newChildren) {
throw new UnsupportedOperationException("this type of node doesn't have any children to replace");
}

public String qualifier() {
return qualifier;
}

public String qualifiedName() {
return qualifier != null ? "[" + qualifier + "].[" + name() + "]" : name();
}

@Override
public Nullability nullable() {
return nullability;
Expand All @@ -70,26 +104,40 @@ public AttributeSet references() {
}

public Attribute withLocation(Source source) {
return Objects.equals(source(), source) ? this : clone(source, name(), dataType(), nullable(), id(), synthetic());
return Objects.equals(source(), source) ? this : clone(source, qualifier(), name(), dataType(), nullable(), id(), synthetic());
}

public Attribute withQualifier(String qualifier) {
return Objects.equals(qualifier, qualifier) ? this : clone(source(), qualifier, name(), dataType(), nullable(), id(), synthetic());
}

public Attribute withName(String name) {
return Objects.equals(name(), name) ? this : clone(source(), name, dataType(), nullable(), id(), synthetic());
return Objects.equals(name(), name) ? this : clone(source(), qualifier(), name, dataType(), nullable(), id(), synthetic());
}

public Attribute withNullability(Nullability nullability) {
return Objects.equals(nullable(), nullability) ? this : clone(source(), name(), dataType(), nullability, id(), synthetic());
return Objects.equals(nullable(), nullability)
? this
: clone(source(), qualifier(), name(), dataType(), nullability, id(), synthetic());
}

public Attribute withId(NameId id) {
return clone(source(), name(), dataType(), nullable(), id, synthetic());
return clone(source(), qualifier(), name(), dataType(), nullable(), id, synthetic());
}

public Attribute withDataType(DataType type) {
return Objects.equals(dataType(), type) ? this : clone(source(), name(), type, nullable(), id(), synthetic());
return Objects.equals(dataType(), type) ? this : clone(source(), qualifier(), name(), type, nullable(), id(), synthetic());
}

protected abstract Attribute clone(Source source, String name, DataType type, Nullability nullability, NameId id, boolean synthetic);
protected abstract Attribute clone(
Source source,
String qualifier,
String name,
DataType type,
Nullability nullability,
NameId id,
boolean synthetic
);

@Override
public Attribute toAttribute() {
Expand All @@ -108,24 +156,24 @@ public boolean semanticEquals(Expression other) {

@Override
protected Expression canonicalize() {
return clone(Source.EMPTY, name(), dataType(), nullability, id(), synthetic());
return clone(Source.EMPTY, qualifier(), name(), dataType(), nullability, id(), synthetic());
}

@Override
@SuppressWarnings("checkstyle:EqualsHashCode")// equals is implemented in parent. See innerEquals instead
public int hashCode() {
return Objects.hash(super.hashCode(), nullability);
return Objects.hash(super.hashCode(), qualifier, nullability);
}

@Override
protected boolean innerEquals(Object o) {
var other = (Attribute) o;
return super.innerEquals(other) && Objects.equals(nullability, other.nullability);
return super.innerEquals(other) && Objects.equals(qualifier, other.qualifier) && Objects.equals(nullability, other.nullability);
}

@Override
public String toString() {
return name() + "{" + label() + (synthetic() ? "$" : "") + "}" + "#" + id();
return qualifiedName() + "{" + label() + (synthetic() ? "$" : "") + "}" + "#" + id();
}

@Override
Expand Down Expand Up @@ -154,4 +202,22 @@ public static boolean dataTypeEquals(List<Attribute> left, List<Attribute> right
* @return true if the attribute represents a TSDB dimension type
*/
public abstract boolean isDimension();

protected void checkAndSerializeQualifier(PlanStreamOutput out, TransportVersion version) throws IOException {
if (version.onOrAfter(ESQL_QUALIFIERS_IN_ATTRIBUTES)) {
out.writeOptionalCachedString(qualifier());
} else if (qualifier() != null) {
// Non-null qualifier means the query specifically defined one. Old nodes don't know what to do with it and just writing
// null would lose information and lead to undefined, likely invalid queries.
// IllegalArgumentException returns a 400 to the user, which is what we want here.
throw new IllegalArgumentException("Trying to serialize an Attribute with a qualifier to an old node");
}
}

protected static String readQualifier(PlanStreamInput in, TransportVersion version) throws IOException {
if (version.onOrAfter(ESQL_QUALIFIERS_IN_ATTRIBUTES)) {
return in.readOptionalCachedString();
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ public String getWriteableName() {
}

@Override
protected Attribute clone(Source source, String name, DataType type, Nullability nullability, NameId id, boolean synthetic) {
protected Attribute clone(
Source source,
String qualifier,
String name,
DataType type,
Nullability nullability,
NameId id,
boolean synthetic
) {
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ public final class Expressions {

private Expressions() {}

public static NamedExpression wrapAsNamed(Expression exp) {
return exp instanceof NamedExpression ne ? ne : new Alias(exp.source(), exp.sourceText(), exp);
}

public static List<Attribute> asAttributes(List<? extends NamedExpression> named) {
if (named.isEmpty()) {
return emptyList();
Expand Down Expand Up @@ -109,7 +105,9 @@ public static AttributeSet references(List<? extends Expression> exps) {
}

public static String name(Expression e) {
return e instanceof NamedExpression ne ? ne.name() : e.sourceText();
return e instanceof Attribute attr && attr.qualifier() != null ? attr.qualifiedName()
: e instanceof NamedExpression ne ? ne.name()
: e.sourceText();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,28 +55,40 @@ public record FieldName(String string) {};
private final EsField field;
protected FieldName lazyFieldName;

@Deprecated
/**
* For testing only
*/
public FieldAttribute(Source source, String name, EsField field) {
this(source, null, name, field);
this(source, null, null, name, field, Nullability.TRUE, null, false);
}

public FieldAttribute(Source source, @Nullable String parentName, String name, EsField field) {
this(source, parentName, name, field, Nullability.TRUE, null, false);
public FieldAttribute(
Source source,
@Nullable String parentName,
@Nullable String qualifier,
String name,
EsField field,
boolean synthetic
) {
this(source, parentName, qualifier, name, field, Nullability.TRUE, null, synthetic);
}

public FieldAttribute(Source source, @Nullable String parentName, String name, EsField field, boolean synthetic) {
this(source, parentName, name, field, Nullability.TRUE, null, synthetic);
public FieldAttribute(Source source, @Nullable String parentName, @Nullable String qualifier, String name, EsField field) {
this(source, parentName, qualifier, name, field, Nullability.TRUE, null, false);
}

public FieldAttribute(
Source source,
@Nullable String parentName,
@Nullable String qualifier,
String name,
EsField field,
Nullability nullability,
@Nullable NameId id,
boolean synthetic
) {
super(source, name, field.getDataType(), nullability, id, synthetic);
super(source, qualifier, name, field.getDataType(), nullability, id, synthetic);
this.parentName = parentName;
this.field = field;
}
Expand All @@ -92,6 +104,7 @@ private static FieldAttribute innerReadFrom(StreamInput in) throws IOException {
*/
Source source = Source.readFrom((StreamInput & PlanStreamInput) in);
String parentName = ((PlanStreamInput) in).readOptionalCachedString();
String qualifier = readQualifier((PlanStreamInput) in, in.getTransportVersion());
String name = readCachedStringWithVersionCheck(in);
if (in.getTransportVersion().before(ESQL_FIELD_ATTRIBUTE_DROP_TYPE)) {
DataType.readFrom(in);
Expand All @@ -103,21 +116,22 @@ private static FieldAttribute innerReadFrom(StreamInput in) throws IOException {
Nullability nullability = in.readEnum(Nullability.class);
NameId nameId = NameId.readFrom((StreamInput & PlanStreamInput) in);
boolean synthetic = in.readBoolean();
return new FieldAttribute(source, parentName, name, field, nullability, nameId, synthetic);
return new FieldAttribute(source, parentName, qualifier, name, field, nullability, nameId, synthetic);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
if (((PlanStreamOutput) out).writeAttributeCacheHeader(this)) {
Source.EMPTY.writeTo(out);
((PlanStreamOutput) out).writeOptionalCachedString(parentName);
checkAndSerializeQualifier((PlanStreamOutput) out, out.getTransportVersion());
writeCachedStringWithVersionCheck(out, name());
if (out.getTransportVersion().before(ESQL_FIELD_ATTRIBUTE_DROP_TYPE)) {
dataType().writeTo(out);
}
field.writeTo(out);
if (out.getTransportVersion().before(ESQL_FIELD_ATTRIBUTE_DROP_TYPE)) {
// We used to write the qualifier here. We can still do if needed in the future.
// We used to write the qualifier here, even though it was always null.
out.writeOptionalString(null);
}
out.writeEnum(nullable());
Expand All @@ -137,7 +151,7 @@ public String getWriteableName() {

@Override
protected NodeInfo<FieldAttribute> info() {
return NodeInfo.create(this, FieldAttribute::new, parentName, name(), field, nullable(), id(), synthetic());
return NodeInfo.create(this, FieldAttribute::new, parentName, qualifier(), name(), field, nullable(), id(), synthetic());
}

public String parentName() {
Expand Down Expand Up @@ -185,13 +199,30 @@ public FieldAttribute exactAttribute() {
}

private FieldAttribute innerField(EsField type) {
return new FieldAttribute(source(), fieldName().string, name() + "." + type.getName(), type, nullable(), id(), synthetic());
return new FieldAttribute(
source(),
fieldName().string,
qualifier(),
name() + "." + type.getName(),
type,
nullable(),
id(),
synthetic()
);
}

@Override
protected Attribute clone(Source source, String name, DataType type, Nullability nullability, NameId id, boolean synthetic) {
protected Attribute clone(
Source source,
String qualifier,
String name,
DataType type,
Nullability nullability,
NameId id,
boolean synthetic
) {
// Ignore `type`, this must be the same as the field's type.
return new FieldAttribute(source, parentName, name, field, nullability, id, synthetic);
return new FieldAttribute(source, parentName, qualifier, name, field, nullability, id, synthetic);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,16 @@ public String getWriteableName() {
}

@Override
protected MetadataAttribute clone(Source source, String name, DataType type, Nullability nullability, NameId id, boolean synthetic) {
protected MetadataAttribute clone(
Source source,
String qualifier,
String name,
DataType type,
Nullability nullability,
NameId id,
boolean synthetic
) {
// Ignores qualifier, as metadata attributes do not have qualifiers.
return new MetadataAttribute(source, name, type, nullability, id, synthetic, searchable);
}

Expand Down
Loading