Add parameter resolution and validation in the analyzer

What does this MR do and why?

This is MR 3 of 5 for parameterised field syntax.

MR 1 (!406 (merged)) added FieldParameterDef metadata so each analytics source can declare what parameters its fields accept. MR 2 (!407 (merged)) added the ParameterizedField variant, parser grammar, and FunctionArg types so syntax like finished(weekly) could be parsed. But analytics mode still rejected all function syntax at the parser level to avoid a validation gap (since FieldFunction.base_field() returns None, removing the parser guard without analyzer validation would silently accept invalid syntax).

This MR lifts that parser guard and replaces it with proper analyzer-level resolution and validation. After this MR, users can write parameterised syntax like finished(weekly) or durationQuantile(0.95) in analytics dimensions and metrics, and the compiler validates both the structure and values against the source's parameter metadata.

Resolution flow

A new resolve_analytics_parameters() step runs in the compile pipeline between apply_default_fields and analyze, keeping analyze() as a pure validation pass with immutable &Context. The resolution:

  • Converts FieldFunction to ParameterizedField for fields that accept parameters, resolving positional or named arguments against FieldParameterDef metadata
  • Applies default parameter values to Static fields that are parameterisable (e.g. bare started becomes ParameterizedField(Started, [("granularity", "weekly")]))
  • Rejects function syntax on non-parameterisable fields with a clear error
  • Validates structural rules: mixed positional/named args, too many args, duplicate keys, unknown keys
  • Validates values against constraints: enum membership for granularity, numeric range for quantile

Standard mode rejection

When validate_standard_fields encounters a FieldFunction whose name matches a parameterisable field from the source's analytics mode (currently only Pipelines), it rejects with a clear error directing the user to analytics mode. This narrows the pre-existing validation gap tracked in #139.

What this MR does NOT do

Codegen still uses hardcoded values for granularity and quantile. MR 4 threads the resolved parameters through codegen and sort, and removes the hardcoded defaults. The resolved parameters are stored on the DisplayField but not yet consumed by the rendering layer.

References

How to set up and validate locally

  1. cargo test -- all 937 tests should pass
  2. cargo clippy -- should be clean
  3. cargo fmt --check -- should be clean

Manual testing via the Ruby extension

Build the native extension (re-run after any Rust changes):

cd glql_rb
bundle install
bundle exec rake compile

Parameterised dimension (positional syntax)

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

echo "q = Glql.compile('${GLQL_QUERY}', { mode: 'analytics', dimensions: 'finished(weekly), status', metrics: 'totalCount, successRate', sort: 'finished desc' })[\"output\"]; puts q; exit" | bundle exec rake console

Note: codegen still uses hardcoded parameter values (MR 4 threads resolved parameters through codegen), so the GraphQL output won't yet reflect the user-specified granularity.

Parameterised metric (named syntax)

mode: analytics
query: type = Pipeline and project = "gitlab-org/gitlab" and finished > -30d
dimensions: ref
metrics: totalCount, durationQuantile(quantile=0.95)
Test via ruby extension
GLQL_QUERY='type = Pipeline and project = "gitlab-org/gitlab" and finished > -30d'

echo "q = Glql.compile('${GLQL_QUERY}', { mode: 'analytics', dimensions: 'ref', metrics: 'totalCount, durationQuantile(quantile=0.95)' })[\"output\"]; puts q; exit" | bundle exec rake console

Invalid parameter error

mode: analytics
query: type = Pipeline and project = "gitlab-org/gitlab" and finished > -30d
dimensions: finished(yearly)
metrics: totalCount
Test via ruby extension
GLQL_QUERY='type = Pipeline and project = "gitlab-org/gitlab" and finished > -30d'

echo "puts Glql.compile('${GLQL_QUERY}', { mode: 'analytics', dimensions: 'finished(yearly)', metrics: 'totalCount' })[\"output\"]; exit" | bundle exec rake console

Expected output:

Error: `yearly` is not a valid value for `finished(granularity=...)`. Supported values: daily, weekly, monthly

Standard mode rejection

mode: standard
query: type = Pipeline and project = "gitlab-org/gitlab"
fields: finished(weekly)
Test via ruby extension
GLQL_QUERY='type = Pipeline and project = "gitlab-org/gitlab"'

echo "puts Glql.compile('${GLQL_QUERY}', { fields: 'finished(weekly)' })[\"output\"]; exit" | bundle exec rake console

Expected output:

Error: `finished` accepts parameters in analytics mode for pipelines. Parameterised fields are not supported in standard mode.
Edited by Robert Hunt

Merge request reports

Loading