22// SPDX-License-Identifier: OSL-3.0
33mod casts;
44mod functions;
5+ mod limit_offset;
56mod predicates;
67
78use sqlparser:: ast:: VisitMut ;
@@ -22,6 +23,8 @@ use sqlparser::parser::Parser;
2223/// - PostgreSQL type names are normalized to Trino equivalents
2324/// - `ILIKE` becomes `lower(x) LIKE lower(pattern)`
2425/// - PostgreSQL function names are mapped to Trino equivalents
26+ /// - `LIMIT n OFFSET m` is reordered into Trino order
27+ /// (`OFFSET m FETCH FIRST n ROWS ONLY`)
2528///
2629/// If parsing fails (e.g. for `SET`, `SHOW`, `DISCARD` commands), the original
2730/// SQL is returned unchanged.
@@ -57,9 +60,11 @@ pub fn rewrite_sql(sql: &str) -> String {
5760 let mut cast_rewriter = casts:: CastRewriter ;
5861 let mut ilike_rewriter = predicates:: ILikeRewriter ;
5962 let mut fn_renamer = functions:: FunctionRenamer ;
63+ let mut limit_offset_rewriter = limit_offset:: LimitOffsetRewriter ;
6064 let _ = stmt. visit ( & mut cast_rewriter) ;
6165 let _ = stmt. visit ( & mut ilike_rewriter) ;
6266 let _ = stmt. visit ( & mut fn_renamer) ;
67+ let _ = stmt. visit ( & mut limit_offset_rewriter) ;
6368
6469 stmt. to_string ( )
6570}
@@ -133,6 +138,36 @@ mod tests {
133138 must_contain : & [ "SELECT" , "FROM" ] ,
134139 must_not_contain : & [ ] ,
135140 } ,
141+ Case {
142+ name : "LIMIT n OFFSET m → OFFSET m FETCH FIRST n (no bare LIMIT)" ,
143+ input : "SELECT name FROM t ORDER BY name LIMIT 2 OFFSET 1" ,
144+ must_contain : & [ "OFFSET 1" , "FETCH FIRST 2" ] ,
145+ must_not_contain : & [ "LIMIT" ] ,
146+ } ,
147+ Case {
148+ name : "LIMIT only is left unchanged" ,
149+ input : "SELECT name FROM t LIMIT 5" ,
150+ must_contain : & [ "LIMIT 5" ] ,
151+ must_not_contain : & [ "FETCH" , "OFFSET" ] ,
152+ } ,
153+ Case {
154+ name : "OFFSET only is left unchanged" ,
155+ input : "SELECT name FROM t OFFSET 3" ,
156+ must_contain : & [ "OFFSET 3" ] ,
157+ must_not_contain : & [ "FETCH" , "LIMIT" ] ,
158+ } ,
159+ Case {
160+ name : "LIMIT ALL OFFSET m → bare OFFSET (ALL dropped, no FETCH)" ,
161+ input : "SELECT name FROM t LIMIT ALL OFFSET 4" ,
162+ must_contain : & [ "OFFSET 4" ] ,
163+ must_not_contain : & [ "FETCH" , "LIMIT" , "ALL" ] ,
164+ } ,
165+ Case {
166+ name : "subquery LIMIT+OFFSET is reordered too" ,
167+ input : "SELECT * FROM (SELECT name FROM t ORDER BY name LIMIT 2 OFFSET 1) x" ,
168+ must_contain : & [ "OFFSET 1" , "FETCH FIRST 2" ] ,
169+ must_not_contain : & [ "LIMIT" ] ,
170+ } ,
136171 ] ;
137172
138173 #[ test]
@@ -162,6 +197,20 @@ mod tests {
162197 assert_eq ! ( rewrite_sql( input) , input) ;
163198 }
164199
200+ /// The reordered clause must place `OFFSET` before the row-limiting
201+ /// `FETCH` — the whole point of the rewrite, which the substring-based
202+ /// `Case` table cannot assert on its own.
203+ #[ test]
204+ fn limit_offset_emits_offset_before_fetch ( ) {
205+ let result = rewrite_sql ( "SELECT name FROM t ORDER BY name LIMIT 2 OFFSET 1" ) ;
206+ let offset_at = result. find ( "OFFSET" ) . expect ( "OFFSET present" ) ;
207+ let fetch_at = result. find ( "FETCH" ) . expect ( "FETCH present" ) ;
208+ assert ! (
209+ offset_at < fetch_at,
210+ "expected OFFSET before FETCH in: {result}"
211+ ) ;
212+ }
213+
165214 #[ test]
166215 fn show_passes_through_non_empty ( ) {
167216 let result = rewrite_sql ( "SHOW server_version" ) ;
0 commit comments