2020import org .elasticsearch .compute .aggregation .TopIpAggregatorFunctionSupplier ;
2121import org .elasticsearch .compute .aggregation .TopLongAggregatorFunctionSupplier ;
2222import org .elasticsearch .xpack .esql .EsqlIllegalArgumentException ;
23+ import org .elasticsearch .xpack .esql .capabilities .PostOptimizationVerificationAware ;
24+ import org .elasticsearch .xpack .esql .common .Failures ;
2325import org .elasticsearch .xpack .esql .core .expression .Expression ;
24- import org .elasticsearch .xpack .esql .core .expression .FoldContext ;
2526import org .elasticsearch .xpack .esql .core .expression .Literal ;
2627import org .elasticsearch .xpack .esql .core .tree .NodeInfo ;
2728import org .elasticsearch .xpack .esql .core .tree .Source ;
3031import org .elasticsearch .xpack .esql .expression .function .Example ;
3132import org .elasticsearch .xpack .esql .expression .function .FunctionInfo ;
3233import org .elasticsearch .xpack .esql .expression .function .FunctionType ;
34+ import org .elasticsearch .xpack .esql .expression .function .FunctionUtils ;
3335import org .elasticsearch .xpack .esql .expression .function .Param ;
3436import org .elasticsearch .xpack .esql .io .stream .PlanStreamInput ;
3537import org .elasticsearch .xpack .esql .planner .ToAggregator ;
3941
4042import static java .util .Arrays .asList ;
4143import static org .elasticsearch .common .logging .LoggerMessageFormat .format ;
44+ import static org .elasticsearch .xpack .esql .common .Failure .fail ;
4245import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .ParamOrdinal .FIRST ;
4346import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .ParamOrdinal .SECOND ;
4447import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .ParamOrdinal .THIRD ;
45- import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .isNotNullAndFoldable ;
48+ import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .isNotNull ;
4649import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .isString ;
4750import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .isType ;
4851
49- public class Top extends AggregateFunction implements ToAggregator , SurrogateExpression {
52+ public class Top extends AggregateFunction implements ToAggregator , SurrogateExpression , PostOptimizationVerificationAware {
5053 public static final NamedWriteableRegistry .Entry ENTRY = new NamedWriteableRegistry .Entry (Expression .class , "Top" , Top ::new );
5154
5255 private static final String ORDER_ASC = "ASC" ;
@@ -116,16 +119,18 @@ Expression orderField() {
116119 return parameters ().get (1 );
117120 }
118121
119- private int limitValue () {
120- return (int ) limitField ().fold (FoldContext .small () /* TODO remove me */ );
121- }
122-
123- private String orderRawValue () {
124- return BytesRefs .toString (orderField ().fold (FoldContext .small () /* TODO remove me */ ));
122+ private Integer limitValue () {
123+ return FunctionUtils .limitValue (limitField (), sourceText ());
125124 }
126125
127126 private boolean orderValue () {
128- return orderRawValue ().equalsIgnoreCase (ORDER_ASC );
127+ if (orderField () instanceof Literal literal ) {
128+ String order = BytesRefs .toString (literal .value ());
129+ if (ORDER_ASC .equalsIgnoreCase (order ) || ORDER_DESC .equalsIgnoreCase (order )) {
130+ return order .equalsIgnoreCase (ORDER_ASC );
131+ }
132+ }
133+ throw new EsqlIllegalArgumentException ("Order value must be a literal, found: " + orderField ());
129134 }
130135
131136 @ Override
@@ -148,31 +153,88 @@ protected TypeResolution resolveType() {
148153 "ip" ,
149154 "string" ,
150155 "numeric except unsigned_long or counter types"
151- ).and (isNotNullAndFoldable (limitField (), sourceText (), SECOND ))
156+ ).and (isNotNull (limitField (), sourceText (), SECOND ))
152157 .and (isType (limitField (), dt -> dt == DataType .INTEGER , sourceText (), SECOND , "integer" ))
153- .and (isNotNullAndFoldable (orderField (), sourceText (), THIRD ))
158+ .and (isNotNull (orderField (), sourceText (), THIRD ))
154159 .and (isString (orderField (), sourceText (), THIRD ));
155160
156161 if (typeResolution .unresolved ()) {
157162 return typeResolution ;
158163 }
159164
160- var limit = limitValue ();
161- var order = orderRawValue ();
162-
163- if (limit <= 0 ) {
164- return new TypeResolution (format (null , "Limit must be greater than 0 in [{}], found [{}]" , sourceText (), limit ));
165+ TypeResolution result = resolveTypeLimit ();
166+ if (result .equals (TypeResolution .TYPE_RESOLVED ) == false ) {
167+ return result ;
165168 }
166-
167- if (order .equalsIgnoreCase (ORDER_ASC ) == false && order .equalsIgnoreCase (ORDER_DESC ) == false ) {
168- return new TypeResolution (
169- format (null , "Invalid order value in [{}], expected [{}, {}] but got [{}]" , sourceText (), ORDER_ASC , ORDER_DESC , order )
170- );
169+ result = resolveTypeOrder ();
170+ if (result .equals (TypeResolution .TYPE_RESOLVED ) == false ) {
171+ return result ;
171172 }
173+ return TypeResolution .TYPE_RESOLVED ;
174+ }
175+
176+ /**
177+ * We check that the limit is not null and that if it is a literal, it is a positive integer
178+ * We will do a more thorough check in the postOptimizationVerification once folding is done.
179+ */
180+ private TypeResolution resolveTypeLimit () {
181+ return FunctionUtils .resolveTypeLimit (limitField (), sourceText ());
182+ }
172183
184+ /**
185+ * We check that the order is not null and that if it is a literal, it is one of the two valid values: "asc" or "desc".
186+ * We will do a more thorough check in the postOptimizationVerification once folding is done.
187+ */
188+ private TypeResolution resolveTypeOrder () {
189+ Expression order = orderField ();
190+ if (order == null ) {
191+ return new TypeResolution (format (null , "Order must be a valid string in [{}], found [{}]" , sourceText (), order ));
192+ }
193+ if (order instanceof Literal literal ) {
194+ if (literal .value () == null ) {
195+ return new TypeResolution (
196+ format (null , "Invalid order value in [{}], expected [{}, {}] but got [{}]" , sourceText (), ORDER_ASC , ORDER_DESC , order )
197+ );
198+ }
199+ String value = BytesRefs .toString (literal .value ());
200+ if (value == null || value .equalsIgnoreCase (ORDER_ASC ) == false && value .equalsIgnoreCase (ORDER_DESC ) == false ) {
201+ return new TypeResolution (
202+ format (null , "Invalid order value in [{}], expected [{}, {}] but got [{}]" , sourceText (), ORDER_ASC , ORDER_DESC , order )
203+ );
204+ }
205+ }
173206 return TypeResolution .TYPE_RESOLVED ;
174207 }
175208
209+ @ Override
210+ public void postOptimizationVerification (Failures failures ) {
211+ postOptimizationVerificationLimit (failures );
212+ postOptimizationVerificationOrder (failures );
213+ }
214+
215+ private void postOptimizationVerificationLimit (Failures failures ) {
216+ FunctionUtils .postOptimizationVerificationLimit (failures , limitField (), sourceText ());
217+ }
218+
219+ private void postOptimizationVerificationOrder (Failures failures ) {
220+ Expression order = orderField ();
221+ if (order == null ) {
222+ failures .add (fail (order , "Order must be a valid string in [{}], found [{}]" , sourceText (), order ));
223+ }
224+ if (order instanceof Literal literal ) {
225+ String value = BytesRefs .toString (literal .value ());
226+ if (value == null || value .equalsIgnoreCase (ORDER_ASC ) == false && value .equalsIgnoreCase (ORDER_DESC ) == false ) {
227+ failures .add (
228+ fail (order , "Invalid order value in [{}], expected [{}, {}] but got [{}]" , sourceText (), ORDER_ASC , ORDER_DESC , order )
229+ );
230+ }
231+ } else {
232+ // it is expected that the expression is a literal after folding
233+ // we fail if it is not a literal
234+ failures .add (fail (order , "Order must be a valid string in [{}], found [{}]" , sourceText (), order ));
235+ }
236+ }
237+
176238 @ Override
177239 public DataType dataType () {
178240 return field ().dataType ().noText ();
@@ -215,15 +277,20 @@ public AggregatorFunctionSupplier supplier() {
215277 @ Override
216278 public Expression surrogate () {
217279 var s = source ();
218-
219- if (limitValue () == 1 ) {
220- if (orderValue ()) {
221- return new Min (s , field ());
222- } else {
223- return new Max (s , field ());
280+ try {
281+ if (limitValue () == 1 ) {
282+ if (orderValue ()) {
283+ return new Min (s , field ());
284+ } else {
285+ return new Max (s , field ());
286+ }
224287 }
288+ } catch (EsqlIllegalArgumentException e ) {
289+ // If the limit is not a literal or is not a positive integer, we cannot create a surrogate
290+ // so we return null to indicate that no surrogate can be created.
291+ // This is possible if the limit is an expression, and folding has not been done yet.
292+ return null ;
225293 }
226-
227294 return null ;
228295 }
229296}
0 commit comments