Files
zeekayandHanzo Dev 914fbfaa70 datastoretraces: ingested_at is the server's, not the sender's
The span writer bound ingested_at. In event.span's DDL that one column is three
things at once:

  ReplacingMergeTree(ingested_at)     -- which duplicate survives a merge
  PARTITION BY toDate(ingested_at)    -- which partition the row lands in
  TTL toDateTime(ingested_at) + 30d   -- when the row is deleted

Verified against the live table, not assumed:

  ingested_at  DateTime64(3)  DEFAULT  now64(3)
  ReplacingMergeTree(ingested_at) PARTITION BY toDate(ingested_at)
  ORDER BY (org, trace_id, time, id)
  TTL toDateTime(ingested_at) + toIntervalDay(30)

So binding it handed a sender the retention of its own rows: date a batch
forward and it outlives the policy, date it back and it is deleted on arrival,
and either way it can beat a newer row in a Replacing merge. That is not a
retention policy, it is the absence of one — the column exists to record when WE
received a row, and only the server's clock can mean that.

The column already carries DEFAULT now64(3), so nothing is lost by omitting it.

cloud's planesink.go always omitted it and cloud enforces the rule downstream
(TestRetentionIsNotARequestParameter, apps/analytics/capture_test.go). This
writer was the divergent side — and it is the side that owns the DDL, so the
rule now lives here too: a guard enforced only downstream is one the schema's
own writers can still break.

TestPlaneWritersNeverBindIngestedAt asserts no INSERT template in this package
binds it; re-adding the column to spanSQLTmpl fails it with the reason. It
covers all six write paths, not just the one that was wrong.
TestPlaneTablesAreQualified keeps every INSERT database-qualified, since the
templates take the database as a parameter and an unqualified one would silently
write to another plane with the same table names.

WithNow keeps feeding the support tables' unix_milli, the resource bucket and
the zero-time fallbacks; it no longer feeds event.span.

  ok  github.com/hanzoai/o11y/pkg/datastoretraces

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 22:09:57 -07:00

71 lines
2.5 KiB
Go

package datastoretraces
import (
"strings"
"testing"
"github.com/hanzoai/o11y/pkg/telemetrytraces"
)
// planeWriters is every INSERT this package issues, paired with the table it
// targets. Adding a write path means adding it here.
var planeWriters = []struct {
table string
tmpl string
}{
{telemetrytraces.SpanTableName, spanSQLTmpl},
{telemetrytraces.SpanAttributeTableName, attributeSQLTmpl},
{telemetrytraces.SpanKeyTableName, keySQLTmpl},
{telemetrytraces.SpanResourceTableName, resourceSQLTmpl},
{telemetrytraces.OperationTableName, operationSQLTmpl},
{telemetrytraces.TraceTableName, traceSQLTmpl},
}
// TestPlaneWritersNeverBindIngestedAt is the guard for the rule this package
// broke: ingested_at is the event plane's retention anchor and must be set by
// the SERVER, never by a writer, and therefore never by whoever produced the
// batch.
//
// In event.span's DDL that one column is simultaneously
//
// ReplacingMergeTree(ingested_at) -- which duplicate wins a merge
// PARTITION BY toDate(ingested_at) -- which partition the row lands in
// TTL toDateTime(ingested_at) + 30d -- when the row is deleted
//
// so binding it lets a sender choose its own retention — keep data past the
// policy by dating it forward, or destroy it by dating it back. The column
// carries DEFAULT now64(3) precisely so no writer has to.
//
// cloud enforces the same rule downstream (TestRetentionIsNotARequestParameter,
// apps/analytics/capture_test.go). It lives here too because THIS repo owns the
// DDL, and a rule enforced only downstream is a rule the schema's own writers
// can still break.
func TestPlaneWritersNeverBindIngestedAt(t *testing.T) {
for _, w := range planeWriters {
if strings.Contains(w.tmpl, "ingested_at") {
t.Errorf("%s writer binds ingested_at — the sender would control the "+
"Replacing version, the partition and the TTL of its own rows; "+
"drop the column and let DEFAULT now64(3) apply", w.table)
}
}
}
// TestPlaneTablesAreQualified keeps every INSERT database-qualified. The
// templates take the database as a parameter (WithDatabase), so an unqualified
// one would silently write to whatever database the connection happened to
// default to — a different plane, with the same table names.
func TestPlaneTablesAreQualified(t *testing.T) {
for _, w := range planeWriters {
if !strings.HasPrefix(w.tmpl, "INSERT INTO %s.%s (") {
t.Errorf("%s writer is not database-qualified: %q", w.table, first(w.tmpl, 60))
}
}
}
func first(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}