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

Add "LIMIT 1" to fetchOne requests #515

Merged
merged 22 commits into from
Apr 18, 2019
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f9868bd
Update tests to expect "LIMIT 1" where appropriate
alextrob Apr 11, 2019
af1fd6b
More tests for presence of LIMIT 1
alextrob Apr 11, 2019
072fa25
Add hints for use in request preparation
alextrob Apr 11, 2019
dd139b6
Fix testing lastSQLQuery when a cursor hasn't retrieved a record
alextrob Apr 11, 2019
207b90f
Update FetchRequest protocol in README
alextrob Apr 11, 2019
088f6c9
Update inline docs
alextrob Apr 11, 2019
20aecd5
Add failing test for fetchOne with a non-zero offset
alextrob Apr 16, 2019
7070ded
Replace FetchRequestHint with a boolean for single results
alextrob Apr 16, 2019
235a612
Remove LIMIT 1 from expected SQL when filtering by key
alextrob Apr 16, 2019
e2211ac
Exclude LIMIT 1 in more cases
alextrob Apr 16, 2019
1b3f0e8
Update FetchRequest protocol in README
alextrob Apr 16, 2019
46791c8
Remove QueryInterfaceRequest.init(query:)
groue Apr 16, 2019
4716f91
Bake the single result expectation right into `filter(key:)` methods
groue Apr 16, 2019
693e6da
Do not test that fetchOne does not add LIMIT 1 to raw SQL requests
groue Apr 17, 2019
e3a0935
Add SQL tests for all record testFetchOne…() methods
groue Apr 17, 2019
65fe2bb
Tests for the singleResult hint and fetching methods
groue Apr 17, 2019
21c0175
Tests for the singleResult hint and fetchCount
groue Apr 17, 2019
a128300
Tests for the singleResult hint and databaseRegion
groue Apr 17, 2019
3f8047f
Tests for the singleResult hint and SQLRequest.init(_:request:)
groue Apr 17, 2019
a172473
CHANGELOG
groue Apr 17, 2019
2711b9b
Merge branch 'GRDB-4.0' into feature/fetchOne-limit-1
groue Apr 18, 2019
5af1686
Thank you notice
groue Apr 18, 2019
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
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