package stats_test import ( "context" "regexp" "strings" "testing" "rond-with-you/internal/stats" ) // placeCols 里每个相关子查询都必须带 `<别名>.dataset_id = p.dataset_id`: // idx_visits_place 是 (dataset_id, place_id),缺了前导列,每轮循环都要为库里 // 每个 dataset 各下一次索引(实测 Buffer 命中 23737 → 5669,耗时 53ms → 31ms)。 // 谓词本身冗余,删掉不会报错、也不会算错,只会悄悄变慢 —— 所以只能这么钉。 func TestPlaceColsCarriesDatasetIDInCorrelatedSubqueries(t *testing.T) { // 去掉全部空白再找,免得因为换行/缩进/等号两边空格的差异误报。 compact := strings.NewReplacer(" ", "", "\n", "", "\t", "", "\r", "").Replace(stats.PlaceCols) // 必须按子查询分段判断:v2 出现三次,全文 Contains 会被另外两处顶包。 hits := regexp.MustCompile(`FROMvisits(v\d+)`).FindAllStringSubmatchIndex(compact, -1) if len(hits) != 5 { t.Fatalf("placeCols 里的 visits 子查询数量变成 %d(期望 5),请同步更新本用例", len(hits)) } for i, m := range hits { alias := compact[m[2]:m[3]] end := len(compact) if i+1 < len(hits) { end = hits[i+1][0] } if seg := compact[m[1]:end]; !strings.Contains(seg, alias+".dataset_id=p.dataset_id") { t.Errorf("placeCols 中 %s 的子查询缺少 %s.dataset_id=p.dataset_id(会导致索引前导列缺失)", alias, alias) } } } // placeCols 里 top 活动三轴(name/color/icon)是三个**互相独立**的众数, // 不能合并成一个 GROUP BY:合并后同一活动名下的颜色/图标会跟着最多的那一行走, // 而独立众数允许「名字取 A、颜色取 B」。这里用 Go 侧独立数一遍钉住语义。 // SQL 的并列取法是未定义的,所以只在「三轴都存在唯一严格最大值」的地点上断言。 func TestPlaceColsTopActivityMatchesIndependentCount(t *testing.T) { db, dsID := setup(t) ctx := context.Background() d := stats.DB{DB: db} type counts map[string]int type placeAgg struct { name, color, icon counts home, work bool } aggs := map[int64]*placeAgg{} var placeIDs []int64 rows, err := db.QueryContext(ctx, `SELECT id FROM places WHERE dataset_id=$1`, dsID) if err != nil { t.Fatal(err) } for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { t.Fatal(err) } placeIDs = append(placeIDs, id) aggs[id] = &placeAgg{name: counts{}, color: counts{}, icon: counts{}} } rows.Close() if err := rows.Err(); err != nil { t.Fatal(err) } // 有活动的到访:top 三轴的独立计数。 rows, err = db.QueryContext(ctx, `SELECT v.place_id, a.name, a.color, a.icon FROM visits v JOIN activities a ON a.id=v.activity_id WHERE v.dataset_id=$1`, dsID) if err != nil { t.Fatal(err) } for rows.Next() { var pid int64 var name, color, icon string if err := rows.Scan(&pid, &name, &color, &icon); err != nil { t.Fatal(err) } a := aggs[pid] if a == nil { continue } a.name[name]++ a.color[color]++ a.icon[icon]++ } rows.Close() if err := rows.Err(); err != nil { t.Fatal(err) } // is_home / is_work 来自 visits 本身,不 join activities。 for _, tc := range []struct { q string mark func(*placeAgg) }{ {`SELECT place_id FROM visits WHERE dataset_id=$1 AND is_home`, func(a *placeAgg) { a.home = true }}, {`SELECT place_id FROM visits WHERE dataset_id=$1 AND is_work`, func(a *placeAgg) { a.work = true }}, } { rows, err := db.QueryContext(ctx, tc.q, dsID) if err != nil { t.Fatal(err) } for rows.Next() { var pid int64 if err := rows.Scan(&pid); err != nil { t.Fatal(err) } if a := aggs[pid]; a != nil { tc.mark(a) } } rows.Close() if err := rows.Err(); err != nil { t.Fatal(err) } } got, err := d.MapPlaces(ctx, stats.Filter{DatasetID: dsID}, 4000) if err != nil { t.Fatal(err) } if len(got) == 0 { t.Skip("MapPlaces 返回空,无法比对") } strict := func(c counts) (string, bool) { // 唯一严格众数 best, n, tie := "", 0, false for v, k := range c { switch { case k > n: best, n, tie = v, k, false case k == n: tie = true } } return best, best != "" && !tie } checked := 0 for _, p := range got { a := aggs[p.ID] if a == nil { t.Fatalf("MapPlaces 返回了不属于本数据集的地点 %d", p.ID) } if p.IsHome != a.home || p.IsWork != a.work { t.Errorf("地点 %d: is_home=%v is_work=%v,独立统计为 %v/%v", p.ID, p.IsHome, p.IsWork, a.home, a.work) } name, nOK := strict(a.name) color, cOK := strict(a.color) icon, iOK := strict(a.icon) if !nOK || !cOK || !iOK { continue // SQL 的并列取法未定义,跳过 } checked++ if p.TopActivity != name || p.TopColor != color || p.TopIcon != icon { t.Errorf("地点 %d: top 三轴 = %q/%q/%q,独立数出 %q/%q/%q", p.ID, p.TopActivity, p.TopColor, p.TopIcon, name, color, icon) } } if checked == 0 { t.Skip("本数据集没有「三轴唯一众数」的地点,比不了") } t.Logf("已比对 %d/%d 个地点(其余有并列,跳过)", checked, len(got)) }