GLQL: Parameterised field syntax for analytics dimensions and metrics

Summary

Analytics dimensions and metrics that accept backend parameters (e.g., granularity on time fields, quantile on duration metrics) need a GLQL syntax for users to control these values. This issue tracks the design and implementation of parameterised fields, replacing the earlier timeSegment() proposal with a generic pattern that works for any parameterised field.

Resolved design

The design was resolved through discussion in this thread, with input from @drosse, @pshutsin, @jiaan, and @rob.hunt.

Syntax

Fields that accept backend parameters use positional or named parameter syntax:

mode: analytics
query: type = Pipeline and project = "gitlab-org/gitlab" and finished > -30d
dimensions: finished(weekly), status
metrics: totalCount, successRate, durationQuantile(0.95)
sort: finished desc
  • Fields with a single parameter accept positional or named syntax: durationQuantile(0.95) and durationQuantile(quantile=0.95) are equivalent
  • Fields with multiple parameters require named syntax: finished(granularity=weekly, offset=5)
  • Named parameters use = as the separator (e.g., granularity=weekly)
  • Values are unquoted for known identifiers and numbers
  • Mixing positional and named arguments in the same field is not allowed
  • Fields without explicit parameters use a source-defined default automatically
  • Sort inherits parameters from the matching dimension or metric

Parameter types

Two parameter types exist across all six aggregation engines. No others.

Granularity (on time-based dimensions): fixed set, validated server-side per engine.

Engine Fields Allowed values
FinishedPipelines started, finished daily, weekly, monthly
CodeSuggestions timestamp monthly
AiUsageEvents timestamp daily, weekly, monthly
AgentPlatformSessions createdEventAt weekly, monthly
Contributions createdAt daily, weekly, monthly
Deployments createdAt, finishedAt daily, weekly, monthly

Quantile (on duration metrics): arbitrary float in [0.01, 0.99], not validated server-side (goes straight to ClickHouse).

Engine Field
FinishedPipelines durationQuantile
AgentPlatformSessions durationQuantile
Deployments deploymentDurationQuantile

Compiler types

enum ParameterConstraint {
    Enum(&'static [&'static str]),
    Range { min: f64, max: f64 },
}

enum ParameterValue {
    Str(&'static str),
    Float(f64),
}

struct FieldParameterDef {
    graphql_key: &'static str,
    constraint: ParameterConstraint,
    default_value: ParameterValue,
}

FieldParameterDef::new() is a const constructor that panics on mismatched variant/constraint pairings (e.g., Float default with Enum constraint), caught at compile time for static definitions.

Shared constants avoid typos across sources:

const GRANULARITY_DAILY: &str = "daily";
const GRANULARITY_WEEKLY: &str = "weekly";
const GRANULARITY_MONTHLY: &str = "monthly";

const QUANTILE_RANGE: ParameterConstraint = ParameterConstraint::Range { min: 0.01, max: 0.99 };

The parser distinguishes positional from named arguments at parse time:

enum FunctionArg {
    Positional(String),
    Named(String, String),
}

The SourceAnalyzer trait exposes parameter metadata as a slice, supporting one or many parameters per field:

fn field_parameters(&self, field: &Field) -> &[FieldParameterDef] {
    &[] // default: field doesn't accept parameters
}

DisplayField variant

A new ParameterizedField variant on DisplayField separates backend parameters from client-side field functions:

pub enum DisplayField {
    Static(Field),
    FieldFunction(String, Vec<FunctionArg>),                // client-side only (labels("bug"))
    ParameterizedField(Field, Vec<(String, String)>),       // backend params (started, [("granularity", "weekly")])
    AliasedDisplayField(Box<DisplayField>, String),
}

ParameterizedField wraps a Field and a vector of resolved key-value pairs, so base_field() returns Some(&field) and it participates fully in validation and sort coupling. FieldFunction stays as-is for client-side transforms.

Resolution flow

The parser stays mode-agnostic. It produces FieldFunction("started", [Positional("weekly")]) or FieldFunction("started", [Named("granularity", "weekly")]) depending on the syntax used. The analyzer then resolves it to ParameterizedField(Field::Started, [("granularity", "weekly")]) using field_parameters() metadata. Resolution happens at the top of validate_analytics_fields, before existing validation.

Resolution rules:

