Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/user/ppl/cmd/timechart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ Syntax

.. code-block:: text

timechart [span=<time_interval>] [limit=<number>] [useother=<boolean>] <aggregation_function> [by <field>]
timechart [timefield=<field_name>] [span=<time_interval>] [limit=<number>] [useother=<boolean>] <aggregation_function> [by <field>]

**Parameters:**

* **timefield**: optional. Specifies the timestamp field to use for time interval grouping.

* Default: ``@timestamp``
* Specify a timestamp field for the time-based aggregation.

* **span**: optional. Specifies the time interval for grouping data.

* Default: 1m (1 minute)
Expand Down Expand Up @@ -105,7 +110,7 @@ Return type: DOUBLE
Notes
=====

* The ``timechart`` command requires a timestamp field named ``@timestamp`` in the data.
* The ``timechart`` command requires a timestamp field in the data. By default, it uses the ``@timestamp`` field, but you can specify a different field using the ``timefield`` parameter.
* Results are returned in an unpivoted format with separate rows for each time-field combination that has data.
* Only combinations with actual data are included in the results - empty combinations are omitted rather than showing null or zero values.
* The "top N" values for the ``limit`` parameter are selected based on the sum of values across all time intervals for each distinct field value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.opensearch.client.ResponseException;
import org.opensearch.sql.common.utils.StringUtils;
import org.opensearch.sql.ppl.PPLIntegTestCase;

