Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift
T
2025-01-28 12:28:03 +08:00

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")
}
}
}
}
}