  • Positional args, single-param field (field_parameters().len() == 1): each positional value is mapped to that parameter's graphql_key
  • Positional args, multi-param field (field_parameters().len() > 1): compile error: "finished accepts multiple parameters, use named syntax: finished(granularity=weekly, ...)"
  • Named args: each key is matched against graphql_key entries in field_parameters(). Unknown keys are a compile error.
  • Mixed positional and named: compile error
  • Too many positional args for a single-param field: compile error
  • Duplicate named keys: compile error
  • Value validation: resolved values are checked against ParameterConstraint (enum membership for Enum, bounds for Range)

Sort inheritance

Sort inherits parameters from the matching dimension or metric. Users write sort: started desc and the compiler looks up the granularity from the resolved dimension. Sort fields don't accept parameters directly (Sort holds a Field, not a DisplayField), so dimension/sort granularity mismatches can't happen.

v1 limitations

  • One instance per field name. Duplicate metrics with different parameters (e.g., durationQuantile(0.5) and durationQuantile(0.95)) are not supported in v1. The planned syntax for this uses aliasing:
metrics: durationQuantile(0.5) as "p50", durationQuantile(0.95) as "p95"
sort: p95 desc
  • All v1 fields accept a single parameter, so positional syntax is always valid today. When multi-parameter fields are added in future, those fields will require named syntax. Existing single-param queries (positional or named) will continue to work unchanged.

Worked examples

Pipeline success rates by week

mode: analytics
query: type = Pipeline and project = "gitlab-org/gitlab" and finished > -30d
dimensions: finished(weekly), status
metrics: totalCount, successRate
sort: finished desc

The named form finished(granularity=weekly) is also valid.

Compiles to:

query GLQL($before: String, $after: String, $limit: Int) {
  project(fullPath: "gitlab-org/gitlab") {
    analytics {
      finishedPipelines(finishedAtFrom: "2026-05-17 00:00") {
        aggregated(
          before: $before, after: $after, first: $limit
          orderBy: [{ identifier: "finishedAt", direction: DESC, parameters: { granularity: "weekly" } }]
        ) {
          nodes {
            dimensions { finishedAt(granularity: "weekly") status }
            totalCount
            successRate
          }
        }
      }
    }
  }
}

Code suggestions with default granularity

mode: analytics
query: type = CodeSuggestion and timestamp >= -3m
dimensions: timestamp, language
metrics: totalCount, acceptanceRate
sort: timestamp asc

timestamp has no explicit parameter, so the compiler applies the default: monthly (the only value CodeSuggestions supports). The compile output includes { field: "timestamp", parameter: { granularity: "monthly" } } so the frontend knows what was applied.

Invalid granularity

dimensions: timestamp(daily), language

Compile error: timestamp(daily) is not supported for CodeSuggestion. Supported granularities: monthly

Quantile

metrics: totalCount, durationQuantile(0.95)
sort: durationQuantile desc

The named form durationQuantile(quantile=0.95) is also valid.

Sort inherits the quantile parameter from the metric:

orderBy: [{ identifier: "durationQuantile", direction: DESC, parameters: { quantile: 0.95 } }]

Implementation plan

GLQL compiler (gitlab-org/glql)

MR 1: Add FieldParameterDef metadata to SourceAnalyzer trait (!406 (merged))

  • Add FieldParameterDef, ParameterConstraint, ParameterValue and shared constants (GRANULARITY_*, QUANTILE_RANGE)
  • FieldParameterDef::new() const constructor validates variant/constraint alignment at compile time
  • Add field_parameters() default method on SourceAnalyzer (returns &[])
  • Override for PipelinesAnalyticsAnalyzer and CodeSuggestionsSourceAnalyzer
  • Tests: verify metadata returned correctly per source/field, Display formatting, default-within-range validation
  • No behaviour change

MR 2: Add ParameterizedField variant and parser support

