494 lines
21 KiB
Swift
494 lines
21 KiB
Swift
import Foundation
|
|
import XCTest
|
|
@testable import GRDB // TODO: remove @testable when RowDecodingError is public
|
|
|
|
private protocol StrategyProvider {
|
|
static var strategy: DatabaseDateDecodingStrategy { get }
|
|
}
|
|
|
|
private enum StrategyDeferredToDate: StrategyProvider {
|
|
static let strategy: DatabaseDateDecodingStrategy = .deferredToDate
|
|
}
|
|
|
|
private enum StrategyTimeIntervalSinceReferenceDate: StrategyProvider {
|
|
static let strategy: DatabaseDateDecodingStrategy = .timeIntervalSinceReferenceDate
|
|
}
|
|
|
|
private enum StrategyTimeIntervalSince1970: StrategyProvider {
|
|
static let strategy: DatabaseDateDecodingStrategy = .timeIntervalSince1970
|
|
}
|
|
|
|
private enum StrategyMillisecondsSince1970: StrategyProvider {
|
|
static let strategy: DatabaseDateDecodingStrategy = .millisecondsSince1970
|
|
}
|
|
|
|
private enum StrategyIso8601: StrategyProvider {
|
|
static let strategy: DatabaseDateDecodingStrategy = .iso8601
|
|
}
|
|
|
|
private enum StrategyFormatted: StrategyProvider {
|
|
static let strategy: DatabaseDateDecodingStrategy = .formatted({
|
|
let formatter = DateFormatter()
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
formatter.timeZone = TimeZone(secondsFromGMT: 0)!
|
|
formatter.dateStyle = .full
|
|
formatter.timeStyle = .full
|
|
return formatter
|
|
}())
|
|
}
|
|
|
|
private enum StrategyCustom: StrategyProvider {
|
|
static let strategy: DatabaseDateDecodingStrategy = .custom { dbValue in
|
|
if dbValue == "invalid".databaseValue {
|
|
return nil
|
|
}
|
|
return Date(timeIntervalSinceReferenceDate: 123456)
|
|
}
|
|
}
|
|
|
|
private struct RecordWithDate<Strategy: StrategyProvider>: FetchableRecord, Decodable {
|
|
static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { Strategy.strategy }
|
|
var date: Date
|
|
}
|
|
|
|
private struct RecordWithOptionalDate<Strategy: StrategyProvider>: FetchableRecord, Decodable {
|
|
static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { Strategy.strategy }
|
|
var date: Date?
|
|
}
|
|
|
|
class DatabaseDateDecodingStrategyTests: GRDBTestCase {
|
|
/// test the conversion from a database value to a date extracted from a record
|
|
private func test<T: FetchableRecord>(
|
|
_ db: Database,
|
|
record: T.Type,
|
|
date: (T) -> Date?,
|
|
databaseValue: (any DatabaseValueConvertible)?,
|
|
with test: (Date?) -> Void) throws
|
|
{
|
|
let request = SQLRequest<Void>(sql: "SELECT ? AS date", arguments: [databaseValue])
|
|
do {
|
|
// test decoding straight from SQLite
|
|
let record = try T.fetchOne(db, request)!
|
|
test(date(record))
|
|
}
|
|
do {
|
|
// test decoding from copied row
|
|
let record = try T(row: Row.fetchOne(db, request)!)
|
|
test(date(record))
|
|
}
|
|
}
|
|
|
|
/// test the conversion from a database value to a date with a given strategy
|
|
private func test<Strategy: StrategyProvider>(
|
|
_ db: Database,
|
|
strategy: Strategy.Type,
|
|
databaseValue: some DatabaseValueConvertible,
|
|
_ test: (Date) -> Void)
|
|
throws
|
|
{
|
|
try self.test(db, record: RecordWithDate<Strategy>.self, date: { $0.date }, databaseValue: databaseValue, with: { test($0!) })
|
|
try self.test(db, record: RecordWithOptionalDate<Strategy>.self, date: { $0.date }, databaseValue: databaseValue, with: { test($0!) })
|
|
}
|
|
|
|
private func testNullDecoding<Strategy: StrategyProvider>(_ db: Database, strategy: Strategy.Type) throws {
|
|
try self.test(db, record: RecordWithOptionalDate<Strategy>.self, date: { $0.date }, databaseValue: nil) { date in
|
|
XCTAssertNil(date)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - deferredToDate
|
|
|
|
extension DatabaseDateDecodingStrategyTests {
|
|
func testDeferredToDate() throws {
|
|
try makeDatabaseQueue().read { db in
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
|
|
|
// Null
|
|
try testNullDecoding(db, strategy: StrategyDeferredToDate.self)
|
|
|
|
// YMD
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "2015-07-22") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 0)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 0)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 0)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// YMD_HM
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "2015-07-22 01:02") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 1)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 2)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 0)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// YMD_HMS
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "2015-07-22 01:02:03") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 1)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 2)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 3)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// YMD_HMSS
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "2015-07-22 01:02:03.00456") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
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.
|
|
}
|
|
|
|
// Timestamp
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: 1437526920) { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 1)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 2)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 0)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// Iso8601YMD_HM
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "2015-07-22T01:02") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 1)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 2)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 0)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// Iso8601YMD_HMS
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "2015-07-22T01:02:03") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 1)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 2)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 3)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// Iso8601YMD_HMSS
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "2015-07-22T01:02:03.00456") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2015)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 22)
|
|
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.
|
|
}
|
|
|
|
// error
|
|
do {
|
|
try test(db, strategy: StrategyDeferredToDate.self, databaseValue: "Yesterday") { date in
|
|
XCTFail("Unexpected Date")
|
|
}
|
|
} catch let error as RowDecodingError {
|
|
switch error {
|
|
case .valueMismatch:
|
|
XCTAssertEqual(error.description, """
|
|
could not decode Date from database value "Yesterday" - \
|
|
column: "date", \
|
|
column index: 0, \
|
|
row: [date:"Yesterday"], \
|
|
sql: `SELECT ? AS date`, \
|
|
arguments: ["Yesterday"]
|
|
""")
|
|
default:
|
|
XCTFail("Unexpected Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - timeIntervalSinceReferenceDate
|
|
|
|
extension DatabaseDateDecodingStrategyTests {
|
|
func testTimeIntervalSinceReferenceDate() throws {
|
|
try makeDatabaseQueue().read { db in
|
|
// Null
|
|
try testNullDecoding(db, strategy: StrategyTimeIntervalSinceReferenceDate.self)
|
|
|
|
// 0
|
|
try test(db, strategy: StrategyTimeIntervalSinceReferenceDate.self, databaseValue: 0) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSinceReferenceDate: 0))
|
|
}
|
|
|
|
// 123.456
|
|
try test(db, strategy: StrategyTimeIntervalSinceReferenceDate.self, databaseValue: 123.456) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSinceReferenceDate: 123.456))
|
|
}
|
|
|
|
// error
|
|
do {
|
|
try test(db, strategy: StrategyTimeIntervalSinceReferenceDate.self, databaseValue: "Yesterday") { date in
|
|
// Decoding from SQLite statement works:
|
|
// "Yesterday" is decoded as 0.
|
|
XCTAssertEqual(date, Date(timeIntervalSinceReferenceDate: 0))
|
|
}
|
|
} catch let error as RowDecodingError {
|
|
// Decoding from DatabaseValue does not work:
|
|
// "Yesterday" is not decoded as 0.
|
|
switch error {
|
|
case .valueMismatch:
|
|
XCTAssertEqual(error.description, """
|
|
could not decode Date from database value "Yesterday" - \
|
|
column: "date", \
|
|
column index: 0, \
|
|
row: [date:"Yesterday"]
|
|
""")
|
|
default:
|
|
XCTFail("Unexpected Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - timeIntervalSince1970
|
|
|
|
extension DatabaseDateDecodingStrategyTests {
|
|
func testTimeIntervalSince1970() throws {
|
|
try makeDatabaseQueue().read { db in
|
|
// Null
|
|
try testNullDecoding(db, strategy: StrategyTimeIntervalSince1970.self)
|
|
|
|
// 0
|
|
try test(db, strategy: StrategyTimeIntervalSince1970.self, databaseValue: 0) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 0))
|
|
}
|
|
|
|
// 123.456
|
|
try test(db, strategy: StrategyTimeIntervalSince1970.self, databaseValue: 123.456) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 123.456))
|
|
}
|
|
|
|
// error
|
|
do {
|
|
try test(db, strategy: StrategyTimeIntervalSince1970.self, databaseValue: "Yesterday") { date in
|
|
// Decoding from SQLite statement works:
|
|
// "Yesterday" is decoded as 0.
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 0))
|
|
}
|
|
} catch let error as RowDecodingError {
|
|
// Decoding from DatabaseValue does not work:
|
|
// "Yesterday" is not decoded as 0.
|
|
switch error {
|
|
case .valueMismatch:
|
|
XCTAssertEqual(error.description, """
|
|
could not decode Date from database value "Yesterday" - \
|
|
column: "date", \
|
|
column index: 0, \
|
|
row: [date:"Yesterday"]
|
|
""")
|
|
default:
|
|
XCTFail("Unexpected Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - millisecondsSince1970
|
|
|
|
extension DatabaseDateDecodingStrategyTests {
|
|
func testMillisecondsSince1970() throws {
|
|
try makeDatabaseQueue().read { db in
|
|
// Null
|
|
try testNullDecoding(db, strategy: StrategyMillisecondsSince1970.self)
|
|
|
|
// 0
|
|
try test(db, strategy: StrategyMillisecondsSince1970.self, databaseValue: 0) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 0))
|
|
}
|
|
|
|
// 123.456
|
|
try test(db, strategy: StrategyMillisecondsSince1970.self, databaseValue: 123.456) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 123.456 / 1000 ))
|
|
}
|
|
|
|
// 123456
|
|
try test(db, strategy: StrategyMillisecondsSince1970.self, databaseValue: 123456) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 123456.0 / 1000))
|
|
}
|
|
|
|
// 123456.789
|
|
try test(db, strategy: StrategyMillisecondsSince1970.self, databaseValue: 123456.789) { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 123456.789 / 1000))
|
|
}
|
|
|
|
// error
|
|
do {
|
|
try test(db, strategy: StrategyMillisecondsSince1970.self, databaseValue: "Yesterday") { date in
|
|
// Decoding from SQLite statement works:
|
|
// "Yesterday" is decoded as 0.
|
|
XCTAssertEqual(date, Date(timeIntervalSince1970: 0))
|
|
}
|
|
} catch let error as RowDecodingError {
|
|
// Decoding from DatabaseValue does not work:
|
|
// "Yesterday" is not decoded as 0.
|
|
switch error {
|
|
case .valueMismatch:
|
|
XCTAssertEqual(error.description, """
|
|
could not decode Date from database value "Yesterday" - \
|
|
column: "date", \
|
|
column index: 0, \
|
|
row: [date:"Yesterday"]
|
|
""")
|
|
default:
|
|
XCTFail("Unexpected Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - iso8601(ISO8601DateFormatter)
|
|
|
|
extension DatabaseDateDecodingStrategyTests {
|
|
func testIso8601() throws {
|
|
// check ISO8601DateFormatter availabiliity
|
|
try makeDatabaseQueue().read { db in
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
|
|
|
// Null
|
|
try testNullDecoding(db, strategy: StrategyIso8601.self)
|
|
|
|
// Date
|
|
try test(db, strategy: StrategyIso8601.self, databaseValue: "2018-08-19T17:18:07Z") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2018)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 8)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 19)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 17)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 18)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 7)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// error
|
|
do {
|
|
try test(db, strategy: StrategyIso8601.self, databaseValue: "Yesterday") { date in
|
|
XCTFail("Unexpected Date")
|
|
}
|
|
} catch let error as RowDecodingError {
|
|
switch error {
|
|
case .valueMismatch:
|
|
XCTAssertEqual(error.description, """
|
|
could not decode Date from database value "Yesterday" - \
|
|
column: "date", \
|
|
column index: 0, \
|
|
row: [date:"Yesterday"], \
|
|
sql: `SELECT ? AS date`, \
|
|
arguments: ["Yesterday"]
|
|
""")
|
|
default:
|
|
XCTFail("Unexpected Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - formatted(DateFormatter)
|
|
|
|
extension DatabaseDateDecodingStrategyTests {
|
|
func testFormatted() throws {
|
|
try makeDatabaseQueue().read { db in
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
|
|
|
// Null
|
|
try testNullDecoding(db, strategy: StrategyFormatted.self)
|
|
|
|
// Date
|
|
try test(db, strategy: StrategyFormatted.self, databaseValue: "Sunday, August 19, 2018 at 5:21:55 PM Greenwich Mean Time") { date in
|
|
XCTAssertEqual(calendar.component(.year, from: date), 2018)
|
|
XCTAssertEqual(calendar.component(.month, from: date), 8)
|
|
XCTAssertEqual(calendar.component(.day, from: date), 19)
|
|
XCTAssertEqual(calendar.component(.hour, from: date), 17)
|
|
XCTAssertEqual(calendar.component(.minute, from: date), 21)
|
|
XCTAssertEqual(calendar.component(.second, from: date), 55)
|
|
XCTAssertEqual(calendar.component(.nanosecond, from: date), 0)
|
|
}
|
|
|
|
// error
|
|
do {
|
|
try test(db, strategy: StrategyFormatted.self, databaseValue: "Yesterday") { date in
|
|
XCTFail("Unexpected Date")
|
|
}
|
|
} catch let error as RowDecodingError {
|
|
switch error {
|
|
case .valueMismatch:
|
|
XCTAssertEqual(error.description, """
|
|
could not decode Date from database value "Yesterday" - \
|
|
column: "date", \
|
|
column index: 0, \
|
|
row: [date:"Yesterday"], \
|
|
sql: `SELECT ? AS date`, \
|
|
arguments: ["Yesterday"]
|
|
""")
|
|
default:
|
|
XCTFail("Unexpected Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - custom((DatabaseValue) -> Date?
|
|
|
|
extension DatabaseDateDecodingStrategyTests {
|
|
func testCustom() throws {
|
|
try makeDatabaseQueue().read { db in
|
|
// Null
|
|
try testNullDecoding(db, strategy: StrategyCustom.self)
|
|
|
|
// Date
|
|
try test(db, strategy: StrategyCustom.self, databaseValue: "valid") { date in
|
|
XCTAssertEqual(date, Date(timeIntervalSinceReferenceDate: 123456))
|
|
}
|
|
|
|
// error
|
|
do {
|
|
try test(db, strategy: StrategyCustom.self, databaseValue: "invalid") { date in
|
|
XCTFail("Unexpected Date")
|
|
}
|
|
} catch let error as RowDecodingError {
|
|
switch error {
|
|
case .valueMismatch:
|
|
XCTAssertEqual(error.description, """
|
|
could not decode Date from database value "invalid" - \
|
|
column: "date", \
|
|
column index: 0, \
|
|
row: [date:"invalid"], \
|
|
sql: `SELECT ? AS date`, \
|
|
arguments: ["invalid"]
|
|
""")
|
|
default:
|
|
XCTFail("Unexpected Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|