Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
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
5 changes: 5 additions & 0 deletions docs/changelog/137511.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 137511
summary: Fixes esql class cast bug in STATS at planning level
area: ES|QL
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -4392,3 +4392,20 @@ row a = 1
c:long
1
;

fixClassCastBugWithSeveralCounts
required_capability: inline_stats
required_capability: fix_stats_classcast_exception

FROM sample_data, sample_data_str
| EVAL one_ip = client_ip::ip
| INLINE STATS count1=count(client_ip::ip), count2=count(one_ip)
| KEEP count1, count2
| LIMIT 3
;

count1:long |count2:long
14 |14
14 |14
14 |14
;
Original file line number Diff line number Diff line change
Expand Up @@ -3463,3 +3463,72 @@ employees:long | job_positions:keyword
15 | Tech Lead
;

fixClassCastBugWithCountDistinct
required_capability: fix_stats_classcast_exception

from airports
| rename scalerank AS x
| stats a = count(x), b = count(x) + count(x), c = count_distinct(x)
;

a:long | b:long | c:long
891 | 1782 | 8
;

fixClassCastBugWithValuesFn
required_capability: fix_stats_classcast_exception

ROW x = [1,2,3]
| STATS a = MV_COUNT(VALUES(x)), b = VALUES(x), c = SUM(x)
;

a:integer | b:integer | c:long
3 | [1, 2, 3] | 6
;

fixClassCastBugWithSeveralCountDistincts
required_capability: fix_stats_classcast_exception

ROW x = 1
| STATS a = 2*COUNT_DISTINCT(x), b = COUNT_DISTINCT(x), c = MAX(x)
;

a:long | b:long | c:integer
2 | 1 | 1
;

fixClassCastBugWithMedianPlusCountDistinct
required_capability: fix_stats_classcast_exception

FROM sample_data_ts_long
| EVAL sym1 = 0, sym5 = 1
| STATS sym2 = median(sym5) + 0, sym3 = median(sym5), sym4 = count_distinct(sym1)
;

sym2:double |sym3:double | sym4:long
1.0 | 1.0 | 1
;

fixClassCastBugWithFoldableLiterals
required_capability: fix_stats_classcast_exception

from airports
| rename scalerank AS x
| stats a = count(x), b = count(x) + count(x), c = count_distinct(x, 10), d = count_distinct(x, 10 + 1 - 1)
;

a:long | b:long | c:long | d:long
891 | 1782 | 8 | 8
;

fixClassCastBugWithSurrogateExpressions
required_capability: fix_stats_classcast_exception

from airports
| rename scalerank AS x
| stats a = median(x), b = percentile(x, 50), c = count_distinct(x)
;

