v3.1.0
What's Changed
New aggregate
COLLECT_OBJECTforGROUP BYqueries — collects rows in
each group into an array of structured objects via an inner mini-SELECT
with optionalORDER BY. Plugs into the existing aggregate pipeline
alongsideCOUNT/SUM/GROUP_CONCAT; no breaking changes to other
aggregates or to the public API.
Added
COLLECT_OBJECT(...)aggregate function. PerGROUP BYgroup, returns
array<array<string, mixed>>— each row in the group projected through an
inner SELECT (with field aliases) and optionally sorted by an inner
ORDER BY(ASC/DESC, multi-key). Inner items accept scalar functions
(CONCAT,ROUND,IF,COALESCE,UPPER,LOWER, …), arithmetic
expressions (price * 1.21 AS price_with_vat), and aliases.
FQL grammar:
COLLECT_OBJECT(expr [AS alias], … [ORDER BY expr [ASC|DESC], …]).FQL\Query\Builder\CollectObjectfluent builder. Tiny chainable DSL —
select(string ...$fields)(full FQL expression syntax, accepts inline
"expr AS alias"and comma-separated lists viaFieldListSplitter),
as(string $alias)for the idiomatic main-Query-style aliasing, plus the
orderBy/asc/desctriple inherited fromSortable. Used as
$query->collectObject((new CollectObject())->select('id')->as('i')->orderBy('name'))->as('alias').FQL\Sql\Ast\Expression\WholeRowNode. New AST node that the
ExpressionEvaluatorresolves to the entire source$item. Used as the
spec.expressionofCOLLECT_OBJECT, so the standard
Stream::applyGroupingpath —$evaluator->evaluate(spec.expression, $item)
followed by$class::accumulate($acc, $value)— automatically delivers the
whole row as the aggregate's value, with no special case in the Stream
pipeline.CollectObjectis a plainAggregateFunctionand finalises by
running a one-offQueryover aResultStreamProviderof the collected
rows — full SELECT/ORDER BY pipeline reuse, no parallel evaluator state.
Changed
ExpressionEvaluatorlearned to evaluateWholeRowNode(returns the
source$item).Streamaggregate grouping path is unchanged.Traits\Sortablereturn types changed fromQuerytostatic. Same
for the correspondingInterface\Querysignatures (orderBy,sortBy,
asc,desc). Existing fluent chains onQuerykeep their behaviour
(Query continues to return itself); the change unlocksuse Sortable;in
builders that aren't fullQueryobjects —Builder\CollectObjectnow
inherits the trio instead of duplicating it.OrderByClauseParser::parseItem()is now public, enablingORDER BYitem
reuse inside expression contexts (used byCOLLECT_OBJECT(... ORDER BY …)).ExpressionParsergained a lazysetOrderByParser()setter, wired in
Parser::create()(full FQL statements) and
Sql\Provider::freshExpressionParser()(fragment parsers used by the
fluent API).ExpressionCompilerlearned to render
CollectObjectExpressionNodeso SELECT round-trip (compile → string →
re-parse) works for FQL-string inputs.
Notes
- Empty groups produce no output row (consistent with the other
aggregates). ORDER BYinsideCOLLECT_OBJECTrecognises projected aliases —
finalisation runs as a fullQueryover the accumulated rows, so the
ordering clause sees both source columns and the aliases declared inside
COLLECT_OBJECT(...). Standard SQL semantics.- Null values propagate into the produced objects (unlike
SUM/AVG,
which skip them). - Stable sort preserves accumulation order on ties.
- Aggregates inside
COLLECT_OBJECTare supported but rarely useful —
inner aggregates collapse the accumulated rows to a single output object,
soCOLLECT_OBJECT(SUM(x))yields an array of length 1. Prefer scalar
aggregates at the outer level alongsideCOLLECT_OBJECTfor per-group
summary numbers. - Out of MVP scope (rejected with a clear exception):
DISTINCT,
LIMIT,WHEREinsideCOLLECT_OBJECT, and nestedCOLLECT_OBJECT.
Full Changelog: v3.0.2...v3.1.0