From 362c4baeca9b0a75b82cb0146753674c6eba01ec Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Sun, 15 Apr 2018 19:47:16 +0700 Subject: [PATCH 01/34] initial scanf() based date string parser --- .../Foundation/DatabaseDateComponents.swift | 8 +- .../Support/Foundation/SQLiteDateParser.swift | 90 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 GRDB/Core/Support/Foundation/SQLiteDateParser.swift diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 93d7978d40..4ad2848764 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -2,6 +2,8 @@ import Foundation /// DatabaseDateComponents reads and stores DateComponents in the database. public struct DatabaseDateComponents : DatabaseValueConvertible { + + public static var useScanfStrategy = true /// The available formats for reading and storing date components. public enum Format : String { @@ -130,7 +132,11 @@ public struct DatabaseDateComponents : DatabaseValueConvertible { guard let string = String.fromDatabaseValue(dbValue) else { return nil } - + + if useScanfStrategy { + return SQLiteDateParser.components(from: string) + } + var dateComponents = DateComponents() let scanner = Scanner(string: string) scanner.charactersToBeSkipped = CharacterSet() diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift new file mode 100644 index 0000000000..4e7d2c8953 --- /dev/null +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -0,0 +1,90 @@ +import Foundation + +// inspired by: http://jordansmith.io/performant-date-parsing/ + +class SQLiteDateParser { + + private static var mutex: pthread_mutex_t = { + var mutex = pthread_mutex_t() + pthread_mutex_init(&mutex, nil) + return mutex + }() + + private static let year = UnsafeMutablePointer.allocate(capacity: 1) + private static let month = UnsafeMutablePointer.allocate(capacity: 1) + private static let day = UnsafeMutablePointer.allocate(capacity: 1) + private static let hour = UnsafeMutablePointer.allocate(capacity: 1) + private static let minute = UnsafeMutablePointer.allocate(capacity: 1) + private static let second = UnsafeMutablePointer.allocate(capacity: 1) + + public static func components(from dateString: String) -> DatabaseDateComponents? { + switch dateString.count { + case 23, 19, 16, 10: + return datetimeComponents(from: dateString) + case 12, 8, 5: + return timeComponents(from: dateString) + default: + return nil + } + } + + // - YYYY-MM-DD + // - YYYY-MM-DD HH:MM + // - YYYY-MM-DD HH:MM:SS + // - YYYY-MM-DD HH:MM:SS.SSS + // - YYYY-MM-DDTHH:MM + // - YYYY-MM-DDTHH:MM:SS + // - YYYY-MM-DDTHH:MM:SS.SSS + private static func datetimeComponents(from dateString: String) -> DatabaseDateComponents? { + pthread_mutex_lock(&mutex) + + let parseCount = withVaList([year, month, day, hour, minute, second]) { pointer in + vsscanf(dateString, "%d-%d-%d%*c%d:%d:%f", pointer) + } + + var components = DateComponents() + + components.year = Int(year.pointee) + components.month = Int(month.pointee) + components.day = Int(day.pointee) + + if parseCount >= 5 { + components.hour = Int(hour.pointee) + components.minute = Int(minute.pointee) + } + + if parseCount >= 6 { + components.second = Int(second.pointee) + components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) + } + + pthread_mutex_unlock(&mutex) + + return DatabaseDateComponents(components, format: .YMD_HMSS) + } + + // - HH:MM + // - HH:MM:SS + // - HH:MM:SS.SSS + private static func timeComponents(from timeString: String) -> DatabaseDateComponents? { + pthread_mutex_lock(&mutex) + + let parseCount = withVaList([hour, minute, second]) { pointer in + vsscanf(timeString, "%d:%d:%f", pointer) + } + + var components = DateComponents() + + components.hour = Int(hour.pointee) + components.minute = Int(minute.pointee) + + if parseCount >= 3 { + components.second = Int(second.pointee) + components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) + } + + pthread_mutex_unlock(&mutex) + + return DatabaseDateComponents(components, format: .HMSS) + } +} From f5f8ab59717f33f81dae82bbeae24ec44f4593f7 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Mon, 16 Apr 2018 16:32:32 +0700 Subject: [PATCH 02/34] add SQLiteDateParser to project file --- GRDB.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 093beb6246..fe8efafdc1 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -625,6 +625,9 @@ 56FF455A1D2CDA5200F21EF9 /* RecordUniqueIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FF45551D2CDA5200F21EF9 /* RecordUniqueIndexTests.swift */; }; 6340BF801E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340BF7F1E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift */; }; 6340BF841E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340BF7F1E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift */; }; + C96C0F2B2084A442006B2981 /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96C0F242084A442006B2981 /* SQLiteDateParser.swift */; }; + C96C0F2C2084A459006B2981 /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96C0F242084A442006B2981 /* SQLiteDateParser.swift */; }; + C96C0F2D2084A45A006B2981 /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96C0F242084A442006B2981 /* SQLiteDateParser.swift */; }; DC2393C81ABE35F8003FF113 /* GRDB-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2393C61ABE35F8003FF113 /* GRDB-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC3773F919C8CBB3004FCF85 /* GRDB.h in Headers */ = {isa = PBXBuildFile; fileRef = DC3773F819C8CBB3004FCF85 /* GRDB.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ @@ -1028,6 +1031,7 @@ 56FF453F1D2C23BA00F21EF9 /* MutablePersistableDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutablePersistableDeleteTests.swift; sourceTree = ""; }; 56FF45551D2CDA5200F21EF9 /* RecordUniqueIndexTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordUniqueIndexTests.swift; sourceTree = ""; }; 6340BF7F1E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordPersistenceConflictPolicy.swift; sourceTree = ""; }; + C96C0F242084A442006B2981 /* SQLiteDateParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteDateParser.swift; sourceTree = ""; }; DC2393C61ABE35F8003FF113 /* GRDB-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GRDB-Bridging.h"; sourceTree = ""; }; DC3773F319C8CBB3004FCF85 /* GRDB.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GRDB.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC3773F719C8CBB3004FCF85 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1135,6 +1139,7 @@ 5605F1521C672E4000235C62 /* NSString.swift */, 5657AB0E1D10899D006283EF /* URL.swift */, 56A8C22F1D1914540096E9D4 /* UUID.swift */, + C96C0F242084A442006B2981 /* SQLiteDateParser.swift */, ); path = Foundation; sourceTree = ""; @@ -2195,6 +2200,7 @@ 565490D91D5AE252005622CB /* Migration.swift in Sources */, 56CEB5001EAA2F4D00BFAF62 /* FTS3.swift in Sources */, 5698AD271DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, + C96C0F2D2084A45A006B2981 /* SQLiteDateParser.swift in Sources */, 566A841C2041146100E50BFD /* DatabaseSnapshot.swift in Sources */, 565490D51D5AE252005622CB /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, 565490C51D5AE236005622CB /* SerializedDatabase.swift in Sources */, @@ -2360,6 +2366,7 @@ 56D1215D1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */, 566AD8B51D5318F4002EC1A8 /* TableDefinition.swift in Sources */, 5698AD241DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, + C96C0F2C2084A459006B2981 /* SQLiteDateParser.swift in Sources */, 5605F1681C672E4000235C62 /* NSNumber.swift in Sources */, 56CEB5041EAA2F4D00BFAF62 /* FTS4.swift in Sources */, 5605F1741C672E4000235C62 /* StandardLibrary.swift in Sources */, @@ -2762,6 +2769,7 @@ 5698AD211DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 56A238831B9C75030082EB20 /* DatabaseQueue.swift in Sources */, 5605F1671C672E4000235C62 /* NSNumber.swift in Sources */, + C96C0F2B2084A442006B2981 /* SQLiteDateParser.swift in Sources */, 56A238871B9C75030082EB20 /* Row.swift in Sources */, 5605F1731C672E4000235C62 /* StandardLibrary.swift in Sources */, 567517C820131B220094BF41 /* RecordBox.swift in Sources */, From 59d7909f5018ee98a7aac83a125c66a955705071 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Mon, 16 Apr 2018 17:21:00 +0700 Subject: [PATCH 03/34] return correct Format for partial date/time combinations --- .../Support/Foundation/SQLiteDateParser.swift | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 4e7d2c8953..a1de51307b 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -37,28 +37,29 @@ class SQLiteDateParser { // - YYYY-MM-DDTHH:MM:SS.SSS private static func datetimeComponents(from dateString: String) -> DatabaseDateComponents? { pthread_mutex_lock(&mutex) + defer { pthread_mutex_unlock(&mutex) } let parseCount = withVaList([year, month, day, hour, minute, second]) { pointer in vsscanf(dateString, "%d-%d-%d%*c%d:%d:%f", pointer) } + guard parseCount >= 3 else { return nil } + var components = DateComponents() components.year = Int(year.pointee) components.month = Int(month.pointee) components.day = Int(day.pointee) - if parseCount >= 5 { - components.hour = Int(hour.pointee) - components.minute = Int(minute.pointee) - } + guard parseCount >= 5 else { return DatabaseDateComponents(components, format: .YMD) } - if parseCount >= 6 { - components.second = Int(second.pointee) - components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) - } + components.hour = Int(hour.pointee) + components.minute = Int(minute.pointee) - pthread_mutex_unlock(&mutex) + guard parseCount >= 6 else { return DatabaseDateComponents(components, format: .YMD_HM) } + + components.second = Int(second.pointee) + components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) return DatabaseDateComponents(components, format: .YMD_HMSS) } @@ -68,23 +69,24 @@ class SQLiteDateParser { // - HH:MM:SS.SSS private static func timeComponents(from timeString: String) -> DatabaseDateComponents? { pthread_mutex_lock(&mutex) + defer { pthread_mutex_unlock(&mutex) } let parseCount = withVaList([hour, minute, second]) { pointer in vsscanf(timeString, "%d:%d:%f", pointer) } + guard parseCount >= 2 else { return nil } + var components = DateComponents() components.hour = Int(hour.pointee) components.minute = Int(minute.pointee) - if parseCount >= 3 { - components.second = Int(second.pointee) - components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) - } - - pthread_mutex_unlock(&mutex) - + guard parseCount >= 3 else { return DatabaseDateComponents(components, format: .HM) } + + components.second = Int(second.pointee) + components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) + return DatabaseDateComponents(components, format: .HMSS) } } From b444d376f4e5648d90ec35da482aec2b196408f0 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Mon, 16 Apr 2018 17:39:41 +0700 Subject: [PATCH 04/34] differentiate between SS and SS.SSS formats --- .../Support/Foundation/SQLiteDateParser.swift | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index a1de51307b..eae297b3e3 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -15,7 +15,8 @@ class SQLiteDateParser { private static let day = UnsafeMutablePointer.allocate(capacity: 1) private static let hour = UnsafeMutablePointer.allocate(capacity: 1) private static let minute = UnsafeMutablePointer.allocate(capacity: 1) - private static let second = UnsafeMutablePointer.allocate(capacity: 1) + private static let second = UnsafeMutablePointer.allocate(capacity: 1) + private static let nanosecond = UnsafeMutablePointer.allocate(capacity: 1) public static func components(from dateString: String) -> DatabaseDateComponents? { switch dateString.count { @@ -39,14 +40,13 @@ class SQLiteDateParser { pthread_mutex_lock(&mutex) defer { pthread_mutex_unlock(&mutex) } - let parseCount = withVaList([year, month, day, hour, minute, second]) { pointer in - vsscanf(dateString, "%d-%d-%d%*c%d:%d:%f", pointer) + let parseCount = withVaList([year, month, day, hour, minute, second, nanosecond]) { pointer in + vsscanf(dateString, "%d-%d-%d%*c%d:%d:%d.%d", pointer) } guard parseCount >= 3 else { return nil } var components = DateComponents() - components.year = Int(year.pointee) components.month = Int(month.pointee) components.day = Int(day.pointee) @@ -59,7 +59,10 @@ class SQLiteDateParser { guard parseCount >= 6 else { return DatabaseDateComponents(components, format: .YMD_HM) } components.second = Int(second.pointee) - components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) + + guard parseCount >= 7 else { return DatabaseDateComponents(components, format: .YMD_HMS) } + + components.nanosecond = Int(nanosecond.pointee) return DatabaseDateComponents(components, format: .YMD_HMSS) } @@ -71,22 +74,24 @@ class SQLiteDateParser { pthread_mutex_lock(&mutex) defer { pthread_mutex_unlock(&mutex) } - let parseCount = withVaList([hour, minute, second]) { pointer in - vsscanf(timeString, "%d:%d:%f", pointer) + let parseCount = withVaList([hour, minute, second, nanosecond]) { pointer in + vsscanf(timeString, "%d:%d:%d.%d", pointer) } guard parseCount >= 2 else { return nil } var components = DateComponents() - components.hour = Int(hour.pointee) components.minute = Int(minute.pointee) guard parseCount >= 3 else { return DatabaseDateComponents(components, format: .HM) } - + components.second = Int(second.pointee) - components.nanosecond = Int((second.pointee - Float(components.second!)) * 1_000_000_000) - + + guard parseCount >= 4 else { return DatabaseDateComponents(components, format: .HMS) } + + components.nanosecond = Int(nanosecond.pointee) + return DatabaseDateComponents(components, format: .HMSS) } } From 7a1aa88edb1ad82408cdc281b9eff550b6bbabb1 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Mon, 16 Apr 2018 19:18:49 +0700 Subject: [PATCH 05/34] fractions of seconds are not the same thing as integer counts of nanoseconds --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index eae297b3e3..75b164fcd2 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -62,7 +62,7 @@ class SQLiteDateParser { guard parseCount >= 7 else { return DatabaseDateComponents(components, format: .YMD_HMS) } - components.nanosecond = Int(nanosecond.pointee) + components.nanosecond = nanosecondsInt(for: nanosecond.pointee) return DatabaseDateComponents(components, format: .YMD_HMSS) } @@ -90,8 +90,17 @@ class SQLiteDateParser { guard parseCount >= 4 else { return DatabaseDateComponents(components, format: .HMS) } - components.nanosecond = Int(nanosecond.pointee) + components.nanosecond = nanosecondsInt(for: nanosecond.pointee) return DatabaseDateComponents(components, format: .HMSS) } + + private static func nanosecondsInt(for secondFractional: Int32) -> Int { + guard secondFractional > 0 else { return 0 } + + let magnitude = Int32(log10(Double(secondFractional))) + let multiplier = Int32(pow(10.0, Double(8 - magnitude))) + + return Int(secondFractional * multiplier) + } } From bb59f5e6f7cc3e31477e195518ae93f6fd5214b1 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 11:05:47 +0700 Subject: [PATCH 06/34] 0.123 is not the same thing as 0.000123 --- .../Support/Foundation/SQLiteDateParser.swift | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 75b164fcd2..d7cb74e015 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -16,7 +16,7 @@ class SQLiteDateParser { private static let hour = UnsafeMutablePointer.allocate(capacity: 1) private static let minute = UnsafeMutablePointer.allocate(capacity: 1) private static let second = UnsafeMutablePointer.allocate(capacity: 1) - private static let nanosecond = UnsafeMutablePointer.allocate(capacity: 1) + private static let nanosecond = UnsafeMutablePointer.allocate(capacity: 9) public static func components(from dateString: String) -> DatabaseDateComponents? { switch dateString.count { @@ -41,7 +41,7 @@ class SQLiteDateParser { defer { pthread_mutex_unlock(&mutex) } let parseCount = withVaList([year, month, day, hour, minute, second, nanosecond]) { pointer in - vsscanf(dateString, "%d-%d-%d%*c%d:%d:%d.%d", pointer) + vsscanf(dateString, "%d-%d-%d%*c%d:%d:%d.%s", pointer) } guard parseCount >= 3 else { return nil } @@ -62,7 +62,7 @@ class SQLiteDateParser { guard parseCount >= 7 else { return DatabaseDateComponents(components, format: .YMD_HMS) } - components.nanosecond = nanosecondsInt(for: nanosecond.pointee) + components.nanosecond = nanosecondsInt(for: nanosecond) return DatabaseDateComponents(components, format: .YMD_HMSS) } @@ -75,7 +75,7 @@ class SQLiteDateParser { defer { pthread_mutex_unlock(&mutex) } let parseCount = withVaList([hour, minute, second, nanosecond]) { pointer in - vsscanf(timeString, "%d:%d:%d.%d", pointer) + vsscanf(timeString, "%d:%d:%d.%s", pointer) } guard parseCount >= 2 else { return nil } @@ -90,17 +90,14 @@ class SQLiteDateParser { guard parseCount >= 4 else { return DatabaseDateComponents(components, format: .HMS) } - components.nanosecond = nanosecondsInt(for: nanosecond.pointee) + components.nanosecond = nanosecondsInt(for: nanosecond) return DatabaseDateComponents(components, format: .HMSS) } - private static func nanosecondsInt(for secondFractional: Int32) -> Int { - guard secondFractional > 0 else { return 0 } - - let magnitude = Int32(log10(Double(secondFractional))) - let multiplier = Int32(pow(10.0, Double(8 - magnitude))) - - return Int(secondFractional * multiplier) + private static func nanosecondsInt(for nanoCString: UnsafePointer) -> Int { + let nanoString = "0." + String(cString: nanoCString) + guard let doubleValue = Double(nanoString) else { return 0 } + return Int(doubleValue * 1_000_000_000) } } From dad163771ff18368e54acd9c1248caab8ee94e66 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 12:38:23 +0700 Subject: [PATCH 07/34] not all nanosecond fractions have 3 sig figs, so can't hard code the acceptable string lengths --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index d7cb74e015..e176c08811 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -19,14 +19,17 @@ class SQLiteDateParser { private static let nanosecond = UnsafeMutablePointer.allocate(capacity: 9) public static func components(from dateString: String) -> DatabaseDateComponents? { - switch dateString.count { - case 23, 19, 16, 10: + guard dateString.count >= 5 else { return nil } + + if dateString[dateString.index(dateString.startIndex, offsetBy: 4)] == "-" { return datetimeComponents(from: dateString) - case 12, 8, 5: + } + + if dateString[dateString.index(dateString.startIndex, offsetBy: 2)] == ":" { return timeComponents(from: dateString) - default: - return nil } + + return nil } // - YYYY-MM-DD From eba5df1fcc1ae9a3c9ac346bed8bda886a420103 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 12:57:59 +0700 Subject: [PATCH 08/34] fix the tests to expect less nanosecond precision loss --- Tests/GRDBTests/FoundationDateTests.swift | 4 ++-- Tests/GRDBTests/FoundationNSDateTests.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/GRDBTests/FoundationDateTests.swift b/Tests/GRDBTests/FoundationDateTests.swift index 920d15f013..6e0a29fefd 100644 --- a/Tests/GRDBTests/FoundationDateTests.swift +++ b/Tests/GRDBTests/FoundationDateTests.swift @@ -179,7 +179,7 @@ class FoundationDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date), 1) XCTAssertEqual(calendar.component(.minute, from: date), 2) 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. + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 10) } } @@ -259,7 +259,7 @@ class FoundationDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date), 1) XCTAssertEqual(calendar.component(.minute, from: date), 2) 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. + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 10) } } } diff --git a/Tests/GRDBTests/FoundationNSDateTests.swift b/Tests/GRDBTests/FoundationNSDateTests.swift index ea43fb71f1..0b3330c5ef 100644 --- a/Tests/GRDBTests/FoundationNSDateTests.swift +++ b/Tests/GRDBTests/FoundationNSDateTests.swift @@ -162,7 +162,7 @@ class FoundationNSDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date as Date), 1) XCTAssertEqual(calendar.component(.minute, from: date as Date), 2) XCTAssertEqual(calendar.component(.second, from: date as Date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_000_000) < 10) // We actually get 4_000_008. Some precision is lost during the NSDateComponents -> NSDate conversion. Not a big deal. + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 10) } } @@ -242,7 +242,7 @@ class FoundationNSDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date as Date), 1) XCTAssertEqual(calendar.component(.minute, from: date as Date), 2) XCTAssertEqual(calendar.component(.second, from: date as Date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_000_000) < 10) // We actually get 4_000_008. Some precision is lost during the NSDateComponents -> NSDate conversion. Not a big deal. + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 10) } } } From 3bcc5b806425b3f94bc927e3360cdc566481c425 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 13:20:11 +0700 Subject: [PATCH 09/34] permit precision loss down to 3 sig figs --- Tests/GRDBTests/FoundationDateTests.swift | 2 +- Tests/GRDBTests/FoundationNSDateTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/GRDBTests/FoundationDateTests.swift b/Tests/GRDBTests/FoundationDateTests.swift index 6e0a29fefd..43e7a9b66f 100644 --- a/Tests/GRDBTests/FoundationDateTests.swift +++ b/Tests/GRDBTests/FoundationDateTests.swift @@ -259,7 +259,7 @@ class FoundationDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date), 1) XCTAssertEqual(calendar.component(.minute, from: date), 2) XCTAssertEqual(calendar.component(.second, from: date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 10) + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 600_000) // permit precision loss down to 3 sig figs } } } diff --git a/Tests/GRDBTests/FoundationNSDateTests.swift b/Tests/GRDBTests/FoundationNSDateTests.swift index 0b3330c5ef..411c307add 100644 --- a/Tests/GRDBTests/FoundationNSDateTests.swift +++ b/Tests/GRDBTests/FoundationNSDateTests.swift @@ -242,7 +242,7 @@ class FoundationNSDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date as Date), 1) XCTAssertEqual(calendar.component(.minute, from: date as Date), 2) XCTAssertEqual(calendar.component(.second, from: date as Date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 10) + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 600_000) // permit precision loss down to 3 sig figs } } } From 33066133fc2b206f833e6fb389c75854cd99afd6 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 14:32:33 +0700 Subject: [PATCH 10/34] truncate date/time strings that include seconds precision beyond the nanosecond scale --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index e176c08811..02eaa672a5 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -16,17 +16,19 @@ class SQLiteDateParser { private static let hour = UnsafeMutablePointer.allocate(capacity: 1) private static let minute = UnsafeMutablePointer.allocate(capacity: 1) private static let second = UnsafeMutablePointer.allocate(capacity: 1) - private static let nanosecond = UnsafeMutablePointer.allocate(capacity: 9) + private static let nanosecond = UnsafeMutablePointer.allocate(capacity: 11) public static func components(from dateString: String) -> DatabaseDateComponents? { guard dateString.count >= 5 else { return nil } if dateString[dateString.index(dateString.startIndex, offsetBy: 4)] == "-" { - return datetimeComponents(from: dateString) + // a date string with full nanosecond precision is 29 chars + return datetimeComponents(from: String(dateString.prefix(29))) } if dateString[dateString.index(dateString.startIndex, offsetBy: 2)] == ":" { - return timeComponents(from: dateString) + // a time string with full nanosecond precision is 18 chars + return timeComponents(from: String(dateString.prefix(18))) } return nil From bde72e562e18501bfe9d0f2208d11633edc545dd Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 15:14:09 +0700 Subject: [PATCH 11/34] Revert "permit precision loss down to 3 sig figs" This reverts commit 3bcc5b806425b3f94bc927e3360cdc566481c425. --- Tests/GRDBTests/FoundationDateTests.swift | 2 +- Tests/GRDBTests/FoundationNSDateTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/GRDBTests/FoundationDateTests.swift b/Tests/GRDBTests/FoundationDateTests.swift index 43e7a9b66f..6e0a29fefd 100644 --- a/Tests/GRDBTests/FoundationDateTests.swift +++ b/Tests/GRDBTests/FoundationDateTests.swift @@ -259,7 +259,7 @@ class FoundationDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date), 1) XCTAssertEqual(calendar.component(.minute, from: date), 2) XCTAssertEqual(calendar.component(.second, from: date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 600_000) // permit precision loss down to 3 sig figs + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 10) } } } diff --git a/Tests/GRDBTests/FoundationNSDateTests.swift b/Tests/GRDBTests/FoundationNSDateTests.swift index 411c307add..0b3330c5ef 100644 --- a/Tests/GRDBTests/FoundationNSDateTests.swift +++ b/Tests/GRDBTests/FoundationNSDateTests.swift @@ -242,7 +242,7 @@ class FoundationNSDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date as Date), 1) XCTAssertEqual(calendar.component(.minute, from: date as Date), 2) XCTAssertEqual(calendar.component(.second, from: date as Date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 600_000) // permit precision loss down to 3 sig figs + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 10) } } } From 7320f880667ee762db303d0c2a0a897e536dc063 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 15:14:18 +0700 Subject: [PATCH 12/34] Revert "fix the tests to expect less nanosecond precision loss" This reverts commit eba5df1fcc1ae9a3c9ac346bed8bda886a420103. --- Tests/GRDBTests/FoundationDateTests.swift | 4 ++-- Tests/GRDBTests/FoundationNSDateTests.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/GRDBTests/FoundationDateTests.swift b/Tests/GRDBTests/FoundationDateTests.swift index 6e0a29fefd..920d15f013 100644 --- a/Tests/GRDBTests/FoundationDateTests.swift +++ b/Tests/GRDBTests/FoundationDateTests.swift @@ -179,7 +179,7 @@ class FoundationDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date), 1) XCTAssertEqual(calendar.component(.minute, from: date), 2) XCTAssertEqual(calendar.component(.second, from: date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 10) + 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. } } @@ -259,7 +259,7 @@ class FoundationDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date), 1) XCTAssertEqual(calendar.component(.minute, from: date), 2) XCTAssertEqual(calendar.component(.second, from: date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date) - 4_560_000) < 10) + 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. } } } diff --git a/Tests/GRDBTests/FoundationNSDateTests.swift b/Tests/GRDBTests/FoundationNSDateTests.swift index 0b3330c5ef..ea43fb71f1 100644 --- a/Tests/GRDBTests/FoundationNSDateTests.swift +++ b/Tests/GRDBTests/FoundationNSDateTests.swift @@ -162,7 +162,7 @@ class FoundationNSDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date as Date), 1) XCTAssertEqual(calendar.component(.minute, from: date as Date), 2) XCTAssertEqual(calendar.component(.second, from: date as Date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 10) + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_000_000) < 10) // We actually get 4_000_008. Some precision is lost during the NSDateComponents -> NSDate conversion. Not a big deal. } } @@ -242,7 +242,7 @@ class FoundationNSDateTests : GRDBTestCase { XCTAssertEqual(calendar.component(.hour, from: date as Date), 1) XCTAssertEqual(calendar.component(.minute, from: date as Date), 2) XCTAssertEqual(calendar.component(.second, from: date as Date), 3) - XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_560_000) < 10) + XCTAssertTrue(abs(calendar.component(.nanosecond, from: date as Date) - 4_000_000) < 10) // We actually get 4_000_008. Some precision is lost during the NSDateComponents -> NSDate conversion. Not a big deal. } } } From 3b546dedd8fd087b01db4cac096eca9f83fdc3b5 Mon Sep 17 00:00:00 2001 From: Matt Greenfield Date: Tue, 17 Apr 2018 15:17:09 +0700 Subject: [PATCH 13/34] truncate nanosecond precision to 3 sig figs --- .../Core/Support/Foundation/SQLiteDateParser.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 02eaa672a5..2e7fe23679 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -22,13 +22,19 @@ class SQLiteDateParser { guard dateString.count >= 5 else { return nil } if dateString[dateString.index(dateString.startIndex, offsetBy: 4)] == "-" { - // a date string with full nanosecond precision is 29 chars - return datetimeComponents(from: String(dateString.prefix(29))) + /*** + Note: A date string with full nanosecond precision is 29 chars. + This call is truncating the nanosecond fraction to a max of 3 sig figs (ie 23 chars). + */ + return datetimeComponents(from: String(dateString.prefix(23))) } if dateString[dateString.index(dateString.startIndex, offsetBy: 2)] == ":" { - // a time string with full nanosecond precision is 18 chars - return timeComponents(from: String(dateString.prefix(18))) + /*** + Note: A time string with full nanosecond precision is 18 chars. + This call is truncating the nanosecond fraction to a max of 3 sig figs (ie 12 chars). + */ + return timeComponents(from: String(dateString.prefix(12))) } return nil From 6d9bce3a25da5a39c17410df666773497d159d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 13:08:17 +0200 Subject: [PATCH 14/34] Add SQLiteDateParser.swift to GRDBCustom and GRDBCipher ... so that all GRDB flavors can use the new technique. GRDB has three distinct projects since #282. --- GRDBCipher.xcodeproj/project.pbxproj | 6 ++++++ GRDBCustom.xcodeproj/project.pbxproj | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/GRDBCipher.xcodeproj/project.pbxproj b/GRDBCipher.xcodeproj/project.pbxproj index 56d9918cba..0593c452df 100755 --- a/GRDBCipher.xcodeproj/project.pbxproj +++ b/GRDBCipher.xcodeproj/project.pbxproj @@ -284,6 +284,8 @@ 566B912F1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */; }; 566B91341FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566B91371FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; + 567071F8208A00D4006AD95A /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567071F6208A00D4006AD95A /* SQLiteDateParser.swift */; }; + 567071F9208A00D4006AD95A /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567071F6208A00D4006AD95A /* SQLiteDateParser.swift */; }; 567156141CB141D0007DC145 /* DatabaseQueueInMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238141B9C74A90082EB20 /* DatabaseQueueInMemoryTests.swift */; }; 567156171CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */; }; 5671561D1CB16729007DC145 /* DatabaseValueConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2381B1B9C74A90082EB20 /* DatabaseValueConversionTests.swift */; }; @@ -919,6 +921,7 @@ 566B91221FA4CF810012D5B0 /* Database+Schema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+Schema.swift"; sourceTree = ""; }; 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatementAuthorizer.swift; sourceTree = ""; }; 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionObserver.swift; sourceTree = ""; }; + 567071F6208A00D4006AD95A /* SQLiteDateParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteDateParser.swift; sourceTree = ""; }; 567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueReadOnlyTests.swift; sourceTree = ""; }; 5671566C1CB16729007DC145 /* GRDBCipherOSXEncryptedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBCipherOSXEncryptedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 567156701CB18050007DC145 /* EncryptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionTests.swift; sourceTree = ""; }; @@ -1168,6 +1171,7 @@ 5605F1521C672E4000235C62 /* NSString.swift */, 5657AB0E1D10899D006283EF /* URL.swift */, 56A8C22F1D1914540096E9D4 /* UUID.swift */, + 567071F6208A00D4006AD95A /* SQLiteDateParser.swift */, ); path = Foundation; sourceTree = ""; @@ -2069,6 +2073,7 @@ 5698AD221DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 560FC53C1CB003810014AA8E /* DatabaseQueue.swift in Sources */, 560FC53D1CB003810014AA8E /* NSNumber.swift in Sources */, + 567071F8208A00D4006AD95A /* SQLiteDateParser.swift in Sources */, 560FC53F1CB003810014AA8E /* Row.swift in Sources */, 560FC5401CB003810014AA8E /* StandardLibrary.swift in Sources */, 5680520C2013209300E87861 /* RecordBox.swift in Sources */, @@ -2464,6 +2469,7 @@ 5698AD251DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 56AFCA121CB1A8BB00F48B96 /* NSNumber.swift in Sources */, 56AFCA141CB1A8BB00F48B96 /* StandardLibrary.swift in Sources */, + 567071F9208A00D4006AD95A /* SQLiteDateParser.swift in Sources */, 56AFCA151CB1A8BB00F48B96 /* Persistable.swift in Sources */, 56AFCA161CB1A8BB00F48B96 /* StatementColumnConvertible.swift in Sources */, 5680520D2013209900E87861 /* RecordBox.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 314f5d50d0..f0a9878320 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -111,6 +111,8 @@ 566B91301FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */; }; 566B91351FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566B91381FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; + 567071F4208A00BE006AD95A /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */; }; + 567071F5208A00BE006AD95A /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */; }; 5671FC221DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */; }; 5671FC251DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */; }; 56741EAB1E66A8B3003E422D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56741EA71E66A8B3003E422D /* RequestTests.swift */; }; @@ -618,6 +620,7 @@ 566B91221FA4CF810012D5B0 /* Database+Schema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+Schema.swift"; sourceTree = ""; }; 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatementAuthorizer.swift; sourceTree = ""; }; 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionObserver.swift; sourceTree = ""; }; + 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteDateParser.swift; sourceTree = ""; }; 567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueReadOnlyTests.swift; sourceTree = ""; }; 567156701CB18050007DC145 /* EncryptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionTests.swift; sourceTree = ""; }; 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3TokenizerDescriptor.swift; sourceTree = ""; }; @@ -855,6 +858,7 @@ 5605F1521C672E4000235C62 /* NSString.swift */, 5657AB0E1D10899D006283EF /* URL.swift */, 56A8C22F1D1914540096E9D4 /* UUID.swift */, + 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */, ); path = Foundation; sourceTree = ""; @@ -1770,6 +1774,7 @@ 5698AD261DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, F3BA802C1CFB289B003DC1BA /* SQLCollatedExpression.swift in Sources */, F3BA80301CFB289F003DC1BA /* DatabaseMigrator.swift in Sources */, + 567071F5208A00BE006AD95A /* SQLiteDateParser.swift in Sources */, F3BA80361CFB28A4003DC1BA /* TableMapping.swift in Sources */, F3BA80271CFB2891003DC1BA /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, 5680520E201320A200E87861 /* RecordBox.swift in Sources */, @@ -2017,6 +2022,7 @@ 5698AD231DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, F3BA80881CFB2E70003DC1BA /* SQLCollatedExpression.swift in Sources */, F3BA808C1CFB2E75003DC1BA /* DatabaseMigrator.swift in Sources */, + 567071F4208A00BE006AD95A /* SQLiteDateParser.swift in Sources */, F3BA80921CFB2E7A003DC1BA /* TableMapping.swift in Sources */, F3BA80831CFB2E67003DC1BA /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, 568052092013207000E87861 /* RecordBox.swift in Sources */, From 7aabfeaeac102d39efb8728bb5db1cb5705d83b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 13:09:22 +0200 Subject: [PATCH 15/34] SQLiteDateParser: get rid of the mutex --- .../Core/Support/Foundation/SQLiteDateParser.swift | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 2e7fe23679..98fad7b607 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -3,13 +3,7 @@ import Foundation // inspired by: http://jordansmith.io/performant-date-parsing/ class SQLiteDateParser { - - private static var mutex: pthread_mutex_t = { - var mutex = pthread_mutex_t() - pthread_mutex_init(&mutex, nil) - return mutex - }() - + private static let year = UnsafeMutablePointer.allocate(capacity: 1) private static let month = UnsafeMutablePointer.allocate(capacity: 1) private static let day = UnsafeMutablePointer.allocate(capacity: 1) @@ -48,9 +42,6 @@ class SQLiteDateParser { // - YYYY-MM-DDTHH:MM:SS // - YYYY-MM-DDTHH:MM:SS.SSS private static func datetimeComponents(from dateString: String) -> DatabaseDateComponents? { - pthread_mutex_lock(&mutex) - defer { pthread_mutex_unlock(&mutex) } - let parseCount = withVaList([year, month, day, hour, minute, second, nanosecond]) { pointer in vsscanf(dateString, "%d-%d-%d%*c%d:%d:%d.%s", pointer) } @@ -82,9 +73,6 @@ class SQLiteDateParser { // - HH:MM:SS // - HH:MM:SS.SSS private static func timeComponents(from timeString: String) -> DatabaseDateComponents? { - pthread_mutex_lock(&mutex) - defer { pthread_mutex_unlock(&mutex) } - let parseCount = withVaList([hour, minute, second, nanosecond]) { pointer in vsscanf(timeString, "%d:%d:%d.%s", pointer) } From 27194c802d767daead0410e1c65552490e3e995f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 13:30:26 +0200 Subject: [PATCH 16/34] SQLiteDateParser: use withUnsafeMutablePointer instead of allocations OK, not really: the nanosecond components remains in the heap. But we may find a way to put 11 chars on the stack. --- .../Support/Foundation/SQLiteDateParser.swift | 81 +++++++++++++------ 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 98fad7b607..4721dbca98 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -4,13 +4,15 @@ import Foundation class SQLiteDateParser { - private static let year = UnsafeMutablePointer.allocate(capacity: 1) - private static let month = UnsafeMutablePointer.allocate(capacity: 1) - private static let day = UnsafeMutablePointer.allocate(capacity: 1) - private static let hour = UnsafeMutablePointer.allocate(capacity: 1) - private static let minute = UnsafeMutablePointer.allocate(capacity: 1) - private static let second = UnsafeMutablePointer.allocate(capacity: 1) - private static let nanosecond = UnsafeMutablePointer.allocate(capacity: 11) + 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.init(repeating: 0, count: 11) + } public static func components(from dateString: String) -> DatabaseDateComponents? { guard dateString.count >= 5 else { return nil } @@ -42,29 +44,47 @@ class SQLiteDateParser { // - YYYY-MM-DDTHH:MM:SS // - YYYY-MM-DDTHH:MM:SS.SSS private static func datetimeComponents(from dateString: String) -> DatabaseDateComponents? { - let parseCount = withVaList([year, month, day, hour, minute, second, nanosecond]) { pointer in - vsscanf(dateString, "%d-%d-%d%*c%d:%d:%d.%s", pointer) + var parserComponents = ParserComponents() + + // TODO: Get rid of this pyramid when SE-0210 has shipped + let parseCount = withUnsafeMutablePointer(to: &parserComponents.year) { yearP in + withUnsafeMutablePointer(to: &parserComponents.month) { monthP in + withUnsafeMutablePointer(to: &parserComponents.day) { dayP in + withUnsafeMutablePointer(to: &parserComponents.hour) { hourP in + withUnsafeMutablePointer(to: &parserComponents.minute) { minuteP in + withUnsafeMutablePointer(to: &parserComponents.second) { secondP in + parserComponents.nanosecond.withUnsafeMutableBufferPointer { nanosecondBuffer in + // TODO: what if vsscanf parses a string longer than nanosecond length? + withVaList([yearP, monthP, dayP, hourP, minuteP, secondP, nanosecondBuffer.baseAddress!]) { pointer in + vsscanf(dateString, "%d-%d-%d%*c%d:%d:%d.%s", pointer) + } + } + } + } + } + } + } } guard parseCount >= 3 else { return nil } var components = DateComponents() - components.year = Int(year.pointee) - components.month = Int(month.pointee) - components.day = Int(day.pointee) + components.year = Int(parserComponents.year) + components.month = Int(parserComponents.month) + components.day = Int(parserComponents.day) guard parseCount >= 5 else { return DatabaseDateComponents(components, format: .YMD) } - components.hour = Int(hour.pointee) - components.minute = Int(minute.pointee) + components.hour = Int(parserComponents.hour) + components.minute = Int(parserComponents.minute) guard parseCount >= 6 else { return DatabaseDateComponents(components, format: .YMD_HM) } - components.second = Int(second.pointee) + components.second = Int(parserComponents.second) guard parseCount >= 7 else { return DatabaseDateComponents(components, format: .YMD_HMS) } - components.nanosecond = nanosecondsInt(for: nanosecond) + components.nanosecond = nanosecondsInt(for: parserComponents.nanosecond) return DatabaseDateComponents(components, format: .YMD_HMSS) } @@ -73,29 +93,40 @@ class SQLiteDateParser { // - HH:MM:SS // - HH:MM:SS.SSS private static func timeComponents(from timeString: String) -> DatabaseDateComponents? { - let parseCount = withVaList([hour, minute, second, nanosecond]) { pointer in - vsscanf(timeString, "%d:%d:%d.%s", pointer) + var parserComponents = ParserComponents() + // TODO: Get rid of this pyramid when SE-0210 has shipped + let parseCount = withUnsafeMutablePointer(to: &parserComponents.hour) { hourP in + withUnsafeMutablePointer(to: &parserComponents.minute) { minuteP in + withUnsafeMutablePointer(to: &parserComponents.second) { secondP in + parserComponents.nanosecond.withUnsafeMutableBufferPointer { nanosecondBuffer in + // TODO: what if vsscanf parses a string longer than nanosecond length? + withVaList([hourP, minuteP, secondP, nanosecondBuffer.baseAddress!]) { pointer in + vsscanf(timeString, "%d:%d:%d.%s", pointer) + } + } + } + } } - + guard parseCount >= 2 else { return nil } var components = DateComponents() - components.hour = Int(hour.pointee) - components.minute = Int(minute.pointee) + components.hour = Int(parserComponents.hour) + components.minute = Int(parserComponents.minute) guard parseCount >= 3 else { return DatabaseDateComponents(components, format: .HM) } - components.second = Int(second.pointee) + components.second = Int(parserComponents.second) guard parseCount >= 4 else { return DatabaseDateComponents(components, format: .HMS) } - components.nanosecond = nanosecondsInt(for: nanosecond) + components.nanosecond = nanosecondsInt(for: parserComponents.nanosecond) return DatabaseDateComponents(components, format: .HMSS) } - private static func nanosecondsInt(for nanoCString: UnsafePointer) -> Int { - let nanoString = "0." + String(cString: nanoCString) + private static func nanosecondsInt(for nanosecond: ContiguousArray) -> Int { + let nanoString = "0." + nanosecond.withUnsafeBufferPointer { String(cString: $0.baseAddress!) } guard let doubleValue = Double(nanoString) else { return 0 } return Int(doubleValue * 1_000_000_000) } From e9a668ca561eb87025c5b57c1ee0db3c59d0c8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 13:31:35 +0200 Subject: [PATCH 17/34] SQLiteDateParser: no more static methods --- GRDB/Core/Support/Foundation/DatabaseDateComponents.swift | 2 +- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 4ad2848764..ab877a257d 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -134,7 +134,7 @@ public struct DatabaseDateComponents : DatabaseValueConvertible { } if useScanfStrategy { - return SQLiteDateParser.components(from: string) + return SQLiteDateParser().components(from: string) } var dateComponents = DateComponents() diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 4721dbca98..e015786dce 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -14,7 +14,7 @@ class SQLiteDateParser { var nanosecond = ContiguousArray.init(repeating: 0, count: 11) } - public static func components(from dateString: String) -> DatabaseDateComponents? { + public func components(from dateString: String) -> DatabaseDateComponents? { guard dateString.count >= 5 else { return nil } if dateString[dateString.index(dateString.startIndex, offsetBy: 4)] == "-" { @@ -43,7 +43,7 @@ class SQLiteDateParser { // - YYYY-MM-DDTHH:MM // - YYYY-MM-DDTHH:MM:SS // - YYYY-MM-DDTHH:MM:SS.SSS - private static func datetimeComponents(from dateString: String) -> DatabaseDateComponents? { + private func datetimeComponents(from dateString: String) -> DatabaseDateComponents? { var parserComponents = ParserComponents() // TODO: Get rid of this pyramid when SE-0210 has shipped @@ -92,7 +92,7 @@ class SQLiteDateParser { // - HH:MM // - HH:MM:SS // - HH:MM:SS.SSS - private static func timeComponents(from timeString: String) -> DatabaseDateComponents? { + private func timeComponents(from timeString: String) -> DatabaseDateComponents? { var parserComponents = ParserComponents() // TODO: Get rid of this pyramid when SE-0210 has shipped let parseCount = withUnsafeMutablePointer(to: &parserComponents.hour) { hourP in @@ -125,7 +125,7 @@ class SQLiteDateParser { return DatabaseDateComponents(components, format: .HMSS) } - private static func nanosecondsInt(for nanosecond: ContiguousArray) -> Int { + private func nanosecondsInt(for nanosecond: ContiguousArray) -> Int { let nanoString = "0." + nanosecond.withUnsafeBufferPointer { String(cString: $0.baseAddress!) } guard let doubleValue = Double(nanoString) else { return 0 } return Int(doubleValue * 1_000_000_000) From d1a0e8d906521b786f627a621b340735462da28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 13:31:57 +0200 Subject: [PATCH 18/34] SQLiteDateParser.components(from:) is not public --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index e015786dce..7ec4a10e64 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -14,7 +14,7 @@ class SQLiteDateParser { var nanosecond = ContiguousArray.init(repeating: 0, count: 11) } - public func components(from dateString: String) -> DatabaseDateComponents? { + func components(from dateString: String) -> DatabaseDateComponents? { guard dateString.count >= 5 else { return nil } if dateString[dateString.index(dateString.startIndex, offsetBy: 4)] == "-" { From 0640f90fc1e2abf68a1d8a1c7d0f6de30493ec38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 13:32:50 +0200 Subject: [PATCH 19/34] (whitespace) --- .../Support/Foundation/SQLiteDateParser.swift | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 7ec4a10e64..eba61b9072 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -13,10 +13,10 @@ class SQLiteDateParser { var second: Int32 = 0 var nanosecond = ContiguousArray.init(repeating: 0, count: 11) } - + func components(from dateString: String) -> DatabaseDateComponents? { guard dateString.count >= 5 else { return nil } - + if dateString[dateString.index(dateString.startIndex, offsetBy: 4)] == "-" { /*** Note: A date string with full nanosecond precision is 29 chars. @@ -24,7 +24,7 @@ class SQLiteDateParser { */ return datetimeComponents(from: String(dateString.prefix(23))) } - + if dateString[dateString.index(dateString.startIndex, offsetBy: 2)] == ":" { /*** Note: A time string with full nanosecond precision is 18 chars. @@ -32,10 +32,10 @@ class SQLiteDateParser { */ return timeComponents(from: String(dateString.prefix(12))) } - + return nil } - + // - YYYY-MM-DD // - YYYY-MM-DD HH:MM // - YYYY-MM-DD HH:MM:SS @@ -65,30 +65,30 @@ class SQLiteDateParser { } } } - + guard parseCount >= 3 else { return nil } - + var components = DateComponents() components.year = Int(parserComponents.year) components.month = Int(parserComponents.month) components.day = Int(parserComponents.day) - + guard parseCount >= 5 else { return DatabaseDateComponents(components, format: .YMD) } - + components.hour = Int(parserComponents.hour) components.minute = Int(parserComponents.minute) - + guard parseCount >= 6 else { return DatabaseDateComponents(components, format: .YMD_HM) } - + components.second = Int(parserComponents.second) - + guard parseCount >= 7 else { return DatabaseDateComponents(components, format: .YMD_HMS) } - + components.nanosecond = nanosecondsInt(for: parserComponents.nanosecond) - + return DatabaseDateComponents(components, format: .YMD_HMSS) } - + // - HH:MM // - HH:MM:SS // - HH:MM:SS.SSS @@ -109,22 +109,22 @@ class SQLiteDateParser { } guard parseCount >= 2 else { return nil } - + var components = DateComponents() components.hour = Int(parserComponents.hour) components.minute = Int(parserComponents.minute) - + guard parseCount >= 3 else { return DatabaseDateComponents(components, format: .HM) } - + components.second = Int(parserComponents.second) - + guard parseCount >= 4 else { return DatabaseDateComponents(components, format: .HMS) } - + components.nanosecond = nanosecondsInt(for: parserComponents.nanosecond) - + return DatabaseDateComponents(components, format: .HMSS) } - + private func nanosecondsInt(for nanosecond: ContiguousArray) -> Int { let nanoString = "0." + nanosecond.withUnsafeBufferPointer { String(cString: $0.baseAddress!) } guard let doubleValue = Double(nanoString) else { return 0 } From 8915672ce5dea7cacc196b6eee37b6d05f4a4acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 13:51:20 +0200 Subject: [PATCH 20/34] SQLiteDateParser: parse C strings Since the goal of this PR is efficiency, we'll soon parse Date and DatabaseDateComponents right from raw SQLite buffers. --- .../Support/Foundation/SQLiteDateParser.swift | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index eba61b9072..d434ba2da3 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -11,26 +11,24 @@ class SQLiteDateParser { var hour: Int32 = 0 var minute: Int32 = 0 var second: Int32 = 0 - var nanosecond = ContiguousArray.init(repeating: 0, count: 11) + var nanosecond = ContiguousArray.init(repeating: 0, count: 10) // 9 digits, and trailing \0 } func components(from dateString: String) -> DatabaseDateComponents? { - guard dateString.count >= 5 else { return nil } - - if dateString[dateString.index(dateString.startIndex, offsetBy: 4)] == "-" { - /*** - Note: A date string with full nanosecond precision is 29 chars. - This call is truncating the nanosecond fraction to a max of 3 sig figs (ie 23 chars). - */ - return datetimeComponents(from: String(dateString.prefix(23))) + return dateString.withCString { cString in + components(cString: cString, length: strlen(cString)) } + } + + func components(cString: UnsafePointer, length: Int) -> DatabaseDateComponents? { + guard length >= 5 else { return nil } - if dateString[dateString.index(dateString.startIndex, offsetBy: 2)] == ":" { - /*** - Note: A time string with full nanosecond precision is 18 chars. - This call is truncating the nanosecond fraction to a max of 3 sig figs (ie 12 chars). - */ - return timeComponents(from: String(dateString.prefix(12))) + if cString.advanced(by: 4).pointee == 45 /* '-' */ { + return datetimeComponents(cString: cString, length: length) + } + + if cString.advanced(by: 2).pointee == 58 /* ':' */ { + return timeComponents(cString: cString, length: length) } return nil @@ -43,7 +41,7 @@ class SQLiteDateParser { // - YYYY-MM-DDTHH:MM // - YYYY-MM-DDTHH:MM:SS // - YYYY-MM-DDTHH:MM:SS.SSS - private func datetimeComponents(from dateString: String) -> DatabaseDateComponents? { + private func datetimeComponents(cString: UnsafePointer, length: Int) -> DatabaseDateComponents? { var parserComponents = ParserComponents() // TODO: Get rid of this pyramid when SE-0210 has shipped @@ -54,9 +52,8 @@ class SQLiteDateParser { withUnsafeMutablePointer(to: &parserComponents.minute) { minuteP in withUnsafeMutablePointer(to: &parserComponents.second) { secondP in parserComponents.nanosecond.withUnsafeMutableBufferPointer { nanosecondBuffer in - // TODO: what if vsscanf parses a string longer than nanosecond length? withVaList([yearP, monthP, dayP, hourP, minuteP, secondP, nanosecondBuffer.baseAddress!]) { pointer in - vsscanf(dateString, "%d-%d-%d%*c%d:%d:%d.%s", pointer) + vsscanf(cString, "%d-%d-%d%*c%d:%d:%d.%10s", pointer) } } } @@ -92,16 +89,16 @@ class SQLiteDateParser { // - HH:MM // - HH:MM:SS // - HH:MM:SS.SSS - private func timeComponents(from timeString: String) -> DatabaseDateComponents? { + private func timeComponents(cString: UnsafePointer, length: Int) -> DatabaseDateComponents? { var parserComponents = ParserComponents() + // TODO: Get rid of this pyramid when SE-0210 has shipped let parseCount = withUnsafeMutablePointer(to: &parserComponents.hour) { hourP in withUnsafeMutablePointer(to: &parserComponents.minute) { minuteP in withUnsafeMutablePointer(to: &parserComponents.second) { secondP in parserComponents.nanosecond.withUnsafeMutableBufferPointer { nanosecondBuffer in - // TODO: what if vsscanf parses a string longer than nanosecond length? withVaList([hourP, minuteP, secondP, nanosecondBuffer.baseAddress!]) { pointer in - vsscanf(timeString, "%d:%d:%d.%s", pointer) + vsscanf(cString, "%d:%d:%d.%10s", pointer) } } } @@ -126,7 +123,8 @@ class SQLiteDateParser { } private func nanosecondsInt(for nanosecond: ContiguousArray) -> Int { - let nanoString = "0." + nanosecond.withUnsafeBufferPointer { String(cString: $0.baseAddress!) } + // truncate the nanosecond fraction + let nanoString = ("0." + nanosecond.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }).prefix(5) guard let doubleValue = Double(nanoString) else { return 0 } return Int(doubleValue * 1_000_000_000) } From 936b31f1f08fb6cb3a454e472106cb1f2334c09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 14:04:12 +0200 Subject: [PATCH 21/34] DatabaseDateComponents adopts StatementColumnConvertible We'll need benchmarks to evaluate the gain, here --- .../Support/Foundation/DatabaseDateComponents.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index ab877a257d..0afbdaa544 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -1,7 +1,7 @@ import Foundation /// DatabaseDateComponents reads and stores DateComponents in the database. -public struct DatabaseDateComponents : DatabaseValueConvertible { +public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumnConvertible { public static var useScanfStrategy = true @@ -70,6 +70,17 @@ public struct DatabaseDateComponents : DatabaseValueConvertible { self.dateComponents = dateComponents } + // MARK: - StatementColumnConvertible adoption + + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let cString = UnsafePointer(OpaquePointer(sqlite3_column_text(sqliteStatement, index)!)) + let length = Int(sqlite3_column_bytes(sqliteStatement, index)) // avoid an strlen + guard let components = SQLiteDateParser().components(cString: cString, length: length) else { + fatalError("could not convert database value \(String(cString: cString)) to DatabaseDateComponents") + } + self.dateComponents = components.dateComponents + self.format = components.format + } // MARK: - DatabaseValueConvertible adoption From c70689fe546b0e47c5739927af3b4cccf81b4795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 18:34:19 +0200 Subject: [PATCH 22/34] Check useScanfStrategy in DatabaseDateComponents.init(sqliteStatement:index:) ...so that we can eventually compare gains --- .../Support/Foundation/DatabaseDateComponents.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 0afbdaa544..29d7ff6e7b 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -73,6 +73,16 @@ public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumn // MARK: - StatementColumnConvertible adoption public init(sqliteStatement: SQLiteStatement, index: Int32) { + guard DatabaseDateComponents.useScanfStrategy else { + let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) + guard let components = DatabaseDateComponents.fromDatabaseValue(dbValue) else { + fatalError("could not convert database value \(dbValue) to DatabaseDateComponents") + } + self.dateComponents = components.dateComponents + self.format = components.format + return + } + let cString = UnsafePointer(OpaquePointer(sqlite3_column_text(sqliteStatement, index)!)) let length = Int(sqlite3_column_bytes(sqliteStatement, index)) // avoid an strlen guard let components = SQLiteDateParser().components(cString: cString, length: length) else { From 11a4a6acbdcb9eec11774c03e5da7e74f1068beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 18:41:58 +0200 Subject: [PATCH 23/34] Documentation --- GRDB/Core/Support/Foundation/DatabaseDateComponents.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 29d7ff6e7b..5ae1e9575b 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -72,6 +72,11 @@ public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumn // MARK: - StatementColumnConvertible adoption + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. public init(sqliteStatement: SQLiteStatement, index: Int32) { guard DatabaseDateComponents.useScanfStrategy else { let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) From 266040747ecd51f611055b4505b69c3bc3849f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 18:42:12 +0200 Subject: [PATCH 24/34] Date adopts StatementColumnConvertible --- GRDB/Core/Support/Foundation/Date.swift | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/GRDB/Core/Support/Foundation/Date.swift b/GRDB/Core/Support/Foundation/Date.swift index f19b0d9355..51ee8eb77e 100644 --- a/GRDB/Core/Support/Foundation/Date.swift +++ b/GRDB/Core/Support/Foundation/Date.swift @@ -108,6 +108,31 @@ extension Date : DatabaseValueConvertible { } } +extension Date: StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + switch sqlite3_column_type(sqliteStatement, index) { + case SQLITE_INTEGER, SQLITE_FLOAT: + self.init(timeIntervalSince1970: sqlite3_column_double(sqliteStatement, Int32(index))) + case SQLITE_TEXT: + let databaseDateComponents = DatabaseDateComponents(sqliteStatement: sqliteStatement, index: index) + guard let date = Date(databaseDateComponents: databaseDateComponents) else { + let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) + fatalError("could not convert database value \(dbValue) to Date") + } + self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate) + default: + let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) + fatalError("could not convert database value \(dbValue) to Date") + } + } +} + /// The DatabaseDate date formatter for stored dates. private let storageDateFormatter: DateFormatter = { let formatter = DateFormatter() From f12495b24add08dce72161f1a7b9d7de1c6f4691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 19:06:02 +0200 Subject: [PATCH 25/34] New DateParsingTests in GRDBOSXPerformanceTests --- GRDB.xcodeproj/project.pbxproj | 4 +++ .../GRDBOSXPerformanceTests.xcscheme | 2 ++ Tests/Performance/DateParsingTests.swift | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 Tests/Performance/DateParsingTests.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index fe8efafdc1..9f31314c68 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -259,6 +259,7 @@ 566B91331FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566B91361FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566B91391FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; + 56707201208A509C006AD95A /* DateParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567071FA208A509C006AD95A /* DateParsingTests.swift */; }; 567156181CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */; }; 5671FC201DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */; }; 5671FC231DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */; }; @@ -843,6 +844,7 @@ 566B91221FA4CF810012D5B0 /* Database+Schema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+Schema.swift"; sourceTree = ""; }; 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatementAuthorizer.swift; sourceTree = ""; }; 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionObserver.swift; sourceTree = ""; }; + 567071FA208A509C006AD95A /* DateParsingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateParsingTests.swift; sourceTree = ""; }; 567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueReadOnlyTests.swift; sourceTree = ""; }; 567156701CB18050007DC145 /* EncryptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionTests.swift; sourceTree = ""; }; 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3TokenizerDescriptor.swift; sourceTree = ""; }; @@ -1574,6 +1576,7 @@ 56DE7B361C42BBBB00861EB8 /* PerformanceCoreDataTests.sqlite */, 56DE7B2D1C42B23B00861EB8 /* PerformanceModel.xcdatamodeld */, 563363F81C960711000BE133 /* PerformanceRealmTests.realm */, + 567071FA208A509C006AD95A /* DateParsingTests.swift */, 56DE7B271C41302500861EB8 /* FetchNamedValuesTests.swift */, 56DE7B291C4130AF00861EB8 /* FetchPositionalValuesTests.swift */, 56DE7B2B1C41311900861EB8 /* FetchRecordClassTests.swift */, @@ -2136,6 +2139,7 @@ files = ( 56D3BE711F4EB1A00034C6D2 /* FetchRecordStructTests.swift in Sources */, 560C98231C0E23BB00BF8471 /* InsertRecordClassTests.swift in Sources */, + 56707201208A509C006AD95A /* DateParsingTests.swift in Sources */, 56DE7B2C1C41311900861EB8 /* FetchRecordClassTests.swift in Sources */, 56DE7B281C41302500861EB8 /* FetchNamedValuesTests.swift in Sources */, 56D507831F6D7B2E00AE1C5B /* InsertRecordStructTests.swift in Sources */, diff --git a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSXPerformanceTests.xcscheme b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSXPerformanceTests.xcscheme index 2f1ecb6f3b..4394516c37 100644 --- a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSXPerformanceTests.xcscheme +++ b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSXPerformanceTests.xcscheme @@ -10,6 +10,7 @@ buildConfiguration = "Release" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" shouldUseLaunchSchemeArgsEnv = "YES"> Date: Fri, 20 Apr 2018 19:14:32 +0200 Subject: [PATCH 26/34] Cleanup --- Tests/Performance/DateParsingTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Performance/DateParsingTests.swift b/Tests/Performance/DateParsingTests.swift index 05c5c917ae..35d74cf09c 100644 --- a/Tests/Performance/DateParsingTests.swift +++ b/Tests/Performance/DateParsingTests.swift @@ -17,7 +17,7 @@ class DateParsingTests: XCTestCase { func testParseDateComponents() { measure { - _ = try! DatabaseQueue().inDatabase { db in + try! DatabaseQueue().inDatabase { db in let cursor = try DatabaseDateComponents.fetchCursor(db, request) while try cursor.next() != nil { } } @@ -26,7 +26,7 @@ class DateParsingTests: XCTestCase { func testParseDate() { measure { - _ = try! DatabaseQueue().inDatabase { db in + try! DatabaseQueue().inDatabase { db in let cursor = try Date.fetchCursor(db, request) while try cursor.next() != nil { } } From 282255b841cb291168fd4c43845d4fe0022e3b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 19:14:58 +0200 Subject: [PATCH 27/34] No need for useScanfStrategy any longer --- .../Foundation/DatabaseDateComponents.swift | 170 +----------------- 1 file changed, 1 insertion(+), 169 deletions(-) diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 5ae1e9575b..700dcae0d7 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -3,8 +3,6 @@ import Foundation /// DatabaseDateComponents reads and stores DateComponents in the database. public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumnConvertible { - public static var useScanfStrategy = true - /// The available formats for reading and storing date components. public enum Format : String { @@ -78,16 +76,6 @@ public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumn /// - sqliteStatement: A pointer to an SQLite statement. /// - index: The column index. public init(sqliteStatement: SQLiteStatement, index: Int32) { - guard DatabaseDateComponents.useScanfStrategy else { - let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) - guard let components = DatabaseDateComponents.fromDatabaseValue(dbValue) else { - fatalError("could not convert database value \(dbValue) to DatabaseDateComponents") - } - self.dateComponents = components.dateComponents - self.format = components.format - return - } - let cString = UnsafePointer(OpaquePointer(sqlite3_column_text(sqliteStatement, index)!)) let length = Int(sqlite3_column_bytes(sqliteStatement, index)) // avoid an strlen guard let components = SQLiteDateParser().components(cString: cString, length: length) else { @@ -158,163 +146,7 @@ public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumn guard let string = String.fromDatabaseValue(dbValue) else { return nil } - - if useScanfStrategy { - return SQLiteDateParser().components(from: string) - } - - var dateComponents = DateComponents() - let scanner = Scanner(string: string) - scanner.charactersToBeSkipped = CharacterSet() - - let hasDate: Bool - - // YYYY or HH - var initialNumber: Int = 0 - if !scanner.scanInt(&initialNumber) { - return nil - } - switch scanner.scanLocation { - case 2: - // HH - hasDate = false - - let hour = initialNumber - if hour >= 0 && hour <= 23 { - dateComponents.hour = hour - } else { - return nil - } - - case 4: - // YYYY - hasDate = true - - let year = initialNumber - if year >= 0 && year <= 9999 { - dateComponents.year = year - } else { - return nil - } - - // - - if !scanner.scanString("-", into: nil) { - return nil - } - - // MM - var month: Int = 0 - if scanner.scanInt(&month) && month >= 1 && month <= 12 { - dateComponents.month = month - } else { - return nil - } - - // - - if !scanner.scanString("-", into: nil) { - return nil - } - - // DD - var day: Int = 0 - if scanner.scanInt(&day) && day >= 1 && day <= 31 { - dateComponents.day = day - } else { - return nil - } - - // YYYY-MM-DD - if scanner.isAtEnd { - return DatabaseDateComponents(dateComponents, format: .YMD) - } - - // T/space - if !scanner.scanString("T", into: nil) && !scanner.scanString(" ", into: nil) { - return nil - } - - // HH - var hour: Int = 0 - if scanner.scanInt(&hour) && hour >= 0 && hour <= 23 { - dateComponents.hour = hour - } else { - return nil - } - - default: - return nil - } - - // : - if !scanner.scanString(":", into: nil) { - return nil - } - - // MM - var minute: Int = 0 - if scanner.scanInt(&minute) && minute >= 0 && minute <= 59 { - dateComponents.minute = minute - } else { - return nil - } - - // [YYYY-MM-DD] HH:MM - if scanner.isAtEnd { - if hasDate { - return DatabaseDateComponents(dateComponents, format: .YMD_HM) - } else { - return DatabaseDateComponents(dateComponents, format: .HM) - } - } - - // : - if !scanner.scanString(":", into: nil) { - return nil - } - - // SS - var second: Int = 0 - if scanner.scanInt(&second) && second >= 0 && second <= 59 { - dateComponents.second = second - } else { - return nil - } - - // [YYYY-MM-DD] HH:MM:SS - if scanner.isAtEnd { - if hasDate { - return DatabaseDateComponents(dateComponents, format: .YMD_HMS) - } else { - return DatabaseDateComponents(dateComponents, format: .HMS) - } - } - - // . - if !scanner.scanString(".", into: nil) { - return nil - } - - // SSS - var millisecondDigits: NSString? = nil - if scanner.scanCharacters(from: .decimalDigits, into: &millisecondDigits), var millisecondDigits = millisecondDigits { - if millisecondDigits.length > 3 { - millisecondDigits = NSString(string: millisecondDigits.substring(to: 3)) - } - dateComponents.nanosecond = millisecondDigits.integerValue * 1_000_000 - } else { - return nil - } - - // [YYYY-MM-DD] HH:MM:SS.SSS - if scanner.isAtEnd { - if hasDate { - return DatabaseDateComponents(dateComponents, format: .YMD_HMSS) - } else { - return DatabaseDateComponents(dateComponents, format: .HMSS) - } - } - // Unknown format - return nil + return SQLiteDateParser().components(from: string) } } From 85fbf64068ecc1a21390ec283711c5148ed93b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 19:17:24 +0200 Subject: [PATCH 28/34] Add missing imports (should fix travis issues) --- GRDB/Core/Support/Foundation/DatabaseDateComponents.swift | 5 +++++ GRDB/Core/Support/Foundation/Date.swift | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 700dcae0d7..7d2db6f00b 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -1,4 +1,9 @@ import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif /// DatabaseDateComponents reads and stores DateComponents in the database. public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumnConvertible { diff --git a/GRDB/Core/Support/Foundation/Date.swift b/GRDB/Core/Support/Foundation/Date.swift index 51ee8eb77e..78cdbbd30f 100644 --- a/GRDB/Core/Support/Foundation/Date.swift +++ b/GRDB/Core/Support/Foundation/Date.swift @@ -1,4 +1,9 @@ import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif /// NSDate is stored in the database using the format /// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone. From 9efe4a6efadc22c0947a6d153e81c17db4e0687e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 19:18:46 +0200 Subject: [PATCH 29/34] Add a sanity check --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index d434ba2da3..98d934b967 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -21,6 +21,8 @@ class SQLiteDateParser { } func components(cString: UnsafePointer, length: Int) -> DatabaseDateComponents? { + assert(strlen(cString) == length) + guard length >= 5 else { return nil } if cString.advanced(by: 4).pointee == 45 /* '-' */ { From 316d28746bdab05cdc2669228a0a3a3070dcb3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 19:26:42 +0200 Subject: [PATCH 30/34] SQLiteDateParser: optimize nanoseconds parsing --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 98d934b967..0d8949e64a 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -125,9 +125,8 @@ class SQLiteDateParser { } private func nanosecondsInt(for nanosecond: ContiguousArray) -> Int { - // truncate the nanosecond fraction - let nanoString = ("0." + nanosecond.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }).prefix(5) - guard let doubleValue = Double(nanoString) else { return 0 } - return Int(doubleValue * 1_000_000_000) + return 1_000_000 * nanosecond.prefix(3).reduce(0) { (r, char) in + r * 10 + Int(char) - 48 /* '0' */ + } } } From 632bce55215018c37744007b0d4b16df10fad17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 20 Apr 2018 19:41:34 +0200 Subject: [PATCH 31/34] SQLiteDateParser: don't parse garbage into nanoseconds --- .../Core/Support/Foundation/SQLiteDateParser.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 0d8949e64a..1f78d53b03 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -119,14 +119,20 @@ class SQLiteDateParser { guard parseCount >= 4 else { return DatabaseDateComponents(components, format: .HMS) } - components.nanosecond = nanosecondsInt(for: parserComponents.nanosecond) + guard let nanoseconds = nanosecondsInt(for: parserComponents.nanosecond) else { return nil } + components.nanosecond = nanoseconds return DatabaseDateComponents(components, format: .HMSS) } - private func nanosecondsInt(for nanosecond: ContiguousArray) -> Int { - return 1_000_000 * nanosecond.prefix(3).reduce(0) { (r, char) in - r * 10 + Int(char) - 48 /* '0' */ + private func nanosecondsInt(for nanosecond: ContiguousArray) -> Int? { + // truncate after the third digit + var result = 0 + for char in nanosecond.prefix(3) { + let digit = Int(char) - 48 /* '0' */ + guard (0...9).contains(digit) else { return nil } + result = result * 10 + digit } + return 1_000_000 * result } } From 219f96a11a907d2f29ca1a3ace3e6f2f3eb895ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Apr 2018 10:16:49 +0200 Subject: [PATCH 32/34] More DatabaseDateComponents parsing tests --- .../FoundationDateComponentsTests.swift | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/Tests/GRDBTests/FoundationDateComponentsTests.swift b/Tests/GRDBTests/FoundationDateComponentsTests.swift index 7edceb2707..417a2af692 100644 --- a/Tests/GRDBTests/FoundationDateComponentsTests.swift +++ b/Tests/GRDBTests/FoundationDateComponentsTests.swift @@ -354,6 +354,156 @@ class FoundationDateComponentsTests : GRDBTestCase { } } + func testDatabaseDateComponentsParsing() { + func assertParse(_ string: String, _ dateComponent: DatabaseDateComponents, file: StaticString = #file, line: UInt = #line) { + do { + // Test DatabaseValueConvertible adoption + let parsed = DatabaseDateComponents.fromDatabaseValue(string.databaseValue)! + 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, "SELECT ?", arguments: [string])! + } + XCTAssertEqual(parsed.format, dateComponent.format, file: file, line: line) + XCTAssertEqual(parsed.dateComponents, dateComponent.dateComponents, file: file, line: line) + } + } + + assertParse( + "0000-01-01", + DatabaseDateComponents( + DateComponents(year: 0, month: 1, day: 1, hour: nil, minute: nil, second: nil, nanosecond: nil), + format: .YMD)!) + assertParse( + "2018-12-31", + DatabaseDateComponents( + DateComponents(year: 2018, month: 12, day: 31, hour: nil, minute: nil, second: nil, nanosecond: nil), + format: .YMD)!) + assertParse( + "2018-04-21 00:00", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil), + format: .YMD_HM)!) + assertParse( + "2018-04-21T23:59", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: nil, nanosecond: nil), + format: .YMD_HM)!) + assertParse( + "2018-04-21 00:00:00", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: nil), + format: .YMD_HMS)!) + assertParse( + "2018-04-21T23:59:59", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: nil), + format: .YMD_HMS)!) + assertParse( + "2018-04-21 00:00:00.0", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 0), + format: .YMD_HMSS)!) + assertParse( + "2018-04-21T23:59:59.9", + DatabaseDateComponents( + DateComponents(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( + DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 0), + format: .YMD_HMSS)!) + assertParse( + "2018-04-21 00:00:00.01", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 10_000_000), + format: .YMD_HMSS)!) + assertParse( + "2018-04-21T23:59:59.99", + DatabaseDateComponents( + DateComponents(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( + DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 0), + format: .YMD_HMSS)!) + assertParse( + "2018-04-21 00:00:00.001", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 1_000_000), + format: .YMD_HMSS)!) + assertParse( + "2018-04-21T23:59:59.999", + DatabaseDateComponents( + DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .YMD_HMSS)!) + assertParse( + "00:00", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: nil, nanosecond: nil), + format: .HM)!) + assertParse( + "23:59", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil), + format: .HM)!) + assertParse( + "00:00:00", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: nil), + format: .HMS)!) + assertParse( + "23:59:59", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: nil), + format: .HMS)!) + assertParse( + "00:00:00.0", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 0), + format: .HMSS)!) + assertParse( + "23:59:59.9", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000), + format: .HMSS)!) + assertParse( + "00:00:00.00", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 0), + format: .HMSS)!) + assertParse( + "00:00:00.01", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 10_000_000), + format: .HMSS)!) + assertParse( + "23:59:59.99", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000), + format: .HMSS)!) + assertParse( + "00:00:00.000", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 0), + format: .HMSS)!) + assertParse( + "00:00:00.001", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 1_000_000), + format: .HMSS)!) + assertParse( + "23:59:59.999", + DatabaseDateComponents( + DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000), + format: .HMSS)!) + } + func testDatabaseDateComponentsFromUnparsableString() { let databaseDateComponents = DatabaseDateComponents.fromDatabaseValue("foo".databaseValue) XCTAssertTrue(databaseDateComponents == nil) From 1311f6c87c7482645edc0986fbcfa958f5338505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Apr 2018 10:17:07 +0200 Subject: [PATCH 33/34] SQLiteDateParser: fix nanosecond parsing --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 1f78d53b03..591d3c1275 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -128,11 +128,13 @@ class SQLiteDateParser { private func nanosecondsInt(for nanosecond: ContiguousArray) -> Int? { // truncate after the third digit var result = 0 - for char in nanosecond.prefix(3) { + let multipliers = [100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1] + for (char, multiplier) in zip(nanosecond.prefix(3), multipliers) { + if char == 0 { return result } let digit = Int(char) - 48 /* '0' */ guard (0...9).contains(digit) else { return nil } - result = result * 10 + digit + result += multiplier * digit } - return 1_000_000 * result + return result } } From 6c5d6ba6311c20537c7285ee7ff88676465a6283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Apr 2018 10:37:03 +0200 Subject: [PATCH 34/34] SQLiteDateParser: tighten vsscanf format --- GRDB/Core/Support/Foundation/SQLiteDateParser.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift index 591d3c1275..8ac84c250d 100644 --- a/GRDB/Core/Support/Foundation/SQLiteDateParser.swift +++ b/GRDB/Core/Support/Foundation/SQLiteDateParser.swift @@ -55,7 +55,7 @@ class SQLiteDateParser { withUnsafeMutablePointer(to: &parserComponents.second) { secondP in parserComponents.nanosecond.withUnsafeMutableBufferPointer { nanosecondBuffer in withVaList([yearP, monthP, dayP, hourP, minuteP, secondP, nanosecondBuffer.baseAddress!]) { pointer in - vsscanf(cString, "%d-%d-%d%*c%d:%d:%d.%10s", pointer) + vsscanf(cString, "%4d-%2d-%2d%*1[ T]%2d:%2d:%2d.%10s", pointer) } } } @@ -100,7 +100,7 @@ class SQLiteDateParser { withUnsafeMutablePointer(to: &parserComponents.second) { secondP in parserComponents.nanosecond.withUnsafeMutableBufferPointer { nanosecondBuffer in withVaList([hourP, minuteP, secondP, nanosecondBuffer.baseAddress!]) { pointer in - vsscanf(cString, "%d:%d:%d.%10s", pointer) + vsscanf(cString, "%2d:%2d:%2d.%10s", pointer) } } }