Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support the use of unions in subqueries #178

Merged
merged 3 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"),
] }
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)
}
}
Loading
Loading