public class CalciteTimechartCommandIT extends PPLIntegTestCase {
Expand Down Expand Up @@ -64,7 +65,7 @@ public void testTimechartWithMinuteSpanAndGroupBy() throws IOException {
}

@Test
public void testTimechartWithoutTimestampField() throws IOException {
public void testTimechartWithoutTimestampField() {
Throwable exception =
assertThrows(
ResponseException.class,
Expand All @@ -74,6 +75,16 @@ public void testTimechartWithoutTimestampField() throws IOException {
verifyErrorMessageContains(exception, "Field [@timestamp] not found.");
}

@Test
public void testTimechartWithCustomTimeField() throws IOException {
JSONObject result =
executeQuery(
StringUtils.format(
"source=%s | timechart timefield=birthdate span=1year count()", TEST_INDEX_BANK));
verifySchema(result, schema("birthdate", "timestamp"), schema("count()", "bigint"));
verifyDataRows(result, rows("2017-01-01 00:00:00", 2), rows("2018-01-01 00:00:00", 5));
}

@Test
public void testTimechartWithMinuteSpanNoGroupBy() throws IOException {
JSONObject result = executeQuery("source=events | timechart span=1m avg(cpu_usage)");
Expand Down
1 change: 1 addition & 0 deletions ppl/src/main/antlr/OpenSearchPPLLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ LIMIT: 'LIMIT';
USEOTHER: 'USEOTHER';
OTHERSTR: 'OTHERSTR';
NULLSTR: 'NULLSTR';
TIMEFIELD: 'TIMEFIELD';
INPUT: 'INPUT';
OUTPUT: 'OUTPUT';
PATH: 'PATH';
Expand Down
2 changes: 2 additions & 0 deletions ppl/src/main/antlr/OpenSearchPPLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ timechartParameter
: LIMIT EQUAL integerLiteral
| SPAN EQUAL spanLiteral
| USEOTHER EQUAL (booleanLiteral | ident)
| TIMEFIELD EQUAL (ident | stringLiteral)
;

spanLiteral
Expand Down Expand Up @@ -1562,6 +1563,7 @@ searchableKeyWord
| SED
| MAX_MATCH
| OFFSET_FIELD
| TIMEFIELD
| patternMethod
| patternMode
// AGGREGATIONS AND WINDOW
Expand Down
51 changes: 18 additions & 33 deletions ppl/src/main/java/org/opensearch/sql/ppl/parser/AstBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@
import org.opensearch.sql.ast.expression.SearchAnd;
import org.opensearch.sql.ast.expression.SearchExpression;
import org.opensearch.sql.ast.expression.SearchGroup;
import org.opensearch.sql.ast.expression.Span;
import org.opensearch.sql.ast.expression.SpanUnit;
import org.opensearch.sql.ast.expression.UnresolvedArgument;
import org.opensearch.sql.ast.expression.UnresolvedExpression;
import org.opensearch.sql.ast.expression.WindowFrame;
Expand Down Expand Up @@ -760,41 +758,28 @@ private List<UnresolvedExpression> parseAggTerms(
/** Timechart command. */
@Override
public UnresolvedPlan visitTimechartCommand(OpenSearchPPLParser.TimechartCommandContext ctx) {
UnresolvedExpression binExpression =
AstDSL.span(AstDSL.implicitTimestampField(), AstDSL.intLiteral(1), SpanUnit.m);
Integer limit = 10;
Boolean useOther = true;
// Process timechart parameters
for (OpenSearchPPLParser.TimechartParameterContext paramCtx : ctx.timechartParameter()) {
UnresolvedExpression param = internalVisitExpression(paramCtx);
if (param instanceof Span) {
binExpression = param;
} else if (param instanceof Literal literal) {
if (DataType.BOOLEAN.equals(literal.getType())) {
useOther = (Boolean) literal.getValue();
} else if (DataType.INTEGER.equals(literal.getType())
|| DataType.LONG.equals(literal.getType())) {
limit = (Integer) literal.getValue();
}
}
}
List<Argument> arguments = ArgumentFactory.getArgumentList(ctx, expressionBuilder);
ArgumentMap argMap = ArgumentMap.of(arguments);
Literal spanLiteral = argMap.getOrDefault("spanliteral", AstDSL.stringLiteral("1m"));
String timeFieldName =
Optional.ofNullable(argMap.get("timefield"))
.map(l -> (String) l.getValue())
.orElse(OpenSearchConstants.IMPLICIT_FIELD_TIMESTAMP);
Field spanField = AstDSL.field(timeFieldName);
Alias span =
AstDSL.alias(timeFieldName, AstDSL.spanFromSpanLengthLiteral(spanField, spanLiteral));
UnresolvedExpression aggregateFunction = parseAggTerms(List.of(ctx.statsAggTerm())).getFirst();

UnresolvedExpression byField =
ctx.fieldExpression() != null ? internalVisitExpression(ctx.fieldExpression()) : null;
List<Argument> arguments =
List.of(
new Argument("limit", AstDSL.intLiteral(limit)),
new Argument("useother", AstDSL.booleanLiteral(useOther)));
binExpression = AstDSL.alias(OpenSearchConstants.IMPLICIT_FIELD_TIMESTAMP, binExpression);
if (byField != null) {
byField =
AstDSL.alias(
StringUtils.unquoteIdentifier(getTextInQuery(ctx.fieldExpression())), byField);
}
Optional.ofNullable(ctx.fieldExpression())
.map(
f ->
AstDSL.alias(
StringUtils.unquoteIdentifier(getTextInQuery(f)),
internalVisitExpression(f)))
.orElse(null);
return Chart.builder()
.aggregationFunction(aggregateFunction)
.rowSplit(binExpression)
.rowSplit(span)
.columnSplit(byField)
.arguments(arguments)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ public UnresolvedExpression visitMaxOption(OpenSearchPPLParser.MaxOptionContext
return new Argument("max", (Literal) this.visit(ctx.integerLiteral()));
}

private QualifiedName visitIdentifiers(List<? extends ParserRuleContext> ctx) {
public QualifiedName visitIdentifiers(List<? extends ParserRuleContext> ctx) {
return new QualifiedName(
ctx.stream()
.map(RuleContext::getText)
Expand Down Expand Up @@ -978,47 +978,6 @@ public UnresolvedExpression visitTimeModifierValue(
return AstDSL.stringLiteral(osDateMathExpression);
}

@Override
public UnresolvedExpression visitTimechartParameter(
OpenSearchPPLParser.TimechartParameterContext ctx) {
UnresolvedExpression timechartParameter;
if (ctx.SPAN() != null) {
// Convert span=1h to span(@timestamp, 1h)
Literal spanLiteral = (Literal) visit(ctx.spanLiteral());
timechartParameter =
AstDSL.spanFromSpanLengthLiteral(AstDSL.implicitTimestampField(), spanLiteral);
} else if (ctx.LIMIT() != null) {
Literal limit = (Literal) visit(ctx.integerLiteral());
if ((Integer) limit.getValue() < 0) {
throw new IllegalArgumentException("Limit must be a non-negative number");
}
timechartParameter = limit;
} else if (ctx.USEOTHER() != null) {
UnresolvedExpression useOther;
if (ctx.booleanLiteral() != null) {
useOther = visit(ctx.booleanLiteral());
} else if (ctx.ident() != null) {
QualifiedName ident = visitIdentifiers(List.of(ctx.ident()));
String useOtherValue = ident.toString();
if ("true".equalsIgnoreCase(useOtherValue) || "t".equalsIgnoreCase(useOtherValue)) {
useOther = AstDSL.booleanLiteral(true);
} else if ("false".equalsIgnoreCase(useOtherValue) || "f".equalsIgnoreCase(useOtherValue)) {
useOther = AstDSL.booleanLiteral(false);
} else {
throw new IllegalArgumentException(
"Invalid useOther value: " + ctx.ident().getText() + ". Expected true/false or t/f");
}
} else {
throw new IllegalArgumentException("value for useOther must be a boolean or identifier");
}
timechartParameter = useOther;
} else {
throw new IllegalArgumentException(
String.format("A parameter of timechart must be a span, limit or useOther, got %s", ctx));
}
return timechartParameter;
}

/**
* Process time range expressions (EARLIEST='value' or LATEST='value') It creates a Comparison
* filter like @timestamp >= timeModifierValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SortFieldContext;
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.StreamstatsCommandContext;
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SuffixSortFieldContext;
import org.opensearch.sql.ppl.parser.AstExpressionBuilder;

/** Util class to get all arguments as a list from the PPL command. */
public class ArgumentFactory {
Expand Down Expand Up @@ -232,6 +233,60 @@ public static List<Argument> getArgumentList(ChartCommandContext ctx) {
return arguments;
}

public static List<Argument> getArgumentList(
OpenSearchPPLParser.TimechartCommandContext timechartCtx,
AstExpressionBuilder expressionBuilder) {
List<Argument> arguments = new ArrayList<>();
for (OpenSearchPPLParser.TimechartParameterContext ctx : timechartCtx.timechartParameter()) {
if (ctx.SPAN() != null) {
arguments.add(
new Argument("spanliteral", (Literal) expressionBuilder.visit(ctx.spanLiteral())));
} else if (ctx.LIMIT() != null) {
Literal limit = getArgumentValue(ctx.integerLiteral());
if ((Integer) limit.getValue() < 0) {
throw new IllegalArgumentException("Limit must be a non-negative number");
}
arguments.add(new Argument("limit", limit));
} else if (ctx.USEOTHER() != null) {
Literal useOther;
if (ctx.booleanLiteral() != null) {
useOther = getArgumentValue(ctx.booleanLiteral());
} else if (ctx.ident() != null) {
String identLiteral = expressionBuilder.visitIdentifiers(List.of(ctx.ident())).toString();
if ("true".equalsIgnoreCase(identLiteral) || "t".equalsIgnoreCase(identLiteral)) {
useOther = AstDSL.booleanLiteral(true);
} else if ("false".equalsIgnoreCase(identLiteral) || "f".equalsIgnoreCase(identLiteral)) {
useOther = AstDSL.booleanLiteral(false);
} else {
throw new IllegalArgumentException(
"Invalid useOther value: "
+ ctx.ident().getText()
+ ". Expected true/false or t/f");
}
} else {
throw new IllegalArgumentException("value for useOther must be a boolean or identifier");
}
arguments.add(new Argument("useother", useOther));
} else if (ctx.TIMEFIELD() != null) {
Literal timeField;
if (ctx.ident() != null) {
timeField =
AstDSL.stringLiteral(
expressionBuilder.visitIdentifiers(List.of(ctx.ident())).toString());
} else {
timeField = getArgumentValue(ctx.stringLiteral());
}
arguments.add(new Argument("timefield", timeField));
} else {
throw new IllegalArgumentException(
String.format(
"A parameter of timechart must be a span, limit, useother, or timefield, got %s",
ctx));
}
}
return arguments;
}

/**
* Get list of {@link Argument}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1249,10 +1249,7 @@ public void testTimechartWithPerSecondFunction() {
alias("@timestamp", span(field("@timestamp"), intLiteral(1), SpanUnit.of("m"))))
.columnSplit(null)
.aggregationFunction(alias("per_second(a)", aggregate("sum", field("a"))))
.arguments(
exprList(
argument("limit", intLiteral(10)),
argument("useother", booleanLiteral(true))))
.arguments(exprList())
.build(),
let(
field("per_second(a)"),
Expand Down Expand Up @@ -1281,10 +1278,7 @@ public void testTimechartWithPerMinuteFunction() {
alias("@timestamp", span(field("@timestamp"), intLiteral(1), SpanUnit.of("m"))))
.columnSplit(null)
.aggregationFunction(alias("per_minute(a)", aggregate("sum", field("a"))))
.arguments(
exprList(
argument("limit", intLiteral(10)),
argument("useother", booleanLiteral(true))))
.arguments(exprList())
.build(),
let(
field("per_minute(a)"),
Expand Down Expand Up @@ -1313,10 +1307,7 @@ public void testTimechartWithPerHourFunction() {
alias("@timestamp", span(field("@timestamp"), intLiteral(1), SpanUnit.of("m"))))
.columnSplit(null)
.aggregationFunction(alias("per_hour(a)", aggregate("sum", field("a"))))
.arguments(
exprList(
argument("limit", intLiteral(10)),
argument("useother", booleanLiteral(true))))
.arguments(exprList())
.build(),
let(
field("per_hour(a)"),
Expand Down Expand Up @@ -1345,10 +1336,7 @@ public void testTimechartWithPerDayFunction() {
alias("@timestamp", span(field("@timestamp"), intLiteral(1), SpanUnit.of("m"))))
.columnSplit(null)
.aggregationFunction(alias("per_day(a)", aggregate("sum", field("a"))))
.arguments(
exprList(
argument("limit", intLiteral(10)),
argument("useother", booleanLiteral(true))))
.arguments(exprList())
.build(),
let(
field("per_day(a)"),
Expand Down
Loading
Loading