feat(parser): extend Parser with parseOrderKeys
This commit is contained in:
1 parent
b362403ac5
commit
81653b5d5a
1 file changed
+149
-9
@@ -953,9 +953,59 @@ class Parser(private val tokens: List<Token>) {
|
||||
return if (keys.isEmpty()) null else OrderBy(keys)
|
||||
}
|
||||
|
||||
private fun parseLimit(): Int? {
|
||||
if (!match(TokenType.Limit)) return null
|
||||
return expect(TokenType.IntLiteral, "expected an integer after LIMIT").intValue!!.toInt()
|
||||
/** Shared by top-level ORDER BY and a window function's `OVER (... ORDER BY ...)` clause. */
|
||||
private fun parseOrderKeys(): List<OrderKey> {
|
||||
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 {
|
||||
@@ -963,29 +1013,119 @@ class Parser(private val tokens: List<Token>) {
|
||||
expect(TokenType.From, "expected FROM after DELETE")
|
||||
val tableName = expectIdentifier("expected table name")
|
||||
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 {
|
||||
expect(TokenType.Update, "expected UPDATE")
|
||||
val tableName = expectIdentifier("expected 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>()
|
||||
do {
|
||||
val columnName = expectIdentifier("expected column name")
|
||||
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))
|
||||
val where = if (match(TokenType.Where)) parseExpression() else null
|
||||
return UpdateStatement(tableName, assignments, where)
|
||||
return assignments
|
||||
}
|
||||
|
||||
/**
|
||||
* `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 {
|
||||
expect(TokenType.Drop, "expected DROP")
|
||||
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"))
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user