Skip to content

Commit dfe501e

Browse files
ioanatiaKubik42
authored andcommitted
ES|QL: Add validation for offset and type (#137824)
1 parent 84dcbd8 commit dfe501e

File tree

2 files changed

+123
-9
lines changed
  • x-pack/plugin/esql/src
    • main/java/org/elasticsearch/xpack/esql/expression/function/scalar/score
    • test/java/org/elasticsearch/xpack/esql/analysis

2 files changed

+123
-9
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/score/Decay.java

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1313
import org.elasticsearch.common.io.stream.StreamInput;
1414
import org.elasticsearch.common.io.stream.StreamOutput;
15+
import org.elasticsearch.common.lucene.BytesRefs;
1516
import org.elasticsearch.compute.ann.Evaluator;
1617
import org.elasticsearch.compute.ann.Fixed;
1718
import org.elasticsearch.compute.operator.EvalOperator;
@@ -45,11 +46,13 @@
4546
import java.util.Collection;
4647
import java.util.HashMap;
4748
import java.util.List;
49+
import java.util.Locale;
4850
import java.util.Map;
4951
import java.util.Set;
5052
import java.util.function.Predicate;
5153
import java.util.stream.Collectors;
5254

55+
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
5356
import static org.elasticsearch.xpack.esql.common.Failure.fail;
5457
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
5558
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FOURTH;
@@ -61,6 +64,7 @@
6164
import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
6265
import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
6366
import static org.elasticsearch.xpack.esql.core.type.DataType.LONG;
67+
import static org.elasticsearch.xpack.esql.core.type.DataType.NULL;
6468
import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT;
6569
import static org.elasticsearch.xpack.esql.core.type.DataType.TIME_DURATION;
6670
import static org.elasticsearch.xpack.esql.core.type.DataType.isDateNanos;
@@ -212,8 +216,9 @@ protected TypeResolution resolveType() {
212216
return new TypeResolution("Unresolved children");
213217
}
214218

215-
return validateValue().and(() -> validateOriginAndScale(value.dataType()))
216-
.and(() -> Options.resolveWithMultipleDataTypesAllowed(options, source(), FOURTH, ALLOWED_OPTIONS));
219+
return validateValue().and(() -> Options.resolveWithMultipleDataTypesAllowed(options, source(), FOURTH, ALLOWED_OPTIONS))
220+
.and(() -> validateOriginScaleAndOffset(value.dataType()))
221+
.and(this::validateTypeOption);
217222
}
218223

219224
private TypeResolution validateValue() {
@@ -222,34 +227,100 @@ private TypeResolution validateValue() {
222227
);
223228
}
224229

