Каталог разрезов и измеренный род агрегации
- род метрики выводится сверкой минутного слоя с часовым: часовое значение сходится с суммой минутных — накопительная, со средним — мгновенная, иначе `unknown` и свёртка не предлагается вовсе. На живом архиве (123 доставки, 31 метрика) 7 накопительных, 9 мгновенных, противоречащих часов ноль - `GET /api/v1/metrics` под токеном чтения отдаёт единицы, слои с границами и род вместе с основанием измерения; род нигде не хранится — он функция витрины, а витрина функция журнала, устаревать в нём нечему - миграция 00009: покрывающий индекс, чтобы каталог отвечал по учётным колонкам, не разжимая содержимое объектов
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user