Skip to main content

GraphQL Extensions

GraphQL Extensions in Hugr provide a standardized way to return transformation results and performance metrics alongside query responses. Extensions appear in the extensions field of the GraphQL response and complement the main data field.

Overview​

What are GraphQL Extensions?​

GraphQL Extensions are additional data returned in the response that don't fit into the standard data field. In Hugr, extensions are used to return:

  • JQ transformation results: Results of server-side data transformations
  • Performance statistics: Query execution metrics and timings

Extension Format​

Extensions always follow a hierarchical structure that mirrors the query structure:

{
"data": {
// Main query results
},
"extensions": {
"node_name": {
"extension_type": {
// Extension data
}
}
}
}

The extension hierarchy matches the query node hierarchy, with nested nodes appearing under a children property.

Extension Types​

Hugr implements two types of GraphQL extensions:

1. JQ Extension​

The jq extension contains results of JQ transformations applied to query results. It appears when using the jq() query or the /jq-query endpoint.

Key characteristics:

  • Returns transformed data according to JQ expression
  • Located in extensions.<query_name>.jq
  • Can be combined with other extensions
  • See JQ Transformations for detailed documentation

2. Stats Extension​

The stats extension returns performance metrics for query execution. It is activated by applying the @stats directive to queries, fields, or mutations.

Key characteristics:

  • Provides detailed timing information
  • Can be applied at query level or field level
  • Supports hierarchical statistics
  • Useful for performance optimization and monitoring

Stats Extension​

Using the @stats Directive​

The @stats directive can be applied to:

  • Entire query: Get overall query execution time
  • Specific fields: Get per-field execution metrics
  • Multiple fields: Track multiple fields independently

Syntax​

query @stats {
# Query execution stats
}

query {
field @stats {
# Field execution stats
}
}

Stats Metrics​

The stats extension provides the following metrics:

MetricDescriptionLevel
compile_timeTime spent compiling the queryField
exec_timeTime spent executing the queryField
node_timeTotal time for the node (including compilation and execution)Field
planning_timeTime spent planning the query executionField
total_timeTotal query execution timeQuery
nameName of the field or queryAll

Examples​

Example 1: Single Field Stats​

Apply @stats to a single field to get its execution metrics:

query {
op2023 {
providers_aggregation @stats {
_rows_count
}
}
}

Response:

{
"data": {
"op2023": {
"providers_aggregation": {
"_rows_count": 628012
}
}
},
"extensions": {
"op2023": {
"children": {
"providers_aggregation": {
"stats": {
"compile_time": "212.791µs",
"exec_time": "1.96875ms",
"name": "providers_aggregation",
"node_time": "2.181541ms",
"planning_time": "180.291µs"
}
}
}
}
}
}

Stats breakdown:

  • compile_time: 212.791µs - time to compile the query
  • exec_time: 1.96875ms - actual query execution time
  • node_time: 2.181541ms - total time (compile + plan + exec)
  • planning_time: 180.291µs - time to plan query execution

Example 2: Multiple Field Stats​

Track statistics for multiple fields:

query {
op2023 {
providers_aggregation @stats {
_rows_count
}
general_payments_aggregation @stats {
_rows_count
}
}
}

Response:

{
"data": {
"op2023": {
"general_payments_aggregation": {
"_rows_count": 14607336
},
"providers_aggregation": {
"_rows_count": 628012
}
}
},
"extensions": {
"op2023": {
"children": {
"general_payments_aggregation": {
"stats": {
"compile_time": "212.084µs",
"exec_time": "2.185083ms",
"name": "general_payments_aggregation",
"node_time": "2.397167ms",
"planning_time": "177.625µs"
}
},
"providers_aggregation": {
"stats": {
"compile_time": "195.875µs",
"exec_time": "2.19775ms",
"name": "providers_aggregation",
"node_time": "2.393625ms",
"planning_time": "162.5µs"
}
}
}
}
}
}

This allows you to compare performance between different fields and identify bottlenecks.

Example 3: Query-Level Stats​

Apply @stats to the entire query to get overall execution time:

