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)anddurationQuantile(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'sgraphql_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_keyentries infield_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 forEnum, bounds forRange)
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)anddurationQuantile(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 descThe 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 asctimestamp 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), languageCompile error: timestamp(daily) is not supported for CodeSuggestion. Supported granularities: monthly
Quantile
metrics: totalCount, durationQuantile(0.95)
sort: durationQuantile descThe 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,ParameterValueand shared constants (GRANULARITY_*,QUANTILE_RANGE) FieldParameterDef::new()const constructor validates variant/constraint alignment at compile time- Add
field_parameters()default method onSourceAnalyzer(returns&[]) - Override for
PipelinesAnalyticsAnalyzerandCodeSuggestionsSourceAnalyzer - Tests: verify metadata returned correctly per source/field,
Displayformatting, default-within-range validation - No behaviour change
MR 2: Add ParameterizedField variant and parser support
- Add
FunctionArgenum (Positional(String),Named(String, String)) - Add
ParameterizedField(Field, Vec<(String, String)>)variant toDisplayFieldwith.name(),.key(),.label(),.base_field()implementations - Extend parser argument grammar to accept unquoted identifiers, floats, and
key=valuepairs alongside existing quoted strings - Update
FieldFunctionargs fromVec<String>toVec<FunctionArg> FieldFunction.key()quotes all parameter values (both positional and named) for round-trip safety through the Typed transform path, which re-parses keys viaparse_fields()- Add passthrough match arm in
transform_fields parse_fields_without_functions()remains unchanged -- the parser accepts the new syntax viaparse_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 allowFieldFunctionin analytics dimensions/metrics (moved from MR 2 to avoid a validation gap --FieldFunction.base_field()returnsNone, so removing the parser-level guard without simultaneous analyzer-level validation would silently accept invalid syntax) - Standard mode rejection: when
validate_standard_fieldsencounters aFieldFunctionwhose 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->ParameterizedFieldusingfield_parameters()metadata - Positional resolution: if
field_parameters().len() == 1, map value to that parameter's key - Named resolution: match keys against
graphql_keyentries - Apply
default_valuefor parameterisable fields without explicit parameters. Note:default_valueis nowParameterValue(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 forEnum, bounds forRange). UseParameterValue::Floatfor direct numeric comparison againstRangebounds 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'sDisplayimpl to format values in GraphQL output (handles string vs float formatting automatically) - Remove hardcoded
"weekly"and"0.95"fromPipelinesAnalyticsAnalyzer - 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