Skip to content

Commit

Permalink
Support the use of unions in subqueries (#178)
Browse files Browse the repository at this point in the history
* Bump dependency versions, remove irrelevant language feature flags
* Add somewhat hackneyed support for using unions in subqueries. Thanks to bad design choices in the original union support (my bad), this turns out to be mildly annoying to actually use and extremely painful to follow in the actual implementation.
  • Loading branch information
gwynne committed May 17, 2024
1 parent 9afdc96 commit 25d8170
Show file tree
Hide file tree
Showing 7 changed files with 436 additions and 165 deletions.
10 changes: 2 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ let package = Package(
.library(name: "SQLKitBenchmark", targets: ["SQLKitBenchmark"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -50,10 +50,4 @@ let package = Package(
var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ConciseMagicFile"),
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("ImportObjcForwardDeclarations"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableUpcomingFeature("IsolatedDefaultValues"),
.enableUpcomingFeature("GlobalConcurrency"),
.enableUpcomingFeature("StrictConcurrency"),
.enableExperimentalFeature("StrictConcurrency=complete"),
] }
8 changes: 2 additions & 6 deletions Package@swift-5.9.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ let package = Package(
.library(name: "SQLKitBenchmark", targets: ["SQLKitBenchmark"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -51,10 +51,6 @@ var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("ConciseMagicFile"),
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("ImportObjcForwardDeclarations"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableUpcomingFeature("IsolatedDefaultValues"),
.enableUpcomingFeature("GlobalConcurrency"),
.enableUpcomingFeature("StrictConcurrency"),
.enableExperimentalFeature("StrictConcurrency=complete"),
] }
84 changes: 84 additions & 0 deletions Sources/SQLKit/Builders/Implementations/SQLSubqueryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,87 @@ extension SQLSubquery {
return builder.query
}
}

/// Builds ``SQLUnion`` subqueries meant to be embedded within other queries.
public final class SQLUnionSubqueryBuilder: SQLCommonUnionBuilder {
/// The union subquery built by this builder.
public var subquery: SQLUnionSubquery

// See `SQLCommonUnionBuilder.union`.
public var union: SQLUnion {
get { self.subquery.subquery }
set { self.subquery.subquery = newValue }
}

/// Create a new ``SQLUnionSubqueryBuilder``.
@inlinable
public init(initialQuery: SQLSelect) {
self.subquery = .init(.init(initialQuery: initialQuery))
}

/// Render the builder's combined unions into an ``SQLExpression`` which may be used as a subquery.
///
/// The same effect can be achieved by writing `.union` instead of `.finish()`, but providing an
/// explicit "complete the union" API improves readability and makes the intent more explicit, whereas
/// using yet _another_ meaning of the term "union" for the _third_ time in rapid succession is nothing
/// but confusing. It was confusing enough coming up with the subquery API for unions at all.
///
/// Example:
///
/// ```swift
/// try await db.update("foos")
/// .set(SQLIdentifier("bar_id"), to: SQLSubquery
/// .union { $0
/// .column("id")
/// .from("bars")
/// .where("baz", .notEqual, "bamf")
/// }
/// .union(all: { $0
/// .column("id")
/// .from("bars")
/// .where("baz", .equal, "bop")
/// })
/// .finish()
/// )
/// .run()
/// ```
@inlinable
public func finish() -> some SQLExpression {
self.subquery
}
}

extension SQLSubquery {
/// Create a ``SQLSubquery`` expression using an inline query builder which generates the first `SELECT`
/// query in a `UNION`.
///
/// Example usage:
///
/// ```swift
/// try await db.update("foos")
/// .set(SQLIdentifier("bar_id"), to: SQLSubquery
/// .union { $0
/// .column("id")
/// .from("bars")
/// .where("baz", .notEqual, "bamf")
/// }
/// .union(all: { $0
/// .column("id")
/// .from("bars")
/// .where("baz", .equal, "bop")
/// })
/// .finish()
/// )
/// .run()
/// ```
///
/// > Note: The need to start with `.union` and call `.finish()`, rather than using ``SQLSubquery/select(_:)`` and
/// > chaining `.union()` within that builder, is the result of yet another of the design flaws making use of
/// > unions in subqueries far more involved than ought to be necessary.
@inlinable
public static func union(
_ initialBuild: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder
) rethrows -> SQLUnionSubqueryBuilder {
.init(initialQuery: try initialBuild(SQLSubqueryBuilder()).select)
}
}
173 changes: 23 additions & 150 deletions Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Builds ``SQLUnion`` queries.
public final class SQLUnionBuilder: SQLQueryBuilder, SQLQueryFetcher, SQLPartialResultBuilder {
/// The ``SQLUnion`` being built.
/// Builds top-level ``SQLUnion`` queries which may be executed on their own.
public final class SQLUnionBuilder: SQLQueryBuilder, SQLQueryFetcher, SQLCommonUnionBuilder {
// See `SQLCommonUnionBuilder.union`.
public var union: SQLUnion

// See `SQLQueryBuilder.database`.
Expand All @@ -18,77 +18,6 @@ public final class SQLUnionBuilder: SQLQueryBuilder, SQLQueryFetcher, SQLPartial
self.union = .init(initialQuery: initialQuery)
self.database = database
}

/// Add a query to the union in `UNION DISTINCT` mode
/// (all results from both queries are returned, with duplicates removed).
@inlinable
public func union(distinct query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .union))
return self
}

/// Add a query to the union in `UNION ALL` mode
/// (all results from both queries are returned, with duplicates preserved).
@inlinable
public func union(all query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .unionAll))
return self
}

/// Add a query to the union in `INTERSECT DISTINCT` mode
/// (only results that come from both queries are returned, with duplicates removed).
@inlinable
public func intersect(distinct query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .intersect))
return self
}

