Aggregation framework: rate metric returns inf/nan instead of NULL when the denominator is zero
Summary
Gitlab::Database::Aggregation::ClickHouse::Rate builds a division with no zero-denominator guard. When the denominator matches zero rows, ClickHouse returns inf or nan instead of NULL. These values reach dashboards and GraphQL Float fields as-is.
Discovered while implementing the ratio metric type for the aggregation framework: https://gitlab.com/gitlab-org/gitlab/-/issues/617052
Steps to reproduce
- Declare or query a
ratemetric whosedenominator_if:condition can match zero rows in the selected period (for example adeploymentssuccess rate over a date range with no finished deployments). - Read the resulting metric value via the aggregation engine or its GraphQL field.
What is the current bug behavior?
lib/gitlab/database/aggregation/click_house/rate.rb line 28 builds:
Arel::Nodes::NamedFunction.new('countIf', [inner_column.eq(1)]) / denominatordenominator is countIf(denominator_condition) when denominator_if: is declared, otherwise Arel::Nodes::Count.new([Arel.star]) (count(*)). Neither path guards against a zero denominator. ClickHouse does not raise on float division by zero; it returns inf or nan. That value is exposed through GraphQL as a Float, so API consumers and dashboards can receive inf or nan instead of a "no data" signal.
Seven call sites across five shipped engines in ee/app/models/analytics/aggregation_engines/ use rate and are exposed to this:
| Engine | Metric | Denominator |
|---|---|---|
deployments.rb |
success (line 44) |
denominator_if: transient(:is_finished_status) |
deployments.rb |
failure (line 49) |
denominator_if: transient(:is_finished_status) |
deployments.rb |
canceled (line 54) |
denominator_if: transient(:is_finished_status) |
code_suggestions.rb |
acceptance (line 27) |
denominator_if: |
pipelines.rb |
outcome (line 43) |
denominator_if: |
agent_platform_sessions.rb |
completion (line 37) |
count(*) fallback (no denominator_if:) |
merge_requests.rb |
acceptance (line 36) |
count(*) fallback (no denominator_if:) |
The five sites with an explicit denominator_if: are the main exposure: that condition can legitimately match zero rows within a period. The two count(*) fallback sites are lower risk, reachable only when the entire filtered result set is empty for the period.
What is the expected correct behavior?
When the denominator matches zero rows, the metric should return NULL, so the caller (dashboard, GraphQL response) can render "no data" rather than inf or nan.
Relevant code
# lib/gitlab/database/aggregation/click_house/rate.rb, line 28
Arel::Nodes::NamedFunction.new('countIf', [inner_column.eq(1)]) / denominatorPossible fixes
Wrap the denominator in nullIf(denominator, 0), matching the pattern already used by the sibling Ratio metric type in the same aggregation framework:
numerator / Arel::Nodes::NamedFunction.new('nullIf', [denominator, Arel::Nodes.build_quoted(0)])This is a one-line change but it alters observable behavior of already-shipped metrics, from inf/nan to null. Frontend consumers currently receiving inf/nan would begin receiving null. It warrants its own review and likely a changelog entry, separate from any dependent feature work.
Follow-up worth considering separately: Rate is conceptually a special case of Ratio (countIf/countIf). Refactoring Rate to be implemented in terms of Ratio would fix this and remove the duplication, but that is a larger change than this bug warrants.