feat(formatters): add columns descriptor and fix ungrouped aggregation formatting

What does this MR do and why?

Two problems with the unified response format for aggregation queries:

  1. Ungrouped aggregations were broken. COUNT with no group_by returned an empty response — the SQL produced a valid row but the formatter dropped it because there was no entity type/ID to create a node from.

  2. No way to identify aggregate columns. Consumers couldn't distinguish computed values (like total, avg_size) from entity properties without inspecting the ontology. Our decision to omit columns from the response (!503 (merged)) made ungrouped aggs worse — the synthetic node carried values with no metadata about what they meant.

What changed

columns field on GraphResponse — present for all aggregation queries (grouped and ungrouped). Each entry is a ColumnDescriptor with name, function, and optional target/property/value.

For ungrouped aggregations, values live directly on the column descriptors and nodes is empty:

{
  "query_type": "aggregation",
  "nodes": [],
  "edges": [],
  "columns": [
    {"name": "total", "function": "count", "target": "u", "value": 42}
  ]
}

For grouped aggregations, columns describes the computed properties but values stay on the entity nodes (no value field):

{
  "query_type": "aggregation",
  "nodes": [
    {"type": "User", "id": 1, "username": "alice", "group_count": 2}
  ],
  "edges": [],
  "columns": [
    {"name": "group_count", "function": "count", "target": "g"}
  ]
}

Mixed aggregation rejection — the compiler now rejects queries that mix grouped and ungrouped aggregations in the same request, since the formatter handles them differently.

References

Edited by Michael Usachenko

Merge request reports

Loading