query @stats {
op2023 {
providers_aggregation {
_rows_count
}
general_payments_aggregation {
_rows_count
}
}
}

Response:

{
"data": {
"op2023": {
"general_payments_aggregation": {
"_rows_count": 14607336
},
"providers_aggregation": {
"_rows_count": 628012
}
}
},
"extensions": {
"children": {},
"stats": {
"name": "",
"total_time": "2.216625ms"
}
}
}

Note: Query-level stats appear at the root of extensions and only include total_time and name (empty for anonymous queries).

Example 4: Field with Top-Level Query​

Apply stats to a specific field inside a query with top-level stats:

query {
h3(resolution: 6) @stats {
cell_id
geometry
}
}

Response:

{
"data": {
"h3": [
{
"cell_id": "862ba107fffffff",
"geometry": "..."
}
]
},
"extensions": {
"h3": {
"stats": {
"compile_time": "1.471792ms",
"exec_time": "21.653323208s",
"name": "h3",
"node_time": "21.654795s",
"planning_time": "1.017042ms"
}
}
}
}

This example shows a long-running query (21.6 seconds) with detailed timing breakdown.

Combining JQ and Stats Extensions​

You can combine JQ transformations with stats collection to both transform data and measure performance.

Example 1: JQ with Field Stats​

query {
jq(query: ".op2023.providers_aggregation") {
op2023 {
providers_aggregation @stats {
_rows_count
}
general_payments_aggregation {
_rows_count
}
}
}
}

Response:

{
"data": {
"jq": {
"op2023": {
"providers_aggregation": {
"_rows_count": 628012
},
"general_payments_aggregation": {
"_rows_count": 14607336
}
}
}
},
"extensions": {
"jq": {
"children": {
"op2023": {
"children": {
"providers_aggregation": {
"stats": {
"compile_time": "195.875µs",
"exec_time": "2.19775ms",
"name": "providers_aggregation",
"node_time": "2.393625ms",
"planning_time": "162.5µs"
}
}
}
}
},
"jq": {
"_rows_count": 628012
}
}
}
}

Extension structure:

  • extensions.jq.jq: Contains the JQ transformation result
  • extensions.jq.children.op2023.children.providers_aggregation.stats: Contains field stats

Example 2: Multiple JQ with Stats​

query {
jq(query: ".op2023.providers_aggregation") {
op2023 {
providers_aggregation {
_rows_count
}
general_payments_aggregation {
_rows_count
}
}
}
payments: jq(query: ".op2023.general_payments_aggregation") {
op2023 {
general_payments_aggregation @stats {
_rows_count
}
}
}
}

Response:

{
"data": {
"jq": null,
"payments": null
},
"extensions": {
"jq": {
"jq": {
"_rows_count": 628012
}
},
"payments": {
"children": {
"op2023": {
"children": {
"general_payments_aggregation": {
"stats": {
"compile_time": "194.417µs",
"exec_time": "2.013292ms",
"name": "general_payments_aggregation",
"node_time": "2.207709ms",
"planning_time": "159.125µs"
}
}
}
}
},
"jq": {
"_rows_count": 14607336
}
}
}
}

Each JQ query has its own extension section with both jq transformation results and stats (if applied).

Example 3: Query-Level Stats with JQ​

query @stats {
jq(query: ".op2023.providers_aggregation") {
op2023 {
providers_aggregation {
_rows_count
}
general_payments_aggregation {
_rows_count
}
}
}
payments: jq(query: ".op2023.general_payments_aggregation") {
op2023 {
general_payments_aggregation @stats {
_rows_count
}
}
}
}

Response:

{
"data": {
"jq": null,
"payments": null
},
"extensions": {
"children": {
"jq": {
"jq": {
"_rows_count": 628012
}
},
"payments": {
"children": {
"op2023": {
"children": {
"general_payments_aggregation": {
"stats": {
"compile_time": "194.417µs",
"exec_time": "2.013292ms",
"name": "general_payments_aggregation",
"node_time": "2.207709ms",
"planning_time": "159.125µs"
}
}
}
}
},
"jq": {
"_rows_count": 14607336
}
}
},
"stats": {
"name": "",
"total_time": "2.731416ms"
}
}
}

