feat(parser): extend Parser with parseDistinctClause

This commit is contained in:
dualfroz committed 2026-09-02 23:01:40 +02:00
1 parent c2329a388e
commit b362403ac5
1 file changed
+179 -17
+179 -17
View File
@@ -723,9 +723,76 @@ class Parser(private val tokens: List<Token>) {
val where = if (match(TokenType.Where)) parseExpression() else null val where = if (match(TokenType.Where)) parseExpression() else null
val groupBy = parseGroupBy() val groupBy = parseGroupBy()
val having = if (match(TokenType.Having)) parseExpression() else null 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 orderBy = parseOrderBy()
val limit = parseLimit() val (limit, offset) = parseLimitOffset()
return SelectStatement(projection, tableName, tableAlias, joins, where, groupBy, having, orderBy, limit) 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 { private fun parseProjection(): Projection {
@@ -739,14 +806,27 @@ class Parser(private val tokens: List<Token>) {
return ExpressionProjection(items) 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 tableName = expectIdentifier("expected table name")
val alias = when { val alias = when {
match(TokenType.As) -> expectIdentifier("expected alias after AS") match(TokenType.As) -> expectIdentifier("expected alias after AS")
check(TokenType.Identifier) -> advance().lexeme check(TokenType.Identifier) -> advance().lexeme
else -> null else -> null
} }
return tableName to alias return TableSource(tableName, alias)
} }
private fun parseJoins(): List<JoinClause> { private fun parseJoins(): List<JoinClause> {
@@ -759,36 +839,118 @@ class Parser(private val tokens: List<Token>) {
match(TokenType.Left) -> { match(TokenType.Left) -> {
match(TokenType.Outer); expect(TokenType.Join, "expected JOIN after LEFT"); JoinType.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 match(TokenType.Join) -> JoinType.Inner
else -> break 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") expect(TokenType.On, "expected ON after joined table")
joins.add(JoinClause(type, tableName, alias, parseExpression())) parseExpression()
}
joins.add(JoinClause(type, taggedFrom, on))
} }
return joins return joins
} }
private fun parseGroupBy(): List<Expression> { private data class GroupByClause(val groupBy: List<Expression>, val spec: GroupingSpec?)
if (!match(TokenType.Group)) return emptyList()
/**
* `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") 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>() val expressions = mutableListOf<Expression>()
do { do {
expressions.add(parseExpression()) expressions.add(parseExpression())
} while (match(TokenType.Comma)) } 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 return expressions
} }
private fun parseOrderBy(): OrderBy? { private fun parseOrderBy(): OrderBy? {
if (!match(TokenType.Order)) return null val keys = parseOrderKeys()
expect(TokenType.By, "expected BY after ORDER") return if (keys.isEmpty()) null else OrderBy(keys)
val expression = parseExpression()
val descending = when {
match(TokenType.Desc) -> true
match(TokenType.Asc) -> false
else -> false
}
return OrderBy(expression, descending)
} }
private fun parseLimit(): Int? { private fun parseLimit(): Int? {