File tree Expand file tree Collapse file tree
pgdog/src/frontend/router/parser/query Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -318,11 +318,28 @@ impl QueryParser {
318318 }
319319 }
320320
321- Ok ( if stmt. locking_clause . is_empty ( ) {
322- FunctionBehavior :: default ( )
323- } else {
324- FunctionBehavior :: writes_only ( )
325- } )
321+ if !stmt. locking_clause . is_empty ( ) {
322+ return Ok ( FunctionBehavior :: writes_only ( ) ) ;
323+ }
324+
325+ // Recurse into CTEs so a locking clause or write-only function
326+ // nested inside a WITH clause still routes to the primary.
327+ if let Some ( ref with_clause) = stmt. with_clause {
328+ for cte in & with_clause. ctes {
329+ if let Some ( NodeEnum :: CommonTableExpr ( ref expr) ) = cte. node {
330+ if let Some ( ref query) = expr. ctequery {
331+ if let Some ( NodeEnum :: SelectStmt ( ref inner) ) = query. node {
332+ let behavior = Self :: functions ( inner) ?;
333+ if behavior. writes {
334+ return Ok ( behavior) ;
335+ }
336+ }
337+ }
338+ }
339+ }
340+ }
341+
342+ Ok ( FunctionBehavior :: default ( ) )
326343 }
327344
328345 /// Check for CTEs that could trigger this query to go to a primary.
Original file line number Diff line number Diff line change @@ -292,6 +292,28 @@ fn test_select_for_update() {
292292 assert ! ( route. is_write( ) ) ;
293293}
294294
295+ #[ test]
296+ fn test_select_for_update_in_cte ( ) {
297+ // FOR UPDATE buried inside a CTE should still route to the primary.
298+ let route = query ! (
299+ "WITH locked AS (SELECT * FROM sharded WHERE id = 1 FOR UPDATE) SELECT * FROM locked"
300+ ) ;
301+ assert ! ( route. is_write( ) ) ;
302+
303+ // Doubly-nested CTE: the locking clause is two WITH levels deep.
304+ let route = query ! (
305+ "WITH outer_cte AS (\
306+ WITH inner_cte AS (SELECT * FROM sharded WHERE id = 1 FOR UPDATE) \
307+ SELECT * FROM inner_cte\
308+ ) SELECT * FROM outer_cte"
309+ ) ;
310+ assert ! ( route. is_write( ) ) ;
311+
312+ // Sanity check: a plain SELECT inside a CTE without locking is still a read.
313+ let route = query ! ( "WITH plain AS (SELECT * FROM sharded WHERE id = 1) SELECT * FROM plain" ) ;
314+ assert ! ( route. is_read( ) ) ;
315+ }
316+
295317#[ test]
296318fn test_omni ( ) {
297319 let mut omni_round_robin = HashSet :: new ( ) ;
You can’t perform that action at this time.
0 commit comments