mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-21 02:40:40 +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
122 lines
3.8 KiB
Go
122 lines
3.8 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"context"
|
|
|
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
"github.com/huandu/go-sqlbuilder"
|
|
)
|
|
|
|
// Condition compiles a resolved key into the conditions of one filter term.
|
|
// The storage's part in the fingerprint split narrows the fields. Every
|
|
// field then compiles through the storage's Compile. It returns the
|
|
// per-field warnings. The resolution carries its own warnings.
|
|
func Condition(
|
|
ctx context.Context,
|
|
q qbtypes.QueryInfo,
|
|
storage qbtypes.Storage,
|
|
resolved qbtypes.Resolved,
|
|
dropResourceFields bool,
|
|
operator qbtypes.FilterOperator,
|
|
value any,
|
|
sb *sqlbuilder.SelectBuilder,
|
|
) ([]string, []string, error) {
|
|
if resolved.Skipped {
|
|
return nil, nil, nil
|
|
}
|
|
if operator.IsFunctionOperator() && operator != qbtypes.FilterOperatorSearch {
|
|
for _, logical := range resolved.Fields {
|
|
switch logical.FieldContext {
|
|
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextScope:
|
|
// a body function on a map-backed key is a user error, and the split must not hide it
|
|
return nil, nil, NewFunctionUnsupportedError(operator)
|
|
}
|
|
}
|
|
}
|
|
|
|
fields := resolved.Fields
|
|
switch storage.Traits().Split {
|
|
case qbtypes.MainOfSplit:
|
|
// the sub-query cannot know fallback keys, so those stay
|
|
if dropResourceFields && !resolved.FromFallback {
|
|
filtered := make([]*telemetrytypes.LogicalField, 0, len(fields))
|
|
for _, logical := range fields {
|
|
if logical.FieldContext != telemetrytypes.FieldContextResource {
|
|
filtered = append(filtered, logical)
|
|
}
|
|
}
|
|
if len(filtered) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
fields = filtered
|
|
}
|
|
case qbtypes.FingerprintOfSplit:
|
|
filtered := make([]*telemetrytypes.LogicalField, 0, len(fields))
|
|
for _, logical := range fields {
|
|
if logical.FieldContext == telemetrytypes.FieldContextResource {
|
|
filtered = append(filtered, logical)
|
|
}
|
|
}
|
|
fields = filtered
|
|
}
|
|
|
|
conds := make([]string, 0, len(fields))
|
|
var warnings []string
|
|
for _, logical := range fields {
|
|
compiled, err := storage.Compile(ctx, q, logical, operator, value, sb)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if compiled.Condition != "" {
|
|
conds = append(conds, compiled.Condition)
|
|
}
|
|
warnings = append(warnings, compiled.Warnings...)
|
|
}
|
|
return conds, warnings, nil
|
|
}
|
|
|
|
// RejectsBodyFunction reports the error a storage without body functions
|
|
// returns for one, before resolution. The fingerprint side of a split skips
|
|
// the term instead, because the main query evaluates it.
|
|
func RejectsBodyFunction(traits qbtypes.Traits, operator qbtypes.FilterOperator) (skip bool, err error) {
|
|
if !operator.IsFunctionOperator() && operator != qbtypes.FilterOperatorSearch {
|
|
return false, nil
|
|
}
|
|
if traits.SupportsBodyFunctions {
|
|
return false, nil
|
|
}
|
|
if traits.Split == qbtypes.FingerprintOfSplit {
|
|
return true, nil
|
|
}
|
|
return false, NewFunctionUnsupportedError(operator)
|
|
}
|
|
|
|
// Conditions resolves one key and compiles it: the filter condition for callers
|
|
// that do not run the filter visitor. The warnings carry the resolution's
|
|
// warnings first.
|
|
func Conditions(
|
|
ctx context.Context,
|
|
q qbtypes.QueryInfo,
|
|
storage qbtypes.Storage,
|
|
key *telemetrytypes.TelemetryFieldKey,
|
|
operator qbtypes.FilterOperator,
|
|
value any,
|
|
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
dropResourceFields bool,
|
|
sb *sqlbuilder.SelectBuilder,
|
|
) ([]string, []string, error) {
|
|
if _, err := RejectsBodyFunction(storage.Traits(), operator); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
resolved, err := Resolve(ctx, q, storage, key, operator, value, fieldKeys)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
conds, warnings, err := Condition(ctx, q, storage, resolved, dropResourceFields, operator, value, sb)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return conds, append(resolved.Warnings, warnings...), nil
|
|
}
|