Aggregation engines: origin-anchored dynamic day granularity (Xd) for date_bucket dimensions
What does this MR do and why?
Dashboards need to compare "last 30 days" against "the previous 30 days" in a single aggregation request. Calendar granularities (daily/weekly/monthly) cannot express windows anchored at arbitrary dates, so this is the first of two MRs implementing https://gitlab.com/gitlab-org/gitlab/-/work_items/609138, which adds origin-anchored dynamic day granularity (Xd) to date_bucket dimensions.
This MR adds the framework capability only and changes no production behavior: no aggregation engine adopts the new values (their explicit parameters: declarations are unchanged), there is no GraphQL schema change, no generated artifact changes, no database migrations, and no changelog entry (the user-facing entry ships with part 2). Part 2 removes the per-engine granularity declarations, introduces built-in default parameters on the dimension, and exposes the feature through GraphQL.
The framework layer in lib/gitlab/database/aggregation/ gains:
ClickHouse::DateBucketDimensionaccepts a dynamic day granularity matching/\A(?<days>\d{1,3})d\z/(for example30d), capped at 1 to 366 days, compiling totoStartOfInterval(column, INTERVAL 30 DAY)(buckets aligned to the Unix epoch).- An optional
origin(:datetime) parameter anchors dynamic day buckets via the three-argument form. ClickHouse requires the origin to be on or before every bucketed value, so the engine shifts the user origin to its phase-equivalent anchor near the Unix epoch (origin Unix timestamp modulo the interval) and emitstoStartOfInterval(column, INTERVAL 30 DAY, toDateTime64('<anchor>', 6, 'UTC')). Bucket boundaries are identical to anchoring at the user origin; rows older than the origin land in earlier buckets instead of failing the query; origin never filters rows. The three-argument form requires ClickHouse 24.9 or later (CI runs 25.12 and 26.2). - Origin values are normalized (String, Date, Time, ActiveSupport::TimeWithZone all become UTC whole-second times), so a GraphQL
Timeargument and the same value as a JSON string inorderBy.parametersproduce identical result keys. ParameterizedDefinitionin:allowlists accept Regexp entries mixed with strings. Patterns are matched with RE2 throughGitlab::UntrustedRegexp(no backtracking, so request values cannot trigger ReDoS), compiled once per definition at class-load time. Ruby flags are translated (/ito(?i),/mto RE2'ss); extended mode (/x) and^/$anchors are rejected when the definition is built (use\A/\z).- Validations: day count outside 1..366 rejected (the digit-bounded pattern also rejects arbitrarily long digit strings before integer parsing);
originwith a calendar granularity or without a granularity rejected; unparseable origin rejected; one error per field.
Generated SQL, for database reviewers:
-- calendar granularity (unchanged)
toStartOfInterval(column, INTERVAL 1 month)
-- new: dynamic day granularity, epoch-aligned
toStartOfInterval(column, INTERVAL 30 DAY)
-- new: dynamic day granularity with origin (anchor = origin mod interval, near 1970)
toStartOfInterval(column, INTERVAL 30 DAY, toDateTime64('1970-01-26 00:00:00', 6, 'UTC'))References
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/609138
- Part 2 (applies defaults to engines and exposes the feature in GraphQL): link to follow
- ClickHouse
toStartOfIntervaldocumentation: https://clickhouse.com/docs/sql-reference/functions/date-time-functions#tostartofinterval
Screenshots or screen recordings
| Before | After |
|---|---|
How to set up and validate locally
-
Set up ClickHouse in GDK (GDK howto):
gdk config set clickhouse.enabled true, setclickhouse.bin, thengdk reconfigure && gdk start clickhouse, and:echo "CREATE DATABASE IF NOT EXISTS gitlab_clickhouse_development" | curl -s --data-binary @- http://127.0.0.1:8123/ bundle exec rake gitlab:clickhouse:migrate -
Seed rows in
rails console(any project works):project = Project.first path = project.project_namespace.traversal_path(with_organization: true).to_s rows = [[9001, 80], [9002, 40], [9003, 10]].map do |id, days| t = days.days.ago.utc.strftime('%Y-%m-%d %H:%M:%S') "(#{id}, 100, #{project.id}, '#{path}', 'main', 1, 'success', 60, '#{t}', '#{t}', now(), false)" end.join(', ') ClickHouse::Client.execute(<<~SQL, :main) INSERT INTO siphon_p_ci_pipelines (id, partition_id, project_id, traversal_path, ref, source, status, duration, started_at, finished_at, _siphon_replicated_at, _siphon_deleted) VALUES #{rows} SQL -
Build a throwaway engine in the same console. On this branch the parameters must be declared explicitly (built-in defaults arrive in part 2):
engine_class = Gitlab::Database::Aggregation::ClickHouse::Engine.build do self.table_name = 'siphon_p_ci_pipelines' dimensions do date_bucket :started_at, :datetime, parameters: { granularity: { type: :string, in: ['daily', 'weekly', 'monthly', Gitlab::Database::Aggregation::ClickHouse::DateBucketDimension::DYNAMIC_GRANULARITY_FORMAT] }, origin: { type: :datetime } } end metrics { count } end engine = engine_class.new(context: { scope: ClickHouse::Client::QueryBuilder.new('siphon_p_ci_pipelines') }) origin = 30.days.ago.beginning_of_day.utc request = Gitlab::Database::Aggregation::Request.new( dimensions: [{ identifier: :started_at, parameters: { granularity: '30d', origin: origin } }], metrics: [{ identifier: :total_count }] ) response = engine.execute(request) response[:data].to_aExpected: three rows with bucket values
origin - 60.days,origin - 30.days, andorigin, one per seeded pipeline — the two pipelines older than the origin land in earlier buckets instead of failing the query. Swap the parameters for{ granularity: 'monthly', origin: origin }or{ granularity: '367d' }to see the validation errors (response.error?/response[:errors]). -
Or run the specs directly (needs a ClickHouse server on
127.0.0.1:8123andconfig/click_house.ymlcreated from the example file, withlocalhostchanged to127.0.0.1): 18 examples indate_bucket_dimension_spec.rband 29 inparameterized_definition_spec.rb, all executed against a real ClickHouse server:bundle exec rspec spec/lib/gitlab/database/aggregation/click_house/date_bucket_dimension_spec.rb \ spec/lib/gitlab/database/aggregation/parameterized_definition_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related to #609138
Summary
This update adds two new features to the date bucketing dimension used in analytics aggregations:
Dynamic day granularity: Instead of only supporting fixed calendar periods like "weekly" or "monthly," users can now specify custom intervals in days (e.g., 30d, 7d, 366d). Valid values are between 1 and 366 days. Buckets are aligned to the Unix epoch by default.
Origin parameter: Users can optionally provide an origin datetime to anchor bucket boundaries to a specific point in time (e.g., "today"), which is useful for current-vs-previous period comparisons. The origin is automatically shifted back to a phase-equivalent point near the Unix epoch so that older data still falls into valid buckets rather than causing query errors. Origin only works with dynamic day granularities, not calendar-based ones.
Security improvements: The allowlist validation logic was hardened to support Regexp entries safely. Any regular expressions used in allowlists are compiled using RE2 (which prevents catastrophic backtracking / ReDoS attacks), and patterns using unsafe anchors (^/$) or extended mode are rejected at startup rather than at request time.
Appropriate validation error messages are surfaced for all invalid combinations, and the new .pot locale file entries make those messages translatable.