Skip to content

Commit 22426ba

Browse files
committed
add UT and IT for timechart command
Signed-off-by: Kai Huang <ahkcs@amazon.com>
1 parent 8977a8f commit 22426ba

File tree

3 files changed

+145
-9
lines changed

3 files changed

+145
-9
lines changed

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteReverseCommandIT.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public void init() throws Exception {
2929
loadIndex(Index.BANK);
3030
loadIndex(Index.TIME_TEST_DATA);
3131
loadIndex(Index.STATE_COUNTRY);
32+
loadIndex(Index.EVENTS);
3233
}
3334

3435
@Test
@@ -454,4 +455,62 @@ public void testReverseWithTimestampAfterAggregation() throws IOException {
454455
// Categories: A=26, B=25, C=25, D=24
455456
verifyDataRows(result, rows(26, "A"), rows(25, "B"), rows(25, "C"), rows(24, "D"));
456457
}
458+
459+
// ==================== Timechart with Reverse tests ====================
460+
// These tests verify that reverse works correctly with timechart.
461+
// Timechart always adds a sort at the end of its plan (tier 1), so reverse
462+
// will find the collation via metadata query and flip the sort direction.
463+
464+
@Test
465+
public void testTimechartWithReverse() throws IOException {
466+
// Timechart adds ORDER BY @timestamp ASC at the end
467+
// Reverse should flip it to DESC, returning data in reverse chronological order
468+
JSONObject result = executeQuery("source=events | timechart span=1m count() | reverse");
469+
verifySchema(result, schema("@timestamp", "timestamp"), schema("count()", "bigint"));
470+
// Events data has timestamps at 00:00, 00:01, 00:02, 00:03, 00:04
471+
// Reversed order: 00:04, 00:03, 00:02, 00:01, 00:00
472+
verifyDataRowsInOrder(
473+
result,
474+
rows("2024-07-01 00:04:00", 1),
475+
rows("2024-07-01 00:03:00", 1),
476+
rows("2024-07-01 00:02:00", 1),
477+
rows("2024-07-01 00:01:00", 1),
478+
rows("2024-07-01 00:00:00", 1));
479+
}
480+
481+
@Test
482+
public void testTimechartWithCustomTimefieldAndReverse() throws IOException {
483+
// Test timechart with custom timefield (birthdate instead of @timestamp)
484+
// PR #4784 allows users to specify a custom timefield in timechart
485+
// The sort should be on the custom field, not @timestamp
486+
JSONObject result =
487+
executeQuery(
488+
String.format(
489+
"source=%s | timechart timefield=birthdate span=1year count() | reverse",
490+
TEST_INDEX_BANK));
491+
verifySchema(result, schema("birthdate", "timestamp"), schema("count()", "bigint"));
492+
// Bank data has birthdates in 2017 and 2018
493+
// Timechart groups by year: 2017 (2 records), 2018 (5 records)
494+
// Reversed order: 2018, 2017
495+
verifyDataRowsInOrder(result, rows("2018-01-01 00:00:00", 5), rows("2017-01-01 00:00:00", 2));
496+
}
497+
498+
@Test
499+
public void testTimechartWithGroupByAndReverse() throws IOException {
500+
// Test timechart with group by and reverse
501+
// The sort is on both @timestamp and the group by field
502+
JSONObject result = executeQuery("source=events | timechart span=1h count() by host | reverse");
503+
verifySchema(
504+
result,
505+
schema("@timestamp", "timestamp"),
506+
schema("host", "string"),
507+
schema("count()", "bigint"));
508+
// All events are in the same hour, so only one time bucket
509+
// Hosts are grouped and results are reversed
510+
verifyDataRows(
511+
result,
512+
rows("2024-07-01 00:00:00", "db-01", 1),
513+
rows("2024-07-01 00:00:00", "web-01", 2),
514+
rows("2024-07-01 00:00:00", "web-02", 2));
515+
}
457516
}

ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLStreamstatsTest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,8 @@ public void testStreamstatsWithReverse() {
229229
+ " LogicalSort(sort0=[$8], dir0=[DESC])\n"
230230
+ " LogicalSort(sort0=[$8], dir0=[ASC])\n"
231231
+ " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4],"
232-
+ " SAL=[$5], COMM=[$6], DEPTNO=[$7], __stream_seq__=[$8], max(SAL)=[CASE(IS NOT"
233-
+ " NULL($7), MAX($5) OVER (PARTITION BY $7 ROWS UNBOUNDED PRECEDING), null:DECIMAL(7,"
234-
+ " 2))])\n"
232+
+ " SAL=[$5], COMM=[$6], DEPTNO=[$7], __stream_seq__=[$8], max(SAL)=[MAX($5) OVER"
233+
+ " (PARTITION BY $7 ROWS UNBOUNDED PRECEDING)])\n"
235234
+ " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4],"
236235
+ " SAL=[$5], COMM=[$6], DEPTNO=[$7], __stream_seq__=[ROW_NUMBER() OVER ()])\n"
237236
+ " LogicalTableScan(table=[[scott, EMP]])\n";
@@ -240,9 +239,8 @@ public void testStreamstatsWithReverse() {
240239
String expectedSparkSql =
241240
"SELECT `EMPNO`, `ENAME`, `JOB`, `MGR`, `HIREDATE`, `SAL`, `COMM`, `DEPTNO`, `max(SAL)`\n"
242241
+ "FROM (SELECT `EMPNO`, `ENAME`, `JOB`, `MGR`, `HIREDATE`, `SAL`, `COMM`, `DEPTNO`,"
243-
+ " `__stream_seq__`, CASE WHEN `DEPTNO` IS NOT NULL THEN MAX(`SAL`) OVER (PARTITION"
244-
+ " BY `DEPTNO` ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) ELSE NULL END"
245-
+ " `max(SAL)`\n"
242+
+ " `__stream_seq__`, MAX(`SAL`) OVER (PARTITION BY `DEPTNO` ROWS BETWEEN UNBOUNDED"
243+
+ " PRECEDING AND CURRENT ROW) `max(SAL)`\n"
246244
+ "FROM (SELECT `EMPNO`, `ENAME`, `JOB`, `MGR`, `HIREDATE`, `SAL`, `COMM`, `DEPTNO`,"
247245
+ " ROW_NUMBER() OVER () `__stream_seq__`\n"
248246
+ "FROM `scott`.`EMP`) `t`\n"

ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLTimechartTest.java

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,28 @@ protected Frameworks.ConfigBuilder config(CalciteAssert.SchemaSpec... schemaSpec
5353
ImmutableList<Object[]> rows =
5454
ImmutableList.of(
5555
new Object[] {
56-
java.sql.Timestamp.valueOf("2024-07-01 00:00:00"), "web-01", "us-east", 45.2, 120
56+
java.sql.Timestamp.valueOf("2024-07-01 00:00:00"),
57+
java.sql.Timestamp.valueOf("2024-01-15 10:00:00"),
58+
"web-01",
59+
"us-east",
60+
45.2,
61+
120
5762
},
5863
new Object[] {
59-
java.sql.Timestamp.valueOf("2024-07-01 00:01:00"), "web-02", "us-west", 38.7, 150
64+
java.sql.Timestamp.valueOf("2024-07-01 00:01:00"),
65+
java.sql.Timestamp.valueOf("2024-02-20 11:00:00"),
66+
"web-02",
67+
"us-west",
68+
38.7,
69+
150
6070
},
6171
new Object[] {
62-
java.sql.Timestamp.valueOf("2024-07-01 00:02:00"), "web-01", "us-east", 55.3, 200
72+
java.sql.Timestamp.valueOf("2024-07-01 00:02:00"),
73+
java.sql.Timestamp.valueOf("2024-03-25 12:00:00"),
74+
"web-01",
75+
"us-east",
76+
55.3,
77+
200
6378
});
6479
schema.add("events", new EventsTable(rows));
6580
return Frameworks.newConfigBuilder()
@@ -347,6 +362,68 @@ public void testTimechartUsingZeroSpanShouldThrow() {
347362
verifyErrorMessageContains(t, "Zero or negative time interval not supported: 0h");
348363
}
349364

365+
// ==================== Timechart with Reverse tests ====================
366+
// These tests verify that reverse works correctly with timechart.
367+
// Timechart always adds a sort at the end of its plan, so reverse will
368+
// find the collation via metadata query (tier 1) and flip the sort direction.
369+
370+
@Test
371+
public void testTimechartWithReverse() {
372+
// Timechart adds ORDER BY @timestamp ASC at the end
373+
// Reverse should flip it to DESC
374+
String ppl = "source=events | timechart count() | reverse";
375+
RelNode root = getRelNode(ppl);
376+
// The plan should have two sorts: original ASC from timechart, then DESC from reverse
377+
String expectedLogical =
378+
"LogicalSort(sort0=[$0], dir0=[DESC])\n"
379+
+ " LogicalSort(sort0=[$0], dir0=[ASC])\n"
380+
+ " LogicalProject(@timestamp=[$0], count()=[$1])\n"
381+
+ " LogicalAggregate(group=[{0}], count()=[COUNT()])\n"
382+
+ " LogicalProject(@timestamp0=[SPAN($0, 1, 'm')])\n"
383+
+ " LogicalFilter(condition=[IS NOT NULL($0)])\n"
384+
+ " LogicalTableScan(table=[[scott, events]])\n";
385+
verifyLogical(root, expectedLogical);
386+
387+
String expectedSparkSql =
388+
"SELECT *\n"
389+
+ "FROM (SELECT SPAN(`@timestamp`, 1, 'm') `@timestamp`, COUNT(*) `count()`\n"
390+
+ "FROM `scott`.`events`\n"
391+
+ "WHERE `@timestamp` IS NOT NULL\n"
392+
+ "GROUP BY SPAN(`@timestamp`, 1, 'm')\n"
393+
+ "ORDER BY 1 NULLS LAST) `t3`\n"
394+
+ "ORDER BY `@timestamp` DESC NULLS FIRST";
395+
verifyPPLToSparkSQL(root, expectedSparkSql);
396+
}
397+
398+
@Test
399+
public void testTimechartWithCustomTimefieldAndReverse() {
400+
// Timechart with custom timefield should also work with reverse
401+
// The sort is on created_at (the custom field), not @timestamp
402+
String ppl = "source=events | timechart timefield=created_at span=1month count() | reverse";
403+
RelNode root = getRelNode(ppl);
404+
405+
// Verify the logical plan shows two sorts: ASC from timechart, DESC from reverse
406+
String expectedLogical =
407+
"LogicalSort(sort0=[$0], dir0=[DESC])\n"
408+
+ " LogicalSort(sort0=[$0], dir0=[ASC])\n"
409+
+ " LogicalProject(created_at=[$0], count()=[$1])\n"
410+
+ " LogicalAggregate(group=[{0}], count()=[COUNT()])\n"
411+
+ " LogicalProject(created_at0=[SPAN($1, 1, 'M')])\n"
412+
+ " LogicalFilter(condition=[IS NOT NULL($1)])\n"
413+
+ " LogicalTableScan(table=[[scott, events]])\n";
414+
verifyLogical(root, expectedLogical);
415+
416+
String expectedSparkSql =
417+
"SELECT *\n"
418+
+ "FROM (SELECT SPAN(`created_at`, 1, 'M') `created_at`, COUNT(*) `count()`\n"
419+
+ "FROM `scott`.`events`\n"
420+
+ "WHERE `created_at` IS NOT NULL\n"
421+
+ "GROUP BY SPAN(`created_at`, 1, 'M')\n"
422+
+ "ORDER BY 1 NULLS LAST) `t3`\n"
423+
+ "ORDER BY `created_at` DESC NULLS FIRST";
424+
verifyPPLToSparkSQL(root, expectedSparkSql);
425+
}
426+
350427
private UnresolvedPlan parsePPL(String query) {
351428
PPLSyntaxParser parser = new PPLSyntaxParser();
352429
AstBuilder astBuilder = new AstBuilder(query);
@@ -363,6 +440,8 @@ public static class EventsTable implements ScannableTable {
363440
.builder()
364441
.add("@timestamp", SqlTypeName.TIMESTAMP)
365442
.nullable(true)
443+
.add("created_at", SqlTypeName.TIMESTAMP)
444+
.nullable(true)
366445
.add("host", SqlTypeName.VARCHAR)
367446
.nullable(true)
368447
.add("region", SqlTypeName.VARCHAR)

0 commit comments

Comments
 (0)