Extension structure:

  • extensions.stats: Query-level total execution time
  • extensions.children.jq: JQ transformation results
  • extensions.children.payments.children...stats: Field-level stats
  • extensions.children.payments.jq: JQ transformation results

Example 4: JQ with @stats on JQ Query​

Apply @stats to the jq() query itself to measure JQ transformation performance:

query {
jq(query: ".op2023.providers_aggregation") @stats {
op2023 {
providers_aggregation {
_rows_count
}
general_payments_aggregation {
_rows_count
}
}
}
payments: jq(query: ".op2023.general_payments_aggregation") {
op2023 {
general_payments_aggregation @stats {
_rows_count
}
}
}
}

Response:

{
"data": {
"jq": null,
"payments": null
},
"extensions": {
"jq": {
"jq": {
"_rows_count": 628012
},
"stats": {
"compiler_time": "59.042µs",
"data_request_time": "2.46025ms",
"execution_time": "9.083µs",
"node_time": "2.464583ms",
"runs": 1,
"serialization_time": "46.125µs",
"transformed": 1
}
},
"payments": {
"children": {
"op2023": {
"children": {
"general_payments_aggregation": {
"stats": {
"compile_time": "194.5µs",
"exec_time": "2.031958ms",
"name": "general_payments_aggregation",
"node_time": "2.226458ms",
"planning_time": "163.292µs"
}
}
}
}
},
"jq": {
"_rows_count": 14607336
}
}
}
}

JQ-specific stats:

  • compiler_time: Time to compile JQ expression
  • data_request_time: Time to fetch data for transformation
  • execution_time: Time to execute JQ transformation
  • serialization_time: Time to serialize result
  • runs: Number of times JQ expression was executed
  • transformed: Number of items transformed

Example 5: Nested Stats with JQ​

query {
jq(query: ".op2023.providers_aggregation") @stats {
op2023 {
providers_aggregation @stats {
_rows_count
}
general_payments_aggregation {
_rows_count
}
}
}
payments: jq(query: ".op2023.general_payments_aggregation") {
op2023 {
general_payments_aggregation @stats {
_rows_count
}
}
}
}

Response:

{
"data": {
"jq": null,
"payments": null
},
"extensions": {
"jq": {
"children": {
"op2023": {
"children": {
"providers_aggregation": {
"stats": {
"compile_time": "107.458µs",
"exec_time": "1.455209ms",
"name": "providers_aggregation",
"node_time": "1.562667ms",
"planning_time": "88.292µs"
}
}
}
}
},
"jq": {
"_rows_count": 628012
},
"stats": {
"compiler_time": "50.958µs",
"data_request_time": "1.736125ms",
"execution_time": "12.583µs",
"node_time": "1.740625ms",
"runs": 1,
"serialization_time": "36.917µs",
"transformed": 1
}
},
"payments": {
"children": {
"op2023": {
"children": {
"general_payments_aggregation": {
"stats": {
"compile_time": "115.583µs",
"exec_time": "1.650875ms",
"name": "general_payments_aggregation",
"node_time": "1.766458ms",
"planning_time": "95.958µs"
}
}
}
}
},
"jq": {
"_rows_count": 14607336
}
}
}
}

This example shows:

  • JQ transformation stats at extensions.jq.stats
  • Field execution stats at extensions.jq.children.op2023.children.providers_aggregation.stats
  • JQ transformation result at extensions.jq.jq

Extension Hierarchy​

Extensions follow a hierarchical structure that mirrors the query structure:

extensions
├── <query_name>
│ ├── stats (if @stats applied to query)
│ ├── jq (if jq() query)
│ └── children
│ └── <field_name>
│ ├── stats (if @stats applied to field)
│ └── children
│ └── <nested_field>
│ └── stats
└── stats (if @stats applied to anonymous query)

Hierarchy Rules​

  1. Root level: Query-level stats (for queries with @stats)
  2. Query name level: JQ results and query-specific data
  3. Children: Nested fields and their stats
  4. Recursive: Structure repeats for nested fields

Use Cases​

