Skip to content

Commit d334760

Browse files
authored
fix: detect select for update in CTEs (#879)
fix #878
1 parent 38eccf3 commit d334760

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

pgdog/src/frontend/router/parser/query/select.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff 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.

pgdog/src/frontend/router/parser/query/test/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff 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]
296318
fn test_omni() {
297319
let mut omni_round_robin = HashSet::new();

0 commit comments

Comments
 (0)