feat(parser): extend Parser with parseDistinctClause
This commit is contained in:
1 parent
c2329a388e
commit
b362403ac5
1 file changed
+179
-17
@@ -723,9 +723,76 @@ class Parser(private val tokens: List<Token>) {
|
||||
val where = if (match(TokenType.Where)) parseExpression() else null
|
||||
val groupBy = parseGroupBy()
|
||||
val having = if (match(TokenType.Having)) parseExpression() else null
|
||||
return SelectStatement(
|
||||
projection, from, joins, where, groupBy.groupBy, groupBy.spec, having,
|
||||
distinct = distinct, distinctOn = distinctOn
|
||||
)
|
||||
}
|
||||
|
||||
/** `SELECT [ALL | DISTINCT | DISTINCT ON (expr, ...)] ...` - `ALL` (the default) is accepted and discarded, matching Postgres. */
|
||||
private fun parseDistinctClause(): Pair<Boolean, List<Expression>> {
|
||||
if (match(TokenType.All)) return false to emptyList()
|
||||
if (!match(TokenType.Distinct)) return false to emptyList()
|
||||
if (!match(TokenType.On)) return true to emptyList()
|
||||
expect(TokenType.LeftParen, "expected '(' after DISTINCT ON")
|
||||
val expressions = mutableListOf<Expression>()
|
||||
do {
|
||||
expressions.add(parseExpression())
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' after DISTINCT ON expressions")
|
||||
return true to expressions
|
||||
}
|
||||
|
||||
private fun parseQueryStatement(): QueryStatement {
|
||||
if (check(TokenType.With)) return parseWithQuery()
|
||||
return parseCompoundQuery()
|
||||
}
|
||||
|
||||
private fun parseWithQuery(): WithQuery {
|
||||
expect(TokenType.With, "expected WITH")
|
||||
val recursive = match(TokenType.Recursive)
|
||||
val ctes = mutableListOf<CommonTableExpression>()
|
||||
do {
|
||||
val name = expectIdentifier("expected common table expression name")
|
||||
var columns: List<String>? = null
|
||||
if (match(TokenType.LeftParen)) {
|
||||
val names = mutableListOf<String>()
|
||||
do {
|
||||
names.add(expectIdentifier("expected column name"))
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' after column list")
|
||||
columns = names
|
||||
}
|
||||
expect(TokenType.As, "expected AS after common table expression name")
|
||||
expect(TokenType.LeftParen, "expected '(' after AS")
|
||||
val query = parseCompoundQuery()
|
||||
expect(TokenType.RightParen, "expected ')' after common table expression")
|
||||
ctes.add(CommonTableExpression(name, columns, query))
|
||||
} while (match(TokenType.Comma))
|
||||
val body = parseCompoundQuery()
|
||||
return WithQuery(recursive, ctes, body)
|
||||
}
|
||||
|
||||
private fun parseCompoundQuery(): QueryStatement {
|
||||
var left: QueryStatement = parseSelectCore()
|
||||
while (true) {
|
||||
val operator = when {
|
||||
match(TokenType.Union) -> if (match(TokenType.All)) SetOperator.UnionAll else SetOperator.Union
|
||||
match(TokenType.Intersect) -> if (match(TokenType.All)) SetOperator.IntersectAll else SetOperator.Intersect
|
||||
match(TokenType.Except) -> if (match(TokenType.All)) SetOperator.ExceptAll else SetOperator.Except
|
||||
else -> break
|
||||
}
|
||||
val right = parseSelectCore()
|
||||
left = SetOperationStatement(operator, left, right)
|
||||
}
|
||||
val orderBy = parseOrderBy()
|
||||
val limit = parseLimit()
|
||||
return SelectStatement(projection, tableName, tableAlias, joins, where, groupBy, having, orderBy, limit)
|
||||
val (limit, offset) = parseLimitOffset()
|
||||
if (orderBy == null && limit == null && offset == null) return left
|
||||
return when (left) {
|
||||
is SelectStatement -> left.copy(orderBy = orderBy, limit = limit, offset = offset)
|
||||
is SetOperationStatement -> left.copy(orderBy = orderBy, limit = limit, offset = offset)
|
||||
is WithQuery -> error("unreachable: parseSelectCore never produces a WITH query")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseProjection(): Projection {
|
||||
@@ -739,14 +806,27 @@ class Parser(private val tokens: List<Token>) {
|
||||
return ExpressionProjection(items)
|
||||
}
|
||||
|
||||
private fun parseTableRef(): Pair<String, String?> {
|
||||
private fun parseFromItem(): FromItem {
|
||||
if (match(TokenType.LeftParen)) {
|
||||
if (!check(TokenType.Select) && !check(TokenType.With)) {
|
||||
throw error("expected SELECT or WITH after '(' in FROM but found '${describe(peek())}'", peek())
|
||||
}
|
||||
val select = parseQueryStatement()
|
||||
expect(TokenType.RightParen, "expected ')' after derived table subquery")
|
||||
val alias = when {
|
||||
match(TokenType.As) -> expectIdentifier("expected alias after AS")
|
||||
check(TokenType.Identifier) -> advance().lexeme
|
||||
else -> throw error("a derived table in FROM requires an alias", peek())
|
||||
}
|
||||
return SubquerySource(select, alias)
|
||||
}
|
||||
val tableName = expectIdentifier("expected table name")
|
||||
val alias = when {
|
||||
match(TokenType.As) -> expectIdentifier("expected alias after AS")
|
||||
check(TokenType.Identifier) -> advance().lexeme
|
||||
else -> null
|
||||
}
|
||||
return tableName to alias
|
||||
return TableSource(tableName, alias)
|
||||
}
|
||||
|
||||
private fun parseJoins(): List<JoinClause> {
|
||||
@@ -759,36 +839,118 @@ class Parser(private val tokens: List<Token>) {
|
||||
match(TokenType.Left) -> {
|
||||
match(TokenType.Outer); expect(TokenType.Join, "expected JOIN after LEFT"); JoinType.Left
|
||||
}
|
||||
match(TokenType.Right) -> {
|
||||
match(TokenType.Outer); expect(TokenType.Join, "expected JOIN after RIGHT"); JoinType.Right
|
||||
}
|
||||
match(TokenType.Full) -> {
|
||||
match(TokenType.Outer); expect(TokenType.Join, "expected JOIN after FULL"); JoinType.Full
|
||||
}
|
||||
match(TokenType.Cross) -> {
|
||||
expect(TokenType.Join, "expected JOIN after CROSS"); JoinType.Cross
|
||||
}
|
||||
match(TokenType.Join) -> JoinType.Inner
|
||||
else -> break
|
||||
}
|
||||
val (tableName, alias) = parseTableRef()
|
||||
val lateral = match(TokenType.Lateral)
|
||||
// A LATERAL right-hand side has no independent row set (it is re-evaluated per left
|
||||
// row), so there is nothing to pad with NULLs for an unmatched *right* row - RIGHT/FULL
|
||||
// JOIN LATERAL is therefore meaningless and rejected here rather than at execution time.
|
||||
if (lateral && (type == JoinType.Right || type == JoinType.Full)) {
|
||||
throw error("RIGHT/FULL JOIN LATERAL is not supported: LATERAL has no independent right-hand row set", peek())
|
||||
}
|
||||
val from = parseFromItem()
|
||||
if (lateral && from !is SubquerySource) {
|
||||
throw error("LATERAL is only allowed before a derived table subquery", peek())
|
||||
}
|
||||
val taggedFrom = if (lateral) (from as SubquerySource).copy(lateral = true) else from
|
||||
// CROSS JOIN has no ON clause at all - matches every left/right pairing unconditionally.
|
||||
val on = if (type == JoinType.Cross) {
|
||||
if (check(TokenType.On)) throw error("CROSS JOIN does not take an ON condition", peek())
|
||||
null
|
||||
} else {
|
||||
expect(TokenType.On, "expected ON after joined table")
|
||||
joins.add(JoinClause(type, tableName, alias, parseExpression()))
|
||||
parseExpression()
|
||||
}
|
||||
joins.add(JoinClause(type, taggedFrom, on))
|
||||
}
|
||||
return joins
|
||||
}
|
||||
|
||||
private fun parseGroupBy(): List<Expression> {
|
||||
if (!match(TokenType.Group)) return emptyList()
|
||||
private data class GroupByClause(val groupBy: List<Expression>, val spec: GroupingSpec?)
|
||||
|
||||
/**
|
||||
* `ROLLUP`/`CUBE`/`GROUPING SETS` are not reserved keywords (matching how most SQL engines treat
|
||||
* them - only meaningful right after `GROUP BY`), so they are recognized here by peeking at an
|
||||
* [TokenType.Identifier]'s text rather than via a dedicated token type; a bare column actually
|
||||
* named e.g. `rollup` is unaffected since the lookahead also requires the following token to open
|
||||
* the parenthesized argument list. Mixing plain columns with one of these in the same clause (SQL
|
||||
* allows it, producing a cross product of sets) is a scope cut - exactly one form fills the whole
|
||||
* clause.
|
||||
*/
|
||||
private fun parseGroupBy(): GroupByClause {
|
||||
if (!match(TokenType.Group)) return GroupByClause(emptyList(), null)
|
||||
expect(TokenType.By, "expected BY after GROUP")
|
||||
if (isContextualKeyword("ROLLUP") && peekNext().type == TokenType.LeftParen) {
|
||||
advance()
|
||||
val expressions = parseParenthesizedExpressionList("ROLLUP")
|
||||
return GroupByClause(expressions, RollupSpec(expressions))
|
||||
}
|
||||
if (isContextualKeyword("CUBE") && peekNext().type == TokenType.LeftParen) {
|
||||
advance()
|
||||
val expressions = parseParenthesizedExpressionList("CUBE")
|
||||
return GroupByClause(expressions, CubeSpec(expressions))
|
||||
}
|
||||
if (isContextualKeyword("GROUPING") && peekNext().let { it.type == TokenType.Identifier && it.lexeme.equals("SETS", ignoreCase = true) }) {
|
||||
advance()
|
||||
advance()
|
||||
expect(TokenType.LeftParen, "expected '(' after GROUPING SETS")
|
||||
val sets = mutableListOf<List<Expression>>()
|
||||
do {
|
||||
sets.add(parseGroupingSetElement())
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' to close GROUPING SETS")
|
||||
val union = sets.flatten().distinct()
|
||||
return GroupByClause(union, GroupingSetsSpec(sets))
|
||||
}
|
||||
val expressions = mutableListOf<Expression>()
|
||||
do {
|
||||
expressions.add(parseExpression())
|
||||
} while (match(TokenType.Comma))
|
||||
return GroupByClause(expressions, null)
|
||||
}
|
||||
|
||||
private fun isContextualKeyword(name: String): Boolean =
|
||||
check(TokenType.Identifier) && peek().lexeme.equals(name, ignoreCase = true)
|
||||
|
||||
private fun peekNext(): Token = tokens.getOrElse(position + 1) { tokens.last() }
|
||||
|
||||
private fun parseParenthesizedExpressionList(clauseName: String): List<Expression> {
|
||||
expect(TokenType.LeftParen, "expected '(' after $clauseName")
|
||||
val expressions = mutableListOf<Expression>()
|
||||
if (!check(TokenType.RightParen)) {
|
||||
do {
|
||||
expressions.add(parseExpression())
|
||||
} while (match(TokenType.Comma))
|
||||
}
|
||||
expect(TokenType.RightParen, "expected ')' to close $clauseName argument list")
|
||||
return expressions
|
||||
}
|
||||
|
||||
/** One element of `GROUPING SETS (...)`: `()` (the grand-total set) or a parenthesized expression list. */
|
||||
private fun parseGroupingSetElement(): List<Expression> {
|
||||
expect(TokenType.LeftParen, "expected '(' to open a GROUPING SETS element")
|
||||
if (match(TokenType.RightParen)) return emptyList()
|
||||
val expressions = mutableListOf<Expression>()
|
||||
do {
|
||||
expressions.add(parseExpression())
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' to close a GROUPING SETS element")
|
||||
return expressions
|
||||
}
|
||||
|
||||
private fun parseOrderBy(): OrderBy? {
|
||||
if (!match(TokenType.Order)) return null
|
||||
expect(TokenType.By, "expected BY after ORDER")
|
||||
val expression = parseExpression()
|
||||
val descending = when {
|
||||
match(TokenType.Desc) -> true
|
||||
match(TokenType.Asc) -> false
|
||||
else -> false
|
||||
}
|
||||
return OrderBy(expression, descending)
|
||||
val keys = parseOrderKeys()
|
||||
return if (keys.isEmpty()) null else OrderBy(keys)
|
||||
}
|
||||
|
||||
private fun parseLimit(): Int? {
|
||||
|
||||
Reference in new issue
Block a user