Skip to content

Commit

Permalink
Fix several issues with UNION query support and do some test cleanup (#…
Browse files Browse the repository at this point in the history
…144)

* Fix spelling of "Codable" in filename
* Clean up SQLDialect.swift, adding lots of documentation comments and consolidating the default values.
* Simplify and normalize the SQLBenchmarker interface and adapt the existing benchmarks accordingly.
* Replace use of yet another OptionalType protocol with an explicit Optional-like enum in TestRow and update tests accordingly. Also heavy cleanup of GenericDialect.
* Fix a number of issues and missing features in UNION: Parenthesized subquery expressions are not supported by SQLite, SQLUnionBuilder did not conform to SQLQueryFetcher, no support existed for the INTERSECT and EXCEPT union types supported by Postgres and SQLite, the explicit DISTINCT keyword is not supported by SQLite. A new SQLDialect feature flag set was added to address the various syntax variations.
* Add benchmark (integration) and unit tests for the fixed UNION support
  • Loading branch information
gwynne authored Dec 14, 2021
1 parent d2027b4 commit 9cc30f8
Show file tree
Hide file tree
Showing 12 changed files with 835 additions and 284 deletions.
100 changes: 89 additions & 11 deletions Sources/SQLKit/Builders/SQLUnionBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public final class SQLUnionBuilder: SQLQueryBuilder {
public final class SQLUnionBuilder: SQLQueryBuilder, SQLQueryFetcher {
public var query: SQLExpression { self.union }

public var union: SQLUnion
Expand All @@ -9,15 +9,35 @@ public final class SQLUnionBuilder: SQLQueryBuilder {
self.database = database
}

public func union(distinct query: SQLSelect) -> Self {
self.union.add(query, all: false)
public func union(distinct query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .union))
return self
}
}

public func union(all query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .unionAll))
return self
}

public func intersect(distinct query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .intersect))
return self
}

public func union(all query: SQLSelect) -> Self {
self.union.add(query, all: true)
public func intersect(all query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .intersectAll))
return self
}
}

public func except(distinct query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .except))
return self
}

public func except(all query: SQLSelect) -> Self {
self.union.add(query, joiner: .init(type: .exceptAll))
return self
}
}

