Skip to content

No running through SubQueries #69

Open

Description

@cinifyetr

I have the problem that with SubQueries, the traversing stops.

var ast = new Parser().ParseSql(sql, new PostgreSqlDialect(), new ParserOptions()
{
    RecursionLimit = 20000
});
var visitor = new PostgresVisitor(allowedType);

ast.Visit(visitor);

Here an example sql query:

SELECT * 
FROM ds_customers c
LEFT JOIN LATERAL (
    SELECT *
    FROM ds_orders o
    WHERE o.customer_id = c.id
    ORDER BY o.order_date DESC
    LIMIT 3
) recent_orders ON true;

I am trying to collect all the tables called up. I have built a hotfix that checks if there is a SubQuery and runs it again. I can use it for reading. I also have a “ReplaceVisitor” which replaces table names. I can't get that to work. It would be great if the SubQuery could simply continue to be used normally like the main query. Am I doing something wrong here?

public override ControlFlow PreVisitTableFactor(TableFactor relation)
{
    // Handle TableFactor.Derived (for LATERAL JOINs and subqueries)
    if (relation is TableFactor.Derived derived)
    {
        Console.WriteLine($"[VISITOR] Found TableFactor.Derived - Lateral: {derived.Lateral}");
        if (derived.SubQuery != null)
        {
            Console.WriteLine($"[VISITOR] Derived has SubQuery - will let base visitor handle it");
            Console.WriteLine($"[VISITOR] SubQuery type: {derived.SubQuery.GetType().Name}");
            Console.WriteLine($"[VISITOR] SubQuery sql: {derived.SubQuery.ToSql()}");
            PostgresVisitor visitor = new PostgresVisitor(AllowedType);
            var ast = new Parser().ParseSql(derived.SubQuery.ToSql(), new PostgreSqlDialect(), new ParserOptions()
            {
              RecursionLimit = 20000
            });
            ast.Visit(this);
            return ControlFlow.Continue;
        }
        // Let the base visitor handle the traversal
        return base.PreVisitTableFactor(relation);
    }
    
    if (!(relation is TableFactor.Table))
        return base.PreVisitTableFactor(relation);

    var table = relation.AsTable();
    Console.WriteLine($"[VISITOR] Found TableFactor.Table - Name: {table?.Name}");

    if (table != null)
    {
        // logic for table
    }

    return base.PreVisitTableFactor(relation);
}

Without the derived.SubQuery part, the SubQuery is never run.

