[LIQ] Implement intra-aggregate order by (5: documentation)

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
This commit is contained in:
Matouš Jan Fialka
2026-03-11 08:52:33 +01:00
parent 7eca1562e0
commit bc5c3c8bda
3 changed files with 105 additions and 8 deletions
+35 -7
View File
@@ -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
+8 -1
View File
@@ -23,6 +23,13 @@ General syntax:
select <expression>
]]
Aggregate functions support an optional intra-aggregate `order by` and/or `filter` clause:
<aggregate>(<expression> [order by <expr> [asc|desc] [nulls {first|last}], ...])
<aggregate>(<expression> ...) filter(where <condition>)
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:
@@ -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.