feat(parser): extend Parser with parseDistinctClause
This commit is contained in:
1 parent
c2329a388e
commit
b362403ac5
1 file changed
+180
-18
@@ -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)
|
||||||
expect(TokenType.On, "expected ON after joined table")
|
// A LATERAL right-hand side has no independent row set (it is re-evaluated per left
|
||||||
joins.add(JoinClause(type, tableName, alias, parseExpression()))
|
// 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")
|
||||||
|
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? {
|
||||||
|
|||||||
Reference in new issue
Block a user