diff --git a/pkg/telemetrytraces/condition_builder.go b/pkg/telemetrytraces/condition_builder.go new file mode 100644 index 0000000000..c08264849a --- /dev/null +++ b/pkg/telemetrytraces/condition_builder.go @@ -0,0 +1,352 @@ +package telemetrytraces + +import ( + "context" + "fmt" + + schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/huandu/go-sqlbuilder" +) + +var ( + indexV3Columns = map[string]*schema.Column{ + "ts_bucket_start": {Name: "ts_bucket_start", Type: schema.ColumnTypeUInt64}, + "resource_fingerprint": {Name: "resource_fingerprint", Type: schema.ColumnTypeString}, + + // intrinsic columns + "timestamp": {Name: "timestamp", Type: schema.DateTime64ColumnType{Precision: 9, Timezone: "UTC"}}, + "trace_id": {Name: "trace_id", Type: schema.FixedStringColumnType{Length: 32}}, + "span_id": {Name: "span_id", Type: schema.ColumnTypeString}, + "trace_state": {Name: "trace_state", Type: schema.ColumnTypeString}, + "parent_span_id": {Name: "parent_span_id", Type: schema.ColumnTypeString}, + "flags": {Name: "flags", Type: schema.ColumnTypeUInt32}, + "name": {Name: "name", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "kind": {Name: "kind", Type: schema.ColumnTypeInt8}, + "kind_string": {Name: "kind_string", Type: schema.ColumnTypeString}, + "duration_nano": {Name: "duration_nano", Type: schema.ColumnTypeUInt64}, + "status_code": {Name: "status_code", Type: schema.ColumnTypeInt16}, + "status_message": {Name: "status_message", Type: schema.ColumnTypeString}, + "status_code_string": {Name: "status_code_string", Type: schema.ColumnTypeString}, + + // attributes columns + "attributes_string": {Name: "attributes_string", Type: schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeString, + }}, + "attributes_number": {Name: "attributes_number", Type: schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeFloat64, + }}, + "attributes_bool": {Name: "attributes_bool", Type: schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeBool, + }}, + "resources_string": {Name: "resources_string", Type: schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeString, + }}, + + "events": {Name: "events", Type: schema.ArrayColumnType{ + ElementType: schema.ColumnTypeString, + }}, + "links": {Name: "links", Type: schema.ColumnTypeString}, + // derived columns + "response_status_code": {Name: "response_status_code", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "external_http_url": {Name: "external_http_url", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "http_url": {Name: "http_url", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "external_http_method": {Name: "external_http_method", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "http_method": {Name: "http_method", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "http_host": {Name: "http_host", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "db_name": {Name: "db_name", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "db_operation": {Name: "db_operation", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "has_error": {Name: "has_error", Type: schema.ColumnTypeBool}, + "is_remote": {Name: "is_remote", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + // materialized columns + "resource_string_service$$name": {Name: "resource_string_service$$name", Type: schema.ColumnTypeString}, + "attribute_string_http$$route": {Name: "attribute_string_http$$route", Type: schema.ColumnTypeString}, + "attribute_string_messaging$$system": {Name: "attribute_string_messaging$$system", Type: schema.ColumnTypeString}, + "attribute_string_messaging$$operation": {Name: "attribute_string_messaging$$operation", Type: schema.ColumnTypeString}, + "attribute_string_db$$system": {Name: "attribute_string_db$$system", Type: schema.ColumnTypeString}, + "attribute_string_rpc$$system": {Name: "attribute_string_rpc$$system", Type: schema.ColumnTypeString}, + "attribute_string_rpc$$service": {Name: "attribute_string_rpc$$service", Type: schema.ColumnTypeString}, + "attribute_string_rpc$$method": {Name: "attribute_string_rpc$$method", Type: schema.ColumnTypeString}, + "attribute_string_peer$$service": {Name: "attribute_string_peer$$service", Type: schema.ColumnTypeString}, + + // deprecated intrinsic columns + "traceID": {Name: "traceID", Type: schema.FixedStringColumnType{Length: 32}}, + "spanID": {Name: "spanID", Type: schema.ColumnTypeString}, + "parentSpanID": {Name: "parentSpanID", Type: schema.ColumnTypeString}, + "spanKind": {Name: "spanKind", Type: schema.ColumnTypeString}, + "durationNano": {Name: "durationNano", Type: schema.ColumnTypeUInt64}, + "statusCode": {Name: "statusCode", Type: schema.ColumnTypeInt16}, + "statusMessage": {Name: "statusMessage", Type: schema.ColumnTypeString}, + "statusCodeString": {Name: "statusCodeString", Type: schema.ColumnTypeString}, + + // deprecated derived columns + "references": {Name: "references", Type: schema.ColumnTypeString}, + "responseStatusCode": {Name: "responseStatusCode", Type: schema.ColumnTypeString}, + "externalHttpUrl": {Name: "externalHttpUrl", Type: schema.ColumnTypeString}, + "httpUrl": {Name: "httpUrl", Type: schema.ColumnTypeString}, + "externalHttpMethod": {Name: "externalHttpMethod", Type: schema.ColumnTypeString}, + "httpMethod": {Name: "httpMethod", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "httpHost": {Name: "httpHost", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "dbName": {Name: "dbName", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "dbOperation": {Name: "dbOperation", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "hasError": {Name: "hasError", Type: schema.ColumnTypeBool}, + "isRemote": {Name: "isRemote", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "serviceName": {Name: "serviceName", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "httpRoute": {Name: "httpRoute", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "msgSystem": {Name: "msgSystem", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "msgOperation": {Name: "msgOperation", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "dbSystem": {Name: "dbSystem", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "rpcSystem": {Name: "rpcSystem", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "rpcService": {Name: "rpcService", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "rpcMethod": {Name: "rpcMethod", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + "peerService": {Name: "peerService", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}}, + + // materialized exists columns + "resource_string_service$$name_exists": {Name: "resource_string_service$$name_exists", Type: schema.ColumnTypeBool}, + "attribute_string_http$$route_exists": {Name: "attribute_string_http$$route_exists", Type: schema.ColumnTypeBool}, + "attribute_string_messaging$$system_exists": {Name: "attribute_string_messaging$$system_exists", Type: schema.ColumnTypeBool}, + "attribute_string_messaging$$operation_exists": {Name: "attribute_string_messaging$$operation_exists", Type: schema.ColumnTypeBool}, + "attribute_string_db$$system_exists": {Name: "attribute_string_db$$system_exists", Type: schema.ColumnTypeBool}, + "attribute_string_rpc$$system_exists": {Name: "attribute_string_rpc$$system_exists", Type: schema.ColumnTypeBool}, + "attribute_string_rpc$$service_exists": {Name: "attribute_string_rpc$$service_exists", Type: schema.ColumnTypeBool}, + "attribute_string_rpc$$method_exists": {Name: "attribute_string_rpc$$method_exists", Type: schema.ColumnTypeBool}, + "attribute_string_peer$$service_exists": {Name: "attribute_string_peer$$service_exists", Type: schema.ColumnTypeBool}, + } +) + +// interface check +var _ qbtypes.ConditionBuilder = &conditionBuilder{} + +type conditionBuilder struct { +} + +func NewConditionBuilder() qbtypes.ConditionBuilder { + return &conditionBuilder{} +} + +func (c *conditionBuilder) GetColumn(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) { + + switch key.FieldContext { + case telemetrytypes.FieldContextResource: + return indexV3Columns["resources_string"], nil + case telemetrytypes.FieldContextScope: + // we don't have scope data stored in the spans yet + return nil, qbtypes.ErrColumnNotFound + case telemetrytypes.FieldContextAttribute: + switch key.FieldDataType { + case telemetrytypes.FieldDataTypeString: + return indexV3Columns["attributes_string"], nil + case telemetrytypes.FieldDataTypeInt64, telemetrytypes.FieldDataTypeFloat64, telemetrytypes.FieldDataTypeNumber: + return indexV3Columns["attributes_number"], nil + case telemetrytypes.FieldDataTypeBool: + return indexV3Columns["attributes_bool"], nil + } + case telemetrytypes.FieldContextSpan: + col, ok := indexV3Columns[key.Name] + if !ok { + return nil, qbtypes.ErrColumnNotFound + } + return col, nil + } + + return nil, qbtypes.ErrColumnNotFound +} + +func (c *conditionBuilder) GetTableFieldName(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (string, error) { + column, err := c.GetColumn(ctx, key) + if err != nil { + return "", err + } + + switch column.Type { + case schema.ColumnTypeString, + schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + schema.ColumnTypeUInt64, + schema.ColumnTypeUInt32, + schema.ColumnTypeInt8, + schema.ColumnTypeInt16, + schema.ColumnTypeBool, + schema.DateTime64ColumnType{Precision: 9, Timezone: "UTC"}, + schema.FixedStringColumnType{Length: 32}: + return column.Name, nil + case schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeString, + }: + // a key could have been materialized, if so return the materialized column name + if key.Materialized { + return telemetrytypes.FieldKeyToMaterializedColumnName(key), nil + } + return fmt.Sprintf("%s['%s']", column.Name, key.Name), nil + case schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeFloat64, + }: + // a key could have been materialized, if so return the materialized column name + if key.Materialized { + return telemetrytypes.FieldKeyToMaterializedColumnName(key), nil + } + return fmt.Sprintf("%s['%s']", column.Name, key.Name), nil + case schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeBool, + }: + // a key could have been materialized, if so return the materialized column name + if key.Materialized { + return telemetrytypes.FieldKeyToMaterializedColumnName(key), nil + } + return fmt.Sprintf("%s['%s']", column.Name, key.Name), nil + } + // should not reach here + return column.Name, nil +} + +func (c *conditionBuilder) GetCondition( + ctx context.Context, + key *telemetrytypes.TelemetryFieldKey, + operator qbtypes.FilterOperator, + value any, + sb *sqlbuilder.SelectBuilder, +) (string, error) { + column, err := c.GetColumn(ctx, key) + if err != nil { + return "", err + } + + tblFieldName, err := c.GetTableFieldName(ctx, key) + if err != nil { + return "", err + } + + tblFieldName, value = telemetrytypes.DataTypeCollisionHandledFieldName(key, value, tblFieldName) + + // regular operators + switch operator { + // regular operators + case qbtypes.FilterOperatorEqual: + return sb.E(tblFieldName, value), nil + case qbtypes.FilterOperatorNotEqual: + return sb.NE(tblFieldName, value), nil + case qbtypes.FilterOperatorGreaterThan: + return sb.G(tblFieldName, value), nil + case qbtypes.FilterOperatorGreaterThanOrEq: + return sb.GE(tblFieldName, value), nil + case qbtypes.FilterOperatorLessThan: + return sb.LT(tblFieldName, value), nil + case qbtypes.FilterOperatorLessThanOrEq: + return sb.LE(tblFieldName, value), nil + + // like and not like + case qbtypes.FilterOperatorLike: + return sb.Like(tblFieldName, value), nil + case qbtypes.FilterOperatorNotLike: + return sb.NotLike(tblFieldName, value), nil + case qbtypes.FilterOperatorILike: + return sb.ILike(tblFieldName, value), nil + case qbtypes.FilterOperatorNotILike: + return sb.NotILike(tblFieldName, value), nil + + case qbtypes.FilterOperatorContains: + return sb.ILike(tblFieldName, value), nil + case qbtypes.FilterOperatorNotContains: + return sb.NotILike(tblFieldName, value), nil + + case qbtypes.FilterOperatorRegexp: + exp := fmt.Sprintf(`match(%s, %s)`, tblFieldName, sb.Var(value)) + return sb.And(exp), nil + case qbtypes.FilterOperatorNotRegexp: + exp := fmt.Sprintf(`not match(%s, %s)`, tblFieldName, sb.Var(value)) + return sb.And(exp), nil + + // between and not between + case qbtypes.FilterOperatorBetween: + values, ok := value.([]any) + if !ok { + return "", qbtypes.ErrBetweenValues + } + if len(values) != 2 { + return "", qbtypes.ErrBetweenValues + } + return sb.Between(tblFieldName, values[0], values[1]), nil + case qbtypes.FilterOperatorNotBetween: + values, ok := value.([]any) + if !ok { + return "", qbtypes.ErrBetweenValues + } + if len(values) != 2 { + return "", qbtypes.ErrBetweenValues + } + return sb.NotBetween(tblFieldName, values[0], values[1]), nil + + // in and not in + case qbtypes.FilterOperatorIn: + values, ok := value.([]any) + if !ok { + return "", qbtypes.ErrInValues + } + return sb.In(tblFieldName, values...), nil + case qbtypes.FilterOperatorNotIn: + values, ok := value.([]any) + if !ok { + return "", qbtypes.ErrInValues + } + return sb.NotIn(tblFieldName, values...), nil + + // exists and not exists + // in the query builder, `exists` and `not exists` are used for + // key membership checks, so depending on the column type, the condition changes + case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists: + var value any + switch column.Type { + case schema.ColumnTypeString, + schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + schema.FixedStringColumnType{Length: 32}, + schema.DateTime64ColumnType{Precision: 9, Timezone: "UTC"}: + value = "" + if operator == qbtypes.FilterOperatorExists { + return sb.NE(tblFieldName, value), nil + } else { + return sb.E(tblFieldName, value), nil + } + case schema.ColumnTypeUInt64, + schema.ColumnTypeUInt32, + schema.ColumnTypeUInt8, + schema.ColumnTypeInt8, + schema.ColumnTypeInt16, + schema.ColumnTypeBool: + value = 0 + if operator == qbtypes.FilterOperatorExists { + return sb.NE(tblFieldName, value), nil + } else { + return sb.E(tblFieldName, value), nil + } + case schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeString, + }, schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeBool, + }, schema.MapColumnType{ + KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}, + ValueType: schema.ColumnTypeFloat64, + }: + leftOperand := fmt.Sprintf("mapContains(%s, '%s')", column.Name, key.Name) + if key.Materialized { + leftOperand = telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key) + } + if operator == qbtypes.FilterOperatorExists { + return sb.E(leftOperand, true), nil + } else { + return sb.NE(leftOperand, true), nil + } + default: + return "", fmt.Errorf("exists operator is not supported for column type %s", column.Type) + } + } + return "", nil +} diff --git a/pkg/telemetrytraces/condition_builder_test.go b/pkg/telemetrytraces/condition_builder_test.go new file mode 100644 index 0000000000..e7bd8e94d0 --- /dev/null +++ b/pkg/telemetrytraces/condition_builder_test.go @@ -0,0 +1,298 @@ +package telemetrytraces + +import ( + "context" + "testing" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/huandu/go-sqlbuilder" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetFieldKeyName(t *testing.T) { + ctx := context.Background() + conditionBuilder := &conditionBuilder{} + + testCases := []struct { + name string + key telemetrytypes.TelemetryFieldKey + expectedResult string + expectedError error + }{ + { + name: "Simple column type - timestamp", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + expectedResult: "timestamp", + expectedError: nil, + }, + { + name: "Map column type - string attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "user.id", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + expectedResult: "attributes_string['user.id']", + expectedError: nil, + }, + { + name: "Map column type - number attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "request.size", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeNumber, + }, + expectedResult: "attributes_number['request.size']", + expectedError: nil, + }, + { + name: "Map column type - bool attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "request.success", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeBool, + }, + expectedResult: "attributes_bool['request.success']", + expectedError: nil, + }, + { + name: "Map column type - resource attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "service.name", + FieldContext: telemetrytypes.FieldContextResource, + }, + expectedResult: "resources_string['service.name']", + expectedError: nil, + }, + { + name: "Non-existent column", + key: telemetrytypes.TelemetryFieldKey{ + Name: "nonexistent_field", + FieldContext: telemetrytypes.FieldContextSpan, + }, + expectedResult: "", + expectedError: qbtypes.ErrColumnNotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := conditionBuilder.GetTableFieldName(ctx, &tc.key) + + if tc.expectedError != nil { + assert.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedResult, result) + } + }) + } +} + +func TestGetCondition(t *testing.T) { + ctx := context.Background() + conditionBuilder := NewConditionBuilder() + + testCases := []struct { + name string + key telemetrytypes.TelemetryFieldKey + operator qbtypes.FilterOperator + value any + expectedSQL string + expectedError error + }{ + { + name: "Not Equal operator - timestamp", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorNotEqual, + value: uint64(1617979338000000000), + expectedSQL: "timestamp <> ?", + expectedError: nil, + }, + { + name: "Greater Than operator - number attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "request.duration", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeNumber, + }, + operator: qbtypes.FilterOperatorGreaterThan, + value: float64(100), + expectedSQL: "attributes_number['request.duration'] > ?", + expectedError: nil, + }, + { + name: "Less Than operator - number attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "request.size", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeNumber, + }, + operator: qbtypes.FilterOperatorLessThan, + value: float64(1024), + expectedSQL: "attributes_number['request.size'] < ?", + expectedError: nil, + }, + { + name: "Greater Than Or Equal operator - timestamp", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorGreaterThanOrEq, + value: uint64(1617979338000000000), + expectedSQL: "timestamp >= ?", + expectedError: nil, + }, + { + name: "Less Than Or Equal operator - timestamp", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorLessThanOrEq, + value: uint64(1617979338000000000), + expectedSQL: "timestamp <= ?", + expectedError: nil, + }, + { + name: "ILike operator - string attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "user.id", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + operator: qbtypes.FilterOperatorILike, + value: "%admin%", + expectedSQL: "WHERE LOWER(attributes_string['user.id']) LIKE LOWER(?)", + expectedError: nil, + }, + { + name: "Not ILike operator - string attribute", + key: telemetrytypes.TelemetryFieldKey{ + Name: "user.id", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + operator: qbtypes.FilterOperatorNotILike, + value: "%admin%", + expectedSQL: "WHERE LOWER(attributes_string['user.id']) NOT LIKE LOWER(?)", + expectedError: nil, + }, + { + name: "Between operator - timestamp", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorBetween, + value: []any{uint64(1617979338000000000), uint64(1617979348000000000)}, + expectedSQL: "timestamp BETWEEN ? AND ?", + expectedError: nil, + }, + { + name: "Between operator - invalid value", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorBetween, + value: "invalid", + expectedSQL: "", + expectedError: qbtypes.ErrBetweenValues, + }, + { + name: "Between operator - insufficient values", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorBetween, + value: []any{uint64(1617979338000000000)}, + expectedSQL: "", + expectedError: qbtypes.ErrBetweenValues, + }, + { + name: "Not Between operator - timestamp", + key: telemetrytypes.TelemetryFieldKey{ + Name: "timestamp", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorNotBetween, + value: []any{uint64(1617979338000000000), uint64(1617979348000000000)}, + expectedSQL: "timestamp NOT BETWEEN ? AND ?", + expectedError: nil, + }, + { + name: "Exists operator - map field", + key: telemetrytypes.TelemetryFieldKey{ + Name: "user.id", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + operator: qbtypes.FilterOperatorExists, + value: nil, + expectedSQL: "mapContains(attributes_string, 'user.id') = ?", + expectedError: nil, + }, + { + name: "Not Exists operator - map field", + key: telemetrytypes.TelemetryFieldKey{ + Name: "user.id", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + operator: qbtypes.FilterOperatorNotExists, + value: nil, + expectedSQL: "mapContains(attributes_string, 'user.id') <> ?", + expectedError: nil, + }, + { + name: "Contains operator - map field", + key: telemetrytypes.TelemetryFieldKey{ + Name: "user.id", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + operator: qbtypes.FilterOperatorContains, + value: "admin", + expectedSQL: "WHERE LOWER(attributes_string['user.id']) LIKE LOWER(?)", + expectedError: nil, + }, + { + name: "Non-existent column", + key: telemetrytypes.TelemetryFieldKey{ + Name: "nonexistent_field", + FieldContext: telemetrytypes.FieldContextSpan, + }, + operator: qbtypes.FilterOperatorEqual, + value: "value", + expectedSQL: "", + expectedError: qbtypes.ErrColumnNotFound, + }, + } + + for _, tc := range testCases { + sb := sqlbuilder.NewSelectBuilder() + t.Run(tc.name, func(t *testing.T) { + cond, err := conditionBuilder.GetCondition(ctx, &tc.key, tc.operator, tc.value, sb) + sb.Where(cond) + + if tc.expectedError != nil { + assert.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + sql, _ := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + assert.Contains(t, sql, tc.expectedSQL) + } + }) + } +} diff --git a/pkg/telemetrytraces/tables.go b/pkg/telemetrytraces/tables.go new file mode 100644 index 0000000000..fe7779e253 --- /dev/null +++ b/pkg/telemetrytraces/tables.go @@ -0,0 +1,10 @@ +package telemetrytraces + +const ( + DBName = "signoz_traces" + SpanIndexV3TableName = "distributed_signoz_index_v3" + SpanIndexV3LocalTableName = "signoz_index_v3" + TagAttributesV2TableName = "distributed_tag_attributes_v2" + TagAttributesV2LocalTableName = "tag_attributes_v2" + TopLevelOperationsTableName = "distributed_top_level_operations" +)