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

Enhancements to logical operators #336

Merged
merged 4 commits into from
Apr 21, 2018
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
7 changes: 2 additions & 5 deletions GRDB/QueryInterface/QueryInterfaceRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,6 @@ extension QueryInterfaceRequest where T: TableRecord {

return QueryInterfaceRequest(query: query.mapWhereExpression { (db, expression) in
let keyPredicates: [SQLExpression] = try keys.map { key in
// Prevent filter(db, keys: [[:]])
GRDBPrecondition(!key.isEmpty, "Invalid empty key dictionary")

// Prevent filter(keys: [["foo": 1, "bar": 2]]) where
// ("foo", "bar") is not a unique key (primary key or columns of a
// unique index)
Expand All @@ -274,10 +271,10 @@ extension QueryInterfaceRequest where T: TableRecord {
// Sort key columns in the same order as the unique index
.sorted { (kv1, kv2) in lowercaseOrderedColumns.index(of: kv1.0.lowercased())! < lowercaseOrderedColumns.index(of: kv2.0.lowercased())! }
.map { (column, value) in Column(column) == value }
return SQLBinaryOperator.and.join(columnPredicates)! // not nil because columnPredicates is not empty
return columnPredicates.joined(operator: .and)
}

let keysPredicate = SQLBinaryOperator.or.join(keyPredicates)! // not nil because keyPredicates is not empty
let keysPredicate = keyPredicates.joined(operator: .or)

if let expression = expression {
return expression && keysPredicate
Expand Down
105 changes: 62 additions & 43 deletions GRDB/QueryInterface/SQLExpression+QueryInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,20 +227,6 @@ public struct SQLBinaryOperator : Hashable {
public static func == (lhs: SQLBinaryOperator, rhs: SQLBinaryOperator) -> Bool {
return lhs.sql == rhs.sql
}

// TODO: make it an extension of Sequence (like joined(separator:)) when Swift can better handle existentials
// TODO: make it public eventually
/// Return nil if expressions is empty.
func join(_ expressions: [SQLExpression]) -> SQLExpression? {
switch expressions.count {
case 0:
return nil
case 1:
return expressions[0]
default:
return SQLExpressionBinaryOperatorChain(op: self, expressions: expressions)
}
}
}

/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
Expand Down Expand Up @@ -325,23 +311,6 @@ public struct SQLExpressionBinary : SQLExpression {
return nil
}

case .and:
let lids = lhs.matchedRowIds(rowIdName: rowIdName)
let rids = rhs.matchedRowIds(rowIdName: rowIdName)
switch (lids, rids) {
case (nil, nil): return nil
case let (ids?, nil), let (nil, ids?): return ids
case let (lids?, rids?): return lids.intersection(rids)
}

case .or:
let lids = lhs.matchedRowIds(rowIdName: rowIdName)
let rids = rhs.matchedRowIds(rowIdName: rowIdName)
switch (lids, rids) {
case let (lids?, rids?): return lids.union(rids)
default: return nil
}

default:
return nil
}
Expand All @@ -354,29 +323,79 @@ public struct SQLExpressionBinary : SQLExpression {
}
}

// MARK: - SQLExpressionBinaryOperatorChain
// MARK: - SQLExpressionAnd

struct SQLExpressionBinaryOperatorChain : SQLExpression {
let op: SQLBinaryOperator
struct SQLExpressionAnd : SQLExpression {
let expressions: [SQLExpression]

// Exposed to SQLBinaryOperator.join
fileprivate init(op: SQLBinaryOperator, expressions: [SQLExpression]) {
assert(expressions.count >= 2)
self.op = op
init(_ expressions: [SQLExpression]) {
self.expressions = expressions
}

func expressionSQL(_ arguments: inout StatementArguments?) -> String {
guard let expression = expressions.first else {
// Ruby [].all? # => true
return true.sqlExpression.expressionSQL(&arguments)
}
if expressions.count == 1 {
return expression.expressionSQL(&arguments)
}
let expressionSQLs = expressions.map { $0.expressionSQL(&arguments) }
let joiner = " \(op.sql) "
return "(" + expressionSQLs.joined(separator: joiner) + ")"
return "(" + expressionSQLs.joined(separator: " AND ") + ")"
}

func qualifiedExpression(with qualifier: SQLTableQualifier) -> SQLExpression {
return SQLExpressionBinaryOperatorChain(
op: op,
expressions: expressions.map { $0.qualifiedExpression(with: qualifier) })
return SQLExpressionAnd(expressions.map { $0.qualifiedExpression(with: qualifier) })
}

func matchedRowIds(rowIdName: String?) -> Set<Int64>? {
let matchedRowIds = expressions.compactMap {
$0.matchedRowIds(rowIdName: rowIdName)
}
guard let first = matchedRowIds.first else {
return nil
}
return matchedRowIds.suffix(from: 1).reduce(into: first) { $0.formIntersection($1) }
}
}

// MARK: - SQLExpressionOr

struct SQLExpressionOr : SQLExpression {
let expressions: [SQLExpression]

init(_ expressions: [SQLExpression]) {
self.expressions = expressions
}

func expressionSQL(_ arguments: inout StatementArguments?) -> String {
guard let expression = expressions.first else {
// Ruby [].any? # => false
return false.sqlExpression.expressionSQL(&arguments)
}
if expressions.count == 1 {
return expression.expressionSQL(&arguments)
}
let expressionSQLs = expressions.map { $0.expressionSQL(&arguments) }
return "(" + expressionSQLs.joined(separator: " OR ") + ")"
}

func qualifiedExpression(with qualifier: SQLTableQualifier) -> SQLExpression {
return SQLExpressionOr(expressions.map { $0.qualifiedExpression(with: qualifier) })
}

func matchedRowIds(rowIdName: String?) -> Set<Int64>? {
if expressions.isEmpty {
return []
}
var result: Set<Int64> = []
for expr in expressions {
guard let matchedRowIds = expr.matchedRowIds(rowIdName: rowIdName) else {
return nil
}
result.formUnion(matchedRowIds)
}
return result
}
}

Expand Down
57 changes: 43 additions & 14 deletions GRDB/QueryInterface/Support/SQLOperators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -697,60 +697,52 @@ public func - (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLE

// MARK: - Logical Operators (AND, OR, NOT)

extension SQLBinaryOperator {
/// The `AND` binary operator
static let and = SQLBinaryOperator("AND")

/// The `OR` binary operator
static let or = SQLBinaryOperator("OR")
}

/// A logical SQL expression with the `AND` SQL operator.
///
/// // favorite AND 0
/// Column("favorite") && false
public func && (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression {
return SQLExpressionBinary(.and, lhs.sqlExpression, rhs.sqlExpression)
return SQLExpressionAnd([lhs.sqlExpression, rhs.sqlExpression])
}

/// A logical SQL expression with the `AND` SQL operator.
///
/// // 0 AND favorite
/// false && Column("favorite")
public func && (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression {
return SQLExpressionBinary(.and, lhs.sqlExpression, rhs.sqlExpression)
return SQLExpressionAnd([lhs.sqlExpression, rhs.sqlExpression])
}

/// A logical SQL expression with the `AND` SQL operator.
///
/// // email IS NOT NULL AND favorite
/// Column("email") != nil && Column("favorite")
public func && (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression {
return SQLExpressionBinary(.and, lhs.sqlExpression, rhs.sqlExpression)
return SQLExpressionAnd([lhs.sqlExpression, rhs.sqlExpression])
}

/// A logical SQL expression with the `OR` SQL operator.
///
/// // favorite OR 1
/// Column("favorite") || true
public func || (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression {
return SQLExpressionBinary(.or, lhs.sqlExpression, rhs.sqlExpression)
return SQLExpressionOr([lhs.sqlExpression, rhs.sqlExpression])
}

/// A logical SQL expression with the `OR` SQL operator.
///
/// // 0 OR favorite
/// true || Column("favorite")
public func || (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression {
return SQLExpressionBinary(.or, lhs.sqlExpression, rhs.sqlExpression)
return SQLExpressionOr([lhs.sqlExpression, rhs.sqlExpression])
}

/// A logical SQL expression with the `OR` SQL operator.
///
/// // email IS NULL OR hidden
/// Column("email") == nil || Column("hidden")
public func || (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression {
return SQLExpressionBinary(.or, lhs.sqlExpression, rhs.sqlExpression)
return SQLExpressionOr([lhs.sqlExpression, rhs.sqlExpression])
}

/// A negated logical SQL expression with the `NOT` SQL operator.
Expand All @@ -766,6 +758,43 @@ public prefix func ! (value: SQLSpecificExpressible) -> SQLExpression {
return value.sqlExpression.negated
}

public enum SQLLogicalBinaryOperator {
case and, or
}

extension Sequence where Element == SQLExpression {
/// Returns an expression by joining all elements with an SQL
/// logical operator.
///
/// For example:
///
/// // SELECT * FROM players
/// // WHERE (registered
/// // AND (score >= 1000)
/// // AND (name IS NOT NULL))
/// let conditions = [
/// Column("registered"),
/// Column("score") >= 1000,
/// Column("name") != nil]
/// Player.filter(conditions.joined(operator: .and))
///
/// When the sequence is empty, `joined(operator: .and)` returns true,
/// and `joined(operator: .or)` returns false:
///
/// // SELECT * FROM players WHERE 1
/// Player.filter([].joined(operator: .and))
///
/// // SELECT * FROM players WHERE 0
/// Player.filter([].joined(operator: .or))
public func joined(operator: SQLLogicalBinaryOperator) -> SQLExpression {
let expressions = Array(self)
switch `operator` {
case .and: return SQLExpressionAnd(expressions)
case .or: return SQLExpressionOr(expressions)
}
}
}


// MARK: - Like Operator

Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3423,6 +3423,27 @@ Feed [requests](#requests) with SQL expressions built from your Swift code:
// SELECT * FROM players WHERE ((NOT verified) OR (score < 1000))
Player.filter(!verifiedColumn || scoreColumn < 1000)
```

When you want to join a sequence of expressions with `AND` or `OR` operators, use `joined(operator:)`:

```swift
// SELECT * FROM players WHERE (verified AND (score >= 1000) AND (name IS NOT NULL))
let conditions = [
verifiedColumn,
scoreColumn >=< 1000,
nameColumn != nil]
Player.filter(conditions.joined(operator: .and))
```

When the sequence is empty, `joined(operator: .and)` returns true, and `joined(operator: .or)` returns false:

```swift
// SELECT * FROM players WHERE 1
Player.filter([].joined(operator: .and))

// SELECT * FROM players WHERE 0
Player.filter([].joined(operator: .or))
```

- `BETWEEN`, `IN`, `NOT IN`

Expand Down
34 changes: 34 additions & 0 deletions Tests/GRDBTests/QueryInterfaceExpressionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,40 @@ class QueryInterfaceExpressionsTests: GRDBTestCase {
"SELECT * FROM \"readers\" WHERE (NOT (\"age\" > 18) AND NOT (\"name\" > 'foo'))")
}

func testJoinedOperatorAnd() throws {
let dbQueue = try makeDatabaseQueue()

XCTAssertEqual(
sql(dbQueue, tableRequest.filter([].joined(operator: .and))),
"SELECT * FROM \"readers\" WHERE 1")
XCTAssertEqual(
sql(dbQueue, tableRequest.filter([Col.id == 1].joined(operator: .and))),
"SELECT * FROM \"readers\" WHERE (\"id\" = 1)")
XCTAssertEqual(
sql(dbQueue, tableRequest.filter([Col.id == 1, Col.name != nil].joined(operator: .and))),
"SELECT * FROM \"readers\" WHERE ((\"id\" = 1) AND (\"name\" IS NOT NULL))")
XCTAssertEqual(
sql(dbQueue, tableRequest.filter([Col.id == 1, Col.name != nil, Col.age == nil].joined(operator: .and))),
"SELECT * FROM \"readers\" WHERE ((\"id\" = 1) AND (\"name\" IS NOT NULL) AND (\"age\" IS NULL))")
}

func testJoinedOperatorOr() throws {
let dbQueue = try makeDatabaseQueue()

XCTAssertEqual(
sql(dbQueue, tableRequest.filter([].joined(operator: .or))),
"SELECT * FROM \"readers\" WHERE 0")
XCTAssertEqual(
sql(dbQueue, tableRequest.filter([Col.id == 1].joined(operator: .or))),
"SELECT * FROM \"readers\" WHERE (\"id\" = 1)")
XCTAssertEqual(
sql(dbQueue, tableRequest.filter([Col.id == 1, Col.name != nil].joined(operator: .or))),
"SELECT * FROM \"readers\" WHERE ((\"id\" = 1) OR (\"name\" IS NOT NULL))")
XCTAssertEqual(
sql(dbQueue, tableRequest.filter([Col.id == 1, Col.name != nil, Col.age == nil].joined(operator: .or))),
"SELECT * FROM \"readers\" WHERE ((\"id\" = 1) OR (\"name\" IS NOT NULL) OR (\"age\" IS NULL))")
}


// MARK: - String functions

Expand Down