diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f44b2701..674d671517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index ad66f58634..e0be44e980 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -313,7 +313,7 @@ extension DatabaseValueConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchCursor(_ db: Database, _ request: R) throws -> DatabaseValueCursor { - let (statement, adapter) = try request.prepare(db) + let (statement, adapter) = try request.prepare(db, forSingleResult: false) return try fetchCursor(statement, adapter: adapter) } @@ -329,7 +329,7 @@ extension DatabaseValueConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchAll(_ 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) } @@ -348,7 +348,7 @@ extension DatabaseValueConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchOne(_ 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) } } @@ -533,7 +533,7 @@ extension Optional where Wrapped: DatabaseValueConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchCursor(_ db: Database, _ request: R) throws -> NullableDatabaseValueCursor { - let (statement, adapter) = try request.prepare(db) + let (statement, adapter) = try request.prepare(db, forSingleResult: false) return try fetchCursor(statement, adapter: adapter) } @@ -549,7 +549,7 @@ extension Optional where Wrapped: DatabaseValueConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchAll(_ 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) } } diff --git a/GRDB/Core/FetchRequest.swift b/GRDB/Core/FetchRequest.swift index bd944ddf29..70b3f21705 100644 --- a/GRDB/Core/FetchRequest.swift +++ b/GRDB/Core/FetchRequest.swift @@ -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. /// @@ -33,6 +36,7 @@ public protocol FetchRequest: DatabaseRegionConvertible { } extension FetchRequest { + /// Returns an adapted request. public func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedFetchRequest { return AdaptedFetchRequest(self, adapter) @@ -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)! } @@ -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 } } @@ -79,8 +83,8 @@ public struct AdaptedFetchRequest : 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 { @@ -108,7 +112,7 @@ public struct AdaptedFetchRequest : FetchRequest { public struct AnyFetchRequest : 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 @@ -121,26 +125,26 @@ public struct AnyFetchRequest : 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: diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index eb33c8608f..bc03818e4b 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -839,7 +839,7 @@ extension Row { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchCursor(_ 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) } @@ -855,7 +855,7 @@ extension Row { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchAll(_ 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) } @@ -871,7 +871,7 @@ extension Row { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchOne(_ 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) } } diff --git a/GRDB/Core/SQLRequest.swift b/GRDB/Core/SQLRequest.swift index 7e0a59db43..46efea693d 100644 --- a/GRDB/Core/SQLRequest.swift +++ b/GRDB/Core/SQLRequest.swift @@ -90,7 +90,7 @@ public struct SQLRequest : FetchRequest { /// prepared statement. /// - returns: An SQLRequest public init(_ 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) } @@ -125,9 +125,10 @@ public struct SQLRequest : 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: diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 2ee10eae04..40f16a4fb9 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -312,7 +312,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchCursor(_ db: Database, _ request: R) throws -> FastDatabaseValueCursor { - let (statement, adapter) = try request.prepare(db) + let (statement, adapter) = try request.prepare(db, forSingleResult: false) return try fetchCursor(statement, adapter: adapter) } @@ -328,7 +328,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchAll(_ 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) } @@ -344,7 +344,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchOne(_ 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) } } @@ -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(_ db: Database, _ request: R) throws -> FastNullableDatabaseValueCursor { - let (statement, adapter) = try request.prepare(db) + let (statement, adapter) = try request.prepare(db, forSingleResult: false) return try fetchCursor(statement, adapter: adapter) } @@ -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(_ 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) } } diff --git a/GRDB/QueryInterface/QueryInterfaceRequest.swift b/GRDB/QueryInterface/QueryInterfaceRequest.swift index da70db0109..886b571932 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -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) } @@ -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) } diff --git a/GRDB/QueryInterface/RequestProtocols.swift b/GRDB/QueryInterface/RequestProtocols.swift index 7cf40c8df2..13a408323d 100644 --- a/GRDB/QueryInterface/RequestProtocols.swift +++ b/GRDB/QueryInterface/RequestProtocols.swift @@ -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. /// @@ -198,19 +211,21 @@ extension TableRequest where Self: FilteredRequest { /// Creates a request with the provided primary key *predicate*. public func filter(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, @@ -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 diff --git a/GRDB/QueryInterface/SQLSelectQuery.swift b/GRDB/QueryInterface/SQLSelectQuery.swift index eaeaab1505..66b0b64231 100644 --- a/GRDB/QueryInterface/SQLSelectQuery.swift +++ b/GRDB/QueryInterface/SQLSelectQuery.swift @@ -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 @@ -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) } diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index a01e63bf3b..6297393125 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -281,7 +281,7 @@ extension FetchableRecord { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchCursor(_ db: Database, _ request: R) throws -> RecordCursor { - let (statement, adapter) = try request.prepare(db) + let (statement, adapter) = try request.prepare(db, forSingleResult: false) return try fetchCursor(statement, adapter: adapter) } @@ -297,7 +297,7 @@ extension FetchableRecord { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchAll(_ 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) } @@ -313,7 +313,7 @@ extension FetchableRecord { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchOne(_ 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) } } diff --git a/README.md b/README.md index e90efb16b6..d6dca7ae8b 100644 --- a/README.md +++ b/README.md @@ -4612,7 +4612,7 @@ protocol FetchRequest: DatabaseRegionConvertible { associatedtype RowDecoder /// A tuple that contains 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?) /// The number of rows fetched by the request. func fetchCount(_ db: Database) throws -> Int @@ -4621,7 +4621,7 @@ protocol FetchRequest: DatabaseRegionConvertible { When the `RowDecoder` associated type is [Row](#fetching-rows), or a [value](#value-queries), or a type that conforms to [FetchableRecord], the request can fetch: see [Fetching From Custom Requests](#fetching-from-custom-requests) below. -The `prepare` method returns a prepared statement and an optional row adapter. The [prepared statement](#prepared-statements) tells which SQL query should be executed. The row adapter helps presenting the fetched rows in the way expected by the row decoders (see [row adapter](#row-adapters)). +The `prepare(_:forSingleResult:)` method accepts a database connection, a `singleResult` hint, and returns a prepared statement and an optional row adapter. Conforming types can use the `singleResult` hint as an optimization opportunity, and return a [prepared statement](#prepared-statements) that fetches at most one row, with a `LIMIT` SQL clause, when possible. The optional row adapter helps presenting the fetched rows in the way expected by the row decoders (see [row adapters](#row-adapters)). The `fetchCount` method has a default implementation that builds a correct but naive SQL query from the statement returned by `prepare`: `SELECT COUNT(*) FROM (...)`. Adopting types can refine the counting SQL by customizing their `fetchCount` implementation. @@ -8657,7 +8657,7 @@ Sample Code **Thanks** - [Pierlis](http://pierlis.com), where we write great software. -- [@bellebethcooper](https://github.com/bellebethcooper), [@bfad](https://github.com/bfad), [@cfilipov](https://github.com/cfilipov), [@charlesmchen-signal](https://github.com/charlesmchen-signal), [@Chiliec](https://github.com/Chiliec), [@darrenclark](https://github.com/darrenclark), [@davidkraus](https://github.com/davidkraus), [@fpillet](http://github.com/fpillet), [@gusrota](https://github.com/gusrota), [@hartbit](https://github.com/hartbit), [@kdubb](https://github.com/kdubb), [@kluufger](https://github.com/kluufger), [@KyleLeneau](https://github.com/KyleLeneau), [@Marus](https://github.com/Marus), [@michaelkirk-signal](https://github.com/michaelkirk-signal), [@pakko972](https://github.com/pakko972), [@peter-ss](https://github.com/peter-ss), [@pierlo](https://github.com/pierlo), [@pocketpixels](https://github.com/pocketpixels), [@schveiguy](https://github.com/schveiguy), [@SD10](https://github.com/SD10), [@sobri909](https://github.com/sobri909), [@sroddy](https://github.com/sroddy), [@swiftlyfalling](https://github.com/swiftlyfalling), [@valexa](https://github.com/valexa), and [@zmeyc](https://github.com/zmeyc) for their contributions, help, and feedback on GRDB. +- [@alextrob](https://github.com/alextrob), [@bellebethcooper](https://github.com/bellebethcooper), [@bfad](https://github.com/bfad), [@cfilipov](https://github.com/cfilipov), [@charlesmchen-signal](https://github.com/charlesmchen-signal), [@Chiliec](https://github.com/Chiliec), [@darrenclark](https://github.com/darrenclark), [@davidkraus](https://github.com/davidkraus), [@fpillet](http://github.com/fpillet), [@gusrota](https://github.com/gusrota), [@hartbit](https://github.com/hartbit), [@kdubb](https://github.com/kdubb), [@kluufger](https://github.com/kluufger), [@KyleLeneau](https://github.com/KyleLeneau), [@Marus](https://github.com/Marus), [@michaelkirk-signal](https://github.com/michaelkirk-signal), [@pakko972](https://github.com/pakko972), [@peter-ss](https://github.com/peter-ss), [@pierlo](https://github.com/pierlo), [@pocketpixels](https://github.com/pocketpixels), [@schveiguy](https://github.com/schveiguy), [@SD10](https://github.com/SD10), [@sobri909](https://github.com/sobri909), [@sroddy](https://github.com/sroddy), [@swiftlyfalling](https://github.com/swiftlyfalling), [@valexa](https://github.com/valexa), and [@zmeyc](https://github.com/zmeyc) for their contributions, help, and feedback on GRDB. - [@aymerick](https://github.com/aymerick) and [@kali](https://github.com/kali) because SQL. - [ccgus/fmdb](https://github.com/ccgus/fmdb) for its excellency. diff --git a/Tests/GRDBTests/AssociationBelongsToRowScopeTests.swift b/Tests/GRDBTests/AssociationBelongsToRowScopeTests.swift index 865607b7f4..a49d8f518d 100644 --- a/Tests/GRDBTests/AssociationBelongsToRowScopeTests.swift +++ b/Tests/GRDBTests/AssociationBelongsToRowScopeTests.swift @@ -47,7 +47,7 @@ class AssociationBelongsToRowScopeTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let request = Player.joining(required: Player.defaultTeam) - let (_, adapter) = try request.prepare(db) + let (_, adapter) = try request.prepare(db, forSingleResult: false) XCTAssertNil(adapter) } } diff --git a/Tests/GRDBTests/AssociationHasOneThroughRowScopeTests.swift b/Tests/GRDBTests/AssociationHasOneThroughRowScopeTests.swift index 27e2f39342..575bf009df 100644 --- a/Tests/GRDBTests/AssociationHasOneThroughRowScopeTests.swift +++ b/Tests/GRDBTests/AssociationHasOneThroughRowScopeTests.swift @@ -63,7 +63,7 @@ class AssociationHasOneThroughRowscopeTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let request = A.joining(required: A.defaultC) - let (_, adapter) = try request.prepare(db) + let (_, adapter) = try request.prepare(db, forSingleResult: false) XCTAssertNil(adapter) } } diff --git a/Tests/GRDBTests/CompilationProtocolTests.swift b/Tests/GRDBTests/CompilationProtocolTests.swift index f92531a469..ebcb93ff57 100644 --- a/Tests/GRDBTests/CompilationProtocolTests.swift +++ b/Tests/GRDBTests/CompilationProtocolTests.swift @@ -112,7 +112,7 @@ private class UserPersistableRecord2 : PersistableRecord { private struct UserRowRequest : FetchRequest { struct CustomType { } typealias RowDecoder = CustomType - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { preconditionFailure() } + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { preconditionFailure() } } // MARK: - FetchableRecord diff --git a/Tests/GRDBTests/FetchRequestTests.swift b/Tests/GRDBTests/FetchRequestTests.swift index df38746dd9..0768afc62e 100644 --- a/Tests/GRDBTests/FetchRequestTests.swift +++ b/Tests/GRDBTests/FetchRequestTests.swift @@ -12,7 +12,7 @@ class FetchRequestTests: GRDBTestCase { func testRequestFetchRows() throws { struct CustomRequest : FetchRequest { typealias RowDecoder = Row - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { return try (db.makeSelectStatement(sql: "SELECT * FROM table1"), nil) } } @@ -37,7 +37,7 @@ class FetchRequestTests: GRDBTestCase { func testRequestFetchValues() throws { struct CustomRequest : FetchRequest { typealias RowDecoder = Int - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { return try (db.makeSelectStatement(sql: "SELECT id FROM table1"), nil) } } @@ -65,7 +65,7 @@ class FetchRequestTests: GRDBTestCase { } struct CustomRequest : FetchRequest { typealias RowDecoder = CustomRecord - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { return try (db.makeSelectStatement(sql: "SELECT id FROM table1"), nil) } } @@ -90,7 +90,7 @@ class FetchRequestTests: GRDBTestCase { func testRequestFetchCount() throws { struct CustomRequest : FetchRequest { typealias RowDecoder = Row - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { return try (db.makeSelectStatement(sql: "SELECT * FROM table1"), nil) } } @@ -113,7 +113,7 @@ class FetchRequestTests: GRDBTestCase { func testRequestCustomizedFetchCount() throws { struct CustomRequest : FetchRequest { typealias RowDecoder = Row - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { return try (db.makeSelectStatement(sql: "INVALID"), nil) } @@ -136,4 +136,244 @@ class FetchRequestTests: GRDBTestCase { XCTAssertEqual(count, 2) } } + + // Test for the `singleResult` parameter of + // FetchRequest.prepare(_:singleResult:) + func testSingleResultHint() throws { + struct CustomRequest: FetchRequest { + typealias RowDecoder = T + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { + if singleResult { + return try (db.makeSelectStatement(sql: "SELECT 'single' AS hint"), nil) + } else { + return try (db.makeSelectStatement(sql: "SELECT 'multiple' AS hint"), nil) + } + } + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + // Row + do { + do { + let request = CustomRequest() + do { + let rows = try request.fetchAll(db) + XCTAssertEqual(rows.count, 1) + XCTAssertEqual(rows[0], ["hint": "multiple"]) + } + do { + let rows = try request.fetchCursor(db) + while let row = try rows.next() { + XCTAssertEqual(row, ["hint": "multiple"]) + } + } + do { + let row = try request.fetchOne(db)! + XCTAssertEqual(row, ["hint": "single"]) + } + } + do { + let request = CustomRequest() + do { + let rows = try Row.fetchAll(db, request) + XCTAssertEqual(rows.count, 1) + XCTAssertEqual(rows[0], ["hint": "multiple"]) + } + do { + let rows = try Row.fetchCursor(db, request) + while let row = try rows.next() { + XCTAssertEqual(row, ["hint": "multiple"]) + } + } + do { + let row = try Row.fetchOne(db, request)! + XCTAssertEqual(row, ["hint": "single"]) + } + } + } + + // DatabaseValueConvertible + do { + struct Value: DatabaseValueConvertible { + var string: String + var databaseValue: DatabaseValue { return string.databaseValue } + static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Value? { + return String.fromDatabaseValue(dbValue).map(Value.init) + } + } + do { + let request = CustomRequest() + do { + let values = try request.fetchAll(db) + XCTAssertEqual(values.count, 1) + XCTAssertEqual(values[0].string, "multiple") + } + do { + let values = try request.fetchCursor(db) + while let value = try values.next() { + XCTAssertEqual(value.string, "multiple") + } + } + do { + let value = try request.fetchOne(db)! + XCTAssertEqual(value.string, "single") + } + } + do { + let request = CustomRequest() + do { + let values = try Value.fetchAll(db, request) + XCTAssertEqual(values.count, 1) + XCTAssertEqual(values[0].string, "multiple") + } + do { + let values = try Value.fetchCursor(db, request) + while let value = try values.next() { + XCTAssertEqual(value.string, "multiple") + } + } + do { + let value = try Value.fetchOne(db, request)! + XCTAssertEqual(value.string, "single") + } + } + } + + // DatabaseValueConvertible + StatementColumnConvertible + do { + struct Value: DatabaseValueConvertible, StatementColumnConvertible { + var string: String + init(string: String) { + self.string = string + } + init(sqliteStatement: SQLiteStatement, index: Int32) { + self.init(string: String(sqliteStatement: sqliteStatement, index: index)) + } + var databaseValue: DatabaseValue { return string.databaseValue } + static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Value? { + return String.fromDatabaseValue(dbValue).map { Value(string: $0) } + } + } + do { + let request = CustomRequest() + do { + let values = try request.fetchAll(db) + XCTAssertEqual(values.count, 1) + XCTAssertEqual(values[0].string, "multiple") + } + do { + let values = try request.fetchCursor(db) + while let value = try values.next() { + XCTAssertEqual(value.string, "multiple") + } + } + do { + let value = try request.fetchOne(db)! + XCTAssertEqual(value.string, "single") + } + } + do { + let request = CustomRequest() + do { + let values = try Value.fetchAll(db, request) + XCTAssertEqual(values.count, 1) + XCTAssertEqual(values[0].string, "multiple") + } + do { + let values = try Value.fetchCursor(db, request) + while let value = try values.next() { + XCTAssertEqual(value.string, "multiple") + } + } + do { + let value = try Value.fetchOne(db, request)! + XCTAssertEqual(value.string, "single") + } + } + } + + // FetchableRecord + do { + struct Record: FetchableRecord { + var string: String + init(row: Row) { + string = row[0] + } + } + do { + let request = CustomRequest() + do { + let records = try request.fetchAll(db) + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0].string, "multiple") + } + do { + let records = try request.fetchCursor(db) + while let record = try records.next() { + XCTAssertEqual(record.string, "multiple") + } + } + do { + let record = try request.fetchOne(db)! + XCTAssertEqual(record.string, "single") + } + } + do { + let request = CustomRequest() + do { + let records = try Record.fetchAll(db, request) + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0].string, "multiple") + } + do { + let records = try Record.fetchCursor(db, request) + while let record = try records.next() { + XCTAssertEqual(record.string, "multiple") + } + } + do { + let record = try Record.fetchOne(db, request)! + XCTAssertEqual(record.string, "single") + } + } + } + } + } + + func testSingleResultHintIsNotUsedForDefaultFetchCount() throws { + struct CustomRequest: FetchRequest { + typealias RowDecoder = Void + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { + if singleResult { fatalError("not implemented") } + return try (db.makeSelectStatement(sql: "SELECT 'multiple'"), nil) + } + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + let request = CustomRequest() + _ = try request.fetchCount(db) + XCTAssertEqual(lastSQLQuery, "SELECT COUNT(*) FROM (SELECT 'multiple')") + } + } + + func testSingleResultHintIsNotUsedForDefaultDatabaseRegion() throws { + struct CustomRequest: FetchRequest { + typealias RowDecoder = Void + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { + if singleResult { fatalError("not implemented") } + return try (db.makeSelectStatement(sql: "SELECT * FROM multiple"), nil) + } + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.execute(sql: "CREATE TABLE multiple(a)") + + let request = CustomRequest() + let region = try request.databaseRegion(db) + XCTAssertEqual(region.description, "multiple(a)") + } + } } diff --git a/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift index b1cb52e657..a6147b7b5e 100644 --- a/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift @@ -92,7 +92,7 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase { do { let reader = try request.fetchOne(db)! - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1") XCTAssertEqual(reader.id!, arthur.id!) XCTAssertEqual(reader.name, arthur.name) XCTAssertEqual(reader.age, arthur.age) @@ -100,10 +100,12 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase { do { let names = try request.fetchCursor(db).map { $0.name } - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") XCTAssertEqual(try names.next()!, arthur.name) XCTAssertEqual(try names.next()!, barbara.name) XCTAssertTrue(try names.next() == nil) + + // validate query *after* cursor has retrieved a record + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") } } } @@ -130,7 +132,7 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase { do { let reader = try Reader.fetchOne(db)! - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1") XCTAssertEqual(reader.id!, arthur.id!) XCTAssertEqual(reader.name, arthur.name) XCTAssertEqual(reader.age, arthur.age) @@ -139,10 +141,20 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase { do { let cursor = try Reader.fetchCursor(db) let names = cursor.map { $0.name } - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") XCTAssertEqual(try names.next()!, arthur.name) XCTAssertEqual(try names.next()!, barbara.name) XCTAssertTrue(try names.next() == nil) + + // validate query *after* cursor has retrieved a record + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") + } + + do { + let reader = try Reader.limit(1, offset: 1).fetchOne(db)! + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1 OFFSET 1") + XCTAssertEqual(reader.id!, barbara.id!) + XCTAssertEqual(reader.name, barbara.name) + XCTAssertEqual(reader.age, barbara.age) } } } @@ -171,7 +183,7 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase { do { let reader = try AltReader.fetchOne(db, request)! - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1") XCTAssertEqual(reader.id!, arthur.id!) XCTAssertEqual(reader.name, arthur.name) XCTAssertEqual(reader.age, arthur.age) @@ -179,10 +191,12 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase { do { let names = try AltReader.fetchCursor(db, request).map { $0.name } - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") XCTAssertEqual(try names.next()!, arthur.name) XCTAssertEqual(try names.next()!, barbara.name) XCTAssertTrue(try names.next() == nil) + + // validate query *after* cursor has retrieved a record + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") } } } diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 23b0b2165b..f24c25f59a 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -146,7 +146,7 @@ class GRDBTestCase: XCTestCase { // Compare SQL strings (ignoring leading and trailing white space and semicolons. func assertEqualSQL(_ db: Database, _ request: Request, _ sql: String, file: StaticString = #file, line: UInt = #line) throws { - let (statement, _) = try request.prepare(db) + let (statement, _) = try request.prepare(db, forSingleResult: false) try statement.makeCursor().next() assertEqualSQL(lastSQLQuery, sql, file: file, line: line) } @@ -160,7 +160,7 @@ class GRDBTestCase: XCTestCase { func sql(_ databaseReader: DatabaseReader, _ request: Request) -> String { return try! databaseReader.unsafeRead { db in - let (statement, _) = try request.prepare(db) + let (statement, _) = try request.prepare(db, forSingleResult: false) try statement.makeCursor().next() return lastSQLQuery } diff --git a/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift b/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift index 42ada6df2e..209f5c0561 100644 --- a/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift +++ b/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift @@ -67,7 +67,7 @@ class QueryInterfaceExtensibilityTests: GRDBTestCase { let request = Record.select(strftime("%Y", Column("date"))) let year = try Int.fetchOne(db, request) XCTAssertEqual(year, 1970) - XCTAssertEqual(self.lastSQLQuery, "SELECT STRFTIME('%Y', \"date\") FROM \"records\"") + XCTAssertEqual(self.lastSQLQuery, "SELECT STRFTIME('%Y', \"date\") FROM \"records\" LIMIT 1") } } @@ -109,7 +109,7 @@ class QueryInterfaceExtensibilityTests: GRDBTestCase { default: XCTFail("Expected data blob") } - XCTAssertEqual(self.lastSQLQuery, "SELECT (CAST(\"text\" AS BLOB)) FROM \"records\"") + XCTAssertEqual(self.lastSQLQuery, "SELECT (CAST(\"text\" AS BLOB)) FROM \"records\" LIMIT 1") } } } diff --git a/Tests/GRDBTests/QueryInterfaceRequestTests.swift b/Tests/GRDBTests/QueryInterfaceRequestTests.swift index eaaadb0cbc..885981edda 100644 --- a/Tests/GRDBTests/QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/QueryInterfaceRequestTests.swift @@ -46,7 +46,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { func testSimpleRequestDoesNotUseAnyRowAdapter() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - let (_, adapter) = try Reader.all().prepare(db) + let (_, adapter) = try Reader.all().prepare(db, forSingleResult: false) XCTAssertNil(adapter) } } @@ -73,7 +73,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { do { let row = try Row.fetchOne(db, tableRequest)! - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1") XCTAssertEqual(row["id"] as Int64, 1) XCTAssertEqual(row["name"] as String, "Arthur") XCTAssertEqual(row["age"] as Int, 42) @@ -238,7 +238,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { let request = tableRequest.select(Col.name.aliased("nom"), (Col.age + 1).aliased("agePlusOne")) let row = try Row.fetchOne(db, request)! - XCTAssertEqual(lastSQLQuery, "SELECT \"name\" AS \"nom\", (\"age\" + 1) AS \"agePlusOne\" FROM \"readers\"") + XCTAssertEqual(lastSQLQuery, "SELECT \"name\" AS \"nom\", (\"age\" + 1) AS \"agePlusOne\" FROM \"readers\" LIMIT 1") XCTAssertEqual(row["nom"] as String, "Arthur") XCTAssertEqual(row["agePlusOne"] as Int, 43) } diff --git a/Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift index acedfdfa20..ccffce00c5 100644 --- a/Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift @@ -84,7 +84,7 @@ class RecordQueryInterfaceRequestTests: GRDBTestCase { do { let reader = try request.fetchOne(db)! - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1") XCTAssertEqual(reader.id!, arthur.id!) XCTAssertEqual(reader.name, arthur.name) XCTAssertEqual(reader.age, arthur.age) @@ -93,10 +93,12 @@ class RecordQueryInterfaceRequestTests: GRDBTestCase { do { let cursor = try request.fetchCursor(db) let names = cursor.map { $0.name } - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") XCTAssertEqual(try names.next()!, arthur.name) XCTAssertEqual(try names.next()!, barbara.name) XCTAssertTrue(try names.next() == nil) + + // validate query *after* cursor has retrieved a record + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"") } } } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index cd5368ee4d..d4d2670926 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -310,6 +310,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { let fetchedRecord = try MinimalRowID.fetchOne(db, key: ["id": record.id])! XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE (\"id\" = \(record.id!))") } } @@ -380,6 +381,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { let fetchedRecord = try MinimalRowID.filter(key: ["id": record.id]).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE (\"id\" = \(record.id!))") } } @@ -460,6 +462,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { do { let fetchedRecord = try MinimalRowID.fetchOne(db, key: record.id)! XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE (\"id\" = \(record.id!))") } } } @@ -529,6 +532,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { do { let fetchedRecord = try MinimalRowID.filter(key: record.id).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE (\"id\" = \(record.id!))") } } } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift index 6a4068cc7f..4e921deb56 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift @@ -321,6 +321,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { let fetchedRecord = try MinimalSingle.fetchOne(db, key: ["UUID": record.UUID])! XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE (\"UUID\" = '\(record.UUID!)')") } } @@ -396,6 +397,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { let fetchedRecord = try MinimalSingle.filter(key: ["UUID": record.UUID]).fetchOne(db)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE (\"UUID\" = '\(record.UUID!)')") } } @@ -481,6 +483,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { do { let fetchedRecord = try MinimalSingle.fetchOne(db, key: record.UUID)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE (\"UUID\" = '\(record.UUID!)')") } } } @@ -555,6 +558,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { do { let fetchedRecord = try MinimalSingle.filter(key: record.UUID).fetchOne(db)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE (\"UUID\" = '\(record.UUID!)')") } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift index 09421b5c0b..339a3bc7c6 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift @@ -393,6 +393,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE (\"rowid\" = \(record.id!))") } } @@ -466,6 +467,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE (\"rowid\" = \(record.id!))") } } @@ -549,6 +551,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE (\"rowid\" = \(record.id!))") } } } @@ -621,6 +624,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE (\"rowid\" = \(record.id!))") } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift b/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift index 746e1d82c1..6815c91e99 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift @@ -347,6 +347,7 @@ class RecordPrimaryKeyMultipleTests: GRDBTestCase { XCTAssertTrue(fetchedRecord.personName == record.personName) XCTAssertTrue(fetchedRecord.countryName == record.countryName) XCTAssertTrue(fetchedRecord.native == record.native) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"citizenships\" WHERE ((\"personName\" = '\(record.personName!)') AND (\"countryName\" = '\(record.countryName!)'))") } } @@ -419,6 +420,7 @@ class RecordPrimaryKeyMultipleTests: GRDBTestCase { XCTAssertTrue(fetchedRecord.personName == record.personName) XCTAssertTrue(fetchedRecord.countryName == record.countryName) XCTAssertTrue(fetchedRecord.native == record.native) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"citizenships\" WHERE ((\"personName\" = '\(record.personName!)') AND (\"countryName\" = '\(record.countryName!)'))") } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift index a75a13c4ed..bc01ae6a1b 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift @@ -100,6 +100,7 @@ class RecordPrimaryKeyNoneTests: GRDBTestCase { let fetchedRecord = try Item.fetchOne(db, key: ["email": record.email])! XCTAssertTrue(fetchedRecord.email == record.email) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"items\" WHERE (\"email\" = 'item@example.com')") } } @@ -114,6 +115,7 @@ class RecordPrimaryKeyNoneTests: GRDBTestCase { let fetchedRecord = try Item.filter(key: ["email": record.email]).fetchOne(db)! XCTAssertTrue(fetchedRecord.email == record.email) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"items\" WHERE (\"email\" = 'item@example.com')") } } @@ -203,6 +205,7 @@ class RecordPrimaryKeyNoneTests: GRDBTestCase { do { let fetchedRecord = try Item.fetchOne(db, key: id)! XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"items\" WHERE (\"rowid\" = \(id))") } } } @@ -281,6 +284,7 @@ class RecordPrimaryKeyNoneTests: GRDBTestCase { do { let fetchedRecord = try Item.filter(key: id).fetchOne(db)! XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"items\" WHERE (\"rowid\" = \(id))") } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift index 54b508fd2f..49375e57a6 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift @@ -389,6 +389,7 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"persons\" WHERE (\"id\" = \(record.id!))") } } @@ -462,6 +463,7 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"persons\" WHERE (\"id\" = \(record.id!))") } } @@ -545,6 +547,7 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"persons\" WHERE (\"id\" = \(record.id!))") } } } @@ -617,6 +620,7 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase { XCTAssertTrue(fetchedRecord.name == record.name) XCTAssertTrue(fetchedRecord.age == record.age) XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"persons\" WHERE (\"id\" = \(record.id!))") } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift index f4f738e3fd..7999e68d21 100644 --- a/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift @@ -339,6 +339,7 @@ class RecordPrimaryKeySingleTests: GRDBTestCase { let fetchedRecord = try Pet.fetchOne(db, key: ["UUID": record.UUID])! XCTAssertTrue(fetchedRecord.UUID == record.UUID) XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"pets\" WHERE (\"UUID\" = '\(record.UUID!)')") } } @@ -410,6 +411,7 @@ class RecordPrimaryKeySingleTests: GRDBTestCase { let fetchedRecord = try Pet.filter(key: ["UUID": record.UUID]).fetchOne(db)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"pets\" WHERE (\"UUID\" = '\(record.UUID!)')") } } @@ -491,6 +493,7 @@ class RecordPrimaryKeySingleTests: GRDBTestCase { let fetchedRecord = try Pet.fetchOne(db, key: record.UUID)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"pets\" WHERE (\"UUID\" = '\(record.UUID!)')") } } } @@ -561,6 +564,7 @@ class RecordPrimaryKeySingleTests: GRDBTestCase { let fetchedRecord = try Pet.filter(key: record.UUID).fetchOne(db)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"pets\" WHERE (\"UUID\" = '\(record.UUID!)')") } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift b/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift index 200224d6cd..a08194c0d5 100644 --- a/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift @@ -328,6 +328,7 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase { let fetchedRecord = try Email.fetchOne(db, key: ["email": record.email])! XCTAssertTrue(fetchedRecord.email == record.email) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"emails\" WHERE (\"email\" = '\(record.email!)')") } } @@ -403,6 +404,7 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase { let fetchedRecord = try Email.filter(key: ["email": record.email]).fetchOne(db)! XCTAssertTrue(fetchedRecord.email == record.email) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"emails\" WHERE (\"email\" = '\(record.email!)')") } } @@ -488,6 +490,7 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase { do { let fetchedRecord = try Email.fetchOne(db, key: record.email)! XCTAssertTrue(fetchedRecord.email == record.email) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"emails\" WHERE (\"email\" = '\(record.email!)')") } } } @@ -562,6 +565,7 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase { do { let fetchedRecord = try Email.filter(key: record.email).fetchOne(db)! XCTAssertTrue(fetchedRecord.email == record.email) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"emails\" WHERE (\"email\" = '\(record.email!)')") } } } diff --git a/Tests/GRDBTests/SQLRequestTests.swift b/Tests/GRDBTests/SQLRequestTests.swift index 92323f3074..babd6c5bb0 100644 --- a/Tests/GRDBTests/SQLRequestTests.swift +++ b/Tests/GRDBTests/SQLRequestTests.swift @@ -13,7 +13,7 @@ class SQLRequestTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let request = SQLRequest(sql: "SELECT 1") - let (statement, adapter) = try request.prepare(db) + let (statement, adapter) = try request.prepare(db, forSingleResult: false) XCTAssertEqual(statement.sql, "SELECT 1") XCTAssertNil(adapter) let row = try request.fetchOne(db) @@ -25,7 +25,7 @@ class SQLRequestTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let request = SQLRequest(sql: "SELECT ?, ?", arguments: [1, 2], adapter: SuffixRowAdapter(fromIndex: 1)) - let (statement, adapter) = try request.prepare(db) + let (statement, adapter) = try request.prepare(db, forSingleResult: false) XCTAssertEqual(statement.sql, "SELECT ?, ?") XCTAssertNotNil(adapter) let int = try request.fetchOne(db)! @@ -37,8 +37,8 @@ class SQLRequestTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let request = SQLRequest(sql: "SELECT 1") - let (statement1, _) = try request.prepare(db) - let (statement2, _) = try request.prepare(db) + let (statement1, _) = try request.prepare(db, forSingleResult: false) + let (statement2, _) = try request.prepare(db, forSingleResult: false) XCTAssertTrue(statement1 !== statement2) } } @@ -47,8 +47,8 @@ class SQLRequestTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let request = SQLRequest(sql: "SELECT 1", cached: true) - let (statement1, _) = try request.prepare(db) - let (statement2, _) = try request.prepare(db) + let (statement1, _) = try request.prepare(db, forSingleResult: false) + let (statement2, _) = try request.prepare(db, forSingleResult: false) XCTAssertTrue(statement1 === statement2) } } @@ -56,7 +56,7 @@ class SQLRequestTests: GRDBTestCase { func testRequestInitializer() throws { struct CustomRequest: FetchRequest { typealias RowDecoder = Row - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { let statement = try db.makeSelectStatement(sql: "SELECT ? AS a, ? AS b") statement.arguments = [1, "foo"] return (statement, nil) @@ -72,6 +72,21 @@ class SQLRequestTests: GRDBTestCase { } } + func testRequestInitializerAndSingleResultHint() throws { + struct CustomRequest: FetchRequest { + typealias RowDecoder = Row + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { + if singleResult { fatalError("not implemented") } + return try (db.makeSelectStatement(sql: "SELECT 'multiple'"), nil) + } + } + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + let request = try SQLRequest(db, request: CustomRequest()) + XCTAssertEqual(request.sql, "SELECT 'multiple'") + } + } + func testSQLLiteralInitializer() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in