  • Add FunctionArg enum (Positional(String), Named(String, String))
  • Add ParameterizedField(Field, Vec<(String, String)>) variant to DisplayField with .name(), .key(), .label(), .base_field() implementations
  • Extend parser argument grammar to accept unquoted identifiers, floats, and key=value pairs alongside existing quoted strings
  • Update FieldFunction args from Vec<String> to Vec<FunctionArg>
  • FieldFunction.key() quotes all parameter values (both positional and named) for round-trip safety through the Typed transform path, which re-parses keys via parse_fields()
  • Add passthrough match arm in transform_fields
  • parse_fields_without_functions() remains unchanged -- the parser accepts the new syntax via parse_fields() but analytics mode does not use it yet (deferred to MR 3 to avoid a validation gap)
  • Tests: parser round-trips for positional and named forms, DisplayField methods, transform passthrough for ParameterizedField
  • Known gap: standard mode does not validate field function names at compile time, so finished(weekly) in standard mode compiles but produces invalid GraphQL. This is a pre-existing issue tracked in #139 and is not introduced by this work
  • No behaviour change

MR 3: Add resolution and validation in the analyzer

  • Relax parse_fields_without_functions() to allow FieldFunction in analytics dimensions/metrics (moved from MR 2 to avoid a validation gap -- FieldFunction.base_field() returns None, so removing the parser-level guard without simultaneous analyzer-level validation would silently accept invalid syntax)
  • Standard mode rejection: when validate_standard_fields encounters a FieldFunction whose name matches a parameterisable field (field_parameters() returns non-empty), reject with a clear error directing the user to analytics mode. This narrows the pre-existing gap tracked in #139
  • Resolution step at top of validate_analytics_fields: FieldFunction -> ParameterizedField using field_parameters() metadata
  • Positional resolution: if field_parameters().len() == 1, map value to that parameter's key
  • Named resolution: match keys against graphql_key entries
  • Apply default_value for parameterisable fields without explicit parameters. Note: default_value is now ParameterValue (typed), so resolution code should match on the enum variant (Str/Float) rather than parsing strings
  • Structural validation:
    • Positional args on multi-param field: compile error listing expected param names
    • Named key doesn't match any known parameter: compile error
    • Mixed positional and named args: compile error
    • Too many positional args for single-param field: compile error
    • Duplicate named keys: compile error
  • Value validation against ParameterConstraint (enum membership for Enum, bounds for Range). Use ParameterValue::Float for direct numeric comparison against Range bounds without parsing
  • Tests: valid params resolve (both positional and named), invalid params error, defaults applied, non-parameterisable fields rejected, structural validation errors
  • Behaviour change: new syntax is accepted but codegen still uses hardcoded values

MR 4: Thread parameters through codegen and sort

  • Analytics codegen reads parameters from resolved ParameterizedField
  • Sort codegen reads parameters from matching resolved dimension/metric
  • Use ParameterValue's Display impl to format values in GraphQL output (handles string vs float formatting automatically)
  • Remove hardcoded "weekly" and "0.95" from PipelinesAnalyticsAnalyzer
  • Tests: generated GraphQL uses resolved parameters, sort parameters match, existing queries work via defaults
  • Behaviour change: users gain control over granularity and quantile values

MR 5: Expose resolved parameters in compile output

  • Include resolved parameter info in serialised compile output per field
  • Tests: verify compile output shape
  • No frontend changes, just makes the data available

GitLab frontend (gitlab-org/gitlab)

Tracked separately in gitlab#603198 (closed) -- use compile output parameter metadata to replace hardcoded TIME_DIMENSIONS, surface applied defaults in UI, update docs and query examples.

References

  • Original discussion: !367 (merged) comment
  • Parameterised fields proposal: comment
  • Design resolution: comment
  • Named parameters feedback: comment
  • Positional/named design: comment
  • Hardcoded values to replace: src/analyzer/sources/pipelines/analytics.rs:90-120
  • showTrends (depends on this): #102
  • Sparklines (depends on this): #105
  • Parent epic: &21207
Edited by Robert Hunt