feat(parser): extend Parser with parseOrderKeys

This commit is contained in:
dualfroz committed 2026-09-04 11:23:23 +02:00
1 parent b362403ac5
commit 81653b5d5a
1 file changed
+149 -9
+149 -9
View File
@@ -953,9 +953,59 @@ class Parser(private val tokens: List<Token>) {
return if (keys.isEmpty()) null else OrderBy(keys) return if (keys.isEmpty()) null else OrderBy(keys)
} }
private fun parseLimit(): Int? { /** Shared by top-level ORDER BY and a window function's `OVER (... ORDER BY ...)` clause. */
if (!match(TokenType.Limit)) return null private fun parseOrderKeys(): List<OrderKey> {
return expect(TokenType.IntLiteral, "expected an integer after LIMIT").intValue!!.toInt() if (!match(TokenType.Order)) return emptyList()
expect(TokenType.By, "expected BY after ORDER")
val keys = mutableListOf<OrderKey>()
do {
val expression = parseExpression()
val descending = when {
match(TokenType.Desc) -> true
match(TokenType.Asc) -> false
else -> false
}
val nullsFirst = parseOptionalNullsClause()
keys.add(OrderKey(expression, descending, nullsFirst))
} while (match(TokenType.Comma))
return keys
}
/**
* `NULLS FIRST` / `NULLS LAST`, optional after each ORDER BY key's own `ASC`/`DESC`. Like
* `ROLLUP`/`CUBE`/`FILTER`/`GENERATED` elsewhere in this parser, `NULLS`/`FIRST`/`LAST` are not
* reserved keywords - a column legitimately named any of the three is unaffected outside this
* exact position, since [isContextualKeyword] only fires here, right after an order key.
*/
private fun parseOptionalNullsClause(): Boolean? {
if (!isContextualKeyword("NULLS")) return null
advance()
return when {
isContextualKeyword("FIRST") -> { advance(); true }
isContextualKeyword("LAST") -> { advance(); false }
else -> throw error("expected FIRST or LAST after NULLS but found '${describe(peek())}'", peek())
}
}
/**
* `LIMIT`/`OFFSET` may appear in either order (matching Postgres, unlike the standard's
* `OFFSET ... FETCH ...`), but not repeated - a second `LIMIT` or `OFFSET` stops the loop and is
* left for whatever follows (typically a parse error from the caller, e.g. a stray token before
* `;`) rather than silently overwriting the first.
*/
private fun parseLimitOffset(): Pair<Int?, Int?> {
var limit: Int? = null
var offset: Int? = null
while (true) {
if (limit == null && match(TokenType.Limit)) {
limit = expect(TokenType.IntLiteral, "expected an integer after LIMIT").intValue!!.toInt()
} else if (offset == null && match(TokenType.Offset)) {
offset = expect(TokenType.IntLiteral, "expected an integer after OFFSET").intValue!!.toInt()
} else {
break
}
}
return limit to offset
} }
private fun parseDelete(): DeleteStatement { private fun parseDelete(): DeleteStatement {
@@ -963,29 +1013,119 @@ class Parser(private val tokens: List<Token>) {
expect(TokenType.From, "expected FROM after DELETE") expect(TokenType.From, "expected FROM after DELETE")
val tableName = expectIdentifier("expected table name") val tableName = expectIdentifier("expected table name")
val where = if (match(TokenType.Where)) parseExpression() else null val where = if (match(TokenType.Where)) parseExpression() else null
return DeleteStatement(tableName, where) val returning = parseReturning()
return DeleteStatement(tableName, where, returning)
} }
private fun parseUpdate(): UpdateStatement { private fun parseUpdate(): UpdateStatement {
expect(TokenType.Update, "expected UPDATE") expect(TokenType.Update, "expected UPDATE")
val tableName = expectIdentifier("expected table name") val tableName = expectIdentifier("expected table name")
expect(TokenType.Set, "expected SET after table name") expect(TokenType.Set, "expected SET after table name")
val assignments = parseAssignmentList()
val where = if (match(TokenType.Where)) parseExpression() else null
val returning = parseReturning()
return UpdateStatement(tableName, assignments, where, returning)
}
private fun parseAssignmentList(): List<Assignment> {
val assignments = mutableListOf<Assignment>() val assignments = mutableListOf<Assignment>()
do { do {
val columnName = expectIdentifier("expected column name") val columnName = expectIdentifier("expected column name")
expect(TokenType.Equal, "expected '=' in assignment") expect(TokenType.Equal, "expected '=' in assignment")
assignments.add(Assignment(columnName, parseExpression())) val value = if (match(TokenType.Default)) null else parseExpression()
assignments.add(Assignment(columnName, value))
} while (match(TokenType.Comma)) } while (match(TokenType.Comma))
val where = if (match(TokenType.Where)) parseExpression() else null return assignments
return UpdateStatement(tableName, assignments, where) }
/**
* `MERGE INTO target [[AS] alias] USING source ON condition (WHEN ...)+`. [parseFromItem] is
* reused verbatim for the `USING` source: it is exactly a `FROM`-shaped item (bare table name
* or a parenthesized, aliased derived-table subquery), not a MERGE-specific grammar production.
*/
private fun parseMerge(): MergeStatement {
expect(TokenType.Merge, "expected MERGE")
expect(TokenType.Into, "expected INTO after MERGE")
val targetTable = expectIdentifier("expected target table name")
val targetAlias = parseOptionalAlias()
expect(TokenType.Using, "expected USING after MERGE target")
val source = parseFromItem()
expect(TokenType.On, "expected ON after MERGE USING source")
val onCondition = parseExpression()
val whenClauses = mutableListOf<MergeWhenClause>()
while (match(TokenType.When)) {
whenClauses.add(parseMergeWhenClause())
}
if (whenClauses.isEmpty()) {
throw error("MERGE requires at least one WHEN clause", peek())
}
val returning = parseReturning()
return MergeStatement(targetTable, targetAlias, source, onCondition, whenClauses, returning)
}
private fun parseOptionalAlias(): String? = when {
match(TokenType.As) -> expectIdentifier("expected alias after AS")
check(TokenType.Identifier) -> advance().lexeme
else -> null
}
/**
* `WHEN [NOT] MATCHED [AND condition] THEN action` - the `AND condition` sits between the
* MATCHED/NOT MATCHED keyword and THEN in both categories, matching Postgres. A NOT MATCHED
* action is always INSERT (there is no target row for NOT MATCHED to UPDATE or DELETE);
* [MergeNotMatchedInsert.values] uses the same "bare DEFAULT keyword or expression" per-slot
* grammar as plain INSERT's own VALUES list ([parseInsert]).
*/
private fun parseMergeWhenClause(): MergeWhenClause {
val matched = when {
match(TokenType.Matched) -> true
match(TokenType.Not) -> {
expect(TokenType.Matched, "expected MATCHED after NOT")
false
}
else -> throw error("expected MATCHED or NOT MATCHED after WHEN but found '${describe(peek())}'", peek())
}
val condition = if (match(TokenType.And)) parseExpression() else null
expect(TokenType.Then, "expected THEN after WHEN ${if (matched) "MATCHED" else "NOT MATCHED"}")
if (matched) {
return when {
match(TokenType.Update) -> {
expect(TokenType.Set, "expected SET after WHEN MATCHED THEN UPDATE")
MergeMatchedUpdate(condition, parseAssignmentList())
}
match(TokenType.Delete) -> MergeMatchedDelete(condition)
else -> throw error("expected UPDATE or DELETE after WHEN MATCHED THEN but found '${describe(peek())}'", peek())
}
}
expect(TokenType.Insert, "expected INSERT after WHEN NOT MATCHED THEN")
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.Values, "expected VALUES after WHEN NOT MATCHED THEN INSERT")
expect(TokenType.LeftParen, "expected '(' before value list")
val values = mutableListOf<Expression?>()
do {
values.add(if (match(TokenType.Default)) null else parseExpression())
} while (match(TokenType.Comma))
expect(TokenType.RightParen, "expected ')' after value list")
return MergeNotMatchedInsert(condition, columns, values)
} }
private fun parseDrop(): Statement { private fun parseDrop(): Statement {
expect(TokenType.Drop, "expected DROP") expect(TokenType.Drop, "expected DROP")
return when { return when {
match(TokenType.Table) -> DropTableStatement(expectIdentifier("expected table name")) match(TokenType.Table) -> DropTableStatement(expectIdentifier("expected table name"), parseDropMode())
match(TokenType.Index) -> DropIndexStatement(expectIdentifier("expected index name")) match(TokenType.Index) -> DropIndexStatement(expectIdentifier("expected index name"))
else -> throw error("expected TABLE or INDEX after DROP but found '${describe(peek())}'", peek()) match(TokenType.View) -> DropViewStatement(expectIdentifier("expected view name"), parseDropMode())
match(TokenType.Trigger) -> DropTriggerStatement(expectIdentifier("expected trigger name"))
match(TokenType.Sequence) -> DropSequenceStatement(expectIdentifier("expected sequence name"))
else -> throw error("expected TABLE, INDEX, VIEW, TRIGGER, or SEQUENCE after DROP but found '${describe(peek())}'", peek())
} }
} }