Merge pull request #1878 from mjf/liq-implement-offset

This commit is contained in:
Zef Hemel
2026-03-11 19:44:04 +01:00
committed by GitHub
8 changed files with 341 additions and 56 deletions
+6
View File
@@ -305,6 +305,7 @@ export type LuaQueryClause =
| LuaFromClause
| LuaWhereClause
| LuaLimitClause
| LuaOffsetClause
| LuaOrderByClause
| LuaSelectClause
| LuaGroupByClause
@@ -327,6 +328,11 @@ export type LuaLimitClause = {
offset?: LuaExpression;
} & ASTContext;
export type LuaOffsetClause = {
type: "Offset";
offset: LuaExpression;
} & ASTContext;
export type LuaOrderByClause = {
type: "OrderBy";
orderBy: LuaOrderBy[];
+9
View File
@@ -883,6 +883,15 @@ export function evalExpression(
}
break;
}
case "Offset": {
const offsetVal = await evalExpression(
clause.offset,
env,
sf,
);
query.offset = Number(offsetVal);
break;
}
case "GroupBy": {
query.groupBy = clause.expressions;
break;
+2
View File
@@ -96,6 +96,7 @@ QueryClause {
OrderByClause |
SelectClause |
LimitClause |
OffsetClause |
GroupByClause |
HavingClause
}
@@ -103,6 +104,7 @@ QueryClause {
FromClause { ckw<"from"> (Name "=")? exp }
WhereClause { ckw<"where"> exp }
LimitClause { ckw<"limit"> exp ("," exp)? }
OffsetClause { ckw<"offset"> exp }
OrderByClause { ckw<"order"> ckw<"by"> list<OrderBy> }
OrderBy { exp (ckw<"desc"> | ckw<"asc"> | kw<"using"> (Name | kw<"function"> FuncBody))? (ckw<"nulls"> (ckw<"first"> | ckw<"last">))? }
SelectClause { ckw<"select"> exp }
File diff suppressed because one or more lines are too long
+15 -14
View File
@@ -22,17 +22,18 @@ export const Comment = 1,
OrderByClause = 77,
SelectClause = 78,
LimitClause = 80,
GroupByClause = 82,
HavingClause = 84,
IfStatement = 91,
ForStatement = 96,
ForNumeric = 98,
ForGeneric = 99,
NameList = 100,
ExpList = 102,
FuncName = 104,
VarList = 108,
AttNameList = 110,
AttName = 111,
Attrib = 112,
ReturnStatement = 113;
OffsetClause = 82,
GroupByClause = 84,
HavingClause = 86,
IfStatement = 93,
ForStatement = 98,
ForNumeric = 100,
ForGeneric = 101,
NameList = 102,
ExpList = 104,
FuncName = 106,
VarList = 110,
AttNameList = 112,
AttName = 113,
Attrib = 114,
ReturnStatement = 115;
+8 -1
View File
@@ -36,7 +36,7 @@ const luaStyleTags = styleTags({
CompareOp: t.operator,
"true false": t.bool,
Comment: t.lineComment,
"return break goto do end while repeat until function local if then else elseif in for nil or and not query from where limit select order by desc asc nulls first last group having filter using":
"return break goto do end while repeat until function local if then else elseif in for nil or and not query from where limit offset select order by desc asc nulls first last group having filter using":
t.keyword,
});
@@ -1347,6 +1347,13 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause {
ctx: context(t, ctx),
};
}
case "OffsetClause": {
return {
type: "Offset",
offset: parseExpression(t.children![1], ctx),
ctx: context(t, ctx),
};
}
case "OrderByClause": {
const orderBy: LuaOrderBy[] = [];
for (const child of t.children!) {
+219
View File
@@ -2887,3 +2887,222 @@ do
"expected `order by` error, got: " .. tostring(err)
)
end
-- 92. Standalone offset clause
-- 92a. Offset only, bound
do
local r = query [[
from
p = pages
select { name = p.name }
order by
p.name
offset
2
]]
assertEquals(#r, 5)
assertEquals(r[1].name, "Carol")
end
-- 92b. Offset only, unbound
do
local r = query [[
from
pages
order by
name
offset
5
]]
assertEquals(#r, 2)
assertEquals(r[1].name, "Fran")
assertEquals(r[2].name, "Greg")
end
-- 92c. Offset 0 returns everything
do
local r = query [[
from
pages
offset
0
]]
assertEquals(#r, #pages)
end
-- 92d. Offset larger than dataset returns empty
do
local r = query [[
from
pages
offset
100
]]
assertEquals(#r, 0)
end
-- 92e. Offset equal to dataset size returns empty
do
local r = query [[
from
pages
offset
7
]]
assertEquals(#r, 0)
end
-- 93. Standalone offset + limit (separate clauses)
-- 93a. Offset before limit
do
local r = query [[
from
p = pages
select { name = p.name }
order by
p.name
offset
2
limit
3
]]
-- skip 2, take 3 -> Carol, Dave, Ed
assertEquals(#r, 3)
assertEquals(r[1].name, "Carol")
assertEquals(r[2].name, "Dave")
assertEquals(r[3].name, "Ed")
end
-- 93b. Limit before offset (order of clauses doesn't matter)
do
local r = query [[
from
p = pages
select { name = p.name }
order by
p.name
limit
3
offset
2
]]
assertEquals(#r, 3)
assertEquals(r[1].name, "Carol")
end
-- 93c. Offset beyond available rows with limit returns empty
do
local r = query [[
from
pages
limit
3
offset
100
]]
assertEquals(#r, 0)
end
-- 93d. Limit larger than remaining after offset
do
local r = query [[
from
p = pages
order by
p.name
offset
5
limit
100
]]
assertEquals(#r, 2)
end
-- 94. Standalone offset with where + order by
do
local r = query [[
from
p = pages
where
p.size >= 5
order by
p.size
select { name = p.name }
offset
1
]]
assertEquals(#r, 3)
assertEquals(r[1].name, "Alice")
assertEquals(r[2].name, "Dave")
assertEquals(r[3].name, "Bob")
end
-- 95. Standalone offset with group by
do
local r = query [[
from
p = pages
where
p.tags[1] ~= nil
group by
p.tags[1]
select {
tag = key,
n = count(),
}
order by
tag
offset
1
]]
assertEquals(#r, 2)
assertEquals(r[1].tag, "random")
assertEquals(r[2].tag, "work")
end
-- 96. Standalone offset + limit with group by + having
do
local r = query [[
from
p = pages
where
p.tags[1] ~= nil
group by
p.tags[1]
having
count() > 0
select {
tag = key,
n = count(),
}
order by
tag
offset
1
limit
1
]]
assertEquals(#r, 1)
assertEquals(r[1].tag, "random")
end
-- 97. Standalone offset wins over inline offset (last one wins)
do
local r = query [[
from
p = pages
order by
p.name
limit
3, 1
offset
2
]]
assertEquals(#r, 3)
assertEquals(r[1].name, "Carol")
end
+55 -15
View File
@@ -6,22 +6,43 @@ Lua Integrated Query (LIQ) is a SilverBullet specific Lua extension. It adds a c
The syntax for LIQ is `query[[my query]]`. In regular Lua `[[my query]]` is just another way of writing `"my query"` (it is an alternative string syntax). Function calls that only take a string argument can omit parentheses, therefore `query[[my query]]` is equivalent to `query("my query")`.
However, in [[Space Lua]] it is interpreted as an SQL (and [LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/))-inspired integrated query language.
However, in [[Space Lua]] it is interpreted as an SQL- and [LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/)-inspired integrated query language.
General syntax:
query [[
from <expression>
where <expression>
group by <expression>[, ...]
having <expression>
order by <expression>
[asc | desc | using <comparator>]
[nulls { first | last }]
[, ...]
limit <expression>[, ...]
select <expression>
]]
```postgres
query [[
from <<expression>>
[ where <<expression>> ]
[ group by <<expression>> [, ...] ]
[ having <<expression>> ]
[ order by <<sort_expression>> [, ...] ]
[ limit <<expression>> [, <<expression>>] ]
[ offset <<expression>> ]
[ select <<expression>> ]
]]
```
Unlike in SQL where clauses must be written in a particular order, LIQ clauses can be written in any order, and the only mandatory clause is the `from` clause.
The `order by` clause uses `<<sort_expression>>`:
```postgres
<<expression>>
[ asc | desc | using <<comparator>> ]
[ nulls { first | last } ]
```
If an aggregate function call is used for projection (`select`), the general syntax is:
```postgres
<<aggregator>>( <<expression>> [, ...]
[ order by <<sort_expression>> [, ...] ]
[ filter (where <<expression>>) ]
)
```
See [[Space Lua/Lua Integrated Query/Aggregating]] for details.
Aggregate functions support an optional intra-aggregate `order by` and/or `filter` clause:
@@ -238,16 +259,35 @@ ${query [[
The query engine uses a *stable merge sort* algorithm with guaranteed performance. Items that compare as equal preserve their original order and an invalid comparator cannot cause an infinite loop or crash — the violation is detected and reported as an error.
## `limit`
The `limit` clause allows you to limit the number of results, optionally with an offset.
The `limit` clause allows you to limit the number of results, optionally with an inline offset.
Example:
${query[[from {1, 2, 3, 4, 5} limit 3]]}
You can also specify an offset to skip some results:
You can also specify an offset as a second argument to skip some results:
${query[[from {1, 2, 3, 4, 5} limit 3, 2]]}
> **note** Note
> If both an inline offset (`limit 3, 2`) and a standalone `offset` clause are present, the last one encountered wins.
## `offset`
The `offset` clause skips a number of rows from the beginning of the result set. It can be used with or without `limit`.
Skip the first 2 results:
${query[[from {1, 2, 3, 4, 5} offset 2]]}
Combined with `limit`:
${query[[from {1, 2, 3, 4, 5} offset 2 limit 3]]}
If the offset is larger than the number of available rows, the result is empty — this is not an error, matching PostgreSQL semantics.
> **note** Note
> If both a standalone `offset` clause and an inline offset in `limit` (`limit 3, 2`) are present, the last one encountered wins.
## `select`
The `select` clause allows you to transform each item in the result set. If omitted, it defaults to returning the item itself.