diff --git a/CHANGELOG.md b/CHANGELOG.md index 194a0777d8..c49aac69f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Release Notes ## Next version -**New** +**New: Error Handling** - GRDB activates SQLite's [extended result codes](https://www.sqlite.org/rescode.html) for more detailed error reporting. - The new `ResultCode` type defines constants for all SQLite [result codes and extended result codes](https://www.sqlite.org/rescode.html). @@ -19,6 +19,18 @@ Release Notes } ``` +**New: Request** + +- The Request protocol for [custom requests](https://github.com/groue/GRDB.swift#custom-requests) learned how to count: + + ```swift + let request: Request = ... + let count = try request.fetchCount(db) // Int + ``` + + Default implementation performs a naive counting based on the request SQL: `SELECT COUNT(*) FROM (...)`. Adopting types can refine the counting SQL by refining their `fetchCount` implementation. + + ## 0.101.1 Released January 20, 2017 diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 7a7abcaa11..726d46d44b 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -552,6 +552,14 @@ 567404891CEF84C8003ED5CC /* RowAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567404871CEF84C8003ED5CC /* RowAdapter.swift */; }; 5674048A1CEF84C8003ED5CC /* RowAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567404871CEF84C8003ED5CC /* RowAdapter.swift */; }; 5674048B1CEF84C8003ED5CC /* RowAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567404871CEF84C8003ED5CC /* RowAdapter.swift */; }; + 56741EA81E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; + 56741EA91E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; + 56741EAA1E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; + 56741EAB1E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; + 56741EAC1E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; + 56741EAD1E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; + 56741EAE1E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; + 56741EAF1E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; 567A80541D41350C00C7DCEC /* IndexInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A80521D41350C00C7DCEC /* IndexInfoTests.swift */; }; 567A80551D41350C00C7DCEC /* IndexInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A80521D41350C00C7DCEC /* IndexInfoTests.swift */; }; 567A80561D41350C00C7DCEC /* IndexInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A80521D41350C00C7DCEC /* IndexInfoTests.swift */; }; @@ -1764,6 +1772,7 @@ 5672DE581CDB72520022BA81 /* DatabaseQueueBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueBackupTests.swift; sourceTree = ""; }; 5672DE661CDB751D0022BA81 /* DatabasePoolBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolBackupTests.swift; sourceTree = ""; }; 567404871CEF84C8003ED5CC /* RowAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowAdapter.swift; sourceTree = ""; }; + 56741EA71E66A8B3003E422D /* RequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; }; 567A80521D41350C00C7DCEC /* IndexInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndexInfoTests.swift; sourceTree = ""; }; 5683C2681B4D445E00296494 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.4.sdk/usr/lib/libsqlite3.dylib; sourceTree = DEVELOPER_DIR; }; 5687359E1CEDE16C009B9116 /* Betty.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Betty.jpeg; sourceTree = ""; }; @@ -2287,6 +2296,14 @@ path = Encryption; sourceTree = ""; }; + 56741EA61E66A88C003E422D /* Request */ = { + isa = PBXGroup; + children = ( + 56741EA71E66A8B3003E422D /* RequestTests.swift */, + ); + path = Request; + sourceTree = ""; + }; 567D10621C9A911C00ACC500 /* GRDBiOS */ = { isa = PBXGroup; children = ( @@ -2414,11 +2431,12 @@ 560A37A91C90084600949E71 /* DatabasePool */, 563363BB1C93FD32000BE133 /* DatabaseQueue */, 569C1EB01CF07DC80042627B /* DatabaseScheduler */, - 560B3FA41C19DFF800C58EC7 /* Persistable */, 56EA86921C91DFDA002BB4DF /* DatabaseReader */, 56A238171B9C74A90082EB20 /* DatabaseValue */, 56A238191B9C74A90082EB20 /* DatabaseValueConvertible */, 560C97BF1BFD0B5B00BF8471 /* Function */, + 560B3FA41C19DFF800C58EC7 /* Persistable */, + 56741EA61E66A88C003E422D /* Request */, 56A2381D1B9C74A90082EB20 /* Row */, 56C3F74A1CF9F11000F6A361 /* Savepoint */, 56A238201B9C74A90082EB20 /* Statement */, @@ -3757,6 +3775,7 @@ 560FC5811CB00B880014AA8E /* RecordCopyTests.swift in Sources */, 560FC5821CB00B880014AA8E /* RawRepresentableTests.swift in Sources */, 567E55ED1D2BDD3D00CC6F79 /* EncryptionTests.swift in Sources */, + 56741EA91E66A8B3003E422D /* RequestTests.swift in Sources */, 560FC5841CB00B880014AA8E /* PersistableTests.swift in Sources */, 560FC5871CB00B880014AA8E /* RecordSubClassTests.swift in Sources */, 560FC5891CB00B880014AA8E /* TransactionObserverTests.swift in Sources */, @@ -3969,6 +3988,7 @@ 5657AB501D108BA9006283EF /* NSNumberTests.swift in Sources */, 567156441CB16729007DC145 /* RecordSubClassTests.swift in Sources */, 5690C3391D23E7D200E59934 /* DateTests.swift in Sources */, + 56741EAA1E66A8B3003E422D /* RequestTests.swift in Sources */, 567156461CB16729007DC145 /* TransactionObserverTests.swift in Sources */, 567A80551D41350C00C7DCEC /* IndexInfoTests.swift in Sources */, 567156481CB16729007DC145 /* DatabaseValueTests.swift in Sources */, @@ -4161,6 +4181,7 @@ 5698ACBB1DA6285E0056AF8C /* FTS3TokenizerTests.swift in Sources */, 56AFCA561CB1AA9900F48B96 /* DatabasePoolConcurrencyTests.swift in Sources */, 56AFCA571CB1AA9900F48B96 /* TransactionObserverTests.swift in Sources */, + 56741EAD1E66A8B3003E422D /* RequestTests.swift in Sources */, 5623935C1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, 5698AC851DA380A20056AF8C /* VirtualTableModuleTests.swift in Sources */, 5698AC451DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */, @@ -4280,6 +4301,7 @@ 5698ACBC1DA6285E0056AF8C /* FTS3TokenizerTests.swift in Sources */, 5657AB441D108BA9006283EF /* NSDataTests.swift in Sources */, 5623935D1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, + 56741EAE1E66A8B3003E422D /* RequestTests.swift in Sources */, 5698AC861DA380A20056AF8C /* VirtualTableModuleTests.swift in Sources */, 5698AC461DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */, 56AF74711D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift in Sources */, @@ -4464,6 +4486,7 @@ 5657AB6A1D108BA9006283EF /* URLTests.swift in Sources */, 56F3E74D1E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */, 56EB0AB31BCD787300A3DC55 /* DataMemoryTests.swift in Sources */, + 56741EAC1E66A8B3003E422D /* RequestTests.swift in Sources */, 5672DE5C1CDB72520022BA81 /* DatabaseQueueBackupTests.swift in Sources */, 56A2385E1B9C74A90082EB20 /* RecordCopyTests.swift in Sources */, 563363B21C933FF8000BE133 /* PersistableTests.swift in Sources */, @@ -4582,6 +4605,7 @@ 56D496661D813086008276D7 /* QueryInterfaceRequestTests.swift in Sources */, 56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */, 5698ACD71DA925420056AF8C /* RowTestCase.swift in Sources */, + 56741EA81E66A8B3003E422D /* RequestTests.swift in Sources */, 56D496831D813147008276D7 /* SavepointTests.swift in Sources */, 56D496871D81316E008276D7 /* DatabaseTimestampTests.swift in Sources */, 56D496581D81304E008276D7 /* DateTests.swift in Sources */, @@ -4897,6 +4921,7 @@ F3BA810B1CFB3056003DC1BA /* Row+FoundationTests.swift in Sources */, 5698AC901DA389380056AF8C /* FTS3TableBuilderTests.swift in Sources */, F3BA80DB1CFB300E003DC1BA /* DatabaseValueConversionTests.swift in Sources */, + 56741EAF1E66A8B3003E422D /* RequestTests.swift in Sources */, 5657AB5D1D108BA9006283EF /* NSStringTests.swift in Sources */, F3BA81241CFB3063003DC1BA /* PrimaryKeySingleWithReplaceConflictResolutionTests.swift in Sources */, F3BA80E81CFB3016003DC1BA /* DictionaryRowTests.swift in Sources */, @@ -5094,6 +5119,7 @@ F3BA81381CFB3064003DC1BA /* RecordSubClassTests.swift in Sources */, F3BA81351CFB3064003DC1BA /* RecordEditedTests.swift in Sources */, 5657AB691D108BA9006283EF /* URLTests.swift in Sources */, + 56741EAB1E66A8B3003E422D /* RequestTests.swift in Sources */, 56B964D61DA521450002DA19 /* FTS5TableBuilderTests.swift in Sources */, F3BA81331CFB3064003DC1BA /* RecordAwakeFromFetchTests.swift in Sources */, 562393511DEDFEFB00A6B01F /* EnumeratedCursorTests.swift in Sources */, diff --git a/GRDB/Core/Request.swift b/GRDB/Core/Request.swift index 6ea20ddafc..dd960abda9 100644 --- a/GRDB/Core/Request.swift +++ b/GRDB/Core/Request.swift @@ -12,6 +12,31 @@ public protocol Request { /// A tuple that contains a prepared statement that is ready to be /// executed, and an eventual row adapter. func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) + + /// The number of rows matched by the request. + /// + /// Default implementation builds a naive SQL query based on the statement + /// returned by the `prepare` method: `SELECT COUNT(*) FROM (...)`. + /// + /// Adopting types can refine this countRequest method and return more + /// efficient SQL. + /// + /// - parameter db: A database connection. + func fetchCount(_ db: Database) throws -> Int +} + +extension Request { + /// The number of rows matched by the request. + /// + /// This default implementation builds a naive SQL query based on the + /// statement returned by the `prepare` method: `SELECT COUNT(*) FROM (...)`. + /// + /// - parameter db: A database connection. + public func fetchCount(_ db: Database) throws -> Int { + let (statement, _) = try prepare(db) + let sql = "SELECT COUNT(*) FROM (\(statement.sql))" + return try Int.fetchOne(db, sql, arguments: statement.arguments)! + } } extension Request { diff --git a/GRDB/QueryInterface/QueryInterfaceRequest.swift b/GRDB/QueryInterface/QueryInterfaceRequest.swift index 6cbed55e44..4986243eb6 100644 --- a/GRDB/QueryInterface/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -238,7 +238,7 @@ extension QueryInterfaceRequest { /// /// - parameter db: A database connection. public func fetchCount(_ db: Database) throws -> Int { - return try query.countRequest.fetchOne(db)! + return try query.fetchCount(db) } } diff --git a/GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift b/GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift index 45f8be2df6..7c520d49ed 100644 --- a/GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift +++ b/GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift @@ -90,9 +90,9 @@ struct QueryInterfaceSelectQueryDefinition { return sql } - /// Returns a fetch request that counts the number of rows matched by self. - var countRequest: AnyTypedRequest { - return countQuery.bound(to: Int.self) + /// Part of Request protocol + func fetchCount(_ db: Database) throws -> Int { + return try Int.fetchOne(db, countQuery)! } private var countQuery: QueryInterfaceSelectQueryDefinition { diff --git a/TODO.md b/TODO.md index 967d5cbca3..b2337182c0 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,12 @@ +- [ ] registerMigrationWithDisabledForeignKeyChecks should be renamed registerMigrationWithDeferredForeignKeyChecks +- [ ] Enable extended result codes (https://github.com/groue/GRDB.swift/issues/171) +- [ ] Request.fetchCount() (see https://github.com/groue/GRDB.swift/issues/176#issuecomment-282783884). This method should be a customization point, not an extension. + - [X] implementation + - [X] tests + - [ ] documentation +- [ ] Check that https://github.com/groue/GRDB.swift/issues/172#issuecomment-282511719 is true (manual deferred foreign key check) +- [ ] Document how to query external content tables (https://github.com/groue/GRDB.swift/issues/178) +- [ ] SQLiteLib 3.17.0 - [ ] fts3tokenize was introduced in SQLite 3.7.17 (iOS 8.2 and OS X 10.10). And GRDB uses it before. - [ ] Make GRDB less stringly-typed: For each API that eats column names, check if it couldn't eat both Column and String. If this requires Column to adopt ExpressibleByStringLiteral, check if it does not introduce awful ambiguities - [ ] Check for SQLCipher at runtime with `PRAGMA cipher_version`: https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688 diff --git a/Tests/Public/Core/Request/RequestTests.swift b/Tests/Public/Core/Request/RequestTests.swift new file mode 100644 index 0000000000..55cfb43a35 --- /dev/null +++ b/Tests/Public/Core/Request/RequestTests.swift @@ -0,0 +1,83 @@ +import XCTest +#if USING_SQLCIPHER + import GRDBCipher +#elseif USING_CUSTOMSQLITE + import GRDBCustomSQLite +#else + import GRDB +#endif + +class RequestTests: GRDBTestCase { + + func testRequestFetch() throws { + struct CustomRequest : Request { + func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try (db.makeSelectStatement("SELECT * FROM table1"), nil) + } + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "table1") { t in + t.column("id", .integer).primaryKey() + } + try db.execute("INSERT INTO table1 DEFAULT VALUES") + try db.execute("INSERT INTO table1 DEFAULT VALUES") + + let request = CustomRequest() + let rows = try Row.fetchAll(db, request) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM table1") + XCTAssertEqual(rows.count, 2) + XCTAssertEqual(rows[0], ["id": 1]) + XCTAssertEqual(rows[1], ["id": 2]) + } + } + + func testRequestFetchCount() throws { + struct CustomRequest : Request { + func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try (db.makeSelectStatement("SELECT * FROM table1"), nil) + } + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "table1") { t in + t.column("id", .integer).primaryKey() + } + try db.execute("INSERT INTO table1 DEFAULT VALUES") + try db.execute("INSERT INTO table1 DEFAULT VALUES") + + let request = CustomRequest() + let count = try request.fetchCount(db) + XCTAssertEqual(lastSQLQuery, "SELECT COUNT(*) FROM (SELECT * FROM table1)") + XCTAssertEqual(count, 2) + } + } + + func testRequestCustomizedFetchCount() throws { + struct CustomRequest : Request { + func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try (db.makeSelectStatement("INVALID"), nil) + } + + func fetchCount(_ db: Database) throws -> Int { + return 2 + } + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "table1") { t in + t.column("id", .integer).primaryKey() + } + try db.execute("INSERT INTO table1 DEFAULT VALUES") + try db.execute("INSERT INTO table1 DEFAULT VALUES") + + let request = CustomRequest() + let count = try request.fetchCount(db) + XCTAssertEqual(lastSQLQuery, "INSERT INTO table1 DEFAULT VALUES") + XCTAssertEqual(count, 2) + } + } +}