Skip to content

iter_children() / children() has incomplete coverage — misses Any/All subqueries and DML target tables inside CTEs #75

@devmaha

Description

@devmaha

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions