Merge pull request #1878 from mjf/liq-implement-offset
This commit is contained in:
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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!) {
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user