-
Notifications
You must be signed in to change notification settings - Fork 40
Description
iter_children() in traversal.rs doesn't yield children for several expression variants, causing get_tables(), find_all(), dfs(), bfs(), and TreeContext::build() to silently miss tables.
Two specific gaps:
A) Expression::Any / Expression::All fall through to _ => {}
The QuantifiedExpr struct has two Expression fields (this and subquery), but neither is yielded:
// expressions.rs
pub struct QuantifiedExpr {
pub this: Expression,
pub subquery: Expression,
pub op: Option,
}
B) DML target tables inside CTE bodies are not reachable
Cte correctly pushes cte.this (the CTE body), so the DFS recurses into e.g. an Insert. But Insert's iter_children yields query, with, on_conflict, values, returning, etc. — NOT insert.table, because TableRef is a struct field, not an Expression. Same for Update.table, Delete.table, and their auxiliary table lists (Update.extra_tables, Delete.using).
For top-level DML this isn't a problem (callers access the target table directly). But inside CTEs, traversal is the only discovery path.
Both gaps share the same root cause: iter_children() doesn't yield all reachable sub-expressions.
Reproduction
A) Any/All:
SELECT id, name FROM articles
WHERE id = ANY(SELECT article_id FROM featured_articles WHERE active = true)
let tables = get_tables(&exprs[0]);
// Actual: ["articles"]
// Expected: ["articles", "featured_articles"]
B) CTE DML:
WITH inserted AS (
INSERT INTO orders (customer_id, amount) VALUES (1, 100) RETURNING *
)
SELECT * FROM inserted
let tables = get_tables(&exprs[0]);
// Actual: ["inserted"] (CTE alias only)
// Expected: ["inserted", "orders"]
Suggested Fix
A) Add a match arm for Any/All:
Expression::Any(q) | Expression::All(q) => {
children.push(("this", &q.this));
children.push(("subquery", &q.subquery));
}
B) For DML nodes, synthesize Expression::Table children from TableRef fields, or add a dedicated get_all_referenced_tables() that combines DFS with explicit TableRef extraction. The latter is what we do in our FFI layer as a workaround today.
Impact
Any PostgreSQL query using ANY(SELECT ...) or ALL(SELECT ...) will have inner tables invisible to traversal
Any writable CTE (WITH ... AS (INSERT/UPDATE/DELETE ...)) will have its DML target table invisible to get_tables()
Both are common PostgreSQL patterns