fix: table order by with builder queries (#5308)

This commit is contained in:
Srikanth Chekuri 2024-06-25 13:42:40 +05:30 committed by GitHub
parent 5df25e83d1
commit 52e0303997
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1006 additions and 4 deletions

View File

@ -3013,6 +3013,7 @@ func (aH *APIHandler) QueryRangeV3Format(w http.ResponseWriter, r *http.Request)
RespondError(w, apiErrorObj, nil)
return
}
queryRangeParams.Version = "v3"
aH.Respond(w, queryRangeParams)
}
@ -3067,6 +3068,14 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
postprocess.FillGaps(result, queryRangeParams)
}
if queryRangeParams.CompositeQuery.PanelType == v3.PanelTypeTable && queryRangeParams.FormatForWeb {
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL {
result = postprocess.TransformToTableForClickHouseQueries(result)
} else if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
result = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
}
}
resp := v3.QueryRangeResponse{
Result: result,
}
@ -3315,8 +3324,10 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
}
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
result, err = postprocess.PostProcessResult(result, queryRangeParams)
} else if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL &&
queryRangeParams.CompositeQuery.PanelType == v3.PanelTypeTable && queryRangeParams.FormatForWeb {
result = postprocess.TransformToTableForClickHouseQueries(result)
}
if err != nil {
@ -3340,6 +3351,7 @@ func (aH *APIHandler) QueryRangeV4(w http.ResponseWriter, r *http.Request) {
RespondError(w, apiErrorObj, nil)
return
}
queryRangeParams.Version = "v4"
// add temporality for each metric
temporalityErr := aH.populateTemporality(r.Context(), queryRangeParams)

View File

@ -354,6 +354,8 @@ type QueryRangeParamsV3 struct {
CompositeQuery *CompositeQuery `json:"compositeQuery"`
Variables map[string]interface{} `json:"variables,omitempty"`
NoCache bool `json:"noCache"`
Version string `json:"-"`
FormatForWeb bool `json:"formatForWeb,omitempty"`
}
type PromQuery struct {
@ -986,10 +988,24 @@ type QueryRangeResponse struct {
Result []*Result `json:"result"`
}
type TableColumn struct {
Name string `json:"name"`
}
type TableRow struct {
Data []interface{} `json:"data"`
}
type Table struct {
Columns []*TableColumn `json:"columns"`
Rows []*TableRow `json:"rows"`
}
type Result struct {
QueryName string `json:"queryName"`
Series []*Series `json:"series"`
List []*Row `json:"list"`
QueryName string `json:"queryName,omitempty"`
Series []*Series `json:"series,omitempty"`
List []*Row `json:"list,omitempty"`
Table *Table `json:"table,omitempty"`
}
type LogsLiveTailClient struct {

View File

@ -86,6 +86,13 @@ func PostProcessResult(result []*v3.Result, queryRangeParams *v3.QueryRangeParam
if queryRangeParams.CompositeQuery.FillGaps {
FillGaps(result, queryRangeParams)
}
if queryRangeParams.FormatForWeb &&
queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder &&
queryRangeParams.CompositeQuery.PanelType == v3.PanelTypeTable {
result = TransformToTableForBuilderQueries(result, queryRangeParams)
}
return result, nil
}

View File

@ -0,0 +1,299 @@
package postprocess
import (
"fmt"
"sort"
"strings"
"go.signoz.io/signoz/pkg/query-service/constants"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
)
func getAutoColNameForQuery(queryName string, params *v3.QueryRangeParamsV3) string {
q := params.CompositeQuery.BuilderQueries[queryName]
if q.DataSource == v3.DataSourceTraces || q.DataSource == v3.DataSourceLogs {
if q.AggregateAttribute.Key != "" {
return fmt.Sprintf("%s(%s)", q.AggregateOperator, q.AggregateAttribute.Key)
}
return string(q.AggregateOperator)
} else if q.DataSource == v3.DataSourceMetrics {
if q.SpaceAggregation != "" && params.Version == "v4" {
return fmt.Sprintf("%s(%s)", q.SpaceAggregation, q.AggregateAttribute.Key)
}
return fmt.Sprintf("%s(%s)", q.AggregateOperator, q.AggregateAttribute.Key)
}
return queryName
}
func TransformToTableForBuilderQueries(results []*v3.Result, params *v3.QueryRangeParamsV3) []*v3.Result {
if len(results) == 0 {
return []*v3.Result{}
}
// Sort results by QueryName
sort.Slice(results, func(i, j int) bool {
return results[i].QueryName < results[j].QueryName
})
// Create a map to store all unique labels
seen := make(map[string]struct{})
labelKeys := []string{}
for _, result := range results {
for _, series := range result.Series {
for _, labels := range series.LabelsArray {
for key := range labels {
if _, ok := seen[key]; !ok {
seen[key] = struct{}{}
labelKeys = append(labelKeys, key)
}
}
}
}
}
// Create columns
// There will be one column for each label key and one column for each query name
columns := make([]*v3.TableColumn, 0, len(labelKeys)+len(results))
for _, key := range labelKeys {
columns = append(columns, &v3.TableColumn{Name: key})
}
for _, result := range results {
columns = append(columns, &v3.TableColumn{Name: result.QueryName})
}
// Create a map to store unique rows
rowMap := make(map[string]*v3.TableRow)
for _, result := range results {
for _, series := range result.Series {
if len(series.Points) == 0 {
continue
}
// Create a key for the row based on labels
var keyParts []string
rowData := make([]interface{}, len(columns))
for i, key := range labelKeys {
value := "n/a"
for _, labels := range series.LabelsArray {
if v, ok := labels[key]; ok {
value = v
break
}
}
keyParts = append(keyParts, fmt.Sprintf("%s=%s", key, value))
rowData[i] = value
}
rowKey := strings.Join(keyParts, ",")
// Get or create the row
row, ok := rowMap[rowKey]
if !ok {
row = &v3.TableRow{Data: rowData}
rowMap[rowKey] = row
}
// Add the value for this query
for i, col := range columns {
if col.Name == result.QueryName {
row.Data[i] = series.Points[0].Value
break
}
}
}
}
// Convert rowMap to a slice of TableRows
rows := make([]*v3.TableRow, 0, len(rowMap))
for _, row := range rowMap {
for i, value := range row.Data {
if value == nil {
row.Data[i] = "n/a"
}
}
rows = append(rows, row)
}
// Get sorted query names
queryNames := make([]string, 0, len(params.CompositeQuery.BuilderQueries))
for queryName := range params.CompositeQuery.BuilderQueries {
queryNames = append(queryNames, queryName)
}
sort.Strings(queryNames)
// Sort rows based on OrderBy from BuilderQueries
sortRows(rows, columns, params.CompositeQuery.BuilderQueries, queryNames)
for _, column := range columns {
if _, exists := params.CompositeQuery.BuilderQueries[column.Name]; exists {
column.Name = getAutoColNameForQuery(column.Name, params)
}
}
// Create the final result
tableResult := v3.Result{
Table: &v3.Table{
Columns: columns,
Rows: rows,
},
}
return []*v3.Result{&tableResult}
}
func sortRows(rows []*v3.TableRow, columns []*v3.TableColumn, builderQueries map[string]*v3.BuilderQuery, queryNames []string) {
sort.SliceStable(rows, func(i, j int) bool {
for _, queryName := range queryNames {
query := builderQueries[queryName]
orderByList := query.OrderBy
if len(orderByList) == 0 {
// If no orderBy is specified, sort by value in descending order
orderByList = []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "desc"}}
}
for _, orderBy := range orderByList {
name := orderBy.ColumnName
if name == constants.SigNozOrderByValue {
name = queryName
}
colIndex := -1
for k, col := range columns {
if col.Name == name {
colIndex = k
break
}
}
if colIndex == -1 {
continue
}
valI := rows[i].Data[colIndex]
valJ := rows[j].Data[colIndex]
// Handle "n/a" values
if valI == "n/a" && valJ == "n/a" {
continue
}
// Compare based on the data type
switch v := valI.(type) {
case float64:
switch w := valJ.(type) {
case float64:
if v != w {
return (v < w) == (orderBy.Order == "asc")
}
default:
// For any other type, sort float64 first
return orderBy.Order == "asc"
}
case string:
switch w := valJ.(type) {
case float64:
// If types are different, sort numbers before strings
return orderBy.Order != "asc"
case string:
if v != w {
return (v < w) == (orderBy.Order == "asc")
}
default:
// For any other type, sort strings before bools
return orderBy.Order == "asc"
}
case bool:
switch w := valJ.(type) {
case float64, string:
// If types are different, sort bools after numbers and strings
return orderBy.Order != "asc"
case bool:
if v != w {
return (!v && w) == (orderBy.Order == "asc")
}
}
}
}
}
return false
})
}
func TransformToTableForClickHouseQueries(results []*v3.Result) []*v3.Result {
if len(results) == 0 {
return []*v3.Result{}
}
// Sort results by QueryName
sort.Slice(results, func(i, j int) bool {
return results[i].QueryName < results[j].QueryName
})
// Create a map to store all unique labels
seen := make(map[string]struct{})
labelKeys := []string{}
for _, result := range results {
for _, series := range result.Series {
for _, labels := range series.LabelsArray {
for key := range labels {
if _, ok := seen[key]; !ok {
seen[key] = struct{}{}
labelKeys = append(labelKeys, key)
}
}
}
}
}
// Create columns
// Why don't we have a column for each query name?
// Because we don't know if the query is an aggregation query or a non-aggregation query
// So we create a column for each query name that has at least one point
columns := make([]*v3.TableColumn, 0)
for _, key := range labelKeys {
columns = append(columns, &v3.TableColumn{Name: key})
}
for _, result := range results {
if len(result.Series) > 0 && len(result.Series[0].Points) > 0 {
columns = append(columns, &v3.TableColumn{Name: result.QueryName})
}
}
rows := make([]*v3.TableRow, 0)
for _, result := range results {
for _, series := range result.Series {
// Create a key for the row based on labels
rowData := make([]interface{}, len(columns))
for i, key := range labelKeys {
value := "n/a"
for _, labels := range series.LabelsArray {
if v, ok := labels[key]; ok {
value = v
break
}
}
rowData[i] = value
}
// Get or create the row
row := &v3.TableRow{Data: rowData}
// Add the value for this query
for i, col := range columns {
if col.Name == result.QueryName && len(series.Points) > 0 {
row.Data[i] = series.Points[0].Value
break
}
}
rows = append(rows, row)
}
}
// Create the final result
tableResult := v3.Result{
Table: &v3.Table{
Columns: columns,
Rows: rows,
},
}
return []*v3.Result{&tableResult}
}

View File

@ -0,0 +1,668 @@
package postprocess
import (
"encoding/json"
"reflect"
"testing"
"go.signoz.io/signoz/pkg/query-service/constants"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
)
func TestSortRows(t *testing.T) {
tests := []struct {
name string
rows []*v3.TableRow
columns []*v3.TableColumn
builderQueries map[string]*v3.BuilderQuery
queryNames []string
expected []*v3.TableRow
}{
{
name: "Sort by single numeric query, ascending order",
rows: []*v3.TableRow{
{Data: []interface{}{"service2", 20.0}},
{Data: []interface{}{"service1", 10.0}},
{Data: []interface{}{"service3", 30.0}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "asc"}}},
},
queryNames: []string{"A"},
expected: []*v3.TableRow{
{Data: []interface{}{"service1", 10.0}},
{Data: []interface{}{"service2", 20.0}},
{Data: []interface{}{"service3", 30.0}},
},
},
{
name: "Sort by single numeric query, descending order",
rows: []*v3.TableRow{
{Data: []interface{}{"service2", 20.0}},
{Data: []interface{}{"service1", 10.0}},
{Data: []interface{}{"service3", 30.0}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "desc"}}},
},
queryNames: []string{"A"},
expected: []*v3.TableRow{
{Data: []interface{}{"service3", 30.0}},
{Data: []interface{}{"service2", 20.0}},
{Data: []interface{}{"service1", 10.0}},
},
},
{
name: "Sort by single string query, ascending order",
rows: []*v3.TableRow{
{Data: []interface{}{"service2", "b"}},
{Data: []interface{}{"service1", "c"}},
{Data: []interface{}{"service3", "a"}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "A", Order: "asc"}}},
},
queryNames: []string{"A"},
expected: []*v3.TableRow{
{Data: []interface{}{"service3", "a"}},
{Data: []interface{}{"service2", "b"}},
{Data: []interface{}{"service1", "c"}},
},
},
{
name: "Sort with n/a values",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", 10.0, "n/a"}},
{Data: []interface{}{"service2", "n/a", 15.0}},
{Data: []interface{}{"service3", 30.0, 25.0}},
{Data: []interface{}{"service4", "n/a", "n/a"}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
{Name: "B"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "asc"}}},
"B": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "desc"}}},
},
queryNames: []string{"A", "B"},
expected: []*v3.TableRow{
{Data: []interface{}{"service1", 10.0, "n/a"}},
{Data: []interface{}{"service3", 30.0, 25.0}},
{Data: []interface{}{"service4", "n/a", "n/a"}},
{Data: []interface{}{"service2", "n/a", 15.0}},
},
},
{
name: "Sort with different data types",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", "string", 10.0, true}},
{Data: []interface{}{"service2", 20.0, "string", false}},
{Data: []interface{}{"service3", true, 30.0, "string"}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
{Name: "B"},
{Name: "C"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "asc"}}},
"B": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "desc"}}},
"C": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "asc"}}},
},
queryNames: []string{"A", "B", "C"},
expected: []*v3.TableRow{
{Data: []interface{}{"service2", 20.0, "string", false}},
{Data: []interface{}{"service1", "string", 10.0, true}},
{Data: []interface{}{"service3", true, 30.0, "string"}},
},
},
{
name: "Sort with SigNozOrderByValue",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", 20.0}},
{Data: []interface{}{"service2", 10.0}},
{Data: []interface{}{"service3", 30.0}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "desc"}}},
},
queryNames: []string{"A"},
expected: []*v3.TableRow{
{Data: []interface{}{"service3", 30.0}},
{Data: []interface{}{"service1", 20.0}},
{Data: []interface{}{"service2", 10.0}},
},
},
{
name: "Sort by multiple queries with mixed types",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", 10.0, "b", true}},
{Data: []interface{}{"service2", 20.0, "a", false}},
{Data: []interface{}{"service3", 10.0, "c", true}},
{Data: []interface{}{"service4", 20.0, "b", false}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
{Name: "B"},
{Name: "C"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "A", Order: "asc"}}},
"B": {OrderBy: []v3.OrderBy{{ColumnName: "B", Order: "desc"}}},
"C": {OrderBy: []v3.OrderBy{{ColumnName: "C", Order: "asc"}}},
},
queryNames: []string{"A", "B", "C"},
expected: []*v3.TableRow{
{Data: []interface{}{"service3", 10.0, "c", true}},
{Data: []interface{}{"service1", 10.0, "b", true}},
{Data: []interface{}{"service4", 20.0, "b", false}},
{Data: []interface{}{"service2", 20.0, "a", false}},
},
},
{
name: "Sort with all n/a values",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", "n/a", "n/a"}},
{Data: []interface{}{"service2", "n/a", "n/a"}},
{Data: []interface{}{"service3", "n/a", "n/a"}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
{Name: "B"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "A", Order: "asc"}}},
"B": {OrderBy: []v3.OrderBy{{ColumnName: "B", Order: "desc"}}},
},
queryNames: []string{"A", "B"},
expected: []*v3.TableRow{
{Data: []interface{}{"service1", "n/a", "n/a"}},
{Data: []interface{}{"service2", "n/a", "n/a"}},
{Data: []interface{}{"service3", "n/a", "n/a"}},
},
},
{
name: "Sort with negative numbers",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", -10.0}},
{Data: []interface{}{"service2", 20.0}},
{Data: []interface{}{"service3", -30.0}},
{Data: []interface{}{"service4", 0.0}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "A", Order: "asc"}}},
},
queryNames: []string{"A"},
expected: []*v3.TableRow{
{Data: []interface{}{"service3", -30.0}},
{Data: []interface{}{"service1", -10.0}},
{Data: []interface{}{"service4", 0.0}},
{Data: []interface{}{"service2", 20.0}},
},
},
{
name: "Sort with mixed case strings",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", "Apple"}},
{Data: []interface{}{"service2", "banana"}},
{Data: []interface{}{"service3", "Cherry"}},
{Data: []interface{}{"service4", "date"}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "A", Order: "asc"}}},
},
queryNames: []string{"A"},
expected: []*v3.TableRow{
{Data: []interface{}{"service1", "Apple"}},
{Data: []interface{}{"service3", "Cherry"}},
{Data: []interface{}{"service2", "banana"}},
{Data: []interface{}{"service4", "date"}},
},
},
{
name: "Sort with empty strings",
rows: []*v3.TableRow{
{Data: []interface{}{"service1", ""}},
{Data: []interface{}{"service2", "b"}},
{Data: []interface{}{"service3", ""}},
{Data: []interface{}{"service4", "a"}},
},
columns: []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
},
builderQueries: map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "A", Order: "asc"}}},
},
queryNames: []string{"A"},
expected: []*v3.TableRow{
{Data: []interface{}{"service1", ""}},
{Data: []interface{}{"service3", ""}},
{Data: []interface{}{"service4", "a"}},
{Data: []interface{}{"service2", "b"}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sortRows(tt.rows, tt.columns, tt.builderQueries, tt.queryNames)
if !reflect.DeepEqual(tt.rows, tt.expected) {
exp, _ := json.Marshal(tt.expected)
got, _ := json.Marshal(tt.rows)
t.Errorf("sortRows() = %v, want %v", string(got), string(exp))
}
})
}
}
func TestSortRowsWithEmptyQueries(t *testing.T) {
rows := []*v3.TableRow{
{Data: []interface{}{"service1", 20.0}},
{Data: []interface{}{"service2", 10.0}},
{Data: []interface{}{"service3", 30.0}},
}
columns := []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
}
builderQueries := map[string]*v3.BuilderQuery{}
queryNames := []string{}
sortRows(rows, columns, builderQueries, queryNames)
// Expect the original order to be maintained
expected := []*v3.TableRow{
{Data: []interface{}{"service1", 20.0}},
{Data: []interface{}{"service2", 10.0}},
{Data: []interface{}{"service3", 30.0}},
}
if !reflect.DeepEqual(rows, expected) {
t.Errorf("sortRows() with empty queries = %v, want %v", rows, expected)
}
}
func TestSortRowsWithInvalidColumnName(t *testing.T) {
rows := []*v3.TableRow{
{Data: []interface{}{"service1", 20.0}},
{Data: []interface{}{"service2", 10.0}},
{Data: []interface{}{"service3", 30.0}},
}
columns := []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
}
builderQueries := map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "InvalidColumn", Order: "asc"}}},
}
queryNames := []string{"A"}
sortRows(rows, columns, builderQueries, queryNames)
// Expect the original order to be maintained
expected := []*v3.TableRow{
{Data: []interface{}{"service1", 20.0}},
{Data: []interface{}{"service2", 10.0}},
{Data: []interface{}{"service3", 30.0}},
}
if !reflect.DeepEqual(rows, expected) {
t.Errorf("sortRows() with invalid column name = %v, want %v", rows, expected)
}
}
func TestSortRowsStability(t *testing.T) {
rows := []*v3.TableRow{
{Data: []interface{}{"service1", 10.0, "a"}},
{Data: []interface{}{"service2", 10.0, "b"}},
{Data: []interface{}{"service3", 10.0, "c"}},
}
columns := []*v3.TableColumn{
{Name: "service_name"},
{Name: "A"},
{Name: "B"},
}
builderQueries := map[string]*v3.BuilderQuery{
"A": {OrderBy: []v3.OrderBy{{ColumnName: "A", Order: "asc"}}},
}
queryNames := []string{"A"}
sortRows(rows, columns, builderQueries, queryNames)
// Expect the original order to be maintained for equal values
expected := []*v3.TableRow{
{Data: []interface{}{"service1", 10.0, "a"}},
{Data: []interface{}{"service2", 10.0, "b"}},
{Data: []interface{}{"service3", 10.0, "c"}},
}
if !reflect.DeepEqual(rows, expected) {
t.Errorf("sortRows() stability test failed = %v, want %v", rows, expected)
}
}
func TestTransformToTableForClickHouseQueries(t *testing.T) {
tests := []struct {
name string
input []*v3.Result
expected []*v3.Result
}{
{
name: "Empty input",
input: []*v3.Result{},
expected: []*v3.Result{},
},
{
name: "Single result with one series",
input: []*v3.Result{
{
QueryName: "A",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "frontend"},
},
Points: []v3.Point{
{Value: 10.0},
},
},
},
},
},
expected: []*v3.Result{
{
Table: &v3.Table{
Columns: []*v3.TableColumn{
{Name: "service"},
{Name: "A"},
},
Rows: []*v3.TableRow{
{Data: []interface{}{"frontend", 10.0}},
},
},
},
},
},
{
name: "Multiple results with multiple series",
input: []*v3.Result{
{
QueryName: "A",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "frontend", "env": "prod"},
},
Points: []v3.Point{
{Value: 10.0},
},
},
{
LabelsArray: []map[string]string{
{"service": "backend", "env": "prod"},
},
Points: []v3.Point{
{Value: 20.0},
},
},
},
},
{
QueryName: "B",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "frontend", "env": "prod"},
},
Points: []v3.Point{
{Value: 15.0},
},
},
{
LabelsArray: []map[string]string{
{"service": "backend", "env": "prod"},
},
Points: []v3.Point{
{Value: 25.0},
},
},
},
},
},
expected: []*v3.Result{
{
Table: &v3.Table{
Columns: []*v3.TableColumn{
{Name: "service"},
{Name: "env"},
{Name: "A"},
{Name: "B"},
},
Rows: []*v3.TableRow{
{Data: []interface{}{"frontend", "prod", 10.0, nil}},
{Data: []interface{}{"backend", "prod", 20.0, nil}},
{Data: []interface{}{"frontend", "prod", nil, 15.0}},
{Data: []interface{}{"backend", "prod", nil, 25.0}},
},
},
},
},
},
{
name: "Results with missing labels",
input: []*v3.Result{
{
QueryName: "A",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "frontend"},
},
Points: []v3.Point{
{Value: 10.0},
},
},
},
},
{
QueryName: "B",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"env": "prod"},
},
Points: []v3.Point{
{Value: 20.0},
},
},
},
},
},
expected: []*v3.Result{
{
Table: &v3.Table{
Columns: []*v3.TableColumn{
{Name: "service"},
{Name: "env"},
{Name: "A"},
{Name: "B"},
},
Rows: []*v3.TableRow{
{Data: []interface{}{"frontend", "n/a", 10.0, nil}},
{Data: []interface{}{"n/a", "prod", nil, 20.0}},
},
},
},
},
},
{
name: "Results with empty series",
input: []*v3.Result{
{
QueryName: "A",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "frontend"},
},
Points: []v3.Point{
{Value: 10.0},
},
},
},
},
{
QueryName: "B",
Series: []*v3.Series{},
},
},
expected: []*v3.Result{
{
Table: &v3.Table{
Columns: []*v3.TableColumn{
{Name: "service"},
{Name: "A"},
},
Rows: []*v3.TableRow{
{Data: []interface{}{"frontend", 10.0}},
},
},
},
},
},
{
name: "Results with empty points",
input: []*v3.Result{
{
QueryName: "A",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "frontend"},
},
Points: []v3.Point{},
},
},
},
{
QueryName: "B",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "backend"},
},
Points: []v3.Point{
{Value: 20.0},
},
},
},
},
},
expected: []*v3.Result{
{
Table: &v3.Table{
Columns: []*v3.TableColumn{
{Name: "service"},
{Name: "B"},
},
Rows: []*v3.TableRow{
{Data: []interface{}{"frontend", nil}},
{Data: []interface{}{"backend", 20.0}},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := TransformToTableForClickHouseQueries(tt.input)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("TransformToTableForClickHouseQueries() = %v, want %v", result, tt.expected)
}
})
}
}
func TestTransformToTableForClickHouseQueriesSorting(t *testing.T) {
input := []*v3.Result{
{
QueryName: "B",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "frontend"},
},
Points: []v3.Point{
{Value: 10.0},
},
},
},
},
{
QueryName: "A",
Series: []*v3.Series{
{
LabelsArray: []map[string]string{
{"service": "backend"},
},
Points: []v3.Point{
{Value: 20.0},
},
},
},
},
}
expected := []*v3.Result{
{
Table: &v3.Table{
Columns: []*v3.TableColumn{
{Name: "service"},
{Name: "A"},
{Name: "B"},
},
Rows: []*v3.TableRow{
{Data: []interface{}{"backend", 20.0, nil}},
{Data: []interface{}{"frontend", nil, 10.0}},
},
},
},
}
result := TransformToTableForClickHouseQueries(input)
if !reflect.DeepEqual(result, expected) {
t.Errorf("TransformToTableForClickHouseQueries() sorting test failed. Got %v, want %v", result, expected)
}
}