Description
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])
}
}