---
product: "AG Studio"
title: "Implementing Queries"
description: "A custom engine receives each query Studio needs answered, translates it into the backend's own query language, and returns the results in an agreed shape."
framework: angular
version: "3.0.0"
related:
    - title: "Custom Engine"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/angular/server-side-data/"
llms: "https://www.ag-grid.com/studio/archive/3.0.0/llms.txt"
---

# Implementing Queries

A custom engine receives each query Studio needs answered, translates it into the backend's own query language, and returns the results in an agreed shape.

Implement the optional `executeCube()` method as well if your dashboard uses Pivot Grid, Treemap, Sunburst, or legend-grouped charts. See [Pivot and Hierarchy Queries](https://www.ag-grid.com/studio/archive/3.0.0/angular/server-side-data-implementation/#pivot-and-hierarchy-queries).

## Query Anatomy

Studio calls `execute()` with one or more `AgExecuteRequest` objects, each carrying an `AgStudioQuery` to translate. Every field reference in an `AgStudioQuery` is an `AgStudioQueryField` object carrying `key`, `fieldId`, `sourceId`, and optional properties like `aggregation`.

### Core Fields

| Field | Purpose | Shape |
| --- | --- | --- |
| `axes` | Group-by dimensions for aggregation | `[{ dimensions: [{ field: { key, fieldId, sourceId } }] }]` |
| `measures` | Aggregated columns | `[{ field: { key, fieldId, sourceId, aggregation: 'sum' } }]` |
| `projection` | Raw columns when not aggregating (mutually exclusive with `axes`/`measures`) | `[{ field: { key, fieldId, sourceId } }]` |
| `filter` | WHERE clause tree (recursive groups with `combinator`, leaves with `operator`) | `{ combinator: 'and', conditions: [{ field, operator: 'equals', value }] }` |
| `sort` | ORDER BY specification | `[{ field: { key, fieldId, sourceId }, direction: 'desc' }]` |
| `limit` | LIMIT and OFFSET | `{ count: 100, offset: 20 }` |
| `computedFields` | Computed columns with expression ASTs and evaluation phase | See [Computed Fields](https://www.ag-grid.com/studio/archive/3.0.0/angular/server-side-data-implementation/#computed-fields) |

> **Warning**
>
> Honouring `limit` (`count` and `offset`) is mandatory. Server-side row models fetch data page by page and expect each page to return the rows starting at its own `offset` - an engine that ignores it and always returns the same page makes every page look identical, which surfaces as a row-identity warning (see [Row Identity](https://www.ag-grid.com/studio/archive/3.0.0/angular/server-side-data-implementation/#row-identity)) rather than as a pagination bug.

### Additional Fields

These fields appear when the dashboard uses features that require them. If your engine does not support a given field, the corresponding UI feature will not work correctly.

| Field | Purpose |
| --- | --- |
| `having` | Post-aggregation filter (SQL `HAVING`) |
| `window` | Window functions (`rank`, `denseRank`, `rowNumber`) |
| `joins` | Multi-source joins (see [Sources and Joins](https://www.ag-grid.com/studio/archive/3.0.0/angular/server-side-data-implementation/#sources-and-joins)) |
| `scope` | Dimension-member slice for slicer/page-filter semantics |
| `distinct` | Row deduplication for projection-mode queries |
| `from` | Derived-table composition (SQL `FROM (SELECT ...)`) |

Properties available on the `AgStudioQuery` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `axes` | `AgAxisDefinition[]` |  | Dimension axes for OLAP aggregation. Empty `axes: []` combined with measures yields a single grand-total group. Mutually exclusive with `projection`. |
| `measures` | `AgMeasureDefinition[]` |  | Measures aggregated at each cell. Required when `axes` is present. |
| `projection` | `AgDimensionDefinition[]` |  | Output columns for detail/raw-row queries. Mutually exclusive with `axes`/`measures`. |
| `joins` | `AgJoinClause[]` |  | Ordered join chain - the query's source topology. Omit for a single-source query. Engines must consume clauses in order and must not perform schema lookup to reconstruct the topology. The `FROM` clause's base source is `joins[0].leftSourceId` (see AgJoinClause); with no `joins`, the query's single source is the base source. |
| `filter` | `AgStudioFilterDefinition` |  | Row-level predicate applied BEFORE any aggregation (SQL `WHERE` semantics). Leaves reference source fields on the joined plan. Affects both the result set and the denominator used by measures with `totalsScope: 'filtered'`. |
| `having` | `AgStudioFilterDefinition` |  | Post-aggregation predicate applied to cells, AFTER measures are computed (SQL `HAVING` semantics). Leaves must reference measure aliases or dimension fields (via `AgExprFieldRef`); row-level source fields are rejected. Cells the predicate rejects are dropped; the measure denominators are NOT recomputed. |
| `scope` | `AgScopeDefinition` |  | Dimension-member slice (MDX-inspired). Restricts the visible cells to the chosen members but - unlike `filter` - does NOT remove the excluded rows from the denominator used by measures with `totalsScope: 'unrestricted'`. Use for slicer/page-filter semantics where "% of total" should still divide by the full dataset. |
| `sort` | `AgStudioSortDefinition[]` |  | Result ordering (SQL `ORDER BY`). Applied AFTER aggregation, computed fields, and windows. Multiple entries form a tie-break chain in array order. Sort fields must reference output columns (measure aliases, dimension fields, or computed-field outputs). |
| `window` | `AgStudioWindowDefinition[]` |  | Window function evaluations (SQL `OVER (...)`). Each entry produces an output column computed over a partition of rows defined by `partitionBy` and ordered by `orderBy`. Evaluated after aggregation; outputs may be referenced by post-aggregation computed fields and `having`. |
| `limit` | `AgStudioLimitDefinition` |  | Hard cap on returned rows, with optional offset (SQL `LIMIT` / `OFFSET`). Applied as the final step after `sort`; pagination is therefore stable only when `sort` produces a total ordering. |
| `distinct` | `boolean` |  | Projection-mode row deduplication (SQL `SELECT DISTINCT`). In aggregation mode, use distinct-within-measure aggregations (`countd` etc.) instead. |
| `computedFields` | `AgStudioComputedFieldDefinition[]` |  | Evaluation schedule for computed expression fields. Topologically sorted - engines evaluate in array order. Each entry carries the expression in `AgStudioExpression` AST form and an explicit evaluation phase. Synthetic entries decompose cross-source expressions into single-source intermediates. Absent when the query has no expression fields. |
| `from` | `AgStudioQuery` |  | Derived-table composition (SQL `FROM (SELECT ...)`). Engines execute the inner query, materialise its result, and run the outer query over those rows. Each materialised level costs memory and latency, so engines should consider imposing a depth limit and rejecting deeper nesting with an error rather than silently truncating. |

### Computed Fields

`computedFields` is a topologically sorted evaluation schedule. Each entry carries an expression AST and an explicit evaluation phase:

- **`phase: 'pre-agg'`**: evaluated on source rows before grouping (SQL column expression)
- **`phase: 'post-agg'`**: evaluated on grouped output after measures are computed
- **`synthetic: true`**: internally generated intermediates; exclude from user-facing result columns

Engines evaluate entries in array order. Each entry's dependencies are satisfied by prior entries or source scans.

Expression nodes are either operations (`{ operator, inputs, options? }`) or leaves: field references (`{ field: AgStudioQueryField }`), values (`{ type, value }`, e.g. `{ type: 'number', value: 2 }`), or alias references (`{ ref: string }`).

## Field Identifiers

Every field reference in a query is an `AgStudioQueryField`:

| Property | Where it comes from | What to use it for |
| --- | --- | --- |
| `fieldId` | Source-qualified column ID from `getDataSources()`: `"sales.revenue"`. For computed fields (`sourceId: ''`), a bare identifier like `"pct_of_total"`. | Map to a backend column. Strip the source prefix for source fields; use as-is for computed fields. |
| `sourceId` | The `id` of the source the field belongs to: `"sales"`. | Pick which backend table or endpoint the query targets. |
| `key` | An opaque string Studio builds per-field per-query. | The column name in your result rows. Your result `rows[i][field.key]` must round-trip cleanly. **Treat `key` as opaque; do not parse it.** |

Measure fields also carry `aggregation` (e.g. `'sum'`, `'avg'`, `'count'`). Access it via `measure.field.aggregation`.

Properties available on the `AgStudioQueryField` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string` |  | Composite identity key and output column alias. Treat as opaque. When the wrapping definition (window / measure / dimension / projection entry) carries an `as`, that alias - not this `key` - becomes the output column identity. This `key` then only carries the field's *source* identity for resolution. |
| `sourceId` | `string` |  | Owning source ID; `''` for computed/inline fields. |
| `fieldId` | `string` |  | The underlying field identifier within the source. |
| `aggregation` | `AgAggregationFunction` |  | Aggregation function for measure fields; omitted for raw dimensions. |
| `determinant` | `string` |  | Additional identity component for fields that would otherwise collide on `(sourceId, fieldId, aggregation)` - e.g. window outputs with different orderings. |
| `sourceAlias` | `string` |  | Alias for self-joins - disambiguates multiple instances of the same underlying source in one query. |
| `expression` | `AgStudioExpression` |  | Inline expression AST - present for computed / inline fields (where `sourceId === ''`) and also the mechanism for granularity transforms such as `dateTrunc` and numeric bucketing. |
| `isMeasure` | `boolean` |  | Inline-field intent disambiguator. For inline fields, `true` marks a measure (lives under `AgStudioQuery.measures`), `false` marks a dimension (lives under `axes[].dimensions`). Validator rejects mismatched slot/intent pairs. |
| `dataType` | `AgDataType` |  | Optional data-type hint. Studio-produced fields set this from the schema; hand-constructed queries may omit it. |

## Filter Structure

The `filter` and `having` fields share the same recursive tree structure:

**`AgStudioFilterGroup`**: a boolean combinator (`'and'`, `'or'`, or `'not'`) wrapping child nodes:

```ts
{
    combinator: 'and',
    conditions: [/* AgStudioFilterCondition | AgStudioFilterGroup */],
}
```

**`AgStudioFilterCondition`**: a leaf predicate on a single field:

```ts
{
    field: { key, fieldId, sourceId },
    operator: 'equals',
    value: 'EMEA',
}
```

Walk the tree recursively: groups become parenthesised boolean expressions; conditions become backend predicates. See the [reference examples](https://www.ag-grid.com/studio/archive/3.0.0/angular/server-side-data-implementation/#reference-examples) for complete filter translation.

Properties available on the `AgStudioFilterCondition` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `field` | `AgStudioQueryField` |  | The field tested by the predicate. |
| `operator` | `"contains" \| "equals" \| "between" \| "endsWith" \| "startsWith" \| "notEqual" \| "greaterThan" \| "lessThan" \| "greaterThanOrEqual" \| "lessThanOrEqual" \| "inRelativeRange" \| "isNull" \| "isNotNull" \| "isUndefined" \| "isNotUndefined" \| "isBlank" \| "isNotBlank" \| "isNaN" \| "notContains" \| "isIn" \| "isTrue" \| "isFalse"` |  | The comparison applied between `field` and `value`. `rank` is excluded rank-style filtering is expressed via `AgDimensionDefinition.topN`. |
| `value` | `AgPrimitive \| AgPrimitive[] \| AgRelativeRangeFilterValue \| [AgPrimitive, AgPrimitive]` |  | `undefined` for `isNull` / `isNotNull` / `isTrue` / `isFalse`; a 2-element array for `between` (check `operator` to distinguish from `isIn`); an array for `isIn`; a relative-range descriptor for `inRelativeRange`; single primitive otherwise. |
| `options` | `{ crossFilter?: boolean; scopeDerived?: boolean; matchCase?: boolean; escapeChar?: string }` |  | Routing metadata carried to the engine and explain output. |

Properties available on the `AgStudioFilterGroup` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `combinator` | `"and" \| "or" \| "not"` |  | How `conditions` are combined. `not` negates the conjunction of its children (`NOT (c1 AND c2 AND ...)`); to negate a single predicate, wrap it in a `not` group with one child. |
| `conditions` | `AgStudioFilterDefinition[]` |  | Child filters - leaf conditions or nested groups. |

## Result Format

Return one `AgExecuteResult` per request, in the same order, discriminated on `dataShape`. Return `'rows'` (default) or `'columns'` depending on `options.shape`.

> **Note**
>
> The `dataShape` discriminator on each result must match the data you return. If you set `dataShape: 'columns'` but populate `rows` (or vice versa), widgets will render empty.

Both shapes carry an `AgResultMetadata` object. `rowCount` is the number of rows in the returned result. When your engine applies a `limit`, set `totalRowCount` to the pre-limit row count.

### Row Identity

Grid widgets need a stable, unique identifier per row. Studio injects a computed field into relevant queries to support this (using all the group fields). If your engine does not evaluate computed fields, you must provide your own unique row id as part of execution results. If your result rows already carry one, you can set `metadata.rowIdField` to its name.

```ts
return {
    dataShape: 'rows',
    rows: [
        { id: 'ord-1', 'sales.region': 'EMEA', 'sales.revenue': 123456 },
        { id: 'ord-2', 'sales.region': 'APAC', 'sales.revenue': 234567 },
    ],
    metadata: { rowCount: 2, rowIdField: 'id' },
};
```

### Rows Format

The default shape. Return each row as an object keyed by field identifier.

```ts
return {
    dataShape: 'rows',
    rows: [
        { 'sales.region': 'EMEA', 'sales.revenue': 123456 },
        { 'sales.region': 'APAC', 'sales.revenue': 234567 },
    ],
    metadata: { rowCount: 2, totalRowCount: 5432 },
};
```

### Columns Format

Requested through `options.shape`. Return one array of values per field, all the same length.

```ts
const columns = new Map<string, ReadonlyArray<AgPrimitive | null>>();
columns.set('sales.region', ['EMEA', 'APAC']);
columns.set('sales.revenue', [123456, 234567]);

return {
    dataShape: 'columns',
    columns,
    metadata: { rowCount: 2 },
};
```

Properties available on the `AgExecuteRequest&lt;TShape extends AgResultShape = AgResultShape&gt;` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `query` | `AgStudioQuery` |  | The query to execute against the data source. |
| `options` | `AgRequestOptions<TShape>` |  | Per-request options controlling result shape, cancellation, and validation. |
| `info` | `AgEngineCallInfo` |  | Advisory metadata for logging, tracing, and batch coalescing. |

Properties available on the `AgRowsResult` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `metadata` | `AgResultMetadata` |  | Row count and optional pre-limit total. |
| `dataShape` | `"rows"` |  | Discriminator - always `'rows'` for this shape. |
| `rows` | `Record<string, unknown>[]` |  | Result rows keyed by output field alias. |

Properties available on the `AgColumnsResult` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `metadata` | `AgResultMetadata` |  | Row count and optional pre-limit total. |
| `dataShape` | `"columns"` |  | Discriminator - always `'columns'` for this shape. |
| `columns` | `AgColumnsMap` |  | Column arrays keyed by output field alias. |

## Sources and Joins

When a widget pulls fields from multiple related sources (declared via [Relationships](https://www.ag-grid.com/studio/archive/3.0.0/angular/data-modelling/#relationships)), Studio produces a single `AgStudioQuery` with `joins` populated. Your `execute()` receives one query that spans multiple sources. Each field's `sourceId` and `fieldId` identify which backend table it belongs to, so your translator can resolve every reference.

If your backend does not support joins, throw a descriptive error when a query contains `joins`.

## Batching and Cancellation

Studio calls `execute(...requests)` with every request in the current render cycle. Each request carries an `info` object:

- **`batchId`:** All requests in the same `execute()` call share a `batchId`. Use it to coalesce backend round-trips (e.g. one combined request per batch). When absent, treat each request independently.
- **`queryId`:** Distinguishes independent queries from the same widget (e.g. `'rows'` vs `'grandTotal'`). Studio pairs it with `widgetId` to decide which in-flight query a new one replaces, so two queries the same widget runs under different `queryId`s never cancel each other.

`info` is advisory. Its values are not stable across re-renders, so mint your own identifiers if you need to tie a request to a backend call in your logs.

### Cancelling Superseded Queries

Each request carries an optional `options.signal: AbortSignal`. Studio aborts it in two cases:

- **A newer query replaces this one.** The same widget asking again under the same `queryId` supersedes the earlier request. This happens whenever a filter changes before the previous queries have returned.
- **The request exceeds `dataOptions.queryTimeout`,** 60 seconds by default. Set it to `0` or `Infinity` to disable the timeout.

Check the signal before starting work, then hand it to your backend call:

```ts
private async runOne(request: AgExecuteRequest<AgResultShape>): Promise<AgExecuteResult> {
    const { query, options } = request;
    if (options?.signal?.aborted) {
        return { dataShape: 'rows', rows: [], metadata: { rowCount: 0 } };
    }
    const res = await fetch(this.toBackendUrl(query), { signal: options?.signal });
    const rows = await res.json();
    return { dataShape: 'rows', rows, metadata: { rowCount: rows.length } };
}
```

A query can be superseded before the batch it belongs to reaches your engine, so `execute()` is sometimes handed a request whose signal is already aborted.

Honouring the signal is optional. Studio replaces the result of any request it aborted with an empty one, so a late response from an engine that ignored the signal never reaches a widget. What ignoring it costs is backend work nobody reads, which adds up quickly when a user steps through a date picker or drags a slider and every move issues a fresh query.

## Errors

Requests in one `execute()` call come from unrelated widgets, so resolve each of them on its own. Rejecting the returned promise for a problem confined to a single request blanks every widget in the batch: Studio resolves all of them with empty results and reports one failure. Return an empty result for the request that failed instead, and record the reason in your own logs.

Throw only when the failure covers the whole batch, such as an unreachable backend. Throw an `Error`, because Studio reads `message` off it to report the failure.

```ts
async execute(...requests: AgExecuteRequest<AgResultShape>[]): Promise<AgExecuteResult[]> {
    try {
        return await this.runQueries(requests);
    } catch (error) {
        throw new Error('Backend query failed', { cause: error });
    }
}
```

## Pivot and Hierarchy Queries

Some widgets need cube-aggregated data that `execute()` can't express: multiple independent grouping axes with dense cell coverage, and subtotal placement within a hierarchy. For these, Studio calls an optional second method, `executeCube(...requests: AgCubeResolvedExecuteRequest[])`, and expects one `AgCubeResult` per request - axis tuples and a sparse cell store, not flat rows.

`executeCube` is required for:

- **Pivot Grid** widgets, always.
- **Treemap** and **Sunburst** widgets, always - hierarchy subtotal placement (`includeSubtotals`) has no flat equivalent.
- Any chart configured with a **legend field** - legend grouping is a pivot query under the hood.

A chart's own group-by fields, without a legend field, never need `executeCube`: they're a flat `execute()` query regardless of how many are configured.

### Omitting executeCube

`executeCube` is optional. If you omit it:

- Pivot Grid, Treemap, and Sunburst widget types are omitted from the widget picker entirely, with a validation warning explaining why.
- A chart configured with a legend field still renders, but without the legend grouping - one ungrouped series, plus a validation warning.

Properties available on the `AgCubeResolvedExecuteRequest` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `query` | `AgCubeResolvedStudioQuery` |  | AgCubeResolvedStudioQuery |
| `options` | `AgCubeExecuteOptions` |  | AgCubeExecuteOptions |
| `info` | `AgEngineCallInfo` |  | AgEngineCallInfo |

Properties available on the `AgCubeResult&lt;TValue = AgPrimitive&gt;` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `dataShape` | `"cube"` |  | "cube" |
| `axes` | `AgResultAxis[]` |  | Axes in query order; length ≥ 2. |
| `measures` | `AgResultMeasure[]` |  | Ordered measure descriptors; each key matches a cell record key. |
| `cells` | `AgCubeCells` |  | Sparse N-D cell store keyed by axis-tuple index vector. |
| `metadata` | `AgResultMetadata` |  | Row count and optional pre-limit total. |

## Reference Examples

> **Note**
>
> These are reference implementations for learning purposes. They are not production-ready integrations and will not cover every query feature. Use them as starting points for your own engine.

### ClickHouse over HTTP

Translates `AgStudioQuery` to ClickHouse SQL, posts it over HTTP, and returns the response. The dashboard queries ClickHouse's [uk_price_paid](https://clickhouse.com/docs/getting-started/example-datasets/uk-price-paid) dataset (~28M rows) without downloading any of it.

#### ClickHouse Server-Side

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: ClickHouse Server-Side](https://www.ag-grid.com/studio/archive/3.0.0/examples/server-side-data-implementation/clickhouse-serverside/angular/)

Contains HM Land Registry data © Crown copyright and database right 2021. This data is licensed under the Open Government Licence v3.0. ([Source](https://www.gov.uk/government/statistical-data-sets/price-paid-data-downloads), [Fields](https://www.gov.uk/guidance/about-the-price-paid-data))

### ClickHouse Server-Side Pivot

Adds `executeCube` (see [Pivot and Hierarchy Queries](https://www.ag-grid.com/studio/archive/3.0.0/angular/server-side-data-implementation/#pivot-and-hierarchy-queries)) to the ClickHouse engine above, so it can also serve Pivot Grid, Treemap, and Sunburst widgets. The implementation groups by both axes' dimensions in a single query, then reshapes the flat result into the cube shape.

#### ClickHouse Server-Side Pivot

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: ClickHouse Server-Side Pivot](https://www.ag-grid.com/studio/archive/3.0.0/examples/server-side-data-implementation/clickhouse-serverside-pivot/angular/)

Contains HM Land Registry data © Crown copyright and database right 2021. This data is licensed under the Open Government Licence v3.0. ([Source](https://www.gov.uk/government/statistical-data-sets/price-paid-data-downloads), [Fields](https://www.gov.uk/guidance/about-the-price-paid-data))

### REST API: World Bank Countries

Queries the World Bank Open Data API. Supported predicates are sent as URL parameters; remaining filters, aggregation, and sorting are applied locally.

#### World Bank Countries Server-Side

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: World Bank Countries Server-Side](https://www.ag-grid.com/studio/archive/3.0.0/examples/server-side-data-implementation/restcountries-serverside/angular/)

Contains data from The World Bank: Countries API, licensed under [Creative Commons Attribution 4.0 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/). ([Terms of Use](https://www.worldbank.org/en/about/legal/terms-of-use-for-datasets))
