mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-27 13:50:41 +01:00
#### Description
- Every user-controlled field name that reaches generated SQL goes
through the new `pkg/clickhousesql` package (`Identifier`,
`StringLiteral`, `Literal`, `LikePattern`): map reads and `mapContains`,
JSON sub-column paths and the JSON body access plan, labels, fingerprint
labels, materialized column names, select aliases, group-by and order-by
references, the legacy string-body JSONPath, and the raw SQL in the
trace funnel, trace detail and infra monitoring modules. Filter
expressions built from request or telemetry values use
`querybuilder.FilterStringLiteral`. The same package now also renders
dashboard variable values in the querier, LIKE patterns in the metadata
store and label lists in the PromQL transpiler, which each had their own
escaping.
- A `$` followed by a digit, `{` or `?` is written as `\x24`, which
ClickHouse decodes in identifiers and literals. Those are the forms the
tools react to: go-sqlbuilder resolves `$0` in a compiled fragment to
its own WHERE clause and recurses until the stack overflows, and
clickhouse-go rejects a query mixing `$<digits>` with `?` arguments. Any
other `$` stays literal, so materialized column names keep their `$$`
and render exactly as before; a key like `http.2xx` becomes ``
`attribute_string_http$\x242xx` `` instead of failing in the driver.
- Compiled sqlbuilder fragments (Select, GroupBy, OrderBy, raw Where
text) are wrapped with `sqlbuilder.Escape`; the metrics builder escapes
its compiled time-series subquery, which is compiled a second time when
joined.
- The raw statement validator (`ErrIfStatementIsNotValid`,
`LogIfStatementIsNotValid`) moves from
`pkg/querybuilder/clickhouse_sql.go` to
`pkg/clickhousesql/statement.go`. Its `Code*` identifiers drop the
`ClickHouseSQL` prefix; the code strings are unchanged.
- Unit tests round-trip the helpers over hostile names and drive them
through the modules' raw SQL;
`tests/integration/tests/queriercommon/08_field_name_quoting.py` and
`querier_json_body/07_field_name_quoting.py` query such names through
the logs, traces and metrics builders against a real ClickHouse.
#### Additional Information
- `docs/contributing/go/clickhousesql.md` documents the quoting
functions, where `sqlbuilder.Escape` belongs, the `$` rule and the
statement validator; `.claude/rules/go-contrib.md` points at it.
- `pkg/clickhousesql` is a leaf package so `telemetrytypes` (JSON access
plan) and `querybuilder` share one implementation without a cycle.
- For names without special characters the generated SQL is byte
identical.
- Not covered here: the legacy v3/v4 query_range builders and the
`pkg/query-service/utils` quoting helpers (`QuoteEscapedString`,
`QuoteEscapedStringForContains`, `ClickHouseFormattedValue`,
`AddBackTickToFormatTag`), the collector's `JSONSubColumnIndexExpr`, and
aggregation arguments naming a key that contains a backtick (rejected by
the SQL parser, a 500 as before).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
129 lines
1.9 KiB
Go
129 lines
1.9 KiB
Go
package querier
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
func getPointerValue(v any) any {
|
|
|
|
// Check if the interface value is nil
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
// Use reflection to check if the pointer is nil
|
|
rv := reflect.ValueOf(v)
|
|
if rv.Kind() == reflect.Pointer && rv.IsNil() {
|
|
return nil
|
|
}
|
|
|
|
switch x := v.(type) {
|
|
case *uint8:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *uint16:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *uint32:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *uint64:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *int:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *int8:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *int16:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *int32:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *int64:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *float32:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *float64:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *string:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case *bool:
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
return *x
|
|
case []any:
|
|
values := []any{}
|
|
for _, val := range x {
|
|
values = append(values, getPointerValue(val))
|
|
}
|
|
return values
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
// formatValueForProm formats the value to be used in promql.
|
|
func formatValueForProm(v any) string {
|
|
switch x := v.(type) {
|
|
case int:
|
|
return fmt.Sprintf("%d", x)
|
|
case float32, float64:
|
|
return fmt.Sprintf("%f", x)
|
|
case string:
|
|
return x
|
|
case bool:
|
|
return fmt.Sprintf("%v", x)
|
|
case []interface{}:
|
|
if len(x) == 0 {
|
|
return ""
|
|
}
|
|
switch x[0].(type) {
|
|
case string, int, float32, float64, bool:
|
|
// list of values joined by | for promql - a value can contain whitespace
|
|
var str []string
|
|
for _, sVal := range x {
|
|
str = append(str, fmt.Sprintf("%v", sVal))
|
|
}
|
|
return strings.Join(str, "|")
|
|
default:
|
|
return ""
|
|
}
|
|
default:
|
|
return ""
|
|
}
|
|
}
|