diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bd6334e41..bd9d7dbaf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14459,7 +14459,7 @@ impl<'a> Parser<'a> { let order_by = self.parse_optional_order_by()?; - let limit_clause = self.parse_optional_limit_clause()?; + let mut limit_clause = self.parse_optional_limit_clause()?; let settings = self.parse_settings()?; @@ -14479,6 +14479,16 @@ impl<'a> Parser<'a> { locks.push(self.parse_lock()?); } } + + // Some databases (e.g. PostgreSQL) accept `LIMIT`/`OFFSET` after the + // row-locking clause (`... FOR UPDATE SKIP LOCKED LIMIT 5`) as well + // as before it. The locking clause above is parsed for every + // dialect, so accept a trailing limit here too rather than gating it + // behind a dialect flag. + if limit_clause.is_none() { + limit_clause = self.parse_optional_limit_clause()?; + } + let format_clause = if self.dialect.supports_select_format() && self.parse_keyword(Keyword::FORMAT) { if self.parse_keyword(Keyword::NULL) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2fa12574f..20bdea2fe 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9486,3 +9486,20 @@ fn exclude_as_column_name() { } } } + +#[test] +fn parse_limit_after_locking_clause() { + // PostgreSQL accepts `LIMIT`/`OFFSET` after the row-locking clause as well + // as before it; both orderings are semantically identical. The AST renders + // the limit in its canonical position (before the locking clause). + pg().one_statement_parses_to( + "SELECT * FROM t ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5", + "SELECT * FROM t ORDER BY id LIMIT 5 FOR UPDATE SKIP LOCKED", + ); + pg().one_statement_parses_to( + "SELECT * FROM t FOR UPDATE LIMIT 5", + "SELECT * FROM t LIMIT 5 FOR UPDATE", + ); + // The pre-existing ordering keeps round-tripping unchanged. + pg().verified_stmt("SELECT * FROM t ORDER BY id LIMIT 5 FOR UPDATE SKIP LOCKED"); +}