feat(parser): extend Parser with parseStringAggCall
This commit is contained in:
1 parent
1f216ebd5d
commit
7a2c8037e2
1 file changed
+194
-3
@@ -1577,9 +1577,200 @@ class Parser(private val tokens: List<Token>) {
|
||||
arguments.add(parseExpression())
|
||||
} while (match(TokenType.Comma))
|
||||
}
|
||||
val argument = parseExpression()
|
||||
expect(TokenType.RightParen, "expected ')' after function argument")
|
||||
return FunctionExpression(name, argument, star = false)
|
||||
expect(TokenType.RightParen, "expected ')' after function arguments")
|
||||
// `FILTER` is not a reserved keyword either (same reasoning as ROLLUP/CUBE/GROUPING above) -
|
||||
// only meaningful right after an aggregate call's closing paren when immediately followed by
|
||||
// `(`, so a column or table legitimately named `filter` is unaffected.
|
||||
var filter: Expression? = null
|
||||
if (isContextualKeyword("FILTER") && peekNext().type == TokenType.LeftParen) {
|
||||
if (!isAggregate) throw error("FILTER can only be used with an aggregate function", peek())
|
||||
advance()
|
||||
advance()
|
||||
expect(TokenType.Where, "expected WHERE after FILTER (")
|
||||
filter = parseExpression()
|
||||
expect(TokenType.RightParen, "expected ')' to close FILTER clause")
|
||||
}
|
||||
if (check(TokenType.Over)) {
|
||||
if (distinct) throw error("DISTINCT is not supported in window functions", peek())
|
||||
if (filter != null) throw error("FILTER is not supported in window functions", peek())
|
||||
return parseWindowExpression(name, upperName, isAggregate, arguments, star)
|
||||
}
|
||||
if (isAggregate) {
|
||||
if (star) return FunctionExpression(name, argument = null, star = true, filter = filter)
|
||||
if (arguments.size != 1) {
|
||||
throw error("aggregate function $name requires exactly one argument", peek())
|
||||
}
|
||||
return FunctionExpression(name, arguments.single(), star = false, distinct = distinct, filter = filter)
|
||||
}
|
||||
if (upperName == "NEXTVAL" || upperName == "CURRVAL") {
|
||||
return parseSequenceFunctionCall(name, upperName, arguments)
|
||||
}
|
||||
if (upperName == "MERGE_ACTION") {
|
||||
if (arguments.isNotEmpty()) throw error("MERGE_ACTION takes no arguments", peek())
|
||||
return MergeActionExpression
|
||||
}
|
||||
return CallExpression(name, arguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* `STRING_AGG(value, separator [ORDER BY key [ASC|DESC], ...])` - shaped nothing like the rest of
|
||||
* [AGGREGATE_FUNCTIONS] (two required arguments, no `*` form, an optional ORDER BY *inside* the
|
||||
* parens that sorts values within each group independently of the query's own ORDER BY), so it
|
||||
* gets its own parse path called before `(` is even consumed for the generic aggregate branch.
|
||||
* The left paren was already consumed by [parseFunctionCall]. Never valid with `OVER` - unlike
|
||||
* COUNT/SUM/AVG/MIN/MAX, [kestrel.engine.AggregateFunctions] has no per-row windowed form for it
|
||||
* (concatenation order inside a sliding frame is a separate, unimplemented feature), so `OVER`
|
||||
* right after the closing `)` is rejected here with a clear message instead of falling through to
|
||||
* a generic "expected ';'" parse error later.
|
||||
*/
|
||||
private fun parseStringAggCall(name: String): Expression {
|
||||
val value = parseExpression()
|
||||
expect(TokenType.Comma, "expected ',' after STRING_AGG value argument")
|
||||
val separator = parseExpression()
|
||||
val orderBy = parseOrderKeys()
|
||||
expect(TokenType.RightParen, "expected ')' after STRING_AGG arguments")
|
||||
if (check(TokenType.Over)) throw error("STRING_AGG cannot be used as a window function", peek())
|
||||
return FunctionExpression(name, value, star = false, separator = separator, orderBy = orderBy)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike every other function call, [arguments] must be exactly one string literal - the
|
||||
* sequence's name is resolved once here, at parse time, rather than carried as a general
|
||||
* [Expression] evaluated per row (see [NextValueExpression]'s own doc for why). A bad argument
|
||||
* (wrong count, wrong shape - a column reference, another call, ...) is a parse-time error here
|
||||
* rather than something [kestrel.engine.Evaluator] discovers later, matching how this parser
|
||||
* already treats e.g. a non-identifier index/table name elsewhere.
|
||||
*/
|
||||
private fun parseSequenceFunctionCall(name: String, upperName: String, arguments: List<Expression>): Expression {
|
||||
val sequenceNameArgument = arguments.singleOrNull() as? LiteralExpression
|
||||
val sequenceName = (sequenceNameArgument?.value as? Value.TextValue)?.value
|
||||
?: throw error("$name requires a single string literal argument naming the sequence", peek())
|
||||
return if (upperName == "NEXTVAL") NextValueExpression(sequenceName) else CurrentValueExpression(sequenceName)
|
||||
}
|
||||
|
||||
/**
|
||||
* `name(args) OVER (...)`, reached once `name(args)` is already parsed and `OVER` is next.
|
||||
* Ranking functions (ROW_NUMBER/RANK/DENSE_RANK) take no arguments at all; the aggregate
|
||||
* vocabulary (COUNT/SUM/AVG/MIN/MAX) is exactly the [AGGREGATE_FUNCTIONS] set, reused here with
|
||||
* per-row windowed semantics instead of collapsing a group. LAG/LEAD take 1-3 arguments (value,
|
||||
* offset, default); FIRST_VALUE/LAST_VALUE/NTILE take exactly one. Any other name before OVER is
|
||||
* a clear error rather than silently falling back to a plain (non-window) call.
|
||||
*/
|
||||
private fun parseWindowExpression(
|
||||
name: String,
|
||||
upperName: String,
|
||||
isAggregate: Boolean,
|
||||
arguments: List<Expression>,
|
||||
star: Boolean
|
||||
): Expression {
|
||||
val kind = WINDOW_RANKING_FUNCTIONS[upperName]
|
||||
?: WINDOW_OTHER_FUNCTIONS[upperName]
|
||||
?: (if (isAggregate) WindowFunctionKind.Aggregate else null)
|
||||
?: throw error("function $name cannot be used as a window function", peek())
|
||||
when (kind) {
|
||||
WindowFunctionKind.RowNumber, WindowFunctionKind.Rank, WindowFunctionKind.DenseRank ->
|
||||
if (star || arguments.isNotEmpty()) throw error("$name does not take arguments", peek())
|
||||
WindowFunctionKind.Aggregate ->
|
||||
if (!star && arguments.size != 1) throw error("window function $name requires exactly one argument", peek())
|
||||
WindowFunctionKind.Lag, WindowFunctionKind.Lead ->
|
||||
if (star || arguments.isEmpty() || arguments.size > 3) {
|
||||
throw error("$name requires between 1 and 3 arguments", peek())
|
||||
}
|
||||
WindowFunctionKind.FirstValue, WindowFunctionKind.LastValue, WindowFunctionKind.Ntile ->
|
||||
if (star || arguments.size != 1) throw error("$name requires exactly one argument", peek())
|
||||
}
|
||||
expect(TokenType.Over, "expected OVER")
|
||||
expect(TokenType.LeftParen, "expected '(' after OVER")
|
||||
val partitionBy = mutableListOf<Expression>()
|
||||
if (match(TokenType.Partition)) {
|
||||
expect(TokenType.By, "expected BY after PARTITION")
|
||||
do {
|
||||
partitionBy.add(parseExpression())
|
||||
} while (match(TokenType.Comma))
|
||||
}
|
||||
val orderBy = parseOrderKeys()
|
||||
val frame = parseWindowFrame()
|
||||
expect(TokenType.RightParen, "expected ')' to close OVER clause")
|
||||
val argument = if (kind == WindowFunctionKind.Aggregate) arguments.singleOrNull() else arguments.getOrNull(0)
|
||||
val offset = if (kind == WindowFunctionKind.Lag || kind == WindowFunctionKind.Lead) arguments.getOrNull(1) else null
|
||||
val defaultValue = if (kind == WindowFunctionKind.Lag || kind == WindowFunctionKind.Lead) arguments.getOrNull(2) else null
|
||||
return WindowExpression(name, kind, argument, star, partitionBy, orderBy, offset, defaultValue, frame)
|
||||
}
|
||||
|
||||
/**
|
||||
* `ROWS|RANGE|GROUPS BETWEEN <bound> AND <bound>`, or the shorthand `ROWS|RANGE|GROUPS <bound>`
|
||||
* (equivalent to `... BETWEEN <bound> AND CURRENT ROW`, exactly like every other SQL engine's
|
||||
* shorthand).
|
||||
*/
|
||||
private fun parseWindowFrame(): WindowFrame? {
|
||||
val unit = when {
|
||||
match(TokenType.Rows) -> FrameUnit.Rows
|
||||
match(TokenType.Range) -> FrameUnit.Range
|
||||
match(TokenType.Groups) -> FrameUnit.Groups
|
||||
else -> return null
|
||||
}
|
||||
val (start, end) = if (match(TokenType.Between)) {
|
||||
val start = parseFrameBound()
|
||||
expect(TokenType.And, "expected AND between the two bounds of a frame")
|
||||
val end = parseFrameBound()
|
||||
start to end
|
||||
} else {
|
||||
parseFrameBound() to FrameBound(FrameBoundType.CurrentRow)
|
||||
}
|
||||
if (frameBoundRank(start.type) > frameBoundRank(end.type)) {
|
||||
throw error("frame start cannot come after frame end", peek())
|
||||
}
|
||||
return WindowFrame(unit, start, end, parseFrameExclude())
|
||||
}
|
||||
|
||||
/**
|
||||
* `EXCLUDE CURRENT ROW | EXCLUDE GROUP | EXCLUDE TIES | EXCLUDE NO OTHERS`, optional and always
|
||||
* following the frame's bounds. Absent entirely is [FrameExclude.NoOthers], identical to writing
|
||||
* `EXCLUDE NO OTHERS` out - SQL engines never require it and this parser accepts either.
|
||||
*/
|
||||
private fun parseFrameExclude(): FrameExclude {
|
||||
if (!match(TokenType.Exclude)) return FrameExclude.NoOthers
|
||||
return when {
|
||||
match(TokenType.Current) -> {
|
||||
expect(TokenType.Row, "expected ROW after EXCLUDE CURRENT")
|
||||
FrameExclude.CurrentRow
|
||||
}
|
||||
match(TokenType.Group) -> FrameExclude.Group
|
||||
match(TokenType.Ties) -> FrameExclude.Ties
|
||||
match(TokenType.No) -> {
|
||||
expect(TokenType.Others, "expected OTHERS after EXCLUDE NO")
|
||||
FrameExclude.NoOthers
|
||||
}
|
||||
else -> throw error("expected CURRENT ROW, GROUP, TIES, or NO OTHERS after EXCLUDE", peek())
|
||||
}
|
||||
}
|
||||
|
||||
private fun frameBoundRank(type: FrameBoundType): Int = when (type) {
|
||||
FrameBoundType.UnboundedPreceding -> 0
|
||||
FrameBoundType.Preceding -> 1
|
||||
FrameBoundType.CurrentRow -> 2
|
||||
FrameBoundType.Following -> 3
|
||||
FrameBoundType.UnboundedFollowing -> 4
|
||||
}
|
||||
|
||||
private fun parseFrameBound(): FrameBound {
|
||||
if (match(TokenType.Unbounded)) {
|
||||
return when {
|
||||
match(TokenType.Preceding) -> FrameBound(FrameBoundType.UnboundedPreceding)
|
||||
match(TokenType.Following) -> FrameBound(FrameBoundType.UnboundedFollowing)
|
||||
else -> throw error("expected PRECEDING or FOLLOWING after UNBOUNDED", peek())
|
||||
}
|
||||
}
|
||||
if (match(TokenType.Current)) {
|
||||
expect(TokenType.Row, "expected ROW after CURRENT")
|
||||
return FrameBound(FrameBoundType.CurrentRow)
|
||||
}
|
||||
val offset = parseExpression()
|
||||
return when {
|
||||
match(TokenType.Preceding) -> FrameBound(FrameBoundType.Preceding, offset)
|
||||
match(TokenType.Following) -> FrameBound(FrameBoundType.Following, offset)
|
||||
else -> throw error("expected PRECEDING or FOLLOWING", peek())
|
||||
}
|
||||
}
|
||||
|
||||
private fun peek(): Token = tokens[position]
|
||||
|
||||
Reference in new issue
Block a user