diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/categorize.md b/docs/reference/query-languages/esql/_snippets/functions/layout/categorize.md index f9d782d304bfa..53b9e2f09ae49 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/categorize.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/categorize.md @@ -7,6 +7,7 @@ may be changed or removed in a future release. Elastic will work to fix any issu are not subject to the support SLA of official GA features. ::: + **Syntax** :::{image} ../../../images/functions/categorize.svg diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/kql.md b/docs/reference/query-languages/esql/_snippets/functions/layout/kql.md index b9adaac7f3e50..04c50cfded194 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/kql.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/kql.md @@ -7,6 +7,7 @@ may be changed or removed in a future release. Elastic will work to fix any issu are not subject to the support SLA of official GA features. ::: + **Syntax** :::{image} ../../../images/functions/kql.svg diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/match.md b/docs/reference/query-languages/esql/_snippets/functions/layout/match.md index 82d861909abfd..b5368c12e2b4a 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/match.md @@ -2,13 +2,14 @@ ## `MATCH` [esql-match] :::{warning} -###### COMING 9.1.0 - Do not use on production environments. This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. +::: -Support for optional named parameters is only available from 9.1.0 +:::{note} +###### Serverless: GA, Elastic Stack: COMING +Support for optional named parameters is only available in serverless, or in a future {{es}} release ::: **Syntax** diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/qstr.md b/docs/reference/query-languages/esql/_snippets/functions/layout/qstr.md index 19f6dda64ef1d..9154ed5343624 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/qstr.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/qstr.md @@ -2,13 +2,14 @@ ## `QSTR` [esql-qstr] :::{warning} -###### COMING 9.1.0 - Do not use on production environments. This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. +::: -Support for optional named parameters is only available from 9.1.0 +:::{note} +###### Serverless: GA, Elastic Stack: COMING +Support for optional named parameters is only available in serverless, or in a future {{es}} release ::: **Syntax** diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/term.md b/docs/reference/query-languages/esql/_snippets/functions/layout/term.md index 64967e792a59c..85499b3bffdc3 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/term.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/term.md @@ -7,6 +7,7 @@ may be changed or removed in a future release. Elastic will work to fix any issu are not subject to the support SLA of official GA features. ::: + **Syntax** :::{image} ../../../images/functions/term.svg diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/to_aggregate_metric_double.md b/docs/reference/query-languages/esql/_snippets/functions/layout/to_aggregate_metric_double.md index 19e9867c5e5b2..9c62b3d63ded8 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/to_aggregate_metric_double.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/to_aggregate_metric_double.md @@ -2,7 +2,8 @@ ## `TO_AGGREGATE_METRIC_DOUBLE` [esql-to_aggregate_metric_double] ```{applies_to} -product: COMING 9.1 +product: COMING +serverless: GA ``` **Syntax** diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/to_lower.md b/docs/reference/query-languages/esql/_snippets/functions/layout/to_lower.md index 0327f8a7579e1..e567ccf6fd1c4 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/to_lower.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/to_lower.md @@ -2,8 +2,7 @@ ## `TO_LOWER` [esql-to_lower] :::{note} -###### COMING 9.1.0 - +###### Serverless: GA, Elastic Stack: COMING 9.1.0 Support for multivalued parameters is only available from 9.1.0 ::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/to_upper.md b/docs/reference/query-languages/esql/_snippets/functions/layout/to_upper.md index bafcb218a54c9..1ab785c4b4cdf 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/to_upper.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/to_upper.md @@ -2,8 +2,7 @@ ## `TO_UPPER` [esql-to_upper] :::{note} -###### COMING 9.1.0 - +###### Serverless: GA, Elastic Stack: COMING 9.1.0 Support for multivalued parameters is only available from 9.1.0 ::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/values.md b/docs/reference/query-languages/esql/_snippets/functions/layout/values.md index 9cc069b0c4f58..0a2e9c2d5adc4 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/values.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/values.md @@ -2,13 +2,12 @@ ## `VALUES` [esql-values] :::{warning} -###### PREVIEW - Do not use on production environments. This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. ::: + **Syntax** :::{image} ../../../images/functions/values.svg diff --git a/docs/reference/query-languages/esql/esql-commands.md b/docs/reference/query-languages/esql/esql-commands.md index 386edc4e726f6..0da0d1c5c9a88 100644 --- a/docs/reference/query-languages/esql/esql-commands.md +++ b/docs/reference/query-languages/esql/esql-commands.md @@ -666,10 +666,6 @@ FROM employees ## `LOOKUP JOIN` [esql-lookup-join] ::::{warning} -```{applies_to} -stack: preview 9.0, coming 9.1 -serverless: preview -``` This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. :::: @@ -753,10 +749,6 @@ FROM Left ## `MV_EXPAND` [esql-mv_expand] ::::{warning} -```{applies_to} -stack: preview 9.0, coming 9.1 -serverless: preview -``` This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. :::: diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/to_aggregate_metric_double.md b/docs/reference/query-languages/esql/kibana/docs/functions/to_aggregate_metric_double.md index 569059d790b0e..d461121e036b2 100644 --- a/docs/reference/query-languages/esql/kibana/docs/functions/to_aggregate_metric_double.md +++ b/docs/reference/query-languages/esql/kibana/docs/functions/to_aggregate_metric_double.md @@ -5,7 +5,7 @@ This is generated by ESQL’s AbstractFunctionTestCase. Do no edit it. See ../RE ### TO_AGGREGATE_METRIC_DOUBLE Encode a numeric to an aggregate_metric_double. -``` +```esql ROW x = 3892095203 | EVAL agg_metric = TO_AGGREGATE_METRIC_DOUBLE(x) ``` diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesTo.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesTo.java index 8d9fae9761935..f8d5917314632 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesTo.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesTo.java @@ -17,4 +17,6 @@ String version() default ""; String description() default ""; + + boolean serverless() default true; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesToLifecycle.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesToLifecycle.java index 351c6d5b19fd7..5ad512e69c089 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesToLifecycle.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionAppliesToLifecycle.java @@ -8,12 +8,22 @@ package org.elasticsearch.xpack.esql.expression.function; public enum FunctionAppliesToLifecycle { - PREVIEW, - BETA, - DEVELOPMENT, - DEPRECATED, - COMING, - DISCONTINUED, - UNAVAILABLE, - GA + PREVIEW(true), + BETA(false), + DEVELOPMENT(false), + DEPRECATED(true), + COMING(true), + DISCONTINUED(false), + UNAVAILABLE(false), + GA(true); + + private final boolean serverless; + + FunctionAppliesToLifecycle(boolean serverless) { + this.serverless = serverless; + } + + public FunctionAppliesToLifecycle serverlessLifecycle() { + return serverless ? GA : this; + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Values.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Values.java index 7cda030d86039..5e05c3a448295 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Values.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Values.java @@ -23,8 +23,6 @@ import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.Example; -import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; -import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.FunctionType; import org.elasticsearch.xpack.esql.expression.function.Param; @@ -86,8 +84,7 @@ public class Values extends AggregateFunction implements ToAggregator { a [Circuit Breaker Error](docs-content://troubleshoot/elasticsearch/circuit-breaker-errors.md). ::::""", type = FunctionType.AGGREGATE, - examples = @Example(file = "string", tag = "values-grouped"), - appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.PREVIEW) } + examples = @Example(file = "string", tag = "values-grouped") ) public Values( Source source, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 6e74ca7084d4a..901521f928c0e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -165,8 +165,7 @@ public class Match extends FullTextFunction implements OptionalArgument, PostAna appliesTo = { @FunctionAppliesTo( lifeCycle = FunctionAppliesToLifecycle.COMING, - version = "9.1.0", - description = "Support for optional named parameters is only available from 9.1.0" + description = "Support for optional named parameters is only available in serverless, or in a future {{es}} release" ) } ) public Match( diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java index d426308eff775..662cd69e220a1 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java @@ -122,8 +122,7 @@ public class QueryString extends FullTextFunction implements OptionalArgument { appliesTo = { @FunctionAppliesTo( lifeCycle = FunctionAppliesToLifecycle.COMING, - version = "9.1.0", - description = "Support for optional named parameters is only available from 9.1.0" + description = "Support for optional named parameters is only available in serverless, or in a future {{es}} release" ) } ) public QueryString( diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToAggregateMetricDouble.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToAggregateMetricDouble.java index e6e2aa053ce22..23d4d895bb0fc 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToAggregateMetricDouble.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToAggregateMetricDouble.java @@ -71,7 +71,7 @@ public class ToAggregateMetricDouble extends AbstractConvertFunction { examples = { @Example(file = "convert", tag = "toAggregateMetricDouble"), @Example(description = "The expression also accepts multi-values", file = "convert", tag = "toAggregateMetricDoubleMv") }, - appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.COMING, version = "9.1") } + appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.COMING) } ) public ToAggregateMetricDouble( Source source, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3Support.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3Support.java index 8181281d04f2f..0d20d90a95c83 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3Support.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3Support.java @@ -71,7 +71,23 @@ import static org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry.mapParam; import static org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry.param; import static org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry.paramWithoutAnnotation; - +import static org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle.GA; + +/** + * This class exists to support the new Docs V3 system. + * Between Elasticsearch 8.x and 9.0 the reference documents were completely re-written, with several key changes: + *
    + *
  1. Port from ASCIIDOC to MD (markdown)
  2. + *
  3. Restructures all Elastic docs with clearer separate between reference docs and other docs
  4. + *
  5. All versions published from the main branch, + * requiring version specific information to be included in the docs as appropriate
  6. + *
  7. Including sub-docs inside bigger docs works differently, requiring a new directory structure
  8. + *
  9. Images and Kibana docs cannot be in the same location as snippets
  10. + *