/// Add a query to the union in `INTERSECT ALL` mode
/// (only results that come from both queries are returned, with duplicates preserved).
@inlinable
public func intersect(all query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .intersectAll))
return self
}

/// Add a query to the union in `EXCEPT DISTINCT` mode
/// (only results that come from the left query but not the right are returned, with duplicates removed).
@inlinable
public func except(distinct query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .except))
return self
}

/// Add a query to the union in `EXCEPT ALL` mode
/// (only results that come from both queries are returned, with duplicates preserved).
@inlinable
public func except(all query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .exceptAll))
return self
}
}

extension SQLUnionBuilder {
// See `SQLPartialResultBuilder.orderBys`.
@inlinable
public var orderBys: [any SQLExpression] {
get { self.union.orderBys }
set { self.union.orderBys = newValue }
}

// See `SQLPartialResultBuilder.limit`.
@inlinable
public var limit: Int? {
get { self.union.limit }
set { self.union.limit = newValue }
}

// See `SQLPartialResultBuilder.offset`.
@inlinable
public var offset: Int? {
get { self.union.offset }
set { self.union.offset = newValue }
}
}

extension SQLDatabase {
Expand All @@ -99,114 +28,58 @@ extension SQLDatabase {
}
}

extension SQLUnionBuilder {
/// Call ``union(distinct:)-6q90v`` with a query generated by a builder.
@inlinable
public func union(distinct predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.union(distinct: predicate(.init(on: self.database)).select)
}

/// Call ``union(all:)-76ei4`` with a query generated by a builder.
@inlinable
public func union(all predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.union(all: predicate(.init(on: self.database)).select)
}

/// Alias ``union(distinct:)-79krl`` so it acts as the "default".
@inlinable
public func union(_ predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.union(distinct: predicate)
}

/// Call ``intersect(distinct:)-1i7fc`` with a query generated by a builder.
@inlinable
public func intersect(distinct predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.intersect(distinct: predicate(.init(on: self.database)).select)
}

/// Call ``intersect(all:)-5r4e9`` with a query generated by a builder.
@inlinable
public func intersect(all predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.intersect(all: predicate(.init(on: self.database)).select)
}

/// Alias ``intersect(distinct:)-1i7fc`` so it acts as the "default".
@inlinable
public func intersect(_ predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.intersect(distinct: predicate)
}

/// Call ``except(distinct:)-8pdro`` with a query generated by a builder.
@inlinable
public func except(distinct predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.except(distinct: predicate(.init(on: self.database)).select)
}

/// Call ``except(all:)-3i25o`` with a query generated by a builder.
@inlinable
public func except(all predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.except(all: predicate(.init(on: self.database)).select)
}

/// Alias ``except(distinct:)-8pdro`` so it acts as the "default".
@inlinable
public func except(_ predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> Self {
try self.except(distinct: predicate)
}
}

extension SQLSelectBuilder {
// See `SQLUnionBuilder.union(distinct:)`.
// See `SQLCommonUnionBuilder.union(distinct:)`.
@inlinable
public func union(distinct predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func union(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try .init(on: self.database, initialQuery: self.select).union(distinct: predicate)
}

// See `SQLUnionBuilder.union(all:)`.
// See `SQLCommonUnionBuilder.union(all:)`.
@inlinable
public func union(all predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func union(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try .init(on: self.database, initialQuery: self.select).union(all: predicate)
}

// See `SQLUnionBuilder.union(_:)`.
// See `SQLCommonUnionBuilder.union(_:)`.
@inlinable
public func union(_ predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func union(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try self.union(distinct: predicate)
}

// See `SQLUnionBuilder.intersect(distinct:)`.
// See `SQLCommonUnionBuilder.intersect(distinct:)`.
@inlinable
public func intersect(distinct predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func intersect(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try .init(on: self.database, initialQuery: self.select).intersect(distinct: predicate)
}

// See `SQLUnionBuilder.intersect(all:)`.
// See `SQLCommonUnionBuilder.intersect(all:)`.
@inlinable
public func intersect(all predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func intersect(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try .init(on: self.database, initialQuery: self.select).intersect(all: predicate)
}

// See `SQLUnionBuilder.intersect(_:)`.
// See `SQLCommonUnionBuilder.intersect(_:)`.
@inlinable
public func intersect(_ predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func intersect(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try self.intersect(distinct: predicate)
}

// See `SQLUnionBuilder.except(distinct:)`.
// See `SQLCommonUnionBuilder.except(distinct:)`.
@inlinable
public func except(distinct predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func except(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try .init(on: self.database, initialQuery: self.select).except(distinct: predicate)
}

// See `SQLUnionBuilder.except(all:)`.
// See `SQLCommonUnionBuilder.except(all:)`.
@inlinable
public func except(all predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func except(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try .init(on: self.database, initialQuery: self.select).except(all: predicate)
}

// See `SQLUnionBuilder.except(_:)`.
// See `SQLCommonUnionBuilder.except(_:)`.
@inlinable
public func except(_ predicate: (SQLSelectBuilder) throws -> SQLSelectBuilder) rethrows -> SQLUnionBuilder {
public func except(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> SQLUnionBuilder {
try self.except(distinct: predicate)
}
}

0 comments on commit 25d8170

Please sign in to comment.