From f9868bd8b5fd5bb7e2f1c346ba4510d55253f74b Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Thu, 11 Apr 2019 10:31:21 +1000 Subject: [PATCH 01/21] Update tests to expect "LIMIT 1" where appropriate Tests currently fail --- .../FetchableRecord+QueryInterfaceRequestTests.swift | 6 +++--- Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift | 4 ++-- Tests/GRDBTests/QueryInterfaceRequestTests.swift | 4 ++-- Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift | 2 +- Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift | 3 +++ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift index b1cb52e657..c8ab600f42 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) @@ -130,7 +130,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) @@ -171,7 +171,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) 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..f6433d6d4e 100644 --- a/Tests/GRDBTests/QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/QueryInterfaceRequestTests.swift @@ -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..fd45cd42ed 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) diff --git a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift index a75a13c4ed..5aa548aabf 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') LIMIT 1") } } @@ -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))") } } } From af1fd6b90c30699ff75b7fb81b9b9ff8d7041063 Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Thu, 11 Apr 2019 13:08:29 +1000 Subject: [PATCH 02/21] More tests for presence of LIMIT 1 --- Tests/GRDBTests/FetchableRecordTests.swift | 37 ++++++++++--------- .../RecordMinimalPrimaryKeyRowIDTests.swift | 2 + 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Tests/GRDBTests/FetchableRecordTests.swift b/Tests/GRDBTests/FetchableRecordTests.swift index dd01409228..3bd952ef9b 100644 --- a/Tests/GRDBTests/FetchableRecordTests.swift +++ b/Tests/GRDBTests/FetchableRecordTests.swift @@ -261,48 +261,49 @@ class FetchableRecordTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - func test(_ nilBecauseMissingRow: Fetched?) { + func test(_ nilBecauseMissingRow: Fetched?, sql: String) { XCTAssertTrue(nilBecauseMissingRow == nil) + XCTAssertEqual(lastSQLQuery, sql) } do { let sql = "SELECT 1 WHERE 0" let statement = try db.makeSelectStatement(sql: sql) - try test(Fetched.fetchOne(db, sql: sql)) - try test(Fetched.fetchOne(statement)) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql))) - try test(SQLRequest(sql: sql).fetchOne(db)) + try test(Fetched.fetchOne(db, sql: sql), sql: sql) + try test(Fetched.fetchOne(statement), sql: sql) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql)), sql: sql) + try test(SQLRequest(sql: sql).fetchOne(db), sql: sql) } do { let sql = "SELECT 0, 1 WHERE 0" let statement = try db.makeSelectStatement(sql: sql) let adapter = SuffixRowAdapter(fromIndex: 1) - try test(Fetched.fetchOne(db, sql: sql, adapter: adapter)) - try test(Fetched.fetchOne(statement, adapter: adapter)) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter))) - try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db)) + try test(Fetched.fetchOne(db, sql: sql, adapter: adapter), sql: sql) + try test(Fetched.fetchOne(statement, adapter: adapter), sql: sql) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter)), sql: sql) + try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db), sql: sql) } } do { - func test(_ record: Fetched?) { + func test(_ record: Fetched?, sql: String) { XCTAssertEqual(record!.firstName, "Arthur") XCTAssertEqual(record!.lastName, "Martin") } do { let sql = "SELECT 'Arthur' AS firstName, 'Martin' AS lastName" let statement = try db.makeSelectStatement(sql: sql) - try test(Fetched.fetchOne(db, sql: sql)) - try test(Fetched.fetchOne(statement)) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql))) - try test(SQLRequest(sql: sql).fetchOne(db)) + try test(Fetched.fetchOne(db, sql: sql), sql: sql) + try test(Fetched.fetchOne(statement), sql: sql) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql)), sql: sql) + try test(SQLRequest(sql: sql).fetchOne(db), sql: sql) } do { let sql = "SELECT 0 AS firstName, 'Arthur' AS firstName, 'Martin' AS lastName" let statement = try db.makeSelectStatement(sql: sql) let adapter = SuffixRowAdapter(fromIndex: 1) - try test(Fetched.fetchOne(db, sql: sql, adapter: adapter)) - try test(Fetched.fetchOne(statement, adapter: adapter)) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter))) - try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db)) + try test(Fetched.fetchOne(db, sql: sql, adapter: adapter), sql: sql) + try test(Fetched.fetchOne(statement, adapter: adapter), sql: sql) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter)), sql: sql) + try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db), sql: sql) } } } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index cd5368ee4d..8a71a5c179 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!)) LIMIT 1") } } From 072fa2583a6384575a8534a304026de3dc97a434 Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Thu, 11 Apr 2019 13:14:41 +1000 Subject: [PATCH 03/21] Add hints for use in request preparation Hints are ignored by SQLRequest. When you write raw SQL you probably don't want GRDB to modify your query. --- GRDB/Core/DatabaseValueConvertible.swift | 2 +- GRDB/Core/FetchRequest.swift | 45 ++++++++++++++----- GRDB/Core/Row.swift | 2 +- GRDB/Core/SQLRequest.swift | 3 +- GRDB/Core/StatementColumnConvertible.swift | 2 +- .../QueryInterfaceRequest.swift | 15 ++++++- GRDB/Record/FetchableRecord+TableRecord.swift | 4 +- GRDB/Record/FetchableRecord.swift | 14 +++++- .../GRDBTests/CompilationProtocolTests.swift | 2 +- Tests/GRDBTests/FetchRequestTests.swift | 10 ++--- Tests/GRDBTests/SQLRequestTests.swift | 2 +- 11 files changed, 75 insertions(+), 26 deletions(-) diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index ad66f58634..b3747aa07c 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -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, hint: .limitOne) return try fetchOne(statement, adapter: adapter) } } diff --git a/GRDB/Core/FetchRequest.swift b/GRDB/Core/FetchRequest.swift index bd944ddf29..adc028b844 100644 --- a/GRDB/Core/FetchRequest.swift +++ b/GRDB/Core/FetchRequest.swift @@ -15,10 +15,20 @@ public protocol FetchRequest: DatabaseRegionConvertible { /// Returns a tuple that contains a prepared statement that is ready to be /// executed, and an eventual row adapter. /// + /// Default implementation uses `prepare(db, hint: nil)`. + /// /// - parameter db: A database connection. /// - returns: A prepared statement and an eventual row adapter. func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) + /// 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 hint: A hint as to how the request should be prepared. + /// - returns: A prepared statement and an eventual row adapter. + func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) + /// Returns the number of rows fetched by the request. /// /// The default implementation builds a naive SQL query based on the @@ -33,6 +43,10 @@ public protocol FetchRequest: DatabaseRegionConvertible { } extension FetchRequest { + public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try self.prepare(db, hint: nil) + } + /// Returns an adapted request. public func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedFetchRequest { return AdaptedFetchRequest(self, adapter) @@ -79,8 +93,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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { + let (statement, baseAdapter) = try base.prepare(db, hint: hint) if let baseAdapter = baseAdapter { return try (statement, ChainedAdapter(first: baseAdapter, second: adapter(db))) } else { @@ -108,7 +122,7 @@ public struct AdaptedFetchRequest : FetchRequest { public struct AnyFetchRequest : FetchRequest { public typealias RowDecoder = T - private let _prepare: (Database) throws -> (SelectStatement, RowAdapter?) + private let _prepare: (Database, FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) private let _fetchCount: (Database) throws -> Int private let _databaseRegion: (Database) throws -> DatabaseRegion @@ -121,26 +135,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, FetchRequestHint?) throws -> (SelectStatement, RowAdapter?)) { + _prepare = { db, hint in + try prepare(db, hint) } _fetchCount = { db in - let (statement, _) = try prepare(db) + let (statement, _) = try prepare(db, nil) 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, nil) return statement.databaseRegion } } /// :nodoc: - public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { - return try _prepare(db) + public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { + return try _prepare(db, hint) } /// :nodoc: @@ -153,3 +167,14 @@ public struct AnyFetchRequest : FetchRequest { return try _databaseRegion(db) } } + +/// A hint as to how the fetch request should be prepared. +/// +/// FetchRequest implementations (such as SQLRequest) may ignore these hints. +/// +/// - limitOne: Only 1 record should be fetched. +/// - primaryKeyOrUnique: The query filters on primary keys or a unique index and is expected to return 1 record. +public enum FetchRequestHint { + case limitOne + case primaryKeyOrUnique +} diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index eb33c8608f..85a3cc4938 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -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, hint: .limitOne) return try fetchOne(statement, adapter: adapter) } } diff --git a/GRDB/Core/SQLRequest.swift b/GRDB/Core/SQLRequest.swift index 7e0a59db43..15b3e24c17 100644 --- a/GRDB/Core/SQLRequest.swift +++ b/GRDB/Core/SQLRequest.swift @@ -125,9 +125,10 @@ public struct SQLRequest : FetchRequest { /// executed, and an eventual row adapter. /// /// - parameter db: A database connection. + /// - parameter hint: SQLRequest disregards this value. /// /// :nodoc: - public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { let statement: SelectStatement switch cache { case .none: diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 2ee10eae04..fca16a140a 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -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, hint: .limitOne) return try fetchOne(statement, adapter: adapter) } } diff --git a/GRDB/QueryInterface/QueryInterfaceRequest.swift b/GRDB/QueryInterface/QueryInterfaceRequest.swift index da70db0109..1365070ee3 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -30,6 +30,10 @@ /// See https://github.com/groue/GRDB.swift#the-query-interface public struct QueryInterfaceRequest { var query: SQLSelectQuery + + init(query: SQLSelectQuery) { + self.query = query + } } extension QueryInterfaceRequest : FetchRequest { @@ -41,7 +45,16 @@ extension QueryInterfaceRequest : FetchRequest { /// - parameter db: A database connection. /// - returns: A prepared statement and an eventual row adapter. /// :nodoc: - public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { + let query: SQLSelectQuery + + switch hint { + case .limitOne?: + query = self.query.limit(1) + case nil, .primaryKeyOrUnique?: + query = self.query + } + return try SQLSelectQueryGenerator(query).prepare(db) } diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index 91fad6b2c7..f9ba2792f5 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -121,7 +121,7 @@ extension FetchableRecord where Self: TableRecord { // Avoid hitting the database return nil } - return try filter(key: key).fetchOne(db) + return try filter(key: key).fetchOne(db, hint: .primaryKeyOrUnique) } } @@ -186,6 +186,6 @@ extension FetchableRecord where Self: TableRecord { // Avoid hitting the database return nil } - return try filter(key: key).fetchOne(db) + return try filter(key: key).fetchOne(db, hint: .primaryKeyOrUnique) } } diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index a01e63bf3b..9e2858303e 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -313,7 +313,12 @@ 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) + return try Self.fetchOne(db, request, hint: .limitOne) + } + + @inlinable + static func fetchOne(_ db: Database, _ request: R, hint: FetchRequestHint?) throws -> Self? { + let (statement, adapter) = try request.prepare(db, hint: hint) return try fetchOne(statement, adapter: adapter) } } @@ -368,7 +373,12 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public func fetchOne(_ db: Database) throws -> RowDecoder? { - return try RowDecoder.fetchOne(db, self) + return try fetchOne(db, hint: .limitOne) + } + + @inlinable + func fetchOne(_ db: Database, hint: FetchRequestHint?) throws -> RowDecoder? { + return try RowDecoder.fetchOne(db, self, hint: hint) } } diff --git a/Tests/GRDBTests/CompilationProtocolTests.swift b/Tests/GRDBTests/CompilationProtocolTests.swift index f92531a469..9a121e67a8 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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { preconditionFailure() } } // MARK: - FetchableRecord diff --git a/Tests/GRDBTests/FetchRequestTests.swift b/Tests/GRDBTests/FetchRequestTests.swift index df38746dd9..dfb3e2904d 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { return try (db.makeSelectStatement(sql: "INVALID"), nil) } diff --git a/Tests/GRDBTests/SQLRequestTests.swift b/Tests/GRDBTests/SQLRequestTests.swift index 92323f3074..ded7738171 100644 --- a/Tests/GRDBTests/SQLRequestTests.swift +++ b/Tests/GRDBTests/SQLRequestTests.swift @@ -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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { let statement = try db.makeSelectStatement(sql: "SELECT ? AS a, ? AS b") statement.arguments = [1, "foo"] return (statement, nil) From dd139b62680e98610b0c8f19a6ce4f30b851b6fa Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Thu, 11 Apr 2019 13:19:04 +1000 Subject: [PATCH 04/21] Fix testing lastSQLQuery when a cursor hasn't retrieved a record This problem was masked by the fact all queries in the test expected the same SQL query. --- .../FetchableRecord+QueryInterfaceRequestTests.swift | 12 +++++++++--- .../Record+QueryInterfaceRequestTests.swift | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift index c8ab600f42..a450e6f72b 100644 --- a/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift @@ -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\"") } } } @@ -139,10 +141,12 @@ 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\"") } } } @@ -179,10 +183,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/Record+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift index fd45cd42ed..ccffce00c5 100644 --- a/Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/Record+QueryInterfaceRequestTests.swift @@ -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\"") } } } From 207b90f1b503a7a0f8a58acb29862758448da993 Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Thu, 11 Apr 2019 13:27:32 +1000 Subject: [PATCH 05/21] Update FetchRequest protocol in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97a2513f9c..6ace6515bf 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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) /// The number of rows fetched by the request. func fetchCount(_ db: Database) throws -> Int From 088f6c92c734281cc4cf445464ac660fc28f8f00 Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Thu, 11 Apr 2019 13:50:57 +1000 Subject: [PATCH 06/21] Update inline docs --- GRDB/QueryInterface/QueryInterfaceRequest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/GRDB/QueryInterface/QueryInterfaceRequest.swift b/GRDB/QueryInterface/QueryInterfaceRequest.swift index 1365070ee3..73624c30ac 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -43,6 +43,7 @@ extension QueryInterfaceRequest : FetchRequest { /// executed, and an eventual row adapter. /// /// - parameter db: A database connection. + /// - parameter hint: A hint about how the query should be prepared. /// - returns: A prepared statement and an eventual row adapter. /// :nodoc: public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { From 20aecd532872b93d7577a7db0264b0e0f20ebaad Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Tue, 16 Apr 2019 10:19:24 +1000 Subject: [PATCH 07/21] Add failing test for fetchOne with a non-zero offset --- .../FetchableRecord+QueryInterfaceRequestTests.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift index a450e6f72b..a6147b7b5e 100644 --- a/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift @@ -148,6 +148,14 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase { // 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) + } } } From 7070dedf1ab3da95a6627e8bdc99f9b4bb4a32a9 Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Tue, 16 Apr 2019 10:07:30 +1000 Subject: [PATCH 08/21] Replace FetchRequestHint with a boolean for single results Fixes previous test failure --- GRDB/Core/DatabaseValueConvertible.swift | 10 ++-- GRDB/Core/FetchRequest.swift | 57 ++++++------------- GRDB/Core/Row.swift | 6 +- GRDB/Core/SQLRequest.swift | 6 +- GRDB/Core/StatementColumnConvertible.swift | 10 ++-- .../QueryInterfaceRequest.swift | 14 ++--- GRDB/Record/FetchableRecord+TableRecord.swift | 6 +- GRDB/Record/FetchableRecord.swift | 16 +++--- .../AssociationBelongsToRowScopeTests.swift | 2 +- ...ssociationHasOneThroughRowScopeTests.swift | 2 +- .../GRDBTests/CompilationProtocolTests.swift | 2 +- Tests/GRDBTests/FetchRequestTests.swift | 10 ++-- Tests/GRDBTests/GRDBTestCase.swift | 4 +- .../QueryInterfaceRequestTests.swift | 2 +- Tests/GRDBTests/SQLRequestTests.swift | 14 ++--- 15 files changed, 70 insertions(+), 91 deletions(-) diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index b3747aa07c..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, hint: .limitOne) + 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 adc028b844..70b3f21705 100644 --- a/GRDB/Core/FetchRequest.swift +++ b/GRDB/Core/FetchRequest.swift @@ -11,23 +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. - /// - /// Default implementation uses `prepare(db, hint: nil)`. - /// - /// - parameter db: A database connection. - /// - returns: A prepared statement and an eventual row adapter. - func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) - + /// 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 hint: A hint as to how the request should be prepared. + /// - 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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) /// Returns the number of rows fetched by the request. /// @@ -43,10 +36,7 @@ public protocol FetchRequest: DatabaseRegionConvertible { } extension FetchRequest { - public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { - return try self.prepare(db, hint: nil) - } - + /// Returns an adapted request. public func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedFetchRequest { return AdaptedFetchRequest(self, adapter) @@ -59,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)! } @@ -71,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 } } @@ -93,8 +83,8 @@ public struct AdaptedFetchRequest : FetchRequest { } /// :nodoc: - public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { - let (statement, baseAdapter) = try base.prepare(db, hint: hint) + 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 { @@ -122,7 +112,7 @@ public struct AdaptedFetchRequest : FetchRequest { public struct AnyFetchRequest : FetchRequest { public typealias RowDecoder = T - private let _prepare: (Database, FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) + private let _prepare: (Database, _ singleResult: Bool) throws -> (SelectStatement, RowAdapter?) private let _fetchCount: (Database) throws -> Int private let _databaseRegion: (Database) throws -> DatabaseRegion @@ -135,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, FetchRequestHint?) throws -> (SelectStatement, RowAdapter?)) { - _prepare = { db, hint in - try prepare(db, hint) + 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, nil) + 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, nil) + let (statement, _) = try prepare(db, false) return statement.databaseRegion } } /// :nodoc: - public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { - return try _prepare(db, hint) + public func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { + return try _prepare(db, singleResult) } /// :nodoc: @@ -167,14 +157,3 @@ public struct AnyFetchRequest : FetchRequest { return try _databaseRegion(db) } } - -/// A hint as to how the fetch request should be prepared. -/// -/// FetchRequest implementations (such as SQLRequest) may ignore these hints. -/// -/// - limitOne: Only 1 record should be fetched. -/// - primaryKeyOrUnique: The query filters on primary keys or a unique index and is expected to return 1 record. -public enum FetchRequestHint { - case limitOne - case primaryKeyOrUnique -} diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 85a3cc4938..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, hint: .limitOne) + 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 15b3e24c17..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,10 +125,10 @@ public struct SQLRequest : FetchRequest { /// executed, and an eventual row adapter. /// /// - parameter db: A database connection. - /// - parameter hint: SQLRequest disregards this value. + /// - parameter singleResult: SQLRequest disregards this hint. /// /// :nodoc: - public func prepare(_ db: Database, hint: FetchRequestHint?) 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 fca16a140a..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, hint: .limitOne) + 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 73624c30ac..be0b5bb359 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -43,17 +43,15 @@ extension QueryInterfaceRequest : FetchRequest { /// executed, and an eventual row adapter. /// /// - parameter db: A database connection. - /// - parameter hint: A hint about how the query should be prepared. + /// - 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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { - let query: SQLSelectQuery + public func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { - switch hint { - case .limitOne?: - query = self.query.limit(1) - case nil, .primaryKeyOrUnique?: - query = self.query + var query = self.query + + if singleResult { + query.limit = SQLLimit(limit: 1, offset: query.limit?.offset) } return try SQLSelectQueryGenerator(query).prepare(db) diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index f9ba2792f5..0ab7ee146c 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -121,7 +121,8 @@ extension FetchableRecord where Self: TableRecord { // Avoid hitting the database return nil } - return try filter(key: key).fetchOne(db, hint: .primaryKeyOrUnique) + // Fetch using `forSingleResult: false` to omit unnecessary "LIMIT 1" + return try filter(key: key).fetchOne(db, forSingleResult: false) } } @@ -186,6 +187,7 @@ extension FetchableRecord where Self: TableRecord { // Avoid hitting the database return nil } - return try filter(key: key).fetchOne(db, hint: .primaryKeyOrUnique) + // Fetch using `forSingleResult: false` to omit unnecessary "LIMIT 1" + return try filter(key: key).fetchOne(db, forSingleResult: false) } } diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 9e2858303e..347a67bf3e 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,12 +313,12 @@ extension FetchableRecord { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public static func fetchOne(_ db: Database, _ request: R) throws -> Self? { - return try Self.fetchOne(db, request, hint: .limitOne) + return try Self.fetchOne(db, request, forSingleResult: true) } @inlinable - static func fetchOne(_ db: Database, _ request: R, hint: FetchRequestHint?) throws -> Self? { - let (statement, adapter) = try request.prepare(db, hint: hint) + static func fetchOne(_ db: Database, _ request: R, forSingleResult singleResult: Bool) throws -> Self? { + let (statement, adapter) = try request.prepare(db, forSingleResult: singleResult) return try fetchOne(statement, adapter: adapter) } } @@ -373,12 +373,12 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public func fetchOne(_ db: Database) throws -> RowDecoder? { - return try fetchOne(db, hint: .limitOne) + return try fetchOne(db, forSingleResult: true) } @inlinable - func fetchOne(_ db: Database, hint: FetchRequestHint?) throws -> RowDecoder? { - return try RowDecoder.fetchOne(db, self, hint: hint) + func fetchOne(_ db: Database, forSingleResult singleResult: Bool) throws -> RowDecoder? { + return try RowDecoder.fetchOne(db, self, forSingleResult: singleResult) } } 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 9a121e67a8..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, hint: FetchRequestHint?) 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 dfb3e2904d..bbdf574dc3 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) 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, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { + func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { return try (db.makeSelectStatement(sql: "INVALID"), nil) } 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/QueryInterfaceRequestTests.swift b/Tests/GRDBTests/QueryInterfaceRequestTests.swift index f6433d6d4e..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) } } diff --git a/Tests/GRDBTests/SQLRequestTests.swift b/Tests/GRDBTests/SQLRequestTests.swift index ded7738171..a8b9d7cf99 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, hint: FetchRequestHint?) 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) From 235a612ce0e12471d71de1d682fe7e88e5a5afbe Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Tue, 16 Apr 2019 10:29:40 +1000 Subject: [PATCH 09/21] Remove LIMIT 1 from expected SQL when filtering by key Tests now fail --- Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift | 2 +- Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index 8a71a5c179..c7cd25fff9 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -381,7 +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!)) LIMIT 1") + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE (\"id\" = \(record.id!))") } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift index 5aa548aabf..0919d114f0 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift @@ -115,7 +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') LIMIT 1") + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"items\" WHERE (\"email\" = 'item@example.com')") } } From e2211aca21356b6b783961aa0b0aa4ac2b9e2200 Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Tue, 16 Apr 2019 11:10:43 +1000 Subject: [PATCH 10/21] Exclude LIMIT 1 in more cases Adds a flag to SQLSelectQuery indicating that it expects a single record (for example when querying by primary or unique key). Ensures that this example doesn't add an unnecessary LIMIT 1 to the resulting query: let request = Player.filter(key: 123) // or Player.filter(key: ["email": "me@example.com"]) let player = try dbQueue.read { db in try request.fetchOne(db) } --- GRDB/QueryInterface/QueryInterfaceRequest.swift | 8 ++++---- GRDB/QueryInterface/SQLSelectQuery.swift | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/GRDB/QueryInterface/QueryInterfaceRequest.swift b/GRDB/QueryInterface/QueryInterfaceRequest.swift index be0b5bb359..df3e0850f3 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -47,10 +47,10 @@ extension QueryInterfaceRequest : FetchRequest { /// - returns: A prepared statement and an eventual row adapter. /// :nodoc: public func prepare(_ db: Database, forSingleResult singleResult: Bool) throws -> (SelectStatement, RowAdapter?) { - var query = self.query - if singleResult { + // Optimize query by setting a limit of 1 when appropriate + if singleResult && !query.expectsSingleRecord { query.limit = SQLLimit(limit: 1, offset: query.limit?.offset) } @@ -472,7 +472,7 @@ extension TableRecord { /// all requests by the `TableRecord.databaseSelection` property, or /// for individual requests with the `TableRecord.select` method. public static func filter(key: PrimaryKeyType?) -> QueryInterfaceRequest { - return all().filter(key: key) + return all().mapQuery { $0.expectingSingleRecord() }.filter(key: key) } /// Creates a request with the provided primary key *predicate*. @@ -499,7 +499,7 @@ extension TableRecord { /// all requests by the `TableRecord.databaseSelection` property, or /// for individual requests with the `TableRecord.select` method. public static func filter(key: [String: DatabaseValueConvertible?]?) -> QueryInterfaceRequest { - return all().filter(key: key) + return all().mapQuery { $0.expectingSingleRecord() }.filter(key: key) } /// Creates a request with the provided primary key *predicate*. diff --git a/GRDB/QueryInterface/SQLSelectQuery.swift b/GRDB/QueryInterface/SQLSelectQuery.swift index eaeaab1505..e1c5a3324b 100644 --- a/GRDB/QueryInterface/SQLSelectQuery.swift +++ b/GRDB/QueryInterface/SQLSelectQuery.swift @@ -4,19 +4,22 @@ struct SQLSelectQuery { var relation: SQLRelation var isDistinct: Bool + var expectsSingleRecord: Bool var groupPromise: DatabasePromise<[SQLExpression]>? var havingExpression: SQLExpression? var limit: SQLLimit? - + init( relation: SQLRelation, isDistinct: Bool = false, + expectsSingleRecord: Bool = false, groupPromise: DatabasePromise<[SQLExpression]>? = nil, havingExpression: SQLExpression? = nil, limit: SQLLimit? = nil) { self.relation = relation self.isDistinct = isDistinct + self.expectsSingleRecord = expectsSingleRecord self.groupPromise = groupPromise self.havingExpression = havingExpression self.limit = limit @@ -37,6 +40,12 @@ extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest { query.isDistinct = true return query } + + func expectingSingleRecord() -> SQLSelectQuery { + var query = self + query.expectsSingleRecord = true + return query + } func filter(_ predicate: @escaping (Database) throws -> SQLExpressible) -> SQLSelectQuery { return mapRelation { $0.filter(predicate) } From 1b3f0e8ece2ff605cab03d4030228afdce10ea0e Mon Sep 17 00:00:00 2001 From: Alex Robinson Date: Tue, 16 Apr 2019 11:11:37 +1000 Subject: [PATCH 11/21] Update FetchRequest protocol in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ace6515bf..45b7d2eae4 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, hint: FetchRequestHint?) 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. From 46791c8f74c75a85304a39574d6260e1c464a548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 16 Apr 2019 07:56:44 +0200 Subject: [PATCH 12/21] Remove QueryInterfaceRequest.init(query:) --- GRDB/QueryInterface/QueryInterfaceRequest.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/GRDB/QueryInterface/QueryInterfaceRequest.swift b/GRDB/QueryInterface/QueryInterfaceRequest.swift index df3e0850f3..ea3bc81be7 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -30,10 +30,6 @@ /// See https://github.com/groue/GRDB.swift#the-query-interface public struct QueryInterfaceRequest { var query: SQLSelectQuery - - init(query: SQLSelectQuery) { - self.query = query - } } extension QueryInterfaceRequest : FetchRequest { From 4716f91d8bd3b8c072cd64be2d32edbc8292baa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 16 Apr 2019 08:35:34 +0200 Subject: [PATCH 13/21] Bake the single result expectation right into `filter(key:)` methods This better hides this flag inside SQLSelectQuery, and avoids explicit support from FetchableRecord and TableRecord --- .../QueryInterfaceRequest.swift | 19 ++++++++++--- GRDB/QueryInterface/RequestProtocols.swift | 27 ++++++++++++++++--- GRDB/QueryInterface/SQLSelectQuery.swift | 10 +++---- GRDB/Record/FetchableRecord+TableRecord.swift | 6 ++--- GRDB/Record/FetchableRecord.swift | 14 ++-------- 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/GRDB/QueryInterface/QueryInterfaceRequest.swift b/GRDB/QueryInterface/QueryInterfaceRequest.swift index ea3bc81be7..886b571932 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -44,9 +44,9 @@ extension QueryInterfaceRequest : FetchRequest { /// :nodoc: 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.expectsSingleRecord { + if singleResult && !query.expectsSingleResult { query.limit = SQLLimit(limit: 1, offset: query.limit?.offset) } @@ -191,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) } @@ -468,7 +479,7 @@ extension TableRecord { /// all requests by the `TableRecord.databaseSelection` property, or /// for individual requests with the `TableRecord.select` method. public static func filter(key: PrimaryKeyType?) -> QueryInterfaceRequest { - return all().mapQuery { $0.expectingSingleRecord() }.filter(key: key) + return all().filter(key: key) } /// Creates a request with the provided primary key *predicate*. @@ -495,7 +506,7 @@ extension TableRecord { /// all requests by the `TableRecord.databaseSelection` property, or /// for individual requests with the `TableRecord.select` method. public static func filter(key: [String: DatabaseValueConvertible?]?) -> QueryInterfaceRequest { - return all().mapQuery { $0.expectingSingleRecord() }.filter(key: key) + return all().filter(key: key) } /// Creates a request with the provided primary key *predicate*. 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 e1c5a3324b..66b0b64231 100644 --- a/GRDB/QueryInterface/SQLSelectQuery.swift +++ b/GRDB/QueryInterface/SQLSelectQuery.swift @@ -4,7 +4,7 @@ struct SQLSelectQuery { var relation: SQLRelation var isDistinct: Bool - var expectsSingleRecord: Bool + var expectsSingleResult: Bool var groupPromise: DatabasePromise<[SQLExpression]>? var havingExpression: SQLExpression? var limit: SQLLimit? @@ -12,14 +12,14 @@ struct SQLSelectQuery { init( relation: SQLRelation, isDistinct: Bool = false, - expectsSingleRecord: Bool = false, + expectsSingleResult: Bool = false, groupPromise: DatabasePromise<[SQLExpression]>? = nil, havingExpression: SQLExpression? = nil, limit: SQLLimit? = nil) { self.relation = relation self.isDistinct = isDistinct - self.expectsSingleRecord = expectsSingleRecord + self.expectsSingleResult = expectsSingleResult self.groupPromise = groupPromise self.havingExpression = havingExpression self.limit = limit @@ -41,9 +41,9 @@ extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest { return query } - func expectingSingleRecord() -> SQLSelectQuery { + func expectingSingleResult() -> SQLSelectQuery { var query = self - query.expectsSingleRecord = true + query.expectsSingleResult = true return query } diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index 0ab7ee146c..91fad6b2c7 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -121,8 +121,7 @@ extension FetchableRecord where Self: TableRecord { // Avoid hitting the database return nil } - // Fetch using `forSingleResult: false` to omit unnecessary "LIMIT 1" - return try filter(key: key).fetchOne(db, forSingleResult: false) + return try filter(key: key).fetchOne(db) } } @@ -187,7 +186,6 @@ extension FetchableRecord where Self: TableRecord { // Avoid hitting the database return nil } - // Fetch using `forSingleResult: false` to omit unnecessary "LIMIT 1" - return try filter(key: key).fetchOne(db, forSingleResult: false) + return try filter(key: key).fetchOne(db) } } diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 347a67bf3e..6297393125 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -313,12 +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? { - return try Self.fetchOne(db, request, forSingleResult: true) - } - - @inlinable - static func fetchOne(_ db: Database, _ request: R, forSingleResult singleResult: Bool) throws -> Self? { - let (statement, adapter) = try request.prepare(db, forSingleResult: singleResult) + let (statement, adapter) = try request.prepare(db, forSingleResult: true) return try fetchOne(statement, adapter: adapter) } } @@ -373,12 +368,7 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. @inlinable public func fetchOne(_ db: Database) throws -> RowDecoder? { - return try fetchOne(db, forSingleResult: true) - } - - @inlinable - func fetchOne(_ db: Database, forSingleResult singleResult: Bool) throws -> RowDecoder? { - return try RowDecoder.fetchOne(db, self, forSingleResult: singleResult) + return try RowDecoder.fetchOne(db, self) } } From 693e6da7735b2b619d1a50d0d378fd937e253ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 17 Apr 2019 13:08:51 +0200 Subject: [PATCH 14/21] Do not test that fetchOne does not add LIMIT 1 to raw SQL requests We do not add LIMIT 1 to raw SQL requests because it is difficult. But it would not be wrong to do so. --- Tests/GRDBTests/FetchableRecordTests.swift | 37 +++++++++++----------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/Tests/GRDBTests/FetchableRecordTests.swift b/Tests/GRDBTests/FetchableRecordTests.swift index 3bd952ef9b..dd01409228 100644 --- a/Tests/GRDBTests/FetchableRecordTests.swift +++ b/Tests/GRDBTests/FetchableRecordTests.swift @@ -261,49 +261,48 @@ class FetchableRecordTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - func test(_ nilBecauseMissingRow: Fetched?, sql: String) { + func test(_ nilBecauseMissingRow: Fetched?) { XCTAssertTrue(nilBecauseMissingRow == nil) - XCTAssertEqual(lastSQLQuery, sql) } do { let sql = "SELECT 1 WHERE 0" let statement = try db.makeSelectStatement(sql: sql) - try test(Fetched.fetchOne(db, sql: sql), sql: sql) - try test(Fetched.fetchOne(statement), sql: sql) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql)), sql: sql) - try test(SQLRequest(sql: sql).fetchOne(db), sql: sql) + try test(Fetched.fetchOne(db, sql: sql)) + try test(Fetched.fetchOne(statement)) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql))) + try test(SQLRequest(sql: sql).fetchOne(db)) } do { let sql = "SELECT 0, 1 WHERE 0" let statement = try db.makeSelectStatement(sql: sql) let adapter = SuffixRowAdapter(fromIndex: 1) - try test(Fetched.fetchOne(db, sql: sql, adapter: adapter), sql: sql) - try test(Fetched.fetchOne(statement, adapter: adapter), sql: sql) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter)), sql: sql) - try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db), sql: sql) + try test(Fetched.fetchOne(db, sql: sql, adapter: adapter)) + try test(Fetched.fetchOne(statement, adapter: adapter)) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter))) + try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db)) } } do { - func test(_ record: Fetched?, sql: String) { + func test(_ record: Fetched?) { XCTAssertEqual(record!.firstName, "Arthur") XCTAssertEqual(record!.lastName, "Martin") } do { let sql = "SELECT 'Arthur' AS firstName, 'Martin' AS lastName" let statement = try db.makeSelectStatement(sql: sql) - try test(Fetched.fetchOne(db, sql: sql), sql: sql) - try test(Fetched.fetchOne(statement), sql: sql) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql)), sql: sql) - try test(SQLRequest(sql: sql).fetchOne(db), sql: sql) + try test(Fetched.fetchOne(db, sql: sql)) + try test(Fetched.fetchOne(statement)) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql))) + try test(SQLRequest(sql: sql).fetchOne(db)) } do { let sql = "SELECT 0 AS firstName, 'Arthur' AS firstName, 'Martin' AS lastName" let statement = try db.makeSelectStatement(sql: sql) let adapter = SuffixRowAdapter(fromIndex: 1) - try test(Fetched.fetchOne(db, sql: sql, adapter: adapter), sql: sql) - try test(Fetched.fetchOne(statement, adapter: adapter), sql: sql) - try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter)), sql: sql) - try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db), sql: sql) + try test(Fetched.fetchOne(db, sql: sql, adapter: adapter)) + try test(Fetched.fetchOne(statement, adapter: adapter)) + try test(Fetched.fetchOne(db, SQLRequest(sql: sql, adapter: adapter))) + try test(SQLRequest(sql: sql, adapter: adapter).fetchOne(db)) } } } From e3a09359146d955de74205a6177d2c5d821c0bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 17 Apr 2019 14:01:16 +0200 Subject: [PATCH 15/21] =?UTF-8?q?Add=20SQL=20tests=20for=20all=20record=20?= =?UTF-8?q?testFetchOne=E2=80=A6()=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We usually update all those tests together --- Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift | 2 ++ Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift | 4 ++++ Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift | 4 ++++ Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift | 2 ++ Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift | 1 + Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift | 4 ++++ Tests/GRDBTests/RecordPrimaryKeySingleTests.swift | 4 ++++ ...rdPrimaryKeySingleWithReplaceConflictResolutionTests.swift | 4 ++++ 8 files changed, 25 insertions(+) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index c7cd25fff9..d4d2670926 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -462,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!))") } } } @@ -531,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 0919d114f0..bc01ae6a1b 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift @@ -284,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!)')") } } } From 65fe2bb50c2e87a73c5fa30577c2c51d74b6fcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 17 Apr 2019 21:31:30 +0200 Subject: [PATCH 16/21] Tests for the singleResult hint and fetching methods --- Tests/GRDBTests/FetchRequestTests.swift | 204 ++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/Tests/GRDBTests/FetchRequestTests.swift b/Tests/GRDBTests/FetchRequestTests.swift index bbdf574dc3..2712157376 100644 --- a/Tests/GRDBTests/FetchRequestTests.swift +++ b/Tests/GRDBTests/FetchRequestTests.swift @@ -136,4 +136,208 @@ 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") + } + } + } + } + } } From 21c0175a71fc211279d7de48eec9b17a5be3302d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 17 Apr 2019 21:31:53 +0200 Subject: [PATCH 17/21] Tests for the singleResult hint and fetchCount --- Tests/GRDBTests/FetchRequestTests.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tests/GRDBTests/FetchRequestTests.swift b/Tests/GRDBTests/FetchRequestTests.swift index 2712157376..bd8bb78dd3 100644 --- a/Tests/GRDBTests/FetchRequestTests.swift +++ b/Tests/GRDBTests/FetchRequestTests.swift @@ -340,4 +340,21 @@ class FetchRequestTests: GRDBTestCase { } } } + + 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')") + } + } } From a12830050b9951973b8b81f1a157ab0cba6e411a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 17 Apr 2019 21:32:13 +0200 Subject: [PATCH 18/21] Tests for the singleResult hint and databaseRegion --- Tests/GRDBTests/FetchRequestTests.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Tests/GRDBTests/FetchRequestTests.swift b/Tests/GRDBTests/FetchRequestTests.swift index bd8bb78dd3..0768afc62e 100644 --- a/Tests/GRDBTests/FetchRequestTests.swift +++ b/Tests/GRDBTests/FetchRequestTests.swift @@ -357,4 +357,23 @@ class FetchRequestTests: GRDBTestCase { 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)") + } + } } From 3f8047fc0e17be726d15595fb215e51c0e4d9f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 17 Apr 2019 21:32:47 +0200 Subject: [PATCH 19/21] Tests for the singleResult hint and SQLRequest.init(_:request:) --- Tests/GRDBTests/SQLRequestTests.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/GRDBTests/SQLRequestTests.swift b/Tests/GRDBTests/SQLRequestTests.swift index a8b9d7cf99..babd6c5bb0 100644 --- a/Tests/GRDBTests/SQLRequestTests.swift +++ b/Tests/GRDBTests/SQLRequestTests.swift @@ -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 From a172473e375fcc08c7ee3fa9f6502a5d6001d6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 17 Apr 2019 21:35:23 +0200 Subject: [PATCH 20/21] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b5e0c00a..65fa18a9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection: - [#499](https://github.com/groue/GRDB.swift/pull/499): Extract EncodableRecord from MutablePersistableRecord - [#502](https://github.com/groue/GRDB.swift/pull/502): Rename Future to DatabaseFuture - [#503](https://github.com/groue/GRDB.swift/pull/503): IFNULL support for association aggregates +- [#515](https://github.com/groue/GRDB.swift/pull/515): Add "LIMIT 1" to `fetchOne` requests ### Fixed From 5af16866777a76a667a767c0bdc28801a23ecc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 18 Apr 2019 07:02:42 +0200 Subject: [PATCH 21/21] Thank you notice --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20301a67b5..d6dca7ae8b 100644 --- a/README.md +++ b/README.md @@ -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.