extension SQLDatabase {
Expand All @@ -31,13 +51,39 @@ extension SQLUnionBuilder {
return self.union(distinct: predicate(.init(on: self.database)).select)
}

public func union(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.union(all: predicate(.init(on: self.database)).select)
}

/// Alias the `distinct` variant so it acts as the "default".
public func union(_ predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.union(distinct: predicate)
}

public func union(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.union(all: predicate(.init(on: self.database)).select)
public func intersect(distinct predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.intersect(distinct: predicate(.init(on: self.database)).select)
}

public func intersect(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.intersect(all: predicate(.init(on: self.database)).select)
}

/// Alias the `distinct` variant so it acts as the "default".
public func intersect(_ predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.intersect(distinct: predicate)
}

public func except(distinct predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.except(distinct: predicate(.init(on: self.database)).select)
}

public func except(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.except(all: predicate(.init(on: self.database)).select)
}

/// Alias the `distinct` variant so it acts as the "default".
public func except(_ predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> Self {
return self.except(distinct: predicate)
}
}

Expand All @@ -47,14 +93,46 @@ extension SQLSelectBuilder {
.union(distinct: predicate(.init(on: self.database)).select)
}

public func union(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.union(all: predicate(.init(on: self.database)).select)
}

/// Alias the `distinct` variant so it acts as the "default".
public func union(_ predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.union(distinct: predicate(.init(on: self.database)).select)
}

public func union(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
public func intersect(distinct predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.union(all: predicate(.init(on: self.database)).select)
.intersect(distinct: predicate(.init(on: self.database)).select)
}

public func intersect(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.intersect(all: predicate(.init(on: self.database)).select)
}

/// Alias the `distinct` variant so it acts as the "default".
public func intersect(_ predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.intersect(distinct: predicate(.init(on: self.database)).select)
}

public func except(distinct predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.except(distinct: predicate(.init(on: self.database)).select)
}

public func except(all predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.except(all: predicate(.init(on: self.database)).select)
}

/// Alias the `distinct` variant so it acts as the "default".
public func except(_ predicate: (SQLSelectBuilder) -> SQLSelectBuilder) -> SQLUnionBuilder {
return SQLUnionBuilder(on: self.database, initialQuery: self.select)
.except(distinct: predicate(.init(on: self.database)).select)
}
}
79 changes: 70 additions & 9 deletions Sources/SQLKit/Query/SQLUnion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,90 @@ public struct SQLUnion: SQLExpression {
}

public mutating func add(_ query: SQLSelect, all: Bool) {
self.unions.append((.init(all: all), query))
self.add(query, joiner: .init(type: all ? .unionAll : .union))
}

public mutating func add(_ query: SQLSelect, joiner: SQLUnionJoiner) {
self.unions.append((joiner, query))
}

public func serialize(to serializer: inout SQLSerializer) {
assert(!self.unions.isEmpty, "Serializing a union with only one query is invalid.")
SQLGroupExpression(self.initialQuery).serialize(to: &serializer)
self.unions
.forEach { (joiner, select) in
joiner.serialize(to: &serializer)
SQLGroupExpression(select).serialize(to: &serializer)

serializer.statement { statement in
func appendQuery(_ query: SQLSelect) {
if statement.dialect.unionFeatures.contains(.parenthesizedSubqueries) {
statement.append(SQLGroupExpression(query))
} else {
statement.append(query)
}
}

appendQuery(self.initialQuery)
self.unions.forEach { joiner, query in
statement.append(joiner)
appendQuery(query)
}
}
}
}

/// - Note: There's no technical reason that this is an `enum` nested in a `struct` rather than just a bare
/// `enum`. It's this way because Gwynne merged a PR for an early version of this code and it was released
/// publicly before she realized there were several missing pieces; changing it now would be potentially
/// source-breaking, so it has to be left like this until the next major version.
public struct SQLUnionJoiner: SQLExpression {
public var all: Bool
public enum `Type`: Equatable, CaseIterable {
case union, unionAll, intersect, intersectAll, except, exceptAll
}

public var type: `Type`

@available(*, deprecated, message: "Use .type` instead.")
public var all: Bool {
get { [.unionAll, .intersectAll, .exceptAll].contains(self.type) }
set { switch (self.type, newValue) {
case (.union, true): self.type = .unionAll
case (.unionAll, false): self.type = .union
case (.intersect, true): self.type = .intersectAll
case (.intersectAll, false): self.type = .intersect
case (.except, true): self.type = .exceptAll
case (.exceptAll, false): self.type = .except
default: break
} }
}

@available(*, deprecated, message: "Use .init(type:)` instead.")
public init(all: Bool) {
self.all = all
self.init(type: all ? .unionAll : .union)
}

public init(type: `Type`) {
self.type = type
}

public func serialize(to serializer: inout SQLSerializer) {
serializer.write(" UNION\(self.all ? " ALL" : "") ")
func serialize(keyword: String, if flag: SQLUnionFeatures, uniqued: Bool, to statement: inout SQLStatement) {
if !statement.dialect.unionFeatures.contains(flag) {
return print("WARNING: The \(statement.dialect.name) dialect does not support \(keyword)\(uniqued ? " ALL" :"")!")
}
statement.append(keyword)
if !uniqued {
statement.append("ALL")
} else if statement.dialect.unionFeatures.contains(.explicitDistinct) {
statement.append("DISTINCT")
}
}
serializer.statement {
switch self.type {
case .union: serialize(keyword: "UNION", if: .union, uniqued: true, to: &$0)
case .unionAll: serialize(keyword: "UNION", if: .unionAll, uniqued: false, to: &$0)
case .intersect: serialize(keyword: "INTERSECT", if: .intersect, uniqued: true, to: &$0)
case .intersectAll: serialize(keyword: "INTERSECT", if: .intersectAll, uniqued: false, to: &$0)
case .except: serialize(keyword: "EXCEPT", if: .except, uniqued: true, to: &$0)
case .exceptAll: serialize(keyword: "EXCEPT", if: .exceptAll, uniqued: false, to: &$0)
}
}
}
}

Loading

0 comments on commit 9cc30f8

Please sign in to comment.