Skip to content

Commit 7e0a20c

Browse files
radimclaude
andcommitted
test: cover index drift surviving the table filter
Add TestBuild_TableFilterKeepsIndexRows: a table=orders filter must keep orders_pkey's sizing and idx_scan drift (the #8 fix) while dropping an unrelated table, with an unfiltered control proving the filter is what excludes it. Adds index-bearing planner/activity fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 183d739 commit 7e0a20c

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

internal/snapdiff/snapdiff_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,98 @@ func TestBuild_NodeMissingOnOneSide(t *testing.T) {
538538
t.Fatalf("only the primary (present on both sides) should be diffed, got %+v", res.ActivityDelta)
539539
}
540540
}
541+
542+
func qn(name string) snapshot.QualifiedName {
543+
return snapshot.QualifiedName{Schema: "public", Name: name}
544+
}
545+
546+
// plannerWithIndex / activityWithIndex go beyond the bare-table helpers above:
547+
// they also carry an index entry (orders_pkey) plus an unrelated second table
548+
// (invoices), which is exactly the shape needed to prove a table filter keeps the
549+
// table's index rows while still dropping a different table.
550+
func plannerWithIndex(schemaRef, hash string, ts time.Time, ordersRel float64, idxSize int64, invoicesRel float64) *snapshot.PlannerStatsSnapshot {
551+
return &snapshot.PlannerStatsSnapshot{
552+
SchemaRefHash: schemaRef, ContentHash: hash, Database: "appdb", Timestamp: ts,
553+
Tables: []snapshot.TableSizingEntry{
554+
{Table: qn("orders"), Sizing: snapshot.TableSizing{Reltuples: ordersRel, Relpages: 10, TableSize: 81920}},
555+
{Table: qn("invoices"), Sizing: snapshot.TableSizing{Reltuples: invoicesRel, Relpages: 2, TableSize: 16384}},
556+
},
557+
Indexes: []snapshot.IndexSizingEntry{
558+
{Table: qn("orders"), Index: "orders_pkey", Sizing: snapshot.IndexSizing{Reltuples: ordersRel, Relpages: 5, Size: idxSize}},
559+
},
560+
}
561+
}
562+
563+
func activityWithIndex(schemaRef, hash, node string, ts time.Time, ordersScan, idxScan, invoicesScan int64) *snapshot.ActivityStatsSnapshot {
564+
return &snapshot.ActivityStatsSnapshot{
565+
SchemaRefHash: schemaRef, ContentHash: hash,
566+
Node: snapshot.NodeIdentity{Source: node, PgVersion: "PostgreSQL 17.0", Timestamp: ts},
567+
Tables: []snapshot.TableActivityEntry{
568+
{Table: qn("orders"), Activity: snapshot.TableActivity{SeqScan: ordersScan, IdxScan: 5}},
569+
{Table: qn("invoices"), Activity: snapshot.TableActivity{SeqScan: invoicesScan, IdxScan: 1}},
570+
},
571+
Indexes: []snapshot.IndexActivityEntry{
572+
{Table: qn("orders"), Index: "orders_pkey", Activity: snapshot.IndexActivity{IdxScan: idxScan, IdxTupRead: idxScan * 2}},
573+
},
574+
}
575+
}
576+
577+
// TestBuild_TableFilterKeepsIndexRows is the #8 fix in one test: when you drill
578+
// into a single hot table, you want the *whole* table's story, and an index is
579+
// part of that story. Before, a table= filter matched rows by name and an index
580+
// row's name is the index, not the table — so orders_pkey's bloat and idx_scan
581+
// drift quietly vanished the moment you narrowed to orders, which is the worst
582+
// time to lose it. The fix threads the owning table onto the index identity, so
583+
// here orders_pkey must survive the filter carrying both its sizing and its
584+
// activity drift, while a genuinely-unrelated table (invoices) is correctly
585+
// dropped. The unfiltered control proves invoices was only ever excluded by the
586+
// filter and not by some other accident of the fixture.
587+
func TestBuild_TableFilterKeepsIndexRows(t *testing.T) {
588+
ctx := context.Background()
589+
store := openStore(t)
590+
t0 := time.Now().Truncate(time.Second).Add(-3 * time.Hour)
591+
t1 := t0.Add(2 * time.Hour)
592+
593+
// identical table shapes at both moments (distinct hashes) so the diff is
594+
// driven entirely by stats/activity, not DDL — keeps the index rows the only
595+
// thing under test rather than tangled up with schema changes.
596+
tbls := []snapshot.Table{table("orders", "id"), table("invoices", "id")}
597+
put(t, store, history.WrapSchema(mkSchema("schema-a", t0, tbls...)))
598+
put(t, store, history.WrapPlanner(plannerWithIndex("schema-a", "planner-a", t0.Add(time.Minute), 1000, 8192, 100)))
599+
put(t, store, history.WrapActivity(activityWithIndex("schema-a", "activity-a", "primary", t0.Add(time.Minute), 10, 3, 1)))
600+
601+
put(t, store, history.WrapSchema(mkSchema("schema-b", t1, tbls...)))
602+
put(t, store, history.WrapPlanner(plannerWithIndex("schema-b", "planner-b", t1.Add(time.Minute), 5000, 81920, 500)))
603+
put(t, store, history.WrapActivity(activityWithIndex("schema-b", "activity-b", "primary", t1.Add(time.Minute), 100, 90, 50)))
604+
605+
// control: with no filter, the unrelated invoices table is present
606+
unfiltered, err := Build(ctx, store, key(), Options{From: "latest~1", To: "latest", Kind: "schema"})
607+
if err != nil {
608+
t.Fatalf("Build (unfiltered): %v", err)
609+
}
610+
if findObject(unfiltered.Objects, "invoices") == nil {
611+
t.Fatal("fixture sanity: invoices should appear without a filter")
612+
}
613+
614+
res, err := Build(ctx, store, key(), Options{From: "latest~1", To: "latest", Kind: "schema", Table: "orders"})
615+
if err != nil {
616+
t.Fatalf("Build (filtered): %v", err)
617+
}
618+
619+
idx := findObject(res.Objects, "orders_pkey")
620+
if idx == nil || idx.Kind != "index" {
621+
t.Fatalf("orders_pkey index drift must survive a table=orders filter, got objects %+v", res.Objects)
622+
}
623+
if len(idx.Sizing) == 0 {
624+
t.Error("the index should carry its sizing drift (size grew 10x)")
625+
}
626+
if len(idx.Activity) == 0 {
627+
t.Error("the index should carry its idx_scan drift")
628+
}
629+
if findObject(res.Objects, "orders") == nil {
630+
t.Error("the table itself should still be present")
631+
}
632+
if findObject(res.Objects, "invoices") != nil {
633+
t.Error("an unrelated table must be filtered out")
634+
}
635+
}

0 commit comments

Comments
 (0)