From e3879fd97e902d61403967efbe4ac1f16a22b7bb Mon Sep 17 00:00:00 2001
From: Yuanchun Shen <yuanchu@amazon.com>
Date: Thu, 30 Oct 2025 10:07:13 +0800
Subject: [PATCH] Support millisecond span (#4672)

* Support millisecond span

Signed-off-by: Yuanchun Shen <yuanchu@amazon.com>

* Update per funciton tests

Signed-off-by: Yuanchun Shen <yuanchu@amazon.com>

---------

Signed-off-by: Yuanchun Shen <yuanchu@amazon.com>
(cherry picked from commit b224750d9bb865aa6695055c6ec4246485f597c2)
---
 .../sql/ast/expression/IntervalUnit.java      |  2 +-
 .../sql/ast/expression/SpanUnit.java          |  2 +
 .../opensearch/sql/ast/tree/Timechart.java    | 10 +-
 .../sql/calcite/ExtendedRexBuilder.java       | 21 +++++
 .../sql/calcite/utils/PlanUtils.java          |  9 +-
 .../datetime/DateTimeFunctions.java           |  9 +-
 .../sql/ast/tree/TimechartTest.java           | 20 ++--
 .../sql/calcite/remote/CalciteExplainIT.java  | 16 ++--
 .../rest-api-spec/test/issues/4550.yml        | 93 +++++++++++++++++++
 ppl/src/main/antlr/OpenSearchPPLLexer.g4      |  4 +-
 ppl/src/main/antlr/OpenSearchPPLParser.g4     |  2 +
 .../ppl/calcite/CalcitePPLTimechartTest.java  | 24 ++---
 .../sql/ppl/parser/AstBuilderTest.java        | 16 ++--
 13 files changed, 181 insertions(+), 47 deletions(-)
 create mode 100644 integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/4550.yml

diff --git a/core/src/main/java/org/opensearch/sql/ast/expression/IntervalUnit.java b/core/src/main/java/org/opensearch/sql/ast/expression/IntervalUnit.java
index 19e1b07e39b..e2da2eabd7e 100644
--- a/core/src/main/java/org/opensearch/sql/ast/expression/IntervalUnit.java
+++ b/core/src/main/java/org/opensearch/sql/ast/expression/IntervalUnit.java
@@ -14,8 +14,8 @@
 @RequiredArgsConstructor
 public enum IntervalUnit {
   UNKNOWN,
-
   MICROSECOND,
+  MILLISECOND,
   SECOND,
   MINUTE,
   HOUR,
diff --git a/core/src/main/java/org/opensearch/sql/ast/expression/SpanUnit.java b/core/src/main/java/org/opensearch/sql/ast/expression/SpanUnit.java
index 8a78dd4eddd..43f4c8ab497 100644
--- a/core/src/main/java/org/opensearch/sql/ast/expression/SpanUnit.java
+++ b/core/src/main/java/org/opensearch/sql/ast/expression/SpanUnit.java
@@ -15,6 +15,8 @@
 public enum SpanUnit {
   UNKNOWN("unknown"),
   NONE(""),
+  MICROSECOND("us"),
+  US("us"),
   MILLISECOND("ms"),
   MS("ms"),
   SECONDS("s"),
diff --git a/core/src/main/java/org/opensearch/sql/ast/tree/Timechart.java b/core/src/main/java/org/opensearch/sql/ast/tree/Timechart.java
index 17e34ce564c..19972358721 100644
--- a/core/src/main/java/org/opensearch/sql/ast/tree/Timechart.java
+++ b/core/src/main/java/org/opensearch/sql/ast/tree/Timechart.java
@@ -10,7 +10,7 @@
 import static org.opensearch.sql.ast.dsl.AstDSL.eval;
 import static org.opensearch.sql.ast.dsl.AstDSL.function;
 import static org.opensearch.sql.ast.dsl.AstDSL.stringLiteral;
-import static org.opensearch.sql.ast.expression.IntervalUnit.SECOND;
+import static org.opensearch.sql.ast.expression.IntervalUnit.MILLISECOND;
 import static org.opensearch.sql.ast.tree.Timechart.PerFunctionRateExprBuilder.sum;
 import static org.opensearch.sql.ast.tree.Timechart.PerFunctionRateExprBuilder.timestampadd;
 import static org.opensearch.sql.ast.tree.Timechart.PerFunctionRateExprBuilder.timestampdiff;
@@ -112,11 +112,13 @@ private UnresolvedPlan transformPerFunction() {
     Span span = (Span) this.binExpression;
     Field spanStartTime = AstDSL.field(IMPLICIT_FIELD_TIMESTAMP);
     Function spanEndTime = timestampadd(span.getUnit(), span.getValue(), spanStartTime);
-    Function spanSeconds = timestampdiff(SECOND, spanStartTime, spanEndTime);
-
+    Function spanMillis = timestampdiff(MILLISECOND, spanStartTime, spanEndTime);
+    final int SECOND_IN_MILLISECOND = 1000;
     return eval(
         timechart(AstDSL.alias(perFunc.aggName, sum(perFunc.aggArg))),
-        let(perFunc.aggName).multiply(perFunc.seconds).dividedBy(spanSeconds));
+        let(perFunc.aggName)
+            .multiply(perFunc.seconds * SECOND_IN_MILLISECOND)
+            .dividedBy(spanMillis));
   }
 
   private Timechart timechart(UnresolvedExpression newAggregateFunction) {
diff --git a/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java b/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java
index b1078f2d320..35db7c064fd 100644
--- a/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java
+++ b/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java
@@ -56,38 +56,59 @@ public RelDataType commonType(RexNode... nodes) {
   public SqlIntervalQualifier createIntervalUntil(SpanUnit unit) {
     TimeUnit timeUnit;
     switch (unit) {
+      case MICROSECOND:
+      case US:
+        timeUnit = TimeUnit.MICROSECOND;
+        break;
       case MILLISECOND:
       case MS:
         timeUnit = TimeUnit.MILLISECOND;
         break;
+      case SECONDS:
       case SECOND:
+      case SECS:
+      case SEC:
       case S:
         timeUnit = TimeUnit.SECOND;
         break;
+      case MINUTES:
       case MINUTE:
+      case MINS:
+      case MIN:
       case m:
         timeUnit = TimeUnit.MINUTE;
         break;
+      case HOURS:
       case HOUR:
+      case HRS:
+      case HR:
       case H:
         timeUnit = TimeUnit.HOUR;
         break;
+      case DAYS:
       case DAY:
       case D:
         timeUnit = TimeUnit.DAY;
         break;
+      case WEEKS:
       case WEEK:
       case W:
         timeUnit = TimeUnit.WEEK;
         break;
+      case MONTHS:
       case MONTH:
+      case MON:
       case M:
         timeUnit = TimeUnit.MONTH;
         break;
+      case QUARTERS:
       case QUARTER:
+      case QTRS:
+      case QTR:
       case Q:
         timeUnit = TimeUnit.QUARTER;
         break;
+      case YEARS:
       case YEAR:
       case Y:
         timeUnit = TimeUnit.YEAR;
diff --git a/core/src/main/java/org/opensearch/sql/calcite/utils/PlanUtils.java b/core/src/main/java/org/opensearch/sql/calcite/utils/PlanUtils.java
index 1f755d50df1..7476df97db5 100644
--- a/core/src/main/java/org/opensearch/sql/calcite/utils/PlanUtils.java
+++ b/core/src/main/java/org/opensearch/sql/calcite/utils/PlanUtils.java
@@ -70,8 +70,10 @@ static SpanUnit intervalUnitToSpanUnit(IntervalUnit unit) {
     SpanUnit result;
     switch (unit) {
       case MICROSECOND:
-        result = SpanUnit.MILLISECOND;
+        result = SpanUnit.MICROSECOND;
         break;
+        case MILLISECOND:
+            result = SpanUnit.MILLISECOND;
       case SECOND:
         result = SpanUnit.SECOND;
         break;
@@ -108,9 +110,12 @@ static SpanUnit intervalUnitToSpanUnit(IntervalUnit unit) {
 
   static IntervalUnit spanUnitToIntervalUnit(SpanUnit unit) {
     switch (unit) {
+      case MICROSECOND:
+      case US:
+        return IntervalUnit.MICROSECOND;
       case MILLISECOND:
       case MS:
-        return IntervalUnit.MICROSECOND;
+        return IntervalUnit.MILLISECOND;
       case SECOND:
       case SECONDS:
       case SEC:
diff --git a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunctions.java b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunctions.java
index 40c306fd29b..29b330b73f2 100644
--- a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunctions.java
+++ b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunctions.java
@@ -8,6 +8,7 @@
 import static java.time.temporal.ChronoUnit.DAYS;
 import static java.time.temporal.ChronoUnit.HOURS;
 import static java.time.temporal.ChronoUnit.MICROS;
+import static java.time.temporal.ChronoUnit.MILLIS;
 import static java.time.temporal.ChronoUnit.MINUTES;
 import static java.time.temporal.ChronoUnit.MONTHS;
 import static java.time.temporal.ChronoUnit.SECONDS;
@@ -2152,6 +2153,9 @@ public static ExprValue exprTimestampAdd(
       case "MICROSECOND":
         temporalUnit = MICROS;
         break;
+      case "MILLISECOND":
+        temporalUnit = MILLIS;
+        break;
       case "SECOND":
         temporalUnit = SECONDS;
         break;
@@ -2191,10 +2195,13 @@ public static ExprValue exprTimestampAddForTimeType(
 
   private ExprValue getTimeDifference(String part, LocalDateTime startTime, LocalDateTime endTime) {
     long returnVal;
-    switch (part) {
+    switch (part.toUpperCase(Locale.ROOT)) {
       case "MICROSECOND":
         returnVal = MICROS.between(startTime, endTime);
         break;
+      case "MILLISECOND":
+        returnVal = MILLIS.between(startTime, endTime);
+        break;
       case "SECOND":
         returnVal = SECONDS.between(startTime, endTime);
         break;
diff --git a/core/src/test/java/org/opensearch/sql/ast/tree/TimechartTest.java b/core/src/test/java/org/opensearch/sql/ast/tree/TimechartTest.java
index 85e4de0462f..d587ff71787 100644
--- a/core/src/test/java/org/opensearch/sql/ast/tree/TimechartTest.java
+++ b/core/src/test/java/org/opensearch/sql/ast/tree/TimechartTest.java
@@ -54,9 +54,9 @@ void should_transform_per_second_for_different_spans(
                 let(
                     "per_second(bytes)",
                     divide(
-                        multiply("per_second(bytes)", 1.0),
+                        multiply("per_second(bytes)", 1000.0),
                         timestampdiff(
-                            "SECOND",
+                            "MILLISECOND",
                             "@timestamp",
                             timestampadd(expectedIntervalUnit, spanValue, "@timestamp")))),
                 timechart(span(spanValue, spanUnit), alias("per_second(bytes)", sum("bytes")))));
@@ -73,9 +73,9 @@ void should_transform_per_minute_for_different_spans(
                 let(
                     "per_minute(bytes)",
                     divide(
-                        multiply("per_minute(bytes)", 60.0),
+                        multiply("per_minute(bytes)", 60000.0),
                         timestampdiff(
-                            "SECOND",
+                            "MILLISECOND",
                             "@timestamp",
                             timestampadd(expectedIntervalUnit, spanValue, "@timestamp")))),
                 timechart(span(spanValue, spanUnit), alias("per_minute(bytes)", sum("bytes")))));
@@ -92,9 +92,9 @@ void should_transform_per_hour_for_different_spans(
                 let(
                     "per_hour(bytes)",
                     divide(
-                        multiply("per_hour(bytes)", 3600.0),
+                        multiply("per_hour(bytes)", 3600000.0),
                         timestampdiff(
-                            "SECOND",
+                            "MILLISECOND",
                             "@timestamp",
                             timestampadd(expectedIntervalUnit, spanValue, "@timestamp")))),
                 timechart(span(spanValue, spanUnit), alias("per_hour(bytes)", sum("bytes")))));
@@ -111,9 +111,9 @@ void should_transform_per_day_for_different_spans(
                 let(
                     "per_day(bytes)",
                     divide(
-                        multiply("per_day(bytes)", 86400.0),
+                        multiply("per_day(bytes)", 8.64E7),
                         timestampdiff(
-                            "SECOND",
+                            "MILLISECOND",
                             "@timestamp",
                             timestampadd(expectedIntervalUnit, spanValue, "@timestamp")))),
                 timechart(span(spanValue, spanUnit), alias("per_day(bytes)", sum("bytes")))));
@@ -149,9 +149,9 @@ void should_preserve_all_fields_during_per_function_transformation() {
                 let(
                     "per_second(bytes)",
                     divide(
-                        multiply("per_second(bytes)", 1.0),
+                        multiply("per_second(bytes)", 1000.0),
                         timestampdiff(
-                            "SECOND", "@timestamp", timestampadd("MINUTE", 5, "@timestamp")))),
+                            "MILLISECOND", "@timestamp", timestampadd("MINUTE", 5, "@timestamp")))),
                 expected));
   }
 
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java
index 45eb0b84536..3b408302bbc 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java
@@ -444,8 +444,8 @@ public void testExplainTimechartPerSecond() throws IOException {
     var result = explainQueryToString("source=events | timechart span=2m per_second(cpu_usage)");
     assertTrue(
         result.contains(
-            "per_second(cpu_usage)=[DIVIDE(*($1, 1.0E0), "
-                + "TIMESTAMPDIFF('SECOND':VARCHAR, $0, TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
+            "per_second(cpu_usage)=[DIVIDE(*($1, 1000.0E0), TIMESTAMPDIFF('MILLISECOND':VARCHAR,"
+                + " $0, TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
     assertTrue(result.contains("per_second(cpu_usage)=[SUM($0)]"));
   }
 
@@ -454,8 +454,8 @@ public void testExplainTimechartPerMinute() throws IOException {
     var result = explainQueryToString("source=events | timechart span=2m per_minute(cpu_usage)");
     assertTrue(
         result.contains(
-            "per_minute(cpu_usage)=[DIVIDE(*($1, 60.0E0), "
-                + "TIMESTAMPDIFF('SECOND':VARCHAR, $0, TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
+            "per_minute(cpu_usage)=[DIVIDE(*($1, 60000.0E0), TIMESTAMPDIFF('MILLISECOND':VARCHAR,"
+                + " $0, TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
     assertTrue(result.contains("per_minute(cpu_usage)=[SUM($0)]"));
   }
 
@@ -464,8 +464,8 @@ public void testExplainTimechartPerHour() throws IOException {
     var result = explainQueryToString("source=events | timechart span=2m per_hour(cpu_usage)");
     assertTrue(
         result.contains(
-            "per_hour(cpu_usage)=[DIVIDE(*($1, 3600.0E0), "
-                + "TIMESTAMPDIFF('SECOND':VARCHAR, $0, TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
+            "per_hour(cpu_usage)=[DIVIDE(*($1, 3600000.0E0), TIMESTAMPDIFF('MILLISECOND':VARCHAR,"
+                + " $0, TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
     assertTrue(result.contains("per_hour(cpu_usage)=[SUM($0)]"));
   }
 
@@ -474,8 +474,8 @@ public void testExplainTimechartPerDay() throws IOException {
     var result = explainQueryToString("source=events | timechart span=2m per_day(cpu_usage)");
     assertTrue(
         result.contains(
-            "per_day(cpu_usage)=[DIVIDE(*($1, 86400.0E0), "
-                + "TIMESTAMPDIFF('SECOND':VARCHAR, $0, TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
+            "per_day(cpu_usage)=[DIVIDE(*($1, 8.64E7), TIMESTAMPDIFF('MILLISECOND':VARCHAR, $0,"
+                + " TIMESTAMPADD('MINUTE':VARCHAR, 2, $0)))]"));
     assertTrue(result.contains("per_day(cpu_usage)=[SUM($0)]"));
   }
 
diff --git a/integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/4550.yml b/integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/4550.yml
new file mode 100644
index 00000000000..e676e8cb490
--- /dev/null
+++ b/integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/4550.yml
@@ -0,0 +1,93 @@
+setup:
+  - do:
+      indices.create:
+        index: test_data_2023
+        body:
+          mappings:
+            properties:
+              "@timestamp":
+                type: date
+              "packets":
+                type: integer
+  - do:
+      bulk:
+        index: test_data_2023
+        refresh: true
+        body:
+          - '{"index":{}}'
+          - '{"@timestamp":"2023-10-08T10:00:00.000Z","packets":10}'
+          - '{"index":{}}'
+          - '{"@timestamp":"2023-10-08T10:00:00.500Z","packets":15}'
+          - '{"index":{}}'
+          - '{"@timestamp":"2023-10-08T10:00:01.000Z","packets":20}'
+          - '{"index":{}}'
+          - '{"@timestamp":"2023-10-08T10:00:01.500Z","packets":25}'
+          - '{"index":{}}'
+          - '{"@timestamp":"2023-10-08T10:00:02.000Z","packets":30}'
+
+---
+"timechart with millisecond span":
+  - skip:
+      features:
+        - headers
+        - allowed_warnings
+  - do:
+      headers:
+        Content-Type: 'application/json'
+      ppl:
+        body:
+          query: source=test_data_2023 | timechart span=500ms count()
+
+  - match: { total: 5 }
+  - match: { "schema": [ { "name": "@timestamp", "type": "timestamp" }, { "name": "count", "type": "bigint" }] }
+  - match: {"datarows": [["2023-10-08 10:00:00", 1], ["2023-10-08 10:00:00.5", 1], ["2023-10-08 10:00:01", 1], ["2023-10-08 10:00:01.5", 1], ["2023-10-08 10:00:02", 1]]}
+
+---
+"timechart with millisecond span and per_second function":
+  - skip:
+      features:
+        - headers
+        - allowed_warnings
+  - do:
+      headers:
+        Content-Type: 'application/json'
+      ppl:
+        body:
+          query: source=test_data_2023 | timechart span=1000ms per_second(packets)
+
+  - match: { total: 3 }
+  - match: { "schema": [ { "name": "@timestamp", "type": "timestamp" }, { "name": "per_second(packets)", "type": "double" }] }
+  - match: {"datarows": [["2023-10-08 10:00:00", 25.0], ["2023-10-08 10:00:01", 45.0], ["2023-10-08 10:00:02", 30.0]]}
+
+---
+"timechart with milliseconds":
+  - skip:
+      features:
+        - headers
+        - allowed_warnings
+  - do:
+      headers:
+        Content-Type: 'application/json'
+      ppl:
+        body:
+          query: source=test_data_2023 | timechart span=250milliseconds count()
+
+  - match: { total: 5 }
+  - match: { "schema": [ { "name": "@timestamp", "type": "timestamp" }, { "name": "count", "type": "bigint" }] }
+
+---
+"timechart with second span for comparison":
+  - skip:
+      features:
+        - headers
+        - allowed_warnings
+  - do:
+      headers:
+        Content-Type: 'application/json'
+      ppl:
+        body:
+          query: source=test_data_2023 | timechart span=1s count()
+
+  - match: { total: 3 }
+  - match: { "schema": [ { "name": "@timestamp", "type": "timestamp" }, { "name": "count", "type": "bigint" }] }
+  - match: {"datarows": [["2023-10-08 10:00:00", 2], ["2023-10-08 10:00:01", 2], ["2023-10-08 10:00:02", 1]]}
diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4
index 368c32f250f..0899de168fb 100644
--- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4
+++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4
@@ -170,6 +170,7 @@ HOUR_MINUTE:                        'HOUR_MINUTE';
 HOUR_OF_DAY:                        'HOUR_OF_DAY';
 HOUR_SECOND:                        'HOUR_SECOND';
 INTERVAL:                           'INTERVAL';
+MILLISECOND:                        'MILLISECOND';
 MICROSECOND:                        'MICROSECOND';
 MINUTE:                             'MINUTE';
 MINUTE_MICROSECOND:                 'MINUTE_MICROSECOND';
@@ -505,7 +506,8 @@ ALIGNTIME:                          'ALIGNTIME';
 PERCENTILE_SHORTCUT:                PERC(INTEGER_LITERAL | DECIMAL_LITERAL) | 'P'(INTEGER_LITERAL | DECIMAL_LITERAL);
 
 SPANLENGTH: [0-9]+ (
-    'US'|'MS'|'CS'|'DS'
+    'US' |'CS'|'DS'
+    |'MS'|'MILLISECOND'|'MILLISECONDS'
     |'S'|'SEC'|'SECS'|'SECOND'|'SECONDS'
     |'MIN'|'MINS'|'MINUTE'|'MINUTES'
     |'H'|'HR'|'HRS'|'HOUR'|'HOURS'
diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4
index 5c4aa0bf6a5..fda22fc2481 100644
--- a/ppl/src/main/antlr/OpenSearchPPLParser.g4
+++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4
@@ -1157,6 +1157,7 @@ extractFunctionCall
 
 simpleDateTimePart
    : MICROSECOND
+   | MILLISECOND
    | SECOND
    | MINUTE
    | HOUR
@@ -1335,6 +1336,7 @@ timestampLiteral
 
 intervalUnit
    : MICROSECOND
+   | MILLISECOND
    | SECOND
    | MINUTE
    | HOUR
diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLTimechartTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLTimechartTest.java
index 0470fc19957..ee6b82f2d85 100644
--- a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLTimechartTest.java
+++ b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLTimechartTest.java
@@ -86,9 +86,9 @@ public void testTimechartBasic() {
   public void testTimechartPerSecond() {
     withPPLQuery("source=events | timechart per_second(cpu_usage)")
         .expectSparkSQL(
-            "SELECT `@timestamp`, `DIVIDE`(`per_second(cpu_usage)` * 1.0E0, TIMESTAMPDIFF('SECOND',"
-                + " `@timestamp`, TIMESTAMPADD('MINUTE', 1, `@timestamp`)))"
-                + " `per_second(cpu_usage)`\n"
+            "SELECT `@timestamp`, `DIVIDE`(`per_second(cpu_usage)` * 1.0000E3,"
+                + " TIMESTAMPDIFF('MILLISECOND', `@timestamp`, TIMESTAMPADD('MINUTE', 1,"
+                + " `@timestamp`))) `per_second(cpu_usage)`\n"
                 + "FROM (SELECT `SPAN`(`@timestamp`, 1, 'm') `@timestamp`, SUM(`cpu_usage`)"
                 + " `per_second(cpu_usage)`\n"
                 + "FROM `scott`.`events`\n"
@@ -100,9 +100,9 @@ public void testTimechartPerSecond() {
   public void testTimechartPerMinute() {
     withPPLQuery("source=events | timechart per_minute(cpu_usage)")
         .expectSparkSQL(
-            "SELECT `@timestamp`, `DIVIDE`(`per_minute(cpu_usage)` * 6.00E1,"
-                + " TIMESTAMPDIFF('SECOND', `@timestamp`, TIMESTAMPADD('MINUTE', 1, `@timestamp`)))"
-                + " `per_minute(cpu_usage)`\n"
+            "SELECT `@timestamp`, `DIVIDE`(`per_minute(cpu_usage)` * 6.00000E4,"
+                + " TIMESTAMPDIFF('MILLISECOND', `@timestamp`, TIMESTAMPADD('MINUTE', 1,"
+                + " `@timestamp`))) `per_minute(cpu_usage)`\n"
                 + "FROM (SELECT `SPAN`(`@timestamp`, 1, 'm') `@timestamp`, SUM(`cpu_usage`)"
                 + " `per_minute(cpu_usage)`\n"
                 + "FROM `scott`.`events`\n"
@@ -114,9 +114,9 @@ public void testTimechartPerMinute() {
   public void testTimechartPerHour() {
     withPPLQuery("source=events | timechart per_hour(cpu_usage)")
         .expectSparkSQL(
-            "SELECT `@timestamp`, `DIVIDE`(`per_hour(cpu_usage)` * 3.6000E3,"
-                + " TIMESTAMPDIFF('SECOND', `@timestamp`, TIMESTAMPADD('MINUTE', 1, `@timestamp`)))"
-                + " `per_hour(cpu_usage)`\n"
+            "SELECT `@timestamp`, `DIVIDE`(`per_hour(cpu_usage)` * 3.6000000E6,"
+                + " TIMESTAMPDIFF('MILLISECOND', `@timestamp`, TIMESTAMPADD('MINUTE', 1,"
+                + " `@timestamp`))) `per_hour(cpu_usage)`\n"
                 + "FROM (SELECT `SPAN`(`@timestamp`, 1, 'm') `@timestamp`, SUM(`cpu_usage`)"
                 + " `per_hour(cpu_usage)`\n"
                 + "FROM `scott`.`events`\n"
@@ -128,9 +128,9 @@ public void testTimechartPerHour() {
   public void testTimechartPerDay() {
     withPPLQuery("source=events | timechart per_day(cpu_usage)")
         .expectSparkSQL(
-            "SELECT `@timestamp`, `DIVIDE`(`per_day(cpu_usage)` * 8.64000E4,"
-                + " TIMESTAMPDIFF('SECOND', `@timestamp`, TIMESTAMPADD('MINUTE', 1, `@timestamp`)))"
-                + " `per_day(cpu_usage)`\n"
+            "SELECT `@timestamp`, `DIVIDE`(`per_day(cpu_usage)` * 8.64E7,"
+                + " TIMESTAMPDIFF('MILLISECOND', `@timestamp`, TIMESTAMPADD('MINUTE', 1,"
+                + " `@timestamp`))) `per_day(cpu_usage)`\n"
                 + "FROM (SELECT `SPAN`(`@timestamp`, 1, 'm') `@timestamp`, SUM(`cpu_usage`)"
                 + " `per_day(cpu_usage)`\n"
                 + "FROM `scott`.`events`\n"
diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/parser/AstBuilderTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/parser/AstBuilderTest.java
index 1080095b375..12609ed5c4c 100644
--- a/ppl/src/test/java/org/opensearch/sql/ppl/parser/AstBuilderTest.java
+++ b/ppl/src/test/java/org/opensearch/sql/ppl/parser/AstBuilderTest.java
@@ -1195,10 +1195,10 @@ public void testTimechartWithPerSecondFunction() {
                 field("per_second(a)"),
                 function(
                     "/",
-                    function("*", field("per_second(a)"), doubleLiteral(1.0)),
+                    function("*", field("per_second(a)"), doubleLiteral(1000.0)),
                     function(
                         "timestampdiff",
-                        stringLiteral("SECOND"),
+                        stringLiteral("MILLISECOND"),
                         field("@timestamp"),
                         function(
                             "timestampadd",
@@ -1220,10 +1220,10 @@ public void testTimechartWithPerMinuteFunction() {
                 field("per_minute(a)"),
                 function(
                     "/",
-                    function("*", field("per_minute(a)"), doubleLiteral(60.0)),
+                    function("*", field("per_minute(a)"), doubleLiteral(60000.0)),
                     function(
                         "timestampdiff",
-                        stringLiteral("SECOND"),
+                        stringLiteral("MILLISECOND"),
                         field("@timestamp"),
                         function(
                             "timestampadd",
@@ -1245,10 +1245,10 @@ public void testTimechartWithPerHourFunction() {
                 field("per_hour(a)"),
                 function(
                     "/",
-                    function("*", field("per_hour(a)"), doubleLiteral(3600.0)),
+                    function("*", field("per_hour(a)"), doubleLiteral(3600000.0)),
                     function(
                         "timestampdiff",
-                        stringLiteral("SECOND"),
+                        stringLiteral("MILLISECOND"),
                         field("@timestamp"),
                         function(
                             "timestampadd",
@@ -1270,10 +1270,10 @@ public void testTimechartWithPerDayFunction() {
                 field("per_day(a)"),
                 function(
                     "/",
-                    function("*", field("per_day(a)"), doubleLiteral(86400.0)),
+                    function("*", field("per_day(a)"), doubleLiteral(8.64E7)),
                     function(
                         "timestampdiff",
-                        stringLiteral("SECOND"),
+                        stringLiteral("MILLISECOND"),
                         field("@timestamp"),
                         function(
                             "timestampadd",
