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)
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user