Files

1983 lines
73 KiB
Swift

import Foundation
import XCTest
@testable import GRDB
class FetchableRecordDecodableTests: GRDBTestCase { }
// MARK: - FetchableRecord conformance derived from Decodable
extension FetchableRecordDecodableTests {
func testTrivialDecodable() throws {
struct Struct : FetchableRecord, Decodable {
let value: String
}
do {
let s = try Struct(row: ["value": "foo"])
XCTAssertEqual(s.value, "foo")
}
}
func testCustomDecodable() throws {
struct Struct : FetchableRecord, Decodable {
let value: String
private enum CodingKeys : String, CodingKey {
case value = "someColumn"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
value = try container.decode(String.self, forKey: .value)
}
}
do {
let s = try Struct(row: ["someColumn": "foo"])
XCTAssertEqual(s.value, "foo")
}
}
func testCustomFetchableRecord() {
struct Struct : FetchableRecord, Decodable {
let value: String
init(row: Row) {
value = (row["value"] as String) + " (FetchableRecord)"
}
}
do {
let s = Struct(row: ["value": "foo"])
XCTAssertEqual(s.value, "foo (FetchableRecord)")
}
}
}
// MARK: - Different kinds of single-value properties
extension FetchableRecordDecodableTests {
func testTrivialProperty() throws {
struct Struct : FetchableRecord, Decodable {
let int64: Int64
let optionalInt64: Int64?
}
do {
// No null values
let s = try Struct(row: ["int64": 1, "optionalInt64": 2])
XCTAssertEqual(s.int64, 1)
XCTAssertEqual(s.optionalInt64, 2)
}
do {
// Null values
let s = try Struct(row: ["int64": 2, "optionalInt64": nil])
XCTAssertEqual(s.int64, 2)
XCTAssertNil(s.optionalInt64)
}
do {
// Missing and extra values
let s = try Struct(row: ["int64": 3, "ignored": "?"])
XCTAssertEqual(s.int64, 3)
XCTAssertNil(s.optionalInt64)
}
}
func testTrivialSingleValueDecodableProperty() throws {
struct Value : Decodable {
let string: String
init(from decoder: Decoder) throws {
string = try decoder.singleValueContainer().decode(String.self)
}
}
struct Struct : FetchableRecord, Decodable {
let value: Value
let optionalValue: Value?
}
do {
// No null values
let s = try Struct(row: ["value": "foo", "optionalValue": "bar"])
XCTAssertEqual(s.value.string, "foo")
XCTAssertEqual(s.optionalValue!.string, "bar")
}
do {
// Null values
let s = try Struct(row: ["value": "foo", "optionalValue": nil])
XCTAssertEqual(s.value.string, "foo")
XCTAssertNil(s.optionalValue)
}
do {
// Missing and extra values
let s = try Struct(row: ["value": "foo", "ignored": "?"])
XCTAssertEqual(s.value.string, "foo")
XCTAssertNil(s.optionalValue)
}
}
func testNonTrivialSingleValueDecodableProperty() throws {
struct NestedValue : Decodable {
let string: String
init(from decoder: Decoder) throws {
string = try decoder.singleValueContainer().decode(String.self)
}
}
struct Value : Decodable {
let nestedValue: NestedValue
init(from decoder: Decoder) throws {
nestedValue = try decoder.singleValueContainer().decode(NestedValue.self)
}
}
struct Struct : FetchableRecord, Decodable {
let value: Value
let optionalValue: Value?
}
do {
// No null values
let s = try Struct(row: ["value": "foo", "optionalValue": "bar"])
XCTAssertEqual(s.value.nestedValue.string, "foo")
XCTAssertEqual(s.optionalValue!.nestedValue.string, "bar")
}
do {
// Null values
let s = try Struct(row: ["value": "foo", "optionalValue": nil])
XCTAssertEqual(s.value.nestedValue.string, "foo")
XCTAssertNil(s.optionalValue)
}
do {
// Missing and extra values
let s = try Struct(row: ["value": "foo", "ignored": "?"])
XCTAssertEqual(s.value.nestedValue.string, "foo")
XCTAssertNil(s.optionalValue)
}
}
func testDecodableRawRepresentableProperty() throws {
// This test is somewhat redundant with testSingleValueDecodableProperty,
// since a RawRepresentable enum is a "single-value" Decodable.
//
// But with an explicit test for enums, we are sure that enums, widely
// used, are supported.
enum Value : String, Decodable {
case foo, bar
}
struct Struct : FetchableRecord, Decodable {
let value: Value
let optionalValue: Value?
}
do {
// No null values
let s = try Struct(row: ["value": "foo", "optionalValue": "bar"])
XCTAssertEqual(s.value, .foo)
XCTAssertEqual(s.optionalValue!, .bar)
}
do {
// Null values
let s = try Struct(row: ["value": "foo", "optionalValue": nil])
XCTAssertEqual(s.value, .foo)
XCTAssertNil(s.optionalValue)
}
do {
// Missing and extra values
let s = try Struct(row: ["value": "foo", "ignored": "?"])
XCTAssertEqual(s.value, .foo)
XCTAssertNil(s.optionalValue)
}
}
func testDatabaseValueConvertibleProperty() throws {
// This test makes sure that Date, for example, can be read from a String.
//
// Without this preference for fromDatabaseValue(_:) over init(from:Decoder),
// Date would only decode from doubles.
struct Value : Decodable, DatabaseValueConvertible {
let string: String
init(string: String) {
self.string = string
}
init(from decoder: Decoder) throws {
string = try decoder.singleValueContainer().decode(String.self) + " (Decodable)"
}
// DatabaseValueConvertible adoption
var databaseValue: DatabaseValue { fatalError("irrelevant") }
static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? {
if let string = String.fromDatabaseValue(databaseValue) {
return Value(string: string + " (DatabaseValueConvertible)")
} else {
return nil
}
}
}
struct Struct : FetchableRecord, Decodable {
let value: Value
let optionalValue: Value?
}
do {
// No null values
let s = try Struct(row: ["value": "foo", "optionalValue": "bar"])
XCTAssertEqual(s.value.string, "foo (DatabaseValueConvertible)")
XCTAssertEqual(s.optionalValue!.string, "bar (DatabaseValueConvertible)")
}
do {
// Null values
let s = try Struct(row: ["value": "foo", "optionalValue": nil])
XCTAssertEqual(s.value.string, "foo (DatabaseValueConvertible)")
XCTAssertNil(s.optionalValue)
}
do {
// Missing and extra values
let s = try Struct(row: ["value": "foo", "ignored": "?"])
XCTAssertEqual(s.value.string, "foo (DatabaseValueConvertible)")
XCTAssertNil(s.optionalValue)
}
}
}
// MARK: - Foundation Codable Types
extension FetchableRecordDecodableTests {
func testStructWithData() throws {
struct StructWithData : FetchableRecord, Decodable {
let data: Data
}
let dbQueue = try makeDatabaseQueue()
do {
let data = "foo".data(using: .utf8)
do {
let value = try StructWithData(row: ["data": data])
XCTAssertEqual(value.data, data)
}
do {
let value = try dbQueue.read {
try StructWithData.fetchOne($0, sql: "SELECT ? AS data", arguments: [data])!
}
XCTAssertEqual(value.data, data)
}
}
do {
do {
_ = try StructWithData(row: ["data": nil])
XCTFail("Expected Error")
} catch let error as RowDecodingError {
switch error {
case .valueMismatch:
XCTAssertEqual(error.description, """
could not decode Data from database value NULL - \
column: "data", \
column index: 0, \
row: [data:NULL]
""")
default:
XCTFail("Unexpected Error")
}
}
do {
try dbQueue.read {
_ = try StructWithData.fetchOne($0, sql: "SELECT NULL AS data")
}
XCTFail("Expected Error")
} catch let error as RowDecodingError {
switch error {
case .valueMismatch:
XCTAssertEqual(error.description, """
could not decode Data from database value NULL - \
column: "data", \
column index: 0, \
row: [data:NULL], \
sql: `SELECT NULL AS data`, \
arguments: []
""")
default:
XCTFail("Unexpected Error")
}
}
}
}
func testStructWithDate() throws {
struct StructWithDate : FetchableRecord, Decodable {
let date: Date
}
let dbQueue = try makeDatabaseQueue()
do {
let date = Date()
do {
let value = try StructWithDate(row: ["date": date])
XCTAssert(abs(value.date.timeIntervalSince(date)) < 0.001)
}
do {
let value = try dbQueue.read {
try StructWithDate.fetchOne($0, sql: "SELECT ? AS date", arguments: [date])!
}
XCTAssert(abs(value.date.timeIntervalSince(date)) < 0.001)
}
}
do {
do {
_ = try StructWithDate(row: ["date": nil])
XCTFail("Expected Error")
} catch let error as RowDecodingError {
switch error {
case .valueMismatch:
XCTAssertEqual(error.description, """
could not decode Date from database value NULL - \
column: "date", \
column index: 0, \
row: [date:NULL]
""")
default:
XCTFail("Unexpected Error")
}
}
do {
try dbQueue.read {
_ = try StructWithDate.fetchOne($0, sql: "SELECT NULL AS date")
}
XCTFail("Expected Error")
} catch let error as RowDecodingError {
switch error {
case .valueMismatch:
XCTAssertEqual(error.description, """
could not decode Date from database value NULL - \
column: "date", \
column index: 0, \
row: [date:NULL], \
sql: `SELECT NULL AS date`, \
arguments: []
""")
default:
XCTFail("Unexpected Error")
}
}
}
}
func testStructWithURL() throws {
struct StructWithURL : FetchableRecord, Decodable {
let url: URL
}
let url = URL(string: "https://github.com")
let value = try StructWithURL(row: ["url": url])
XCTAssertEqual(value.url, url)
}
func testStructWithUUID() throws {
struct StructWithUUID : FetchableRecord, Decodable {
let uuid: UUID
}
let uuid = UUID()
let value = try StructWithUUID(row: ["uuid": uuid])
XCTAssertEqual(value.uuid, uuid)
}
}
// MARK: - Custom nested Decodable types - nested saved as JSON
extension FetchableRecordDecodableTests {
func testOptionalNestedStruct() throws {
struct NestedStruct : Codable {
let firstName: String?
let lastName: String?
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let nested: NestedStruct?
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("nested", .text)
}
let nested = NestedStruct(firstName: "Bob", lastName: "Dylan")
let value = StructWithNestedType(nested: nested)
try value.insert(db)
let parentModel = try StructWithNestedType.fetchAll(db)
guard let nestedModel = parentModel.first?.nested else {
XCTFail()
return
}
// Check the nested model contains the expected values of first and last name
XCTAssertEqual(nestedModel.firstName, "Bob")
XCTAssertEqual(nestedModel.lastName, "Dylan")
}
}
func testOptionalNestedStructNil() throws {
struct NestedStruct : Codable {
let firstName: String?
let lastName: String?
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let nested: NestedStruct?
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("nested", .text)
}
let value = StructWithNestedType(nested: nil)
try value.insert(db)
let parentModel = try StructWithNestedType.fetchAll(db)
XCTAssertNil(parentModel.first?.nested)
}
}
func testOptionalNestedArrayStruct() throws {
struct NestedStruct : Codable {
let firstName: String?
let lastName: String?
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let nested: [NestedStruct]?
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("nested", .text)
}
let nested = NestedStruct(firstName: "Bob", lastName: "Dylan")
let value = StructWithNestedType(nested: [nested, nested])
try value.insert(db)
let parentModel = try StructWithNestedType.fetchAll(db)
guard let arrayOfNestedModel = parentModel.first?.nested, let firstNestedModelInArray = arrayOfNestedModel.first else {
XCTFail()
return
}
// Check there are two models in array
XCTAssertTrue(arrayOfNestedModel.count == 2)
// Check the nested model contains the expected values of first and last name
XCTAssertEqual(firstNestedModelInArray.firstName, "Bob")
XCTAssertEqual(firstNestedModelInArray.lastName, "Dylan")
}
}
func testOptionalNestedArrayStructNil() throws {
struct NestedStruct: Codable {
let firstName: String?
let lastName: String?
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let nested: [NestedStruct]?
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("nested", .text)
}
let value = StructWithNestedType(nested: nil)
try value.insert(db)
let parentModel = try StructWithNestedType.fetchAll(db)
XCTAssertNil(parentModel.first?.nested)
}
}
func testNonOptionalNestedStruct() throws {
struct NestedStruct: Codable {
let firstName: String?
let lastName: String?
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let nested: NestedStruct
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("nested", .text)
}
let nested = NestedStruct(firstName: "Bob", lastName: "Dylan")
let value = StructWithNestedType(nested: nested)
try value.insert(db)
let parentModel = try StructWithNestedType.fetchAll(db)
guard let nestedModel = parentModel.first?.nested else {
XCTFail()
return
}
// Check the nested model contains the expected values of first and last name
XCTAssertEqual(nestedModel.firstName, "Bob")
XCTAssertEqual(nestedModel.lastName, "Dylan")
}
}
func testNonOptionalNestedArrayStruct() throws {
struct NestedStruct : Codable {
let firstName: String?
let lastName: String?
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let nested: [NestedStruct]
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("nested", .text)
}
}
try dbQueue.inTransaction { db in
let value = StructWithNestedType(nested: [
NestedStruct(firstName: "Bob", lastName: "Dylan"),
NestedStruct(firstName: "Bob", lastName: "Dylan")])
try value.insert(db)
let parentModel = try StructWithNestedType.fetchOne(db)
guard let nested = parentModel?.nested else {
XCTFail()
return .rollback
}
// Check there are two models in array
XCTAssertEqual(nested.count, 2)
// Check the nested model contains the expected values of first and last name
XCTAssertEqual(nested[0].firstName, "Bob")
XCTAssertEqual(nested[0].lastName, "Dylan")
return .rollback
}
try dbQueue.inTransaction { db in
let value = StructWithNestedType(nested: [])
try value.insert(db)
let parentModel = try StructWithNestedType.fetchOne(db)
guard let nested = parentModel?.nested else {
XCTFail()
return .rollback
}
XCTAssertTrue(nested.isEmpty)
return .rollback
}
}
func testCodableExampleCode() throws {
struct Player: PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let name: String
let score: Int
let scores: [Int]
let lastMedal: PlayerMedal
let medals: [PlayerMedal]
let timeline: [String: PlayerMedal]
}
// A simple Codable that will be nested in a parent Codable
struct PlayerMedal : Codable {
let name: String?
let type: String?
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("name", .text)
t.column("score", .integer)
t.column("scores", .integer)
t.column("lastMedal", .text)
t.column("medals", .text)
t.column("timeline", .text)
}
let medal1 = PlayerMedal(name: "First", type: "Gold")
let medal2 = PlayerMedal(name: "Second", type: "Silver")
let timeline = ["Local Contest": medal1, "National Contest": medal2]
let value = Player(name: "PlayerName", score: 10, scores: [1,2,3,4,5], lastMedal: medal1, medals: [medal1, medal2], timeline: timeline)
try value.insert(db)
let parentModel = try Player.fetchAll(db)
guard let first = parentModel.first, let firstNestedModelInArray = first.medals.first, let secondNestedModelInArray = first.medals.last else {
XCTFail()
return
}
// Check there are two models in array
XCTAssertTrue(first.medals.count == 2)
// Check the nested model contains the expected values of first and last name
XCTAssertEqual(firstNestedModelInArray.name, "First")
XCTAssertEqual(secondNestedModelInArray.name, "Second")
XCTAssertEqual(first.name, "PlayerName")
XCTAssertEqual(first.score, 10)
XCTAssertEqual(first.scores, [1,2,3,4,5])
XCTAssertEqual(first.lastMedal.name, medal1.name)
XCTAssertEqual(first.timeline["Local Contest"]?.name, medal1.name)
XCTAssertEqual(first.timeline["National Contest"]?.name, medal2.name)
}
}
// MARK: - JSON data in Detached Rows
func testDetachedRows() throws {
struct NestedStruct : PersistableRecord, FetchableRecord, Codable {
let firstName: String?
let lastName: String?
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let nested: NestedStruct
}
let row: Row = ["nested": """
{"firstName":"Bob","lastName":"Dylan"}
"""]
let model = try StructWithNestedType(row: row)
XCTAssertEqual(model.nested.firstName, "Bob")
XCTAssertEqual(model.nested.lastName, "Dylan")
}
func testArrayOfDetachedRowsAsData() throws {
struct TestStruct : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let name: String
}
let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}"
let jsonAsData = jsonAsString.data(using: .utf8)
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("name", .text)
}
let model = TestStruct(name: jsonAsString)
try model.insert(db)
}
try dbQueue.read { db in
// ... with an array of detached rows:
let array = try Row.fetchAll(db, sql: "SELECT * FROM t1")
for row in array {
let data1: Data? = row["name"]
XCTAssertEqual(jsonAsData, data1)
try row.withUnsafeData(named: "name") { data in
XCTAssertEqual(jsonAsData, data)
}
}
}
}
func testArrayOfDetachedRowsAsString() throws {
struct TestStruct : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let name: String
}
let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}"
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("name", .text)
}
let model = TestStruct(name: jsonAsString)
try model.insert(db)
}
try dbQueue.read { db in
// ... with an array of detached rows:
let array = try Row.fetchAll(db, sql: "SELECT * FROM t1")
for row in array {
let string: String? = row["name"]
XCTAssertEqual(jsonAsString, string)
}
}
}
func testCursorRowsAsData() throws {
struct TestStruct : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let name: String
}
let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}"
let jsonAsData = jsonAsString.data(using: .utf8)
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("name", .text)
}
let model = TestStruct(name: jsonAsString)
try model.insert(db)
}
try dbQueue.read { db in
// Compare cursor of low-level rows:
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM t1")
while let row = try cursor.next() {
let data1: Data? = row["name"]
XCTAssertEqual(jsonAsData, data1)
try row.withUnsafeData(named: "name") { data in
XCTAssertEqual(jsonAsData, data)
}
}
}
}
func testCursorRowsAsString() throws {
struct TestStruct : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
let name: String
}
let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}"
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("name", .text)
}
let model = TestStruct(name: jsonAsString)
try model.insert(db)
}
try dbQueue.read { db in
// Compare cursor of low-level rows:
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM t1")
while let row = try cursor.next() {
let string: String? = row["name"]
XCTAssertEqual(jsonAsString, string)
}
}
}
func testJSONDataEncodingStrategy() throws {
struct Record: FetchableRecord, Decodable {
let data: Data
let optionalData: Data?
let datas: [Data]
let optionalDatas: [Data?]
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let data = "foo".data(using: .utf8)!
let record = try Record.fetchOne(db, sql: "SELECT ? AS data, ? AS optionalData, ? AS datas, ? AS optionalDatas", arguments: [
data,
data,
"[\"Zm9v\"]",
"[null, \"Zm9v\"]"
])!
XCTAssertEqual(record.data, data)
XCTAssertEqual(record.optionalData!, data)
XCTAssertEqual(record.datas.count, 1)
XCTAssertEqual(record.datas[0], data)
XCTAssertEqual(record.optionalDatas.count, 2)
XCTAssertNil(record.optionalDatas[0])
XCTAssertEqual(record.optionalDatas[1]!, data)
}
}
func testJSONDateEncodingStrategy() throws {
struct Record: FetchableRecord, Decodable {
let date: Date
let optionalDate: Date?
let dates: [Date]
let optionalDates: [Date?]
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let record = try Record.fetchOne(db, sql: "SELECT ? AS date, ? AS optionalDate, ? AS dates, ? AS optionalDates", arguments: [
"1970-01-01 00:02:08.000",
"1970-01-01 00:02:08.000",
"[128000]",
"[null,128000]"
])!
XCTAssertEqual(record.date.timeIntervalSince1970, 128)
XCTAssertEqual(record.optionalDate!.timeIntervalSince1970, 128)
XCTAssertEqual(record.dates.count, 1)
XCTAssertEqual(record.dates[0].timeIntervalSince1970, 128)
XCTAssertEqual(record.optionalDates.count, 2)
XCTAssertNil(record.optionalDates[0])
XCTAssertEqual(record.optionalDates[1]!.timeIntervalSince1970, 128)
}
}
// MARK: - DatabaseColumnDecodingStrategy
func testDatabaseColumnDecodingStrategy_useDefaultKeys() throws {
struct Record: FetchableRecord, Decodable, Equatable {
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.useDefaultKeys
let requiredId: Int
let optionalName: String?
let requiredDates: [Date]
let optionalDates: [Date?]
let optionalDates2: [Date]?
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"requiredId": 1,
"optionalName": "test1",
"requiredDates": "[128000]",
"optionalDates": "[null, 128000]",
"optionalDates2": "[128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: "test1",
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: [Date(timeIntervalSince1970: 128)]))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"REQUIREDID": 1,
"OPTIONALNAME": "test1",
"REQUIREDDATES": "[128000]",
"OPTIONALDATES": "[null, 128000]",
"OPTIONALDATES2": "[128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: "test1",
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: [Date(timeIntervalSince1970: 128)]))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"requiredId": 1,
"optionalName": nil,
"requiredDates": "[128000]",
"optionalDates": "[null, 128000]",
"optionalDates2": nil,
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: nil,
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: nil))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"requiredId": 1,
"requiredDates": "[128000]",
"optionalDates": "[null, 128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: nil,
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: nil))
}
do {
_ = try FetchableRecordDecoder().decode(Record.self, from: [
"required_id": 1,
"optionalName": "test1",
"requiredDates": "[128000]",
"optionalDates": "[null, 128000]",
"optionalDates2": "[128000]",
])
XCTFail("Expected error")
} catch let RowDecodingError.keyNotFound(.columnName(column), context) {
XCTAssertEqual(column, "requiredId")
XCTAssertEqual(context.debugDescription, "column not found: \"requiredId\"")
}
}
func testDatabaseColumnDecodingStrategy_convertFromSnakeCase() throws {
struct Record: FetchableRecord, Decodable, Equatable {
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase
let requiredId: Int
let optionalName: String?
let requiredDates: [Date]
let optionalDates: [Date?]
let optionalDates2: [Date]?
}
struct IncorrectRecord: FetchableRecord, Decodable, Equatable {
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase
let requiredID: Int // incorrect: should be requiredId
let optionalName: String?
let requiredDates: [Date]
let optionalDates: [Date?]
let optionalDates2: [Date]?
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"required_id": 1,
"optional_name": "test1",
"required_dates": "[128000]",
"optional_dates": "[null, 128000]",
"optional_dates2": "[128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: "test1",
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: [Date(timeIntervalSince1970: 128)]))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"REQUIRED_ID": 1,
"OPTIONAL_NAME": "test1",
"REQUIRED_DATES": "[128000]",
"OPTIONAL_DATES": "[null, 128000]",
"OPTIONAL_DATES2": "[128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: "test1",
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: [Date(timeIntervalSince1970: 128)]))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"required_id": 1,
"optional_name": nil,
"required_dates": "[128000]",
"optional_dates": "[null, 128000]",
"optional_dates2": nil,
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: nil,
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: nil))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"required_id": 1,
"required_dates": "[128000]",
"optional_dates": "[null, 128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: nil,
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: nil))
}
do {
// Matches JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase behavior
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"requiredId": 1,
"optionalName": "test1",
"requiredDates": "[128000]",
"optionalDates": "[null, 128000]",
"optionalDates2": "[128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: "test1",
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: [Date(timeIntervalSince1970: 128)]))
}
do {
_ = try FetchableRecordDecoder().decode(Record.self, from: [
"required_idx": 1,
"optional_name": "test1",
"required_dates": "[128000]",
"optional_dates": "[null, 128000]",
"optional_dates2": "[128000]",
])
XCTFail("Expected error")
} catch let RowDecodingError.keyNotFound(.columnName(column), context) {
XCTAssertEqual(column, "requiredId")
XCTAssertEqual(context.debugDescription, """
key not found: CodingKeys(stringValue: "requiredId", intValue: nil) ("requiredId"), \
converted to required_id
""")
}
do {
_ = try FetchableRecordDecoder().decode(IncorrectRecord.self, from: [
"required_id": 1,
"optional_name": "test1",
"required_dates": "[128000]",
"optional_dates": "[null, 128000]",
"optional_dates2": "[128000]",
])
XCTFail("Expected error")
} catch let RowDecodingError.keyNotFound(.columnName(column), context) {
XCTAssertEqual(column, "requiredID")
XCTAssertEqual(context.debugDescription, """
key not found: CodingKeys(stringValue: "requiredID", intValue: nil) ("requiredID"), \
with divergent representation requiredId, \
converted to required_id
""")
}
}
func testDatabaseColumnDecodingStrategy_custom() throws {
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int? { nil }
init(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { nil }
}
struct Record: FetchableRecord, Decodable, Equatable {
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.custom { column in
AnyKey(stringValue: String(column.dropFirst()))
}
let requiredId: Int
let optionalName: String?
let requiredDates: [Date]
let optionalDates: [Date?]
let optionalDates2: [Date]?
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"_requiredId": 1,
"_optionalName": "test1",
"_requiredDates": "[128000]",
"_optionalDates": "[null, 128000]",
"_optionalDates2": "[128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: "test1",
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: [Date(timeIntervalSince1970: 128)]))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"_requiredId": 1,
"_optionalName": nil,
"_requiredDates": "[128000]",
"_optionalDates": "[null, 128000]",
"_optionalDates2": nil,
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: nil,
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: nil))
}
do {
let record = try FetchableRecordDecoder().decode(Record.self, from: [
"_requiredId": 1,
"_requiredDates": "[128000]",
"_optionalDates": "[null, 128000]",
])
XCTAssertEqual(record, Record(
requiredId: 1,
optionalName: nil,
requiredDates: [Date(timeIntervalSince1970: 128)],
optionalDates: [nil, Date(timeIntervalSince1970: 128)],
optionalDates2: nil))
}
do {
_ = try FetchableRecordDecoder().decode(Record.self, from: [
"requiredId": 1,
"_optionalName": "test1",
"_requiredDates": "[128000]",
"_optionalDates": "[null, 128000]",
"_optionalDates2": "[128000]",
])
XCTFail("Expected error")
} catch let RowDecodingError.keyNotFound(.columnName(column), context) {
XCTAssertEqual(column, "requiredId")
XCTAssertEqual(context.debugDescription, """
key not found: CodingKeys(stringValue: "requiredId", intValue: nil) ("requiredId")
""")
}
}
}
// MARK: - User Infos & Coding Keys
private let testKeyRoot = CodingUserInfoKey(rawValue: "test1")!
private let testKeyNested = CodingUserInfoKey(rawValue: "test2")!
extension FetchableRecordDecodableTests {
struct NestedKeyed: Decodable {
var name: String
var key: String?
var context: String?
enum CodingKeys: String, CodingKey { case name }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
key = decoder.codingPath.last?.stringValue
context = decoder.userInfo[testKeyNested] as? String
}
}
struct NestedSingle: Decodable {
var name: String
var key: String?
var context: String?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
name = try container.decode(String.self)
key = decoder.codingPath.last?.stringValue
context = decoder.userInfo[testKeyNested] as? String
}
}
struct NestedUnkeyed: Decodable {
var name: String
var key: String?
var context: String?
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
name = try container.decode(String.self)
key = decoder.codingPath.last?.stringValue
context = decoder.userInfo[testKeyNested] as? String
}
}
struct Record: Decodable, FetchableRecord {
var nestedKeyed: NestedKeyed
var nestedSingle: NestedSingle
var nestedUnkeyed: NestedUnkeyed
var key: String?
var context: String?
enum CodingKeys: String, CodingKey {
case nestedKeyed, nestedSingle, nestedUnkeyed
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
nestedKeyed = try container.decode(NestedKeyed.self, forKey: .nestedKeyed)
nestedSingle = try container.decode(NestedSingle.self, forKey: .nestedSingle)
nestedUnkeyed = try container.decode(NestedUnkeyed.self, forKey: .nestedUnkeyed)
key = decoder.codingPath.last?.stringValue
context = decoder.userInfo[testKeyRoot] as? String
}
}
class CustomizedRecord: Decodable, FetchableRecord {
var nestedKeyed: NestedKeyed
var nestedSingle: NestedSingle
var nestedUnkeyed: NestedUnkeyed
var key: String?
var context: String?
enum CodingKeys: String, CodingKey {
case nestedKeyed, nestedSingle, nestedUnkeyed
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
nestedKeyed = try container.decode(NestedKeyed.self, forKey: .nestedKeyed)
nestedSingle = try container.decode(NestedSingle.self, forKey: .nestedSingle)
nestedUnkeyed = try container.decode(NestedUnkeyed.self, forKey: .nestedUnkeyed)
key = decoder.codingPath.last?.stringValue
context = decoder.userInfo[testKeyRoot] as? String
}
static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [
testKeyRoot: "GRDB root",
testKeyNested: "GRDB column or scope"]
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
let decoder = JSONDecoder()
decoder.userInfo = [
testKeyRoot: "JSON root",
testKeyNested: "JSON column: \(column)"]
return decoder
}
}
// Used as a reference
func testFoundationBehavior() throws {
let json = """
{
"nestedKeyed": { "name": "foo" },
"nestedSingle": "bar",
"nestedUnkeyed": ["baz"]
}
""".data(using: .utf8)!
do {
let decoder = JSONDecoder()
let record = try decoder.decode(Record.self, from: json)
XCTAssertNil(record.key)
XCTAssertNil(record.context)
XCTAssertEqual(record.nestedKeyed.name, "foo")
XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed")
XCTAssertNil(record.nestedKeyed.context)
XCTAssertEqual(record.nestedSingle.name, "bar")
XCTAssertEqual(record.nestedSingle.key, "nestedSingle")
XCTAssertNil(record.nestedSingle.context)
XCTAssertEqual(record.nestedUnkeyed.name, "baz")
XCTAssertEqual(record.nestedUnkeyed.key, "nestedUnkeyed")
XCTAssertNil(record.nestedUnkeyed.context)
}
do {
let decoder = JSONDecoder()
decoder.userInfo = [testKeyRoot: "root", testKeyNested: "nested"]
let record = try decoder.decode(Record.self, from: json)
XCTAssertNil(record.key)
XCTAssertEqual(record.context, "root")
XCTAssertEqual(record.nestedKeyed.name, "foo")
XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed")
XCTAssertEqual(record.nestedKeyed.context, "nested")
XCTAssertEqual(record.nestedSingle.name, "bar")
XCTAssertEqual(record.nestedSingle.key, "nestedSingle")
XCTAssertEqual(record.nestedSingle.context, "nested")
XCTAssertEqual(record.nestedUnkeyed.name, "baz")
XCTAssertEqual(record.nestedUnkeyed.key, "nestedUnkeyed")
XCTAssertEqual(record.nestedUnkeyed.context, "nested")
}
}
func testRecord1() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
func test(_ record: Record) {
XCTAssertNil(record.key)
XCTAssertNil(record.context)
// scope
XCTAssertEqual(record.nestedKeyed.name, "foo")
XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed")
XCTAssertNil(record.nestedKeyed.context)
// column
XCTAssertEqual(record.nestedSingle.name, "bar")
XCTAssertEqual(record.nestedSingle.key, "nestedSingle")
XCTAssertNil(record.nestedSingle.context)
// JSON column
XCTAssertEqual(record.nestedUnkeyed.name, "baz")
XCTAssertNil(record.nestedUnkeyed.key)
XCTAssertNil(record.nestedUnkeyed.context)
}
let adapter = SuffixRowAdapter(fromIndex: 1).addingScopes(["nestedKeyed": RangeRowAdapter(0..<1)])
let request = SQLRequest(
sql: "SELECT ? AS name, ? AS nestedSingle, ? AS nestedUnkeyed",
arguments: ["foo", "bar", "[\"baz\"]"],
adapter: adapter)
let record = try Record.fetchOne(db, request)!
test(record)
let row = try Row.fetchOne(db, request)!
try test(Record(row: row))
}
}
func testRecord2() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
func test(_ record: Record) {
XCTAssertNil(record.key)
XCTAssertNil(record.context)
// JSON column
XCTAssertEqual(record.nestedKeyed.name, "foo")
XCTAssertNil(record.nestedKeyed.key)
XCTAssertNil(record.nestedKeyed.context)
// column
XCTAssertEqual(record.nestedSingle.name, "bar")
XCTAssertEqual(record.nestedSingle.key, "nestedSingle")
XCTAssertNil(record.nestedSingle.context)
// JSON column
XCTAssertEqual(record.nestedUnkeyed.name, "baz")
XCTAssertNil(record.nestedUnkeyed.key)
XCTAssertNil(record.nestedUnkeyed.context)
}
let request = SQLRequest(
sql: "SELECT ? AS nestedKeyed, ? AS nestedSingle, ? AS nestedUnkeyed",
arguments: ["{\"name\":\"foo\"}", "bar", "[\"baz\"]"])
let record = try Record.fetchOne(db, request)!
test(record)
let row = try Row.fetchOne(db, request)!
try test(Record(row: row))
}
}
func testCustomizedRecord1() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
func test(_ record: CustomizedRecord) {
XCTAssertNil(record.key)
XCTAssertEqual(record.context, "GRDB root")
// scope
XCTAssertEqual(record.nestedKeyed.name, "foo")
XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed")
XCTAssertEqual(record.nestedKeyed.context, "GRDB column or scope")
// column
XCTAssertEqual(record.nestedSingle.name, "bar")
XCTAssertEqual(record.nestedSingle.key, "nestedSingle")
XCTAssertEqual(record.nestedSingle.context, "GRDB column or scope")
// JSON column
XCTAssertEqual(record.nestedUnkeyed.name, "baz")
XCTAssertNil(record.nestedUnkeyed.key)
XCTAssertEqual(record.nestedUnkeyed.context, "JSON column: nestedUnkeyed")
}
let adapter = SuffixRowAdapter(fromIndex: 1).addingScopes(["nestedKeyed": RangeRowAdapter(0..<1)])
let request = SQLRequest(
sql: "SELECT ? AS name, ? AS nestedSingle, ? AS nestedUnkeyed",
arguments: ["foo", "bar", "[\"baz\"]"],
adapter: adapter)
let record = try CustomizedRecord.fetchOne(db, request)!
test(record)
let row = try Row.fetchOne(db, request)!
try test(CustomizedRecord(row: row))
}
}
func testCustomizedRecord2() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
func test(_ record: CustomizedRecord) {
XCTAssertNil(record.key)
XCTAssertEqual(record.context, "GRDB root")
// JSON column
XCTAssertEqual(record.nestedKeyed.name, "foo")
XCTAssertNil(record.nestedKeyed.key)
XCTAssertEqual(record.nestedKeyed.context, "JSON column: nestedKeyed")
// column
XCTAssertEqual(record.nestedSingle.name, "bar")
XCTAssertEqual(record.nestedSingle.key, "nestedSingle")
XCTAssertEqual(record.nestedSingle.context, "GRDB column or scope")
// JSON column
XCTAssertEqual(record.nestedUnkeyed.name, "baz")
XCTAssertNil(record.nestedUnkeyed.key)
XCTAssertEqual(record.nestedUnkeyed.context, "JSON column: nestedUnkeyed")
}
let request = SQLRequest(
sql: "SELECT ? AS nestedKeyed, ? AS nestedSingle, ? AS nestedUnkeyed",
arguments: ["{\"name\":\"foo\"}", "bar", "[\"baz\"]"])
let record = try CustomizedRecord.fetchOne(db, request)!
test(record)
let row = try Row.fetchOne(db, request)!
try test(CustomizedRecord(row: row))
}
}
func testMissingKeys1() throws {
struct A: Decodable { }
struct B: Decodable { }
struct C: Decodable { }
struct Composed: Decodable, FetchableRecord {
var a: A
var b: B?
var c: C?
}
// No error expected:
// - a is successfully decoded because it consumes the one and unique
// allowed missing key
// - b and c are successfully decoded, because they are optionals, and
// all optionals decode missing keys are nil. This is because GRDB
// records accept rows with missing columns, and b and c may want to
// decode columns.
_ = try FetchableRecordDecoder().decode(Composed.self, from: [:])
}
// This is a regression test for https://github.com/groue/GRDB.swift/issues/664
func testMissingKeys2() throws {
struct A: Decodable { }
struct B: Decodable { }
struct Composed: Decodable, FetchableRecord {
var a: A
var b: B
}
do {
_ = try FetchableRecordDecoder().decode(Composed.self, from: [:])
XCTFail("Expected error")
} catch DecodingError.keyNotFound {
// a or b can not be decoded because only one key is allowed to be missing
}
}
// Regression test for https://github.com/groue/GRDB.swift/issues/836
func testRootKeyDecodingError() throws {
struct Record: Decodable { }
struct Composed: Decodable, FetchableRecord {
var a: Record
var b: Record
var c: Record
}
try makeDatabaseQueue().read { db in
do {
// - a is present
// - root is b and c is missing, or the opposite (two possible user intents)
let row = try Row.fetchOne(db, sql: "SELECT NULL", adapter: ScopeAdapter(["a": EmptyRowAdapter()]))!
_ = try FetchableRecordDecoder().decode(Composed.self, from: row)
XCTFail("Expected error")
} catch let DecodingError.keyNotFound(key, context) {
XCTAssert(["b", "c"].contains(key.stringValue))
XCTAssertEqual(context.debugDescription, "No such key: b or c")
}
do {
// - b is present
// - root is a and c is missing, or the opposite (two possible user intents)
let row = try Row.fetchOne(db, sql: "SELECT NULL", adapter: ScopeAdapter(["b": EmptyRowAdapter()]))!
_ = try FetchableRecordDecoder().decode(Composed.self, from: row)
XCTFail("Expected error")
} catch let DecodingError.keyNotFound(key, context) {
XCTAssert(["a", "c"].contains(key.stringValue))
XCTAssertEqual(context.debugDescription, "No such key: a or c")
}
do {
// - c is present
// - root is a and b is missing, or the opposite (two possible user intents)
let row = try Row.fetchOne(db, sql: "SELECT NULL", adapter: ScopeAdapter(["c": EmptyRowAdapter()]))!
_ = try FetchableRecordDecoder().decode(Composed.self, from: row)
XCTFail("Expected error")
} catch let DecodingError.keyNotFound(key, context) {
XCTAssert(["a", "b"].contains(key.stringValue))
XCTAssertEqual(context.debugDescription, "No such key: a or b")
}
}
}
func testUserInfoJsonDecoding() throws {
struct NestedStruct : Codable {
let firstName: String?
let lastName: String?
init(firstName: String?, lastName: String?) {
self.firstName = firstName
self.lastName = lastName
}
init(from decoder: Decoder) throws {
let userInfoValue = decoder.userInfo[.testKey] as? String
let container = try decoder.container(keyedBy: CodingKeys.self)
if userInfoValue == "correct" {
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
} else {
firstName = try container.decode(String.self, forKey: .lastName)
lastName = try container.decode(String.self, forKey: .firstName)
}
}
}
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
static let databaseTableName = "t1"
static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [CodingUserInfoKey.testKey: "correct"]
let nested: NestedStruct?
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("nested", .text)
}
let nested = NestedStruct(firstName: "Bob", lastName: "Dylan")
let value = StructWithNestedType(nested: nested)
try value.insert(db)
let parentModel = try StructWithNestedType.fetchAll(db)
guard let nestedModel = parentModel.first?.nested else {
XCTFail()
return
}
// Check the nested model contains the expected values of first and last name
XCTAssertEqual(nestedModel.firstName, "Bob")
XCTAssertEqual(nestedModel.lastName, "Dylan")
}
}
}
fileprivate extension CodingUserInfoKey {
static let testKey = CodingUserInfoKey(rawValue: "correct")!
}
// MARK: - Decodable + custom init
extension FetchableRecordDecodableTests {
// Test for <https://github.com/groue/GRDB.swift/issues/1366#issuecomment-1527280022>
func testStructWithCustomizedRowInitializer() throws {
struct Player: Decodable, FetchableRecord {
var id: Int
var name: String
var isFetched: Bool = false
enum CodingKeys: String, CodingKey {
case id, name
}
init(id: Int, name: String) {
self.id = id
self.name = name
self.isFetched = false
}
init(row: Row) throws {
self = try FetchableRecordDecoder().decode(Player.self, from: row)
self.isFetched = true
}
}
do {
let player = Player(id: 1, name: "Arthur")
XCTAssertEqual(player.id, 1)
XCTAssertEqual(player.name, "Arthur")
XCTAssertEqual(player.isFetched, false)
}
do {
let player = try Player(row: ["id": 2, "name": "Barbara"])
XCTAssertEqual(player.id, 2)
XCTAssertEqual(player.name, "Barbara")
XCTAssertEqual(player.isFetched, true)
}
}
}
// MARK: - KeyedContainer tests
extension FetchableRecordDecodableTests {
struct AnyCodingKey: CodingKey {
var stringValue: String
var intValue: Int? { nil }
init(_ key: String) {
self.stringValue = key
}
init(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
return nil
}
}
func test_allKeys_and_containsKey() throws {
struct Witness: Decodable, FetchableRecord {
init(from decoder: any Decoder) throws {
// Top
let container = try decoder.container(keyedBy: AnyCodingKey.self)
do {
// Test allKeys
let allKeys = container.allKeys
XCTAssertEqual(Set(allKeys.map(\.stringValue)), [
"a",
"topLevelScope1",
"topLevelScope2",
"nestedScope1",
"nestedScope2",
"prefetchedRows1",
"prefetchedRows2"])
// Test contains(_:)
for key in allKeys {
XCTAssertTrue(container.contains(key))
}
XCTAssertFalse(container.contains(AnyCodingKey("b")))
XCTAssertFalse(container.contains(AnyCodingKey("c")))
}
// topLevelScope1
let topLevelScope1Container = try container.nestedContainer(
keyedBy: AnyCodingKey.self,
forKey: AnyCodingKey("topLevelScope1"))
do {
// Test allKeys
let allKeys = topLevelScope1Container.allKeys
XCTAssertEqual(Set(allKeys.map(\.stringValue)), [
"c",
])
// Test contains(_:)
for key in allKeys {
XCTAssertTrue(topLevelScope1Container.contains(key))
}
}
// topLevelScope2
let topLevelScope2Container = try container.nestedContainer(
keyedBy: AnyCodingKey.self,
forKey: AnyCodingKey("topLevelScope2"))
do {
// Test allKeys
let allKeys = topLevelScope2Container.allKeys
XCTAssertEqual(Set(allKeys.map(\.stringValue)), [
"nestedScope2",
"nestedScope1",
"prefetchedRows2",
])
// Test contains(_:)
for key in allKeys {
XCTAssertTrue(topLevelScope2Container.contains(key))
}
}
}
}
try makeDatabaseQueue().read { db in
let row = try Row.fetchOne(
db, sql: """
SELECT 1 AS a, -- main row
2 AS b, -- not exposed
3 AS c, -- scope topLevelScope1
4 AS d, -- scope topLevelScope2.nestedScope1
5 AS e -- scope topLevelScope2.nestedScope2
""",
adapter: RangeRowAdapter(0..<1)
.addingScopes([
"topLevelScope1": RangeRowAdapter(2..<3),
"topLevelScope2": EmptyRowAdapter().addingScopes([
"nestedScope1": RangeRowAdapter(3..<4),
"nestedScope2": RangeRowAdapter(4..<5),
]),
]))!
row.prefetchedRows.setRows([], forKeyPath: ["prefetchedRows1"])
row.prefetchedRows.setRows([Row()], forKeyPath: ["topLevelScope2", "prefetchedRows2"])
// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:1]
unadapted: [a:1 b:2 c:3 d:4 e:5]
- topLevelScope1: [c:3]
- topLevelScope2: []
- nestedScope1: [d:4]
- nestedScope2: [e:5]
+ prefetchedRows2: 1 row
+ prefetchedRows1: 0 row
+ prefetchedRows2: 1 row
""")
// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1531>
func test_decodeNil_and_containsKey() throws {
struct Witness: Decodable, FetchableRecord {
struct NestedRecord: Decodable, FetchableRecord { }
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
// column
do {
let key = AnyCodingKey("a")
let nilDecoded = try container.decodeNil(forKey: key)
let value = try container.decodeIfPresent(Int.self, forKey: key)
XCTAssertTrue(nilDecoded == (value == nil))
XCTAssertTrue(container.contains(key))
}
// scope
do {
let key = AnyCodingKey("nested")
let nilDecoded = try container.decodeNil(forKey: key)
let value = try container.decodeIfPresent(NestedRecord.self, forKey: key)
XCTAssertTrue(nilDecoded == (value == nil))
XCTAssertTrue(container.contains(key))
}
// missing key
do {
let key = AnyCodingKey("missing")
try XCTAssertTrue(container.decodeNil(forKey: key))
try XCTAssertNil(container.decodeIfPresent(Int.self, forKey: key))
try XCTAssertNil(container.decodeIfPresent(NestedRecord.self, forKey: key))
XCTAssertFalse(container.contains(key))
}
}
}
try makeDatabaseQueue().read { db in
do {
let row = try Row.fetchOne(
db, sql: """
SELECT 1 AS a, 2 AS b
""",
adapter: ScopeAdapter([
"nested": RangeRowAdapter(1..<2),
]))!
// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:1 b:2]
unadapted: [a:1 b:2]
- nested: [b:2]
""")
// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
do {
let row = try Row.fetchOne(
db, sql: """
SELECT NULL AS a, NULL AS b
""",
adapter: ScopeAdapter([
"nested": RangeRowAdapter(1..<2),
]))!
// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:NULL b:NULL]
unadapted: [a:NULL b:NULL]
- nested: [b:NULL]
""")
// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
}
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1531>
func test_decodeNil_when_scope_and_column_have_the_same_name() throws {
struct Witness: Decodable, FetchableRecord {
struct NestedRecord: Decodable, FetchableRecord { }
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
let key = AnyCodingKey("a")
let nilDecoded = try container.decodeNil(forKey: key)
let intValue = try container.decodeIfPresent(Int.self, forKey: key)
let recordValue = try container.decodeIfPresent(NestedRecord.self, forKey: key)
XCTAssertTrue(nilDecoded == (intValue == nil))
XCTAssertTrue(nilDecoded == (recordValue == nil))
}
}
try makeDatabaseQueue().read { db in
do {
let row = try Row.fetchOne(
db, sql: """
SELECT 1 AS a
""",
adapter: ScopeAdapter([
"a": SuffixRowAdapter(fromIndex: 0),
]))!
// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:1]
unadapted: [a:1]
- a: [a:1]
""")
// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
do {
let row = try Row.fetchOne(
db, sql: """
SELECT NULL AS a
""",
adapter: ScopeAdapter([
"a": SuffixRowAdapter(fromIndex: 0),
]))!
// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:NULL]
unadapted: [a:NULL]
- a: [a:NULL]
""")
// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
}
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1572>
func testSingleValueContainer() throws {
struct Struct: Decodable {
let value: String
}
struct Wrapper<Model: Decodable>: FetchableRecord, Decodable {
var model: Model
var otherValue: String
enum CodingKeys: String, CodingKey {
case otherValue
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherValue = try container.decode(String.self, forKey: . otherValue)
let singleValueContainer = try decoder.singleValueContainer()
model = try singleValueContainer.decode(Model.self)
}
}
let row = Row(["value": "foo", "otherValue": "bar"])
let wrapper = try Wrapper<Struct>(row: row)
XCTAssertEqual(wrapper.model.value, "foo")
XCTAssertEqual(wrapper.otherValue, "bar")
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1572>
// Here we test that `FetchableRecord` takes precedence over `Decodable`
// when a record is encoded with a `SingleValueEncodingContainer`.
func testSingleValueContainerWithFetchableRecord() throws {
struct Struct: Decodable, FetchableRecord {
let value: String
init(row: Row) throws {
value = row["actualValue"]
}
}
struct Wrapper<Model: Decodable>: FetchableRecord, Decodable {
var model: Model
var otherValue: String
enum CodingKeys: String, CodingKey {
case otherValue
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherValue = try container.decode(String.self, forKey: . otherValue)
let singleValueContainer = try decoder.singleValueContainer()
model = try singleValueContainer.decode(Model.self)
}
}
let row = Row(["value": "foo", "otherValue": "bar", "actualValue": "test"])
let wrapper = try Wrapper<Struct>(row: row)
XCTAssertEqual(wrapper.model.value, "test")
XCTAssertEqual(wrapper.otherValue, "bar")
}
}