+ * + * For these reasons the docs generating code that used to live inside AbstractFunctionTestCase has been pulled out + * and partially re-written to satisfy the above requirements. + */ public abstract class DocsV3Support { static final String DOCS_WARNING = @@ -190,6 +206,7 @@ public static void renderNegatedOperator( operatorEntry("match_operator", ":", MatchOperator.class, OperatorCategory.SEARCH) ); + /** Each grouping represents a subsection in the docs. Currently, this is manually maintained, but could be partially automated */ public enum OperatorCategory { BINARY, UNARY, @@ -200,6 +217,7 @@ public enum OperatorCategory { SEARCH } + /** Since operators do not exist in the function registry, we need an equivalent registry here in the docs generating code */ public record OperatorConfig(String name, String symbol, Class clazz, OperatorCategory category, boolean variadic) {} private static Map.Entry operatorEntry( @@ -213,19 +231,53 @@ private static Map.Entry operatorEntry( } private static Map.Entry operatorEntry(String name, String symbol, Class clazz, OperatorCategory category) { - return entry(name, new OperatorConfig(name, symbol, clazz, category, false)); + return operatorEntry(name, symbol, clazz, category, false); + } + + @FunctionalInterface + interface TempFileWriter { + void writeToTempDir(Path dir, String extension, String str) throws IOException; + } + + private class DocsFileWriter implements TempFileWriter { + @Override + public void writeToTempDir(Path dir, String extension, String str) throws IOException { + Files.createDirectories(dir); + Path file = dir.resolve(name + "." + extension); + Files.writeString(file, str); + logger.info("Wrote to file: {}", file); + } } protected final String category; protected final String name; + protected final FunctionDefinition definition; protected final Logger logger; private final Supplier, DataType>> signatures; + private TempFileWriter tempFileWriter; private DocsV3Support(String category, String name, Class testClass, Supplier, DataType>> signatures) { + this(category, name, null, testClass, signatures); + } + + private DocsV3Support( + String category, + String name, + FunctionDefinition definition, + Class testClass, + Supplier, DataType>> signatures + ) { this.category = category; this.name = name; + this.definition = definition == null ? definition(name) : definition; this.logger = LogManager.getLogger(testClass); this.signatures = signatures; + this.tempFileWriter = new DocsFileWriter(); + } + + /** Used in tests to capture output for asserting on the content */ + void setTempFileWriter(TempFileWriter tempFileWriter) { + this.tempFileWriter = tempFileWriter; } String replaceLinks(String text) { @@ -235,7 +287,7 @@ String replaceLinks(String text) { private String replaceAsciidocLinks(String text) { Pattern pattern = Pattern.compile("<<([^>]*)>>"); Matcher matcher = pattern.matcher(text); - StringBuffer result = new StringBuffer(); + StringBuilder result = new StringBuilder(); while (matcher.find()) { String match = matcher.group(1); matcher.appendReplacement(result, getLink(match)); @@ -245,10 +297,10 @@ private String replaceAsciidocLinks(String text) { } private String replaceMacros(String text) { - Pattern pattern = Pattern.compile("\\{([^}]+)}(/[^\\[]+)\\[([^]]+)\\]"); + Pattern pattern = Pattern.compile("\\{([^}]+)}(/[^\\[]+)\\[([^]]+)]"); Matcher matcher = pattern.matcher(text); - StringBuffer result = new StringBuffer(); + StringBuilder result = new StringBuilder(); while (matcher.find()) { String macro = matcher.group(1); String path = matcher.group(2); @@ -346,7 +398,7 @@ private String makeLink(String key, String prefix, String parentFile) { void writeToTempImageDir(String str) 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("images").resolve(category); - writeToTempDir(dir, "svg", str); + tempFileWriter.writeToTempDir(dir, "svg", str); } void writeToTempSnippetsDir(String subdir, String str) throws IOException { @@ -356,20 +408,13 @@ void writeToTempSnippetsDir(String subdir, String str) throws IOException { .resolve("_snippets") .resolve(category) .resolve(subdir); - writeToTempDir(dir, "md", str); + tempFileWriter.writeToTempDir(dir, "md", str); } void writeToTempKibanaDir(String subdir, String extension, String str) 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("kibana").resolve(subdir).resolve(category); - writeToTempDir(dir, extension, str); - } - - private void writeToTempDir(Path dir, String extension, String str) throws IOException { - Files.createDirectories(dir); - Path file = dir.resolve(name + "." + extension); - Files.writeString(file, str); - logger.info("Wrote to file: {}", file); + tempFileWriter.writeToTempDir(dir, extension, str); } protected abstract void renderSignature() throws IOException; @@ -381,20 +426,37 @@ private FunctionDocsSupport(String name, Class testClass) { super("functions", name, testClass, () -> AbstractFunctionTestCase.signatures(testClass)); } + FunctionDocsSupport( + String name, + Class testClass, + FunctionDefinition definition, + Supplier, DataType>> signatures + ) { + super("functions", name, definition, testClass, signatures); + } + @Override protected void renderSignature() throws IOException { String rendered = buildFunctionSignatureSvg(); if (rendered == null) { - logger.info("Skipping rendering signature because the function isn't registered"); + logger.info("Skipping rendering signature because the function '{}' isn't registered", name); } else { - logger.info("Writing function signature"); + logger.info("Writing function signature: {}", name); writeToTempImageDir(rendered); } } @Override protected void renderDocs() throws IOException { - FunctionDefinition definition = definition(name); + if (definition == null) { + logger.info("Skipping rendering docs because the function '{}' isn't registered", name); + } else { + logger.info("Rendering function docs: {}", name); + renderDocs(definition); + } + } + + private void renderDocs(FunctionDefinition definition) throws IOException { EsqlFunctionRegistry.FunctionDescription description = EsqlFunctionRegistry.description(definition); if (name.equals("case")) { /* @@ -460,25 +522,19 @@ private String makeCallout(String type, String text) { } private String makePreviewText(boolean preview, FunctionAppliesTo[] functionAppliesTos) { - StringBuilder previewDescription = new StringBuilder(); - for (FunctionAppliesTo appliesTo : functionAppliesTos) { - if (appliesTo.description().isEmpty() == false) { - previewDescription.append(appliesTo.description()).append("\n"); - } - preview = preview || appliesTo.lifeCycle() == FunctionAppliesToLifecycle.PREVIEW; - } String appliesToTextWithAT = appliesToText(functionAppliesTos); String appliesToText = appliesToTextWithoutAppliesTo(functionAppliesTos); StringBuilder previewText = new StringBuilder(); if (preview) { // We have a preview flag, use the WARNING callout - previewText.append(makeCallout("warning", appliesToText + "\n" + PREVIEW_CALLOUT + "\n" + previewDescription + "\n")); - } else if (previewDescription.isEmpty() == false) { - // We have extra descriptive text, nest inside a NOTE for emphasis - previewText.append(makeCallout("note", appliesToText + "\n" + previewDescription)); - } else if (appliesToTextWithAT.isEmpty() == false) { + previewText.append(makeCallout("warning", "\n" + PREVIEW_CALLOUT + "\n")).append("\n"); + } + if (appliesToTextWithAT.isEmpty() == false) { // No additional text, just use the plan applies_to syntax previewText.append(appliesToTextWithAT); + } else if (appliesToText.isEmpty() == false) { + // We have extra descriptive text, nest inside a NOTE for emphasis + previewText.append(makeCallout("note", appliesToText)); } return previewText.toString(); } @@ -488,11 +544,16 @@ private String appliesToText(FunctionAppliesTo[] functionAppliesTos) { if (functionAppliesTos.length > 0) { appliesToText.append("```{applies_to}\n"); for (FunctionAppliesTo appliesTo : functionAppliesTos) { - appliesToText.append("product: ") - .append(appliesTo.lifeCycle().name()) - .append(" ") - .append(appliesTo.version()) - .append("\n"); + if (appliesTo.description().isEmpty() == false) { + // If any of the appliesTo has descriptive text, we need to format things differently + return ""; + } + appliesToText.append("product: "); + appendLifeCycleAndVersion(appliesToText, appliesTo); + appliesToText.append("\n"); + if (appliesTo.serverless() && appliesTo.lifeCycle().serverlessLifecycle() == GA) { + appliesToText.append("serverless: ").append(GA).append("\n"); + } } appliesToText.append("```\n"); } @@ -505,12 +566,26 @@ private String appliesToTextWithoutAppliesTo(FunctionAppliesTo[] functionApplies appliesToText.append("\n"); for (FunctionAppliesTo appliesTo : functionAppliesTos) { appliesToText.append("###### "); - appliesToText.append(appliesTo.lifeCycle().name()).append(" ").append(appliesTo.version()).append("\n"); + if (appliesTo.serverless() && appliesTo.lifeCycle().serverlessLifecycle() == GA) { + appliesToText.append("Serverless: ").append(GA).append(", Elastic Stack: "); + } + appendLifeCycleAndVersion(appliesToText, appliesTo); + appliesToText.append("\n"); + if (appliesTo.description().isEmpty() == false) { + appliesToText.append(appliesTo.description()).append("\n\n"); + } } } return appliesToText.toString(); } + private void appendLifeCycleAndVersion(StringBuilder appliesToText, FunctionAppliesTo appliesTo) { + appliesToText.append(appliesTo.lifeCycle().name()); + if (appliesTo.version().isEmpty() == false) { + appliesToText.append(" ").append(appliesTo.version()); + } + } + private void renderFullLayout( boolean preview, FunctionAppliesTo[] functionAppliesTos, @@ -559,6 +634,7 @@ private String addInclude(String section) { } } + /** Operator specific docs generating, since it is currently quite different from the function docs generating */ public static class OperatorsDocsSupport extends DocsV3Support { private final OperatorConfig op; @@ -713,7 +789,6 @@ void renderDetailedDescription(String detailedDescription, String note) throws I } protected String buildFunctionSignatureSvg() throws IOException { - FunctionDefinition definition = definition(name); return (definition != null) ? RailRoadDiagram.functionSignature(definition) : null; } @@ -750,21 +825,7 @@ void renderTypes(String name, List args) thro if (sig.getKey().size() > argNames.size()) { // skip variadic [test] cases (but not those with optional parameters) continue; } - StringBuilder b = new StringBuilder("| "); - for (int i = 0; i < sig.getKey().size(); i++) { - DataType argType = sig.getKey().get(i); - EsqlFunctionRegistry.ArgSignature argSignature = args.get(i); - if (argSignature.mapArg()) { - b.append("named parameters"); - } else { - b.append(argType.esNameIfPossible()); - } - b.append(" | "); - } - b.append("| ".repeat(argNames.size() - sig.getKey().size())); - b.append(sig.getValue().esNameIfPossible()); - b.append(" |"); - table.add(b.toString()); + table.add(getTypeRow(args, sig, argNames)); } Collections.sort(table); if (table.isEmpty()) { @@ -775,11 +836,33 @@ void renderTypes(String name, List args) thro String rendered = DOCS_WARNING + """ **Supported types** - """ + header + "\n" + separator + "\n" + table.stream().collect(Collectors.joining("\n")) + "\n\n"; + """ + header + "\n" + separator + "\n" + String.join("\n", table) + "\n\n"; logger.info("Writing function types for [{}]:\n{}", name, rendered); writeToTempSnippetsDir("types", rendered); } + private static String getTypeRow( + List args, + Map.Entry, DataType> sig, + List argNames + ) { + StringBuilder b = new StringBuilder("| "); + for (int i = 0; i < sig.getKey().size(); i++) { + DataType argType = sig.getKey().get(i); + EsqlFunctionRegistry.ArgSignature argSignature = args.get(i); + if (argSignature.mapArg()) { + b.append("named parameters"); + } else { + b.append(argType.esNameIfPossible()); + } + b.append(" | "); + } + b.append("| ".repeat(argNames.size() - sig.getKey().size())); + b.append(sig.getValue().esNameIfPossible()); + b.append(" |"); + return b.toString(); + } + void renderDescription(String description, String detailedDescription, String note) throws IOException { description = replaceLinks(description.trim()); note = replaceLinks(note); @@ -813,14 +896,14 @@ protected boolean renderExamples(FunctionInfo info) throws IOException { builder.append("**Examples**\n\n"); } for (Example example : info.examples()) { - if (example.description().length() > 0) { + if (example.description().isEmpty() == false) { builder.append(replaceLinks(example.description().trim())); builder.append("\n\n"); } String exampleQuery = loadExampleQuery(example); String exampleResult = loadExampleResult(example); builder.append(exampleQuery).append("\n").append(exampleResult).append("\n"); - if (example.explanation().length() > 0) { + if (example.explanation().isEmpty() == false) { builder.append("\n"); builder.append(replaceLinks(example.explanation().trim())); builder.append("\n\n"); @@ -954,12 +1037,12 @@ void renderKibanaFunctionDefinition(String name, FunctionInfo info, List, DataType>> sortedSignatures() { List, DataType>> sortedSignatures = new ArrayList<>(signatures.get().entrySet()); - Collections.sort(sortedSignatures, (lhs, rhs) -> { + sortedSignatures.sort((lhs, rhs) -> { int maxlen = Math.max(lhs.getKey().size(), rhs.getKey().size()); for (int i = 0; i < maxlen; i++) { if (lhs.getKey().size() <= i) { @@ -990,7 +1073,7 @@ protected boolean renderAppendix(String appendix) throws IOException { return true; } - private HashMap> examples = new HashMap<>(); + private final HashMap> examples = new HashMap<>(); protected String loadExampleQuery(Example example) throws IOException { return "```esql\n" + loadExample(example.file(), example.tag()) + "\n```\n"; @@ -1040,7 +1123,7 @@ protected Map loadExampleFile(String csvSpec) throws IOException currentLines = null; currentTag = null; } - } else if (currentTag != null) { + } else if (currentTag != null && currentLines != null) { currentLines.add(line); // Collect lines within the block } } @@ -1050,7 +1133,7 @@ protected Map loadExampleFile(String csvSpec) throws IOException protected String reformatExample(String tag, List lines) { if (tag.endsWith("-result")) { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); for (String line : lines) { sb.append(renderTableLine(line, sb.isEmpty())); } @@ -1070,7 +1153,7 @@ private String renderTableLine(String line, boolean header) { } private String renderTableLine(String[] columns) { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); sb.append("| "); for (int i = 0; i < columns.length; i++) { if (i > 0) { @@ -1083,7 +1166,7 @@ private String renderTableLine(String[] columns) { } private String renderTableSpacerLine(int columns) { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); sb.append("| "); for (int i = 0; i < columns; i++) { if (i > 0) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3SupportTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3SupportTests.java index e17127b98b0bc..a54bc4bb0230f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3SupportTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3SupportTests.java @@ -7,10 +7,20 @@ package org.elasticsearch.xpack.esql.expression.function; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.function.Function; +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 java.io.IOException; import java.lang.reflect.Constructor; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.hamcrest.Matchers.equalTo; @@ -193,6 +203,9 @@ public void testRenderingExampleResultEmojis() throws IOException { public void testRenderingExampleFromClass() throws IOException { String expected = """ + % This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + + **Examples** ```esql FROM employees @@ -261,21 +274,83 @@ public void testRenderingExampleFromClass() throws IOException { | --- | --- | | 1 | 0 | """; + TestDocsFileWriter tempFileWriter = renderTestClassDocs(); + String rendered = tempFileWriter.rendered.get("examples/count.md"); + assertThat(rendered.trim(), equalTo(expected.trim())); + } + + public void testRenderingLayoutFromClass() throws IOException { + String expected = """ + % This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + + ## `COUNT` [esql-count] + :::{warning} + Do not use on production environments. This functionality is in technical preview and + may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview + are not subject to the support SLA of official GA features. + ::: + + :::{note} + ###### Serverless: GA, Elastic Stack: COMING 9.1.0 + Support for optional named parameters is only available from 9.1.0 + + ###### DEVELOPMENT + The ability to generate more imaginative answers to the question is under development + + ###### DISCONTINUED 9.0.0 + The ability to count the number of emojis in a string has been discontinued since 9.0.0 + ::: + + **Syntax** + + :::{image} ../../../images/functions/count.svg + :alt: Embedded + :class: text-center + ::: + + + :::{include} ../parameters/count.md + ::: + + :::{include} ../description/count.md + ::: + + :::{include} ../types/count.md + ::: + + :::{include} ../examples/count.md + ::: + """; + TestDocsFileWriter tempFileWriter = renderTestClassDocs(); + String rendered = tempFileWriter.rendered.get("layout/count.md"); + assertThat(rendered.trim(), equalTo(expected.trim())); + } + + private TestDocsFileWriter renderTestClassDocs() throws IOException { FunctionInfo info = functionInfo(TestClass.class); assert info != null; - DocsV3Support docs = DocsV3Support.forFunctions("count", TestClass.class); - StringBuilder results = new StringBuilder(); - for (Example example : info.examples()) { - if (example.description().isEmpty() == false) { - results.append("\n"); - results.append(docs.replaceLinks(example.description().trim())); - results.append("\n"); - } - String query = docs.loadExampleQuery(example); - String result = docs.loadExampleResult(example); - results.append("\n").append(query).append("\n").append(result); + FunctionDefinition definition = EsqlFunctionRegistry.def(TestClass.class, TestClass::new, "count"); + var docs = new DocsV3Support.FunctionDocsSupport("count", TestClass.class, definition, TestClass::signatures); + TestDocsFileWriter tempFileWriter = new TestDocsFileWriter("count"); + docs.setTempFileWriter(tempFileWriter); + docs.renderDocs(); + return tempFileWriter; + } + + private class TestDocsFileWriter implements DocsV3Support.TempFileWriter { + private final String name; + private final Map rendered = new HashMap<>(); + + TestDocsFileWriter(String name) { + this.name = name; + } + + @Override + public void writeToTempDir(Path dir, String extension, String str) throws IOException { + String file = dir.getFileName() + "/" + name + "." + extension; + rendered.put(file, str); + logger.info("Wrote to file: {}", file); } - assertThat(results.toString(), equalTo(expected)); } private static FunctionInfo functionInfo(Class clazz) { @@ -301,9 +376,10 @@ private static Constructor constructorFor(Class clazz) { return constructors[0]; } - public static class TestClass { + public static class TestClass extends Function { @FunctionInfo( returnType = "long", + preview = true, description = "Returns the total number (count) of input values.", type = FunctionType.AGGREGATE, examples = { @@ -325,8 +401,56 @@ public static class TestClass { `NULL`s: `COUNT(TRUE)` and `COUNT(FALSE)` are both 1, but `COUNT(NULL)` is 0.""", file = "stats", tag = "count-or-null" + ) }, + appliesTo = { + @FunctionAppliesTo( + lifeCycle = FunctionAppliesToLifecycle.COMING, + version = "9.1.0", + description = "Support for optional named parameters is only available from 9.1.0" + ), + @FunctionAppliesTo( + lifeCycle = FunctionAppliesToLifecycle.DEVELOPMENT, + description = "The ability to generate more imaginative answers to the question is under development" + ), + @FunctionAppliesTo( + lifeCycle = FunctionAppliesToLifecycle.DISCONTINUED, + version = "9.0.0", + description = "The ability to count the number of emojis in a string has been discontinued since 9.0.0" ) } ) - public TestClass() {} + public TestClass(Source source, @Param(name = "str", type = { "keyword", "text" }, description = """ + String expression. If `null`, the function returns `null`. + The input can be a single- or multi-valued column or an expression.""") Expression field) { + super(source, List.of(field)); + } + + public static Map, DataType> signatures() { + return Map.of(List.of(DataType.KEYWORD), DataType.LONG); + } + + @Override + public DataType dataType() { + return DataType.LONG; + } + + @Override + public Expression replaceChildren(List newChildren) { + return new TestClass(source(), newChildren.getFirst()); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, TestClass::new, children().getFirst()); + } + + @Override + public String getWriteableName() { + throw new UnsupportedOperationException(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + throw new UnsupportedOperationException(); + } } }