diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f9d8cef5..eb72fbff60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: #### 5.x Releases - - +- `5.2.x` Releases - [5.2.0](#520) - `5.1.x` Releases - [5.1.0](#510) - `5.0.x` Releases - [5.0.0](#500) | [5.0.1](#501) | [5.0.2](#502) | [5.0.3](#503) - `5.0.0` Betas - [5.0.0-beta](#500-beta) | [5.0.0-beta.2](#500-beta2) | [5.0.0-beta.3](#500-beta3) | [5.0.0-beta.4](#500-beta4) | [5.0.0-beta.5](#500-beta5) | [5.0.0-beta.6](#500-beta6) | [5.0.0-beta.7](#500-beta7) | [5.0.0-beta.8](#500-beta8) | [5.0.0-beta.9](#500-beta9) | [5.0.0-beta.10](#500-beta10) | [5.0.0-beta.11](#500-beta11) @@ -69,6 +68,17 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - [0.110.0](#01100), ... +## 5.2.0 + +Released November 29, 2020 • [diff](https://github.com/groue/GRDB.swift/compare/v5.1.0...v5.2.0) + +- **New**: [#868](https://github.com/groue/GRDB.swift/pull/868): ValueObservation optimization is opt-in. +- **New**: [#872](https://github.com/groue/GRDB.swift/pull/872): Parse time zones +- **Documentation update**: The [ValueObservation Performance](https://github.com/groue/GRDB.swift/blob/master/README.md#valueobservation-performance) chapter was extended with a tip for observations that track a constant database region. +- **Documentation update**: The [Date and DateComponents](https://github.com/groue/GRDB.swift/blob/master/README.md#date-and-datecomponents) chapter describes the support for time zones. +- **Documentation update**: A caveat ([#871](https://github.com/groue/GRDB.swift/issues/871)) with the `including(all:)` method, which may fail with a database error of code [`SQLITE_ERROR`](https://www.sqlite.org/rescode.html#error) (1) "Expression tree is too large" when you use a compound foreign key and there are a lot of parent records, is detailed in the [Joining And Prefetching Associated Records](https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md#joining-and-prefetching-associated-records) chapter. + + ## 5.1.0 Released November 1, 2020 • [diff](https://github.com/groue/GRDB.swift/compare/v5.0.3...v5.1.0) diff --git a/Documentation/AssociationsBasics.md b/Documentation/AssociationsBasics.md index 9fb15f9f34..74cbe8ddc0 100644 --- a/Documentation/AssociationsBasics.md +++ b/Documentation/AssociationsBasics.md @@ -937,6 +937,24 @@ The pattern is always the same: you start from a base request, that you extend w Finally, readers who speak SQL may compare `optional` with left joins, and `required` with inner joins. +> :warning: **Warning**: You will get a database error with code [`SQLITE_ERROR`](https://www.sqlite.org/rescode.html#error) (1) "Expression tree is too large", when the following conditions are met: +> +> - You use the `including(all:)` method (say: `Parent.including(all: children)`). +> - The association is based on a compound foreign key (made of two columns or more). +> - The request fetches a lot of parent records. The exact threshold depends on [SQLITE_LIMIT_EXPR_DEPTH](https://www.sqlite.org/limits.html). It is around 1000 parents in recent iOS and macOS systems. To get an exact figure, run: +> +> ```swift +> let limit = try dbQueue.read { db in +> sqlite3_limit(db.sqliteConnection, SQLITE_LIMIT_EXPR_DEPTH, -1) +> } +> ``` +> +> Possible workarounds are: +> +> - Refactor the database schema so that you do not depend on a compound foreign key. +> - Prefetch children with your own code, without using `including(all:)`. +> +> For more information about this caveat, see [issue #871](https://github.com/groue/GRDB.swift/issues/871). ## Combining Associations @@ -2374,6 +2392,10 @@ See [Good Practices for Designing Record Types] for more information. .including(required: Passport.citizen)) ``` +- **The `including(all:)` method may fail with a database error of code [`SQLITE_ERROR`](https://www.sqlite.org/rescode.html#error) (1) "Expression tree is too large" when you use a compound foreign key and there are a lot of parent records.** + + See [Joining And Prefetching Associated Records] for more information about this error. + Come [discuss](http://twitter.com/groue) for more information, or if you wish to help turning those missing features into reality. --- diff --git a/Documentation/FullTextSearch.md b/Documentation/FullTextSearch.md index e34ad3be31..4ff8e7a7b2 100644 --- a/Documentation/FullTextSearch.md +++ b/Documentation/FullTextSearch.md @@ -280,7 +280,7 @@ let pattern = FTS3Pattern(matchingAnyTokenIn: "") // nil let pattern = FTS3Pattern(matchingAnyTokenIn: "*") // nil ``` -FTS3Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html): +FTS3Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html): ```swift let documents = try Document.fetchAll(db, @@ -529,7 +529,7 @@ let pattern = FTS5Pattern(matchingAnyTokenIn: "") // nil let pattern = FTS5Pattern(matchingAnyTokenIn: "*") // nil ``` -FTS5Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html): +FTS5Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html): ```swift let documents = try Document.fetchAll(db, diff --git a/Documentation/GRDB5MigrationGuide.md b/Documentation/GRDB5MigrationGuide.md index 6dfd64d5c2..979673ee3d 100644 --- a/Documentation/GRDB5MigrationGuide.md +++ b/Documentation/GRDB5MigrationGuide.md @@ -93,19 +93,6 @@ let observation = ValueObservation.tracking { db in let observation = ValueObservation.tracking(Player.fetchAll) ``` -If the tracked value is computed from several database requests that are not always the same, make sure you use the `trackingVaryingRegion` method, as below. See [Observing a Varying Database Region] for more information. - -```swift -// An observation which does not always execute the same requests: -let observation = ValueObservation.trackingVaryingRegion { db -> Int in - let preference = try Preference.fetchOne(db) ?? .default - switch preference.selection { - case .food: return try Food.fetchCount(db) - case .beverage: return try Beverage.fetchCount(db) - } -} -``` - Several methods that build observations were removed: ```swift @@ -515,7 +502,6 @@ let publisher = observation [ValueObservation]: ../README.md#valueobservation [DatabaseRegionObservation]: ../README.md#databaseregionobservation [RxGRDB]: http://github.com/RxSwiftCommunity/RxGRDB -[Observing a Varying Database Region]: ../README.md#observing-a-varying-database-region [removeDuplicates]: ../README.md#valueobservationremoveduplicates [Custom SQL functions]: ../README.md#custom-sql-functions [Batch updates]: ../README.md#update-requests diff --git a/Documentation/Migrations.md b/Documentation/Migrations.md index 69de2b2894..f962a66900 100644 --- a/Documentation/Migrations.md +++ b/Documentation/Migrations.md @@ -68,7 +68,7 @@ try dbQueue.read { db in } ``` -See the [DatabaseMigrator reference](http://groue.github.io/GRDB.swift/docs/5.1/Structs/DatabaseMigrator.html) for more migrator methods. +See the [DatabaseMigrator reference](http://groue.github.io/GRDB.swift/docs/5.2/Structs/DatabaseMigrator.html) for more migrator methods. ## The `eraseDatabaseOnSchemaChange` Option diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 36a04bc38a..b744d98b11 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GRDB.swift' - s.version = '5.1.0' + s.version = '5.2.0' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'A toolkit for SQLite databases, with a focus on application development.' diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index cf00ab3a11..a67d8e23e6 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -2,18 +2,7 @@ import Foundation // inspired by: http://jordansmith.io/performant-date-parsing/ -class SQLiteDateParser { - - private struct ParserComponents { - var year: Int32 = 0 - var month: Int32 = 0 - var day: Int32 = 0 - var hour: Int32 = 0 - var minute: Int32 = 0 - var second: Int32 = 0 - var nanosecond = ContiguousArray(repeating: 0, count: 10) // 9 digits, and trailing \0 - } - +final class SQLiteDateParser { func components(from dateString: String) -> DatabaseDateComponents? { dateString.withCString { cString in components(cString: cString, length: strlen(cString)) @@ -29,17 +18,25 @@ class SQLiteDateParser { // "YYYY-..." -> datetime if cString[4] == UInt8(ascii: "-") { var components = DateComponents() - return parseDatetimeFormat(cString: cString, length: length, into: &components) - .map { DatabaseDateComponents(components, format: $0) + var parser = Parser(cString: cString, length: length) + guard let format = parseDatetimeFormat(parser: &parser, into: &components), + parser.length == 0 + else { + return nil } + return DatabaseDateComponents(components, format: format) } // "HH-:..." -> time if cString[2] == UInt8(ascii: ":") { var components = DateComponents() - return parseTimeFormat(cString: cString, length: length, into: &components) - .map { DatabaseDateComponents(components, format: $0) + var parser = Parser(cString: cString, length: length) + guard let format = parseTimeFormat(parser: &parser, into: &components), + parser.length == 0 + else { + return nil } + return DatabaseDateComponents(components, format: format) } // Invalid @@ -54,35 +51,28 @@ class SQLiteDateParser { // - YYYY-MM-DDTHH:MM:SS // - YYYY-MM-DDTHH:MM:SS.SSS private func parseDatetimeFormat( - cString: UnsafePointer, - length: Int, + parser: inout Parser, into components: inout DateComponents) - -> DatabaseDateComponents.Format? + -> DatabaseDateComponents.Format? { - var cString = cString - var remainingLength = length - - if remainingLength < 10 { return nil } - remainingLength -= 10 - guard - let year = parseNNNN(cString: &cString), - parse("-", cString: &cString), - let month = parseNN(cString: &cString), - parse("-", cString: &cString), - let day = parseNN(cString: &cString) - else { return nil } + guard let year = parser.parseNNNN(), + parser.parse("-"), + let month = parser.parseNN(), + parser.parse("-"), + let day = parser.parseNN() + else { return nil } components.year = year components.month = month components.day = day - if remainingLength == 0 { return .YMD } + if parser.length == 0 { return .YMD } - remainingLength -= 1 - guard parse(" ", cString: &cString) || parse("T", cString: &cString) else { + guard parser.parse(" ") || parser.parse("T") + else { return nil } - switch parseTimeFormat(cString: cString, length: remainingLength, into: &components) { + switch parseTimeFormat(parser: &parser, into: &components) { case .HM: return .YMD_HM case .HMS: return .YMD_HMS case .HMSS: return .YMD_HMSS @@ -94,94 +84,145 @@ class SQLiteDateParser { // - HH:MM:SS // - HH:MM:SS.SSS private func parseTimeFormat( - cString: UnsafePointer, - length: Int, + parser: inout Parser, into components: inout DateComponents) - -> DatabaseDateComponents.Format? + -> DatabaseDateComponents.Format? { - var cString = cString - var remainingLength = length - - if remainingLength < 5 { return nil } - remainingLength -= 5 - guard - let hour = parseNN(cString: &cString), - parse(":", cString: &cString), - let minute = parseNN(cString: &cString) - else { return nil } + guard let hour = parser.parseNN(), + parser.parse(":"), + let minute = parser.parseNN() + else { return nil } components.hour = hour components.minute = minute - if remainingLength == 0 { return .HM } + if parser.length == 0 || parseTimeZone(parser: &parser, into: &components) { return .HM } - if remainingLength < 3 { return nil } - remainingLength -= 3 - guard - parse(":", cString: &cString), - let second = parseNN(cString: &cString) - else { return nil } + guard parser.parse(":"), + let second = parser.parseNN() + else { return nil } components.second = second - if remainingLength == 0 { return .HMS } + if parser.length == 0 || parseTimeZone(parser: &parser, into: &components) { return .HMS } - if remainingLength < 1 { return nil } - remainingLength -= 1 - guard parse(".", cString: &cString) else { return nil } + guard parser.parse(".") else { return nil } - // Parse three digits + // Parse one to three digits // Rationale: https://github.com/groue/GRDB.swift/pull/362 - remainingLength = min(remainingLength, 3) var nanosecond = 0 - for _ in 0..) -> Int? { - var number = 0 - guard parseDigit(cString: &cString, into: &number) - && parseDigit(cString: &cString, into: &number) - && parseDigit(cString: &cString, into: &number) - && parseDigit(cString: &cString, into: &number) - else { - return nil + private func parseTimeZone( + parser: inout Parser, + into components: inout DateComponents) + -> Bool + { + if parser.parse("Z") { + components.timeZone = TimeZone(secondsFromGMT: 0) + return true } - return number - } - - @inline(__always) - private func parseNN(cString: inout UnsafePointer) -> Int? { - var number = 0 - guard parseDigit(cString: &cString, into: &number) - && parseDigit(cString: &cString, into: &number) - else { - return nil + + if parser.parse("+"), + let hour = parser.parseNN(), + parser.parse(":"), + let minute = parser.parseNN() + { + components.timeZone = TimeZone(secondsFromGMT: hour * 3600 + minute * 60) + return true } - return number - } - - @inline(__always) - private func parse(_ scalar: Unicode.Scalar, cString: inout UnsafePointer) -> Bool { - guard cString[0] == UInt8(ascii: scalar) else { - return false + + if parser.parse("-"), + let hour = parser.parseNN(), + parser.parse(":"), + let minute = parser.parseNN() + { + components.timeZone = TimeZone(secondsFromGMT: -(hour * 3600 + minute * 60)) + return true } - cString += 1 - return true + + return false } - @inline(__always) - private func parseDigit(cString: inout UnsafePointer, into number: inout Int) -> Bool { - let char = cString[0] - let digit = char - CChar(bitPattern: UInt8(ascii: "0")) - guard digit >= 0 && digit <= 9 else { - return false - } - cString += 1 - number = number * 10 + Int(digit) - return true + private struct Parser { + var cString: UnsafePointer + var length: Int + + @inline(__always) + private mutating func shift() { + cString += 1 + length -= 1 + } + + @inline(__always) + mutating func parse(_ scalar: Unicode.Scalar) -> Bool { + guard length > 0, cString[0] == UInt8(ascii: scalar) else { + return false + } + shift() + return true + } + + @inline(__always) + mutating func parseDigit() -> Int? { + guard length > 0 else { + return nil + } + let char = cString[0] + let digit = char - CChar(bitPattern: UInt8(ascii: "0")) + guard digit >= 0 && digit <= 9 else { + return nil + } + shift() + return Int(digit) + } + + @inline(__always) + mutating func parseDigit(into number: inout Int) -> Bool { + guard let digit = parseDigit() else { + return false + } + number = number * 10 + digit + return true + } + + @inline(__always) + mutating func parseNNNN() -> Int? { + var number = 0 + guard parseDigit(into: &number) + && parseDigit(into: &number) + && parseDigit(into: &number) + && parseDigit(into: &number) + else { + // Don't restore self to initial state because we don't need it + return nil + } + return number + } + + @inline(__always) + mutating func parseNN() -> Int? { + var number = 0 + guard parseDigit(into: &number) + && parseDigit(into: &number) + else { + // Don't restore self to initial state because we don't need it + return nil + } + return number + } } } diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 29ccf89250..9f9fbe6d30 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -409,14 +409,85 @@ extension ValueObservation where Reducer == ValueReducers.Auto { // MARK: - Creating ValueObservation - /// Creates a ValueObservation which notifies the values returned by the - /// *fetch* function whenever a database transaction changes them. + /// Creates an optimized `ValueObservation` that notifies the values + /// returned by the `fetch` function whenever a database transaction + /// changes them. /// - /// The *fetch* function must always performs the same database requests. - /// The stability of the observed database region allows optimizations. + /// The optimization only kicks in when the observation is started from a + /// `DatabasePool`: fresh values are fetched concurrently, and do not block + /// database writes. /// - /// When you want to observe a varying database region, use the - /// `ValueObservation.trackingVaryingRegion(_:)` method instead. + /// - precondition: The *fetch* function must perform requests that fetch + /// from a single and constant database region. The tracked region is made + /// of tables, columns, and, when possible, rowids of individual rows. All + /// changes that happen outside of this region do not impact + /// the observation. + /// + /// For example: + /// + /// // Tracks the full 'player' table + /// let observation = ValueObservation.trackingConstantRegion { db -> [Player] in + /// try Player.fetchAll(db) + /// } + /// + /// // Tracks the row with id 42 in the 'player' table + /// let observation = ValueObservation.trackingConstantRegion { db -> Player? in + /// try Player.fetchOne(db, key: 42) + /// } + /// + /// // Tracks the 'score' column in the 'player' table + /// let observation = ValueObservation.trackingConstantRegion { db -> Int? in + /// try Player.select(max(Column("score"))).fetchOne(db) + /// } + /// + /// // Tracks both the 'player' and 'team' tables + /// let observation = ValueObservation.trackingConstantRegion { db -> ([Team], [Player]) in + /// let teams = try Team.fetchAll(db) + /// let players = try Player.fetchAll(db) + /// return (teams, players) + /// } + /// + /// When you want to observe a varying database region, make sure you use + /// the `ValueObservation.tracking(_:)` method instead, or else some changes + /// will not be notified. + /// + /// For example, consider those three observations below that depend on some + /// user preference. They all track a varying region, and must + /// use `ValueObservation.tracking(_:)`: + /// + /// // Does not always track the same row in the player table. + /// let observation = ValueObservation.tracking { db -> Player? in + /// let pref = try Preference.fetchOne(db) ?? .default + /// return try Player.fetchOne(db, key: pref.favoritePlayerId) + /// } + /// + /// // Only tracks the 'user' table if there are some blocked emails. + /// let observation = ValueObservation.tracking { db -> [User] in + /// let pref = try Preference.fetchOne(db) ?? .default + /// let blockedEmails = pref.blockedEmails + /// return try User.filter(blockedEmails.contains(Column("email"))).fetchAll(db) + /// } + /// + /// // Sometimes tracks the 'food' table, and sometimes the 'beverage' table. + /// let observation = ValueObservation.tracking { db -> Int in + /// let pref = try Preference.fetchOne(db) ?? .default + /// switch pref.selection { + /// case .food: return try Food.fetchCount(db) + /// case .beverage: return try Beverage.fetchCount(db) + /// } + /// } + /// + /// - parameter fetch: A function that fetches the observed value from + /// the database. + public static func trackingConstantRegion( + _ fetch: @escaping (Database) throws -> Value) + -> ValueObservation> + { + .init(makeReducer: { .init(isSelectedRegionDeterministic: true, fetch: fetch) }) + } + + /// Creates a `ValueObservation` that notifies the values returned by the + /// `fetch` function whenever a database transaction changes them. /// /// For example: /// @@ -427,7 +498,7 @@ extension ValueObservation where Reducer == ValueReducers.Auto { /// let cancellable = try observation.start( /// in: dbQueue, /// onError: { error in ... }, - /// onChange:) { players: [Player] in + /// onChange: { players: [Player] in /// print("Players have changed") /// }) /// @@ -437,18 +508,32 @@ extension ValueObservation where Reducer == ValueReducers.Auto { _ fetch: @escaping (Database) throws -> Value) -> ValueObservation> { - .init(makeReducer: { .init(isSelectedRegionDeterministic: true, fetch: fetch) }) + .init(makeReducer: { .init(isSelectedRegionDeterministic: false, fetch: fetch) }) } - /// Creates a ValueObservation which notifies the values returned by the - /// *fetch* function whenever a database transaction changes them. + /// Creates a `ValueObservation` that notifies the values returned by the + /// `fetch` function whenever a database transaction changes them. + /// + /// For example: + /// + /// let observation = ValueObservation.tracking { db in + /// try Player.fetchAll(db) + /// } + /// + /// let cancellable = try observation.start( + /// in: dbQueue, + /// onError: { error in ... }, + /// onChange: { players: [Player] in + /// print("Players have changed") + /// }) /// /// - parameter fetch: A function that fetches the observed value from /// the database. + @available(*, deprecated, renamed: "tracking(_:)") public static func trackingVaryingRegion( _ fetch: @escaping (Database) throws -> Value) -> ValueObservation> { - .init(makeReducer: { .init(isSelectedRegionDeterministic: false, fetch: fetch) }) + tracking(fetch) } } diff --git a/Makefile b/Makefile index 7ab318e5d3..7724f0d0a2 100644 --- a/Makefile +++ b/Makefile @@ -409,10 +409,10 @@ ifdef JAZZY --author 'Gwendal Roué' \ --author_url https://github.com/groue \ --github_url https://github.com/groue/GRDB.swift \ - --github-file-prefix https://github.com/groue/GRDB.swift/tree/v5.1.0 \ - --module-version 5.1.0 \ + --github-file-prefix https://github.com/groue/GRDB.swift/tree/v5.2.0 \ + --module-version 5.2.0 \ --module GRDB \ - --root-url http://groue.github.io/GRDB.swift/docs/5.1/ \ + --root-url http://groue.github.io/GRDB.swift/docs/5.2/ \ --output Documentation/Reference \ --xcodebuild-arguments -project,GRDB.xcodeproj,-scheme,GRDBiOS else diff --git a/README.md b/README.md index 2ffafbaf5e..06f43356b5 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ --- -**Latest release**: November 1, 2020 • version 5.1.0 • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 4 to GRDB 5](Documentation/GRDB5MigrationGuide.md) +**Latest release**: November 29, 2020 • version 5.2.0 • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 4 to GRDB 5](Documentation/GRDB5MigrationGuide.md) **Requirements**: iOS 10.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ • Swift 5.2+ / Xcode 11.4+ | Swift version | GRDB version | | -------------- | ----------------------------------------------------------- | -| **Swift 5.2+** | **v5.1.0** | +| **Swift 5.2+** | **v5.2.0** | | Swift 5.1 | [v4.14.0](https://github.com/groue/GRDB.swift/tree/v4.14.0) | | Swift 5 | [v4.14.0](https://github.com/groue/GRDB.swift/tree/v4.14.0) | | Swift 4.2 | [v4.14.0](https://github.com/groue/GRDB.swift/tree/v4.14.0) | @@ -278,7 +278,7 @@ Documentation #### Reference -- [GRDB Reference](http://groue.github.io/GRDB.swift/docs/5.1/index.html) (generated by [Jazzy](https://github.com/realm/jazzy)) +- [GRDB Reference](http://groue.github.io/GRDB.swift/docs/5.2/index.html) (generated by [Jazzy](https://github.com/realm/jazzy)) #### Getting Started @@ -480,7 +480,7 @@ let dbQueue = try DatabaseQueue( configuration: config) ``` -See [Configuration](http://groue.github.io/GRDB.swift/docs/5.1/Structs/Configuration.html) for more details. +See [Configuration](http://groue.github.io/GRDB.swift/docs/5.2/Structs/Configuration.html) for more details. ## Database Pools @@ -560,7 +560,7 @@ let dbPool = try DatabasePool( configuration: config) ``` -See [Configuration](http://groue.github.io/GRDB.swift/docs/5.1/Structs/Configuration.html) for more details. +See [Configuration](http://groue.github.io/GRDB.swift/docs/5.2/Structs/Configuration.html) for more details. Database pools are more memory-hungry than database queues. See [Memory Management](#memory-management) for more information. @@ -620,7 +620,7 @@ try dbQueue.write { db in } ``` -The `?` and colon-prefixed keys like `:score` in the SQL query are the **statements arguments**. You pass arguments with arrays or dictionaries, as in the example above. See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments. +The `?` and colon-prefixed keys like `:score` in the SQL query are the **statements arguments**. You pass arguments with arrays or dictionaries, as in the example above. See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments. You can also embed query arguments right into your SQL queries, with the `literal` argument label, as in the example below. See [SQL Interpolation] for more details. @@ -823,7 +823,7 @@ try dbQueue.read { db in - **Cursors are granted with direct access to SQLite,** unlike arrays and sets that have to take the time to copy database values. If you look after extra performance, you may prefer cursors. -- **Cursors adopt the [Cursor](http://groue.github.io/GRDB.swift/docs/5.1/Protocols/Cursor.html) protocol, which looks a lot like standard [lazy sequences](https://developer.apple.com/reference/swift/lazysequenceprotocol) of Swift.** As such, cursors come with many convenience methods: `compactMap`, `contains`, `dropFirst`, `dropLast`, `drop(while:)`, `enumerated`, `filter`, `first`, `flatMap`, `forEach`, `joined`, `joined(separator:)`, `max`, `max(by:)`, `min`, `min(by:)`, `map`, `prefix`, `prefix(while:)`, `reduce`, `reduce(into:)`, `suffix`: +- **Cursors adopt the [Cursor](http://groue.github.io/GRDB.swift/docs/5.2/Protocols/Cursor.html) protocol, which looks a lot like standard [lazy sequences](https://developer.apple.com/reference/swift/lazysequenceprotocol) of Swift.** As such, cursors come with many convenience methods: `compactMap`, `contains`, `dropFirst`, `dropLast`, `drop(while:)`, `enumerated`, `filter`, `first`, `flatMap`, `forEach`, `joined`, `joined(separator:)`, `max`, `max(by:)`, `min`, `min(by:)`, `map`, `prefix`, `prefix(while:)`, `reduce`, `reduce(into:)`, `suffix`: ```swift // Prints all Github links @@ -906,7 +906,7 @@ let rows = try Row.fetchAll(db, arguments: ["name": "Arthur"]) ``` -See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments. +See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments. Unlike row arrays that contain copies of the database rows, row cursors are close to the SQLite metal, and require a little care: @@ -1191,7 +1191,7 @@ GRDB ships with built-in support for the following value types: - Generally speaking, all types that adopt the [DatabaseValueConvertible](#custom-value-types) protocol. -Values can be used as [statement arguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html): +Values can be used as [statement arguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html): ```swift let url: URL = ... @@ -1269,24 +1269,26 @@ The non-copied data does not live longer than the iteration step: make sure that Here is how GRDB supports the various [date formats](https://www.sqlite.org/lang_datefunc.html) supported by SQLite: -| SQLite format | Date | DateComponents | -|:---------------------------- |:------------:|:--------------:| -| YYYY-MM-DD | Read ¹ | Read/Write | -| YYYY-MM-DD HH:MM | Read ¹ | Read/Write | -| YYYY-MM-DD HH:MM:SS | Read ¹ | Read/Write | -| YYYY-MM-DD HH:MM:SS.SSS | Read/Write ¹ | Read/Write | -| YYYY-MM-DD**T**HH:MM | Read ¹ | Read | -| YYYY-MM-DD**T**HH:MM:SS | Read ¹ | Read | -| YYYY-MM-DD**T**HH:MM:SS.SSS | Read ¹ | Read | -| HH:MM | | Read/Write | -| HH:MM:SS | | Read/Write | -| HH:MM:SS.SSS | | Read/Write | -| Timestamps since unix epoch | Read ² | | -| `now` | | | +| SQLite format | Date | DateComponents | +|:---------------------------- |:------------------:|:--------------:| +| YYYY-MM-DD | Read ¹ | Read / Write | +| YYYY-MM-DD HH:MM | Read ¹ ² | Read ² / Write | +| YYYY-MM-DD HH:MM:SS | Read ¹ ² | Read ² / Write | +| YYYY-MM-DD HH:MM:SS.SSS | Read ¹ ² / Write ¹ | Read ² / Write | +| YYYY-MM-DD**T**HH:MM | Read ¹ ² | Read ² | +| YYYY-MM-DD**T**HH:MM:SS | Read ¹ ² | Read ² | +| YYYY-MM-DD**T**HH:MM:SS.SSS | Read ¹ ² | Read ² | +| HH:MM | | Read ² / Write | +| HH:MM:SS | | Read ² / Write | +| HH:MM:SS.SSS | | Read ² / Write | +| Timestamps since unix epoch | Read ³ | | +| `now` | | | -¹ Dates are stored and read in the UTC time zone. Missing components are assumed to be zero. +¹ Missing components are assumed to be zero. Dates are stored and read in the UTC time zone, unless the format is followed by a timezone indicator ⁽²⁾. -² GRDB 2+ interprets numerical values as timestamps that fuel `Date(timeIntervalSince1970:)`. Previous GRDB versions used to interpret numbers as [julian days](https://en.wikipedia.org/wiki/Julian_day). Julian days are still supported, with the `Date(julianDay:)` initializer. +² This format may be optionally followed by a timezone indicator of the form `[+-]HH:MM` or just `Z`. + +³ GRDB 2+ interprets numerical values as timestamps that fuel `Date(timeIntervalSince1970:)`. Previous GRDB versions used to interpret numbers as [julian days](https://en.wikipedia.org/wiki/Julian_day). Julian days are still supported, with the `Date(julianDay:)` initializer. #### Date @@ -1598,7 +1600,7 @@ try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction } ``` -Transactions can't be left opened unless you set the [allowsUnsafeTransactions](http://groue.github.io/GRDB.swift/docs/5.1/Structs/Configuration.html) configuration flag: +Transactions can't be left opened unless you set the [allowsUnsafeTransactions](http://groue.github.io/GRDB.swift/docs/5.2/Structs/Configuration.html) configuration flag: ```swift // fatal error: A transaction has been left opened at the end of a database access @@ -1712,7 +1714,7 @@ try dbQueue.write { db in } ``` -The `?` and colon-prefixed keys like `:name` in the SQL query are the statement arguments. You set them with arrays or dictionaries (arguments are actually of type [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html), which happens to adopt the ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral protocols). +The `?` and colon-prefixed keys like `:name` in the SQL query are the statement arguments. You set them with arrays or dictionaries (arguments are actually of type [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html), which happens to adopt the ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral protocols). ```swift updateStatement.arguments = ["name": "Arthur", "score": 1000] @@ -2489,7 +2491,7 @@ try Place.fetchSet(db, sql: "SELECT ...", arguments:...) // Set try Place.fetchOne(db, sql: "SELECT ...", arguments:...) // Place? ``` -See [fetching methods](#fetching-methods) for information about the `fetchCursor`, `fetchAll`, `fetchSet` and `fetchOne` methods. See [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html) for more information about the query arguments. +See [fetching methods](#fetching-methods) for information about the `fetchCursor`, `fetchAll`, `fetchSet` and `fetchOne` methods. See [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html) for more information about the query arguments. > :point_up: **Note**: for performance reasons, the same row argument to `init(row:)` is reused during the iteration of a fetch query. If you want to keep the row for later use, make sure to store a copy: `self.row = row.copy()`. @@ -2870,7 +2872,7 @@ protocol EncodableRecord { } ``` -See [DatabaseDateDecodingStrategy](https://groue.github.io/GRDB.swift/docs/5.1/Enums/DatabaseDateDecodingStrategy.html), [DatabaseDateEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.1/Enums/DatabaseDateEncodingStrategy.html), and [DatabaseUUIDEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.1/Enums/DatabaseUUIDEncodingStrategy.html) to learn about all available strategies. +See [DatabaseDateDecodingStrategy](https://groue.github.io/GRDB.swift/docs/5.2/Enums/DatabaseDateDecodingStrategy.html), [DatabaseDateEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.2/Enums/DatabaseDateEncodingStrategy.html), and [DatabaseUUIDEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.2/Enums/DatabaseUUIDEncodingStrategy.html) to learn about all available strategies. > :point_up: **Note**: there is no customization of uuid decoding, because UUID can already decode all its encoded variants (16-bytes blobs, and uuid strings). @@ -4325,7 +4327,7 @@ Player // SELECT * FROM player ``` -Raw SQL snippets are also accepted, with eventual [arguments](http://groue.github.io/GRDB.swift/docs/5.1/Structs/StatementArguments.html): +Raw SQL snippets are also accepted, with eventual [arguments](http://groue.github.io/GRDB.swift/docs/5.2/Structs/StatementArguments.html): ```swift // SELECT DATE(creationDate), COUNT(*) FROM player WHERE name = 'Arthur' GROUP BY date(creationDate) @@ -4981,7 +4983,7 @@ try Player.customRequest().fetchAll(db) // [Player] - The `adapted(_:)` method eases the consumption of complex rows with [row adapters](#row-adapters). See [Joined Queries Support](#joined-queries-support) for some sample code that uses this method. -- [AnyFetchRequest](http://groue.github.io/GRDB.swift/docs/5.1/Structs/AnyFetchRequest.html): a type-erased request. +- [AnyFetchRequest](http://groue.github.io/GRDB.swift/docs/5.2/Structs/AnyFetchRequest.html): a type-erased request. ## Joined Queries Support @@ -5509,84 +5511,6 @@ let disposable = observation.rx.observe(in: dbQueue).subscribe( Take care that there are use cases that ValueObservation is unfit for. For example, your application may need to process absolutely all changes, and avoid any coalescing. It may also need to process changes before any further modifications are performed in the database file. In those cases, you need to track *individual transactions*, not values. See [DatabaseRegionObservation], and the low-level [TransactionObserver Protocol](#transactionobserver-protocol). -#### Observing a Varying Database Region - -The `ValueObservation.tracking(_:)` creates an observation that tracks a *database region* which is infered from the function argument: - -```swift -// Tracks the full 'player' table -let observation = ValueObservation.tracking { db -> [Player] in - try Player.fetchAll(db) -} - -// Tracks the row with id 42 in the 'player' table -let observation = ValueObservation.tracking { db -> Player? in - try Player.fetchOne(db, key: 42) -} - -// Tracks the 'score' column in the 'player' table -let observation = ValueObservation.tracking { db -> Int? in - try Player.select(max(Column("score"))).fetchOne(db) -} - -// Tracks both the 'player' and 'team' tables -let observation = ValueObservation.tracking { db -> ([Team], [Player]) in - let teams = try Team.fetchAll(db) - let players = try Player.fetchAll(db) - return (teams, players) -} -``` - -The tracked region is made of tables, columns, and, when possible, rowids of individual rows. All changes that happen outside of this region do not impact the observation. - -Most observations track a constant region. But some need to observe a **varying region**, and those need to be created with the `ValueObservation.trackingVaryingRegion(_:)` method, or else they will miss changes. - -For example, consider those three observations that depend on some user preference: - -```swift -// Does not always track the same row in the player table. -let observation = ValueObservation.trackingVaryingRegion { db -> Player? in - let pref = try Preference.fetchOne(db) ?? .default - return try Player.fetchOne(db, key: pref.favoritePlayerId) -} - -// Only tracks the 'user' table if there are some blocked emails. -let observation = ValueObservation.trackingVaryingRegion { db -> [User] in - let pref = try Preference.fetchOne(db) ?? .default - let blockedEmails = pref.blockedEmails - return try User.filter(blockedEmails.contains(Column("email"))).fetchAll(db) -} - -// Sometimes tracks the 'food' table, and sometimes the 'beverage' table. -let observation = ValueObservation.trackingVaryingRegion { db -> Int in - let pref = try Preference.fetchOne(db) ?? .default - switch pref.selection { - case .food: return try Food.fetchCount(db) - case .beverage: return try Beverage.fetchCount(db) - } -} -``` - -As you see, observations of a varying region do not perform always the same requests, or perform several requests that depend on each other. - -When you are in doubt, add the [`print()` method](#valueobservationprint) to your observation before starting it, and look in your application logs for lines that start with `tracked region`. Make sure the printed database region covers the changes you expect to be tracked. - -
- Examples of tracked regions - -- `empty`: The empty region, which tracks nothing and never triggers the observation. -- `player(*)`: The full `player` table -- `player(id,name)`: The `id` and `name` columns of the `player` table -- `player(id,name)[1]`: The `id` and `name` columns of the row with id 1 in the `player` table -- `player(*),preference(*)`: Both the full `player` and `preference` tables - -
- -> :point_up: **Note**: observations of a varying region can not profit from a [database pool](#database-pools)'s ability to perform concurrent readonly requests. They must fetch their fresh values from the writer database connection, and thus postpone other application components that want to write. In other words, observations of varying regions increase write contention. -> -> Conversely, in a [database queue](#database-queues), observations of varying regions do not take any additional toll, compared to observations of a constant region. - - ### ValueObservation Scheduling By default, ValueObservation notifies the initial value, as well as eventual changes and errors, on the main thread, asynchronously: @@ -5819,7 +5743,7 @@ When needed, you can help GRDB optimize observations and reduce database content .share(replay: 1, scope: .whileConnected) ``` -3. :bulb: **Tip**: Use a [DatabasePool](#database-pools), because it can perform multi-threaded database accesses. +3. :bulb: **Tip**: Use a [database pool](#database-pools), because it can perform multi-threaded database accesses. 4. :bulb: **Tip**: When the observation processes some raw fetched values, use the [`map`](#valueobservationmap) operator: @@ -5838,27 +5762,78 @@ When needed, you can help GRDB optimize observations and reduce database content The `map` operator helps reducing database contention because it performs its job without blocking concurrent database reads. -5. :bulb: **Tip**: Consider rewriting [observations of a varying region](#observing-a-varying-database-region) into observations of a constant region, so that they can profit from DatabasePool concurrency: +4. :bulb: **Tip**: When the observation tracks a constant database region, create an optimized observation with the `ValueObservation.trackingConstantRegion(_:)` method. + + The optimization only kicks in when the observation is started from a [database pool](#database-pools): fresh values are fetched concurrently, and do not block database writes. + + The `ValueObservation.trackingConstantRegion(_:)` has a precondition: the observed requests must fetch from a single and constant database region. The tracked region is made of tables, columns, and, when possible, rowids of individual rows. All changes that happen outside of this region do not impact the observation. + + For example: ```swift - // An observation of a varying region - let observation = ValueObservation.trackingVaryingRegion { db -> Int in - switch try Preference.fetchOne(db)!.selection { - case .food: return try Food.fetchCount(db) - case .beverage: return try Beverage.fetchCount(db) - } + // Tracks the full 'player' table (only) + let observation = ValueObservation.trackingConstantRegion { db -> [Player] in + try Player.fetchAll(db) + } + + // Tracks the row with id 42 in the 'player' table (only) + let observation = ValueObservation.trackingConstantRegion { db -> Player? in + try Player.fetchOne(db, key: 42) + } + + // Tracks the 'score' column in the 'player' table (only) + let observation = ValueObservation.trackingConstantRegion { db -> Int? in + try Player.select(max(Column("score"))).fetchOne(db) + } + + // Tracks both the 'player' and 'team' tables (only) + let observation = ValueObservation.trackingConstantRegion { db -> ([Team], [Player]) in + let teams = try Team.fetchAll(db) + let players = try Player.fetchAll(db) + return (teams, players) + } + ``` + + When you want to observe a varying database region, make sure you use the plain `ValueObservation.tracking(_:)` method instead, or else some changes will not be notified. + + For example, consider those three observations below that depend on some user preference. They all track a varying region, and must use `ValueObservation.tracking(_:)`: + + ```swift + // Does not always track the same row in the player table. + let observation = ValueObservation.tracking { db -> Player? in + let pref = try Preference.fetchOne(db) ?? .default + return try Player.fetchOne(db, key: pref.favoritePlayerId) + } + + // Only tracks the 'user' table if there are some blocked emails. + let observation = ValueObservation.tracking { db -> [User] in + let pref = try Preference.fetchOne(db) ?? .default + let blockedEmails = pref.blockedEmails + return try User.filter(blockedEmails.contains(Column("email"))).fetchAll(db) } - // The same observation, rewritten so that it tracks a constant region + // Sometimes tracks the 'food' table, and sometimes the 'beverage' table. let observation = ValueObservation.tracking { db -> Int in - let foodCount = try Food.fetchCount(db) - let beverageCount = try Beverage.fetchCount(db) - switch try Preference.fetchOne(db)!.selection { - case .food: return foodCount - case .beverage: return beverageCount + let pref = try Preference.fetchOne(db) ?? .default + switch pref.selection { + case .food: return try Food.fetchCount(db) + case .beverage: return try Beverage.fetchCount(db) } } ``` + + When you are in doubt, add the [`print()` method](#valueobservationprint) to your observation before starting it, and look in your application logs for lines that start with `tracked region`. Make sure the printed database region covers the changes you expect to be tracked. + +
+ Examples of tracked regions + + - `empty`: The empty region, which tracks nothing and never triggers the observation. + - `player(*)`: The full `player` table + - `player(id,name)`: The `id` and `name` columns of the `player` table + - `player(id,name)[1]`: The `id` and `name` columns of the row with id 1 in the `player` table + - `player(*),preference(*)`: Both the full `player` and `preference` tables + +
## DatabaseRegionObservation @@ -6182,7 +6157,7 @@ After `stopObservingDatabaseChangesUntilNextTransaction()`, the `databaseDidChan ### DatabaseRegion -**[DatabaseRegion](https://groue.github.io/GRDB.swift/docs/5.1/Structs/DatabaseRegion.html) is a type that helps observing changes in the results of a database [request](#requests)**. +**[DatabaseRegion](https://groue.github.io/GRDB.swift/docs/5.2/Structs/DatabaseRegion.html) is a type that helps observing changes in the results of a database [request](#requests)**. A request knows which database modifications can impact its results. It can communicate this information to [transaction observers](#transactionobserver-protocol) by the way of a DatabaseRegion. @@ -6192,7 +6167,7 @@ DatabaseRegion fuels, for example, [ValueObservation and DatabaseRegionObservati For example, if you observe the region of `Player.select(max(Column("score")))`, then you'll get be notified of all changes performed on the `score` column of the `player` table (updates, insertions and deletions), even if they do not modify the value of the maximum score. However, you will not get any notification for changes performed on other database tables, or updates to other columns of the player table. -For more details, see the [reference](http://groue.github.io/GRDB.swift/docs/5.1/Structs/DatabaseRegion.html#/s:4GRDB14DatabaseRegionV10isModified2bySbAA0B5EventV_tF). +For more details, see the [reference](http://groue.github.io/GRDB.swift/docs/5.2/Structs/DatabaseRegion.html#/s:4GRDB14DatabaseRegionV10isModified2bySbAA0B5EventV_tF). #### The DatabaseRegionConvertible Protocol @@ -7296,7 +7271,7 @@ try snapshot2.read { db in ### DatabaseWriter and DatabaseReader Protocols -Both DatabaseQueue and DatabasePool adopt the [DatabaseReader](http://groue.github.io/GRDB.swift/docs/5.1/Protocols/DatabaseReader.html) and [DatabaseWriter](http://groue.github.io/GRDB.swift/docs/5.1/Protocols/DatabaseWriter.html) protocols. DatabaseSnapshot adopts DatabaseReader only. +Both DatabaseQueue and DatabasePool adopt the [DatabaseReader](http://groue.github.io/GRDB.swift/docs/5.2/Protocols/DatabaseReader.html) and [DatabaseWriter](http://groue.github.io/GRDB.swift/docs/5.2/Protocols/DatabaseWriter.html) protocols. DatabaseSnapshot adopts DatabaseReader only. These protocols provide a unified API that let you write generic code that targets all concurrency modes. They fuel, for example: @@ -7885,16 +7860,7 @@ For example: - `player(id,name)[1]`: The `id` and `name` columns of the row with id 1 in the `player` table - `player(*),team(*)`: Both the full `player` and `team` tables -If you happen to see a mismatch between the tracked region and your expectation, then your observation is likely an [observation of a varying region](#observing-a-varying-database-region). Change the definition of your observation by using `trackingVaryingRegion(_:)`: - -```swift -let observation = ValueObservation - .trackingVaryingRegion { db in ... } // <- - .print() -let cancellable = observation.start(...) -``` - -You should witness that the logs which start with `tracked region` now evolve in order to include the expected changes, and that you get the expected notifications. +If you happen to use the `ValueObservation.trackingConstantRegion(_:)` method and see a mismatch between the tracked region and your expectation, then change the definition of your observation by using `tracking(_:)`. You should witness that the logs which start with `tracked region` now evolve in order to include the expected changes, and that you get the expected notifications. If after all those steps (thanks you!), your observation is still failing you, please [open an issue](https://github.com/groue/GRDB.swift/issues/new) and provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)! @@ -7980,7 +7946,7 @@ When this is the case, there are two possible explanations: try db.execute(sql: "UPDATE player SET name = ?", arguments: [name]) ``` -For more information, see [Double-quoted String Literals Are Accepted](https://sqlite.org/quirks.html#dblquote), and [Configuration.acceptsDoubleQuotedStringLiterals](http://groue.github.io/GRDB.swift/docs/5.1/Structs/Configuration.html#/s:4GRDB13ConfigurationV33acceptsDoubleQuotedStringLiteralsSbvp). +For more information, see [Double-quoted String Literals Are Accepted](https://sqlite.org/quirks.html#dblquote), and [Configuration.acceptsDoubleQuotedStringLiterals](http://groue.github.io/GRDB.swift/docs/5.2/Structs/Configuration.html#/s:4GRDB13ConfigurationV33acceptsDoubleQuotedStringLiteralsSbvp). @@ -8140,4 +8106,4 @@ This chapter has been superseded by [ValueObservation] and [DatabaseRegionObserv [Sharing a Database]: Documentation/SharingADatabase.md [FAQ]: #faq [Database Observation]: #database-changes-observation -[SQLRequest]: http://groue.github.io/GRDB.swift/docs/5.1/Structs/SQLRequest.html +[SQLRequest]: http://groue.github.io/GRDB.swift/docs/5.2/Structs/SQLRequest.html diff --git a/Support/Info.plist b/Support/Info.plist index 6a36c07165..982fd557d3 100644 --- a/Support/Info.plist +++ b/Support/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.1.0 + 5.2.0 CFBundleSignature ???? CFBundleVersion diff --git a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift index 17ff11c7a5..755febc62f 100644 --- a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift @@ -33,7 +33,7 @@ class ValueObservationPublisherTests : XCTestCase { func test(writer: DatabaseWriter) throws { let publisher = ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer) let recorder = publisher.record() @@ -78,7 +78,7 @@ class ValueObservationPublisherTests : XCTestCase { let expectation = self.expectation(description: "") let semaphore = DispatchSemaphore(value: 0) let cancellable = ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer) .sink( receiveCompletion: { _ in }, @@ -105,7 +105,7 @@ class ValueObservationPublisherTests : XCTestCase { func test(writer: DatabaseWriter) throws { let publisher = ValueObservation - .tracking { try $0.execute(sql: "THIS IS NOT SQL") } + .trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") } .publisher(in: writer) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) @@ -137,7 +137,7 @@ class ValueObservationPublisherTests : XCTestCase { func test(writer: DatabaseWriter) throws { let publisher = ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer, scheduling: .immediate) let recorder = publisher.record() @@ -190,7 +190,7 @@ class ValueObservationPublisherTests : XCTestCase { }) let observationCancellable = ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer, scheduling: .immediate) .subscribe(testSubject) @@ -212,7 +212,7 @@ class ValueObservationPublisherTests : XCTestCase { func test(writer: DatabaseWriter) throws { let publisher = ValueObservation - .tracking { try $0.execute(sql: "THIS IS NOT SQL") } + .trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") } .publisher(in: writer, scheduling: .immediate) let recorder = publisher.record() let completion = try recorder.completion.get() @@ -283,7 +283,7 @@ class ValueObservationPublisherTests : XCTestCase { receiveValue: { _ in expectation.fulfill() }) ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer) .subscribe(subscriber) @@ -320,7 +320,7 @@ class ValueObservationPublisherTests : XCTestCase { }) ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer) .subscribe(subscriber) @@ -359,7 +359,7 @@ class ValueObservationPublisherTests : XCTestCase { receiveValue: { _ in expectation.fulfill() }) ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer, scheduling: .immediate /* make sure we get the initial db state */) .subscribe(subscriber) @@ -403,7 +403,7 @@ class ValueObservationPublisherTests : XCTestCase { }) ValueObservation - .tracking(Player.fetchCount) + .trackingConstantRegion(Player.fetchCount) .publisher(in: writer, scheduling: .immediate /* make sure we get two db states */) .subscribe(subscriber) diff --git a/Tests/GRDBTests/FoundationDateComponentsTests.swift b/Tests/GRDBTests/FoundationDateComponentsTests.swift index 07dc843077..1e8b73b44b 100644 --- a/Tests/GRDBTests/FoundationDateComponentsTests.swift +++ b/Tests/GRDBTests/FoundationDateComponentsTests.swift @@ -352,14 +352,20 @@ class FoundationDateComponentsTests : GRDBTestCase { func _assertParse(_ string: String, _ dateComponent: DatabaseDateComponents, file: StaticString, line: UInt) { do { // Test DatabaseValueConvertible adoption - let parsed = DatabaseDateComponents.fromDatabaseValue(string.databaseValue)! + guard let parsed = DatabaseDateComponents.fromDatabaseValue(string.databaseValue) else { + XCTFail("Could not parse \(String(reflecting: string))", file: file, line: line) + return + } XCTAssertEqual(parsed.format, dateComponent.format, file: file, line: line) XCTAssertEqual(parsed.dateComponents, dateComponent.dateComponents, file: file, line: line) } do { // Test StatementColumnConvertible adoption - let parsed = DatabaseQueue().inDatabase { - try! DatabaseDateComponents.fetchOne($0, sql: "SELECT ?", arguments: [string])! + guard let parsed = try? DatabaseQueue().inDatabase({ + try DatabaseDateComponents.fetchOne($0, sql: "SELECT ?", arguments: [string]) + }) else { + XCTFail("Could not parse \(String(reflecting: string))", file: file, line: line) + return } XCTAssertEqual(parsed.format, dateComponent.format, file: file, line: line) XCTAssertEqual(parsed.dateComponents, dateComponent.dateComponents, file: file, line: line) @@ -392,6 +398,41 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil), format: .YMD_HM)) + assertParse( + "2018-04-21 00:00Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil), + format: .YMD_HM)) + assertParse( + "2018-04-21 00:00+00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil), + format: .YMD_HM)) + assertParse( + "2018-04-21 00:00-00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil), + format: .YMD_HM)) + assertParse( + "2018-04-21 00:00+01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 4500), + year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil), + format: .YMD_HM)) + assertParse( + "2018-04-21 00:00-01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: -4500), + year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil), + format: .YMD_HM)) assertParse( "2018-04-21T23:59", DatabaseDateComponents( @@ -417,6 +458,13 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000), format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.9Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000), + format: .YMD_HMSS)) assertParse( "2018-04-21 00:00:00.00", DatabaseDateComponents( @@ -432,6 +480,13 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000), format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.99Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000), + format: .YMD_HMSS)) assertParse( "2018-04-21 00:00:00.000", DatabaseDateComponents( @@ -447,6 +502,53 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.999Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.999+00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.999-00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.999+01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 4500), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.999-01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: -4500), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.999123", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)) + assertParse( + "2018-04-21T23:59:59.999123Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)) assertParse( "00:00", DatabaseDateComponents( @@ -457,6 +559,41 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil), format: .HM)) + assertParse( + "23:59Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil), + format: .HM)) + assertParse( + "23:59+00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil), + format: .HM)) + assertParse( + "23:59-00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil), + format: .HM)) + assertParse( + "23:59+01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 4500), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil), + format: .HM)) + assertParse( + "23:59-01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: -4500), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil), + format: .HM)) assertParse( "00:00:00", DatabaseDateComponents( @@ -477,6 +614,13 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000), format: .HMSS)) + assertParse( + "23:59:59.9Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000), + format: .HMSS)) assertParse( "00:00:00.00", DatabaseDateComponents( @@ -492,6 +636,13 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000), format: .HMSS)) + assertParse( + "23:59:59.99Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000), + format: .HMSS)) assertParse( "00:00:00.000", DatabaseDateComponents( @@ -507,6 +658,53 @@ class FoundationDateComponentsTests : GRDBTestCase { DatabaseDateComponents( DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), format: .HMSS)) + assertParse( + "23:59:59.999Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)) + assertParse( + "23:59:59.999+00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)) + assertParse( + "23:59:59.999-00:00", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)) + assertParse( + "23:59:59.999+01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 4500), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)) + assertParse( + "23:59:59.999-01:15", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: -4500), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)) + assertParse( + "23:59:59.999123", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)) + assertParse( + "23:59:59.999123Z", + DatabaseDateComponents( + DateComponents( + timeZone: TimeZone(secondsFromGMT: 0), + year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)) } func testDatabaseDateComponentsFromUnparsableString() { diff --git a/Tests/GRDBTests/FoundationDateTests.swift b/Tests/GRDBTests/FoundationDateTests.swift index 9bd3d96163..63c7e2c809 100644 --- a/Tests/GRDBTests/FoundationDateTests.swift +++ b/Tests/GRDBTests/FoundationDateTests.swift @@ -139,6 +139,25 @@ class FoundationDateTests : GRDBTestCase { } } + func testDateAcceptsFormatYMD_HMZ() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.execute( + sql: "INSERT INTO dates (creationDate) VALUES (?)", + arguments: ["2015-07-22 01:02+01:15"]) + let date = try Date.fetchOne(db, sql: "SELECT creationDate from dates")! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + XCTAssertEqual(calendar.component(.year, from: date), 2015) + XCTAssertEqual(calendar.component(.month, from: date), 7) + XCTAssertEqual(calendar.component(.day, from: date), 21) + XCTAssertEqual(calendar.component(.hour, from: date), 23) + XCTAssertEqual(calendar.component(.minute, from: date), 47) + XCTAssertEqual(calendar.component(.second, from: date), 0) + XCTAssertEqual(calendar.component(.nanosecond, from: date), 0) + } + } + func testDateAcceptsFormatYMD_HMS() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in @@ -158,6 +177,25 @@ class FoundationDateTests : GRDBTestCase { } } + func testDateAcceptsFormatYMD_HMSZ() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.execute( + sql: "INSERT INTO dates (creationDate) VALUES (?)", + arguments: ["2015-07-22 01:02:03+01:15"]) + let date = try Date.fetchOne(db, sql: "SELECT creationDate from dates")! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + XCTAssertEqual(calendar.component(.year, from: date), 2015) + XCTAssertEqual(calendar.component(.month, from: date), 7) + XCTAssertEqual(calendar.component(.day, from: date), 21) + XCTAssertEqual(calendar.component(.hour, from: date), 23) + XCTAssertEqual(calendar.component(.minute, from: date), 47) + XCTAssertEqual(calendar.component(.second, from: date), 3) + XCTAssertEqual(calendar.component(.nanosecond, from: date), 0) + } + } + func testDateAcceptsFormatYMD_HMSS() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in @@ -177,6 +215,25 @@ class FoundationDateTests : GRDBTestCase { } } + func testDateAcceptsFormatYMD_HMSSZ() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.execute( + sql: "INSERT INTO dates (creationDate) VALUES (?)", + arguments: ["2015-07-22 01:02:03.00456+01:15"]) + let date = try Date.fetchOne(db, sql: "SELECT creationDate from dates")! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + XCTAssertEqual(calendar.component(.year, from: date), 2015) + XCTAssertEqual(calendar.component(.month, from: date), 7) + XCTAssertEqual(calendar.component(.day, from: date), 21) + XCTAssertEqual(calendar.component(.hour, from: date), 23) + XCTAssertEqual(calendar.component(.minute, from: date), 47) + XCTAssertEqual(calendar.component(.second, from: date), 3) + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_000_000) < 10) // We actually get 4_000_008. Some precision is lost during the DateComponents -> Date conversion. Not a big deal. + } + } + func testDateAcceptsTimestamp() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in diff --git a/Tests/GRDBTests/ValueObservationCountTests.swift b/Tests/GRDBTests/ValueObservationCountTests.swift index 8124b94fb0..45910416c7 100644 --- a/Tests/GRDBTests/ValueObservationCountTests.swift +++ b/Tests/GRDBTests/ValueObservationCountTests.swift @@ -6,7 +6,7 @@ class ValueObservationCountTests: GRDBTestCase { struct T: TableRecord { } try assertValueObservation( - ValueObservation.tracking(T.fetchCount), + ValueObservation.trackingConstantRegion(T.fetchCount), records: [0, 1, 1, 2, 3, 4], setup: { db in try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") @@ -29,7 +29,7 @@ class ValueObservationCountTests: GRDBTestCase { struct T: TableRecord { } try assertValueObservation( - ValueObservation.tracking(T.fetchCount).removeDuplicates(), + ValueObservation.trackingConstantRegion(T.fetchCount).removeDuplicates(), records: [0, 1, 2, 3, 4], setup: { db in try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") diff --git a/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift b/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift index d738036840..79e0a42364 100644 --- a/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift +++ b/Tests/GRDBTests/ValueObservationDatabaseValueConvertibleTests.swift @@ -21,7 +21,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT name FROM t ORDER BY id") try assertValueObservation( - ValueObservation.tracking(request.fetchAll), + ValueObservation.trackingConstantRegion(request.fetchAll), records: [ [], [Name(rawValue: "foo")], @@ -44,7 +44,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { }) try assertValueObservation( - ValueObservation.tracking(request.fetchAll).removeDuplicates(), + ValueObservation.trackingConstantRegion(request.fetchAll).removeDuplicates(), records: [ [], [Name(rawValue: "foo")], @@ -70,7 +70,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT name FROM t ORDER BY id DESC") try assertValueObservation( - ValueObservation.tracking(request.fetchOne), + ValueObservation.trackingConstantRegion(request.fetchOne), records: [ nil, Name(rawValue: "foo"), @@ -103,7 +103,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { }) try assertValueObservation( - ValueObservation.tracking(request.fetchOne).removeDuplicates(), + ValueObservation.trackingConstantRegion(request.fetchOne).removeDuplicates(), records: [ nil, Name(rawValue: "foo"), @@ -137,7 +137,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT name FROM t ORDER BY id") try assertValueObservation( - ValueObservation.tracking(request.fetchAll), + ValueObservation.trackingConstantRegion(request.fetchAll), records: [ [], [Name(rawValue: "foo")], @@ -160,7 +160,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { }) try assertValueObservation( - ValueObservation.tracking(request.fetchAll).removeDuplicates(), + ValueObservation.trackingConstantRegion(request.fetchAll).removeDuplicates(), records: [ [], [Name(rawValue: "foo")], @@ -186,7 +186,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT name FROM t ORDER BY id DESC") try assertValueObservation( - ValueObservation.tracking(request.fetchOne), + ValueObservation.trackingConstantRegion(request.fetchOne), records: [ nil, Name(rawValue: "foo"), @@ -219,7 +219,7 @@ class ValueObservationDatabaseValueConvertibleTests: GRDBTestCase { }) try assertValueObservation( - ValueObservation.tracking(request.fetchOne).removeDuplicates(), + ValueObservation.trackingConstantRegion(request.fetchOne).removeDuplicates(), records: [ nil, Name(rawValue: "foo"), diff --git a/Tests/GRDBTests/ValueObservationFetchTests.swift b/Tests/GRDBTests/ValueObservationFetchTests.swift index b4914065e0..9da456677c 100644 --- a/Tests/GRDBTests/ValueObservationFetchTests.swift +++ b/Tests/GRDBTests/ValueObservationFetchTests.swift @@ -4,7 +4,7 @@ import GRDB class ValueObservationFetchTests: GRDBTestCase { func testFetch() throws { try assertValueObservation( - ValueObservation.tracking { + ValueObservation.trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }, records: [0, 1, 1, 2], @@ -21,7 +21,7 @@ class ValueObservationFetchTests: GRDBTestCase { func testRemoveDuplicated() throws { try assertValueObservation( ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! } .removeDuplicates(), records: [0, 1, 2], setup: { db in diff --git a/Tests/GRDBTests/ValueObservationMapTests.swift b/Tests/GRDBTests/ValueObservationMapTests.swift index 57e1d4bf61..511cc0395b 100644 --- a/Tests/GRDBTests/ValueObservationMapTests.swift +++ b/Tests/GRDBTests/ValueObservationMapTests.swift @@ -4,7 +4,7 @@ import GRDB class ValueObservationMapTests: GRDBTestCase { func testMap() throws { let valueObservation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! } .map { "\($0)" } try assertValueObservation( @@ -20,7 +20,7 @@ class ValueObservationMapTests: GRDBTestCase { } func testMapPreservesConfiguration() { - var observation = ValueObservation.tracking { _ in } + var observation = ValueObservation.trackingConstantRegion { _ in } observation.requiresWriteAccess = true let mappedObservation = observation.map { _ in } diff --git a/Tests/GRDBTests/ValueObservationPrintTests.swift b/Tests/GRDBTests/ValueObservationPrintTests.swift index 0c9c314c6f..22c4dc9181 100644 --- a/Tests/GRDBTests/ValueObservationPrintTests.swift +++ b/Tests/GRDBTests/ValueObservationPrintTests.swift @@ -31,7 +31,7 @@ class ValueObservationPrintTests: GRDBTestCase { func test(_ dbReader: DatabaseReader) throws { let logger = TestStream() let observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) let expectation = self.expectation(description: "") @@ -65,7 +65,7 @@ class ValueObservationPrintTests: GRDBTestCase { func test(_ dbReader: DatabaseReader) throws { let logger = TestStream() let observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) let expectation = self.expectation(description: "") @@ -97,7 +97,7 @@ class ValueObservationPrintTests: GRDBTestCase { struct TestError: Error { } let logger = TestStream() let observation = ValueObservation - .tracking { _ in throw TestError() } + .trackingConstantRegion { _ in throw TestError() } .print(to: logger) let expectation = self.expectation(description: "") @@ -129,7 +129,7 @@ class ValueObservationPrintTests: GRDBTestCase { struct TestError: Error { } let logger = TestStream() let observation = ValueObservation - .tracking { _ in throw TestError() } + .trackingConstantRegion { _ in throw TestError() } .print(to: logger) let expectation = self.expectation(description: "") @@ -164,7 +164,7 @@ class ValueObservationPrintTests: GRDBTestCase { let logger = TestStream() var observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) observation.requiresWriteAccess = true @@ -206,7 +206,7 @@ class ValueObservationPrintTests: GRDBTestCase { let logger = TestStream() var observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) observation.requiresWriteAccess = true @@ -244,7 +244,7 @@ class ValueObservationPrintTests: GRDBTestCase { func test(_ dbWriter: DatabaseWriter) throws { let logger = TestStream() var observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) observation.requiresWriteAccess = true @@ -271,7 +271,7 @@ class ValueObservationPrintTests: GRDBTestCase { func test(_ dbWriter: DatabaseWriter) throws { let logger = TestStream() var observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) observation.requiresWriteAccess = true @@ -302,7 +302,7 @@ class ValueObservationPrintTests: GRDBTestCase { let logger = TestStream() var observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) observation.requiresWriteAccess = true @@ -345,7 +345,7 @@ class ValueObservationPrintTests: GRDBTestCase { let logger = TestStream() var observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger) observation.requiresWriteAccess = true @@ -394,7 +394,7 @@ class ValueObservationPrintTests: GRDBTestCase { // transaction observer, some write did happen. var needsChange = true let observation = ValueObservation - .tracking({ db -> Int? in + .trackingConstantRegion({ db -> Int? in if needsChange { needsChange = false try dbPool.write { db in @@ -441,7 +441,7 @@ class ValueObservationPrintTests: GRDBTestCase { // transaction observer, some write did happen. var needsChange = true let observation = ValueObservation - .tracking({ db -> Int? in + .trackingConstantRegion({ db -> Int? in if needsChange { needsChange = false try dbPool.write { db in @@ -491,7 +491,7 @@ class ValueObservationPrintTests: GRDBTestCase { let logger = TestStream() let observation = ValueObservation - .trackingVaryingRegion({ db -> Int? in + .tracking({ db -> Int? in let table = try String.fetchOne(db, sql: "SELECT t FROM choice")! return try Int.fetchOne(db, sql: "SELECT MAX(id) FROM \(table)") }) @@ -543,7 +543,7 @@ class ValueObservationPrintTests: GRDBTestCase { let logger1 = TestStream() let logger2 = TestStream() let observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print("", to: logger1) .print("log", to: logger2) @@ -575,7 +575,7 @@ class ValueObservationPrintTests: GRDBTestCase { let logger1 = TestStream() let logger2 = TestStream() let observation = ValueObservation - .tracking { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } + .trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } .print(to: logger1) .removeDuplicates() .map { _ in "foo" } @@ -668,7 +668,7 @@ class ValueObservationPrintTests: GRDBTestCase { } } - let observation = ValueObservation.tracking { + let observation = ValueObservation.trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT MAX(id) FROM player") } diff --git a/Tests/GRDBTests/ValueObservationQueryInterfaceRequestTests.swift b/Tests/GRDBTests/ValueObservationQueryInterfaceRequestTests.swift index 8ef808c272..18eb61e27d 100644 --- a/Tests/GRDBTests/ValueObservationQueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/ValueObservationQueryInterfaceRequestTests.swift @@ -76,7 +76,7 @@ class ValueObservationQueryInterfaceRequestTests: GRDBTestCase { .including(all: Parent.children.orderByPrimaryKey()) .orderByPrimaryKey() .asRequest(of: Row.self) - let observation = ValueObservation.tracking(request.fetchOne) + let observation = ValueObservation.trackingConstantRegion(request.fetchOne) let recorder = observation.record(in: dbQueue) try dbQueue.writeWithoutTransaction(performDatabaseModifications) @@ -113,7 +113,7 @@ class ValueObservationQueryInterfaceRequestTests: GRDBTestCase { .including(all: Parent.children.orderByPrimaryKey()) .orderByPrimaryKey() .asRequest(of: Row.self) - let observation = ValueObservation.tracking(request.fetchAll) + let observation = ValueObservation.trackingConstantRegion(request.fetchAll) let recorder = observation.record(in: dbQueue) try dbQueue.writeWithoutTransaction(performDatabaseModifications) @@ -171,7 +171,7 @@ class ValueObservationQueryInterfaceRequestTests: GRDBTestCase { .asRequest(of: ParentInfo.self) try assertValueObservation( - ValueObservation.tracking(request.fetchOne), + ValueObservation.trackingConstantRegion(request.fetchOne), records: [ nil, ParentInfo( @@ -205,7 +205,7 @@ class ValueObservationQueryInterfaceRequestTests: GRDBTestCase { // The fundamental technique for removing duplicates of non-Equatable types try assertValueObservation( ValueObservation - .tracking { db in try Row.fetchOne(db, request) } + .trackingConstantRegion { db in try Row.fetchOne(db, request) } .removeDuplicates() .map { row in row.map(ParentInfo.init(row:)) }, records: [ @@ -240,7 +240,7 @@ class ValueObservationQueryInterfaceRequestTests: GRDBTestCase { .asRequest(of: ParentInfo.self) try assertValueObservation( - ValueObservation.tracking(request.fetchAll), + ValueObservation.trackingConstantRegion(request.fetchAll), records: [ [], [ @@ -310,7 +310,7 @@ class ValueObservationQueryInterfaceRequestTests: GRDBTestCase { // The fundamental technique for removing duplicates of non-Equatable types try assertValueObservation( ValueObservation - .tracking { db in try Row.fetchAll(db, request) } + .trackingConstantRegion { db in try Row.fetchAll(db, request) } .removeDuplicates() .map { rows in rows.map(ParentInfo.init(row:)) }, records: [ diff --git a/Tests/GRDBTests/ValueObservationReadonlyTests.swift b/Tests/GRDBTests/ValueObservationReadonlyTests.swift index 0697a1be2c..3cff842265 100644 --- a/Tests/GRDBTests/ValueObservationReadonlyTests.swift +++ b/Tests/GRDBTests/ValueObservationReadonlyTests.swift @@ -5,7 +5,7 @@ class ValueObservationReadonlyTests: GRDBTestCase { func testReadOnlyObservation() throws { try assertValueObservation( - ValueObservation.tracking { + ValueObservation.trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }, records: [0, 1], @@ -19,7 +19,7 @@ class ValueObservationReadonlyTests: GRDBTestCase { func testWriteObservationFailsByDefaultWithErrorHandling() throws { try assertValueObservation( - ValueObservation.tracking { db -> Int in + ValueObservation.trackingConstantRegion { db -> Int in try db.execute(sql: "INSERT INTO t DEFAULT VALUES") return 0 }, @@ -35,7 +35,7 @@ class ValueObservationReadonlyTests: GRDBTestCase { } func testWriteObservation() throws { - var observation = ValueObservation.tracking { db -> Int in + var observation = ValueObservation.trackingConstantRegion { db -> Int in XCTAssert(db.isInsideTransaction, "expected a wrapping transaction") try db.execute(sql: "CREATE TEMPORARY TABLE temp AS SELECT * FROM t") let result = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM temp")! @@ -57,7 +57,7 @@ class ValueObservationReadonlyTests: GRDBTestCase { func testWriteObservationIsWrappedInSavepointWithErrorHandling() throws { struct TestError: Error { } - var observation = ValueObservation.tracking { db in + var observation = ValueObservation.trackingConstantRegion { db in try db.execute(sql: "INSERT INTO t DEFAULT VALUES") throw TestError() } diff --git a/Tests/GRDBTests/ValueObservationRecordTests.swift b/Tests/GRDBTests/ValueObservationRecordTests.swift index 0568df3aa4..9f52d61d62 100644 --- a/Tests/GRDBTests/ValueObservationRecordTests.swift +++ b/Tests/GRDBTests/ValueObservationRecordTests.swift @@ -18,7 +18,7 @@ class ValueObservationRecordTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT * FROM t ORDER BY id") try assertValueObservation( - ValueObservation.tracking(request.fetchAll), + ValueObservation.trackingConstantRegion(request.fetchAll), records: [ [], [Player(id: 1, name: "foo")], @@ -43,7 +43,7 @@ class ValueObservationRecordTests: GRDBTestCase { // The fundamental technique for removing duplicates of non-Equatable types try assertValueObservation( ValueObservation - .tracking { try Row.fetchAll($0, request) } + .trackingConstantRegion { try Row.fetchAll($0, request) } .removeDuplicates() .map { $0.map(Player.init(row:)) }, records: [ @@ -71,7 +71,7 @@ class ValueObservationRecordTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT * FROM t ORDER BY id DESC") try assertValueObservation( - ValueObservation.tracking(request.fetchOne), + ValueObservation.trackingConstantRegion(request.fetchOne), records: [ nil, Player(id: 1, name: "foo"), @@ -98,7 +98,7 @@ class ValueObservationRecordTests: GRDBTestCase { // The fundamental technique for removing duplicates of non-Equatable types try assertValueObservation( ValueObservation - .tracking { try Row.fetchOne($0, request) } + .trackingConstantRegion { try Row.fetchOne($0, request) } .removeDuplicates() .map { $0.map(Player.init(row:)) }, records: [ diff --git a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift index 9e0d34f07c..e1ce89817b 100644 --- a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift +++ b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift @@ -110,7 +110,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { // I'm completely paranoid about tuple destructuring - I can't wrap my // head about the rules that allow or disallow it. let dbQueue = try makeDatabaseQueue() - let observation = ValueObservation.tracking { db -> (Int, String) in + let observation = ValueObservation.trackingConstantRegion { db -> (Int, String) in (0, "") } _ = observation.start( @@ -137,7 +137,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { var regions: [DatabaseRegion] = [] let observation = ValueObservation - .trackingVaryingRegion({ db -> Int in + .tracking({ db -> Int in let table = try String.fetchOne(db, sql: "SELECT name FROM source")! return try Int.fetchOne(db, sql: "SELECT IFNULL(SUM(value), 0) FROM \(table)")! }) @@ -188,7 +188,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { var regions: [DatabaseRegion] = [] let observation = ValueObservation - .trackingVaryingRegion({ db -> Int in + .tracking({ db -> Int in let table = try String.fetchOne(db, sql: "SELECT name FROM source")! return try Int.fetchOne(db, sql: "SELECT IFNULL(SUM(value), 0) FROM \(table)")! }) diff --git a/Tests/GRDBTests/ValueObservationRowTests.swift b/Tests/GRDBTests/ValueObservationRowTests.swift index 8b377eb95a..5b01e8e63a 100644 --- a/Tests/GRDBTests/ValueObservationRowTests.swift +++ b/Tests/GRDBTests/ValueObservationRowTests.swift @@ -6,7 +6,7 @@ class ValueObservationRowTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT * FROM t ORDER BY id") try assertValueObservation( - ValueObservation.tracking(request.fetchAll), + ValueObservation.trackingConstantRegion(request.fetchAll), records: [ [], [["id":1, "name":"foo"]], @@ -29,7 +29,7 @@ class ValueObservationRowTests: GRDBTestCase { }) try assertValueObservation( - ValueObservation.tracking(request.fetchAll).removeDuplicates(), + ValueObservation.trackingConstantRegion(request.fetchAll).removeDuplicates(), records: [ [], [["id":1, "name":"foo"]], @@ -55,7 +55,7 @@ class ValueObservationRowTests: GRDBTestCase { let request = SQLRequest(sql: "SELECT * FROM t ORDER BY id DESC") try assertValueObservation( - ValueObservation.tracking(request.fetchOne), + ValueObservation.trackingConstantRegion(request.fetchOne), records: [ nil, ["id":1, "name":"foo"], @@ -80,7 +80,7 @@ class ValueObservationRowTests: GRDBTestCase { }) try assertValueObservation( - ValueObservation.tracking(request.fetchOne).removeDuplicates(), + ValueObservation.trackingConstantRegion(request.fetchOne).removeDuplicates(), records: [ nil, ["id":1, "name":"foo"], @@ -106,7 +106,7 @@ class ValueObservationRowTests: GRDBTestCase { func testFTS4Observation() throws { try assertValueObservation( - ValueObservation.tracking(SQLRequest(sql: "SELECT * FROM ft_documents").fetchAll), + ValueObservation.trackingConstantRegion(SQLRequest(sql: "SELECT * FROM ft_documents").fetchAll), records: [ [], [["content":"foo"]]], @@ -120,7 +120,7 @@ class ValueObservationRowTests: GRDBTestCase { func testSynchronizedFTS4Observation() throws { try assertValueObservation( - ValueObservation.tracking(SQLRequest(sql: "SELECT * FROM ft_documents").fetchAll), + ValueObservation.trackingConstantRegion(SQLRequest(sql: "SELECT * FROM ft_documents").fetchAll), records: [ [], [["content":"foo"]]], @@ -141,7 +141,7 @@ class ValueObservationRowTests: GRDBTestCase { func testJoinedFTS4Observation() throws { try assertValueObservation( - ValueObservation.tracking(SQLRequest(sql: """ + ValueObservation.trackingConstantRegion(SQLRequest(sql: """ SELECT document.* FROM document JOIN ft_document ON ft_document.rowid = document.id WHERE ft_document MATCH 'foo' diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 8c2a1957c0..b3a60cbf66 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -7,7 +7,7 @@ class ValueObservationTests: GRDBTestCase { func test(_ dbWriter: DatabaseWriter) throws { // Create an observation struct TestError: Error { } - let observation = ValueObservation.tracking { _ in throw TestError() } + let observation = ValueObservation.trackingConstantRegion { _ in throw TestError() } // Start observation var error: TestError? @@ -36,7 +36,7 @@ class ValueObservationTests: GRDBTestCase { struct TestError: Error { } var nextError: Error? = nil // If not null, observation throws an error - let observation = ValueObservation.tracking { + let observation = ValueObservation.trackingConstantRegion { _ = try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t") if let error = nextError { throw error @@ -93,7 +93,7 @@ class ValueObservationTests: GRDBTestCase { var region: DatabaseRegion? let expectation = self.expectation(description: "") let observation = ValueObservation - .tracking(request.fetchAll) + .trackingConstantRegion(request.fetchAll) .handleEvents(willTrackRegion: { region = $0 expectation.fulfill() @@ -120,7 +120,7 @@ class ValueObservationTests: GRDBTestCase { // its first read access, and its write access that installs the // transaction observer, some write did happen. var needsChange = true - let observation = ValueObservation.tracking { db -> Int in + let observation = ValueObservation.trackingConstantRegion { db -> Int in if needsChange { needsChange = false try dbPool.write { db in @@ -160,7 +160,7 @@ class ValueObservationTests: GRDBTestCase { // its first read access, and its write access that installs the // transaction observer, some write did happen. var needsChange = true - let observation = ValueObservation.tracking { db -> Int in + let observation = ValueObservation.trackingConstantRegion { db -> Int in if needsChange { needsChange = false try dbPool.write { db in @@ -200,7 +200,7 @@ class ValueObservationTests: GRDBTestCase { // its first read access, and its write access that installs the // transaction observer, no write did happen. var needsChange = true - let observation = ValueObservation.tracking { db -> Int in + let observation = ValueObservation.trackingConstantRegion { db -> Int in if needsChange { needsChange = false DispatchQueue.main.asyncAfter(deadline: .now() + 1) { @@ -259,7 +259,7 @@ class ValueObservationTests: GRDBTestCase { // its first read access, and its write access that installs the // transaction observer, no write did happen. var needsChange = true - let observation = ValueObservation.tracking { db -> Int in + let observation = ValueObservation.trackingConstantRegion { db -> Int in if needsChange { needsChange = false DispatchQueue.main.asyncAfter(deadline: .now() + 1) { @@ -322,7 +322,7 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 2 // Create an observation - let observation = ValueObservation.tracking { + let observation = ValueObservation.trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT * FROM t") } @@ -368,7 +368,7 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 2 // Create an observation - let observation = ValueObservation.tracking { + let observation = ValueObservation.trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT * FROM t") } diff --git a/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift b/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift index 2c605702d5..e3454b23bc 100644 --- a/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift +++ b/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift @@ -7,6 +7,8 @@ let insertedRowCount = 20_000 @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { + try! parseDateComponents() + try! parseDates() try! fetchValues() try! fetchPositionalValues() try! fetchNamedValues() @@ -22,6 +24,46 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - + func parseDateComponents() throws { + /// Selects many dates + let request = """ + WITH RECURSIVE + cnt(x) AS ( + SELECT 1 + UNION ALL + SELECT x+1 FROM cnt + LIMIT 50000 + ) + SELECT '2018-04-20 14:47:12.345' FROM cnt; + """ + + try DatabaseQueue().inDatabase { db in + let cursor = try DatabaseDateComponents.fetchCursor(db, sql: request) + while try cursor.next() != nil { } + } + } + + func parseDates() throws { + /// Selects many dates + let request = """ + WITH RECURSIVE + cnt(x) AS ( + SELECT 1 + UNION ALL + SELECT x+1 FROM cnt + LIMIT 50000 + ) + SELECT '2018-04-20 14:47:12.345' FROM cnt; + """ + + try DatabaseQueue().inDatabase { db in + let cursor = try Date.fetchCursor(db, sql: request) + while try cursor.next() != nil { } + } + } + + // MARK: - + func fetchValues() throws { let databasePath = Bundle(for: type(of: self)).path(forResource: "ProfilingDatabase", ofType: "sqlite")! let dbQueue = try DatabaseQueue(path: databasePath)