Compare commits

..

10 Commits

Author SHA1 Message Date
Nikhil Soni
3942c8dede test(traces-qb): correct the scope declared-path doc comment 2026-08-25 20:06:52 +05:30
Nikhil Soni
3a9e6e78ad refactor(traces-qb): keep the resolved-column comment signal-agnostic
More than one home under one name is the general case; scope is one instance.
Also fold the two rewritten cases back onto single lines, as the rest of the
table is.

Assisted-by: Claude Opus 5
2026-08-25 19:58:55 +05:30
Nikhil Soni
bf27773965 test(traces-qb): expect scope names to union every home they resolve to
A `name`/`version` scope attribute used to be unreachable, so these cases
asserted that `scope.name` meant the declared path alone and that a bare
`name` never left the span column. Both now resolve, and a scope attribute
colliding with a name behaves like any other same-named attribute.

Renamed the three cases: their ids stated the old contract.

Assisted-by: Claude Opus 5
2026-08-25 19:45:26 +05:30
Nikhil Soni
4e06ba0230 refactor(traces-qb): inline the resolved-column lookup
The helper wrapped two lines behind a name that described the old
single-field behaviour. At the call site the fallback reads directly.

Assisted-by: Claude Opus 5
2026-08-25 19:21:06 +05:30
Nikhil Soni
cf5d9dafe1 Revert "test(traces-qb): cover semconv families for context-carrying select keys"
The extension was justified by resolving select through metadata before the
probe. That reordering is gone: a family match now behaves the same as it
always did, since logicalForResolvedColumn returned the family and returns a
list whose only element is that family. The extra key shapes pin nothing this
change alters.

Assisted-by: Claude Opus 5
2026-08-25 19:10:54 +05:30
Nikhil Soni
f665a1e0b9 test(traces-qb): pin the scope-prefixed name under an explicit context
A caller-supplied context means `scope.` is part of the name, not a prefix to
strip, so the key addresses a scope attribute literally named
`scope.testing.env`. Nothing covered the asymmetry with the normalized
spelling, which resolves to `testing.env`.

Assisted-by: Claude Opus 5
2026-08-25 18:45:12 +05:30
Nikhil Soni
73d2c5d09c refactor(traces-qb): keep every metadata match for a resolved column
Resolving select through metadata before the probe worked, but it reordered
resolution for every context-carrying key to fix one thing: a key that resolves
to a column is not necessarily one field.

Say that where it belongs instead. logicalForResolvedColumn already asks
metadata; it just threw away all but a family and fell back to the key itself.
Returning every match it finds fixes the same ambiguity without moving
resolution around: ColumnExpressionFor keeps its probe-first shape and differs
from main by a single line.

The scope column resolves for both a declared path and a same-named scope
attribute, so `scope.name` with such an attribute present now yields both homes
rather than one. Output is identical to the reordered version on every case
that motivated it -- declared-only, attribute-shadowed, materialized attribute,
and an undeclared scope attribute.

Assisted-by: Claude Opus 5
2026-08-25 17:16:41 +05:30
Nikhil Soni
3425776ca6 test(traces-qb): cover semconv families for context-carrying select keys
TestColumnExpressionForFamilyGroupBy only exercised a bare key, which resolves
through the candidate path. A key carrying a context now resolves through
metadata instead, and nothing covered that the family is still reached there.

Run the existing assertion over all three key shapes. The expression is
identical for each: MatchingLogicalFields returns the grouped family directly,
which is what logicalForResolvedColumn was selecting from the same call.

Assisted-by: Claude Opus 5
2026-08-25 16:17:57 +05:30
Nikhil Soni
39b2fc5cef refactor(traces-qb): restore CandidateKeys metadata match to main's form
The context-filtered bare-name match and the appended `{context}.{name}`
spelling existed to let a scope key find its declared path once getColumn
declined scope `name`/`version`. Resolving select through metadata first moved
that job to MatchingLogicalFields, so the extra matching is dead weight: the
suite is green with main's two straight lookups restored.

Keep the scope synthesize case -- it is not redundant. A scope attribute
metadata does not know still reaches CandidateKeys from the filter path, which
passes nil keys for strict contexts, and without the case it resolves to
nothing and the filter fails with "key not found". No test covered that, so
add one.

