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:
-
Ungrouped aggregations were broken.
COUNTwith nogroup_byreturned 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. -
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 omitcolumnsfrom 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
- !503 (merged) (original GraphFormatter, omitted columns)
- !490 (merged) (ADR 004)