refactor: drop evolution for list select

This commit is contained in:
Nikhil Soni
2026-09-07 17:39:49 +05:30
parent 14131aaf56
commit 864faf5c16
8 changed files with 29 additions and 97 deletions

View File

@@ -1,6 +1,6 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:26.4.3
container_name: init-clickhouse
command:
- bash
@@ -18,7 +18,7 @@ services:
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:26.4.3
container_name: clickhouse
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml

View File

@@ -20,8 +20,8 @@ import (
)
// jsonAttrColRe matches the raw `attributes` JSON column in the SELECT list (a bare column, not
// the `attributes_string`/`_number`/`_bool` maps).
var jsonAttrColRe = regexp.MustCompile(`,\s*attributes\s*,`)
// the `attributes_string`/`_number`/`_bool` maps), mid-list or as the last column before FROM.
var jsonAttrColRe = regexp.MustCompile(`,\s*attributes\s*(,| FROM )`)
func newBulkTestBuilder(t *testing.T, releaseTime time.Time) (*traceQueryStatementBuilder, *telemetrytypestest.MockMetadataStore) {
t.Helper()
@@ -46,10 +46,11 @@ func newBulkTestBuilder(t *testing.T, releaseTime time.Time) (*traceQueryStateme
return b, store
}
// TestBulkAttributeColumnsAcrossWindows asserts the empty-selectFields ("all fields") list query
// scans the attribute homes the column evolution resolves to for the window: the legacy maps before
// the JSON rollout, the JSON column after it, and both across it.
func TestBulkAttributeColumnsAcrossWindows(t *testing.T) {
// TestListQuerySelectsAllAttributeHomes asserts the empty-selectFields ("all fields") list query
// always scans every physical home of the attributes bag — the three legacy maps and the JSON
// column — in any window and without consulting evolution metadata. consume.go merges them per
// row, so a row's attributes surface whichever home they were written to.
func TestListQuerySelectsAllAttributeHomes(t *testing.T) {
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
rel := releaseTime.UnixMilli()
day := int64(24 * time.Hour / time.Millisecond)
@@ -57,15 +58,13 @@ func TestBulkAttributeColumnsAcrossWindows(t *testing.T) {
b, _ := newBulkTestBuilder(t, releaseTime)
cases := []struct {
name string
startMs uint64
endMs uint64
wantJSON bool
wantLegacyMap bool
name string
startMs uint64
endMs uint64
}{
{"before rollout -> maps only", uint64(rel - 2*day), uint64(rel - day), false, true},
{"after rollout -> json only", uint64(rel + day), uint64(rel + 2*day), true, false},
{"straddle rollout -> both", uint64(rel - day), uint64(rel + day), true, true},
{"before rollout", uint64(rel - 2*day), uint64(rel - day)},
{"after rollout", uint64(rel + day), uint64(rel + 2*day)},
{"straddling rollout", uint64(rel - day), uint64(rel + day)},
}
for _, tt := range cases {
@@ -79,12 +78,10 @@ func TestBulkAttributeColumnsAcrossWindows(t *testing.T) {
require.NoError(t, err)
selectList := stmt.Query[:strings.Index(stmt.Query, " FROM ")]
assert.Equal(t, tt.wantJSON, jsonAttrColRe.MatchString(stmt.Query),
"json `attributes` column presence; select=%s", selectList)
assert.Equal(t, tt.wantLegacyMap, strings.Contains(stmt.Query, "attributes_string"),
"legacy map presence; select=%s", selectList)
// resources_string stays a legacy map in every window (out of scope for this change).
assert.Contains(t, stmt.Query, "resources_string")
assert.Regexp(t, jsonAttrColRe, stmt.Query, "json `attributes` column; select=%s", selectList)
for _, col := range []string{"attributes_string", "attributes_number", "attributes_bool", "resources_string"} {
assert.Contains(t, stmt.Query, col, "select=%s", selectList)
}
})
}
}

View File

@@ -352,29 +352,6 @@ func lookupIntrinsicOrCalculatedField(name string) (telemetrytypes.TelemetryFiel
}
// buildListQuery builds a query for list panel type.
// bulkAttributeColumnNames returns the attribute columns the empty-selectFields list path scans:
// the physical homes the attributes column-evolution resolves to for [start, end] (legacy maps,
// the JSON column, or both across the rollout). resources_string is appended separately by the
// caller and stays a legacy map until the resource column moves to JSON.
func (b *traceQueryStatementBuilder) bulkAttributeColumnNames(ctx context.Context, start, end uint64) ([]string, error) {
evolutions, err := b.metadataStore.GetColumnEvolutions(ctx, []*telemetrytypes.EvolutionSelector{{
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldName: telemetrytypes.EvolutionFieldNameAll,
}})
if err != nil {
return nil, err
}
cols, err := tracestelemetryschema.BulkAttributeColumns(evolutions, start, end)
if err != nil {
return nil, err
}
names := make([]string, len(cols))
for i, c := range cols {
names[i] = c.Name
}
return names, nil
}
func (b *traceQueryStatementBuilder) buildListQuery(
ctx context.Context,
@@ -410,14 +387,18 @@ func (b *traceQueryStatementBuilder) buildListQuery(
}
if isSelectFieldsEmpty {
attrCols, err := b.bulkAttributeColumnNames(ctx, start, end)
if err != nil {
return nil, err
}
for _, col := range attrCols {
// The attributes bag is read whole: every physical home is selected
// unconditionally — a row's attributes live in exactly one home (or both,
// agreeing, during dual-write), and consume.go merges them per row with the
// JSON column winning. No evolution lookup is needed for the bag (unlike
// per-key reads, which pick typed homes per window for index/cost), and the
// read stays correct for rows written by a maps-only exporter past the
// rollout. The attributes JSON column exists since migration 1012, the same
// assumption getColumn already makes for the resource/scope JSON columns.
for _, col := range tracestelemetryschema.ContextualSpanColumns {
sb.SelectMore(col)
}
sb.SelectMore(tracestelemetryschema.SpanResourcesStringColumn)
sb.SelectMore(tracestelemetryschema.SpanAttributesColumn)
}
// From table

View File

@@ -2407,13 +2407,6 @@ func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx conte
return temporalities, types, nil
}
func (k *telemetryMetaStore) GetColumnEvolutions(ctx context.Context, selectors []*telemetrytypes.EvolutionSelector) ([]*telemetrytypes.EvolutionEntry, error) {
if len(selectors) == 0 {
return nil, nil
}
return k.fetchEvolutionEntryFromClickHouse(ctx, selectors)
}
func (k *telemetryMetaStore) fetchEvolutionEntryFromClickHouse(ctx context.Context, selectors []*telemetrytypes.EvolutionSelector) ([]*telemetrytypes.EvolutionEntry, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("signal", "column_name", "column_type", "field_context", "field_name", "version", "release_time")

View File

@@ -371,28 +371,6 @@ func (m *fieldMapper) resolveColumnExprs(
return exprs, existExprs, columns, nil
}
// BulkAttributeColumns returns the physical attribute columns a whole-bag (list-view) read must
// scan over [startNs, endNs] given the attributes column-evolution entries: the three legacy maps
// before the JSON rollout, the JSON column after it, and both across the rollout — the same
// per-window home selection the per-key path uses, applied to the whole column. With no rollout
// entry it stays on the legacy maps.
func BulkAttributeColumns(evolutions []*telemetrytypes.EvolutionEntry, startNs, endNs uint64) ([]*schema.Column, error) {
maps := []*schema.Column{
indexV3Columns["attributes_string"],
indexV3Columns["attributes_number"],
indexV3Columns["attributes_bool"],
}
if len(evolutions) == 0 {
return maps, nil
}
family := append([]*schema.Column{indexV3Columns["attributes"]}, maps...)
cols, _, err := qbtypes.SelectEvolutionsForColumns(family, evolutions, startNs, endNs)
if err != nil {
return nil, err
}
return cols, nil
}
// attributeColumnEvolutionRegistered reports whether key carries an evolution entry for the given column.
func attributeColumnEvolutionRegistered(key *telemetrytypes.TelemetryFieldKey, columnName string) bool {
for _, e := range key.Evolutions {

View File

@@ -4,10 +4,6 @@ import (
"time"
)
// EvolutionFieldNameAll is the field_name of a column-wide (whole-signal-context) evolution
// entry, as opposed to a per-field promotion entry.
const EvolutionFieldNameAll = "__all__"
type EvolutionEntry struct {
Signal Signal `json:"signal"`
ColumnName string `json:"column_name"`

View File

@@ -19,11 +19,6 @@ type MetadataStore interface {
// GetKey returns a list of keys with the given name.
GetKey(ctx context.Context, orgID valuer.UUID, fieldKeySelector *FieldKeySelector) ([]*TelemetryFieldKey, error)
// GetColumnEvolutions returns the column-evolution entries matching the selectors (including
// __all__ family rows) without resolving concrete keys, so a whole-column read such as the
// list-view attributes bag can pick its physical homes for the query window.
GetColumnEvolutions(ctx context.Context, selectors []*EvolutionSelector) ([]*EvolutionEntry, error)
// GetRelatedValues returns a list of related values for the given key name
// and the existing selection of keys.
GetRelatedValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *FieldValueSelector) ([]string, bool, error)

View File

@@ -163,14 +163,6 @@ func (m *MockMetadataStore) GetKey(ctx context.Context, _ valuer.UUID, fieldKeyS
return result, nil
}
func (m *MockMetadataStore) GetColumnEvolutions(_ context.Context, selectors []*telemetrytypes.EvolutionSelector) ([]*telemetrytypes.EvolutionEntry, error) {
var evolutions []*telemetrytypes.EvolutionEntry
for _, selector := range selectors {
evolutions = append(evolutions, m.ColumnEvolutionMetadataMap[selector.QualifiedName()]...)
}
return evolutions, nil
}
// GetRelatedValues returns a list of related values for the given key name and selection.
func (m *MockMetadataStore) GetRelatedValues(ctx context.Context, _ valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) ([]string, bool, error) {
if fieldValueSelector == nil {