a:double | b:double | c:long
6.0 | 6.0 | 8
;
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,13 @@ public enum Cap {
*/
PUSHING_DOWN_EVAL_WITH_SCORE,

/**
* Fix for ClassCastException in STATS
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Fix for ClassCastException in STATS
* Fix for ClassCastException in STATS
* https://github.com/elastic/elasticsearch/issues/133992

I realize it is a bit tricky to describe the change with java doc.
It might be worth linking an issue as it has a bit detailed description.
There are some prior examples with such links.

* https://github.com/elastic/elasticsearch/issues/133992
* https://github.com/elastic/elasticsearch/issues/136598
*/
FIX_STATS_CLASSCAST_EXCEPTION,

/**
* Fix attribute equality to respect the name id of the attribute.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.elasticsearch.xpack.esql.optimizer.rules.logical.CombineLimitTopN;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.CombineProjections;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ConstantFolding;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.DeduplicateAggs;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ExtractAggregateCommonFilter;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.FoldNull;
import org.elasticsearch.xpack.esql.optimizer.rules.logical.HoistRemoteEnrichLimit;
Expand Down Expand Up @@ -178,6 +179,14 @@ protected static Batch<LogicalPlan> operators() {
new SplitInWithFoldableValue(),
new PropagateEvalFoldables(),
new ConstantFolding(),
/* Then deduplicate aggregations
We need this after the constant folding
because we could have expressions like
count_distinct(_, 9 + 1)
count_distinct(_, 10)
which are semantically identical
*/
new DeduplicateAggs(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting!

Is the constant folding required to run before? Will this take care of cases that have percentile( foo, 25+25) and percentile(foo,2*25)?

I'd have thought that maybe the aggs substitution needs to happen before we replace agg expressions with evals. But this is placing deduplication pretty late into the optimization.

It's not wrong, though! But maybe a bit unfortunate that we'll have 2 rules that dedupe; and we can't assume that aggs come out of the substitutions batch already deduped. But maybe that was never correct to assume.

Copy link
Contributor

Choose a reason for hiding this comment

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

Constant folding in aggregation is an endemic problem and tech debt. Some insights here #112392 (comment).

I don't think we could realistically do the right thing now with constant folding in aggs (due to priorities), and there are already optimization stuff we duplicate because of this combination of surrogate expressions in aggs + constant folding issues - see

new SubstituteSurrogateAggregations(),
new ReplaceAggregateNestedExpressionWithEval()

being called twice in Substitutions batch. Having deduplication called twice for aggs only because agg constant folding doesn't happen correctly and at the right time is, in my mind, the right compromise at this point given how mysterious the errors are, how likely users are to run into them and, also, is not a completely wrong thing to do.

new PartiallyFoldCase(),
// boolean
new BooleanSimplification(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.xpack.esql.optimizer.rules.logical;

/**
* This rule handles duplicate aggregate functions to avoid duplicate compute
* stats a = min(x), b = min(x), c = count(*), d = count() by g
* becomes
* stats a = min(x), c = count(*) by g | eval b = a, d = c | keep a, b, c, d, g
*/
public final class DeduplicateAggs extends ReplaceAggregateAggExpressionWithEval implements OptimizerRules.CoordinatorOnly {

public DeduplicateAggs() {
super(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@
* becomes
* stats a = min(x), c = count(*) by g | eval b = a, d = c | keep a, b, c, d, g
*/
public final class ReplaceAggregateAggExpressionWithEval extends OptimizerRules.OptimizerRule<Aggregate> {
public class ReplaceAggregateAggExpressionWithEval extends OptimizerRules.OptimizerRule<Aggregate> {
private boolean replaceNestedExpressions = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

final and set it always in the constructor.


public ReplaceAggregateAggExpressionWithEval(boolean replaceNestedExpressions) {
super(OptimizerRules.TransformDirection.UP);
this.replaceNestedExpressions = replaceNestedExpressions;
}

public ReplaceAggregateAggExpressionWithEval() {
super(OptimizerRules.TransformDirection.UP);
}
Expand Down Expand Up @@ -88,7 +95,7 @@ protected LogicalPlan rule(Aggregate aggregate) {
// common case - handle duplicates
if (child instanceof AggregateFunction af) {
// canonical representation, with resolved aliases
AggregateFunction canonical = (AggregateFunction) af.canonical().transformUp(e -> aliases.resolve(e, e));
AggregateFunction canonical = getCannonical(af, aliases);

Alias found = rootAggs.get(canonical);
// aggregate is new
Expand All @@ -106,14 +113,15 @@ protected LogicalPlan rule(Aggregate aggregate) {
}
// nested expression over aggregate function or groups
// replace them with reference and move the expression into a follow-up eval
else {
else if (replaceNestedExpressions) {
changed.set(true);
Expression aggExpression = child.transformUp(AggregateFunction.class, af -> {
AggregateFunction canonical = (AggregateFunction) af.canonical();
// canonical representation, with resolved aliases
AggregateFunction canonical = getCannonical(af, aliases);
Alias alias = rootAggs.get(canonical);
if (alias == null) {
// create synthetic alias ove the found agg function
alias = new Alias(af.source(), syntheticName(canonical, child, counter[0]++), canonical, null, true);
// create synthetic alias over the found agg function
alias = new Alias(af.source(), syntheticName(canonical, child, counter[0]++), af.canonical(), null, true);
// and remember it to remove duplicates
rootAggs.put(canonical, alias);
// add it to the list of aggregates and continue
Expand All @@ -132,6 +140,9 @@ protected LogicalPlan rule(Aggregate aggregate) {
Alias alias = as.replaceChild(aggExpression);
newEvals.add(alias);
newProjections.add(alias.toAttribute());
} else {
newAggs.add(agg);
newProjections.add(agg.toAttribute());
Comment on lines +144 to +146
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This block is doing nothing really because it only gets triggered if replaceNestedExpressions is false and it does not seem to be reached in that case ever, at least for the tests in our codebase at the moment.

But I'd rather have this safeguard (just leaving that aggregation as it is) than risking something important to disappear in the plan.

}
}
// not an alias (e.g. grouping field)
Expand All @@ -155,6 +166,10 @@ protected LogicalPlan rule(Aggregate aggregate) {
return plan;
}

private static AggregateFunction getCannonical(AggregateFunction af, AttributeMap<Expression> aliases) {
return (AggregateFunction) af.canonical().transformUp(e -> aliases.resolve(e, e));
}

Comment on lines +170 to +173
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've extracted this as @alex-spies asked me to do

private static String syntheticName(Expression expression, Expression af, int counter) {
return TemporaryNameUtils.temporaryName(expression, af, counter);
}
Expand Down
Loading