mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-20 18:30:41 +01:00
The semantic convention family as first call citizen revealed that the
current state of the query builder needs a bit refactoring for long term
maintenance.
The `FieldMapper` and `ConditionBuilder` are now one abstraction
`Storage`.
A storage now answers
- what the compiler cannot know i.e one read per field key (the bare
SQL, the membership present or absent, what an absent row reads, and
whether the read keeps its type or filters only).
- the fallback for a key metadata does not report
- its traits
- and one Condition compilation part.
And we introduce a new type to use in the system, `Resolved`
```
// Resolved is what resolution produces for one key: its meanings, and how
// they came to be. It is the only thing the compilers receive. Compile it
// with the operator and value it was resolved with.
type Resolved struct {
Key *telemetrytypes.TelemetryFieldKey
Fields []*telemetrytypes.LogicalField
// FromFallback: the fields came from the storage's fallback, not from
// metadata matches.
FromFallback bool
// Ambiguous: the matches held several interpretations.
Ambiguous bool
// Skipped: the storage contributes nothing for this key.
Skipped bool
Warnings []string
}
```
The prepared SQL has no changes, where it changed, it specifically made
the expression better by removing the redundant part.
- The prepared SQL remains identical with this refactoring
- No changes to integration tests
Assisted-by: Claude Fable 5.1
92 lines
2.9 KiB
Go
92 lines
2.9 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
)
|
|
|
|
// Column renders a resolved key as one bare column expression. The caller
|
|
// adds the alias. Group by, order by, and aggregation cast every candidate
|
|
// to the target type. A select field keeps the native read. The guard
|
|
// follows Absent. A sentinel read sits behind its presence test, so an
|
|
// absent row reads NULL. A column every row has reads bare and ends the
|
|
// candidate list. A NULL-reading field takes a presence branch only when
|
|
// other candidates follow it.
|
|
func Column(
|
|
ctx context.Context,
|
|
q qbtypes.QueryInfo,
|
|
storage qbtypes.Storage,
|
|
resolved qbtypes.Resolved,
|
|
target telemetrytypes.FieldDataType,
|
|
) (string, error) {
|
|
if len(resolved.Fields) == 0 {
|
|
return "", NewKeyNotFoundError(resolved.Key.Name, nil)
|
|
}
|
|
|
|
var targetValue any = ""
|
|
if target == telemetrytypes.FieldDataTypeFloat64 {
|
|
targetValue = 0.0
|
|
}
|
|
coerced := target != telemetrytypes.FieldDataTypeUnspecified
|
|
several := len(resolved.Fields) > 1
|
|
|
|
branches := make([]string, 0, len(resolved.Fields)*2)
|
|
filterOnly := false
|
|
for _, logical := range resolved.Fields {
|
|
read, err := LogicalRead(ctx, q, storage, logical)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if read.FilterOnly {
|
|
filterOnly = true
|
|
continue
|
|
}
|
|
// an array cannot sit inside Nullable or multiIf
|
|
if !several && bareRead(logical) {
|
|
return read.SQL, nil
|
|
}
|
|
expr := read.SQL
|
|
if coerced && !read.KeepType {
|
|
expr, _ = DataTypeCollisionHandledFieldName(logical.Single(), targetValue, expr, qbtypes.FilterOperatorUnknown)
|
|
}
|
|
// several native shapes share one multiIf, so every branch reads as text
|
|
branch := expr
|
|
if !coerced && several {
|
|
branch, _ = DataTypeCollisionHandledFieldName(logical.Single(), "", expr, qbtypes.FilterOperatorUnknown)
|
|
}
|
|
switch read.WhenAbsent {
|
|
case qbtypes.AlwaysPresent, qbtypes.AbsentIsValue:
|
|
if len(branches) == 0 {
|
|
return expr, nil
|
|
}
|
|
return fmt.Sprintf("multiIf(%s, %s)", strings.Join(branches, ", "), branch), nil
|
|
case qbtypes.AbsentIsNull:
|
|
if !several {
|
|
return expr, nil
|
|
}
|
|
branches = append(branches, read.Presence, branch)
|
|
default:
|
|
branches = append(branches, read.Presence, branch)
|
|
}
|
|
}
|
|
if len(branches) == 0 {
|
|
if filterOnly {
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "`%s` can be filtered but not selected or grouped", resolved.Key.Name)
|
|
}
|
|
return "", NewKeyNotFoundError(resolved.Key.Name, nil)
|
|
}
|
|
return fmt.Sprintf("multiIf(%s, NULL)", strings.Join(branches, ", ")), nil
|
|
}
|
|
|
|
func bareRead(logical *telemetrytypes.LogicalField) bool {
|
|
key := logical.Single()
|
|
return strings.Contains(key.Name, telemetrytypes.ArraySep) ||
|
|
strings.Contains(key.Name, telemetrytypes.ArrayAnyIndex) ||
|
|
key.FieldDataType.IsArray()
|
|
}
|