mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-06 19:40:41 +01:00
Compare commits
4 Commits
feat/semco
...
proto/tree
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cba59f689 | ||
|
|
540ab020c1 | ||
|
|
634bfef8aa | ||
|
|
9f1e95ec01 |
100
docs/tree-model-prototype.md
Normal file
100
docs/tree-model-prototype.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Tree-model prototype (proto/tree-model)
|
||||
|
||||
A local prototype of the proposed end-state architecture, built on
|
||||
`feat/semconv-phase2-signals` with byte-parity as the acceptance bar: every
|
||||
golden and every integration case must produce identical SQL. The prototype
|
||||
exists to let a reviewer judge the model empirically — how small the generic
|
||||
core really is, how large the per-signal residue really is, and where the
|
||||
line between them falls.
|
||||
|
||||
## What became generic
|
||||
|
||||
### The term compiler — `pkg/querybuilder/term.go`
|
||||
|
||||
`CompileTerm` is the flow of one filter term, written once:
|
||||
|
||||
1. resolve the evidence (`ResolveLogicalFields`, ambiguity warning),
|
||||
2. amend it with intrinsic storage (`TermSchema.AmendEvidence`),
|
||||
3. synthesize when it is empty (`TermSchema.Synthesize`),
|
||||
4. apply the resource-filter policy (`SkipResourcePolicy`),
|
||||
5. compile every field (`TermSchema.CompileField`), collecting warnings in
|
||||
a fixed order.
|
||||
|
||||
Six condition builders became delegates to it: traces, logs, metrics, audit,
|
||||
rule state history, and the resource filter. Each keeps a term-level
|
||||
intercept in front where one exists (`search()` on logs, the function-operator
|
||||
reject or skip), and implements the three `TermSchema` methods. The six
|
||||
hand-rolled copies of the flow are gone; a change to the flow — the order of
|
||||
warnings, the synthesized-exemption of the resource drop — now has one home.
|
||||
|
||||
`CompileFieldWithSharedOperators` is the canonical `CompileField`: the shared
|
||||
operator switch (`LogicalFamilyCondition`) over the merged or single value
|
||||
expression, with the default exists guard for positive operators. Traces uses
|
||||
it for every field; logs and metrics use it for families.
|
||||
|
||||
### The coerced column renderer — `pkg/querybuilder/column.go`
|
||||
|
||||
`RenderCoercedColumn` renders resolved fields for group-by, order, and
|
||||
aggregation arguments: every field exists-guarded and coerced in one
|
||||
`multiIf`, with the NULL group preserved. Traces and logs delegate their
|
||||
coerced modes to it through three leaf questions (`ColumnSchema`):
|
||||
|
||||
- `RawRead` — the uncoerced read of one field (logs overrides the legacy
|
||||
body path),
|
||||
- `Uncoerced` — the coercion exemption (traces time columns, logs legacy
|
||||
body reads),
|
||||
- `BareCandidate` — the fields that cannot sit inside `multiIf` (arrays).
|
||||
|
||||
## What stayed per-signal, and why
|
||||
|
||||
- **The single-key operator switches.** Metrics coerces collisions with its
|
||||
own casts (`toFloat64OrNull`, labels-as-String), logs carries the body
|
||||
machinery and the body-column index forms, audit and rule state history
|
||||
have their own switches. Unifying them changes SQL, so byte-parity forbids
|
||||
it here. This is the real distance to "one operator switch": it exists for
|
||||
families today, and extending it to singles is a re-pinning exercise, not a
|
||||
refactor.
|
||||
- **The resource-filter compile.** Index hints are woven into every operator
|
||||
case with per-operator polarity rules; the whole field compile stays its
|
||||
own (`CompileField` overrides wholesale).
|
||||
- **The raw-select tails.** Traces and logs raw-select shapes differ in more
|
||||
dimensions than they share (guard tests, stringification, collision
|
||||
application), so `ColumnExpressionFor`'s Unspecified mode keeps its
|
||||
per-signal tails. A generic raw renderer would need more knobs than it
|
||||
removes lines.
|
||||
- **The column resolution orders.** Which candidates a column stage sees —
|
||||
the storage probe, the metadata lookups, the swap/append of resolved
|
||||
spellings — keeps its pinned per-signal order in the mappers. Moving it
|
||||
into a generic `Resolve` is the model-B step proper, and it needs the
|
||||
stage-aware synthesis asymmetry (filters synthesize by operand, columns
|
||||
do not) carried as data.
|
||||
- **Metadata.** Its merged-value semantics bind query parameters inside
|
||||
presence guards, which the arg-free `ExistsFor` contract cannot express.
|
||||
Untouched, as declared.
|
||||
|
||||
## The review round (applied)
|
||||
|
||||
- The rule-state-history operator forms are pinned in the commit BEFORE the
|
||||
port (it had no tests and diverges most: IN binds the whole list to one
|
||||
placeholder, exists renders two ways), so the port proves parity.
|
||||
- Audit refuses families in its delegate, before the resource drop, so the
|
||||
wiring tripwire stays loud instead of dropping a resource-context family
|
||||
silently.
|
||||
- The logs coerced-column schema value carries the body mode; the flag reads
|
||||
one time per call. RawRead's dummy-value parameter is a legacy-body shim
|
||||
and leaves with the legacy body.
|
||||
- The compile context is named `CompileScope`: "scope" alone collides with
|
||||
the span search scope, the vocabulary member scope, and the resolution
|
||||
scope.
|
||||
|
||||
## Findings a reviewer should weigh
|
||||
|
||||
1. The condition-side consolidation is real and cheap: six flows became one,
|
||||
with per-signal surface of exactly three methods each, and every golden
|
||||
stayed byte-identical without adjustment.
|
||||
2. The column side splits: coerced modes unify on three knobs; raw select
|
||||
does not pay for unification at today's shapes.
|
||||
3. The full tree model (candidates as data, one `Resolve`, generic raw
|
||||
rendering) requires shape reconciliation that byte-parity forbids —
|
||||
confirming it should ride a forcing feature (the ValueMap reader) with a
|
||||
re-pinning round, not a standalone refactor.
|
||||
@@ -22,6 +22,8 @@ func newConditionBuilder(fm qbtypes.FieldMapper) qbtypes.ConditionBuilder {
|
||||
}
|
||||
|
||||
// Rule state history has no resource sub-query, so options are unused.
|
||||
// ConditionFor rejects the logs-only function operators and hands the term to
|
||||
// the generic flow; rule state history fields have no family support.
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -29,43 +31,37 @@ func (c *conditionBuilder) ConditionFor(
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
logicalFields []*telemetrytypes.LogicalField,
|
||||
_ map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
_ qbtypes.ConditionBuilderOptions,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for rule state history.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.CompileTerm(ctx, scope, c, querybuilder.SkipResourceNone, key, logicalFields, fieldKeys, options, operator, value, sb)
|
||||
}
|
||||
|
||||
// Rule state history fields have no family support, so every logical field
|
||||
// is single-member and flattens losslessly to its physical key; SingleKeys
|
||||
// refuses a family, so a wiring mistake fails loudly.
|
||||
resolved, warning := querybuilder.ResolveLogicalFields(key, logicalFields)
|
||||
keys, err := querybuilder.SingleKeys(resolved)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
// AmendEvidence: rule state history folds no intrinsic storage into the evidence.
|
||||
func (c *conditionBuilder) AmendEvidence(_ context.Context, _ querybuilder.CompileScope, _ *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField {
|
||||
return fields
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
// Synthesize: rule state history synthesizes nothing — an unknown key is an error.
|
||||
func (c *conditionBuilder) Synthesize(_ context.Context, _ querybuilder.CompileScope, key *telemetrytypes.TelemetryFieldKey, _ qbtypes.FilterOperator, _ any, _ map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error) {
|
||||
return nil, nil, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
// CompileField: single keys only — a family is a wiring error for this signal.
|
||||
func (c *conditionBuilder) CompileField(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error) {
|
||||
if logical.IsFamily() {
|
||||
return "", nil, errors.NewInternalf(errors.CodeInternal, "field %q resolved to a family, and this signal compiles single keys only", logical.Name)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
cond, err := c.conditionForKey(ctx, scope.OrgID, scope.StartNs, scope.EndNs, logical.Single(), operator, value, sb)
|
||||
return cond, nil, err
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package implrulestatehistory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
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"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The pins below fix the exact operator forms of this builder, including the
|
||||
// two shapes that exist nowhere else: IN binds the whole list to one
|
||||
// placeholder (sb.In without spreading), and the exists predicate renders
|
||||
// "true" for an intrinsic column but a JSONHas membership check for a label.
|
||||
func TestConditionForPinsOperatorForms(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fm := newFieldMapper()
|
||||
cb := newConditionBuilder(fm)
|
||||
|
||||
intrinsic := &telemetrytypes.TelemetryFieldKey{Name: "state"}
|
||||
label := &telemetrytypes.TelemetryFieldKey{Name: "deployment"}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
key *telemetrytypes.TelemetryFieldKey
|
||||
operator qbtypes.FilterOperator
|
||||
value any
|
||||
expectedSQL string
|
||||
expectedArgs []any
|
||||
}{
|
||||
{name: "intrinsic equal", key: intrinsic, operator: qbtypes.FilterOperatorEqual, value: "firing", expectedSQL: "WHERE state = ?", expectedArgs: []any{"firing"}},
|
||||
{name: "intrinsic not equal", key: intrinsic, operator: qbtypes.FilterOperatorNotEqual, value: "firing", expectedSQL: "WHERE state <> ?", expectedArgs: []any{"firing"}},
|
||||
{name: "label equal", key: label, operator: qbtypes.FilterOperatorEqual, value: "production", expectedSQL: "WHERE JSONExtractString(labels, 'deployment') = ?", expectedArgs: []any{"production"}},
|
||||
{name: "greater than", key: &telemetrytypes.TelemetryFieldKey{Name: "unix_milli"}, operator: qbtypes.FilterOperatorGreaterThan, value: int64(123), expectedSQL: "WHERE unix_milli > ?", expectedArgs: []any{int64(123)}},
|
||||
{name: "like", key: intrinsic, operator: qbtypes.FilterOperatorLike, value: "fir", expectedSQL: "WHERE state LIKE ?", expectedArgs: []any{"fir"}},
|
||||
{name: "contains", key: intrinsic, operator: qbtypes.FilterOperatorContains, value: "fir", expectedSQL: "WHERE LOWER(state) LIKE LOWER(?)", expectedArgs: []any{"%fir%"}},
|
||||
{name: "regexp", key: intrinsic, operator: qbtypes.FilterOperatorRegexp, value: "^f", expectedSQL: "WHERE match(state, ?)", expectedArgs: []any{"^f"}},
|
||||
{name: "between", key: &telemetrytypes.TelemetryFieldKey{Name: "unix_milli"}, operator: qbtypes.FilterOperatorBetween, value: []any{int64(1), int64(2)}, expectedSQL: "WHERE unix_milli BETWEEN ? AND ?", expectedArgs: []any{int64(1), int64(2)}},
|
||||
{name: "in binds the list to one placeholder", key: intrinsic, operator: qbtypes.FilterOperatorIn, value: []any{"firing", "inactive"}, expectedSQL: "WHERE state IN (?)", expectedArgs: []any{[]any{"firing", "inactive"}}},
|
||||
{name: "not in binds the list to one placeholder", key: intrinsic, operator: qbtypes.FilterOperatorNotIn, value: []any{"firing"}, expectedSQL: "WHERE state NOT IN (?)", expectedArgs: []any{[]any{"firing"}}},
|
||||
{name: "intrinsic exists is constant true", key: intrinsic, operator: qbtypes.FilterOperatorExists, value: nil, expectedSQL: "WHERE true", expectedArgs: nil},
|
||||
{name: "label exists is a membership check", key: label, operator: qbtypes.FilterOperatorExists, value: nil, expectedSQL: "WHERE JSONHas(labels, ?)", expectedArgs: []any{"deployment"}},
|
||||
{name: "label not exists negates the membership check", key: label, operator: qbtypes.FilterOperatorNotExists, value: nil, expectedSQL: "WHERE not JSONHas(labels, ?)", expectedArgs: []any{"deployment"}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{tc.key.Name: {tc.key}}
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := cb.ConditionFor(ctx, valuer.UUID{}, 0, 0, tc.key,
|
||||
querybuilder.MatchingLogicalFields(ctx, valuer.UUID{}, nil, telemetrytypes.SignalUnspecified, nil, tc.key, fieldKeys),
|
||||
fieldKeys, qbtypes.ConditionBuilderOptions{}, tc.operator, tc.value, sb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, conds, 1)
|
||||
sb.Where(conds...)
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
assert.Equal(t, tc.expectedSQL, sql)
|
||||
assert.Equal(t, tc.expectedArgs, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An unknown key is an error, and a function operator is rejected before
|
||||
// resolution.
|
||||
func TestConditionForRejections(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cb := newConditionBuilder(newFieldMapper())
|
||||
key := &telemetrytypes.TelemetryFieldKey{Name: "missing"}
|
||||
|
||||
_, _, err := cb.ConditionFor(ctx, valuer.UUID{}, 0, 0, key, nil, nil, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorEqual, "x", sqlbuilder.NewSelectBuilder())
|
||||
assert.ErrorContains(t, err, "not found")
|
||||
|
||||
_, _, err = cb.ConditionFor(ctx, valuer.UUID{}, 0, 0, key, nil, nil, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorHasToken, "x", sqlbuilder.NewSelectBuilder())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
74
pkg/querybuilder/column.go
Normal file
74
pkg/querybuilder/column.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// ColumnSchema is the per-signal surface of the generic coerced-column
|
||||
// renderer (group-by, order, and aggregation arguments). The three methods
|
||||
// are leaf questions about one resolved field; the composition — the guarded,
|
||||
// coerced multiIf with the NULL group — is RenderCoercedColumn and is written
|
||||
// once. Raw select keeps its per-signal shapes in the mappers: its tails
|
||||
// differ by storage in more ways than they share.
|
||||
type ColumnSchema interface {
|
||||
// RawRead returns the uncoerced value read of one field. The canonical
|
||||
// answer is LogicalValueExpr (the merged read for a family, the member's
|
||||
// own read otherwise); logs overrides it for the legacy body path.
|
||||
RawRead(ctx context.Context, scope CompileScope, logical *telemetrytypes.LogicalField, dummyValue any) (string, error)
|
||||
|
||||
// Uncoerced reports a field whose native type must survive the stage
|
||||
// coercion: a time column would collapse to seconds (traces), and a
|
||||
// legacy body read carries its own typing.
|
||||
Uncoerced(ctx context.Context, scope CompileScope, logical *telemetrytypes.LogicalField) (bool, error)
|
||||
|
||||
// BareCandidate reports a field that cannot sit inside Nullable/multiIf
|
||||
// and renders as its bare read when it is the only candidate (arrays).
|
||||
BareCandidate(logical *telemetrytypes.LogicalField) bool
|
||||
}
|
||||
|
||||
// RenderCoercedColumn renders resolved fields as one column expression for
|
||||
// the coerced stages: every field exists-guarded and coerced to the target
|
||||
// type in a single multiIf, so rows holding none of the candidates keep the
|
||||
// NULL group of a single key.
|
||||
func RenderCoercedColumn(
|
||||
ctx context.Context,
|
||||
scope CompileScope,
|
||||
schema ColumnSchema,
|
||||
fm qbtypes.FieldMapper,
|
||||
fields []*telemetrytypes.LogicalField,
|
||||
target telemetrytypes.FieldDataType,
|
||||
) (string, error) {
|
||||
if len(fields) == 1 && schema.BareCandidate(fields[0]) {
|
||||
return schema.RawRead(ctx, scope, fields[0], "")
|
||||
}
|
||||
|
||||
var dummyValue any = ""
|
||||
if target == telemetrytypes.FieldDataTypeFloat64 {
|
||||
dummyValue = 0.0
|
||||
}
|
||||
stmts := make([]string, 0, len(fields)*2)
|
||||
for _, logical := range fields {
|
||||
guard, err := LogicalExistsExpr(ctx, scope.OrgID, scope.StartNs, scope.EndNs, fm, logical, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
read, err := schema.RawRead(ctx, scope, logical, dummyValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
uncoerced, err := schema.Uncoerced(ctx, scope, logical)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !uncoerced {
|
||||
read, _ = DataTypeCollisionHandledFieldName(logical.Single(), dummyValue, read, qbtypes.FilterOperatorUnknown)
|
||||
}
|
||||
stmts = append(stmts, guard, read)
|
||||
}
|
||||
return fmt.Sprintf("multiIf(%s, NULL)", strings.Join(stmts, ", ")), nil
|
||||
}
|
||||
15
pkg/querybuilder/scope.go
Normal file
15
pkg/querybuilder/scope.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// CompileScope carries the compile-time context of one query as one value: the org
|
||||
// and the time range every physical read (evolution selection, probes) needs.
|
||||
// The generic term and column compilers thread it instead of loose
|
||||
// positional parameters, so a dropped axis is a compile error.
|
||||
type CompileScope struct {
|
||||
OrgID valuer.UUID
|
||||
StartNs uint64
|
||||
EndNs uint64
|
||||
}
|
||||
158
pkg/querybuilder/term.go
Normal file
158
pkg/querybuilder/term.go
Normal file
@@ -0,0 +1,158 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// TermSchema is the per-signal surface of the generic term compiler. The
|
||||
// methods answer what exists and how one resolved field compiles; the flow —
|
||||
// evidence, synthesis, the resource-filter policy, the per-field loop — is
|
||||
// CompileTerm and is written once. A signal implements exactly three
|
||||
// methods; term-level intercepts (search(), function rejects) stay in the
|
||||
// signal's ConditionFor delegate, in front of the flow.
|
||||
type TermSchema interface {
|
||||
// AmendEvidence folds intrinsic storage into non-empty resolved evidence
|
||||
// (traces prepends the span column for a bare name). Most signals return
|
||||
// the fields unchanged.
|
||||
AmendEvidence(ctx context.Context, scope CompileScope, key *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField
|
||||
|
||||
// Synthesize returns the logical fields for a name resolution found no
|
||||
// evidence for, with any warnings (not-found advisories). The error is
|
||||
// terminal (unknown key). A (nil, nil, nil) return skips the term: the
|
||||
// signal contributes no condition for the name (resource filter).
|
||||
Synthesize(ctx context.Context, scope CompileScope, key *telemetrytypes.TelemetryFieldKey, operator qbtypes.FilterOperator, value any, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error)
|
||||
|
||||
// CompileField compiles one resolved field into a condition. This is the
|
||||
// signal's storage residue — body JSON, index hints, its own operator
|
||||
// forms — and the exists-guard policy, which differs per storage.
|
||||
// CompileFieldWithSharedOperators is the canonical implementation for
|
||||
// map-backed fields.
|
||||
CompileField(ctx context.Context, scope CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error)
|
||||
}
|
||||
|
||||
// SkipResourcePolicy states how ConditionBuilderOptions.SkipResourceFilter
|
||||
// applies to a signal's resolved fields.
|
||||
type SkipResourcePolicy int
|
||||
|
||||
const (
|
||||
// SkipResourceNone: the option does not apply (metrics, rule state history).
|
||||
SkipResourceNone SkipResourcePolicy = iota
|
||||
// SkipResourceDrop: a resource sub-query covers resource fields, so drop
|
||||
// them from the evidence; when none remain the term is already covered.
|
||||
// Synthesized fields are exempt: the sub-query skips unknown keys.
|
||||
SkipResourceDrop
|
||||
// SkipResourceOnly: the signal stores resource attributes only, so keep
|
||||
// resource fields and omit everything else (the resource fingerprint
|
||||
// filter).
|
||||
SkipResourceOnly
|
||||
)
|
||||
|
||||
// CompileTerm is the generic flow of one filter term: resolve the evidence,
|
||||
// amend it, synthesize when it is empty, apply the resource-filter policy,
|
||||
// and compile every field through the signal's CompileField. Warnings keep
|
||||
// their order: ambiguity first, synthesis advisories second, per-field
|
||||
// advisories last.
|
||||
func CompileTerm(
|
||||
ctx context.Context,
|
||||
scope CompileScope,
|
||||
schema TermSchema,
|
||||
policy SkipResourcePolicy,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
evidence []*telemetrytypes.LogicalField,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
fields, warning := ResolveLogicalFields(key, evidence)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
fields = schema.AmendEvidence(ctx, scope, key, fields)
|
||||
|
||||
synthesized := false
|
||||
if len(fields) == 0 {
|
||||
synthesizedFields, synthWarnings, err := schema.Synthesize(ctx, scope, key, operator, value, fieldKeys)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
warnings = append(warnings, synthWarnings...)
|
||||
if len(synthesizedFields) == 0 {
|
||||
return nil, warnings, nil
|
||||
}
|
||||
fields = synthesizedFields
|
||||
synthesized = true
|
||||
}
|
||||
|
||||
switch policy {
|
||||
case SkipResourceDrop:
|
||||
if options.SkipResourceFilter && !synthesized {
|
||||
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, warnings, nil
|
||||
}
|
||||
fields = filtered
|
||||
}
|
||||
case SkipResourceOnly:
|
||||
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))
|
||||
for _, logical := range fields {
|
||||
cond, fieldWarnings, err := schema.CompileField(ctx, scope, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
warnings = append(warnings, fieldWarnings...)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
// CompileFieldWithSharedOperators is the canonical CompileField for
|
||||
// map-backed fields: the shared operator switch over the merged (family) or
|
||||
// single value expression, with the default exists guard AND-ed for positive
|
||||
// operators when guard is true. The keyless-row contract lives here: the
|
||||
// guard keeps an empty-string equality from matching rows without the key,
|
||||
// and its absence on negative operators keeps them set-complement over all
|
||||
// rows.
|
||||
func CompileFieldWithSharedOperators(
|
||||
ctx context.Context,
|
||||
scope CompileScope,
|
||||
fm qbtypes.FieldMapper,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
guard bool,
|
||||
) (string, error) {
|
||||
condition, err := LogicalFamilyCondition(ctx, scope.OrgID, scope.StartNs, scope.EndNs, fm, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if guard && operator.AddDefaultExistsFilter() {
|
||||
existsCondition, err := LogicalFamilyCondition(ctx, scope.OrgID, scope.StartNs, scope.EndNs, fm, logical, qbtypes.FilterOperatorExists, nil, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sb.And(condition, existsCondition), nil
|
||||
}
|
||||
return condition, nil
|
||||
}
|
||||
@@ -108,7 +108,10 @@ func memberPresenceCondition(sb *sqlbuilder.SelectBuilder, column string, member
|
||||
return sb.And(conditions...)
|
||||
}
|
||||
|
||||
// SkipResourceFilter is not applicable here: the fingerprint table only stores resource attributes.
|
||||
// ConditionFor skips the logs-body function operators (they never apply to
|
||||
// the resource fingerprint table; the main query still evaluates them) and
|
||||
// hands the term to the generic flow. SkipResourceFilter the option is not
|
||||
// applicable here: this builder IS the resource sub-query.
|
||||
func (b *defaultConditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -116,41 +119,35 @@ func (b *defaultConditionBuilder) ConditionFor(
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
logicalFields []*telemetrytypes.LogicalField,
|
||||
_ map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
_ qbtypes.ConditionBuilderOptions,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
op qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
matches := logicalFields
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only functions; they never apply to the
|
||||
// resource fingerprint table, so skip them (the main query still evaluates them).
|
||||
if op.IsFunctionOperator() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.CompileTerm(ctx, scope, b, querybuilder.SkipResourceOnly, key, logicalFields, fieldKeys, options, op, value, sb)
|
||||
}
|
||||
|
||||
logicalFields, warning := querybuilder.ResolveLogicalFields(key, matches)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
// AmendEvidence: the fingerprint table folds no intrinsic storage into the evidence.
|
||||
func (b *defaultConditionBuilder) AmendEvidence(_ context.Context, _ querybuilder.CompileScope, _ *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField {
|
||||
return fields
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(logicalFields))
|
||||
for _, logical := range logicalFields {
|
||||
// the resource fingerprint table only stores resource attributes; fields from
|
||||
// any other context contribute no condition and are omitted. An empty result
|
||||
// (including an unknown key) lets the caller skip this filter entirely.
|
||||
if logical.FieldContext != telemetrytypes.FieldContextResource {
|
||||
continue
|
||||
}
|
||||
cond, err := b.conditionForLogicalField(ctx, orgID, startNs, endNs, logical, op, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
// Synthesize: an unknown key contributes no condition — the caller skips this
|
||||
// filter entirely, and the main query still evaluates the term.
|
||||
func (b *defaultConditionBuilder) Synthesize(_ context.Context, _ querybuilder.CompileScope, _ *telemetrytypes.TelemetryFieldKey, _ qbtypes.FilterOperator, _ any, _ map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// CompileField: the fingerprint operator forms with their bloom-index hints,
|
||||
// for families and singles alike.
|
||||
func (b *defaultConditionBuilder) CompileField(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, op qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error) {
|
||||
cond, err := b.conditionForLogicalField(ctx, scope.OrgID, scope.StartNs, scope.EndNs, logical, op, value, sb)
|
||||
return cond, nil, err
|
||||
}
|
||||
|
||||
func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
|
||||
@@ -129,6 +129,8 @@ func (c *conditionBuilder) conditionFor(
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator)
|
||||
}
|
||||
|
||||
// ConditionFor rejects the logs-only function operators and hands the term to
|
||||
// the generic flow; audit fields have no family support.
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -136,57 +138,46 @@ func (c *conditionBuilder) ConditionFor(
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
logicalFields []*telemetrytypes.LogicalField,
|
||||
_ map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for audit.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Audit fields have no family support, so every logical field is
|
||||
// single-member and flattens losslessly to its physical key; SingleKeys
|
||||
// refuses a family, so a wiring mistake fails loudly.
|
||||
resolved, warning := querybuilder.ResolveLogicalFields(key, logicalFields)
|
||||
keys, err := querybuilder.SingleKeys(resolved)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
// Drop resource keys the sub-query already covers; if none remain, skip the term (not an error).
|
||||
if options.SkipResourceFilter {
|
||||
filtered := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if k.FieldContext != telemetrytypes.FieldContextResource {
|
||||
filtered = append(filtered, k)
|
||||
}
|
||||
// The family refusal runs before the generic flow (and its resource
|
||||
// drop), so a mis-wired family fails loudly instead of dropping silently.
|
||||
for _, logical := range logicalFields {
|
||||
if logical.IsFamily() {
|
||||
return nil, nil, errors.NewInternalf(errors.CodeInternal, "field %q resolved to a family, and this signal compiles single keys only", logical.Name)
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, warnings, nil
|
||||
}
|
||||
keys = filtered
|
||||
}
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.CompileTerm(ctx, scope, c, querybuilder.SkipResourceDrop, key, logicalFields, fieldKeys, options, operator, value, sb)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
// AmendEvidence: audit folds no intrinsic storage into the evidence.
|
||||
func (c *conditionBuilder) AmendEvidence(_ context.Context, _ querybuilder.CompileScope, _ *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField {
|
||||
return fields
|
||||
}
|
||||
|
||||
// Synthesize: audit synthesizes nothing — an unknown key is an error.
|
||||
func (c *conditionBuilder) Synthesize(_ context.Context, _ querybuilder.CompileScope, key *telemetrytypes.TelemetryFieldKey, _ qbtypes.FilterOperator, _ any, _ map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error) {
|
||||
return nil, nil, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
// CompileField: audit compiles single keys only — a family is a wiring error,
|
||||
// because this signal has no family compiler and flattening would silently
|
||||
// read one spelling.
|
||||
func (c *conditionBuilder) CompileField(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error) {
|
||||
if logical.IsFamily() {
|
||||
return "", nil, errors.NewInternalf(errors.CodeInternal, "field %q resolved to a family, and this signal compiles single keys only", logical.Name)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
cond, err := c.conditionForKey(ctx, scope.OrgID, scope.StartNs, scope.EndNs, logical.Single(), operator, value, sb)
|
||||
return cond, nil, err
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
|
||||
@@ -448,6 +448,9 @@ func candidateLookupKeys(key *telemetrytypes.TelemetryFieldKey, fieldKeys map[st
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConditionFor handles search() (which resolves its own scope) in front and
|
||||
// hands the term to the generic flow; the logs-specific behavior lives in the
|
||||
// TermSchema methods below.
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -461,104 +464,79 @@ func (c *conditionBuilder) ConditionFor(
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
matches := logicalFields
|
||||
skipResourceFilter := options.SkipResourceFilter
|
||||
|
||||
// search() resolves its own (optional) scope; handle it before key resolution.
|
||||
if operator == qbtypes.FilterOperatorSearch {
|
||||
return c.conditionForSearch(ctx, orgID, key, value, sb)
|
||||
}
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.CompileTerm(ctx, scope, c, querybuilder.SkipResourceDrop, key, logicalFields, fieldKeys, options, operator, value, sb)
|
||||
}
|
||||
|
||||
logicalFields, warning := querybuilder.ResolveLogicalFields(key, matches)
|
||||
// AmendEvidence: logs fold no intrinsic storage into the evidence.
|
||||
func (c *conditionBuilder) AmendEvidence(_ context.Context, _ querybuilder.CompileScope, _ *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField {
|
||||
return fields
|
||||
}
|
||||
|
||||
// Synthesize: intrinsic log columns pass through as themselves; everything
|
||||
// else goes to CandidateKeys — fold-contexts get the metadata map so a
|
||||
// same-named key under another context wins before the prefix folds into the
|
||||
// key name (matching ColumnExpressionFor), strict contexts pass nil and stay
|
||||
// honored as-is. The body-JSON functions keep body candidates only.
|
||||
func (c *conditionBuilder) Synthesize(ctx context.Context, scope querybuilder.CompileScope, key *telemetrytypes.TelemetryFieldKey, operator qbtypes.FilterOperator, value any, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error) {
|
||||
_, isIntrinsicColumn := logsV2Columns[key.Name]
|
||||
var keys []*telemetrytypes.TelemetryFieldKey
|
||||
switch {
|
||||
case key.FieldContext == telemetrytypes.FieldContextBody && key.Name == "":
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "missing key for body json search - expected key of the form `body.key` (ex: `body.status`)")
|
||||
case key.FieldContext == telemetrytypes.FieldContextLog && isIntrinsicColumn:
|
||||
return querybuilder.WrapAsLogicalFields(key.Name, []*telemetrytypes.TelemetryFieldKey{key}), nil, nil
|
||||
default:
|
||||
keys = c.fm.CandidateKeys(ctx, scope.OrgID, key, value, candidateLookupKeys(key, fieldKeys))
|
||||
if operator.IsFunctionOperator() {
|
||||
if key.FieldContext != telemetrytypes.FieldContextBody {
|
||||
// has/hasAny/hasAll/hasToken are body-JSON only
|
||||
return nil, nil, querybuilder.NewFunctionUnsupportedError(operator)
|
||||
}
|
||||
bodyKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if k.FieldContext == telemetrytypes.FieldContextBody {
|
||||
bodyKeys = append(bodyKeys, k)
|
||||
}
|
||||
}
|
||||
keys = bodyKeys
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, nil, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
return querybuilder.WrapAsLogicalFields(key.Name, keys), []string{querybuilder.NewKeyNotFoundWarning(key.Name)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CompileField: a family compiles through the shared operator switch with the
|
||||
// same default-exists guard as the single-key path — without it, an
|
||||
// empty-string equality (and LIKE '%', or an empty CONTAINS) would match
|
||||
// keyless rows. A single member keeps
|
||||
// the logs storage residue: body JSON, full-text, and the context-based guard
|
||||
// policy of conditionForKey.
|
||||
func (c *conditionBuilder) CompileField(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error) {
|
||||
if logical.IsFamily() {
|
||||
// has/hasAny/hasAll/hasToken are body-JSON only; family members are
|
||||
// attribute and resource keys, so keep the descriptive error.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
cond, err := querybuilder.CompileFieldWithSharedOperators(ctx, scope, c.fm, logical, operator, value, sb, true)
|
||||
return cond, nil, err
|
||||
}
|
||||
k := logical.Single()
|
||||
cond, err := c.conditionForKey(ctx, scope.OrgID, scope.StartNs, scope.EndNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
if w := c.bodyFullTextDefaultWarning(ctx, scope.OrgID, scope.StartNs, scope.EndNs, k, operator); w != "" {
|
||||
warnings = append(warnings, w)
|
||||
}
|
||||
|
||||
synthesized := false
|
||||
if len(logicalFields) == 0 {
|
||||
_, isIntrinsicColumn := logsV2Columns[key.Name]
|
||||
var keys []*telemetrytypes.TelemetryFieldKey
|
||||
switch {
|
||||
case key.FieldContext == telemetrytypes.FieldContextBody && key.Name == "":
|
||||
return nil, warnings, errors.NewInvalidInputf(errors.CodeInvalidInput, "missing key for body json search - expected key of the form `body.key` (ex: `body.status`)")
|
||||
case key.FieldContext == telemetrytypes.FieldContextLog && isIntrinsicColumn:
|
||||
keys = []*telemetrytypes.TelemetryFieldKey{key}
|
||||
default:
|
||||
// Fold-contexts get the metadata map so a same-named key under another context
|
||||
// wins before the prefix folds into the key name (matching ColumnExpressionFor);
|
||||
// strict contexts pass nil and stay honored as-is.
|
||||
keys = c.fm.CandidateKeys(ctx, orgID, key, value, candidateLookupKeys(key, fieldKeys))
|
||||
if operator.IsFunctionOperator() {
|
||||
if key.FieldContext != telemetrytypes.FieldContextBody {
|
||||
// has/hasAny/hasAll/hasToken are body-JSON only
|
||||
return nil, warnings, querybuilder.NewFunctionUnsupportedError(operator)
|
||||
}
|
||||
bodyKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if k.FieldContext == telemetrytypes.FieldContextBody {
|
||||
bodyKeys = append(bodyKeys, k)
|
||||
}
|
||||
}
|
||||
keys = bodyKeys
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
synthesized = true
|
||||
warnings = append(warnings, querybuilder.NewKeyNotFoundWarning(key.Name))
|
||||
}
|
||||
logicalFields = querybuilder.WrapAsLogicalFields(key.Name, keys)
|
||||
}
|
||||
|
||||
if skipResourceFilter && !synthesized {
|
||||
filtered := make([]*telemetrytypes.LogicalField, 0, len(logicalFields))
|
||||
for _, logical := range logicalFields {
|
||||
if logical.FieldContext != telemetrytypes.FieldContextResource {
|
||||
filtered = append(filtered, logical)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, warnings, nil
|
||||
}
|
||||
logicalFields = filtered
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(logicalFields))
|
||||
for _, logical := range logicalFields {
|
||||
if logical.IsFamily() {
|
||||
// has/hasAny/hasAll/hasToken are body-JSON only; family members are
|
||||
// attribute and resource keys, so keep the descriptive error.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cond, err := querybuilder.LogicalFamilyCondition(ctx, orgID, startNs, endNs, c.fm, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// The same default-exists guard as the single-key path: without it,
|
||||
// `key = ''` (and LIKE '%', CONTAINS '') would match keyless rows.
|
||||
if operator.AddDefaultExistsFilter() {
|
||||
existsCond, err := querybuilder.LogicalFamilyCondition(ctx, orgID, startNs, endNs, c.fm, logical, qbtypes.FilterOperatorExists, nil, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cond = sb.And(cond, existsCond)
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
continue
|
||||
}
|
||||
k := logical.Single()
|
||||
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
if w := c.bodyFullTextDefaultWarning(ctx, orgID, startNs, endNs, k, operator); w != "" {
|
||||
warnings = append(warnings, w)
|
||||
}
|
||||
}
|
||||
return conds, warnings, nil
|
||||
return cond, warnings, nil
|
||||
}
|
||||
|
||||
// bodyFullTextDefaultWarning returns the advisory shown when a regexp full-text
|
||||
|
||||
@@ -292,39 +292,14 @@ func (m *fieldMapper) ColumnExpressionFor(
|
||||
}
|
||||
}
|
||||
|
||||
// Group-by/order (String) and aggregation (String/Float64): every candidate is
|
||||
// exists-guarded and coerced to requiredDataType, in a single multiIf. Raw select
|
||||
// (Unspecified) keeps the lighter native shape below.
|
||||
// Group-by/order (String) and aggregation (String/Float64) render through
|
||||
// the generic coerced column; raw select (Unspecified) keeps the lighter
|
||||
// native shape below. The schema value carries the body mode, so the flag
|
||||
// reads one time per call.
|
||||
if requiredDataType != telemetrytypes.FieldDataTypeUnspecified {
|
||||
// arrays cannot sit inside Nullable/multiIf, so a lone array candidate stays bare
|
||||
if len(candidates) == 1 && (strings.Contains(candidates[0].Name, telemetrytypes.ArraySep) ||
|
||||
strings.Contains(candidates[0].Name, telemetrytypes.ArrayAnyIndex) ||
|
||||
candidates[0].FieldDataType.IsArray()) {
|
||||
return m.FieldFor(ctx, orgID, tsStart, tsEnd, candidates[0])
|
||||
}
|
||||
var dummyValue any = ""
|
||||
if requiredDataType == telemetrytypes.FieldDataTypeFloat64 {
|
||||
dummyValue = 0.0
|
||||
}
|
||||
var stmts []string
|
||||
for _, key := range candidates {
|
||||
guard, err := m.ExistsFor(ctx, orgID, tsStart, tsEnd, key, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var fieldExpression string
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody && !bodyJSONEnabled {
|
||||
fieldExpression, _ = GetBodyJSONKey(ctx, key, qbtypes.FilterOperatorUnknown, dummyValue)
|
||||
} else {
|
||||
fieldExpression, err = m.FieldFor(ctx, orgID, tsStart, tsEnd, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fieldExpression, _ = querybuilder.DataTypeCollisionHandledFieldName(key, dummyValue, fieldExpression, qbtypes.FilterOperatorUnknown)
|
||||
}
|
||||
stmts = append(stmts, guard, fieldExpression)
|
||||
}
|
||||
return fmt.Sprintf("multiIf(%s, NULL)", strings.Join(stmts, ", ")), nil
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: tsStart, EndNs: tsEnd}
|
||||
schema := logsColumnSchema{fm: m, bodyJSONEnabled: bodyJSONEnabled}
|
||||
return querybuilder.RenderCoercedColumn(ctx, scope, schema, m, querybuilder.WrapAsLogicalFields(field.Name, candidates), requiredDataType)
|
||||
}
|
||||
|
||||
if len(candidates) == 1 {
|
||||
@@ -377,6 +352,42 @@ func (m *fieldMapper) ColumnExpressionFor(
|
||||
return fmt.Sprintf("multiIf(%s, NULL)", strings.Join(stmts, ", ")), nil
|
||||
}
|
||||
|
||||
// logsColumnSchema answers the generic coerced-column questions with the
|
||||
// body mode fixed for one call.
|
||||
type logsColumnSchema struct {
|
||||
fm *fieldMapper
|
||||
bodyJSONEnabled bool
|
||||
}
|
||||
|
||||
// RawRead returns the uncoerced value read of one field; the legacy
|
||||
// string-body path keeps its own typed read. An array candidate reads bare
|
||||
// through its own field expression on both body modes. dummyValue only feeds
|
||||
// the legacy body read and leaves with it.
|
||||
func (s logsColumnSchema) RawRead(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, dummyValue any) (string, error) {
|
||||
key := logical.Single()
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody && !s.BareCandidate(logical) && !s.bodyJSONEnabled {
|
||||
fieldExpression, _ := GetBodyJSONKey(ctx, key, qbtypes.FilterOperatorUnknown, dummyValue)
|
||||
return fieldExpression, nil
|
||||
}
|
||||
return querybuilder.LogicalValueExpr(ctx, scope.OrgID, scope.StartNs, scope.EndNs, s.fm, logical)
|
||||
}
|
||||
|
||||
// Uncoerced: the legacy body read carries its own typing, so the stage
|
||||
// coercion does not apply to it.
|
||||
func (s logsColumnSchema) Uncoerced(_ context.Context, _ querybuilder.CompileScope, logical *telemetrytypes.LogicalField) (bool, error) {
|
||||
key := logical.Single()
|
||||
return key.FieldContext == telemetrytypes.FieldContextBody && !s.bodyJSONEnabled, nil
|
||||
}
|
||||
|
||||
// BareCandidate: arrays cannot sit inside Nullable/multiIf, so a lone array
|
||||
// candidate stays bare.
|
||||
func (s logsColumnSchema) BareCandidate(logical *telemetrytypes.LogicalField) bool {
|
||||
key := logical.Single()
|
||||
return strings.Contains(key.Name, telemetrytypes.ArraySep) ||
|
||||
strings.Contains(key.Name, telemetrytypes.ArrayAnyIndex) ||
|
||||
key.FieldDataType.IsArray()
|
||||
}
|
||||
|
||||
func (m *fieldMapper) CandidateKeys(_ context.Context, _ valuer.UUID, field *telemetrytypes.TelemetryFieldKey, value any, keys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
|
||||
if matches := keys[field.Name]; len(matches) > 0 {
|
||||
return matches
|
||||
|
||||
@@ -170,7 +170,8 @@ func (c *conditionBuilder) conditionFor(
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator)
|
||||
}
|
||||
|
||||
// Metrics has no resource sub-query, so options are unused.
|
||||
// ConditionFor rejects the logs-only function operators and hands the term to
|
||||
// the generic flow; metrics has no resource sub-query, so options are unused.
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -179,52 +180,55 @@ func (c *conditionBuilder) ConditionFor(
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
logicalFields []*telemetrytypes.LogicalField,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
_ qbtypes.ConditionBuilderOptions,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for metrics.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var warnings []string
|
||||
if len(logicalFields) == 0 {
|
||||
var keys []*telemetrytypes.TelemetryFieldKey
|
||||
if _, isColumn := timeSeriesV4Columns[key.Name]; isColumn {
|
||||
keys = []*telemetrytypes.TelemetryFieldKey{key}
|
||||
} else {
|
||||
if len(fieldKeys[key.Name]) == 0 {
|
||||
warnings = append(warnings, fmt.Sprintf("label `%s` not found in metadata; check the label name for typos", key.Name))
|
||||
}
|
||||
keys = []*telemetrytypes.TelemetryFieldKey{
|
||||
telemetrytypes.NewTelemetryFieldKey(key.Name, telemetrytypes.FieldContextAttribute, key.FieldDataType),
|
||||
}
|
||||
if key.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
keys = append(keys, telemetrytypes.NewTelemetryFieldKey(
|
||||
key.FieldContext.StringValue()+"."+key.Name, telemetrytypes.FieldContextAttribute, key.FieldDataType))
|
||||
}
|
||||
}
|
||||
logicalFields = querybuilder.WrapAsLogicalFields(key.Name, keys)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(logicalFields))
|
||||
for _, logical := range logicalFields {
|
||||
if logical.IsFamily() {
|
||||
cond, err := querybuilder.LogicalFamilyCondition(ctx, orgID, startNs, endNs, c.fm, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
continue
|
||||
}
|
||||
cond, err := c.conditionFor(ctx, orgID, startNs, endNs, logical.Single(), operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.CompileTerm(ctx, scope, c, querybuilder.SkipResourceNone, key, logicalFields, fieldKeys, options, operator, value, sb)
|
||||
}
|
||||
|
||||
// AmendEvidence: metrics fold no intrinsic storage into the evidence.
|
||||
func (c *conditionBuilder) AmendEvidence(_ context.Context, _ querybuilder.CompileScope, _ *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField {
|
||||
return fields
|
||||
}
|
||||
|
||||
// Synthesize: an intrinsic time-series column passes through as itself; any
|
||||
// other name reads as a label (with the context-prefixed spelling second),
|
||||
// with an advisory when the metadata has never seen it. Labels always read
|
||||
// from the JSON, so an unknown name still queries instead of erroring.
|
||||
func (c *conditionBuilder) Synthesize(_ context.Context, _ querybuilder.CompileScope, key *telemetrytypes.TelemetryFieldKey, _ qbtypes.FilterOperator, _ any, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error) {
|
||||
if _, isColumn := timeSeriesV4Columns[key.Name]; isColumn {
|
||||
return querybuilder.WrapAsLogicalFields(key.Name, []*telemetrytypes.TelemetryFieldKey{key}), nil, nil
|
||||
}
|
||||
var warnings []string
|
||||
if len(fieldKeys[key.Name]) == 0 {
|
||||
warnings = append(warnings, fmt.Sprintf("label `%s` not found in metadata; check the label name for typos", key.Name))
|
||||
}
|
||||
keys := []*telemetrytypes.TelemetryFieldKey{
|
||||
telemetrytypes.NewTelemetryFieldKey(key.Name, telemetrytypes.FieldContextAttribute, key.FieldDataType),
|
||||
}
|
||||
if key.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
keys = append(keys, telemetrytypes.NewTelemetryFieldKey(
|
||||
key.FieldContext.StringValue()+"."+key.Name, telemetrytypes.FieldContextAttribute, key.FieldDataType))
|
||||
}
|
||||
return querybuilder.WrapAsLogicalFields(key.Name, keys), warnings, nil
|
||||
}
|
||||
|
||||
// CompileField: a family compiles through the shared operator switch; a
|
||||
// single label keeps the metrics operator forms (labels read back as String
|
||||
// from the labels JSON, so the collision handling differs) and takes no
|
||||
// exists guard.
|
||||
func (c *conditionBuilder) CompileField(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error) {
|
||||
if logical.IsFamily() {
|
||||
cond, err := querybuilder.CompileFieldWithSharedOperators(ctx, scope, c.fm, logical, operator, value, sb, false)
|
||||
return cond, nil, err
|
||||
}
|
||||
cond, err := c.conditionFor(ctx, scope.OrgID, scope.StartNs, scope.EndNs, logical.Single(), operator, value, sb)
|
||||
return cond, nil, err
|
||||
}
|
||||
|
||||
@@ -22,32 +22,12 @@ type conditionBuilder struct {
|
||||
}
|
||||
|
||||
var _ qbtypes.ConditionBuilder = (*conditionBuilder)(nil)
|
||||
var _ querybuilder.TermSchema = (*conditionBuilder)(nil)
|
||||
|
||||
func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
|
||||
return &conditionBuilder{fm: fm}
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
// TODO(srikanthccv): maybe extend this to every possible attribute
|
||||
if logical.Name == "duration_nano" || logical.Name == "durationNano" { // QoL improvement
|
||||
coerced, err := coerceDurationValue(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
value = coerced
|
||||
}
|
||||
return querybuilder.LogicalFamilyCondition(ctx, orgID, startNs, endNs, c.fm, logical, operator, value, sb)
|
||||
}
|
||||
|
||||
func coerceDurationValue(value any) (any, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
@@ -98,9 +78,9 @@ func candidateLookupKeys(key *telemetrytypes.TelemetryFieldKey, fieldKeys map[st
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConditionFor resolves the referenced key to the key(s) to filter on (ResolveKeys, else
|
||||
// synthesized keys with a warning) and builds one condition per resolved key. fieldKeys is
|
||||
// the full metadata map; the builder owns key resolution.
|
||||
// ConditionFor rejects the logs-only function operators and hands the term to
|
||||
// the generic flow; the traces-specific behavior lives in the TermSchema
|
||||
// methods below.
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -114,121 +94,83 @@ func (c *conditionBuilder) ConditionFor(
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for traces.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
matches := logicalFields
|
||||
skipResourceFilter := options.SkipResourceFilter
|
||||
|
||||
logicalFields, warning := querybuilder.ResolveLogicalFields(key, matches)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
// A bare key that names a real column filters on the column too — first. When metadata
|
||||
// only knows the name under other contexts, prepend the column and keep metadata matches
|
||||
// only where their type is consistent with it (a corrupt entry can't degrade the column).
|
||||
if key.FieldContext == telemetrytypes.FieldContextUnspecified && len(logicalFields) > 0 {
|
||||
hasColumn := false
|
||||
for _, logical := range logicalFields {
|
||||
if logical.FieldContext == telemetrytypes.FieldContextSpan {
|
||||
hasColumn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasColumn {
|
||||
probe := telemetrytypes.NewTelemetryFieldKey(key.Name, telemetrytypes.FieldContextSpan, key.FieldDataType)
|
||||
if cols, colErr := c.fm.ColumnFor(ctx, orgID, startNs, endNs, probe); colErr == nil && len(cols) > 0 {
|
||||
combined := make([]*telemetrytypes.LogicalField, 0, len(logicalFields)+1)
|
||||
combined = append(combined, telemetrytypes.SingleLogicalField(key.Name, probe))
|
||||
for _, logical := range logicalFields {
|
||||
if columnMatchesDataType(cols[0], logical.FieldDataType) {
|
||||
combined = append(combined, logical)
|
||||
}
|
||||
}
|
||||
logicalFields = combined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synthesized := false
|
||||
if len(logicalFields) == 0 {
|
||||
// Not in metadata. CandidateKeys resolves it: fold contexts (span/trace) get the
|
||||
// metadata map so it can honor a real column, correct to a stripped-name metadata
|
||||
// match, or synthesize; strict contexts pass nil and keep their synthesize path.
|
||||
logicalFields = querybuilder.WrapAsLogicalFields(key.Name, c.fm.CandidateKeys(ctx, orgID, key, value, candidateLookupKeys(key, fieldKeys)))
|
||||
if len(logicalFields) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
synthesized = true
|
||||
warnings = append(warnings, querybuilder.NewKeyNotFoundWarning(key.Name))
|
||||
}
|
||||
|
||||
// When a resource sub-query already covers the term, drop resource fields from the main
|
||||
// query. Synthesized keys are exempt: the sub-query skips keys absent from metadata.
|
||||
if skipResourceFilter && !synthesized {
|
||||
filtered := make([]*telemetrytypes.LogicalField, 0, len(logicalFields))
|
||||
for _, logical := range logicalFields {
|
||||
if logical.FieldContext != telemetrytypes.FieldContextResource {
|
||||
filtered = append(filtered, logical)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, warnings, nil
|
||||
}
|
||||
logicalFields = filtered
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(logicalFields))
|
||||
for _, logical := range logicalFields {
|
||||
cond, err := c.conditionForLogicalField(ctx, orgID, startNs, endNs, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.CompileTerm(ctx, scope, c, querybuilder.SkipResourceDrop, key, logicalFields, fieldKeys, options, operator, value, sb)
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForLogicalField(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
// AmendEvidence: a bare key that names a real column filters on the column too — first.
|
||||
// When metadata only knows the name under other contexts, prepend the column and keep
|
||||
// metadata matches only where their type is consistent with it (a corrupt entry can't
|
||||
// degrade the column).
|
||||
func (c *conditionBuilder) AmendEvidence(ctx context.Context, scope querybuilder.CompileScope, key *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField {
|
||||
if key.FieldContext != telemetrytypes.FieldContextUnspecified || len(fields) == 0 {
|
||||
return fields
|
||||
}
|
||||
for _, logical := range fields {
|
||||
if logical.FieldContext == telemetrytypes.FieldContextSpan {
|
||||
return fields
|
||||
}
|
||||
}
|
||||
probe := telemetrytypes.NewTelemetryFieldKey(key.Name, telemetrytypes.FieldContextSpan, key.FieldDataType)
|
||||
cols, colErr := c.fm.ColumnFor(ctx, scope.OrgID, scope.StartNs, scope.EndNs, probe)
|
||||
if colErr != nil || len(cols) == 0 {
|
||||
return fields
|
||||
}
|
||||
combined := make([]*telemetrytypes.LogicalField, 0, len(fields)+1)
|
||||
combined = append(combined, telemetrytypes.SingleLogicalField(key.Name, probe))
|
||||
for _, logical := range fields {
|
||||
if columnMatchesDataType(cols[0], logical.FieldDataType) {
|
||||
combined = append(combined, logical)
|
||||
}
|
||||
}
|
||||
return combined
|
||||
}
|
||||
|
||||
// Synthesize: not in metadata. CandidateKeys resolves it: fold contexts (span/trace) get
|
||||
// the metadata map so it can honor a real column, correct to a stripped-name metadata
|
||||
// match, or synthesize; strict contexts pass nil and keep their synthesize path.
|
||||
func (c *conditionBuilder) Synthesize(ctx context.Context, scope querybuilder.CompileScope, key *telemetrytypes.TelemetryFieldKey, _ qbtypes.FilterOperator, value any, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error) {
|
||||
fields := querybuilder.WrapAsLogicalFields(key.Name, c.fm.CandidateKeys(ctx, scope.OrgID, key, value, candidateLookupKeys(key, fieldKeys)))
|
||||
if len(fields) == 0 {
|
||||
return nil, nil, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
return fields, []string{querybuilder.NewKeyNotFoundWarning(key.Name)}, nil
|
||||
}
|
||||
|
||||
// CompileField: span-scope names build their own predicates; everything else compiles
|
||||
// through the shared operator switch (with the duration coercion in front), and the
|
||||
// default exists guard applies except to intrinsic columns, which always exist.
|
||||
func (c *conditionBuilder) CompileField(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error) {
|
||||
if c.isSpanScopeField(logical.Name) {
|
||||
return c.buildSpanScopeCondition(logical.Single(), operator, value, startNs)
|
||||
cond, err := c.buildSpanScopeCondition(logical.Single(), operator, value, scope.StartNs)
|
||||
return cond, nil, err
|
||||
}
|
||||
|
||||
condition, err := c.conditionFor(ctx, orgID, startNs, endNs, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if operator.AddDefaultExistsFilter() {
|
||||
// skip adding exists filter for intrinsic fields
|
||||
field, _ := c.fm.FieldFor(ctx, orgID, startNs, endNs, logical.Single())
|
||||
if slices.Contains(maps.Keys(IntrinsicFields), field) ||
|
||||
slices.Contains(maps.Keys(IntrinsicFieldsDeprecated), field) ||
|
||||
slices.Contains(maps.Keys(CalculatedFields), field) ||
|
||||
slices.Contains(maps.Keys(CalculatedFieldsDeprecated), field) {
|
||||
return condition, nil
|
||||
}
|
||||
|
||||
existsCondition, err := c.conditionFor(ctx, orgID, startNs, endNs, logical, qbtypes.FilterOperatorExists, nil, sb)
|
||||
// TODO(srikanthccv): maybe extend this to every possible attribute
|
||||
if logical.Name == "duration_nano" || logical.Name == "durationNano" { // QoL improvement
|
||||
coerced, err := coerceDurationValue(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
return sb.And(condition, existsCondition), nil
|
||||
value = coerced
|
||||
}
|
||||
return condition, nil
|
||||
|
||||
cond, err := querybuilder.CompileFieldWithSharedOperators(ctx, scope, c.fm, logical, operator, value, sb, !c.isIntrinsic(ctx, scope, logical))
|
||||
return cond, nil, err
|
||||
}
|
||||
|
||||
// isIntrinsic reports whether the field reads an always-present table column,
|
||||
// which needs no exists guard.
|
||||
func (c *conditionBuilder) isIntrinsic(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField) bool {
|
||||
field, _ := c.fm.FieldFor(ctx, scope.OrgID, scope.StartNs, scope.EndNs, logical.Single())
|
||||
return slices.Contains(maps.Keys(IntrinsicFields), field) ||
|
||||
slices.Contains(maps.Keys(IntrinsicFieldsDeprecated), field) ||
|
||||
slices.Contains(maps.Keys(CalculatedFields), field) ||
|
||||
slices.Contains(maps.Keys(CalculatedFieldsDeprecated), field)
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) isSpanScopeField(name string) bool {
|
||||
|
||||
@@ -442,34 +442,12 @@ func (m *fieldMapper) ColumnExpressionFor(
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Group-by/order (String) and aggregation (String/Float64): every candidate is
|
||||
// exists-guarded and coerced to requiredDataType, in a single multiIf. Raw select
|
||||
// (Unspecified) keeps the lighter native shape below.
|
||||
// Group-by/order (String) and aggregation (String/Float64) render through
|
||||
// the generic coerced column; raw select (Unspecified) keeps the lighter
|
||||
// native shape below.
|
||||
if requiredDataType != telemetrytypes.FieldDataTypeUnspecified {
|
||||
var dummyValue any = ""
|
||||
if requiredDataType == telemetrytypes.FieldDataTypeFloat64 {
|
||||
dummyValue = 0.0
|
||||
}
|
||||
stmts := make([]string, 0, len(candidates)*2)
|
||||
for _, logical := range candidates {
|
||||
value, err := querybuilder.LogicalValueExpr(ctx, orgID, startNs, endNs, m, logical)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
guard, err := querybuilder.LogicalExistsExpr(ctx, orgID, startNs, endNs, m, logical, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
coerced := value
|
||||
// a time column keeps its native type; coercing it would yield seconds
|
||||
if temporal, err := m.logicalIsTemporal(ctx, startNs, endNs, logical); err != nil {
|
||||
return "", err
|
||||
} else if !temporal {
|
||||
coerced, _ = querybuilder.DataTypeCollisionHandledFieldName(logical.Single(), dummyValue, value, qbtypes.FilterOperatorUnknown)
|
||||
}
|
||||
stmts = append(stmts, guard, coerced)
|
||||
}
|
||||
return fmt.Sprintf("multiIf(%s, NULL)", strings.Join(stmts, ", ")), nil
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.RenderCoercedColumn(ctx, scope, m, m, candidates, requiredDataType)
|
||||
}
|
||||
|
||||
if len(candidates) == 1 {
|
||||
@@ -506,6 +484,23 @@ func (m *fieldMapper) ColumnExpressionFor(
|
||||
return fmt.Sprintf("multiIf(%s, NULL)", strings.Join(args, ", ")), nil
|
||||
}
|
||||
|
||||
// RawRead returns the uncoerced value read of one field for the generic
|
||||
// coerced column: the merged read for a family, the member's own read
|
||||
// otherwise.
|
||||
func (m *fieldMapper) RawRead(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, _ any) (string, error) {
|
||||
return querybuilder.LogicalValueExpr(ctx, scope.OrgID, scope.StartNs, scope.EndNs, m, logical)
|
||||
}
|
||||
|
||||
// Uncoerced: a time column keeps its native type; coercing it would yield seconds.
|
||||
func (m *fieldMapper) Uncoerced(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField) (bool, error) {
|
||||
return m.logicalIsTemporal(ctx, scope.StartNs, scope.EndNs, logical)
|
||||
}
|
||||
|
||||
// BareCandidate: traces has no array-typed candidates.
|
||||
func (m *fieldMapper) BareCandidate(_ *telemetrytypes.LogicalField) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// logicalIsTemporal reports whether the logical field resolves to a single time
|
||||
// column. A family is attribute-backed and never temporal.
|
||||
func (m *fieldMapper) logicalIsTemporal(ctx context.Context, startNs, endNs uint64, logical *telemetrytypes.LogicalField) (bool, error) {
|
||||
|
||||
Reference in New Issue
Block a user