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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement GROUP BY ... HAVING #69

Merged
merged 5 commits into from Oct 23, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
87 changes: 58 additions & 29 deletions Sources/SQLKit/Builders/SQLPredicateBuilder.swift
Expand Up @@ -13,7 +13,7 @@ public protocol SQLPredicateBuilder: class {
}

extension SQLPredicateBuilder {
/// Adds a column to column comparison to this builder's `WHERE` clause.
/// Adds a column to column comparison to this builder's `WHERE` clause by `AND`ing.
///
/// builder.where("firstName", .equal, column: "lastName")
///
Expand All @@ -22,15 +22,14 @@ extension SQLPredicateBuilder {
/// SELECT * FROM users WHERE firstName = lastName
///
/// - parameters:
/// - lhs: Left-hand column name.
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand column name.
/// - returns: Self for chaining.
/// - rhs: Right-hand side column name.
public func `where`(_ lhs: String, _ op: SQLBinaryOperator, column rhs: String) -> Self {
return self.where(SQLIdentifier(lhs), op, SQLIdentifier(rhs))
}
/// Adds a column comparison to this builder's `WHERE` clause.

/// Adds a column to encodable comparison to this builder's `WHERE` clause by `AND`ing.
///
/// builder.where("name", .equal, "Earth")
///
Expand All @@ -39,44 +38,59 @@ extension SQLPredicateBuilder {
/// SELECT * FROM planets WHERE name = ? // Earth
///
/// - parameters:
/// - lhs: Column name.
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Encodable value.
/// - returns: Self for chaining.
public func `where`(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: Encodable) -> Self {
return self.where(SQLIdentifier(lhs), op, SQLBind(rhs))
}
/// Adds an expression to the `WHERE` clause.

/// Adds a column to expression comparison to the `WHERE` clause by `AND`ing.
///
/// builder.where(.column("name"), .equal, .value("Earth"))
/// builder.where("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
public func `where`(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.where(SQLIdentifier(lhs), op, rhs)
}
/// Adds an expression to the `WHERE` clause.

/// Adds an expression to expression comparison to the `WHERE` clause by `AND`ing.
///
/// builder.where(.column("name"), .equal, .value("Earth"))
/// builder.where("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func `where`(_ lhs: SQLExpression, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.where(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}

/// Adds an expression to the `WHERE` clause.

/// Adds an expression to expression comparison, with an arbitrary
/// expression as operator, to the `WHERE` clause by `AND`ing.
///
/// builder.where(.column("name"), .equal, .value("Earth"))
/// builder.where("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func `where`(_ lhs: SQLExpression, _ op: SQLExpression, _ rhs: SQLExpression) -> Self {
return self.where(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}

/// Adds an expression to the `WHERE` clause.
/// Adds an expression to the `WHERE` clause by `AND`ing.
///
/// builder.where(.binary("name", .notEqual, .literal(.null)))
///
/// - parameters:
/// - expression: Expression to be added via `AND` to the predicate.
/// - expression: Expression to be added to the predicate.
public func `where`(_ expression: SQLExpression) -> Self {
if let existing = self.predicate {
self.predicate = SQLBinaryExpression(
Expand All @@ -90,36 +104,51 @@ extension SQLPredicateBuilder {
return self
}

/// Adds an expression to the `WHERE` clause.
/// Adds a column to expression comparison to the `WHERE` clause by `OR`ing.
///
/// builder.orWhere(.column("name"), .equal, .value("Earth"))
/// builder.orWhere("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
public func orWhere(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.orWhere(SQLIdentifier(lhs), op, rhs)
}
/// Adds an expression to the `WHERE` clause.

/// Adds an expression to expression comparison to the `WHERE` clause by `OR`ing.
///
/// builder.orWhere(.column("name"), .equal, .value("Earth"))
/// builder.orWhere("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func orWhere(_ lhs: SQLExpression, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.orWhere(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}

/// Adds an expression to the `WHERE` clause.

/// Adds an expression to expression comparison, with an arbitrary
/// expression as operator, to the `WHERE` clause by `OR`ing.
///
/// builder.orWhere(.column("name"), .equal, .value("Earth"))
/// builder.orWhere("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func orWhere(_ lhs: SQLExpression, _ op: SQLExpression, _ rhs: SQLExpression) -> Self {
return self.orWhere(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}
/// Adds an expression to the `WHERE` clause.

/// Adds an expression to the `WHERE` clause by `OR`ing.
///
/// builder.orWhere(.binary("name", .notEqual, .literal(.null)))
///
/// - parameters:
/// - expression: Expression to be added via `AND` to the predicate.
/// - expression: Expression to be added to the predicate.
public func orWhere(_ expression: SQLExpression) -> Self {
if let existing = self.predicate {
self.predicate = SQLBinaryExpression(
Expand Down
155 changes: 154 additions & 1 deletion Sources/SQLKit/Builders/SQLSelectBuilder.swift
Expand Up @@ -160,7 +160,160 @@ public final class SQLSelectBuilder: SQLQueryFetcher, SQLQueryBuilder, SQLPredic
self.select.offset = n
return self
}

}

extension SQLSelectBuilder {
/// Adds a column to column comparison to this builder's `HAVING` clause by `AND`ing.
///
/// builder.having("firstName", .equal, column: "lastName")
///
/// This method compares two _columns_.
///
/// SELECT * FROM users HAVING firstName = lastName
///
/// - parameters:
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side column name.
/// - returns: Self for chaining.
public func having(_ lhs: String, _ op: SQLBinaryOperator, column rhs: String) -> Self {
return self.having(SQLIdentifier(lhs), op, SQLIdentifier(rhs))
}

/// Adds a column to encodable comparison to this builder's `HAVING` clause by `AND`ing.
///
/// builder.having("name", .equal, "Earth")
///
/// The encodable value supplied will be bound to the query as a parameter.
///
/// SELECT * FROM planets HAVING name = ? // Earth
///
/// - parameters:
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Encodable value.
/// - returns: Self for chaining.
public func having(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: Encodable) -> Self {
return self.having(SQLIdentifier(lhs), op, SQLBind(rhs))
}

/// Adds a column to expression comparison to the `HAVING` clause by `AND`ing.
///
/// builder.having("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func having(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.having(SQLIdentifier(lhs), op, rhs)
}

/// Adds an expression to expression comparison to the `HAVING` clause by `AND`ing.
///
/// builder.having("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func having(_ lhs: SQLExpression, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.having(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}

/// Adds an expression to expression comparison, with an arbitrary
/// expression as operator, to the `HAVING` clause by `AND`ing.
///
/// builder.having("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func having(_ lhs: SQLExpression, _ op: SQLExpression, _ rhs: SQLExpression) -> Self {
return self.having(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}

/// Adds an expression to the `HAVING` clause by `AND`ing.
///
/// builder.having(.binary("name", .notEqual, .literal(.null)))
///
/// - parameters:
/// - expression: Expression to be added to the predicate.
public func having(_ expression: SQLExpression) -> Self {
if let existing = self.select.having {
self.select.having = SQLBinaryExpression(
left: existing,
op: SQLBinaryOperator.and,
right: expression
)
} else {
self.select.having = expression
}
return self
}

/// Adds a column to expression comparison to the `HAVING` clause by `OR`ing.
///
/// builder.orHaving("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side column name.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func orHaving(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.orHaving(SQLIdentifier(lhs), op, rhs)
}

/// Adds an expression to expression comparison to the `HAVING` clause by `OR`ing.
///
/// builder.orHaving("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func orHaving(_ lhs: SQLExpression, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self {
return self.orHaving(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}

/// Adds an expression to expression comparison, with an arbitrary
/// expression as operator, to the `HAVING` clause by `OR`ing.
///
/// builder.orHaving("name", .equal, .value("Earth"))
///
/// - parameters:
/// - lhs: Left-hand side expression.
/// - op: Binary operator to use for comparison.
/// - rhs: Right-hand side expression.
/// - returns: Self for chaining.
public func orHaving(_ lhs: SQLExpression, _ op: SQLExpression, _ rhs: SQLExpression) -> Self {
return self.orHaving(SQLBinaryExpression(left: lhs, op: op, right: rhs))
}

/// Adds an expression to the `HAVING` clause by `OR`ing.
///
/// builder.orHaving(.binary("name", .notEqual, .literal(.null)))
///
/// - parameters:
/// - expression: Expression to be added to the predicate.
public func orHaving(_ expression: SQLExpression) -> Self {
if let existing = self.select.having {
self.select.having = SQLBinaryExpression(
left: existing,
op: SQLBinaryOperator.or,
right: expression
)
} else {
self.select.having = expression
}
return self
}
}

// MARK: Connection
Expand Down
9 changes: 8 additions & 1 deletion Sources/SQLKit/Query/SQLSelect.swift
Expand Up @@ -13,7 +13,9 @@ public struct SQLSelect: SQLExpression {

/// Zero or more `GROUP BY` clauses.
public var groupBy: [SQLExpression]


public var having: SQLExpression?

/// Zero or more `ORDER BY` clauses.
public var orderBy: [SQLExpression]

Expand All @@ -40,6 +42,7 @@ public struct SQLSelect: SQLExpression {
self.limit = nil
self.offset = nil
self.groupBy = []
self.having = nil
self.orderBy = []
}

Expand All @@ -63,6 +66,10 @@ public struct SQLSelect: SQLExpression {
serializer.write(" GROUP BY ")
SQLList(self.groupBy).serialize(to: &serializer)
}
if let having = self.having {
serializer.write(" HAVING ")
having.serialize(to: &serializer)
}
if !self.orderBy.isEmpty {
serializer.write(" ORDER BY ")
SQLList(self.orderBy).serialize(to: &serializer)
Expand Down
10 changes: 10 additions & 0 deletions Tests/SQLKitTests/SQLKitTests.swift
Expand Up @@ -40,6 +40,16 @@ final class SQLKitTests: XCTestCase {
XCTAssert(serializer.binds.first! as! String == "Earth")
}

func testGroupByHaving() throws {
let db = TestDatabase()
try db.select().column("*")
.from("planets")
.groupBy("color")
.having("color", .equal, "blue")
.run().wait()
XCTAssertEqual(db.results[0], "SELECT * FROM `planets` GROUP BY `color` HAVING `color` = ?")
}

func testIfExists() throws {
let db = TestDatabase()

Expand Down
1 change: 1 addition & 0 deletions Tests/SQLKitTests/XCTestManifests.swift
Expand Up @@ -10,6 +10,7 @@ extension SQLKitTests {
("testLockingClause_forUpdate", testLockingClause_forUpdate),
("testLockingClause_lockInShareMode", testLockingClause_lockInShareMode),
("testRawQueryStringInterpolation", testRawQueryStringInterpolation),
("testGroupByHaving", testGroupByHaving),
("testIfExists", testIfExists),
]
}
Expand Down