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](https://gitlab.com/gitlab-org/glql/-/work_items/130#note_3389296683), with input from @drosse, @pshutsin, @jiaan, and @rob.hunt.
### Syntax
Fields that accept backend parameters use positional or named parameter syntax:
```yaml
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
```rust
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:
```rust
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:
```rust
enum FunctionArg {
Positional(String),
Named(String, String),
}
```
The `SourceAnalyzer` trait exposes parameter metadata as a slice, supporting one or many parameters per field:
```rust
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:
```rust
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:
```yaml
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
```yaml
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:
```graphql
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
```yaml
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
```yaml
dimensions: timestamp(daily), language
```
Compile error: `timestamp(daily) is not supported for CodeSuggestion. Supported granularities: monthly`
### Quantile
```yaml
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:
```graphql
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)
- 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 https://gitlab.com/gitlab-org/glql/-/work_items/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 https://gitlab.com/gitlab-org/glql/-/work_items/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 -- use compile output parameter metadata to replace hardcoded `TIME_DIMENSIONS`, surface applied defaults in UI, update docs and query examples.
## References
- Original discussion: !367 [comment](https://gitlab.com/gitlab-org/glql/-/merge_requests/367#note_3198368171)
- Parameterised fields proposal: [comment](https://gitlab.com/gitlab-org/glql/-/work_items/130#note_3389296683)
- Design resolution: [comment](https://gitlab.com/gitlab-org/glql/-/work_items/130#note_3459939784)
- Named parameters feedback: [comment](https://gitlab.com/gitlab-org/glql/-/work_items/130#note_3460514814)
- Positional/named design: [comment](https://gitlab.com/gitlab-org/glql/-/work_items/130#note_3461349221)
- 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
issue