From 60a55ba961a1e434b6657cfad34f1f92a8f9040b Mon Sep 17 00:00:00 2001 From: truffle Date: Mon, 29 Jun 2026 23:25:30 +0000 Subject: [PATCH 1/2] Postgres: parse LIMIT after the row-locking clause PostgreSQL accepts LIMIT/OFFSET both before and after the row-locking clause (FOR UPDATE/FOR SHARE/...), but the parser only consumed LIMIT before the lock loop, so a query like SELECT * FROM t ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5 failed with "Expected: end of statement, found: LIMIT". Add a supports_limit_after_locking_clause dialect method (off by default, on for Postgres) and parse a trailing LIMIT clause after the locks when no leading one was present. --- src/dialect/mod.rs | 14 ++++++++++++++ src/dialect/postgresql.rs | 4 ++++ src/parser/mod.rs | 9 ++++++++- tests/sqlparser_postgres.rs | 17 +++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f7caa756e..041b404f4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1712,6 +1712,20 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports a `LIMIT`/`OFFSET` clause placed + /// after the row-locking clause (`FOR UPDATE`/`FOR SHARE`/...), in addition + /// to the usual position before it. + /// + /// Example: + /// ```sql + /// SELECT * FROM t ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5; + /// ``` + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-select.html) + fn supports_limit_after_locking_clause(&self) -> bool { + false + } + /// Returns true if this dialect supports the `INTERPOLATE` clause /// in `ORDER BY` expressions. /// diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index e980fc642..c55381c0a 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -291,6 +291,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_limit_after_locking_clause(&self) -> bool { + true + } + fn supports_set_names(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bd6334e41..20059e19e 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,13 @@ impl<'a> Parser<'a> { locks.push(self.parse_lock()?); } } + + // PostgreSQL accepts `LIMIT`/`OFFSET` after the row-locking clause + // (e.g. `... FOR UPDATE SKIP LOCKED LIMIT 5`) as well as before it. + if limit_clause.is_none() && self.dialect.supports_limit_after_locking_clause() { + 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"); +} From c1c4b70d67313e42171b5fd99eb2ace389ce1efb Mon Sep 17 00:00:00 2001 From: truffle Date: Thu, 2 Jul 2026 13:07:05 +0000 Subject: [PATCH 2/2] Drop dialect flag for LIMIT after the locking clause The row-locking clause (FOR UPDATE/FOR SHARE/...) just above is parsed for every dialect, so gating only the trailing LIMIT/OFFSET behind a dialect flag was inconsistent. Parse it unconditionally instead and remove supports_limit_after_locking_clause. --- src/dialect/mod.rs | 14 -------------- src/dialect/postgresql.rs | 4 ---- src/parser/mod.rs | 9 ++++++--- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 041b404f4..f7caa756e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1712,20 +1712,6 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if this dialect supports a `LIMIT`/`OFFSET` clause placed - /// after the row-locking clause (`FOR UPDATE`/`FOR SHARE`/...), in addition - /// to the usual position before it. - /// - /// Example: - /// ```sql - /// SELECT * FROM t ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5; - /// ``` - /// - /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-select.html) - fn supports_limit_after_locking_clause(&self) -> bool { - false - } - /// Returns true if this dialect supports the `INTERPOLATE` clause /// in `ORDER BY` expressions. /// diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index c55381c0a..e980fc642 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -291,10 +291,6 @@ impl Dialect for PostgreSqlDialect { true } - fn supports_limit_after_locking_clause(&self) -> bool { - true - } - fn supports_set_names(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 20059e19e..bd9d7dbaf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14480,9 +14480,12 @@ impl<'a> Parser<'a> { } } - // PostgreSQL accepts `LIMIT`/`OFFSET` after the row-locking clause - // (e.g. `... FOR UPDATE SKIP LOCKED LIMIT 5`) as well as before it. - if limit_clause.is_none() && self.dialect.supports_limit_after_locking_clause() { + // 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()?; }