Elyra
Elyra The coding agent e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Elyra

Grouping

Multi-level grouping with drag-to-group, collapsible groups, per-level aggregates, and drill-into detail rows — all computed in the database.

Enabling grouping

const options = {
  /* ... */
  grouping: { aggregates: [{ field: 'revenue', fn: 'sum', as: 'sum_revenue' }] },
};

Livewire: set public bool $groupable = true; and (optionally) return aggregates() from the component.

Drag-to-group

When grouping is enabled a group panel appears above the grid. Drag a column header onto it to group by that column; drag several to nest levels. Each grouped column shows as a chip with a ✕ to remove it.

Programmatic (client core):

grid.groupBy('region');
grid.groupBy('category');   // second level
grid.ungroup('category');
grid.setGroups(['region']);

How it renders

Grouping produces a hierarchical, indented list of group header rows with a count and per-group aggregates, each collapsible via a chevron:

▾ Region: Nord — 1,666,667 rows · sum_revenue 3,350,158,788
    ▾ Category: Elektronikk — 833,333 rows · sum_revenue 1,675,214,197
    ▾ Category: Bok — 833,334 rows · sum_revenue 1,674,751,357
▾ Region: Sør — 1,666,667 rows · ...
grid.toggleGroup(path);       // collapse / expand a group node
grid.isGroupCollapsed(path);

How it works on the server

For a cross-tab presentation of the same machinery — dimensions on two axes, with margins — see Pivot.

Send groupView: { skip, take } to page the outermost grouping level; every descendant of a selected outer group comes with it, so the tree stays whole, and the response carries groupTotal for building a pager.

Group rows are capped per level by GridLimits::maxGroupRows (10 000). Grouping a high-cardinality column is expensive rather than abusive, so the cap clamps and the response sets groupsTruncated: true — check it and tell the user the grouping is partial, instead of showing a truncated result as complete.

Grouping a column requires it to be filterable — grouping emits the column's distinct values, which is exactly what filterable: false withholds. Levels are capped by GridLimits::maxGroupLevels (5), because the server runs one query per level; raise it under datagrid.limits if you need deeper nesting.

The server runs one GROUP BY query per level (GROUP BY g1, then GROUP BY g1, g2, ...), so each level's aggregates are computed directly over that level's rows. This is correct for non-additive functions (avg, median, percentile, countDistinct) — not just additive sum/count — and The deepest query also fixes the hierarchical ordering of the emitted headers.

GROUP BY ROLLUP(…) would compute the same numbers in one query — it aggregates base rows per grouping set, so its subtotals are correct too — but it is not used here, for three reasons: support differs across the drivers (SQLite has none), its subtotal rows are distinguished by NULL in the grouping column, which is ambiguous when the data itself contains NULL, and its output ordering is not guaranteed to be hierarchical.

// The client sends group[] with per-level aggregates; the server returns
// GridResponse.groups: [{ field, value, count, level, path, aggregates }]

Aggregate functions

sum, avg, min, max, count, countDistinct, median, percentile (with p). median/percentile require the ElyraSQL driver.

grouping: {
  aggregates: [
    { field: 'revenue', fn: 'sum', as: 'sum_revenue' },
    { field: 'revenue', fn: 'percentile', p: 0.95, as: 'p95' },
  ],
}

Per-level aggregates are correct for every function, including non-additive ones like group-level avg and percentile.

Detail rows (drill into a group)

Enable detailRows to let users expand an innermost group and reveal its actual data rows (lazily fetched, filtered by the group path):

// Vue / Svelte
grouping: { detailRows: true, detailLimit: 200, aggregates: [/* ... */] }
// Livewire
public bool $groupDetailRows = true;
public int $groupDetailLimit = 200;

The leaf group's chevron toggles its detail rows; expanding fetches up to detailLimit rows (WHERE groupField = value AND <current filter>, no grouping) and renders them indented under the header. Non-leaf group chevrons still collapse their subgroups.

Footer aggregates (whole result)

Independent of grouping, footer aggregates run over the entire filtered result and are correct for any function. They are cached per filter.

const options = {
  aggregates: [
    { field: 'revenue', fn: 'sum', as: 'sum_revenue' },
    { field: 'margin', fn: 'avg', as: 'avg_margin' },
  ],
};