Assisted-by: Claude Opus 5
2026-08-25 12:44:58 +05:30
Nikhil Soni
bae9a15f2a fix(traces-qb): ask metadata before the column probe when selecting
ColumnExpressionFor resolved a key by probing FieldFor first, while the filter
path asks metadata first. The probe only answers whether a key resolves to a
column, and was standing in for whether it names one field. For a JSON column
that resolves for either of two homes -- a declared scope path or a same-named
scope attribute -- it reported a single resolved field for an ambiguous name,
so getColumn had to decline scope `name` and `version` to force the key back
onto the candidate path. That made a scope attribute so named unselectable:
it failed with "field not found".

Ask metadata first for a key that carries a context, the way the filter path
does. A bare key is left exactly as it was -- it cannot resolve through the
probe at all, since getColumn needs a context, so it already reaches
CandidateKeys, which consults metadata itself.

The filter path is untouched: its rule that one home wins an ambiguous name is
a deliberate choice, and selecting still coalesces the homes instead.

Assisted-by: Claude Opus 5
2026-08-24 12:40:01 +05:30
3 changed files with 7 additions and 10 deletions

View File

@@ -107,8 +107,8 @@ func SynthesizeKeys(field *telemetrytypes.TelemetryFieldKey, value any) []*telem
fieldContext = telemetrytypes.FieldContextAttribute
}
fieldDataType := field.FieldDataType
// Resource and scope values are strings; pin the type so operand coercion applies.
if (fieldContext == telemetrytypes.FieldContextResource || fieldContext == telemetrytypes.FieldContextScope) &&
// Resource values are strings; pin the type so operand coercion applies.
if fieldContext == telemetrytypes.FieldContextResource &&
fieldDataType == telemetrytypes.FieldDataTypeUnspecified {
fieldDataType = telemetrytypes.FieldDataTypeString
}

View File

@@ -513,9 +513,6 @@ func TestConditionForSynthesizedKeys(t *testing.T) {
sb.Where(conds...)
sql, _ := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
assert.Contains(t, sql, "scope.attributes.`custom.attr`")
// `scope.` can be part of the attribute's own name, so the literal spelling is a
// candidate too — the caller ORs the two.
assert.Contains(t, sql, "scope.attributes.`scope.custom.attr`")
})
t.Run("bare key with number operand -> attribute number", func(t *testing.T) {

View File

@@ -423,10 +423,6 @@ func (m *fieldMapper) ColumnExpressionFor(
candidates = []*telemetrytypes.LogicalField{telemetrytypes.SingleLogicalField(field.Name, field)}
}
case errors.Is(err, qbtypes.ErrColumnNotFound):
// The legacy candidate flow, unchanged: column (when the bare name is
// one) plus metadata matches, else synthesized type-variant keys. The
// family step below only swaps candidates for their family; it never
// changes candidate order or non-family behavior.
raw := m.CandidateKeys(ctx, orgID, field, nil, keys)
if len(raw) == 0 {
return "", errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field.Name, errors.NounKeys, maps.Keys(keys))...)
@@ -595,10 +591,14 @@ func (m *fieldMapper) CandidateKeys(ctx context.Context, _ valuer.UUID, field *t
// honored as-is: the stripped name lives in the attribute maps
stripped := telemetrytypes.NewTelemetryFieldKey(field.Name, telemetrytypes.FieldContextUnspecified, field.FieldDataType)
return querybuilder.SynthesizeKeys(stripped, value)
case telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextResource, telemetrytypes.FieldContextScope:
case telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextResource:
// strict context honored as-is: stripped interpretation first, literal spelling second
literal := telemetrytypes.NewTelemetryFieldKey(field.FieldContext.StringValue()+"."+field.Name, field.FieldContext, field.FieldDataType)
return append(querybuilder.SynthesizeKeys(field, value), querybuilder.SynthesizeKeys(literal, value)...)
case telemetrytypes.FieldContextScope:
// Declared scope paths (scope.name / scope.version) arrive via metadata as intrinsics;
// anything reaching synth is an undeclared scope attribute.
return []*telemetrytypes.TelemetryFieldKey{telemetrytypes.NewTelemetryFieldKey(field.Name, telemetrytypes.FieldContextScope, telemetrytypes.FieldDataTypeString)}
}
// contexts that don't exist on spans (log, body, …) have nothing to synthesize
return nil