Skip to content

Commit

Permalink
Merge pull request #515 from alextrob/feature/fetchOne-limit-1
Browse files Browse the repository at this point in the history
Add "LIMIT 1" to `fetchOne` requests
  • Loading branch information
groue authored Apr 18, 2019
2 parents 1af5565 + 5af1686 commit bdf8d0b
Show file tree
Hide file tree
Showing 29 changed files with 426 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection:
- [#503](https://github.com/groue/GRDB.swift/pull/503): IFNULL support for association aggregates
- [#508](https://github.com/groue/GRDB.swift/pull/508) by [@michaelkirk-signal](https://github.com/michaelkirk-signal): Allow Database Connection Configuration
- [#510](https://github.com/groue/GRDB.swift/pull/510) by [@charlesmchen-signal](https://github.com/charlesmchen-signal): Expose DatabaseRegion(table:) initializer
- [#515](https://github.com/groue/GRDB.swift/pull/515) by [@alextrob](https://github.com/alextrob): Add "LIMIT 1" to `fetchOne` requests

### Fixed

Expand Down
10 changes: 5 additions & 5 deletions GRDB/Core/DatabaseValueConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ extension DatabaseValueConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> DatabaseValueCursor<Self> {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchCursor(statement, adapter: adapter)
}

Expand All @@ -329,7 +329,7 @@ extension DatabaseValueConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Self] {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchAll(statement, adapter: adapter)
}

Expand All @@ -348,7 +348,7 @@ extension DatabaseValueConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: true)
return try fetchOne(statement, adapter: adapter)
}
}
Expand Down Expand Up @@ -533,7 +533,7 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> NullableDatabaseValueCursor<Wrapped> {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchCursor(statement, adapter: adapter)
}

Expand All @@ -549,7 +549,7 @@ extension Optional where Wrapped: DatabaseValueConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Wrapped?] {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchAll(statement, adapter: adapter)
}
}
Expand Down
32 changes: 18 additions & 14 deletions GRDB/Core/FetchRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@
public protocol FetchRequest: DatabaseRegionConvertible {
/// The type that tells how fetched database rows should be interpreted.
associatedtype RowDecoder

/// Returns a tuple that contains a prepared statement that is ready to be
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
/// - parameter singleResult: A hint that the query should return a single
/// result. Implementations can optionally use
/// this to optimize the prepared statement.
/// - returns: A prepared statement and an eventual row adapter.
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?)
func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?)

/// Returns the number of rows fetched by the request.
///
Expand All @@ -33,6 +36,7 @@ public protocol FetchRequest: DatabaseRegionConvertible {
}

