From ec3c6e8ffaa4fca818b58b16e853aabce1b424d5 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Thu, 4 Sep 2025 14:17:23 +0200 Subject: [PATCH] console: ensure main table is used when required even when no data The user will get empty data, it may be better than an empty table. Add more tests as well. Fix #1935 --- console/clickhouse.go | 4 +- console/clickhouse_test.go | 151 ++++++++++++++++++++++++++---- console/data/docs/99-changelog.md | 1 + 3 files changed, 135 insertions(+), 21 deletions(-) diff --git a/console/clickhouse.go b/console/clickhouse.go index 594b3839..0a776f4a 100644 --- a/console/clickhouse.go +++ b/console/clickhouse.go @@ -218,7 +218,7 @@ func (c *Component) computeTableAndInterval(input inputContext) (string, time.Du // Select table targetIntervalForTableSelection := targetInterval if input.MainTableRequired { - targetIntervalForTableSelection = time.Second + return "flows", time.Second, targetInterval } startForTableSelection := input.Start if input.StartForTableSelection != nil { @@ -269,7 +269,7 @@ func (c *Component) getBestTable(start time.Time, targetInterval time.Duration) }) // If possible, use the first resolution before the target interval for len(candidates) > 1 { - if c.flowsTables[candidates[1]].Resolution < targetInterval { + if c.flowsTables[candidates[1]].Resolution <= targetInterval { candidates = candidates[1:] } else { break diff --git a/console/clickhouse_test.go b/console/clickhouse_test.go index 83ee91d3..d1ba874d 100644 --- a/console/clickhouse_test.go +++ b/console/clickhouse_test.go @@ -312,7 +312,7 @@ func TestComputeBestTableAndInterval(t *testing.T) { Expected tableIntervalOutput }{ { - Description: "simple query without additional tables", + Description: "only flows table", Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), @@ -320,7 +320,7 @@ func TestComputeBestTableAndInterval(t *testing.T) { }, Expected: tableIntervalOutput{Table: "flows", Interval: 1}, }, { - Description: "query with main table", + Description: "only flows table, require main", Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), @@ -329,28 +329,16 @@ func TestComputeBestTableAndInterval(t *testing.T) { }, Expected: tableIntervalOutput{Table: "flows", Interval: 1}, }, { - Description: "only flows table available", - Tables: []flowsTable{{"flows", 0, time.Date(2022, 3, 10, 15, 45, 10, 0, time.UTC)}}, + Description: "only flows table available, out of range", + Tables: []flowsTable{{"flows", 0, time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC)}}, Context: inputContext{ - Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), - End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Start: time.Date(2022, 4, 8, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 9, 15, 45, 10, 0, time.UTC), Points: 86400, }, Expected: tableIntervalOutput{Table: "flows", Interval: 1}, }, { - Description: "select flows table out of range", - Tables: []flowsTable{ - {"flows", 0, time.Date(2022, 4, 10, 16, 45, 10, 0, time.UTC)}, - {"flows_1m0s", time.Minute, time.Date(2022, 4, 10, 17, 45, 10, 0, time.UTC)}, - }, - Context: inputContext{ - Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), - End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), - Points: 720, // 2-minute resolution, - }, - Expected: tableIntervalOutput{Table: "flows", Interval: 1}, - }, { - Description: "select consolidated table with better resolution", + Description: "consolidated table with better resolution", Tables: []flowsTable{ {"flows", 0, time.Date(2022, 3, 10, 22, 45, 10, 0, time.UTC)}, {"flows_5m0s", 5 * time.Minute, time.Date(2022, 4, 2, 22, 45, 10, 0, time.UTC)}, @@ -362,6 +350,131 @@ func TestComputeBestTableAndInterval(t *testing.T) { Points: 720, // 2-minute resolution, }, Expected: tableIntervalOutput{Table: "flows_1m0s", Interval: 60}, + }, { + Description: "consolidated table available, but main required", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 3, 10, 22, 45, 10, 0, time.UTC)}, + {"flows_5m0s", 5 * time.Minute, time.Date(2022, 4, 2, 22, 45, 10, 0, time.UTC)}, + {"flows_1m0s", time.Minute, time.Date(2022, 4, 2, 22, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 720, // 2-minute resolution, + MainTableRequired: true, + }, + Expected: tableIntervalOutput{Table: "flows", Interval: 1}, + }, { + Description: "consolidated table available, but out of range", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 3, 10, 22, 45, 10, 0, time.UTC)}, + {"flows_5m0s", 5 * time.Minute, time.Date(2022, 4, 20, 22, 45, 10, 0, time.UTC)}, + {"flows_1m0s", time.Minute, time.Date(2022, 4, 20, 22, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 720, // 2-minute resolution, + }, + Expected: tableIntervalOutput{Table: "flows", Interval: 1}, + }, { + Description: "consolidated table available, main table required, out of range", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 4, 20, 22, 45, 10, 0, time.UTC)}, + {"flows_5m0s", 5 * time.Minute, time.Date(2022, 4, 2, 22, 45, 10, 0, time.UTC)}, + {"flows_1m0s", time.Minute, time.Date(2022, 4, 2, 22, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 720, // 2-minute resolution, + MainTableRequired: true, + }, + Expected: tableIntervalOutput{Table: "flows", Interval: 1}, + }, { + Description: "empty flows tables list", + Tables: []flowsTable{}, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 86400, + }, + Expected: tableIntervalOutput{Table: "flows", Interval: 1}, + }, { + Description: "target interval smaller than 1 second", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 4, 10, 12, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 10, 15, 45, 20, 0, time.UTC), // 10 seconds with many points + Points: 100000, + }, + Expected: tableIntervalOutput{Table: "flows", Interval: 1}, + }, { + Description: "multiple tables with same resolution, choose oldest data", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 4, 10, 12, 45, 10, 0, time.UTC)}, + {"flows_1m0s_a", time.Minute, time.Date(2022, 4, 9, 12, 45, 10, 0, time.UTC)}, + {"flows_1m0s_b", time.Minute, time.Date(2022, 4, 8, 12, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 720, // 2-minute resolution + }, + Expected: tableIntervalOutput{Table: "flows_1m0s_b", Interval: 60}, + }, { + Description: "choose best resolution below target interval", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 4, 8, 12, 45, 10, 0, time.UTC)}, + {"flows_10s", 10 * time.Second, time.Date(2022, 4, 9, 12, 45, 10, 0, time.UTC)}, + {"flows_30s", 30 * time.Second, time.Date(2022, 4, 9, 12, 45, 10, 0, time.UTC)}, + {"flows_2m0s", 2 * time.Minute, time.Date(2022, 4, 9, 12, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 1440, // 1-minute target interval + }, + Expected: tableIntervalOutput{Table: "flows_30s", Interval: 30}, + }, { + Description: "all tables out of range, choose table with oldest data", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 4, 15, 12, 45, 10, 0, time.UTC)}, + {"flows_1m0s", time.Minute, time.Date(2022, 4, 14, 12, 45, 10, 0, time.UTC)}, + {"flows_5m0s", 5 * time.Minute, time.Date(2022, 4, 12, 12, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 720, + }, + Expected: tableIntervalOutput{Table: "flows_5m0s", Interval: 300}, + }, { + Description: "resolution exactly matches target interval", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 4, 8, 12, 45, 10, 0, time.UTC)}, + {"flows_2m0s", 2 * time.Minute, time.Date(2022, 4, 9, 12, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 720, // Exactly 2-minute interval + }, + Expected: tableIntervalOutput{Table: "flows_2m0s", Interval: 120}, + }, { + Description: "sub-second resolution gets clamped to 1 second", + Tables: []flowsTable{ + {"flows", 0, time.Date(2022, 4, 8, 12, 45, 10, 0, time.UTC)}, + {"flows_100ms", 100 * time.Millisecond, time.Date(2022, 4, 9, 12, 45, 10, 0, time.UTC)}, + }, + Context: inputContext{ + Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), + End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), + Points: 8640000, // Very high resolution request + }, + Expected: tableIntervalOutput{Table: "flows_100ms", Interval: 1}, // Clamped to 1 second }, } diff --git a/console/data/docs/99-changelog.md b/console/data/docs/99-changelog.md index ff16e90b..9ee937af 100644 --- a/console/data/docs/99-changelog.md +++ b/console/data/docs/99-changelog.md @@ -13,6 +13,7 @@ identified with a specific icon: ## Unreleased - 🩹 *console*: display missing images in documentation +- 🩹 *console*: ensure main table is used when required even when there is no data - 🩹 *docker*: fix broken `/metrics` endpoint for inlet - 🌱 *build*: accept building with a not up-to-date toolchain - 🌱 *docker*: update ClickHouse to 25.8 (not mandatory)