= {
id: column.id,
@@ -73,6 +95,15 @@ function toColumnDef(
header: column.header ?? column.id,
enableSorting: true,
enableColumnFilter: true,
+ // Every numeric column aggregates, mirroring pretable's
+ // `applyGroupAggregates` ("avg" there, `mean` here — the same fold): the
+ // grouping scripts deliberately cost the aggregation stage, and a
+ // comparator that groups without aggregating measures less work than the
+ // grid it is compared against. Inert until `grouping` state is non-empty,
+ // so no other scenario moves.
+ ...(sampleRow !== undefined && typeof sampleRow[column.id] === "number"
+ ? { aggregationFn: "mean" as const }
+ : {}),
// TanStack's default filterFn is "auto" which maps to includesString
// for strings. filter-metadata uses equals semantics in the bench
// plan (see interaction-plan.ts METADATA_FILTER), so set
@@ -166,8 +197,10 @@ export function TanstackAdapter({
const interactionMode = interactionPlan?.mode ?? null;
const columns = useMemo(
() =>
- dataset.columns.map((c) => toColumnDef(c, scriptName, interactionMode)),
- [dataset.columns, scriptName, interactionMode],
+ dataset.columns.map((c) =>
+ toColumnDef(c, scriptName, interactionMode, dataset.rows[0]),
+ ),
+ [dataset.columns, scriptName, interactionMode, dataset.rows],
);
// The scenario's `pinned_left` columns, in dataset order. Empty for every
@@ -178,6 +211,17 @@ export function TanstackAdapter({
[dataset.columns],
);
+ // The `group` script's trigger IS the plan arriving (bench-app sets the
+ // interaction-plan override inside the measured window), so grouping is
+ // derived state: plan present with rowGroups -> grouped, otherwise not.
+ // `expanded: true` keeps every group open, which is the state the plan's
+ // `resultRowCount` arithmetic (leaves + one group row per key) describes.
+ const grouping = useMemo(
+ () =>
+ interactionPlan?.mode === "group" ? [...interactionPlan.rowGroups] : [],
+ [interactionPlan],
+ );
+
const table = useTable({
features: tanstackFeatures,
data,
@@ -186,7 +230,12 @@ export function TanstackAdapter({
// reason `sorting` is: `runKey` remounts the adapter per run and the pinned
// set is derived from the dataset, so it must follow a dataset swap rather
// than latch whatever the first render saw.
- state: { sorting, columnPinning: { start: pinnedColumnIds, end: [] } },
+ state: {
+ sorting,
+ columnPinning: { start: pinnedColumnIds, end: [] },
+ grouping,
+ expanded: true,
+ },
onSortingChange: setSorting,
getRowId: (row) => String(row.id),
});
@@ -384,6 +433,7 @@ export function TanstackAdapter({
>
{virtualRows.map((vr) => {
const row = rows[vr.index];
+ const isGroupRow = row.getIsGrouped();
return (
+ {grouped
+ ? `${String(cell.getValue() ?? "")} (${row.subRows.length})`
+ : aggregated
+ ? String(cell.getValue() ?? "")
+ : ""}
+
+ );
+ }
// Read off TanStack rather than off the dataset: the feature
// owns the state, and `getStart("start")` is the running sum
// of the pinned widths before this column. See
diff --git a/packages/bench-runner/src/__tests__/bench-runner.test.ts b/packages/bench-runner/src/__tests__/bench-runner.test.ts
index 9cbdf642..07451b6f 100644
--- a/packages/bench-runner/src/__tests__/bench-runner.test.ts
+++ b/packages/bench-runner/src/__tests__/bench-runner.test.ts
@@ -592,24 +592,60 @@ describe("bench-runner contract", () => {
).toEqual({ ok: true });
}
- // ...and rejected, with a reason, for every other adapter. Row grouping
- // is AG Grid Enterprise / MUI X Premium and absent from TanStack, so
- // these numbers are absolute, never comparative.
+ // `group` is COMPARATIVE against TanStack: v9 ships the grouping row
+ // model, aggregation and expansion in the free package, and the tanstack
+ // adapter registers them with aggregation parity. AG Grid and MUI stay
+ // excluded on tier (Enterprise / Premium respectively).
+ expect(
+ validateSupportedP0aRequest({
+ ...baseRequest,
+ adapterId: "tanstack",
+ scenarioId: "S2",
+ scriptName: "group",
+ }),
+ ).toEqual({ ok: true });
+ for (const adapterId of ["ag-grid", "mui"] as const) {
+ expect(
+ validateSupportedP0aRequest({
+ ...baseRequest,
+ adapterId,
+ scenarioId: "S2",
+ scriptName: "group",
+ }),
+ ).toEqual({
+ ok: false,
+ reason: expect.stringContaining("Enterprise"),
+ });
+ }
+ // `group-expand` stays pretable-only for a PLUMBING reason (bench-app's
+ // setup/trigger machinery), which the tanstack rejection must state —
+ // repeating the stale "absent from TanStack" claim here is exactly what
+ // this test previously did.
+ expect(
+ validateSupportedP0aRequest({
+ ...baseRequest,
+ adapterId: "tanstack",
+ scenarioId: "S2",
+ scriptName: "group-expand",
+ }),
+ ).toEqual({
+ ok: false,
+ reason: expect.stringContaining("plumbing"),
+ });
+ for (const adapterId of ["ag-grid", "mui"] as const) {
+ expect(
+ validateSupportedP0aRequest({
+ ...baseRequest,
+ adapterId,
+ scenarioId: "S2",
+ scriptName: "group-expand",
+ }),
+ ).toEqual({
+ ok: false,
+ reason: expect.stringContaining("adapter"),
+ });
+ }
for (const adapterId of ["ag-grid", "tanstack", "mui"] as const) {
- for (const scriptName of ["group", "group-expand"] as const) {
- expect(
- validateSupportedP0aRequest({
- ...baseRequest,
- adapterId,
- scenarioId: "S2",
- scriptName,
- }),
- ).toEqual({
- ok: false,
- reason: expect.stringContaining("adapter"),
- });
- }
-
for (const scriptName of [
"group-updates",
"group-updates-stable-keys",
diff --git a/packages/bench-runner/src/index.ts b/packages/bench-runner/src/index.ts
index 828e8c44..90756d89 100644
--- a/packages/bench-runner/src/index.ts
+++ b/packages/bench-runner/src/index.ts
@@ -485,14 +485,31 @@ export function validateSupportedP0aRequest(
}
if (groupingScripts.includes(request.scriptName)) {
- // Row grouping is AG Grid Enterprise and MUI X Premium; TanStack Table
- // ships no row-grouping row model of its own. This repo uses only the
- // free tiers, so there is nothing to compare against and these numbers
- // are ABSOLUTE + a regression tripwire, never a competitive claim.
- if (request.adapterId !== "pretable") {
+ // Row grouping is a PAID tier in AG Grid (Enterprise: `RowGrouping` is in
+ // `EnterpriseModuleName`) and MUI (Premium: no `rowGroupingModel` in the
+ // installed Community package), and this repo uses only the free tiers —
+ // so those two are excluded on capability. TanStack Table v9 is NOT: the
+ // installed 9.1.2 ships `createGroupedRowModel`, `rowAggregationFeature`
+ // and `createExpandedRowModel` in the free package (an earlier comment
+ // here claimed otherwise and was stale), and the tanstack adapter now
+ // registers them, so `group` reads comparatively against it.
+ //
+ // `group-expand` and the grouped streaming scripts stay pretable-only for
+ // a PLUMBING reason, not a capability one: their setup and trigger run
+ // through pretable-specific machinery in bench-app.tsx
+ // (`waitForGroupedRowModel`, `grid.rowModel.setGroupExpanded`, the
+ // streaming update path), which no comparator adapter exposes yet. Until
+ // that exists their numbers are ABSOLUTE + a regression tripwire, never a
+ // competitive claim.
+ const groupCapableAdapters: readonly BenchAdapterId[] =
+ request.scriptName === "group" ? ["pretable", "tanstack"] : ["pretable"];
+ if (!groupCapableAdapters.includes(request.adapterId)) {
return {
ok: false,
- reason: `Unsupported adapter for ${request.scriptName}: ${request.adapterId} (row grouping is AG Grid Enterprise / MUI X Premium and absent from TanStack Table; pretable-only, not a comparative claim)`,
+ reason:
+ request.adapterId === "tanstack"
+ ? `Unsupported adapter for ${request.scriptName}: tanstack (TanStack v9 ships the grouping row model, but this script's setup/trigger plumbing in bench-app is pretable-only; see the gate comment)`
+ : `Unsupported adapter for ${request.scriptName}: ${request.adapterId} (row grouping is AG Grid Enterprise / MUI X Premium; free tiers only in this matrix)`,
};
}
}