extension FetchRequest {

/// Returns an adapted request.
public func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedFetchRequest<Self> {
return AdaptedFetchRequest(self, adapter)
Expand All @@ -45,7 +49,7 @@ extension FetchRequest {
///
/// - parameter db: A database connection.
public func fetchCount(_ db: Database) throws -> Int {
let (statement, _) = try prepare(db)
let (statement, _) = try prepare(db, forSingleResult: false)
let sql = "SELECT COUNT(*) FROM (\(statement.sql))"
return try Int.fetchOne(db, sql: sql, arguments: statement.arguments)!
}
Expand All @@ -57,7 +61,7 @@ extension FetchRequest {
///
/// - parameter db: A database connection.
public func databaseRegion(_ db: Database) throws -> DatabaseRegion {
let (statement, _) = try prepare(db)
let (statement, _) = try prepare(db, forSingleResult: false)
return statement.databaseRegion
}
}
Expand All @@ -79,8 +83,8 @@ public struct AdaptedFetchRequest<Base: FetchRequest> : FetchRequest {
}

/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
let (statement, baseAdapter) = try base.prepare(db)
public func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) {
let (statement, baseAdapter) = try base.prepare(db, forSingleResult: singleResult)
if let baseAdapter = baseAdapter {
return try (statement, ChainedAdapter(first: baseAdapter, second: adapter(db)))
} else {
Expand Down Expand Up @@ -108,7 +112,7 @@ public struct AdaptedFetchRequest<Base: FetchRequest> : FetchRequest {
public struct AnyFetchRequest<T> : FetchRequest {
public typealias RowDecoder = T

private let _prepare: (Database) throws -> (SelectStatement, RowAdapter?)
private let _prepare: (Database, _ singleResult: Bool) throws -> (SelectStatement, RowAdapter?)
private let _fetchCount: (Database) throws -> Int
private let _databaseRegion: (Database) throws -> DatabaseRegion

Expand All @@ -121,26 +125,26 @@ public struct AnyFetchRequest<T> : FetchRequest {

/// Creates a request whose `prepare()` method wraps and forwards
/// operations the argument closure.
public init(_ prepare: @escaping (Database) throws -> (SelectStatement, RowAdapter?)) {
_prepare = { db in
try prepare(db)
public init(_ prepare: @escaping (Database, _ singleResult: Bool) throws -> (SelectStatement, RowAdapter?)) {
_prepare = { db, singleResult in
try prepare(db, singleResult)
}

_fetchCount = { db in
let (statement, _) = try prepare(db)
let (statement, _) = try prepare(db, false)
let sql = "SELECT COUNT(*) FROM (\(statement.sql))"
return try Int.fetchOne(db, sql: sql, arguments: statement.arguments)!
}

_databaseRegion = { db in
let (statement, _) = try prepare(db)
let (statement, _) = try prepare(db, false)
return statement.databaseRegion
}
}

/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
return try _prepare(db)
public func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) {
return try _prepare(db, singleResult)
}

/// :nodoc:
Expand Down
6 changes: 3 additions & 3 deletions GRDB/Core/Row.swift
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ extension Row {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> RowCursor {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchCursor(statement, adapter: adapter)
}

Expand All @@ -855,7 +855,7 @@ extension Row {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Row] {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchAll(statement, adapter: adapter)
}

Expand All @@ -871,7 +871,7 @@ extension Row {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Row? {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: true)
return try fetchOne(statement, adapter: adapter)
}
}
Expand Down
5 changes: 3 additions & 2 deletions GRDB/Core/SQLRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public struct SQLRequest<T> : FetchRequest {
/// prepared statement.
/// - returns: An SQLRequest
public init<Request: FetchRequest>(_ db: Database, request: Request, cached: Bool = false) throws where Request.RowDecoder == RowDecoder {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
self.init(literal: SQLLiteral(sql: statement.sql, arguments: statement.arguments), adapter: adapter, cached: cached)
}

Expand Down Expand Up @@ -125,9 +125,10 @@ public struct SQLRequest<T> : FetchRequest {
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
/// - parameter singleResult: SQLRequest disregards this hint.
///
/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
public func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) {
let statement: SelectStatement
switch cache {
case .none:
Expand Down
10 changes: 5 additions & 5 deletions GRDB/Core/StatementColumnConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> FastDatabaseValueCursor<Self> {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchCursor(statement, adapter: adapter)
}

Expand All @@ -328,7 +328,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Self] {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchAll(statement, adapter: adapter)
}

Expand All @@ -344,7 +344,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: true)
return try fetchOne(statement, adapter: adapter)
}
}
Expand Down Expand Up @@ -529,7 +529,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchCursor<R: FetchRequest>(_ db: Database, _ request: R) throws -> FastNullableDatabaseValueCursor<Wrapped> {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchCursor(statement, adapter: adapter)
}

Expand All @@ -545,7 +545,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchAll<R: FetchRequest>(_ db: Database, _ request: R) throws -> [Wrapped?] {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, forSingleResult: false)
return try fetchAll(statement, adapter: adapter)
}
}
Expand Down
21 changes: 20 additions & 1 deletion GRDB/QueryInterface/QueryInterfaceRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,17 @@ extension QueryInterfaceRequest : FetchRequest {
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
/// - parameter singleResult: A hint as to whether the query should be optimized for a single result.
/// - returns: A prepared statement and an eventual row adapter.
/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
public func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) {
var query = self.query

// Optimize query by setting a limit of 1 when appropriate
if singleResult && !query.expectsSingleResult {
query.limit = SQLLimit(limit: 1, offset: query.limit?.offset)
}

return try SQLSelectQueryGenerator(query).prepare(db)
}

Expand Down Expand Up @@ -183,6 +191,17 @@ extension QueryInterfaceRequest : DerivableRequest, AggregatingRequest {
return mapQuery { $0.filter(predicate) }
}

/// Creates a request which expects a single result.
///
/// It is unlikely you need to call this method. Its net effect is that
/// QueryInterfaceRequest does not use any `LIMIT 1` sql clause when you
/// call a `fetchOne` method.
///
/// :nodoc:
public func expectingSingleResult() -> QueryInterfaceRequest {
return mapQuery { $0.expectingSingleResult() }
}

/// Creates a request grouped according to *expressions promise*.
public func group(_ expressions: @escaping (Database) throws -> [SQLExpressible]) -> QueryInterfaceRequest {
return mapQuery { $0.group(expressions) }
Expand Down
27 changes: 24 additions & 3 deletions GRDB/QueryInterface/RequestProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,23 @@ public protocol FilteredRequest {
/// var request = Player.all()
/// request = request.filter { db in true }
func filter(_ predicate: @escaping (Database) throws -> SQLExpressible) -> Self

/// Creates a request which expects a single result.
///
/// Requests expecting a single result may ignore the second parameter of
/// the `FetchRequest.prepare(_:forSingleResult:)` method, in order to
/// produce sharply tailored SQL.
///
/// This method has a default implementation which returns self.
func expectingSingleResult() -> Self
}

/// :nodoc:
extension FilteredRequest {
public func expectingSingleResult() -> Self {
return self
}

/// Creates a request with the provided *predicate* added to the
/// eventual set of already applied predicates.
///
Expand Down Expand Up @@ -198,19 +211,21 @@ extension TableRequest where Self: FilteredRequest {

/// Creates a request with the provided primary key *predicate*.
public func filter<Sequence: Swift.Sequence>(keys: Sequence) -> Self where Sequence.Element: DatabaseValueConvertible {
var request = self
let keys = Array(keys)
let makePredicate: (Column) -> SQLExpression
switch keys.count {
case 0:
return none()
case 1:
request = request.expectingSingleResult()
makePredicate = { $0 == keys[0] }
default:
makePredicate = { keys.contains($0) }
}

let databaseTableName = self.databaseTableName
return filter { db in
return request.filter { db in
let primaryKey = try db.primaryKey(databaseTableName)
GRDBPrecondition(
primaryKey.columns.count == 1,
Expand All @@ -235,12 +250,18 @@ extension TableRequest where Self: FilteredRequest {
/// When executed, this request raises a fatal error if there is no unique
/// index on the key columns.
public func filter(keys: [[String: DatabaseValueConvertible?]]) -> Self {
guard !keys.isEmpty else {
var request = self
switch keys.count {
case 0:
return none()
case 1:
request = request.expectingSingleResult()
default:
break
}

let databaseTableName = self.databaseTableName
return filter { db in
return request.filter { db in
try keys
.map { key in
// Prevent filter(keys: [["foo": 1, "bar": 2]]) where
Expand Down
11 changes: 10 additions & 1 deletion GRDB/QueryInterface/SQLSelectQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
struct SQLSelectQuery {
var relation: SQLRelation
var isDistinct: Bool
var expectsSingleResult: Bool
var groupPromise: DatabasePromise<[SQLExpression]>?
var havingExpression: SQLExpression?
var limit: SQLLimit?

init(
relation: SQLRelation,
isDistinct: Bool = false,
expectsSingleResult: Bool = false,
groupPromise: DatabasePromise<[SQLExpression]>? = nil,
havingExpression: SQLExpression? = nil,
limit: SQLLimit? = nil)
{
self.relation = relation
self.isDistinct = isDistinct
self.expectsSingleResult = expectsSingleResult
self.groupPromise = groupPromise
self.havingExpression = havingExpression
self.limit = limit
Expand All @@ -37,6 +40,12 @@ extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest {
query.isDistinct = true
return query
}

func expectingSingleResult() -> SQLSelectQuery {
var query = self
query.expectsSingleResult = true
return query
}

func filter(_ predicate: @escaping (Database) throws -> SQLExpressible) -> SQLSelectQuery {
return mapRelation { $0.filter(predicate) }
Expand Down
Loading

0 comments on commit bdf8d0b

Please sign in to comment.