Compare commits

...

1 Commits

Author SHA1 Message Date
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
2 changed files with 36 additions and 15 deletions

View File

@@ -182,10 +182,6 @@ func (m *fieldMapper) getColumn(
case telemetrytypes.FieldContextResource:
return []*schema.Column{indexV3Columns["resource"], indexV3Columns["resources_string"]}, nil
case telemetrytypes.FieldContextScope:
// scope attributes with same name as declared paths create ambiguity and can't be queried directly.
if key.Name == "name" || key.Name == "version" {
return nil, qbtypes.ErrColumnNotFound
}
return []*schema.Column{indexV3Columns["scope"]}, nil
case telemetrytypes.FieldContextAttribute:
switch key.FieldDataType {
@@ -431,19 +427,30 @@ func (m *fieldMapper) ColumnExpressionFor(
keys map[string][]*telemetrytypes.TelemetryFieldKey,
) (string, error) {
// Resolve the candidate logical field(s).
// Resolve the candidate logical field(s). A key carrying a context is asked of metadata
// first, the way the filter path asks: the probe below only answers whether a key
// resolves to a column and stands in for whether it names one field, so a column that
// resolves for either of two homes -- a declared scope path or a same-named scope
// attribute -- would be reported as resolved while still being ambiguous. A bare key
// cannot resolve through the probe at all (getColumn needs a context), so it already
// reaches CandidateKeys, which consults metadata itself, and is left alone here.
var candidates []*telemetrytypes.LogicalField
switch _, err := m.FieldFor(ctx, orgID, startNs, endNs, field); {
case err == nil:
candidates = []*telemetrytypes.LogicalField{m.logicalForResolvedColumn(ctx, orgID, field, keys)}
case errors.Is(err, qbtypes.ErrColumnNotFound):
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))...)
if field.FieldContext != telemetrytypes.FieldContextUnspecified {
candidates = querybuilder.MatchingLogicalFields(ctx, orgID, m.fl, field, keys)
}
if len(candidates) == 0 {
switch _, err := m.FieldFor(ctx, orgID, startNs, endNs, field); {
case err == nil:
candidates = []*telemetrytypes.LogicalField{m.logicalForResolvedColumn(ctx, orgID, field, keys)}
case errors.Is(err, qbtypes.ErrColumnNotFound):
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))...)
}
candidates = m.upgradeToFamilies(ctx, orgID, field, querybuilder.WrapAsLogicalFields(field.Name, raw), keys)
default:
return "", err
}
candidates = m.upgradeToFamilies(ctx, orgID, field, querybuilder.WrapAsLogicalFields(field.Name, raw), keys)
default:
return "", err
}
// Group-by/order (String) and aggregation (String/Float64): every candidate is

View File

@@ -405,6 +405,20 @@ func TestColumnExpressionForScopeDeclaredPath(t *testing.T) {
keys: withAttr,
expectedResult: "multiIf(scope.attributes.`name` IS NOT NULL, scope.attributes.`name`::String, NULL)",
},
{
// metadata knows both homes under this name, so the short spelling coalesces
// them instead of being rejected as ambiguous
name: "short name coalesces a known scope attribute with the declared path",
key: telemetrytypes.TelemetryFieldKey{Name: "name", FieldContext: telemetrytypes.FieldContextScope},
keys: withAttr,
expectedResult: "multiIf(scope.attributes.`name` IS NOT NULL, toString(scope.attributes.`name`::String), scope.name::String <> '', toString(scope.name::String), NULL)",
},
{
name: "short version coalesces a known scope attribute with the declared path",
key: telemetrytypes.TelemetryFieldKey{Name: "version", FieldContext: telemetrytypes.FieldContextScope},
keys: withAttr,
expectedResult: "multiIf(scope.attributes.`version` IS NOT NULL, toString(scope.attributes.`version`::String), scope.version::String <> '', toString(scope.version::String), NULL)",
},
}
for _, tc := range testCases {