feat: anchor fixed-day buckets with an optional origin parameter

What does this MR do and why?

Every analytics date dimension takes an optional origin beside granularity: the instant the engine counts fixed-day buckets from.

mode: analytics
query: type = mergerequest and group = "gitlab-org" and mergedAt > -60d
dimensions: merged(granularity="30d", origin="2026-07-16T00:00:00Z")
metrics: totalCount
sort: merged asc

Without an origin, 30d buckets are counted from the Unix epoch and rarely line up with a query's date range. Anchoring them is what makes "the last 30 days vs. the 30 before" a two-row comparison, the building block the DAP Impact Dashboard needs (#187 (closed), second half; the Xd granularity itself is !528 (merged)).

  • origin value: an ISO 8601 date or date-time. A date can be written bare like the dates in filters (origin=2026-07-16); a date-time needs quotes. 2026-07-16, "2026-07-16T00:00:00Z" and "2026-07-16T02:00:00+02:00" are one instant and compile to one canonical "2026-07-16T00:00:00Z", so they share a GraphQL selection and a response-key slug. Relative spellings such as -60d or today() are not accepted; #230 tracks them.
  • Rule: origin needs a fixed-day granularity. The definition declares that as a prerequisite on granularity, the analyzer checks prerequisites generically, and the schema document publishes it under requires. created(granularity=weekly, origin="2026-07-16"), or created(origin=...) with a calendar default, is a compile error naming both parameters and the value rather than the engine's message.
  • Optional parameters: a parameter can now be declared optional, meaning it is simply not sent when the query leaves it out. That is a third state next to "has a default" and "required". A bare created still compiles to createdAt(granularity: "weekly") alone. The schema document publishes such a parameter with "optional": true and no default, and describes the new Timestamp kind under value_kinds.
  • Positional shorthand: one positional argument binds to the first parameter, so created(weekly) and created(30d) keep working now that created declares two parameters. Two positional arguments on such a field keep the existing "use named syntax" error, since there is no positional order for origin.
  • Sorting on an anchored dimension carries both parameters into orderBy. The slug escape scheme gains : (as _c) so an aliased anchored bucket keeps a lossless response key.
  • Shared declaration: every date dimension on every source declares the same granularity + origin pair through one shared definition.

Backend status

origin needs GitLab 19.5 (gitlab!254135 (merged) and gitlab!254973 (merged)). Sorting on an anchored bucket needs the orderBy parameter coercion fix in gitlab!253703 (merged) (19.4).

On gitlab.com today, origin on a nullable date column returns HTTP 500 unless the query also filters on that column: pipelines started/finished, merge requests merged, agent platform sessions created. With a filter such as started > -60d the same query succeeds. Non-null columns (merge requests created, code suggestions timestamp, Duo workflows created, contributions created) work with or without a filter. Tracked in gitlab#629599 (closed); nothing on the GLQL side changes when it is fixed.

User docs in gitlab-org/gitlab follow with the package bump.

How to set up and validate locally

cargo test
DUMP_GRAPHQL=1 cargo test && npm run test:graphql

Example Usage

cd glql_rb
bundle install
bundle exec rake compile

Duo workflow sessions in 28-day buckets anchored at a release date

mode: analytics
query: type = duoworkflow and group = "gitlab-org" and created > -56d
dimensions: created(granularity="28d", origin="2026-07-16")
metrics: totalCount, usersCount
sort: created desc
Test via ruby extension
cd glql_rb # after the build prerequisite above

bundle exec ruby -Ilib - <<'RUBY'
require "gitlab_query_language"
require "json"
require "net/http"

query   = "type = duoworkflow and group = \"gitlab-org\" and created > -56d"
context = { mode: "analytics", dimensions: "created(granularity=\"28d\", origin=\"2026-07-16\")", metrics: "totalCount, usersCount", sort: "created desc" }

compiled = Glql.compile(query, context)
puts "== Generated GraphQL =="
puts compiled["output"]

uri  = URI("https://gitlab.com/api/graphql")
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = true
req  = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req["PRIVATE-TOKEN"] = ENV.fetch("GITLAB_TOKEN")
req.body = { query: compiled["output"], variables: { limit: 4 } }.to_json
response = JSON.parse(http.request(req).body)

transformed = Glql.transform(response["data"], { mode: context[:mode], fields: compiled["fields"] })
puts "\n== Transformed rows =="
puts JSON.pretty_generate(transformed["data"])
RUBY
query GLQL($before: String, $after: String, $limit: Int) {
  group(fullPath: "gitlab-org") {
    analytics {
      duoWorkflows(createdAtFrom: "2026-07-24 23:59") {
        aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: DESC, identifier: "createdAt", parameters: {granularity: "28d", origin: "2026-07-16T00:00:00Z"}}]) {
          count
          pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
          nodes {
            dimensions {
              createdAt(granularity: "28d", origin: "2026-07-16T00:00:00Z")
            }
            totalCount
            usersCount
          }
        }
      }
    }
  }
}
{
  "count": 3,
  "nodes": [
    { "usersCount": 854,  "totalCount": 37580,  "created": "2026-09-10" },
    { "usersCount": 1126, "totalCount": 113722, "created": "2026-08-13" },
    { "usersCount": 1018, "totalCount": 77627,  "created": "2026-07-16" }
  ]
}

The buckets start on the origin and every 28 days after it.

Merge requests created per 30-day window, aliased

mode: analytics
query: type = mergerequest and group = "gitlab-org" and created > -60d
dimensions: created(granularity=30d, origin="2026-07-16T00:00:00Z") as "Period"
metrics: totalCount
sort: created asc
Test via ruby extension

Same snippet as above with this query and context. Generated selection and rows:

mergeRequests(createdAtFrom: "2026-07-20 23:59") {
  aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: ASC, identifier: "createdAt", parameters: {granularity: "30d", origin: "2026-07-16T00:00:00Z"}}]) {
    nodes {
      dimensions {
        createdAt_granularity_30d_origin_2026_m07_m16T00_c00_c00Z: createdAt(granularity: "30d", origin: "2026-07-16T00:00:00Z")
      }
      totalCount
    }
  }
}
{
  "count": 3,
  "nodes": [
    { "totalCount": 10531, "Period": "2026-07-16" },
    { "totalCount": 14603, "Period": "2026-08-15" },
    { "totalCount": 3160,  "Period": "2026-09-14" }
  ]
}

Compile errors

dimensions: created(granularity=weekly, origin="2026-07-16")
`created(origin=...)` needs `granularity` to be a fixed-day granularity such as `30d`, not a calendar bucket (`daily`, `weekly`, `monthly`); `weekly` is not.
dimensions: created(origin=2026-07-16)
`created(origin=...)` needs `granularity` to be a fixed-day granularity such as `30d`, not a calendar bucket (`daily`, `weekly`, `monthly`); the default `weekly` is not.
dimensions: created(granularity=30d, origin=-60d)
`-60d` is not a valid value for `created(origin=...)`. Expected an ISO 8601 date or date-time, for example 2026-07-16 or 2026-07-16T00:00:00Z
dimensions: created(30d, "2026-07-16")
`created` accepts multiple parameters; use named syntax: `created(granularity=..., origin=...)`

Related to #187 (closed) Related to gitlab#629599 (closed)

Edited by Daniele Rossetti

Merge request reports

Loading
Loading