1. Performance Monitoring​

Track query execution time to identify slow queries:

query @stats {
users {
id
orders @stats {
id
total
}
}
}

Use the stats to:

  • Identify slow fields
  • Optimize query structure
  • Monitor query performance over time
  • Set up alerts for slow queries

2. Data Transformation with Metrics​

Transform data while tracking performance:

query {
transformed: jq(query: ".users | map({id, name})") @stats {
users {
id
name
email
created_at
}
}
}

Benefits:

  • Measure transformation overhead
  • Compare transformation vs query time
  • Optimize JQ expressions

3. A/B Testing Query Performance​

Compare performance of different query approaches:

query @stats {
approach_a: users(limit: 100) @stats {
id
name
}
approach_b: users_optimized(limit: 100) @stats {
id
name
}
}

Use stats to determine which approach is faster.

4. Debugging Slow Queries​

Add @stats to different levels to identify bottlenecks:

query {
catalog @stats {
products @stats {
id
name
reviews @stats {
rating
}
}
}
}

Check which field contributes most to total execution time.

Best Practices​

1. Use Stats Selectively​

Apply @stats only when needed:

  • Development: Add to all fields to identify bottlenecks
  • Production: Add only to critical queries or when debugging
  • Monitoring: Use query-level stats for overview
# Development - detailed stats
query {
catalog @stats {
products @stats {
id
}
}
}

# Production - minimal overhead
query @stats {
catalog {
products {
id
}
}
}

2. Combine with Logging​

Log extension stats for analysis:

const response = await fetchGraphQL(query);
if (response.extensions?.stats) {
logger.info('Query performance', {
query: queryName,
totalTime: response.extensions.stats.total_time
});
}

3. Set Performance Budgets​

Use stats to enforce performance budgets:

const MAX_QUERY_TIME_MS = 100;

if (parseTime(response.extensions.stats.total_time) > MAX_QUERY_TIME_MS) {
logger.warn('Query exceeded performance budget', {
query: queryName,
time: response.extensions.stats.total_time,
budget: MAX_QUERY_TIME_MS
});
}

4. Monitor JQ Transformation Performance​

When using JQ transformations, track their impact:

query {
jq(query: "complex transformation") @stats {
large_dataset {
# many fields
}
}
}

Check compiler_time, execution_time, and serialization_time to ensure JQ isn't causing bottlenecks.

5. Use Hierarchical Stats Wisely​

Apply stats at different levels based on needs:

# Check if entire query is slow
query @stats { ... }

# Identify which field is slow
query {
field1 @stats { ... }
field2 @stats { ... }
}

# Drill down into nested fields
query {
parent {
child @stats { ... }
}
}

Performance Impact​

Stats Overhead​

The @stats directive has minimal performance overhead:

  • Query-level stats: ~0.01ms overhead
  • Field-level stats: ~0.01ms per field
  • Total impact: Typically <1% of query execution time

When to Avoid Stats​

Avoid excessive use of @stats in:

  • High-frequency queries: Where every microsecond matters
  • Large fan-out queries: With hundreds of fields
  • Production hot paths: Unless actively debugging

Use query-level stats for general monitoring and field-level stats only when drilling into specific issues.

Troubleshooting​

Stats Not Appearing​

Problem: Extensions don't contain stats even though @stats is applied.

Solutions:

  1. Check that the query executed successfully (no GraphQL errors)
  2. Verify @stats syntax is correct
  3. Ensure hugr version supports @stats directive
  4. Check server logs for errors

Unexpected Timing Values​

Problem: Stats show unexpectedly high or low values.

Explanations:

  • High compile_time: Complex query or first-time query compilation (not cached)
  • High exec_time: Large dataset, slow data source, or unoptimized query
  • High node_time vs exec_time: Overhead from planning, compilation, or serialization
  • Low values: Results may be cached

Hierarchical Stats Missing​

Problem: Expected nested stats in children don't appear.

Solutions:

  1. Ensure @stats is applied to the specific field
  2. Check that the field actually executed (not null/skipped)
  3. Verify query structure matches expected hierarchy

See Also​

Documentation​