Add traversal_path dimension to aggregation engines
What does this MR do and why?
This MR adds a traversal_path dimension to the ClickHouse aggregation-engine framework. It is framework-only: no engine adopts the dimension in this MR.
The framework now provides:
- A new
TraversalPathDimensionclass (lib/gitlab/database/aggregation/click_house/traversal_path_dimension.rb), usable via thetraversal_pathDSL keyword inside an engine'sdimensions do ... endblock. - Registration of that keyword in
dimensions_mapping(lib/gitlab/database/aggregation/click_house/engine.rb). TraversalPathDimensionexpects an organization-scoped traversal path column, such as7/12/34/, where the first segment is the organization id. It skips that segment, sodepth: 1(the default) is the top-level group,depth: 2the second-level subgroup, and so on. The SQL shape isnullIf(toUInt64OrNull(arrayElement(splitByChar('/', <path column>), depth + 1)), 0). Paths shorter than the requested depth, and the0/placeholder path, resolve to a NULL bucket.depthis validated against1..MAX_DEPTH(MAX_DEPTH = 99) with the framework's existingin:mechanism. An engine can narrow the range withparameters: { depth: { in: 1..2 } }.- Support for parameterized association dimensions in GraphQL: a dimension's parameters (like
depth) become GraphQL field arguments, and the row is read via the parameterized instance key, so a query likegroup(depth: 2) { id }resolves correctly.declare_association_fieldmoved intoBaseResponseType, next todeclare_parameterized_field, and both now share a privatedeclare_parameter_argumentshelper. - The
association:Hash gains apreloaderkey alongside the existingmodel,graphql_type, andfinderkeys. The framework callspreloader.new(records, current_user).executeonce per batch after loading, so an engine can avoid N+1 queries on association fields.current_usertravels through the batch loader key to support this. - Documentation for the dimension and for the
finder/preloaderoptions indoc/development/aggregation_engines.md, including a note thatorderBymust pass the sameparametersas the field selection.
Why
The "Duo adoption by group" dashboard chart needs distinct AI users per group (usersCount grouped by group), and per-group trends when combined with the existing timestamp date bucket. A first attempt, !250645 (closed), grouped by the last segment of the traversal path and was closed after review, because that segment is the project namespace id for events tracked in a project, not a group id. The reviewer proposed a depth parameter instead, which is what this dimension implements.
The same design will be reused for the DuoWorkflows engine, the next planned consumer of this framework class, written up in https://gitlab.com/gitlab-org/gitlab/-/work_items/617805.
Notes for reviewers
Three things changed since the previous revision of this MR, in response to review:
- Removed the
with_organization:option entirely. The dimension now always skips the leading organization segment. The engine reads an org-scopedtraversal_pathcolumn, so no engine needswith_organization: false, and the next planned consumer (DuoWorkflows) already stores org-prefixed paths too. Dropped the docs row and the spec case for the option; the spec fixture now uses org-scoped paths throughout. - Removed the
validate_partoverride that checked "depth must be a positive integer", along with its translatable string inlocale/gitlab.pot. The existingin: 1..MAX_DEPTHrange already rejects0, negative values, and values above the maximum. The spec now covers0,-1, and100, all rejected with "Invalid value(s) for parameterdepth". The only value the range let through was a non-integer Float, becauseRange#include?uses cover semantics for numeric ranges ((1..99).include?(1.5)istrue). That input is unreachable in practice, sincedepthis a GraphQLIntargument and the only code that builds an aggregationRequestis the two GraphQL resolvers. - Replaced
Namespace::NUMBER_OF_ANCESTORS_ALLOWED + 1with a framework constant,MAX_DEPTH = 99, onTraversalPathDimension, solib/gitlab/database/aggregationhas no reference to application models or constants. Specs cover depth99accepted and depth100rejected. The docs row now says1to99.
This MR is now framework-only, force-pushed as a single commit, a390256a, 7 files, +326/-40. The Analytics::AggregationEngines::AiUsageEvents adoption moved to a separate draft MR, !254731 (merged), stacked on this branch (it will be retargeted to master after this MR merges). Merge order: this MR first, then !253365 (merged) (rebuilds ai_usage_events with traversal_path as the leading sort key), then MR 254731.
No engine adopts the dimension in this MR, so there is no GraphQL schema change and no translatable strings added. The Changelog trailer is omitted because nothing user-facing changes until an engine adopts the dimension.
References
- Parent issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/605518
- Engine adoption: !254731 (merged)
- Sibling MR (subtree
groupIdfilter): !254120 (merged) - DuoWorkflows depth design: https://gitlab.com/gitlab-org/gitlab/-/work_items/617805
- Closed first attempt: !250645 (closed)
Screenshots or screen recordings
Backend only, no screenshots.
How to set up and validate locally
Run the two spec files:
bundle exec rspec spec/lib/gitlab/database/aggregation/click_house/traversal_path_dimension_spec.rb
bundle exec rspec spec/graphql/types/analytics/aggregation/engine_response_type_spec.rb19 examples pass locally. There is no GraphQL field to query end-to-end until an engine adopts the dimension; see !254731 (merged) for that.
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.