Activity

  1. mattfennerom commented on Aug 14, 2025

    @mattfennerom

    We also experienced the same thing.

    We found quite a number of paths which don't get visited. Is there a reason for this? or is it just an oversight?

    If there is no reason, would you accept a PR to fix it?

  2. 178f19eb047f commented on Aug 28, 2025

    @178f19eb047f

    Hi 👋, just adding some extra details we found whilst working around this issue that may help others or even help fixing this issue. We're keen to help fix this issue more broadly but want to make sure we take the right approach for this library. We ended up working around the issue for now by implementing our own visitor that visits the all types in the AST with reflection ignoring the VisitAttribute instead of manually calling into known types that aren't being visited as we'd expect.

    Looking briefly into the rust library this project was ported from I'd expect the whole AST to be traversed. They have a test that uses (Pivot) which is one of the other types we noticed not being fully visited. We also ran a simple query containing a subquery (to use Derived) to see what the rust library did.

    Details here for the Pivot test

    Based off running VisitorTests.Visitor_Visits_Sql_Parts

    "SELECT * FROM monthly_sales \nPIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) \nORDER BY EMPID",
    [
        "PRE: STATEMENT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID",
        "PRE: TABLE FACTOR: monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)",
        "POST: TABLE FACTOR: monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)",
        "PRE: EXPR: EMPID",
        "POST: EXPR: EMPID",
        "POST: STATEMENT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID"
    ]
    

    The rust test https://github.com/apache/datafusion-sqlparser-rs/blob/main/src/ast/visitor.rs#L871

    concat!(
        "SELECT * FROM monthly_sales ",
        "PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ",
        "ORDER BY EMPID"
    ),
    vec![
        "PRE: STATEMENT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID",
        "PRE: QUERY: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID",
        "PRE: TABLE FACTOR: monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)",
        "PRE: TABLE FACTOR: monthly_sales",
        "PRE: RELATION: monthly_sales",
        "POST: RELATION: monthly_sales",
        "POST: TABLE FACTOR: monthly_sales",
        "PRE: EXPR: SUM(a.amount)",
        "PRE: EXPR: a.amount",
        "POST: EXPR: a.amount",
        "POST: EXPR: SUM(a.amount)",
        "PRE: EXPR: a.MONTH",
        "POST: EXPR: a.MONTH",
        "PRE: EXPR: 'JAN'",
        "POST: EXPR: 'JAN'",
        "PRE: EXPR: 'FEB'",
        "POST: EXPR: 'FEB'",
        "PRE: EXPR: 'MAR'",
        "POST: EXPR: 'MAR'",
        "PRE: EXPR: 'APR'",
        "POST: EXPR: 'APR'",
        "POST: TABLE FACTOR: monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)",
        "PRE: EXPR: EMPID",
        "POST: EXPR: EMPID",
        "POST: QUERY: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID",
        "POST: STATEMENT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID",
    ]
    
    Results from visitor with a simple query containing a subquery

    Query: SELECT * FROM (SELECT * FROM t1) AS s1

    C# result

    "PRE: STATEMENT: SELECT * FROM (SELECT * FROM t1) AS s1",
    "PRE: TABLE FACTOR: (SELECT * FROM t1) AS s1",
    "POST: TABLE FACTOR: (SELECT * FROM t1) AS s1",
    "POST: STATEMENT: SELECT * FROM (SELECT * FROM t1) AS s1"
    

    Rust result

    "PRE: STATEMENT: SELECT * FROM (SELECT * FROM t1) AS s1",
    "PRE: QUERY: SELECT * FROM (SELECT * FROM t1) AS s1",
    "PRE: TABLE FACTOR: (SELECT * FROM t1) AS s1",
    "PRE: QUERY: SELECT * FROM t1",
    "PRE: TABLE FACTOR: t1", 
    "PRE: RELATION: t1", 
    "POST: RELATION: t1", 
    "POST: TABLE FACTOR: t1",
    "POST: QUERY: SELECT * FROM t1", 
    "POST: TABLE FACTOR: (SELECT * FROM t1) AS s1", 
    "POST: QUERY: SELECT * FROM (SELECT * FROM t1) AS s1", 
    "POST: STATEMENT: SELECT * FROM (SELECT * FROM t1) AS s1"
    

    Before implementing a new visitor we looked to find all the types that could be failing to traverse deeper into the AST. For this we used the code in IElement.GetVisitableChildProperties and compared it to getting all the IElement properties on all of the IElement types. Both Derived and Pivot appear in the list so we suspect a majority of them have a similar issue.

    List of types and properties
    InsertOperation:
       Alias -> SqlParser.Ast.Ident
       Columns -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       AfterColumns -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       On -> SqlParser.Ast.OnInsert
       InsertAlias -> SqlParser.Ast.InsertAliases
    LateralView:
       LateralColAlias -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
    Select:
       Distinct -> SqlParser.Ast.DistinctFilter
    Analyze:
       Columns -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
    CreateFunction:
       ReturnType -> SqlParser.Ast.DataType
       FunctionBody -> SqlParser.Ast.CreateFunctionBody
       Using -> SqlParser.Ast.CreateFunctionUsing
       Language -> SqlParser.Ast.Ident
       Options -> SqlParser.Sequence`1[SqlParser.Ast.SqlOption]
       RemoteConnection -> SqlParser.Ast.ObjectName
    CreateStage:
       DirectoryTableParams -> SqlParser.Sequence`1[SqlParser.Ast.DataLoadingOption]
       FileFormat -> SqlParser.Sequence`1[SqlParser.Ast.DataLoadingOption]
       CopyOptions -> SqlParser.Sequence`1[SqlParser.Ast.DataLoadingOption]
    CreateView:
       Columns -> SqlParser.Sequence`1[SqlParser.Ast.ViewColumnDef]
       Options -> SqlParser.Ast.CreateTableOptions
       ClusterBy -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       To -> SqlParser.Ast.ObjectName
    CreateRole:
       Password -> SqlParser.Ast.Password
       InRole -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       InGroup -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       User -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       Role -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       Admin -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
    CreateSequence:
       DataType -> SqlParser.Ast.DataType
    Derived:
       SubQuery -> SqlParser.Ast.Query
    Function:
       Name -> SqlParser.Ast.ObjectName
       Args -> SqlParser.Sequence`1[SqlParser.Ast.FunctionArg]
    OpenJsonTable:
       JsonExpression -> SqlParser.Ast.Expression
       Columns -> SqlParser.Sequence`1[SqlParser.Ast.OpenJsonTableColumn]
       JsonPath -> SqlParser.Ast.Value
    NestedJoin:
       TableWithJoins -> SqlParser.Ast.TableWithJoins
    Pivot:
       ValueColumns -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       ValueSource -> SqlParser.Ast.PivotValueSource
       DefaultOnNull -> SqlParser.Ast.Expression
       PivotAlias -> SqlParser.Ast.TableAlias
    Unpivot:
       TableFactor -> SqlParser.Ast.TableFactor
       Value -> SqlParser.Ast.Ident
       Name -> SqlParser.Ast.Ident
       Columns -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       PivotAlias -> SqlParser.Ast.TableAlias
    Table:
       Partitions -> SqlParser.Sequence`1[SqlParser.Ast.Ident]
       Version -> SqlParser.Ast.TableVersion
    TableFunction:
       Expression -> SqlParser.Ast.Expression
    UnNest:
       ArrayExpressions -> SqlParser.Sequence`1[SqlParser.Ast.Expression]
       WithOffsetAlias -> SqlParser.Ast.Ident
    JsonTable:
       JsonExpression -> SqlParser.Ast.Expression
       JsonPath -> SqlParser.Ast.Value
       Columns -> SqlParser.Sequence`1[SqlParser.Ast.JsonTableColumn]
    ExpressionTable:
       Expression -> SqlParser.Ast.Expression
    MatchRecognize:
       MatchTable -> SqlParser.Ast.TableFactor
       PartitionBy -> SqlParser.Sequence`1[SqlParser.Ast.Expression]
       OrderBy -> SqlParser.Sequence`1[SqlParser.Ast.OrderByExpression]
       Measures -> SqlParser.Sequence`1[SqlParser.Ast.Measure]
       RowsPerMatch -> SqlParser.Ast.RowsPerMatch
       AfterMatchSkip -> SqlParser.Ast.AfterMatchSkip
       Pattern -> SqlParser.Ast.MatchRecognizePattern
       Symbols -> SqlParser.Sequence`1[SqlParser.Ast.SymbolDefinition]
    

    We also came across PivotValueSource which only implements IWriteSql so even if it did have the Visit attribute VisitChildren in IElement would skip if since it doesn't implement IElement. So it would be good to confirm if this is intentended or not.

    Hopefully this helps out 😄

  3. FranciscoRdslm commented on Oct 17, 2025

    @FranciscoRdslm

    Same issue for us, we need to visit all expressions in the SQL and the (Pre/Post)VisitExpression methods are not being called for expressions either in subqueries or function arguments.
    Visitor breaking on nesting makes it kind of useless for our use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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