55
66package org .opensearch .sql .ast .tree ;
77
8+ import static org .opensearch .sql .ast .dsl .AstDSL .aggregate ;
9+ import static org .opensearch .sql .ast .dsl .AstDSL .doubleLiteral ;
10+ import static org .opensearch .sql .ast .dsl .AstDSL .eval ;
11+ import static org .opensearch .sql .ast .dsl .AstDSL .function ;
12+ import static org .opensearch .sql .ast .dsl .AstDSL .stringLiteral ;
13+ import static org .opensearch .sql .ast .expression .IntervalUnit .SECOND ;
14+ import static org .opensearch .sql .ast .tree .Timechart .PerFunctionRateExprBuilder .sum ;
15+ import static org .opensearch .sql .ast .tree .Timechart .PerFunctionRateExprBuilder .timestampadd ;
16+ import static org .opensearch .sql .ast .tree .Timechart .PerFunctionRateExprBuilder .timestampdiff ;
17+ import static org .opensearch .sql .calcite .plan .OpenSearchConstants .IMPLICIT_FIELD_TIMESTAMP ;
18+ import static org .opensearch .sql .expression .function .BuiltinFunctionName .DIVIDE ;
19+ import static org .opensearch .sql .expression .function .BuiltinFunctionName .MULTIPLY ;
20+ import static org .opensearch .sql .expression .function .BuiltinFunctionName .SUM ;
21+ import static org .opensearch .sql .expression .function .BuiltinFunctionName .TIMESTAMPADD ;
22+ import static org .opensearch .sql .expression .function .BuiltinFunctionName .TIMESTAMPDIFF ;
23+
824import com .google .common .collect .ImmutableList ;
925import java .util .List ;
26+ import java .util .Locale ;
27+ import java .util .Map ;
28+ import java .util .Optional ;
1029import lombok .AllArgsConstructor ;
1130import lombok .EqualsAndHashCode ;
1231import lombok .Getter ;
32+ import lombok .RequiredArgsConstructor ;
1333import lombok .ToString ;
1434import org .opensearch .sql .ast .AbstractNodeVisitor ;
35+ import org .opensearch .sql .ast .dsl .AstDSL ;
36+ import org .opensearch .sql .ast .expression .AggregateFunction ;
37+ import org .opensearch .sql .ast .expression .Field ;
38+ import org .opensearch .sql .ast .expression .Function ;
39+ import org .opensearch .sql .ast .expression .IntervalUnit ;
40+ import org .opensearch .sql .ast .expression .Let ;
41+ import org .opensearch .sql .ast .expression .Span ;
42+ import org .opensearch .sql .ast .expression .SpanUnit ;
1543import org .opensearch .sql .ast .expression .UnresolvedExpression ;
44+ import org .opensearch .sql .calcite .utils .PlanUtils ;
1645
1746/** AST node represent Timechart operation. */
1847@ Getter
@@ -49,8 +78,9 @@ public Timechart useOther(Boolean useOther) {
4978 }
5079
5180 @ Override
52- public Timechart attach (UnresolvedPlan child ) {
53- return toBuilder ().child (child ).build ();
81+ public UnresolvedPlan attach (UnresolvedPlan child ) {
82+ // Transform after child attached to avoid unintentionally overriding it
83+ return toBuilder ().child (child ).build ().transformPerFunction ();
5484 }
5585
5686 @ Override
@@ -62,4 +92,112 @@ public List<UnresolvedPlan> getChild() {
6292 public <T , C > T accept (AbstractNodeVisitor <T , C > nodeVisitor , C context ) {
6393 return nodeVisitor .visitTimechart (this , context );
6494 }
95+
96+ /**
97+ * Transform per function to eval-based post-processing on sum result by timechart. Specifically,
98+ * calculate how many seconds are in the time bucket based on the span option dynamically, then
99+ * divide the aggregated sum value by the number of seconds to get the per-second rate.
100+ *
101+ * <p>For example, with span=5m per_second(field): per second rate = sum(field) / 300 seconds
102+ *
103+ * @return eval+timechart if per function present, or the original timechart otherwise.
104+ */
105+ private UnresolvedPlan transformPerFunction () {
106+ Optional <PerFunction > perFuncOpt = PerFunction .from (aggregateFunction );
107+ if (perFuncOpt .isEmpty ()) {
108+ return this ;
109+ }
110+
111+ PerFunction perFunc = perFuncOpt .get ();
112+ Span span = (Span ) this .binExpression ;
113+ Field spanStartTime = AstDSL .field (IMPLICIT_FIELD_TIMESTAMP );
114+ Function spanEndTime = timestampadd (span .getUnit (), span .getValue (), spanStartTime );
115+ Function spanSeconds = timestampdiff (SECOND , spanStartTime , spanEndTime );
116+
117+ return eval (
118+ timechart (AstDSL .alias (perFunc .aggName , sum (perFunc .aggArg ))),
119+ let (perFunc .aggName ).multiply (perFunc .seconds ).dividedBy (spanSeconds ));
120+ }
121+
122+ private Timechart timechart (UnresolvedExpression newAggregateFunction ) {
123+ return this .toBuilder ().aggregateFunction (newAggregateFunction ).build ();
124+ }
125+
126+ /** TODO: extend to support additional per_* functions */
127+ @ RequiredArgsConstructor
128+ static class PerFunction {
129+ private static final Map <String , Integer > UNIT_SECONDS = Map .of ("per_second" , 1 );
130+ private final String aggName ;
131+ private final UnresolvedExpression aggArg ;
132+ private final int seconds ;
133+
134+ static Optional <PerFunction > from (UnresolvedExpression aggExpr ) {
135+ if (!(aggExpr instanceof AggregateFunction )) {
136+ return Optional .empty ();
137+ }
138+
139+ AggregateFunction aggFunc = (AggregateFunction ) aggExpr ;
140+ String aggFuncName = aggFunc .getFuncName ().toLowerCase (Locale .ROOT );
141+ if (!UNIT_SECONDS .containsKey (aggFuncName )) {
142+ return Optional .empty ();
143+ }
144+
145+ String aggName = toAggName (aggFunc );
146+ return Optional .of (
147+ new PerFunction (aggName , aggFunc .getField (), UNIT_SECONDS .get (aggFuncName )));
148+ }
149+
150+ private static String toAggName (AggregateFunction aggFunc ) {
151+ String fieldName =
152+ (aggFunc .getField () instanceof Field )
153+ ? ((Field ) aggFunc .getField ()).getField ().toString ()
154+ : aggFunc .getField ().toString ();
155+ return String .format (Locale .ROOT , "%s(%s)" , aggFunc .getFuncName (), fieldName );
156+ }
157+ }
158+
159+ private PerFunctionRateExprBuilder let (String fieldName ) {
160+ return new PerFunctionRateExprBuilder (AstDSL .field (fieldName ));
161+ }
162+
163+ /** Fluent builder for creating Let expressions with mathematical operations. */
164+ static class PerFunctionRateExprBuilder {
165+ private final Field field ;
166+ private UnresolvedExpression expr ;
167+
168+ PerFunctionRateExprBuilder (Field field ) {
169+ this .field = field ;
170+ this .expr = field ;
171+ }
172+
173+ PerFunctionRateExprBuilder multiply (Integer multiplier ) {
174+ // Promote to double literal to avoid integer division in downstream
175+ this .expr =
176+ function (
177+ MULTIPLY .getName ().getFunctionName (), expr , doubleLiteral (multiplier .doubleValue ()));
178+ return this ;
179+ }
180+
181+ Let dividedBy (UnresolvedExpression divisor ) {
182+ return AstDSL .let (field , function (DIVIDE .getName ().getFunctionName (), expr , divisor ));
183+ }
184+
185+ static UnresolvedExpression sum (UnresolvedExpression field ) {
186+ return aggregate (SUM .getName ().getFunctionName (), field );
187+ }
188+
189+ static Function timestampadd (
190+ SpanUnit unit , UnresolvedExpression value , UnresolvedExpression timestampField ) {
191+ UnresolvedExpression intervalUnit =
192+ stringLiteral (PlanUtils .spanUnitToIntervalUnit (unit ).toString ());
193+ return function (
194+ TIMESTAMPADD .getName ().getFunctionName (), intervalUnit , value , timestampField );
195+ }
196+
197+ static Function timestampdiff (
198+ IntervalUnit unit , UnresolvedExpression start , UnresolvedExpression end ) {
199+ return function (
200+ TIMESTAMPDIFF .getName ().getFunctionName (), stringLiteral (unit .toString ()), start , end );
201+ }
202+ }
65203}
0 commit comments