225-
private TypeResolution validateOriginAndScale(DataType valueType) {
230+
private TypeResolution validateOriginScaleAndOffset(DataType valueType) {
226231
if (isSpatialPoint(valueType)) {
227232
boolean isGeoPoint = isGeoPoint(valueType);
228233

229-
return validateOriginAndScale(
234+
return validateOriginScaleAndOffset(
230235
DataType::isSpatialPoint,
231236
"spatial point",
232237
isGeoPoint ? DataType::isString : DataType::isNumeric,
238+
isGeoPoint ? "keyword or text" : "numeric",
239+
isGeoPoint ? DataType::isString : DataType::isNumeric,
233240
isGeoPoint ? "keyword or text" : "numeric"
234241
);
235242
} else if (isMillisOrNanos(valueType)) {
236-
return validateOriginAndScale(DataType::isMillisOrNanos, "datetime or date_nanos", DataType::isTimeDuration, "time_duration");
243+
return validateOriginScaleAndOffset(
244+
DataType::isMillisOrNanos,
245+
"datetime or date_nanos",
246+
DataType::isTimeDuration,
247+
"time_duration",
248+
DataType::isTimeDuration,
249+
"time_duration"
250+
);
237251
} else {
238-
return validateOriginAndScale(DataType::isNumeric, "numeric", DataType::isNumeric, "numeric");
252+
return validateOriginScaleAndOffset(
253+
DataType::isNumeric,
254+
"numeric",
255+
DataType::isNumeric,
256+
"numeric",
257+
DataType::isNumeric,
258+
"numeric"
259+
);
239260
}
240261
}
241262

242-
private TypeResolution validateOriginAndScale(
263+
private TypeResolution validateOriginScaleAndOffset(
243264
Predicate<DataType> originPredicate,
244265
String originDesc,
245266
Predicate<DataType> scalePredicate,
246-
String scaleDesc
267+
String scaleDesc,
268+
Predicate<DataType> offsetPredicate,
269+
String offsetDesc
247270
) {
271+
if (options != null) {
272+
Expression offset = ((MapExpression) options).keyFoldedMap().get(OFFSET);
273+
if (offset != null && offset.dataType() != NULL && offsetPredicate.test((offset).dataType()) == false) {
274+
return new TypeResolution(
275+
format(null, "{} option has invalid type, expected [{}], found [{}]", OFFSET, offsetDesc, offset.dataType().typeName())
276+
);
277+
}
278+
}
279+
248280
return isNotNull(origin, sourceText(), SECOND).and(isType(origin, originPredicate, sourceText(), SECOND, originDesc))
249281
.and(isNotNull(scale, sourceText(), THIRD))
250282
.and(isType(scale, scalePredicate, sourceText(), THIRD, scaleDesc));
251283
}
252284

285+
private TypeResolution validateTypeOption() {
286+
if (options == null) {
287+
return TypeResolution.TYPE_RESOLVED;
288+
}
289+
290+
Expression decayType = ((MapExpression) options).keyFoldedMap().get(TYPE);
291+
292+
if (decayType == null || decayType.dataType() == NULL) {
293+
return TypeResolution.TYPE_RESOLVED;
294+
}
295+
296+
if (decayType.dataType() != KEYWORD) {
297+
return new TypeResolution(
298+
format(
299+
null,
300+
"{} option has invalid type, expected [{}], found [{}]",
301+
TYPE,
302+
KEYWORD.typeName(),
303+
decayType.dataType().typeName()
304+
)
305+
);
306+
}
307+
308+
String decayTypeName = BytesRefs.toString(decayType.fold(FoldContext.small())).toLowerCase(Locale.ROOT);
309+
310+
if (DecayFunction.BY_NAME.containsKey(decayTypeName) == false) {
311+
return new TypeResolution(
312+
format(
313+
null,
314+
"{} option has invalid value, expected one of [gauss, linear, exp], found [{}]",
315+
TYPE,
316+
decayType.source().text()
317+
)
318+
);
319+
}
320+
321+
return TypeResolution.TYPE_RESOLVED;
322+
}
323+
253324
@Override
254325
public Expression replaceChildren(List<Expression> newChildren) {
255326
return new Decay(source(), newChildren.get(0), newChildren.get(1), newChildren.get(2), options != null ? newChildren.get(3) : null);

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2638,7 +2638,7 @@ public void testFullTextFunctionsInStats() {
26382638
}
26392639
}
26402640

2641-
public void testDecayFunctionNullArgs() {
2641+
public void testDecayArgs() {
26422642
assumeTrue("Decay function not enabled", EsqlCapabilities.Cap.DECAY_FUNCTION.isEnabled());
26432643

26442644
// First arg cannot be null
@@ -2670,6 +2670,49 @@ public void testDecayFunctionNullArgs() {
26702670
+ "| eval decay_result = decay(value, origin, null, {\"offset\": 0, \"decay\": 0.5, \"type\": \"linear\"})"
26712671
)
26722672
);
2673+
2674+
// Offset value type
2675+
assertEquals(
2676+
"2:23: offset option has invalid type, expected [numeric], found [keyword]",
2677+
error(
2678+
"row value = 10, origin = 10, scale = 1\n"
2679+
+ "| eval decay_result = decay(value, origin, scale, {\"offset\": \"aaa\", \"decay\": 0.5, \"type\": \"linear\"})"
2680+
)
2681+
);
2682+
2683+
assertEquals(
2684+
"1:118: offset option has invalid type, expected [time_duration], found [keyword]",
2685+
error(
2686+
"row value = TO_DATETIME(\"2023-01-01T00:00:00Z\"), origin = TO_DATETIME(\"2023-01-01T00:00:00Z\")"
2687+
+ "| eval decay_result = decay(value, origin, 24 hours, {\"offset\": \"aaa\", \"decay\": 0.5, \"type\": \"linear\"})"
2688+
)
2689+
);
2690+
2691+
assertEquals(
2692+
"1:110: offset option has invalid type, expected [numeric], found [keyword]",
2693+
error(
2694+
"row value = TO_CARTESIANPOINT(\"POINT(10 0)\"), origin = TO_CARTESIANPOINT(\"POINT(0 0)\")"
2695+
+ "| eval decay_result = decay(value, origin, 10.0, {\"offset\": \"aaa\", \"decay\": 0.5, \"type\": \"linear\"})"
2696+
)
2697+
);
2698+
2699+
// Type option value
2700+
assertEquals(
2701+
"2:23: Invalid option [type] in "
2702+
+ "[decay(value, origin, scale, {\"offset\": 1, \"decay\": 0.5, \"type\": 123})], allowed types [[KEYWORD]]",
2703+
error(
2704+
"row value = 10, origin = 10, scale = 1\n"
2705+
+ "| eval decay_result = decay(value, origin, scale, {\"offset\": 1, \"decay\": 0.5, \"type\": 123})"
2706+
)
2707+
);
2708+
2709+
assertEquals(
2710+
"2:23: type option has invalid value, expected one of [gauss, linear, exp], found [\"foobar\"]",
2711+
error(
2712+
"row value = 10, origin = 10, scale = 1\n"
2713+
+ "| eval decay_result = decay(value, origin, scale, {\"offset\": 1, \"decay\": 0.5, \"type\": \"foobar\"})"
2714+
)
2715+
);
26732716
}
26742717

26752718
private void checkFullTextFunctionsInStats(String functionInvocation) {

0 commit comments

Comments
 (0)