diff --git a/Package.swift b/Package.swift index 8736dbd4..f591581e 100644 --- a/Package.swift +++ b/Package.swift @@ -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( @@ -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"), ] } diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 3834e70a..5c00df3d 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -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( @@ -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"), ] } diff --git a/Sources/SQLKit/Builders/Implementations/SQLSubqueryBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLSubqueryBuilder.swift index 7014174e..176bdb1c 100644 --- a/Sources/SQLKit/Builders/Implementations/SQLSubqueryBuilder.swift +++ b/Sources/SQLKit/Builders/Implementations/SQLSubqueryBuilder.swift @@ -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) + } +} diff --git a/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift index a301c68b..16fa00d0 100644 --- a/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift +++ b/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift @@ -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`. @@ -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 { @@ -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) } } diff --git a/Sources/SQLKit/Builders/Prototypes/SQLCommonUnionBuilder.swift b/Sources/SQLKit/Builders/Prototypes/SQLCommonUnionBuilder.swift new file mode 100644 index 00000000..ff805aae --- /dev/null +++ b/Sources/SQLKit/Builders/Prototypes/SQLCommonUnionBuilder.swift @@ -0,0 +1,133 @@ +/// Builds ``SQLUnion`` queries. Provides common behavior for ``SQLUnionBuilder`` and ``SQLUnionSubqueryBuilder``. +/// +/// > Note: This abstraction is necessary only because ``SQLUnionBuilder`` did not take the use of unions in +/// > subqueries into account in its original design; it would break public API to fix it without this workaround. +public protocol SQLCommonUnionBuilder: SQLPartialResultBuilder { + /// The union query generated by this builder. + var union: SQLUnion { get set } +} + +extension SQLCommonUnionBuilder { + // 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 } + } + + /// 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 the left query but not the right are returned, with duplicates preserved). + @inlinable + public func except(all query: SQLSelect) -> Self { + self.union.add(query, joiner: .init(type: .exceptAll)) + return self + } + + /// Call ``union(distinct:)-15xs8`` with a query generated by a builder. + @inlinable + public func union(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.union(distinct: predicate(SQLSubqueryBuilder()).select) + } + + /// Call ``union(all:)-56f28`` with a query generated by a builder. + @inlinable + public func union(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.union(all: predicate(SQLSubqueryBuilder()).select) + } + + /// Alias ``union(distinct:)-1ert0`` so it acts as the "default". + @inlinable + public func union(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.union(distinct: predicate) + } + + /// Call ``intersect(distinct:)-161s9`` with a query generated by a builder. + @inlinable + public func intersect(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.intersect(distinct: predicate(SQLSubqueryBuilder()).select) + } + + /// Call ``intersect(all:)-1wiow`` with a query generated by a builder. + @inlinable + public func intersect(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.intersect(all: predicate(SQLSubqueryBuilder()).select) + } + + /// Alias ``intersect(distinct:)-47w8a`` so it acts as the "default". + @inlinable + public func intersect(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.intersect(distinct: predicate) + } + + /// Call ``except(distinct:)-2ygq0`` with a query generated by a builder. + @inlinable + public func except(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.except(distinct: predicate(SQLSubqueryBuilder()).select) + } + + /// Call ``except(all:)-5exbl`` with a query generated by a builder. + @inlinable + public func except(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.except(all: predicate(SQLSubqueryBuilder()).select) + } + + /// Alias ``except(distinct:)-6vhbz`` so it acts as the "default". + @inlinable + public func except(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self { + try self.except(distinct: predicate) + } +} diff --git a/Sources/SQLKit/Expressions/Clauses/SQLSubquery.swift b/Sources/SQLKit/Expressions/Clauses/SQLSubquery.swift index 6c057954..50bd9d8d 100644 --- a/Sources/SQLKit/Expressions/Clauses/SQLSubquery.swift +++ b/Sources/SQLKit/Expressions/Clauses/SQLSubquery.swift @@ -23,3 +23,30 @@ public struct SQLSubquery: SQLExpression { SQLGroupExpression(self.subquery).serialize(to: &serializer) } } + +/// A trivial copy of ``SQLSubquery`` with a different type for its subquery property. +/// +/// As with ``SQLCommonUnionBuilder``, this type is only necessary because of design oversights made when the original +/// support for unions was added (the way subquery support, which was not implemented at the time, would work was not +/// anticipated, so some types got more hardcoded than was wise); we can't fix them without breaking public API, so +/// this annoying duplication of code is used as a workaround. +/// +/// See also ``SQLUnionSubqueryBuilder``. +public struct SQLUnionSubquery: SQLExpression { + /// The (sub)query. + public var subquery: SQLUnion + + /// Create a new subquery expression from a select query. + /// + /// - Parameter subquery: A ``SQLUnion`` query to use as a subquery. + @inlinable + public init(_ subquery: SQLUnion) { + self.subquery = subquery + } + + // See `SQLExpression.serialize(to:)`. + @inlinable + public func serialize(to serializer: inout SQLSerializer) { + SQLGroupExpression(self.subquery).serialize(to: &serializer) + } +} diff --git a/Tests/SQLKitTests/SQLUnionTests.swift b/Tests/SQLKitTests/SQLUnionTests.swift index a04732ee..af0b0949 100644 --- a/Tests/SQLKitTests/SQLUnionTests.swift +++ b/Tests/SQLKitTests/SQLUnionTests.swift @@ -8,7 +8,7 @@ final class SQLUnionTests: XCTestCase { XCTAssert(isLoggingConfigured) } - // MARK: Unions + // MARK: Top-level unions func testUnion_UNION() { // Check that queries are explicitly malformed without the feature flags @@ -199,4 +199,168 @@ final class SQLUnionTests: XCTestCase { self.db._dialect.unionFeatures = [.union, .unionAll] XCTAssertSerialization(of: self.db.raw("\(query)"), is: "SELECT * UNION ALL SELECT * UNION SELECT *") } + + // MARK: Subquery unions + + func testUnionSubquery_UNION() { + // Check that queries are explicitly malformed without the feature flags + self.db._dialect.unionFeatures = [] + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .union(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` SELECT ``id`` FROM ``t3``)" + ) + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .union(all: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` SELECT ``id`` FROM ``t3``)" + ) + + // Test that queries are correctly formed with the feature flags + self.db._dialect.unionFeatures.formUnion([.union, .unionAll]) + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .union(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` UNION SELECT ``id`` FROM ``t3``)" + ) + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .union(all: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` UNION ALL SELECT ``id`` FROM ``t3``)" + ) + + // Test that the explicit distinct flag is respected + self.db._dialect.unionFeatures.insert(.explicitDistinct) + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .union(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` UNION DISTINCT SELECT ``id`` FROM ``t3``)" + ) + } + + func testUnionSubquery_INTERSECT() { + // Check that queries are explicitly malformed without the feature flags + self.db._dialect.unionFeatures = [] + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .intersect(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` SELECT ``id`` FROM ``t3``)" + ) + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .intersect(all: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` SELECT ``id`` FROM ``t3``)" + ) + + // Test that queries are correctly formed with the feature flags + self.db._dialect.unionFeatures.formUnion([.intersect, .intersectAll]) + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .intersect(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` INTERSECT SELECT ``id`` FROM ``t3``)" + ) + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .intersect(all: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` INTERSECT ALL SELECT ``id`` FROM ``t3``)" + ) + + // Test that the explicit distinct flag is respected + self.db._dialect.unionFeatures.insert(.explicitDistinct) + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .intersect(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` INTERSECT DISTINCT SELECT ``id`` FROM ``t3``)" + ) + } + + func testUnionSubquery_EXCEPT() { + // Check that queries are explicitly malformed without the feature flags + self.db._dialect.unionFeatures = [] + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .except(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` SELECT ``id`` FROM ``t3``)" + ) + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .except(all: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` SELECT ``id`` FROM ``t3``)" + ) + + // Test that queries are correctly formed with the feature flags + self.db._dialect.unionFeatures.formUnion([.except, .exceptAll]) + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .except(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` EXCEPT SELECT ``id`` FROM ``t3``)" + ) + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .except(all: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` EXCEPT ALL SELECT ``id`` FROM ``t3``)" + ) + + // Test that the explicit distinct flag is respected + self.db._dialect.unionFeatures.insert(.explicitDistinct) + + XCTAssertSerialization( + of: self.db.select().column("id").from("t1").where("foo", .notIn, SQLSubquery + .union { $0 .column("id").from("t2") } + .except(distinct: { $0.column("id").from("t3") }) + .finish() + ), + is: "SELECT ``id`` FROM ``t1`` WHERE ``foo`` NOT IN (SELECT ``id`` FROM ``t2`` EXCEPT DISTINCT SELECT ``id`` FROM ``t3``)" + ) + } + }