mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-28 06:22:01 +08:00
268 lines
7.8 KiB
Go
268 lines
7.8 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
chparser "github.com/AfterShip/clickhouse-sql-parser/parser"
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
"github.com/huandu/go-sqlbuilder"
|
|
)
|
|
|
|
type aggExprRewriter struct {
|
|
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
|
fieldMapper qbtypes.FieldMapper
|
|
conditionBuilder qbtypes.ConditionBuilder
|
|
jsonBodyPrefix string
|
|
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
|
}
|
|
|
|
var _ qbtypes.AggExprRewriter = (*aggExprRewriter)(nil)
|
|
|
|
func NewAggExprRewriter(
|
|
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
|
fieldMapper qbtypes.FieldMapper,
|
|
conditionBuilder qbtypes.ConditionBuilder,
|
|
jsonBodyPrefix string,
|
|
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
|
) *aggExprRewriter {
|
|
return &aggExprRewriter{
|
|
fullTextColumn: fullTextColumn,
|
|
fieldMapper: fieldMapper,
|
|
conditionBuilder: conditionBuilder,
|
|
jsonBodyPrefix: jsonBodyPrefix,
|
|
jsonKeyToKey: jsonKeyToKey,
|
|
}
|
|
}
|
|
|
|
// Rewrite parses the given aggregation expression, maps the column, and condition to
|
|
// valid data source column and condition expression, and returns the rewritten expression
|
|
// and the args if the parametric aggregation function is used.
|
|
func (r *aggExprRewriter) Rewrite(
|
|
ctx context.Context,
|
|
expr string,
|
|
rateInterval uint64,
|
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
) (string, []any, error) {
|
|
|
|
wrapped := fmt.Sprintf("SELECT %s", expr)
|
|
p := chparser.NewParser(wrapped)
|
|
stmts, err := p.ParseStmts()
|
|
|
|
if err != nil {
|
|
return "", nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to parse aggregation expression %q", expr)
|
|
}
|
|
|
|
if len(stmts) == 0 {
|
|
return "", nil, errors.NewInternalf(errors.CodeInternal, "no statements found for %q", expr)
|
|
}
|
|
|
|
sel, ok := stmts[0].(*chparser.SelectQuery)
|
|
if !ok {
|
|
return "", nil, errors.NewInternalf(errors.CodeInternal, "expected SelectQuery, got %T", stmts[0])
|
|
}
|
|
|
|
if len(sel.SelectItems) == 0 {
|
|
return "", nil, errors.NewInternalf(errors.CodeInternal, "no SELECT items for %q", expr)
|
|
}
|
|
|
|
visitor := newExprVisitor(keys,
|
|
r.fullTextColumn,
|
|
r.fieldMapper,
|
|
r.conditionBuilder,
|
|
r.jsonBodyPrefix,
|
|
r.jsonKeyToKey,
|
|
)
|
|
// Rewrite the first select item (our expression)
|
|
if err := sel.SelectItems[0].Accept(visitor); err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
if visitor.isRate {
|
|
return fmt.Sprintf("%s/%d", sel.SelectItems[0].String(), rateInterval), visitor.chArgs, nil
|
|
}
|
|
return sel.SelectItems[0].String(), visitor.chArgs, nil
|
|
}
|
|
|
|
// RewriteMulti rewrites a slice of expressions.
|
|
func (r *aggExprRewriter) RewriteMulti(
|
|
ctx context.Context,
|
|
exprs []string,
|
|
rateInterval uint64,
|
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
) ([]string, [][]any, error) {
|
|
out := make([]string, len(exprs))
|
|
var errs []error
|
|
var chArgsList [][]any
|
|
for i, e := range exprs {
|
|
w, chArgs, err := r.Rewrite(ctx, e, rateInterval, keys)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
out[i] = e
|
|
} else {
|
|
out[i] = w
|
|
chArgsList = append(chArgsList, chArgs)
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return out, nil, errors.Join(errs...)
|
|
}
|
|
return out, chArgsList, nil
|
|
}
|
|
|
|
// exprVisitor walks FunctionExpr nodes and applies the mappers.
|
|
type exprVisitor struct {
|
|
chparser.DefaultASTVisitor
|
|
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey
|
|
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
|
fieldMapper qbtypes.FieldMapper
|
|
conditionBuilder qbtypes.ConditionBuilder
|
|
jsonBodyPrefix string
|
|
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
|
Modified bool
|
|
chArgs []any
|
|
isRate bool
|
|
}
|
|
|
|
func newExprVisitor(
|
|
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
|
fieldMapper qbtypes.FieldMapper,
|
|
conditionBuilder qbtypes.ConditionBuilder,
|
|
jsonBodyPrefix string,
|
|
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
|
) *exprVisitor {
|
|
return &exprVisitor{
|
|
fieldKeys: fieldKeys,
|
|
fullTextColumn: fullTextColumn,
|
|
fieldMapper: fieldMapper,
|
|
conditionBuilder: conditionBuilder,
|
|
jsonBodyPrefix: jsonBodyPrefix,
|
|
jsonKeyToKey: jsonKeyToKey,
|
|
}
|
|
}
|
|
|
|
// VisitFunctionExpr is invoked for each function call in the AST.
|
|
func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
|
name := strings.ToLower(fn.Name.Name)
|
|
|
|
aggFunc, ok := AggreFuncMap[valuer.NewString(name)]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var args []chparser.Expr
|
|
if fn.Params != nil && fn.Params.Items != nil {
|
|
args = fn.Params.Items.Items
|
|
}
|
|
|
|
// if we know aggregation function, we must ensure that the number of arguments is correct
|
|
if aggFunc.RequireArgs {
|
|
if len(args) < aggFunc.MinArgs || len(args) > aggFunc.MaxArgs {
|
|
return errors.NewInternalf(errors.CodeInternal, "invalid number of arguments for %q: %d", name, len(args))
|
|
}
|
|
}
|
|
fn.Name.Name = aggFunc.FuncName
|
|
if aggFunc.Rate {
|
|
v.isRate = true
|
|
}
|
|
|
|
dataType := telemetrytypes.FieldDataTypeString
|
|
if aggFunc.Numeric {
|
|
dataType = telemetrytypes.FieldDataTypeFloat64
|
|
}
|
|
|
|
// Handle *If functions with predicate + values
|
|
if aggFunc.FuncCombinator {
|
|
// Map the predicate (last argument)
|
|
origPred := args[len(args)-1].String()
|
|
whereClause, _, err := PrepareWhereClause(
|
|
origPred,
|
|
FilterExprVisitorOpts{
|
|
FieldKeys: v.fieldKeys,
|
|
FieldMapper: v.fieldMapper,
|
|
ConditionBuilder: v.conditionBuilder,
|
|
FullTextColumn: v.fullTextColumn,
|
|
JsonBodyPrefix: v.jsonBodyPrefix,
|
|
JsonKeyToKey: v.jsonKeyToKey,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newPred, chArgs := whereClause.BuildWithFlavor(sqlbuilder.ClickHouse)
|
|
newPred = strings.TrimPrefix(newPred, "WHERE")
|
|
parsedPred, err := parseFragment(newPred)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
args[len(args)-1] = parsedPred
|
|
v.Modified = true
|
|
v.chArgs = chArgs
|
|
|
|
// Map each value column argument
|
|
for i := 0; i < len(args)-1; i++ {
|
|
origVal := args[i].String()
|
|
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(origVal)
|
|
expr, exprArgs, err := CollisionHandledFinalExpr(context.Background(), &fieldKey, v.fieldMapper, v.conditionBuilder, v.fieldKeys, dataType)
|
|
if err != nil {
|
|
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to get table field name for %q", origVal)
|
|
}
|
|
v.chArgs = append(v.chArgs, exprArgs...)
|
|
newVal := expr
|
|
parsedVal, err := parseFragment(newVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
args[i] = parsedVal
|
|
v.Modified = true
|
|
}
|
|
} else {
|
|
// Non-If functions: map every argument as a column/value
|
|
for i, arg := range args {
|
|
orig := arg.String()
|
|
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(orig)
|
|
expr, exprArgs, err := CollisionHandledFinalExpr(context.Background(), &fieldKey, v.fieldMapper, v.conditionBuilder, v.fieldKeys, dataType)
|
|
if err != nil {
|
|
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to get table field name for %q", orig)
|
|
}
|
|
v.chArgs = append(v.chArgs, exprArgs...)
|
|
newCol := expr
|
|
parsed, err := parseFragment(newCol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
args[i] = parsed
|
|
v.Modified = true
|
|
}
|
|
if aggFunc.Rate {
|
|
v.Modified = true
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseFragment parses a SQL expression fragment by wrapping in SELECT.
|
|
func parseFragment(sql string) (chparser.Expr, error) {
|
|
wrapped := fmt.Sprintf("SELECT %s", sql)
|
|
p := chparser.NewParser(wrapped)
|
|
stmts, err := p.ParseStmts()
|
|
if err != nil {
|
|
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to parse re-written expression %q", sql)
|
|
}
|
|
sel, ok := stmts[0].(*chparser.SelectQuery)
|
|
if !ok {
|
|
return nil, errors.NewInternalf(errors.CodeInternal, "unexpected statement type in re-written expression %q: %T", sql, stmts[0])
|
|
}
|
|
if len(sel.SelectItems) == 0 {
|
|
return nil, errors.NewInternalf(errors.CodeInternal, "no select items in re-written expression %q", sql)
|
|
}
|
|
return sel.SelectItems[0].Expr, nil
|
|
}
|