package store import ( "context" "path/filepath" "strings" "testing" ) // План выполнения — единственный оракул требования «каталог не читает // содержимого объектов». `bucket` объявлена WITHOUT ROWID, то есть строка // целиком, вместе со сжатым payload, живёт в дереве первичного ключа: обход по // нему тащил бы страницы содержимого. Проверяется, что обе выборки идут по // ПОКРЫВАЮЩЕМУ индексу — по нему таблица не открывается вовсе. func TestПланЗапросовКаталогаИдётПоИндексу(t *testing.T) { t.Parallel() st, err := Open(filepath.Join(t.TempDir(), "healthlog.db")) if err != nil { t.Fatalf("открытие базы: %v", err) } t.Cleanup(func() { _ = st.Close() }) cases := []struct { name string sql string args []any }{ {"разрезы метрик", layerRangesQuery, nil}, {"общие часы двух слоёв", commonHoursQuery, []any{"step_count", "hour", "step_count", "minute", "2026-08-02T00:00:00Z", 48}}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { t.Parallel() plan := explain(t, st, c.sql, c.args...) t.Logf("план: %s", plan) if !strings.Contains(plan, "COVERING INDEX bucket_catalog") { t.Errorf("выборка не идёт по покрывающему индексу:\n%s", plan) } // Обращение к самой таблице выглядит в плане как SCAN/SEARCH bucket // без слова INDEX — именно оно и тащило бы страницы содержимого. for _, line := range strings.Split(plan, "\n") { if strings.Contains(line, "bucket") && !strings.Contains(line, "INDEX") { t.Errorf("строка плана читает таблицу, а не индекс: %q", line) } } }) } } func explain(t *testing.T, st *Store, query string, args ...any) string { t.Helper() rows, err := st.db.QueryContext(context.Background(), "EXPLAIN QUERY PLAN "+query, args...) if err != nil { t.Fatalf("EXPLAIN QUERY PLAN: %v", err) } defer func() { _ = rows.Close() }() var out []string for rows.Next() { var id, parent, notused int var detail string if err := rows.Scan(&id, &parent, ¬used, &detail); err != nil { t.Fatalf("разбор плана: %v", err) } out = append(out, detail) } if err := rows.Err(); err != nil { t.Fatalf("EXPLAIN QUERY PLAN: %v", err) } if len(out) == 0 { t.Fatal("план пуст — проверять нечего") } return strings.Join(out, "\n") } // Свойство, ради которого выборка объектов окна вынесена отдельной функцией: // сколько бы часов ни было в окне, запрос ОДИН. Чтение по объекту за раз давало // бы под сотню обращений на метрику и столько же снимков витрины. func TestВыборкаОкнаОстаётсяОднимЗапросом(t *testing.T) { t.Parallel() for _, hours := range []int{1, 2, 48, 200} { q := hourPairsQuery(hours) if n := strings.Count(q, ";"); n != 0 { t.Errorf("часов %d: в запросе %d разделителей — это уже не один запрос", hours, n) } // Три подстановки на метрику и слои плюс по одной на час. if got, want := strings.Count(q, "?"), hours+3; got != want { t.Errorf("часов %d: подстановок %d, ждали %d", hours, got, want) } } }