|
@@ -14,17 +14,21 @@ import org.elasticsearch.compute.data.Block;
|
|
import org.elasticsearch.compute.data.BlockUtils;
|
|
import org.elasticsearch.compute.data.BlockUtils;
|
|
import org.elasticsearch.compute.data.Page;
|
|
import org.elasticsearch.compute.data.Page;
|
|
import org.elasticsearch.compute.operator.EvalOperator;
|
|
import org.elasticsearch.compute.operator.EvalOperator;
|
|
|
|
+import org.elasticsearch.core.PathUtils;
|
|
|
|
+import org.elasticsearch.logging.LogManager;
|
|
import org.elasticsearch.test.ESTestCase;
|
|
import org.elasticsearch.test.ESTestCase;
|
|
import org.elasticsearch.xpack.esql.evaluator.EvalMapper;
|
|
import org.elasticsearch.xpack.esql.evaluator.EvalMapper;
|
|
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest;
|
|
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest;
|
|
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
|
|
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
|
|
import org.elasticsearch.xpack.esql.optimizer.FoldNull;
|
|
import org.elasticsearch.xpack.esql.optimizer.FoldNull;
|
|
|
|
+import org.elasticsearch.xpack.esql.plan.logical.show.ShowFunctions;
|
|
import org.elasticsearch.xpack.esql.planner.Layout;
|
|
import org.elasticsearch.xpack.esql.planner.Layout;
|
|
import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
|
|
import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
|
|
import org.elasticsearch.xpack.ql.expression.Expression;
|
|
import org.elasticsearch.xpack.ql.expression.Expression;
|
|
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
|
|
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
|
|
import org.elasticsearch.xpack.ql.expression.Literal;
|
|
import org.elasticsearch.xpack.ql.expression.Literal;
|
|
import org.elasticsearch.xpack.ql.expression.TypeResolutions;
|
|
import org.elasticsearch.xpack.ql.expression.TypeResolutions;
|
|
|
|
+import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
|
|
import org.elasticsearch.xpack.ql.tree.Source;
|
|
import org.elasticsearch.xpack.ql.tree.Source;
|
|
import org.elasticsearch.xpack.ql.type.DataType;
|
|
import org.elasticsearch.xpack.ql.type.DataType;
|
|
import org.elasticsearch.xpack.ql.type.DataTypes;
|
|
import org.elasticsearch.xpack.ql.type.DataTypes;
|
|
@@ -32,12 +36,20 @@ import org.elasticsearch.xpack.ql.type.EsField;
|
|
import org.elasticsearch.xpack.ql.util.NumericUtils;
|
|
import org.elasticsearch.xpack.ql.util.NumericUtils;
|
|
import org.elasticsearch.xpack.versionfield.Version;
|
|
import org.elasticsearch.xpack.versionfield.Version;
|
|
import org.hamcrest.Matcher;
|
|
import org.hamcrest.Matcher;
|
|
|
|
+import org.junit.After;
|
|
|
|
+import org.junit.AfterClass;
|
|
|
|
+import org.junit.BeforeClass;
|
|
|
|
|
|
|
|
+import java.io.IOException;
|
|
import java.math.BigInteger;
|
|
import java.math.BigInteger;
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
+import java.nio.file.Path;
|
|
import java.time.Duration;
|
|
import java.time.Duration;
|
|
import java.time.Period;
|
|
import java.time.Period;
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Arrays;
|
|
|
|
+import java.util.Collections;
|
|
|
|
+import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Locale;
|
|
@@ -624,6 +636,7 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
|
|
}
|
|
}
|
|
|
|
|
|
return suppliers;
|
|
return suppliers;
|
|
|
|
+
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -754,4 +767,111 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
|
|
private static Stream<DataType> representable() {
|
|
private static Stream<DataType> representable() {
|
|
return EsqlDataTypes.types().stream().filter(EsqlDataTypes::isRepresentable);
|
|
return EsqlDataTypes.types().stream().filter(EsqlDataTypes::isRepresentable);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ @AfterClass
|
|
|
|
+ public static void renderSignature() throws IOException {
|
|
|
|
+ FunctionDefinition definition = definition();
|
|
|
|
+ if (definition == null) {
|
|
|
|
+ LogManager.getLogger(getTestClass()).info("Skipping rendering signature because the function isn't registered");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ String rendered = RailRoadDiagram.functionSignature(definition);
|
|
|
|
+ LogManager.getLogger(getTestClass()).info("Writing function signature");
|
|
|
|
+ writeToTempDir("signature", rendered, "svg");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Unique signatures encountered by this test.
|
|
|
|
+ * <p>
|
|
|
|
+ * We clear this at the beginning of the test class with
|
|
|
|
+ * {@link #clearSignatures} out of paranoia. It <strong>is</strong>
|
|
|
|
+ * shared by many tests, after all.
|
|
|
|
+ * </p>
|
|
|
|
+ * <p>
|
|
|
|
+ * After each test method we add the signature it operated on via
|
|
|
|
+ * {@link #trackSignature}. Once the test class is done we render
|
|
|
|
+ * all the unique signatures to a temp file with {@link #renderTypesTable}.
|
|
|
|
+ * We use a temp file because that's all we're allowed to write to.
|
|
|
|
+ * Gradle will move the files into the docs after this is done.
|
|
|
|
+ * </p>
|
|
|
|
+ */
|
|
|
|
+ private static final Map<List<DataType>, DataType> signatures = new HashMap<>();
|
|
|
|
+
|
|
|
|
+ @BeforeClass
|
|
|
|
+ public static void clearSignatures() {
|
|
|
|
+ signatures.clear();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @After
|
|
|
|
+ public void trackSignature() {
|
|
|
|
+ if (testCase.expectedTypeError != null) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ if (testCase.getData().stream().anyMatch(t -> t.type == DataTypes.NULL)) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ signatures.putIfAbsent(testCase.getData().stream().map(TypedData::type).toList(), testCase.expectedType);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @AfterClass
|
|
|
|
+ public static void renderTypesTable() throws IOException {
|
|
|
|
+ FunctionDefinition definition = definition();
|
|
|
|
+ if (definition == null) {
|
|
|
|
+ LogManager.getLogger(getTestClass()).info("Skipping rendering types because the function isn't registered");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ StringBuilder header = new StringBuilder();
|
|
|
|
+ for (String arg : ShowFunctions.signature(definition)) {
|
|
|
|
+ header.append(arg).append(" | ");
|
|
|
|
+ }
|
|
|
|
+ header.append("result");
|
|
|
|
+
|
|
|
|
+ List<String> table = new ArrayList<>();
|
|
|
|
+ for (Map.Entry<List<DataType>, DataType> sig : signatures.entrySet()) {
|
|
|
|
+ StringBuilder b = new StringBuilder();
|
|
|
|
+ for (DataType arg : sig.getKey()) {
|
|
|
|
+ b.append(arg.typeName()).append(" | ");
|
|
|
|
+ }
|
|
|
|
+ b.append(sig.getValue().typeName());
|
|
|
|
+ table.add(b.toString());
|
|
|
|
+ }
|
|
|
|
+ Collections.sort(table);
|
|
|
|
+
|
|
|
|
+ String rendered = """
|
|
|
|
+ [%header.monospaced.styled,format=dsv,separator=|]
|
|
|
|
+ |===
|
|
|
|
+ """ + header + "\n" + table.stream().collect(Collectors.joining("\n")) + "\n|===\n";
|
|
|
|
+ LogManager.getLogger(getTestClass()).info("Writing function types:\n{}", rendered);
|
|
|
|
+ writeToTempDir("types", rendered, "asciidoc");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static FunctionDefinition definition() {
|
|
|
|
+ String name = functionName();
|
|
|
|
+ EsqlFunctionRegistry registry = new EsqlFunctionRegistry();
|
|
|
|
+ if (registry.functionExists(name)) {
|
|
|
|
+ return registry.resolveFunction(name);
|
|
|
|
+ }
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static String functionName() {
|
|
|
|
+ return getTestClass().getSimpleName().replace("Tests", "").toLowerCase(Locale.ROOT);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Write some text to a tempdir so we can copy it to the docs later.
|
|
|
|
+ * <p>
|
|
|
|
+ * We need to write to a tempdir instead of the docs because the tests
|
|
|
|
+ * don't have write permission to the docs.
|
|
|
|
+ * </p>
|
|
|
|
+ */
|
|
|
|
+ private static void writeToTempDir(String subdir, String str, String extension) throws IOException {
|
|
|
|
+ // We have to write to a tempdir because it's all test are allowed to write to. Gradle can move them.
|
|
|
|
+ Path dir = PathUtils.get(System.getProperty("java.io.tmpdir")).resolve("esql").resolve("functions").resolve(subdir);
|
|
|
|
+ Files.createDirectories(dir);
|
|
|
|
+ Path file = dir.resolve(functionName() + "." + extension);
|
|
|
|
+ Files.writeString(file, str);
|
|
|
|
+ }
|
|
}
|
|
}
|