From bc5c3c8bdaab021a09b3f840de0d2e06407f2732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Jan=20Fialka?= Date: Wed, 11 Mar 2026 08:52:33 +0100 Subject: [PATCH] [LIQ] Implement intra-aggregate `order by` (5: documentation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matouš Jan Fialka --- libraries/Library/Std/APIs/Aggregate.md | 42 ++++++++++--- website/Space Lua/Lua Integrated Query.md | 9 ++- .../Lua Integrated Query/Aggregating.md | 62 +++++++++++++++++++ 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/libraries/Library/Std/APIs/Aggregate.md b/libraries/Library/Std/APIs/Aggregate.md index 33ca0512..999936fc 100644 --- a/libraries/Library/Std/APIs/Aggregate.md +++ b/libraries/Library/Std/APIs/Aggregate.md @@ -22,6 +22,16 @@ Optional keys: * `description`: description of the aggregate * `finish`: `function(state)` that transforms the final state into the result +### Extra arguments + +Aggregate functions can accept additional arguments beyond the first value expression. When called as `my_agg(expr, arg2, arg3)`, the extra arguments (`arg2`, `arg3`) are evaluated once before iteration and forwarded to all three callbacks: + +* `initialize(ctx, ...extraArgs)` — receives extra args after the context table +* `iterate(state, value, ctx, ...extraArgs)` — receives extra args after the context table +* `finish(state, ctx, ...extraArgs)` — receives extra args after the context table + +This allows parameterized aggregates, for example a separator argument for string concatenation. + ## aggregate.update(spec) Updates an existing aggregate definition. Same keys as `aggregate.define`. Only the provided keys are overwritten. @@ -30,25 +40,43 @@ Updates an existing aggregate definition. Same keys as `aggregate.define`. Only ## Define a custom aggregate -Define a custom aggregate `concat` that concatenates strings. +Define a custom aggregate `concat` that concatenates strings with a configurable separator (defaulting to `", "`): ```lua aggregate.define { name = 'concat', - initialize = function() - return '' + initialize = function(ctx, sep) + return { sep = sep or ', ', parts = {} } end, - iterate = function(state, value) - if state == '' then - return tostring(value) + iterate = function(state, value, ctx, sep) + if value ~= nil then + state.parts[#state.parts + 1] = tostring(value) end - return state .. ', ' .. tostring(value) + return state + end, + + finish = function(state) + return table.concat(state.parts, state.sep) end, } ``` +Usage in a query: + +```lua +query [[ + from p = data + group by p.category + select { + cat = key, + names = concat(p.name), + names_dash = concat(p.name, " - ") + } +]] +``` + ## Update an existing aggregate ```lua diff --git a/website/Space Lua/Lua Integrated Query.md b/website/Space Lua/Lua Integrated Query.md index 2a36893d..841c6bff 100644 --- a/website/Space Lua/Lua Integrated Query.md +++ b/website/Space Lua/Lua Integrated Query.md @@ -23,6 +23,13 @@ General syntax: select ]] +Aggregate functions support an optional intra-aggregate `order by` and/or `filter` clause: + + ( [order by [asc|desc] [nulls {first|last}], ...]) + ( ...) filter(where ) + +These can be combined. See [[Space Lua/Lua Integrated Query/Aggregating]] for details. + LIQ operates on any Lua collection. For instance, to sort a list of numbers in descending order: @@ -244,7 +251,7 @@ ${query[[from {1, 2, 3, 4, 5} limit 3, 2]]} ## `select` The `select` clause allows you to transform each item in the result set. If omitted, it defaults to returning the item itself. -When used with `group by`, aggregate functions like `sum()`, `count()`, `min()`, `max()`, and `avg()` can be used in the `select` expression to compute values across each group. See [[Space Lua/Lua Integrated Query/Aggregating]] for details. +When used with `group by`, aggregate functions like `sum()`, `count()`, `min()`, `max()`, `avg()`, and `array_agg()` can be used in the `select` expression to compute values across each group. Aggregates also support intra-aggregate `order by` to control the order in which values are processed, and `filter(where ...)` to restrict which rows contribute. See [[Space Lua/Lua Integrated Query/Aggregating]] for details. Some examples: diff --git a/website/Space Lua/Lua Integrated Query/Aggregating.md b/website/Space Lua/Lua Integrated Query/Aggregating.md index 8c62c23f..2f79584d 100644 --- a/website/Space Lua/Lua Integrated Query/Aggregating.md +++ b/website/Space Lua/Lua Integrated Query/Aggregating.md @@ -103,6 +103,68 @@ ${query [[ The filter clause works with all aggregate functions: `count`, `sum`, `min`, `max`, `avg`, `array_agg`, and custom aggregates. When no rows match the filter condition, aggregates return their identity value: `0` for `count` and `sum`, `nil` for `min`, `max`, and `avg`, and an empty table `{}` for `array_agg`. +## Intra-aggregate `order by` +Aggregate functions can include an `order by` clause **inside** the function call to control the order in which values are processed. + +For commutative aggregates like `sum`, `count`, `min`, `max`, and `avg`, the intra-aggregate `order by` has no effect on the result because the value is the same regardless of iteration order. It is only meaningful for order-dependent aggregates like `array_agg`. + +### Basic example + +Collect page names sorted alphabetically within each group: + +${query [[ + from + p = index.tag 'page' + group by + p.tags[1] + select { + tag = key, + names_asc = array_agg(p.name order by p.name asc), + names_desc = array_agg(p.name order by p.name desc) + } + order by + tag + limit + 5 +]]} + +### Combined with `filter(where ...)` +The `order by` and `filter` clauses can be used together. The filter is applied first (excluding rows), then the remaining rows are sorted before iteration: + +${query [[ + from + p = index.tag 'page' + group by + p.tags[1] + select { + tag = key, + big_by_size = array_agg(p.name order by p.size desc) filter(where p.size > 5) + } + order by + tag + limit + 5 +]]} + +### Null handling +The `nulls first` and `nulls last` modifiers work inside intra-aggregate `order by` the same way they do in the query-level `order by`: + +```lua +query [[ + from + p = data + group + by p.category + select { + cat = key, + items = array_agg(p.name + order by + p.priority asc nulls last + ) + } +]] +``` + ## Field access after grouping Non-aggregated field references, such as `name` in `select`, refer to the first item in the group, matching common SQL and MySQL semantics.