938 lines
35 KiB
Swift
938 lines
35 KiB
Swift
import XCTest
|
|
import Foundation
|
|
import GRDB
|
|
|
|
class MutablePersistableRecordEncodableTests: GRDBTestCase { }
|
|
|
|
// MARK: - MutablePersistableRecord conformance derived from Encodable
|
|
|
|
extension MutablePersistableRecordEncodableTests {
|
|
|
|
func testTrivialEncodable() throws {
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let value: String
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("value", .text)
|
|
}
|
|
|
|
var value = Struct(value: "foo")
|
|
try assert(value, isEncodedIn: ["value": "foo"])
|
|
|
|
try value.insert(db)
|
|
let string = try String.fetchOne(db, sql: "SELECT value FROM t1")!
|
|
XCTAssertEqual(string, "foo")
|
|
}
|
|
}
|
|
|
|
func testCustomEncodable() throws {
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let value: String
|
|
|
|
private enum CodingKeys : String, CodingKey {
|
|
case value = "someColumn"
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(value + " (Encodable)", forKey: .value)
|
|
}
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("someColumn", .text)
|
|
}
|
|
|
|
var value = Struct(value: "foo")
|
|
try assert(value, isEncodedIn: ["someColumn": "foo (Encodable)"])
|
|
|
|
try value.insert(db)
|
|
let string = try String.fetchOne(db, sql: "SELECT someColumn FROM t1")!
|
|
XCTAssertEqual(string, "foo (Encodable)")
|
|
}
|
|
}
|
|
|
|
func testCustomMutablePersistableRecord() throws {
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let value: String
|
|
|
|
func encode(to container: inout PersistenceContainer) {
|
|
container["value"] = value + " (MutablePersistableRecord)"
|
|
}
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("value", .text)
|
|
}
|
|
|
|
var value = Struct(value: "foo")
|
|
try assert(value, isEncodedIn: ["value": "foo (MutablePersistableRecord)"])
|
|
|
|
try value.insert(db)
|
|
let string = try String.fetchOne(db, sql: "SELECT value FROM t1")!
|
|
XCTAssertEqual(string, "foo (MutablePersistableRecord)")
|
|
}
|
|
}
|
|
|
|
// Regression test for <https://github.com/groue/GRDB.swift/issues/1565>
|
|
func testSingleValueContainer() throws {
|
|
struct Struct: Encodable {
|
|
let value: String
|
|
}
|
|
|
|
struct Wrapper<Model: Encodable>: MutablePersistableRecord, Encodable {
|
|
static var databaseTableName: String { "t1" }
|
|
var model: Model
|
|
var otherValue: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case otherValue
|
|
}
|
|
|
|
func encode(to encoder: any Encoder) throws {
|
|
var modelContainer = encoder.singleValueContainer()
|
|
try modelContainer.encode(model)
|
|
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(otherValue, forKey: .otherValue)
|
|
}
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("value", .text)
|
|
t.column("otherValue", .text)
|
|
}
|
|
|
|
var value = Wrapper(model: Struct(value: "foo"), otherValue: "bar")
|
|
try assert(value, isEncodedIn: ["value": "foo", "otherValue": "bar"])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT value, otherValue FROM t1")!
|
|
XCTAssertEqual(row[0], "foo")
|
|
XCTAssertEqual(row[1], "bar")
|
|
}
|
|
}
|
|
|
|
// Regression test for <https://github.com/groue/GRDB.swift/issues/1565>
|
|
// Here we test that `EncodableRecord` takes precedence over `Encodable`
|
|
// when a record is encoded with a `SingleValueEncodingContainer`.
|
|
func testSingleValueContainerWithEncodableRecord() throws {
|
|
struct Struct: Encodable, EncodableRecord {
|
|
let value: String
|
|
|
|
func encode(to container: inout PersistenceContainer) throws {
|
|
container["column1"] = "test"
|
|
container["column2"] = 12
|
|
}
|
|
}
|
|
|
|
struct Wrapper<Model: Encodable>: MutablePersistableRecord, Encodable {
|
|
static var databaseTableName: String { "t1" }
|
|
var model: Model
|
|
var otherValue: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case otherValue
|
|
}
|
|
|
|
func encode(to encoder: any Encoder) throws {
|
|
var modelContainer = encoder.singleValueContainer()
|
|
try modelContainer.encode(model)
|
|
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(otherValue, forKey: .otherValue)
|
|
}
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("column1", .text)
|
|
t.column("column2", .integer)
|
|
t.column("otherValue", .text)
|
|
}
|
|
|
|
var value = Wrapper(model: Struct(value: "foo"), otherValue: "bar")
|
|
try assert(value, isEncodedIn: ["column1": "test", "column2": 12, "otherValue": "bar"])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT column1, column2, otherValue FROM t1")!
|
|
XCTAssertEqual(row[0], "test")
|
|
XCTAssertEqual(row[1], 12)
|
|
XCTAssertEqual(row[2], "bar")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Different kinds of single-value properties
|
|
|
|
extension MutablePersistableRecordEncodableTests {
|
|
|
|
func testTrivialProperty() throws {
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let int64: Int64
|
|
let optionalInt64: Int64?
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("int64", .integer)
|
|
t.column("optionalInt64", .integer)
|
|
}
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(int64: 123, optionalInt64: 456)
|
|
try assert(value, isEncodedIn: ["int64": 123, "optionalInt64": 456])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["int64": 123, "optionalInt64": 456])
|
|
return .rollback
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(int64: 123, optionalInt64: nil)
|
|
try assert(value, isEncodedIn: ["int64": 123, "optionalInt64": nil])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["int64": 123, "optionalInt64": nil])
|
|
return .rollback
|
|
}
|
|
}
|
|
|
|
func testTrivialSingleValueEncodableProperty() throws {
|
|
struct Value : Encodable {
|
|
let string: String
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(string)
|
|
}
|
|
}
|
|
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let value: Value
|
|
let optionalValue: Value?
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("value", .integer)
|
|
t.column("optionalValue", .integer)
|
|
}
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: Value(string: "foo"), optionalValue: Value(string: "bar"))
|
|
try assert(value, isEncodedIn: ["value": "foo", "optionalValue": "bar"])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo", "optionalValue": "bar"])
|
|
return .rollback
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: Value(string: "foo"), optionalValue: nil)
|
|
try assert(value, isEncodedIn: ["value": "foo", "optionalValue": nil])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo", "optionalValue": nil])
|
|
return .rollback
|
|
}
|
|
}
|
|
|
|
func testNonTrivialSingleValueEncodableProperty() throws {
|
|
struct NestedValue : Encodable {
|
|
let string: String
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(string)
|
|
}
|
|
}
|
|
|
|
struct Value : Encodable {
|
|
let nestedValue: NestedValue
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(nestedValue)
|
|
}
|
|
}
|
|
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let value: Value
|
|
let optionalValue: Value?
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("value", .integer)
|
|
t.column("optionalValue", .integer)
|
|
}
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: Value(nestedValue: NestedValue(string: "foo")), optionalValue: Value(nestedValue: NestedValue(string: "bar")))
|
|
try assert(value, isEncodedIn: ["value": "foo", "optionalValue": "bar"])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo", "optionalValue": "bar"])
|
|
return .rollback
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: Value(nestedValue: NestedValue(string: "foo")), optionalValue: nil)
|
|
try assert(value, isEncodedIn: ["value": "foo", "optionalValue": nil])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo", "optionalValue": nil])
|
|
return .rollback
|
|
}
|
|
}
|
|
|
|
func testEncodableRawRepresentableProperty() throws {
|
|
// This test is somewhat redundant with testSingleValueEncodableProperty,
|
|
// since a RawRepresentable enum is a "single-value" Encodable.
|
|
//
|
|
// But with an explicit test for enums, we are sure that enums, widely
|
|
// used, are supported.
|
|
enum Value : String, Encodable {
|
|
case foo, bar
|
|
}
|
|
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let value: Value
|
|
let optionalValue: Value?
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("value", .integer)
|
|
t.column("optionalValue", .integer)
|
|
}
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: .foo, optionalValue: .bar)
|
|
try assert(value, isEncodedIn: ["value": "foo", "optionalValue": "bar"])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo", "optionalValue": "bar"])
|
|
return .rollback
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: .foo, optionalValue: nil)
|
|
try assert(value, isEncodedIn: ["value": "foo", "optionalValue": nil])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo", "optionalValue": nil])
|
|
return .rollback
|
|
}
|
|
}
|
|
|
|
func testDatabaseValueConvertibleProperty() throws {
|
|
// This test makes sure that Date, for example, can be stored as a String.
|
|
//
|
|
// Without this preference for databaseValue over encode(to:),
|
|
// Date would encode as a double.
|
|
struct Value : Encodable, DatabaseValueConvertible {
|
|
let string: String
|
|
|
|
init(string: String) {
|
|
self.string = string
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(string + " (Encodable)")
|
|
}
|
|
|
|
// DatabaseValueConvertible adoption
|
|
|
|
var databaseValue: DatabaseValue {
|
|
(string + " (DatabaseValueConvertible)").databaseValue
|
|
}
|
|
|
|
static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? {
|
|
fatalError("irrelevant")
|
|
}
|
|
}
|
|
|
|
struct Struct : MutablePersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let value: Value
|
|
let optionalValue: Value?
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("value", .integer)
|
|
t.column("optionalValue", .integer)
|
|
}
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: Value(string: "foo"), optionalValue: Value(string: "bar"))
|
|
try assert(value, isEncodedIn: ["value": "foo (DatabaseValueConvertible)", "optionalValue": "bar (DatabaseValueConvertible)"])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo (DatabaseValueConvertible)", "optionalValue": "bar (DatabaseValueConvertible)"])
|
|
return .rollback
|
|
}
|
|
|
|
try dbQueue.inTransaction { db in
|
|
var value = Struct(value: Value(string: "foo"), optionalValue: nil)
|
|
try assert(value, isEncodedIn: ["value": "foo (DatabaseValueConvertible)", "optionalValue": nil])
|
|
|
|
try value.insert(db)
|
|
let row = try Row.fetchOne(db, sql: "SELECT * FROM t1")!
|
|
XCTAssertEqual(row, ["value": "foo (DatabaseValueConvertible)", "optionalValue": nil])
|
|
return .rollback
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Foundation Codable Types
|
|
|
|
extension MutablePersistableRecordEncodableTests {
|
|
|
|
func testStructWithDate() throws {
|
|
struct StructWithDate : PersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let date: Date
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("date", .datetime)
|
|
}
|
|
|
|
let value = StructWithDate(date: Date())
|
|
try value.insert(db)
|
|
|
|
let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT date FROM t1")!
|
|
|
|
// Date has a default Encodable implementation which encodes a Double.
|
|
// We expect here a String, because DatabaseValueConvertible has
|
|
// precedence over Encodable.
|
|
XCTAssert(dbValue.storage.value is String)
|
|
|
|
let fetchedDate = Date.fromDatabaseValue(dbValue)!
|
|
XCTAssert(abs(fetchedDate.timeIntervalSince(value.date)) < 0.001)
|
|
}
|
|
}
|
|
|
|
func testStructWithURL() throws {
|
|
struct StructWithURL : PersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let url: URL
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("url", .text)
|
|
}
|
|
|
|
let value = StructWithURL(url: URL(string: "https://github.com")!)
|
|
try assert(value, isEncodedIn: ["url": value.url])
|
|
|
|
try value.insert(db)
|
|
let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT url FROM t1")!
|
|
XCTAssert(dbValue.storage.value is String)
|
|
let fetchedURL = URL.fromDatabaseValue(dbValue)!
|
|
XCTAssertEqual(fetchedURL, value.url)
|
|
}
|
|
}
|
|
|
|
func testStructWithUUID() throws {
|
|
struct StructWithUUID : PersistableRecord, Encodable {
|
|
static let databaseTableName = "t1"
|
|
let uuid: UUID
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "t1") { t in
|
|
t.column("uuid", .blob)
|
|
}
|
|
|
|
let value = StructWithUUID(uuid: UUID())
|
|
try assert(value, isEncodedIn: ["uuid": value.uuid])
|
|
|
|
try value.insert(db)
|
|
let dbValue = try DatabaseValue.fetchOne(db, sql: "SELECT uuid FROM t1")!
|
|
XCTAssert(dbValue.storage.value is Data)
|
|
let fetchedUUID = UUID.fromDatabaseValue(dbValue)!
|
|
XCTAssertEqual(fetchedUUID, value.uuid)
|
|
}
|
|
}
|
|
|
|
func testArrayJSONEncoding() throws {
|
|
struct Record: PersistableRecord, Encodable {
|
|
let array: [String]
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "record") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("array", .text)
|
|
}
|
|
|
|
try Record(array: []).insert(db)
|
|
try Record(array: ["foo"]).insert(db)
|
|
try Record(array: ["foo", "bar"]).insert(db)
|
|
|
|
let rows = try Row.fetchAll(db, sql: "SELECT * FROM record ORDER BY id")
|
|
XCTAssertEqual(rows.count, 3)
|
|
XCTAssertEqual(rows[0]["array"], "[]")
|
|
XCTAssertEqual(rows[1]["array"], "[\"foo\"]")
|
|
XCTAssertEqual(rows[2]["array"], "[\"foo\",\"bar\"]")
|
|
}
|
|
}
|
|
|
|
func testOptionalArrayJSONEncoding() throws {
|
|
struct Record: PersistableRecord, Encodable {
|
|
let array: [String]?
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "record") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("array", .text)
|
|
}
|
|
|
|
try Record(array: nil).insert(db)
|
|
try Record(array: []).insert(db)
|
|
try Record(array: ["foo"]).insert(db)
|
|
try Record(array: ["foo", "bar"]).insert(db)
|
|
|
|
let rows = try Row.fetchAll(db, sql: "SELECT * FROM record ORDER BY id")
|
|
XCTAssertEqual(rows.count, 4)
|
|
XCTAssertTrue(rows[0]["array"] == nil)
|
|
XCTAssertEqual(rows[1]["array"], "[]")
|
|
XCTAssertEqual(rows[2]["array"], "[\"foo\"]")
|
|
XCTAssertEqual(rows[3]["array"], "[\"foo\",\"bar\"]")
|
|
}
|
|
}
|
|
|
|
func testDictionaryJSONEncoding() throws {
|
|
struct Record: PersistableRecord, Encodable {
|
|
let dictionary: [String: Int]
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "record") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("dictionary", .text)
|
|
}
|
|
|
|
try Record(dictionary: [:]).insert(db)
|
|
try Record(dictionary: ["foo": 1]).insert(db)
|
|
|
|
let rows = try Row.fetchAll(db, sql: "SELECT * FROM record ORDER BY id")
|
|
XCTAssertEqual(rows.count, 2)
|
|
XCTAssertEqual(rows[0]["dictionary"], "{}")
|
|
XCTAssertEqual(rows[1]["dictionary"], "{\"foo\":1}")
|
|
}
|
|
}
|
|
|
|
func testOptionalDictionaryJSONEncoding() throws {
|
|
struct Record: PersistableRecord, Encodable {
|
|
let dictionary: [String: Int]?
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "record") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("dictionary", .text)
|
|
}
|
|
|
|
try Record(dictionary: nil).insert(db)
|
|
try Record(dictionary: [:]).insert(db)
|
|
try Record(dictionary: ["foo": 1]).insert(db)
|
|
|
|
let rows = try Row.fetchAll(db, sql: "SELECT * FROM record ORDER BY id")
|
|
XCTAssertEqual(rows.count, 3)
|
|
XCTAssertTrue(rows[0]["dictionary"] == nil)
|
|
XCTAssertEqual(rows[1]["dictionary"], "{}")
|
|
XCTAssertEqual(rows[2]["dictionary"], "{\"foo\":1}")
|
|
}
|
|
}
|
|
|
|
func testJSONDataEncodingStrategy() throws {
|
|
struct Record: PersistableRecord, Encodable {
|
|
let data: Data
|
|
let optionalData: Data?
|
|
let datas: [Data]
|
|
let optionalDatas: [Data?]
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "record") { t in
|
|
t.column("data", .text)
|
|
t.column("optionalData", .text)
|
|
t.column("datas", .text)
|
|
t.column("optionalDatas", .text)
|
|
}
|
|
|
|
let data = "foo".data(using: .utf8)!
|
|
let record = Record(data: data, optionalData: data, datas: [data], optionalDatas: [nil, data])
|
|
try record.insert(db)
|
|
|
|
let row = try Row.fetchOne(db, Record.all())!
|
|
XCTAssertEqual(row["data"], data)
|
|
XCTAssertEqual(row["optionalData"], data)
|
|
XCTAssertEqual(row["datas"], "[\"Zm9v\"]")
|
|
XCTAssertEqual(row["optionalDatas"], "[null,\"Zm9v\"]")
|
|
}
|
|
}
|
|
|
|
func testJSONDateEncodingStrategy() throws {
|
|
struct Record: PersistableRecord, Encodable {
|
|
let date: Date
|
|
let optionalDate: Date?
|
|
let dates: [Date]
|
|
let optionalDates: [Date?]
|
|
}
|
|
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.create(table: "record") { t in
|
|
t.column("date", .text)
|
|
t.column("optionalDate", .text)
|
|
t.column("dates", .text)
|
|
t.column("optionalDates", .text)
|
|
}
|
|
|
|
let date = Date(timeIntervalSince1970: 128)
|
|
let record = Record(date: date, optionalDate: date, dates: [date], optionalDates: [nil, date])
|
|
try record.insert(db)
|
|
|
|
let row = try Row.fetchOne(db, Record.all())!
|
|
XCTAssertEqual(row["date"], "1970-01-01 00:02:08.000")
|
|
XCTAssertEqual(row["optionalDate"], "1970-01-01 00:02:08.000")
|
|
XCTAssertEqual(row["dates"], "[128000]")
|
|
XCTAssertEqual(row["optionalDates"], "[null,128000]")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - User Infos & Coding Keys
|
|
|
|
private let testKeyRoot = CodingUserInfoKey(rawValue: "test1")!
|
|
private let testKeyNested = CodingUserInfoKey(rawValue: "test2")!
|
|
|
|
extension MutablePersistableRecordEncodableTests {
|
|
struct NestedKeyed: Encodable {
|
|
var name: String
|
|
|
|
enum CodingKeys: String, CodingKey { case name, key, context }
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(name, forKey: .name)
|
|
try container.encodeIfPresent(encoder.codingPath.last?.stringValue, forKey: .key)
|
|
try container.encodeIfPresent(encoder.userInfo[testKeyNested] as? String, forKey: .context)
|
|
}
|
|
}
|
|
|
|
struct NestedSingle: Encodable {
|
|
var name: String
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
var value = name
|
|
value += ",key:\(encoder.codingPath.last?.stringValue ?? "nil")"
|
|
value += ",context:\(encoder.userInfo[testKeyNested] as? String ?? "nil")"
|
|
try container.encode(value)
|
|
}
|
|
}
|
|
|
|
struct NestedUnkeyed: Encodable {
|
|
var name: String
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.unkeyedContainer()
|
|
try container.encode(name)
|
|
if let key = encoder.codingPath.last?.stringValue {
|
|
try container.encode(key)
|
|
} else {
|
|
try container.encodeNil()
|
|
}
|
|
if let context = encoder.userInfo[testKeyNested] as? String {
|
|
try container.encode(context)
|
|
} else {
|
|
try container.encodeNil()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Record: Encodable, MutablePersistableRecord {
|
|
var nestedKeyed: NestedKeyed
|
|
var nestedSingle: NestedSingle
|
|
var nestedUnkeyed: NestedUnkeyed
|
|
|
|
init(nestedKeyed: NestedKeyed, nestedSingle: NestedSingle, nestedUnkeyed: NestedUnkeyed) {
|
|
self.nestedKeyed = nestedKeyed
|
|
self.nestedSingle = nestedSingle
|
|
self.nestedUnkeyed = nestedUnkeyed
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case nestedKeyed, nestedSingle, nestedUnkeyed, key, context
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(nestedKeyed, forKey: .nestedKeyed)
|
|
try container.encode(nestedSingle, forKey: .nestedSingle)
|
|
try container.encode(nestedUnkeyed, forKey: .nestedUnkeyed)
|
|
try container.encodeIfPresent(encoder.codingPath.last?.stringValue, forKey: .key)
|
|
try container.encodeIfPresent(encoder.userInfo[testKeyRoot] as? String, forKey: .context)
|
|
}
|
|
}
|
|
|
|
struct CustomizedRecord: Encodable, MutablePersistableRecord {
|
|
var nestedKeyed: NestedKeyed
|
|
var nestedSingle: NestedSingle
|
|
var nestedUnkeyed: NestedUnkeyed
|
|
|
|
init(nestedKeyed: NestedKeyed, nestedSingle: NestedSingle, nestedUnkeyed: NestedUnkeyed) {
|
|
self.nestedKeyed = nestedKeyed
|
|
self.nestedSingle = nestedSingle
|
|
self.nestedUnkeyed = nestedUnkeyed
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case nestedKeyed, nestedSingle, nestedUnkeyed, key, context
|
|
}
|
|
|
|
static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [
|
|
testKeyRoot: "GRDB root",
|
|
testKeyNested: "GRDB nested"]
|
|
|
|
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .sortedKeys
|
|
encoder.userInfo = [
|
|
testKeyRoot: "JSON root",
|
|
testKeyNested: "JSON nested: \(column)"]
|
|
return encoder
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(nestedKeyed, forKey: .nestedKeyed)
|
|
try container.encode(nestedSingle, forKey: .nestedSingle)
|
|
try container.encode(nestedUnkeyed, forKey: .nestedUnkeyed)
|
|
try container.encodeIfPresent(encoder.codingPath.last?.stringValue, forKey: .key)
|
|
try container.encodeIfPresent(encoder.userInfo[testKeyRoot] as? String, forKey: .context)
|
|
}
|
|
}
|
|
|
|
// Used as a reference
|
|
func testFoundationBehavior() throws {
|
|
do {
|
|
let record = Record(
|
|
nestedKeyed: NestedKeyed(name: "foo"),
|
|
nestedSingle: NestedSingle(name: "bar"),
|
|
nestedUnkeyed: NestedUnkeyed(name: "baz"))
|
|
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
|
|
let json = try String(data: encoder.encode(record), encoding: .utf8)!
|
|
XCTAssertEqual(json, """
|
|
{
|
|
"nestedKeyed" : {
|
|
"key" : "nestedKeyed",
|
|
"name" : "foo"
|
|
},
|
|
"nestedSingle" : "bar,key:nestedSingle,context:nil",
|
|
"nestedUnkeyed" : [
|
|
"baz",
|
|
"nestedUnkeyed",
|
|
null
|
|
]
|
|
}
|
|
""")
|
|
}
|
|
|
|
do {
|
|
let record = Record(
|
|
nestedKeyed: NestedKeyed(name: "foo"),
|
|
nestedSingle: NestedSingle(name: "bar"),
|
|
nestedUnkeyed: NestedUnkeyed(name: "baz"))
|
|
|
|
let encoder = JSONEncoder()
|
|
encoder.userInfo = [testKeyRoot: "root", testKeyNested: "nested"]
|
|
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
|
|
let json = try String(data: encoder.encode(record), encoding: .utf8)
|
|
XCTAssertEqual(json, """
|
|
{
|
|
"context" : "root",
|
|
"nestedKeyed" : {
|
|
"context" : "nested",
|
|
"key" : "nestedKeyed",
|
|
"name" : "foo"
|
|
},
|
|
"nestedSingle" : "bar,key:nestedSingle,context:nested",
|
|
"nestedUnkeyed" : [
|
|
"baz",
|
|
"nestedUnkeyed",
|
|
"nested"
|
|
]
|
|
}
|
|
""")
|
|
}
|
|
}
|
|
|
|
func testRecord() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "record") { t in
|
|
t.column("context")
|
|
t.column("key")
|
|
t.column("nestedKeyed")
|
|
t.column("nestedSingle")
|
|
t.column("nestedUnkeyed")
|
|
}
|
|
|
|
var record = Record(
|
|
nestedKeyed: NestedKeyed(name: "foo"),
|
|
nestedSingle: NestedSingle(name: "bar"),
|
|
nestedUnkeyed: NestedUnkeyed(name: "baz"))
|
|
try record.insert(db)
|
|
|
|
let row = try Row.fetchOne(db, Record.all())!
|
|
XCTAssertEqual(row, [
|
|
"context": nil,
|
|
"key": nil,
|
|
"nestedKeyed": "{\"name\":\"foo\"}",
|
|
"nestedSingle": "bar,key:nestedSingle,context:nil",
|
|
"nestedUnkeyed": "[\"baz\",null,null]"])
|
|
}
|
|
}
|
|
|
|
func testCustomizedRecord() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write { db in
|
|
try db.create(table: "customizedRecord") { t in
|
|
t.column("context")
|
|
t.column("key")
|
|
t.column("nestedKeyed")
|
|
t.column("nestedSingle")
|
|
t.column("nestedUnkeyed")
|
|
}
|
|
|
|
var record = CustomizedRecord(
|
|
nestedKeyed: NestedKeyed(name: "foo"),
|
|
nestedSingle: NestedSingle(name: "bar"),
|
|
nestedUnkeyed: NestedUnkeyed(name: "baz"))
|
|
try record.insert(db)
|
|
|
|
let row = try Row.fetchOne(db, CustomizedRecord.all())!
|
|
XCTAssertEqual(row, [
|
|
"context": "GRDB root",
|
|
"key": nil,
|
|
"nestedKeyed": "{\"context\":\"JSON nested: nestedKeyed\",\"name\":\"foo\"}",
|
|
"nestedSingle": "bar,key:nestedSingle,context:GRDB nested",
|
|
"nestedUnkeyed": "[\"baz\",null,\"JSON nested: nestedUnkeyed\"]"])
|
|
}
|
|
}
|
|
|
|
func testUserInfoJsonEncoding() throws {
|
|
struct NestedStruct : Codable {
|
|
let firstName: String?
|
|
let lastName: String?
|
|
|
|
init(firstName: String?, lastName: String?) {
|
|
self.firstName = firstName
|
|
self.lastName = lastName
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
let userInfoValue = encoder.userInfo[.testKey] as? String
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
if userInfoValue == "correct" {
|
|
try container.encode(firstName, forKey: .firstName)
|
|
try container.encode(lastName, forKey: .lastName)
|
|
} else {
|
|
try container.encode(firstName, forKey: .lastName)
|
|
try container.encode(lastName, forKey: .firstName)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable {
|
|
static let databaseTableName = "t1"
|
|
static var databaseEncodingUserInfo: [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")!
|
|
}
|