|
@@ -0,0 +1,255 @@
|
|
|
+/*
|
|
|
+ * 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.expression.function.scalar.convert;
|
|
|
+
|
|
|
+import org.elasticsearch.common.io.stream.StreamOutput;
|
|
|
+import org.elasticsearch.compute.operator.EvalOperator;
|
|
|
+import org.elasticsearch.xpack.esql.core.expression.EntryExpression;
|
|
|
+import org.elasticsearch.xpack.esql.core.expression.Expression;
|
|
|
+import org.elasticsearch.xpack.esql.core.expression.Literal;
|
|
|
+import org.elasticsearch.xpack.esql.core.expression.MapExpression;
|
|
|
+import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
|
|
|
+import org.elasticsearch.xpack.esql.core.tree.Source;
|
|
|
+import org.elasticsearch.xpack.esql.core.type.DataType;
|
|
|
+import org.elasticsearch.xpack.esql.expression.SurrogateExpression;
|
|
|
+import org.elasticsearch.xpack.esql.expression.function.Example;
|
|
|
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
|
|
|
+import org.elasticsearch.xpack.esql.expression.function.MapParam;
|
|
|
+import org.elasticsearch.xpack.esql.expression.function.OptionalArgument;
|
|
|
+import org.elasticsearch.xpack.esql.expression.function.Param;
|
|
|
+import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
|
|
|
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isMapExpression;
|
|
|
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isTypeOrUnionType;
|
|
|
+import static org.elasticsearch.xpack.esql.core.type.DataType.IP;
|
|
|
+import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
|
|
|
+import static org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction.supportedTypesNames;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Converts strings to IPs.
|
|
|
+ * <p>
|
|
|
+ * IPv4 addresses have traditionally parsed quads with leading zeros in three
|
|
|
+ * mutually exclusive ways:
|
|
|
+ * </p>
|
|
|
+ * <ul>
|
|
|
+ * <li>As octal numbers. So {@code 1.1.010.1} becomes {@code 1.1.8.1}.</li>
|
|
|
+ * <li>As decimal numbers. So {@code 1.1.010.1} becomes {@code 1.1.10.1}.</li>
|
|
|
+ * <li>Rejects them entirely. So {@code 1.1.010.1} becomes {@code null} with a warning.</li>
|
|
|
+ * </ul>
|
|
|
+ * <p>
|
|
|
+ * These three ways of handling leading zeros are available with the optional
|
|
|
+ * {@code leading_zeros} named parameter. Set to {@code octal}, {@code decimal},
|
|
|
+ * or {@code reject}. If not sent this defaults to {@code reject} which has
|
|
|
+ * been Elasticsearch's traditional way of handling leading zeros for years.
|
|
|
+ * </p>
|
|
|
+ * <p>
|
|
|
+ * This doesn't extend from {@link AbstractConvertFunction} so that it can
|
|
|
+ * support a named parameter for the leading zeros behavior. Instead, it rewrites
|
|
|
+ * itself into either {@link ToIpLeadingZerosOctal}, {@link ToIpLeadingZerosDecimal},
|
|
|
+ * or {@link ToIpLeadingZerosRejected} which are all {@link AbstractConvertFunction}
|
|
|
+ * subclasses. This keeps the conversion code happy while still allowing us to
|
|
|
+ * expose a single method to users.
|
|
|
+ * </p>
|
|
|
+ */
|
|
|
+public class ToIp extends EsqlScalarFunction implements SurrogateExpression, OptionalArgument, ConvertFunction {
|
|
|
+ private static final String LEADING_ZEROS = "leading_zeros";
|
|
|
+ public static final Map<String, DataType> ALLOWED_OPTIONS = Map.ofEntries(Map.entry(LEADING_ZEROS, KEYWORD));
|
|
|
+
|
|
|
+ private final Expression field;
|
|
|
+ private final Expression options;
|
|
|
+
|
|
|
+ @FunctionInfo(
|
|
|
+ returnType = "ip",
|
|
|
+ description = "Converts an input string to an IP value.",
|
|
|
+ examples = {
|
|
|
+ @Example(file = "ip", tag = "to_ip", explanation = """
|
|
|
+ Note that in this example, the last conversion of the string isn’t possible.
|
|
|
+ When this happens, the result is a `null` value. In this case a _Warning_ header is added to the response.
|
|
|
+ The header will provide information on the source of the failure:
|
|
|
+
|
|
|
+ `"Line 1:68: evaluation of [TO_IP(str2)] failed, treating result as null. Only first 20 failures recorded."`
|
|
|
+
|
|
|
+ A following header will contain the failure reason and the offending value:
|
|
|
+
|
|
|
+ `"java.lang.IllegalArgumentException: 'foo' is not an IP string literal."`"""),
|
|
|
+ @Example(file = "ip", tag = "to_ip_leading_zeros_octal", explanation = """
|
|
|
+ Parse v4 addresses with leading zeros as octal. Like `ping` or `ftp`.
|
|
|
+ """),
|
|
|
+ @Example(file = "ip", tag = "to_ip_leading_zeros_decimal", explanation = """
|
|
|
+ Parse v4 addresses with leading zeros as decimal. Java's `InetAddress.getByName`.
|
|
|
+ """) }
|
|
|
+ )
|
|
|
+ public ToIp(
|
|
|
+ Source source,
|
|
|
+ @Param(
|
|
|
+ name = "field",
|
|
|
+ type = { "ip", "keyword", "text" },
|
|
|
+ description = "Input value. The input can be a single- or multi-valued column or an expression."
|
|
|
+ ) Expression field,
|
|
|
+ @MapParam(
|
|
|
+ name = "options",
|
|
|
+ params = {
|
|
|
+ @MapParam.MapParamEntry(
|
|
|
+ name = "leading_zeros",
|
|
|
+ type = "keyword",
|
|
|
+ valueHint = { "reject", "octal", "decimal" },
|
|
|
+ description = "What to do with leading 0s in IPv4 addresses."
|
|
|
+ ) },
|
|
|
+ description = "(Optional) Additional options.",
|
|
|
+ optional = true
|
|
|
+ ) Expression options
|
|
|
+ ) {
|
|
|
+ super(source, options == null ? List.of(field) : List.of(field, options));
|
|
|
+ this.field = field;
|
|
|
+ this.options = options;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getWriteableName() {
|
|
|
+ throw new UnsupportedOperationException("not serialized");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void writeTo(StreamOutput out) throws IOException {
|
|
|
+ throw new UnsupportedOperationException("not serialized");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public DataType dataType() {
|
|
|
+ return IP;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Expression replaceChildren(List<Expression> newChildren) {
|
|
|
+ return new ToIp(source(), newChildren.get(0), newChildren.size() == 1 ? null : newChildren.get(1));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected NodeInfo<? extends Expression> info() {
|
|
|
+ return NodeInfo.create(this, ToIp::new, field, options);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
|
|
|
+ throw new UnsupportedOperationException("should be rewritten");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Expression surrogate() {
|
|
|
+ return LeadingZeros.from((MapExpression) options).surrogate(source(), field);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Expression field() {
|
|
|
+ return field;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Set<DataType> supportedTypes() {
|
|
|
+ // All ToIpLeadingZeros* functions support the same input set. So we just pick one.
|
|
|
+ return ToIpLeadingZerosRejected.EVALUATORS.keySet();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected TypeResolution resolveType() {
|
|
|
+ if (childrenResolved() == false) {
|
|
|
+ return new TypeResolution("Unresolved children");
|
|
|
+ }
|
|
|
+ TypeResolution resolution = isTypeOrUnionType(
|
|
|
+ field,
|
|
|
+ ToIpLeadingZerosRejected.EVALUATORS::containsKey,
|
|
|
+ sourceText(),
|
|
|
+ null,
|
|
|
+ supportedTypesNames(supportedTypes())
|
|
|
+ );
|
|
|
+ if (resolution.unresolved()) {
|
|
|
+ return resolution;
|
|
|
+ }
|
|
|
+ if (options == null) {
|
|
|
+ return resolution;
|
|
|
+ }
|
|
|
+ resolution = isMapExpression(options, sourceText(), SECOND);
|
|
|
+ if (resolution.unresolved()) {
|
|
|
+ return resolution;
|
|
|
+ }
|
|
|
+ for (EntryExpression e : ((MapExpression) options).entryExpressions()) {
|
|
|
+ String key;
|
|
|
+ if (e.key().dataType() != KEYWORD) {
|
|
|
+ return new TypeResolution("map keys must be strings");
|
|
|
+ }
|
|
|
+ if (e.key() instanceof Literal keyl) {
|
|
|
+ key = (String) keyl.value();
|
|
|
+ } else {
|
|
|
+ return new TypeResolution("map keys must be literals");
|
|
|
+ }
|
|
|
+ DataType expected = ALLOWED_OPTIONS.get(key);
|
|
|
+ if (expected == null) {
|
|
|
+ return new TypeResolution("[" + key + "] is not a supported option");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (e.value().dataType() != expected) {
|
|
|
+ return new TypeResolution("[" + key + "] expects [" + expected + "] but was [" + e.value().dataType() + "]");
|
|
|
+ }
|
|
|
+ if (e.value() instanceof Literal == false) {
|
|
|
+ return new TypeResolution("map values must be literals");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ LeadingZeros.from((MapExpression) options);
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
+ return new TypeResolution(e.getMessage());
|
|
|
+ }
|
|
|
+ return TypeResolution.TYPE_RESOLVED;
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum LeadingZeros {
|
|
|
+ REJECT {
|
|
|
+ @Override
|
|
|
+ public Expression surrogate(Source source, Expression field) {
|
|
|
+ return new ToIpLeadingZerosRejected(source, field);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ DECIMAL {
|
|
|
+ @Override
|
|
|
+ public Expression surrogate(Source source, Expression field) {
|
|
|
+ return new ToIpLeadingZerosDecimal(source, field);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ OCTAL {
|
|
|
+ @Override
|
|
|
+ public Expression surrogate(Source source, Expression field) {
|
|
|
+ return new ToIpLeadingZerosOctal(source, field);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ public static LeadingZeros from(MapExpression exp) {
|
|
|
+ if (exp == null) {
|
|
|
+ return REJECT;
|
|
|
+ }
|
|
|
+ Expression e = exp.keyFoldedMap().get(LEADING_ZEROS);
|
|
|
+ return e == null ? REJECT : from((String) ((Literal) e).value());
|
|
|
+ }
|
|
|
+
|
|
|
+ public static LeadingZeros from(String str) {
|
|
|
+ return switch (str) {
|
|
|
+ case "reject" -> REJECT;
|
|
|
+ case "octal" -> OCTAL;
|
|
|
+ case "decimal" -> DECIMAL;
|
|
|
+ default -> throw new IllegalArgumentException("Illegal leading_zeros [" + str + "]");
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ public abstract Expression surrogate(Source source, Expression field);
|
|
|
+ }
|
|
|
+}
|