Skip to content

Support recursive CTEs in the query interface #1441

Closed
@myyra

Description

@myyra

I tried to make this into a draft PR at least, but ran into so many dead ends that I didn't feel like opening something that would've required more fixing than reviewing... So I thought it would be better to first explain what I'm trying to achieve.

What did you do?

Tried to build a recursive CTE using the query interface / without writing SQL manually.

What did you expect to happen?

I could get a hold of the CTE "table", so that I can use it in the query interface and generate the recursive part of the CTE.

Given that it's already possible to get quite close, I hope we can find a way to build the whole thing using the query interface.

What happened instead?

Somewhat obviously, I can't access the CTE before it has been initialized. And since it needs the request in the initializer, it can't be constructed with a query that uses itself. This can be avoided by defining a CTE with the same name and empty request, and using it to build a request before giving it to the actual CTE.

Environment

GRDB flavor(s): GRDB
GRDB version: 6.20.0
Installation method: SPM
Xcode version: 15.0
Swift version: 5.9
Platform(s) running GRDB: All
macOS version running Xcode: 14.0

Demo Project

A minimal test case that can be added to CommonTableExpressionTests.swift, demonstrating the closest possible solution that can be achieved currently:

func testRecursiveCounterQuery() throws {
    try makeDatabaseQueue().read { db in
        let cteName = CommonTableExpression(named: "counter", literal: "")
        let recursive = cteName.all().select(Column("x") + 1)
        let cte = CommonTableExpression(
            recursive: true,
            named: "counter",
            literal: "SELECT 0 AS x UNION ALL \(recursive)"
        )
        XCTAssertEqual(
            try cte.all().with(cte).limit(5).fetchAll(db),
            [
                ["x": 0],
                ["x": 1],
                ["x": 2],
                ["x": 3],
                ["x": 4],
            ]
        )
    }
}

A slightly longer test case that is closer to the real-world problem I'm trying to solve:

Querying a DAG like structure
func testRecursiveDAG() throws {
    struct Node: Codable, FetchableRecord, PersistableRecord, Equatable {
        var id: Int64?
        
        static let parentEdges = hasMany(Edge.self, using: Edge.childForeignKey)
        static let childEdges = hasMany(Edge.self, using: Edge.parentForeignKey)

        static let parents = hasMany(
            Node.self,
            through: parentEdges,
            using: Edge.parent,
            key: "parent"
        )

        static let children = hasMany(
            Node.self,
            through: childEdges,
            using: Edge.child,
            key: "child"
        )
    }

    struct Edge: Codable, FetchableRecord, PersistableRecord, Equatable {
        let parentId: Int64
        let childId: Int64

        static let parentForeignKey = ForeignKey(["parentId"])
        static let childForeignKey = ForeignKey(["childId"])

        static let parent = belongsTo(Node.self, using: parentForeignKey)
        static let child = belongsTo(Node.self, using: childForeignKey)
    }

    try makeDatabaseQueue().write { db in

        try db.create(table: "node") { t in
            t.autoIncrementedPrimaryKey("id")
        }
        try db.create(table: "edge") { t in
            t.primaryKey {
                t.column("parentId", .integer)
                t.column("childId", .integer)
            }
        }

        let rootNode = try Node(id: 0).inserted(db)
        let node1 = try Node(id: 1).inserted(db)
        let node2 = try Node(id: 2).inserted(db)

        try Edge(parentId: rootNode.id!, childId: node1.id!).insert(db)
        try Edge(parentId: node1.id!, childId: node2.id!).insert(db)

        let cteNameOnly = CommonTableExpression(named: "ancestor", literal: "")

        let association = Node.association(to: cteNameOnly) { node, ancestor in
            node["id"] == ancestor["id"]
        }
        let cteAlias = TableAlias()

        let initialRequest = Node.filter(key: node2.id)
        let recursiveRequest = Node
            .joining(required: Node.children.joining(required: association.aliased(cteAlias)))

        let cte = CommonTableExpression<Node>(
            recursive: true,
            named: "ancestor",
            literal: SQL("\(initialRequest) UNION ALL \(recursiveRequest)")
        )

        let results = try cte.all().with(cte).fetchAll(db)
        XCTAssertEqual(results, [node2, node1, rootNode])
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs revisitingThis issue was closed but it has unfinished business

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions