diff --git a/pkg/telemetrytraces/trace_operator_cte_builder.go b/pkg/telemetrytraces/trace_operator_cte_builder.go index 5a1906ae1bcd..edcdbfdbc4ff 100644 --- a/pkg/telemetrytraces/trace_operator_cte_builder.go +++ b/pkg/telemetrytraces/trace_operator_cte_builder.go @@ -434,7 +434,26 @@ func (b *traceOperatorCTEBuilder) buildListQuery(selectFromCTE string) (*qbtypes sb.From(selectFromCTE) - sb.OrderBy("timestamp DESC") + // Add order by support + keySelectors := b.getKeySelectors() + keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(b.ctx, keySelectors) + if err != nil { + return nil, err + } + + orderApplied := false + for _, orderBy := range b.operator.Order { + colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(b.ctx, &orderBy.Key.TelemetryFieldKey, keys) + if err != nil { + return nil, err + } + sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue())) + orderApplied = true + } + + if !orderApplied { + sb.OrderBy("timestamp DESC") + } if b.operator.Limit > 0 { sb.Limit(b.operator.Limit) @@ -442,6 +461,10 @@ func (b *traceOperatorCTEBuilder) buildListQuery(selectFromCTE string) (*qbtypes sb.Limit(100) } + if b.operator.Offset > 0 { + sb.Offset(b.operator.Offset) + } + sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) return &qbtypes.Statement{ Query: sql, @@ -630,6 +653,17 @@ func (b *traceOperatorCTEBuilder) buildTimeSeriesQuery(selectFromCTE string) (*q sb.GroupBy(groupByKeys...) } + // Add order by support + for _, orderBy := range b.operator.Order { + idx, ok := b.aggOrderBy(orderBy) + if ok { + sb.OrderBy(fmt.Sprintf("__result_%d %s", idx, orderBy.Direction.StringValue())) + } else { + sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue())) + } + } + sb.OrderBy("ts desc") + combinedArgs := append(allGroupByArgs, allAggChArgs...) // Add HAVING clause if specified @@ -637,6 +671,11 @@ func (b *traceOperatorCTEBuilder) buildTimeSeriesQuery(selectFromCTE string) (*q return nil, err } + // Add limit support + if b.operator.Limit > 0 { + sb.Limit(b.operator.Limit) + } + sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...) return &qbtypes.Statement{ Query: sql, @@ -855,6 +894,26 @@ func (b *traceOperatorCTEBuilder) buildScalarQuery(selectFromCTE string) (*qbtyp sb.GroupBy(groupByKeys...) } + // Add order by support + for _, orderBy := range b.operator.Order { + idx, ok := b.aggOrderBy(orderBy) + if ok { + sb.OrderBy(fmt.Sprintf("__result_%d %s", idx, orderBy.Direction.StringValue())) + } else { + sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue())) + } + } + + // Add default ordering if no orderBy specified + if len(b.operator.Order) == 0 { + sb.OrderBy("__result_0 DESC") + } + + // Add limit support + if b.operator.Limit > 0 { + sb.Limit(b.operator.Limit) + } + combinedArgs := append(allGroupByArgs, allAggChArgs...) // Add HAVING clause if specified @@ -887,3 +946,14 @@ func (b *traceOperatorCTEBuilder) addCTE(name, sql string, args []any, dependsOn }) b.cteNameToIndex[name] = len(b.ctes) - 1 } + +func (b *traceOperatorCTEBuilder) aggOrderBy(k qbtypes.OrderBy) (int, bool) { + for i, agg := range b.operator.Aggregations { + if k.Key.Name == agg.Alias || + k.Key.Name == agg.Expression || + k.Key.Name == fmt.Sprintf("__result_%d", i) { + return i, true + } + } + return 0, false +} diff --git a/pkg/types/querybuildertypes/querybuildertypesv5/trace_operator.go b/pkg/types/querybuildertypes/querybuildertypesv5/trace_operator.go index 9e9e427f67c5..d2e69b98fb3e 100644 --- a/pkg/types/querybuildertypes/querybuildertypesv5/trace_operator.go +++ b/pkg/types/querybuildertypes/querybuildertypesv5/trace_operator.go @@ -56,6 +56,7 @@ type QueryBuilderTraceOperator struct { Having *Having `json:"having,omitempty"` Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` Cursor string `json:"cursor,omitempty"` Legend string `json:"legend,omitempty"` @@ -264,6 +265,15 @@ func (q *QueryBuilderTraceOperator) ValidatePagination() error { ) } + if q.Offset < 0 { + return errors.WrapInvalidInputf( + nil, + errors.CodeInvalidInput, + "offset must be non-negative, got %d", + q.Offset, + ) + } + // For production use, you might want to enforce maximum limits if q.Limit > 10000 { return errors.WrapInvalidInputf(