diff --git a/CHANGELOG.md b/CHANGELOG.md index 70cc46deff..cca487f80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Release Notes ### New - [#397](https://github.com/groue/GRDB.swift/pull/397): JSON encoding and decoding of codable record properties +- [#399](https://github.com/groue/GRDB.swift/pull/399): Codable support: customize the `userInfo` context dictionary, and the format of JSON columns - [#393](https://github.com/groue/GRDB.swift/pull/393): Upgrade SQLCipher to 3.4.2, enable FTS5 on GRDBCipher and new pod GRDBPlus. - [#384](https://github.com/groue/GRDB.swift/pull/384): Improve database value decoding diagnostics - Cursors of optimized values (Strint, Int, Date, etc.) have been renamed: FastDatabaseValueCursor and FastNullableDatabaseValueCursor replace the deprecated ColumnCursor and NullableColumnCursor. @@ -26,6 +27,16 @@ Release Notes + func unsafeReentrantRead(_ block: (Database) throws -> T) rethrows -> T { } + protocol FetchableRecord { ++ static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { get } ++ static func databaseJSONDecoder(for column: String) -> JSONDecoder + } + + protocol MutablePersistableRecord: TableRecord { ++ static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get } ++ static func databaseJSONEncoder(for column: String) -> JSONEncoder + } + +final class FastDatabaseValueCursor : Cursor { } +@available(*, deprecated, renamed: "FastDatabaseValueCursor") +typealias ColumnCursor = FastDatabaseValueCursor @@ -39,7 +50,8 @@ Release Notes ### Documentation Diff - [Enabling FTS5 Support](README.md#enabling-fts5-support): Procedure for enabling FTS5 support in GRDB. -- [Codable Records](README.md#codable-records): Updated documentation, for JSON encoding of codable record properties, and for the reuse of coding keys as database columns. +- [Codable Records](README.md#codable-records): Updated documentation for JSON columns, tips, and customization options. +- [Record Customization Options](README.md#record-customization-options): A new chapter that gather all your customization options. ## 3.2.0 diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 139a8fb869..9cb05eef43 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -1,255 +1,250 @@ import Foundation -private struct RowKeyedDecodingContainer: KeyedDecodingContainerProtocol { - let decoder: RowDecoder - var codingPath: [CodingKey] { return decoder.codingPath } +// MARK: - RowDecoder - init(decoder: RowDecoder) { - self.decoder = decoder - } +/// The decoder that decodes a record from a database row +private struct RowDecoder: Decoder { + var row: Row + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] { return Record.databaseDecodingUserInfo } - /// All the keys the `Decoder` has for this container. - /// - /// Different keyed containers from the same `Decoder` may return different keys here; it is possible to encode with multiple key types which are not convertible to one another. This should report all keys present which are convertible to the requested type. - var allKeys: [Key] { - let row = decoder.row - let columnNames = Set(row.columnNames) - let scopeNames = Set(row.scopesTree.names) - return columnNames.union(scopeNames).compactMap { Key(stringValue: $0) } + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + return KeyedDecodingContainer(KeyedContainer(decoder: self)) } - /// Returns whether the `Decoder` contains a value associated with the given key. - /// - /// The value associated with the given key may be a null value as appropriate for the data format. - /// - /// - parameter key: The key to search for. - /// - returns: Whether the `Decoder` has an entry for the given key. - func contains(_ key: Key) -> Bool { - let row = decoder.row - return row.hasColumn(key.stringValue) || (row.scopesTree[key.stringValue] != nil) + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + fatalError("unkeyed decoding from database row is not supported") } - /// Decodes a null value for the given key. - /// - /// - parameter key: The key that the decoded value is associated with. - /// - returns: Whether the encountered value was null. - /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. - func decodeNil(forKey key: Key) throws -> Bool { - let row = decoder.row - return row[key.stringValue] == nil && (row.scopesTree[key.stringValue] == nil) + func singleValueContainer() throws -> SingleValueDecodingContainer { + fatalError("single value decoding from database row is not supported") } - /// Decodes a value of the given type for the given key. - /// - /// - parameter type: The type of value to decode. - /// - parameter key: The key that the decoded value is associated with. - /// - returns: A value of the requested type, if present for the given key and convertible to the requested type. - /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type. - /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. - /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key. - func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { return decoder.row[key.stringValue] } - func decode(_ type: Int.Type, forKey key: Key) throws -> Int { return decoder.row[key.stringValue] } - func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { return decoder.row[key.stringValue] } - func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { return decoder.row[key.stringValue] } - func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { return decoder.row[key.stringValue] } - func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { return decoder.row[key.stringValue] } - func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { return decoder.row[key.stringValue] } - func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { return decoder.row[key.stringValue] } - func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { return decoder.row[key.stringValue] } - func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { return decoder.row[key.stringValue] } - func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { return decoder.row[key.stringValue] } - func decode(_ type: Float.Type, forKey key: Key) throws -> Float { return decoder.row[key.stringValue] } - func decode(_ type: Double.Type, forKey key: Key) throws -> Double { return decoder.row[key.stringValue] } - func decode(_ type: String.Type, forKey key: Key) throws -> String { return decoder.row[key.stringValue] } - - /// Decodes a value of the given type for the given key, if present. - /// - /// This method returns nil if the container does not have a value - /// associated with key, or if the value is null. The difference between - /// these states can be distinguished with a contains(_:) call. - func decodeIfPresent(_ type: T.Type, forKey key: Key) throws -> T? where T : Decodable { - let row = decoder.row - let keyName = key.stringValue - - // Column? - if let index = row.index(ofColumn: keyName) { - // Prefer DatabaseValueConvertible decoding over Decodable. - // This allows decoding Date from String, or DatabaseValue from NULL. - if let type = T.self as? (DatabaseValueConvertible & StatementColumnConvertible).Type { - return type.fastDecodeIfPresent(from: row, atUncheckedIndex: index) as! T? - } else if let type = T.self as? DatabaseValueConvertible.Type { - return type.decodeIfPresent(from: row, atUncheckedIndex: index) as! T? - } else if row.impl.hasNull(atUncheckedIndex: index) { - return nil - } else { - do { - // This decoding will fail for types that decode from keyed - // or unkeyed containers, because we're decoding a single - // value here (string, int, double, data, null). If such an - // error happens, we'll switch to JSON decoding. - return try T(from: RowSingleValueDecoder(row: row, columnIndex: index, codingPath: codingPath + [key])) - } catch is JSONRequiredError { - guard let data = row.dataNoCopy(atIndex: index) else { - fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) - } - return try makeJSONDecoder().decode(type.self, from: data) - } - } + struct KeyedContainer: KeyedDecodingContainerProtocol { + let decoder: RowDecoder + var codingPath: [CodingKey] { return decoder.codingPath } + + init(decoder: RowDecoder) { + self.decoder = decoder } - // Scope? - if let scopedRow = row.scopesTree[keyName], scopedRow.containsNonNullValue { - if let type = T.self as? FetchableRecord.Type { - // Prefer FetchableRecord decoding over Decodable. - // This allows custom row decoding - return (type.init(row: scopedRow) as! T) - } else { - return try T(from: RowDecoder(row: scopedRow, codingPath: codingPath + [key])) + var allKeys: [Key] { + let row = decoder.row + let columnNames = Set(row.columnNames) + let scopeNames = Set(row.scopesTree.names) + return columnNames.union(scopeNames).compactMap { Key(stringValue: $0) } + } + + func contains(_ key: Key) -> Bool { + let row = decoder.row + return row.hasColumn(key.stringValue) || (row.scopesTree[key.stringValue] != nil) + } + + func decodeNil(forKey key: Key) throws -> Bool { + let row = decoder.row + return row[key.stringValue] == nil && (row.scopesTree[key.stringValue] == nil) + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { return decoder.row[key.stringValue] } + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { return decoder.row[key.stringValue] } + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { return decoder.row[key.stringValue] } + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { return decoder.row[key.stringValue] } + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { return decoder.row[key.stringValue] } + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { return decoder.row[key.stringValue] } + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { return decoder.row[key.stringValue] } + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { return decoder.row[key.stringValue] } + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { return decoder.row[key.stringValue] } + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { return decoder.row[key.stringValue] } + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { return decoder.row[key.stringValue] } + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { return decoder.row[key.stringValue] } + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { return decoder.row[key.stringValue] } + func decode(_ type: String.Type, forKey key: Key) throws -> String { return decoder.row[key.stringValue] } + + func decodeIfPresent(_ type: T.Type, forKey key: Key) throws -> T? where T : Decodable { + let row = decoder.row + let keyName = key.stringValue + + // Column? + if let index = row.index(ofColumn: keyName) { + // Prefer DatabaseValueConvertible decoding over Decodable. + // This allows decoding Date from String, or DatabaseValue from NULL. + if let type = T.self as? (DatabaseValueConvertible & StatementColumnConvertible).Type { + return type.fastDecodeIfPresent(from: row, atUncheckedIndex: index) as! T? + } else if let type = T.self as? DatabaseValueConvertible.Type { + return type.decodeIfPresent(from: row, atUncheckedIndex: index) as! T? + } else if row.impl.hasNull(atUncheckedIndex: index) { + return nil + } else { + return try decode(type, fromColumnAtIndex: index, key: key) + } + } + + // Scope? (beware left joins, and check if scoped row contains non-null values) + if let scopedRow = row.scopesTree[keyName], scopedRow.containsNonNullValue { + return try decode(type, fromRow: scopedRow, codingPath: codingPath + [key]) } + + // Key is not a column, and not a scope. + return nil } - return nil - } - - /// Decodes a value of the given type for the given key. - /// - /// - parameter type: The type of value to decode. - /// - parameter key: The key that the decoded value is associated with. - /// - returns: A value of the requested type, if present for the given key and convertible to the requested type. - /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type. - /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. - /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key. - func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { - let row = decoder.row - let keyName = key.stringValue - - // Column? - if let index = row.index(ofColumn: keyName) { - // Prefer DatabaseValueConvertible decoding over Decodable. - // This allows decoding Date from String, or DatabaseValue from NULL. - if let type = T.self as? (DatabaseValueConvertible & StatementColumnConvertible).Type { - return type.fastDecode(from: row, atUncheckedIndex: index) as! T - } else if let type = T.self as? DatabaseValueConvertible.Type { - return type.decode(from: row, atUncheckedIndex: index) as! T - } else { - do { - // This decoding will fail for types that decode from keyed - // or unkeyed containers, because we're decoding a single - // value here (string, int, double, data, null). If such an - // error happens, we'll switch to JSON decoding. - return try T(from: RowSingleValueDecoder(row: row, columnIndex: index, codingPath: codingPath + [key])) - } catch is JSONRequiredError { - guard let data = row.dataNoCopy(atIndex: index) else { - fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) - } - return try makeJSONDecoder().decode(type.self, from: data) + func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { + let row = decoder.row + let keyName = key.stringValue + + // Column? + if let index = row.index(ofColumn: keyName) { + // Prefer DatabaseValueConvertible decoding over Decodable. + // This allows decoding Date from String, or DatabaseValue from NULL. + if let type = T.self as? (DatabaseValueConvertible & StatementColumnConvertible).Type { + return type.fastDecode(from: row, atUncheckedIndex: index) as! T + } else if let type = T.self as? DatabaseValueConvertible.Type { + return type.decode(from: row, atUncheckedIndex: index) as! T + } else { + return try decode(type, fromColumnAtIndex: index, key: key) } } + + // Scope? + if let scopedRow = row.scopesTree[keyName] { + return try decode(type, fromRow: scopedRow, codingPath: codingPath + [key]) + } + + // Key is not a column, and not a scope. + // + // Should be throw an error? Well... The use case is the following: + // + // // SELECT book.*, author.* FROM book + // // JOIN author ON author.id = book.authorId + // let request = Book.including(required: Book.author) + // + // Rows loaded from this request don't have any "book" key: + // + // let row = try Row.fetchOne(db, request)! + // print(row.debugDescription) + // // ▿ [id:1 title:"Moby-Dick" authorId:2] + // // unadapted: [id:1 title:"Moby-Dick" authorId:2 id:2 name:"Melville"] + // // author: [id:2 name:"Melville"] + // + // And yet we have to decode the "book" key when we decode the + // BookInfo type below: + // + // struct BookInfo { + // var book: Book // <- decodes from the "book" key + // var author: Author + // } + // let infos = try BookInfos.fetchAll(db, request) + // + // Our current strategy is to assume that a missing key (such as + // "book", which is not the name of a column, and not the name of a + // scope) has to be decoded right from the base row. + // + // Yeah, there may be better ways to handle this. + return try decode(type, fromRow: row, codingPath: codingPath + [key]) } - - // Scope? - if let scopedRow = row.scopesTree[keyName] { + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + fatalError("not implemented") + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + throw DecodingError.typeMismatch( + UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + } + + func superDecoder() throws -> Decoder { + // Not sure + return decoder + } + + func superDecoder(forKey key: Key) throws -> Decoder { + fatalError("not implemented") + } + + // Helper methods + + @inline(__always) + private func decode(_ type: T.Type, fromRow row: Row, codingPath: [CodingKey]) throws -> T where T: Decodable { if let type = T.self as? FetchableRecord.Type { // Prefer FetchableRecord decoding over Decodable. - // This allows custom row decoding - return type.init(row: scopedRow) as! T + return type.init(row: row) as! T } else { - return try T(from: RowDecoder(row: scopedRow, codingPath: codingPath + [key])) + let decoder = RowDecoder(row: row, codingPath: codingPath) + return try T(from: decoder) } } - // Base row - if let type = T.self as? FetchableRecord.Type { - // Prefer FetchableRecord decoding over Decodable. - // This allows custom row decoding - return type.init(row: row) as! T - } else { - return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + @inline(__always) + private func decode(_ type: T.Type, fromColumnAtIndex index: Int, key: Key) throws -> T where T: Decodable { + let row = decoder.row + do { + // This decoding will fail for types that decode from keyed + // or unkeyed containers, because we're decoding a single + // value here (string, int, double, data, null). If such an + // error happens, we'll switch to JSON decoding. + let columnDecoder = ColumnDecoder( + row: row, + columnIndex: index, + codingPath: codingPath + [key]) + return try T(from: columnDecoder) + } catch is JSONRequiredError { + // Decode from JSON + guard let data = row.dataNoCopy(atIndex: index) else { + fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) + } + return try Record + .databaseJSONDecoder(for: key.stringValue) + .decode(type.self, from: data) + } } } +} + +// MARK: - ColumnDecoder + +/// The decoder that decodes from a database column +private struct ColumnDecoder: Decoder { + var row: Row + var columnIndex: Int + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] { return Record.databaseDecodingUserInfo } - /// Returns the data stored for the given key as represented in a container keyed by the given key type. - /// - /// - parameter type: The key type to use for the container. - /// - parameter key: The key that the nested container is associated with. - /// - returns: A keyed decoding container view into `self`. - /// - throws: `DecodingError.typeMismatch` if the encountered stored value is not a keyed container. - func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { - fatalError("not implemented") - } - - /// Returns the data stored for the given key as represented in an unkeyed container. - /// - /// - parameter key: The key that the nested container is associated with. - /// - returns: An unkeyed decoding container view into `self`. - /// - throws: `DecodingError.typeMismatch` if the encountered stored value is not an unkeyed container. - func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { - throw DecodingError.typeMismatch( - UnkeyedDecodingContainer.self, - DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + // We need to switch to JSON decoding + throw JSONRequiredError() } - /// Returns a `Decoder` instance for decoding `super` from the container associated with the default `super` key. - /// - /// Equivalent to calling `superDecoder(forKey:)` with `Key(stringValue: "super", intValue: 0)`. - /// - /// - returns: A new `Decoder` to pass to `super.init(from:)`. - /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the default `super` key. - /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the default `super` key. - public func superDecoder() throws -> Decoder { - return decoder + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + // We need to switch to JSON decoding + throw JSONRequiredError() } - /// Returns a `Decoder` instance for decoding `super` from the container associated with the given key. - /// - /// - parameter key: The key to decode `super` for. - /// - returns: A new `Decoder` to pass to `super.init(from:)`. - /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. - /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key. - public func superDecoder(forKey key: Key) throws -> Decoder { - return decoder + func singleValueContainer() throws -> SingleValueDecodingContainer { + return self } } -private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { - var row: Row - var columnIndex: Int - var codingPath: [CodingKey] - - /// Decodes a null value. - /// - /// - returns: Whether the encountered value was null. +extension ColumnDecoder: SingleValueDecodingContainer { func decodeNil() -> Bool { return row.hasNull(atIndex: columnIndex) } - - /// Decodes a single value of the given type. - /// - /// - parameter type: The type to decode as. - /// - returns: A value of the requested type. - /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. - /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. - func decode(_ type: Bool.Type) throws -> Bool { return row[columnIndex] } - func decode(_ type: Int.Type) throws -> Int { return row[columnIndex] } - func decode(_ type: Int8.Type) throws -> Int8 { return row[columnIndex] } - func decode(_ type: Int16.Type) throws -> Int16 { return row[columnIndex] } - func decode(_ type: Int32.Type) throws -> Int32 { return row[columnIndex] } - func decode(_ type: Int64.Type) throws -> Int64 { return row[columnIndex] } - func decode(_ type: UInt.Type) throws -> UInt { return row[columnIndex] } - func decode(_ type: UInt8.Type) throws -> UInt8 { return row[columnIndex] } + + func decode(_ type: Bool.Type ) throws -> Bool { return row[columnIndex] } + func decode(_ type: Int.Type ) throws -> Int { return row[columnIndex] } + func decode(_ type: Int8.Type ) throws -> Int8 { return row[columnIndex] } + func decode(_ type: Int16.Type ) throws -> Int16 { return row[columnIndex] } + func decode(_ type: Int32.Type ) throws -> Int32 { return row[columnIndex] } + func decode(_ type: Int64.Type ) throws -> Int64 { return row[columnIndex] } + func decode(_ type: UInt.Type ) throws -> UInt { return row[columnIndex] } + func decode(_ type: UInt8.Type ) throws -> UInt8 { return row[columnIndex] } func decode(_ type: UInt16.Type) throws -> UInt16 { return row[columnIndex] } func decode(_ type: UInt32.Type) throws -> UInt32 { return row[columnIndex] } func decode(_ type: UInt64.Type) throws -> UInt64 { return row[columnIndex] } - func decode(_ type: Float.Type) throws -> Float { return row[columnIndex] } + func decode(_ type: Float.Type ) throws -> Float { return row[columnIndex] } func decode(_ type: Double.Type) throws -> Double { return row[columnIndex] } func decode(_ type: String.Type) throws -> String { return row[columnIndex] } - - /// Decodes a single value of the given type. - /// - /// - parameter type: The type to decode as. - /// - returns: A value of the requested type. - /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. - /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. + func decode(_ type: T.Type) throws -> T where T : Decodable { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. @@ -258,62 +253,18 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { } else if let type = T.self as? DatabaseValueConvertible.Type { return type.decode(from: row, atUncheckedIndex: columnIndex) as! T } else { - return try T(from: RowSingleValueDecoder(row: row, columnIndex: columnIndex, codingPath: codingPath)) + return try T(from: self) } } } -private struct RowDecoder: Decoder { - var row: Row - var codingPath: [CodingKey] - var userInfo: [CodingUserInfoKey : Any] { return [:] } - - func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - return KeyedDecodingContainer(RowKeyedDecodingContainer(decoder: self)) - } - - func unkeyedContainer() throws -> UnkeyedDecodingContainer { - throw JSONRequiredError() - } - - func singleValueContainer() throws -> SingleValueDecodingContainer { - throw JSONRequiredError() - } -} - -private struct RowSingleValueDecoder: Decoder { - var row: Row - var columnIndex: Int - var codingPath: [CodingKey] - var userInfo: [CodingUserInfoKey : Any] { return [:] } - - func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - throw JSONRequiredError() - } - - func unkeyedContainer() throws -> UnkeyedDecodingContainer { - throw JSONRequiredError() - } - - func singleValueContainer() throws -> SingleValueDecodingContainer { - return RowSingleValueDecodingContainer(row: row, columnIndex: columnIndex, codingPath: codingPath) - } -} - /// The error that triggers JSON decoding private struct JSONRequiredError: Error { } -private func makeJSONDecoder() -> JSONDecoder { - let encoder = JSONDecoder() - encoder.dataDecodingStrategy = .base64 - encoder.dateDecodingStrategy = .millisecondsSince1970 - encoder.nonConformingFloatDecodingStrategy = .throw - return encoder -} - extension FetchableRecord where Self: Decodable { /// Initializes a record from `row`. public init(row: Row) { - try! self.init(from: RowDecoder(row: row, codingPath: [])) + let decoder = RowDecoder(row: row, codingPath: []) + try! self.init(from: decoder) } } diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 28d6a1e009..bd8e313e59 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -1,3 +1,4 @@ +import Foundation #if SWIFT_PACKAGE import CSQLite #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER @@ -24,12 +25,95 @@ /// FetchableRecord is adopted by Record. public protocol FetchableRecord { + // MARK: - Row Decoding + /// Creates a record from `row`. /// /// For performance reasons, the row argument may be reused during the /// iteration of a fetch query. If you want to keep the row for later use, /// make sure to store a copy: `self.row = row.copy()`. init(row: Row) + + // MARK: - Customizing the Format of Database Columns + + /// When the FetchableRecord type also adopts the standard Decodable + /// protocol, you can use this dictionary to customize the decoding process + /// from database rows. + /// + /// For example: + /// + /// // A key that holds a decoder's name + /// let decoderName = CodingUserInfoKey(rawValue: "decoderName")! + /// + /// // A FetchableRecord + Decodable record + /// struct Player: FetchableRecord, Decodable { + /// // Customize the decoder name when decoding a database row + /// static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "Database"] + /// + /// init(from decoder: Decoder) throws { + /// // Print the decoder name + /// print(decoder.userInfo[decoderName]) + /// ... + /// } + /// } + /// + /// // prints "Database" + /// let player = try Player.fetchOne(db, ...) + /// + /// // prints "JSON" + /// let decoder = JSONDecoder() + /// decoder.userInfo = [decoderName: "JSON"] + /// let player = try decoder.decode(Player.self, from: ...) + static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { get } + + /// When the FetchableRecord type also adopts the standard Decodable + /// protocol, this method controls the decoding process of nested properties + /// from JSON database columns. + /// + /// The default implementation returns a JSONDecoder with the + /// following properties: + /// + /// - dataDecodingStrategy: .base64 + /// - dateDecodingStrategy: .millisecondsSince1970 + /// - nonConformingFloatDecodingStrategy: .throw + /// + /// You can override those defaults: + /// + /// struct Achievement: Decodable { + /// var name: String + /// var date: Date + /// } + /// + /// struct Player: Decodable, FetchableRecord { + /// // stored in a JSON column + /// var achievements: [Achievement] + /// + /// static func databaseJSONDecoder(for column: String) -> JSONDecoder { + /// let decoder = JSONDecoder() + /// decoder.dateDecodingStrategy = .iso8601 + /// return decoder + /// } + /// } + static func databaseJSONDecoder(for column: String) -> JSONDecoder +} + +extension FetchableRecord { + public static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { + return [:] + } + + /// Returns a JSONDecoder with the following properties: + /// + /// - dataDecodingStrategy: .base64 + /// - dateDecodingStrategy: .millisecondsSince1970 + /// - nonConformingFloatDecodingStrategy: .throw + public static func databaseJSONDecoder(for column: String) -> JSONDecoder { + let decoder = JSONDecoder() + decoder.dataDecodingStrategy = .base64 + decoder.dateDecodingStrategy = .millisecondsSince1970 + decoder.nonConformingFloatDecodingStrategy = .throw + return decoder + } } /// A cursor of records. For example: diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index 8f77eb1d6f..046e0272b7 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -1,177 +1,212 @@ import Foundation -private struct PersistableRecordKeyedEncodingContainer : KeyedEncodingContainerProtocol { - let encode: DatabaseValuePersistenceEncoder - - init(encode: @escaping DatabaseValuePersistenceEncoder) { - self.encode = encode - } - - /// The path of coding keys taken to get to this point in encoding. - /// A `nil` value indicates an unkeyed container. +// MARK: - RecordEncoder + +/// The encoder that encodes a record into GRDB's PersistenceContainer +private class RecordEncoder: Encoder { var codingPath: [CodingKey] { return [] } + var userInfo: [CodingUserInfoKey: Any] { return Record.databaseEncodingUserInfo } + private var _persistenceContainer: PersistenceContainer + var persistenceContainer: PersistenceContainer { return _persistenceContainer } - /// Encodes the given value for the given key. - /// - /// - parameter value: The value to encode. - /// - parameter key: The key to associate the value with. - /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. - mutating func encode(_ value: Bool, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: Int, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: Int8, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: Int16, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: Int32, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: Int64, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: UInt, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: UInt8, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: UInt16, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: UInt32, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: UInt64, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: Float, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: Double, forKey key: Key) throws { encode(value, key.stringValue) } - mutating func encode(_ value: String, forKey key: Key) throws { encode(value, key.stringValue) } - - /// Encodes the given value for the given key. - /// - /// - parameter value: The value to encode. - /// - parameter key: The key to associate the value with. - /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. - mutating func encode(_ value: T, forKey key: Key) throws where T : Encodable { - if let dbValueConvertible = value as? DatabaseValueConvertible { - // Prefer DatabaseValueConvertible encoding over Decodable. - // This allows us to encode Date as String, for example. - encode(dbValueConvertible.databaseValue, key.stringValue) - } else { - do { - // This encoding will fail for types that encode into keyed - // or unkeyed containers, because we're encoding a single - // value here (string, int, double, data, null). If such an - // error happens, we'll switch to JSON encoding. - try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) - } catch is JSONRequiredError { - // Encode to JSON - let jsonData = try makeJSONEncoder().encode(value) - - // Store JSON String in the database for easier debugging and - // database inspection. Thanks to SQLite weak typing, we won't - // have any trouble decoding this string into data when we - // eventually perform JSON decoding. - // TODO: possible optimization: avoid this conversion to string, and store raw data bytes as an SQLite string - let jsonString = String(data: jsonData, encoding: .utf8)! // force unwrap because json data is guaranteed to convert to String - encode(jsonString, key.stringValue) - } - } + init() { + _persistenceContainer = PersistenceContainer() } - // Provide explicit encoding of optionals, because default implementation does not encode nil values. - mutating func encodeNil(forKey key: Key) throws { encode(nil, key.stringValue) } - mutating func encodeIfPresent(_ value: Bool?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: Int?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: Int8?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: Int16?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: Int32?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: Int64?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: UInt?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: Float?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: Double?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: String?, forKey key: Key) throws { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } - mutating func encodeIfPresent(_ value: T?, forKey key: Key) throws where T : Encodable { if let value = value { try encode(value, forKey: key) } else { try encodeNil(forKey: key) } } + /// Helper method + @inline(__always) + fileprivate func encode(_ value: DatabaseValueConvertible?, forKey key: CodingKey) { + _persistenceContainer[key.stringValue] = value + } - /// Stores a keyed encoding container for the given key and returns it. - /// - /// - parameter keyType: The key type to use for the container. - /// - parameter key: The key to encode the container for. - /// - returns: A new keyed encoding container. - mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { - fatalError("Not implemented") + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + let container = KeyedContainer(recordEncoder: self) + return KeyedEncodingContainer(container) } - /// Stores an unkeyed encoding container for the given key and returns it. - /// - /// - parameter key: The key to encode the container for. - /// - returns: A new unkeyed encoding container. - mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - fatalError("Not implemented") + func unkeyedContainer() -> UnkeyedEncodingContainer { + fatalError("unkeyed encoding is not supported") } - /// Stores a new nested container for the default `super` key and returns a new `Encoder` instance for encoding `super` into that container. - /// - /// Equivalent to calling `superEncoder(forKey:)` with `Key(stringValue: "super", intValue: 0)`. - /// - /// - returns: A new `Encoder` to pass to `super.encode(to:)`. - mutating func superEncoder() -> Encoder { - fatalError("Not implemented") + func singleValueContainer() -> SingleValueEncodingContainer { + // @itaiferber on https://forums.swift.org/t/how-to-encode-objects-of-unknown-type/12253/11 + // + // > Encoding a value into a single-value container is equivalent to + // > encoding the value directly into the encoder, with the primary + // > difference being the above: encoding into the encoder writes the + // > contents of a type into the encoder, while encoding to a + // > single-value container gives the encoder a chance to intercept the + // > type as a whole. + // + // Wait for somebody hitting this fatal error so that we can write a + // meaningful regression test. + fatalError("single value encoding is not supported") } - /// Stores a new nested container for the given key and returns a new `Encoder` instance for encoding `super` into that container. - /// - /// - parameter key: The key to encode `super` for. - /// - returns: A new `Encoder` to pass to `super.encode(to:)`. - mutating func superEncoder(forKey key: Key) -> Encoder { - fatalError("Not implemented") + private struct KeyedContainer : KeyedEncodingContainerProtocol { + var recordEncoder: RecordEncoder + var userInfo: [CodingUserInfoKey: Any] { return Record.databaseEncodingUserInfo } + + init(recordEncoder: RecordEncoder) { + self.recordEncoder = recordEncoder + } + + var codingPath: [CodingKey] { return [] } + + func encode(_ value: Bool, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int8, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int16, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int32, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int64, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt8, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt16, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt32, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt64, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Float, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Double, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: String, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + + func encode(_ value: T, forKey key: Key) throws where T : Encodable { + if let value = value as? DatabaseValueConvertible { + // Prefer DatabaseValueConvertible encoding over Decodable. + // This allows us to encode Date as String, for example. + recordEncoder.encode(value.databaseValue, forKey: key) + } else { + do { + // This encoding will fail for types that encode into keyed + // or unkeyed containers, because we're encoding a single + // value here (string, int, double, data, null). If such an + // error happens, we'll switch to JSON encoding. + let encoder = ColumnEncoder(recordEncoder: recordEncoder, key: key) + try value.encode(to: encoder) + } catch is JSONRequiredError { + // Encode to JSON + let jsonData = try Record.databaseJSONEncoder(for: key.stringValue).encode(value) + + // Store JSON String in the database for easier debugging and + // database inspection. Thanks to SQLite weak typing, we won't + // have any trouble decoding this string into data when we + // eventually perform JSON decoding. + // TODO: possible optimization: avoid this conversion to string, and store raw data bytes as an SQLite string + let jsonString = String(data: jsonData, encoding: .utf8)! // force unwrap because json data is guaranteed to convert to String + recordEncoder.encode(jsonString, forKey: key) + } + } + } + + func encodeNil(forKey key: Key) throws { recordEncoder.encode(nil, forKey: key) } + + func encodeIfPresent(_ value: Bool?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: Int?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: Int8?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: Int16?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: Int32?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: Int64?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: UInt?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: Float?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: Double?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + func encodeIfPresent(_ value: String?, forKey key: Key) throws { recordEncoder.encode(value, forKey: key) } + + func encodeIfPresent(_ value: T?, forKey key: Key) throws where T : Encodable { + if let value = value { + try encode(value, forKey: key) + } else { + recordEncoder.encode(nil, forKey: key) + } + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + fatalError("Not implemented") + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + fatalError("Not implemented") + } + + func superEncoder() -> Encoder { + fatalError("Not implemented") + } + + func superEncoder(forKey key: Key) -> Encoder { + fatalError("Not implemented") + } } } -private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { - var codingPath: [CodingKey] { return [key] } +// MARK: - ColumnEncoder + +/// The encoder that encodes into a database column +private struct ColumnEncoder: Encoder { + var recordEncoder: RecordEncoder var key: CodingKey - var encode: DatabaseValuePersistenceEncoder + var codingPath: [CodingKey] { return [key] } + var userInfo: [CodingUserInfoKey: Any] { return Record.databaseEncodingUserInfo } - init(key: CodingKey, encode: @escaping DatabaseValuePersistenceEncoder) { + init(recordEncoder: RecordEncoder, key: CodingKey) { + self.recordEncoder = recordEncoder self.key = key - self.encode = encode } - /// Encodes a null value. - /// - /// - throws: `EncodingError.invalidValue` if a null value is invalid in the current context for this format. - /// - precondition: May not be called after a previous `self.encode(_:)` call. - func encodeNil() throws { encode(nil, key.stringValue) } + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + // Keyed values require JSON encoding: we need to throw + // JSONRequiredError. Since we can't throw right from here, let's + // delegate the job to a dedicated container. + let container = JSONRequiredEncoder.KeyedContainer(codingPath: codingPath) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + // Keyed values require JSON encoding: we need to throw + // JSONRequiredError. Since we can't throw right from here, let's + // delegate the job to a dedicated container. + return JSONRequiredEncoder(codingPath: codingPath) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + return self + } +} + +extension ColumnEncoder: SingleValueEncodingContainer { + func encodeNil() throws { recordEncoder.encode(nil, forKey: key) } - /// Encodes a single value of the given type. - /// - /// - parameter value: The value to encode. - /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. - /// - precondition: May not be called after a previous `self.encode(_:)` call. - func encode(_ value: Bool) throws { encode(value, key.stringValue) } - func encode(_ value: Int) throws { encode(value, key.stringValue) } - func encode(_ value: Int8) throws { encode(value, key.stringValue) } - func encode(_ value: Int16) throws { encode(value, key.stringValue) } - func encode(_ value: Int32) throws { encode(value, key.stringValue) } - func encode(_ value: Int64) throws { encode(value, key.stringValue) } - func encode(_ value: UInt) throws { encode(value, key.stringValue) } - func encode(_ value: UInt8) throws { encode(value, key.stringValue) } - func encode(_ value: UInt16) throws { encode(value, key.stringValue) } - func encode(_ value: UInt32) throws { encode(value, key.stringValue) } - func encode(_ value: UInt64) throws { encode(value, key.stringValue) } - func encode(_ value: Float) throws { encode(value, key.stringValue) } - func encode(_ value: Double) throws { encode(value, key.stringValue) } - func encode(_ value: String) throws { encode(value, key.stringValue) } + func encode(_ value: Bool ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int8 ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int16 ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int32 ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Int64 ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt8 ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt16) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt32) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: UInt64) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Float ) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: Double) throws { recordEncoder.encode(value, forKey: key) } + func encode(_ value: String) throws { recordEncoder.encode(value, forKey: key) } - /// Encodes a single value of the given type. - /// - /// - parameter value: The value to encode. - /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. - /// - precondition: May not be called after a previous `self.encode(_:)` call. func encode(_ value: T) throws where T : Encodable { - if let dbValueConvertible = value as? DatabaseValueConvertible { + if let value = value as? DatabaseValueConvertible { // Prefer DatabaseValueConvertible encoding over Decodable. // This allows us to encode Date as String, for example. - encode(dbValueConvertible.databaseValue, key.stringValue) + recordEncoder.encode(value.databaseValue, forKey: key) } else { do { // This encoding will fail for types that encode into keyed // or unkeyed containers, because we're encoding a single // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON encoding. - try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) + let encoder = ColumnEncoder(recordEncoder: recordEncoder, key: key) + try value.encode(to: encoder) } catch is JSONRequiredError { // Encode to JSON - let jsonData = try makeJSONEncoder().encode(value) + let jsonData = try Record.databaseJSONEncoder(for: key.stringValue).encode(value) // Store JSON String in the database for easier debugging and // database inspection. Thanks to SQLite weak typing, we won't @@ -179,208 +214,119 @@ private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { // eventually perform JSON decoding. // TODO: possible optimization: avoid this conversion to string, and store raw data bytes as an SQLite string let jsonString = String(data: jsonData, encoding: .utf8)! // force unwrap because json data is guaranteed to convert to String - encode(jsonString, key.stringValue) + recordEncoder.encode(jsonString, forKey: key) } } } } -private struct DatabaseValueEncoder: Encoder { - var codingPath: [CodingKey] { return [key] } - var userInfo: [CodingUserInfoKey: Any] = [:] - var key: CodingKey - var encode: DatabaseValuePersistenceEncoder - - init(key: CodingKey, encode: @escaping DatabaseValuePersistenceEncoder) { - self.key = key - self.encode = encode - } +// MARK: - JSONRequiredEncoder - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { - // Keyed values require JSON encoding: we need to throw - // JSONRequiredError. Since we can't throw right from here, let's - // delegate the job to a dedicated container. - return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath)) - } - - func unkeyedContainer() -> UnkeyedEncodingContainer { - // Keyed values require JSON encoding: we need to throw - // JSONRequiredError. Since we can't throw right from here, let's - // delegate the job to a dedicated container. - return JSONRequiredUnkeyedContainer(codingPath: codingPath) - } - - func singleValueContainer() -> SingleValueEncodingContainer { - return DatabaseValueEncodingContainer(key: key, encode: encode) - } -} - -private struct PersistableRecordEncoder: Encoder { - var codingPath: [CodingKey] = [] - var userInfo: [CodingUserInfoKey: Any] = [:] - var encode: DatabaseValuePersistenceEncoder - - init(encode: @escaping DatabaseValuePersistenceEncoder) { - self.encode = encode - } - - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { - return KeyedEncodingContainer(PersistableRecordKeyedEncodingContainer(encode: encode)) - } - - func unkeyedContainer() -> UnkeyedEncodingContainer { - fatalError("unkeyed encoding is not supported") - } - - func singleValueContainer() -> SingleValueEncodingContainer { - fatalError("unkeyed encoding is not supported") - } -} +/// The error that triggers JSON encoding +private struct JSONRequiredError: Error { } -private struct JSONRequiredEncoder: Encoder { +/// The encoder that always ends up with a JSONRequiredError +private struct JSONRequiredEncoder: Encoder { var codingPath: [CodingKey] - var userInfo: [CodingUserInfoKey: Any] = [:] + var userInfo: [CodingUserInfoKey: Any] { return Record.databaseEncodingUserInfo } init(codingPath: [CodingKey]) { self.codingPath = codingPath } func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { - return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath)) + let container = KeyedContainer(codingPath: codingPath) + return KeyedEncodingContainer(container) } func unkeyedContainer() -> UnkeyedEncodingContainer { - return JSONRequiredUnkeyedContainer(codingPath: codingPath) + return self } func singleValueContainer() -> SingleValueEncodingContainer { - return JSONRequiredSingleValueContainer() - } -} - -private struct JSONRequiredKeyedContainer: KeyedEncodingContainerProtocol { - var codingPath: [CodingKey] - - func encodeNil(forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Bool, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Int, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Int8, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Int16, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Int32, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Int64, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: UInt, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: UInt8, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: UInt16, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: UInt32, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: UInt64, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Float, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: Double, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: String, forKey key: KeyType) throws { throw JSONRequiredError() } - func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw JSONRequiredError() } - - func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: KeyType) -> KeyedEncodingContainer where NestedKey : CodingKey { - return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath + [key])) - } - - func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { - return JSONRequiredUnkeyedContainer(codingPath: codingPath) - } - - func superEncoder() -> Encoder { - return JSONRequiredEncoder(codingPath: codingPath) + return self } - func superEncoder(forKey key: KeyType) -> Encoder { - return JSONRequiredEncoder(codingPath: codingPath) + struct KeyedContainer: KeyedEncodingContainerProtocol { + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] { return Record.databaseEncodingUserInfo } + + func encodeNil(forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Bool, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int8, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int16, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int32, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int64, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt8, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt16, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt32, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt64, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Float, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Double, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: String, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw JSONRequiredError() } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: KeyType) -> KeyedEncodingContainer where NestedKey : CodingKey { + let container = KeyedContainer(codingPath: codingPath + [key]) + return KeyedEncodingContainer(container) + } + + func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { + return JSONRequiredEncoder(codingPath: codingPath) + } + + func superEncoder() -> Encoder { + return JSONRequiredEncoder(codingPath: codingPath) + } + + func superEncoder(forKey key: KeyType) -> Encoder { + return JSONRequiredEncoder(codingPath: codingPath) + } } } -private struct JSONRequiredUnkeyedContainer: UnkeyedEncodingContainer { - var codingPath: [CodingKey] - var count: Int { return 0 } - +extension JSONRequiredEncoder: SingleValueEncodingContainer { func encodeNil() throws { throw JSONRequiredError() } - func encode(_ value: Bool) throws { throw JSONRequiredError() } - func encode(_ value: Int) throws { throw JSONRequiredError() } - func encode(_ value: Int8) throws { throw JSONRequiredError() } - func encode(_ value: Int16) throws { throw JSONRequiredError() } - func encode(_ value: Int32) throws { throw JSONRequiredError() } - func encode(_ value: Int64) throws { throw JSONRequiredError() } - func encode(_ value: UInt) throws { throw JSONRequiredError() } - func encode(_ value: UInt8) throws { throw JSONRequiredError() } + func encode(_ value: Bool ) throws { throw JSONRequiredError() } + func encode(_ value: Int ) throws { throw JSONRequiredError() } + func encode(_ value: Int8 ) throws { throw JSONRequiredError() } + func encode(_ value: Int16 ) throws { throw JSONRequiredError() } + func encode(_ value: Int32 ) throws { throw JSONRequiredError() } + func encode(_ value: Int64 ) throws { throw JSONRequiredError() } + func encode(_ value: UInt ) throws { throw JSONRequiredError() } + func encode(_ value: UInt8 ) throws { throw JSONRequiredError() } func encode(_ value: UInt16) throws { throw JSONRequiredError() } func encode(_ value: UInt32) throws { throw JSONRequiredError() } func encode(_ value: UInt64) throws { throw JSONRequiredError() } - func encode(_ value: Float) throws { throw JSONRequiredError() } + func encode(_ value: Float ) throws { throw JSONRequiredError() } func encode(_ value: Double) throws { throw JSONRequiredError() } func encode(_ value: String) throws { throw JSONRequiredError() } func encode(_ value: T) throws where T : Encodable { throw JSONRequiredError() } +} + +extension JSONRequiredEncoder: UnkeyedEncodingContainer { + var count: Int { return 0 } - func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { - return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath)) + mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + let container = KeyedContainer(codingPath: codingPath) + return KeyedEncodingContainer(container) } - func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { return self } - func superEncoder() -> Encoder { - return JSONRequiredEncoder(codingPath: codingPath) - } -} - -private struct JSONRequiredSingleValueContainer: SingleValueEncodingContainer { - var codingPath: [CodingKey] { return [] } - - mutating func encodeNil() throws { throw JSONRequiredError() } - mutating func encode(_ value: Bool) throws { throw JSONRequiredError() } - mutating func encode(_ value: String) throws { throw JSONRequiredError() } - mutating func encode(_ value: Double) throws { throw JSONRequiredError() } - mutating func encode(_ value: Float) throws { throw JSONRequiredError() } - mutating func encode(_ value: Int) throws { throw JSONRequiredError() } - mutating func encode(_ value: Int8) throws { throw JSONRequiredError() } - mutating func encode(_ value: Int16) throws { throw JSONRequiredError() } - mutating func encode(_ value: Int32) throws { throw JSONRequiredError() } - mutating func encode(_ value: Int64) throws { throw JSONRequiredError() } - mutating func encode(_ value: UInt) throws { throw JSONRequiredError() } - mutating func encode(_ value: UInt8) throws { throw JSONRequiredError() } - mutating func encode(_ value: UInt16) throws { throw JSONRequiredError() } - mutating func encode(_ value: UInt32) throws { throw JSONRequiredError() } - mutating func encode(_ value: UInt64) throws { throw JSONRequiredError() } - mutating func encode(_ value: T) throws where T : Encodable { throw JSONRequiredError() } -} - -/// The error that triggers JSON encoding -private struct JSONRequiredError: Error { } - -private typealias DatabaseValuePersistenceEncoder = (_ value: DatabaseValueConvertible?, _ key: String) -> Void - -private func makeJSONEncoder() -> JSONEncoder { - let encoder = JSONEncoder() - encoder.dataEncodingStrategy = .base64 - encoder.dateEncodingStrategy = .millisecondsSince1970 - encoder.nonConformingFloatEncodingStrategy = .throw - if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { - // guarantee some stability in order to ease record comparison - encoder.outputFormatting = .sortedKeys + mutating func superEncoder() -> Encoder { + return self } - return encoder } extension MutablePersistableRecord where Self: Encodable { public func encode(to container: inout PersistenceContainer) { - // The inout container parameter won't enter an escaping closure since - // SE-0035: https://github.com/apple/swift-evolution/blob/master/proposals/0035-limit-inout-capture.md - // - // So let's use it in a non-escaping closure: - func encode(_ encode: DatabaseValuePersistenceEncoder) { - withoutActuallyEscaping(encode) { escapableEncode in - let encoder = PersistableRecordEncoder(encode: escapableEncode) - try! self.encode(to: encoder) - } - } - encode { (value, key) in - container[key] = value - } + let encoder = RecordEncoder() + try! encode(to: encoder) + container = encoder.persistenceContainer } } diff --git a/GRDB/Record/PersistableRecord.swift b/GRDB/Record/PersistableRecord.swift index eea99b42d4..d27adaf385 100644 --- a/GRDB/Record/PersistableRecord.swift +++ b/GRDB/Record/PersistableRecord.swift @@ -1,3 +1,5 @@ +import Foundation + extension Database.ConflictResolution { var invalidatesLastInsertedRowID: Bool { switch self { @@ -320,6 +322,95 @@ public protocol MutablePersistableRecord : TableRecord { /// - returns: Whether the primary key matches a row in the database. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. func exists(_ db: Database) throws -> Bool + + // MARK: - Customizing the Format of Database Columns + + /// When the PersistableRecord type also adopts the standard Encodable + /// protocol, you can use this dictionary to customize the encoding process + /// into database rows. + /// + /// For example: + /// + /// // A key that holds a encoder's name + /// let encoderName = CodingUserInfoKey(rawValue: "encoderName")! + /// + /// // A PersistableRecord + Encodable record + /// struct Player: PersistableRecord, Encodable { + /// // Customize the encoder name when encoding a database row + /// static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [encoderName: "Database"] + /// + /// func encode(to encoder: Encoder) throws { + /// // Print the encoder name + /// print(encoder.userInfo[encoderName]) + /// ... + /// } + /// } + /// + /// let player = Player(...) + /// + /// // prints "Database" + /// try player.insert(db) + /// + /// // prints "JSON" + /// let encoder = JSONEncoder() + /// encoder.userInfo = [encoderName: "JSON"] + /// let data = try encoder.encode(player) + static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get } + + /// When the PersistableRecord type also adopts the standard Encodable + /// protocol, this method controls the encoding process of nested properties + /// into JSON database columns. + /// + /// The default implementation returns a JSONEncoder with the + /// following properties: + /// + /// - dataEncodingStrategy: .base64 + /// - dateEncodingStrategy: .millisecondsSince1970 + /// - nonConformingFloatEncodingStrategy: .throw + /// - outputFormatting: .sortedKeys (iOS 11.0+, macOS 10.13+, watchOS 4.0+) + /// + /// You can override those defaults: + /// + /// struct Achievement: Encodable { + /// var name: String + /// var date: Date + /// } + /// + /// struct Player: Encodable, PersistableRecord { + /// // stored in a JSON column + /// var achievements: [Achievement] + /// + /// static func databaseJSONEncoder(for column: String) -> JSONEncoder { + /// let encoder = JSONEncoder() + /// encoder.dateEncodingStrategy = .iso8601 + /// return encoder + /// } + /// } + static func databaseJSONEncoder(for column: String) -> JSONEncoder +} + +extension MutablePersistableRecord { + public static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { + return [:] + } + + /// Returns a JSONEncoder with the following properties: + /// + /// - dataEncodingStrategy: .base64 + /// - dateEncodingStrategy: .millisecondsSince1970 + /// - nonConformingFloatEncodingStrategy: .throw + /// - outputFormatting: .sortedKeys (iOS 11.0+, macOS 10.13+, watchOS 4.0+) + public static func databaseJSONEncoder(for column: String) -> JSONEncoder { + let encoder = JSONEncoder() + encoder.dataEncodingStrategy = .base64 + encoder.dateEncodingStrategy = .millisecondsSince1970 + encoder.nonConformingFloatEncodingStrategy = .throw + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + // guarantee some stability in order to ease record comparison + encoder.outputFormatting = .sortedKeys + } + return encoder + } } extension MutablePersistableRecord { diff --git a/README.md b/README.md index cd7cf770e1..fef8fb6797 100644 --- a/README.md +++ b/README.md @@ -2060,13 +2060,11 @@ Your custom structs and classes can adopt each protocol individually, and opt in - [TableRecord Protocol](#tablerecord-protocol) - [PersistableRecord Protocol](#persistablerecord-protocol) - [Persistence Methods](#persistence-methods) - - [Customizing the Persistence Methods](#customizing-the-persistence-methods) + - [Customizing the Persistence Methods] - [Codable Records](#codable-records) - [Record Class](#record-class) - [Record Comparison] -- [Conflict Resolution](#conflict-resolution) -- [The Implicit RowID Primary Key](#the-implicit-rowid-primary-key) -- [Customized Decoding of Database Rows](#customized-decoding-of-database-rows) +- [Record Customization Options] **Records in a Glance** @@ -2175,9 +2173,7 @@ Details follow: - [Codable Records](#codable-records) - [Record Class](#record-class) - [Record Comparison] -- [Conflict Resolution](#conflict-resolution) -- [The Implicit RowID Primary Key](#the-implicit-rowid-primary-key) -- [Customized Decoding of Database Rows](#customized-decoding-of-database-rows) +- [Record Customization Options] - [Examples of Record Definitions](#examples-of-record-definitions) - [List of Record Methods](#list-of-record-methods) @@ -2322,7 +2318,7 @@ See [fetching methods](#fetching-methods) for information about the `fetchCursor > :point_up: **Note**: for performance reasons, the same row argument to `init(row:)` is reused during the iteration of a fetch query. If you want to keep the row for later use, make sure to store a copy: `self.row = row.copy()`. -> :point_up: **Note**: The `FetchableRecord.init(row:)` initializer fits the needs of most applications. But some application are more demanding than others. When FetchableRecord does not exactly provide the support you need, have a look at the [Customized Decoding of Database Rows](#customized-decoding-of-database-rows) chapter. +> :point_up: **Note**: The `FetchableRecord.init(row:)` initializer fits the needs of most applications. But some application are more demanding than others. When FetchableRecord does not exactly provide the support you need, have a look at the [Beyond FetchableRecord] chapter. ## TableRecord Protocol @@ -2336,7 +2332,7 @@ protocol TableRecord { } ``` -The `databaseSelection` type property is optional, and documented in the [Columns Selected by a Request](#columns-selected-by-a-request) chapter. +The `databaseSelection` type property is optional, and documented in the [Columns Selected by a Request] chapter. The `databaseTableName` type property is the name of a database table. By default, it is derived from the type name: @@ -2605,7 +2601,44 @@ try dbQueue.write { db in } ``` -When a record contains a codable property that is not a simple [value](#values) (Bool, Int, String, Date, Swift enums, etc.), that value is encoded and decoded as a **JSON string**. For example: +> :point_up: **Note**: Some codable values have a different way to encode and decode themselves in a standard archive vs. a database column. For example, [Date](#date-and-datecomponents) saves itself as a numerical timestamp (archive) or a string (database). When such an ambiguity happens, GRDB always favors customized database encoding and decoding. + +> :point_up: **Note**: When your Codable record provides a custom implementation for `Decodable.init(from:)` or `Encodable.encode(to:)`, you may want to provide a `userInfo` context dictionary: see [The userInfo Dictionary]. + + +If you declare an explicit `CodingKeys` enum ([what is this?](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)), you can use coding keys as [query interface](#the-query-interface) columns, just by adding conformance to the ColumnExpression protocol: + +```swift +struct Player: Codable, FetchableRecord, PersistableRecord { + var name: String + var score: Int + + private enum CodingKeys: String, CodingKey, ColumnExpression { + case name, score + } + + static func filter(name: String) -> QueryInterfaceRequest { + return filter(CodingKeys.name == name) + } + + static var maximumScore: QueryInterfaceRequest { + return select(max(CodingKeys.score)).asRequest(of: Int.self) + } +} + +try dbQueue.read { db in + // SELECT * FROM player WHERE name = 'Arthur' + let arthur = try Player.filter(name: "Arthur").fetchOne(db) // Player? + + // SELECT MAX(score) FROM player + let maxScore = try Player.maximumScore.fetchOne(db) // Int? +} +``` + + +### JSON Columns + +When a [Codable record](#codable-records) contains a property that is not a simple [value](#values) (Bool, Int, String, Date, Swift enums, etc.), that value is encoded and decoded as a **JSON string**. For example: ```swift enum AchievementColor: String, Codable { @@ -2615,12 +2648,13 @@ enum AchievementColor: String, Codable { struct Achievement: Codable { var name: String var color: AchievementColor + var date: Date } struct Player: Codable, FetchableRecord, PersistableRecord { var name: String var score: Int - var achievements: [Achievement] // encoded as JSON + var achievements: [Achievement] // stored in a JSON column } try dbQueue.write { db in @@ -2634,35 +2668,89 @@ try dbQueue.write { db in } ``` -If you declare an explicit `CodingKeys` enum ([what is this?](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)), you can use coding keys as [query interface](#the-query-interface) columns, just by adding conformance to the ColumnExpression protocol: +GRDB uses the standard [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder) and [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder) from Foundation. By default, Data values are handled with the `.base64` strategy, Date with the `.millisecondsSince1970` strategy, and non conforming floats with the `.throw` strategy. + +You can customize the JSON format by implementing those methods: + +```swift +protocol FetchableRecord { + static func databaseJSONDecoder(for column: String) -> JSONDecoder +} + +protocol MutablePersistableRecord { + static func databaseJSONEncoder(for column: String) -> JSONEncoder +} +``` + +For example, here is how the Player type can customize the json format of its "achievements" JSON column: ```swift struct Player: Codable, FetchableRecord, PersistableRecord { var name: String var score: Int - var achievements: [Achievement] + var achievements: [Achievement] // stored in a JSON column - private enum CodingKeys: String, CodingKey, ColumnExpression { - case name, score, achievements + static func databaseJSONDecoder(for column: String) -> JSONDecoder { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder } - static func filter(name: String) -> QueryInterfaceRequest { - return filter(CodingKeys.name == name) + static func databaseJSONEncoder(for column: String) -> JSONEncoder { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .sortedKeys + return encoder } } +``` -let arthur = try dbQueue.read { db in - // SELECT * FROM player WHERE name = 'Arthur' - try Player.filter(name: "Arthur").fetchOne($0) +> :bulb: **Tip**: Make sure you set the JSONEncoder `sortedKeys` option, available from iOS 11.0+, macOS 10.13+, and watchOS 4.0+. This option makes sure that the JSON output is stable, and this helps [Record Comparison] yield the expected results. + + +### The userInfo Dictionary + +Your [Codable records](#codable-records) can be stored in the database, but they may also have other purposes. In this case, you may need to customize their implementations of `Decodable.init(from:)` and `Encodable.encode(to:)`, depending on the context. + +The recommended way to provide such context is the `userInfo` dictionary. Implement those properties: + +```swift +protocol FetchableRecord { + static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { get } +} + +protocol MutablePersistableRecord { + static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get } } ``` -> :point_up: **Note**: Some codable values have a different way to encode and decode themselves in a standard archive vs. a database column. For example, [Date](#date-and-datecomponents) saves itself as a numerical timestamp (archive) or a string (database). When such an ambiguity happens, GRDB always favors customized database encoding and decoding. +For example, here is how the Player type can customize its decoding: -> :point_up: **Note about JSON support**: GRDB uses the standard [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder) and [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder) from Foundation. Data values are handled with the `.base64` strategy, Date with the `.millisecondsSince1970` strategy, and non conforming floats with the `.throw` strategy. Check Foundation documentation for more information. +```swift +// A key that holds a decoder's name +let decoderName = CodingUserInfoKey(rawValue: "decoderName")! -> :point_up: **Note about JSON support**: JSON encoding uses the `.sortedKeys` option when available (iOS 11.0+, macOS 10.13+, watchOS 4.0+). In previous operating system versions, the ordering of JSON keys may be unstable, and this may negatively impact [Record Comparison]. +struct Player: FetchableRecord, Decodable { + init(from decoder: Decoder) throws { + // Print the decoder name + print(decoder.userInfo[decoderName]) + ... + } +} +// prints "JSON" +let decoder = JSONDecoder() +decoder.userInfo = [decoderName: "JSON"] +let player = try decoder.decode(Player.self, from: ...) + +extension Player: FetchableRecord { + // Customize the decoder name when decoding a database row + static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "Database"] +} + +// prints "Database" +let player = try Player.fetchOne(db, ...) +``` ## Record Class @@ -2798,7 +2886,20 @@ player.databaseChanges // ["score": 750] For an efficient algorithm which synchronizes the content of a database table with a JSON payload, check [JSONSynchronization.playground](Playgrounds/JSONSynchronization.playground/Contents.swift). -## Conflict Resolution +## Record Customization Options + +GRDB records come with many default behaviors, that are designed to fit most situations. Many of those defaults can be customized for your specific needs: + +- [Columns Selected by a Request]: define which columns are selected by requests such as `Player.fetchAll(db)`. +- [Customizing the Persistence Methods]: define what happens when you call a persistance method such as `player.insert(db)` +- [Conflict Resolution]: Run `INSERT OR REPLACE` queries, and generally define what happens when a persistence method violates a unique index. +- [The Implicit RowID Primary Key]: all about the special `rowid` column. +- [JSON Columns]: control the format of JSON columns. +- [The userInfo Dictionary]: adapt your Codable implementation for the database. +- [Beyond FetchableRecord]: the FetchableRecord protocol is not the end of the story. + + +### Conflict Resolution **Insertions and updates can create conflicts**: for example, a query may attempt to insert a duplicate row that violates a unique index. @@ -2808,7 +2909,7 @@ The [five different policies](https://www.sqlite.org/lang_conflict.html) are: ab **SQLite let you specify conflict policies at two different places:** -- At the table level +- In the definition of the database table: ```swift // CREATE TABLE player ( @@ -2826,7 +2927,7 @@ The [five different policies](https://www.sqlite.org/lang_conflict.html) are: ab try db.execute("INSERT INTO player (email) VALUES (?)", arguments: ["arthur@example.com"]) ``` -- At the query level: +- In each modification query: ```swift // CREATE TABLE player ( @@ -2843,16 +2944,16 @@ The [five different policies](https://www.sqlite.org/lang_conflict.html) are: ab try db.execute("INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["arthur@example.com"]) ``` -When you want to handle conflicts at the query level, specify a custom `persistenceConflictPolicy` in your type that adopts the MutablePersistableRecord or PersistableRecord protocol. It will alter the INSERT and UPDATE queries run by the `insert`, `update` and `save` [persistence methods](#persistence-methods): +When you want to handle conflicts at the query level, specify a custom `persistenceConflictPolicy` in your type that adopts the PersistableRecord protocol. It will alter the INSERT and UPDATE queries run by the `insert`, `update` and `save` [persistence methods](#persistence-methods): ```swift protocol MutablePersistableRecord { - /// The policy that handles SQLite conflicts when records are inserted - /// or updated. + /// The policy that handles SQLite conflicts when records are + /// inserted or updated. /// - /// This property is optional: its default value uses the ABORT policy - /// for both insertions and updates, and has GRDB generate regular - /// INSERT and UPDATE queries. + /// This property is optional: its default value uses the ABORT + /// policy for both insertions and updates, so that GRDB generate + /// regular INSERT and UPDATE queries. static var persistenceConflictPolicy: PersistenceConflictPolicy { get } } @@ -2868,13 +2969,13 @@ try player.insert(db) > :point_up: **Note**: the `ignore` policy does not play well at all with the `didInsert` method which notifies the rowID of inserted records. Choose your poison: > -> - if you specify the `ignore` policy at the table level, don't implement the `didInsert` method: it will be called with some random id in case of failed insert. +> - if you specify the `ignore` policy in the database table definition, don't implement the `didInsert` method: it will be called with some random id in case of failed insert. > - if you specify the `ignore` policy at the query level, the `didInsert` method is never called. > -> :warning: **Warning**: [`ON CONFLICT REPLACE`](https://www.sqlite.org/lang_conflict.html) may delete rows so that inserts and updates can succeed. Those deletions are not reported to [transaction observers](#transactionobserver-protocol) (this might change in a future release of SQLite). +> :point_up: **Note**: The `replace` policy may have to delete rows so that inserts and updates can succeed. Those deletions are not reported to [transaction observers](#transactionobserver-protocol) (this might change in a future release of SQLite). -## The Implicit RowID Primary Key +### The Implicit RowID Primary Key **All SQLite tables have a primary key.** Even when the primary key is not explicit: @@ -2906,7 +3007,7 @@ try Book.deleteOne(db, key: 1) ``` -### Exposing the RowID Column +#### Exposing the RowID Column **By default, a record type that wraps a table without any explicit primary key doesn't know about the hidden rowid column.** @@ -2991,7 +3092,7 @@ When SQLite won't let you provide an explicit primary key (as in [full-text](#fu ``` -## Customized Decoding of Database Rows +### Beyond FetchableRecord **Some GRDB users eventually discover that the [FetchableRecord] protocol does not fit all situations.** Use cases that are not well handled by FetchableRecord include: @@ -3321,7 +3422,6 @@ So don't miss the [SQL API](#sqlite-api). - [Database Schema](#database-schema) - [Requests](#requests) - - [Columns Selected by a Request](#columns-selected-by-a-request) - [Expressions](#expressions) - [SQL Operators](#sql-operators) - [SQL Functions](#sql-functions) @@ -3553,15 +3653,7 @@ You can now build requests with the following methods: `all`, `none`, `select`, The hidden `rowid` column can be selected as well [when you need it](#the-implicit-rowid-primary-key). -- `select(expression, ...)` defines the selected columns. - - ```swift - // SELECT id, name FROM player - Player.select(idColumn, nameColumn) - - // SELECT MAX(score) AS maxScore FROM player - Player.select(max(scoreColumn).aliased("maxScore")) - ``` +- `select(...)` defines the selected columns. See [Columns Selected by a Request]. - `distinct()` performs uniquing. @@ -4290,7 +4382,7 @@ try request.fetchAll(db) // [Player] See [fetching methods](#fetching-methods) for information about the `fetchCursor`, `fetchAll` and `fetchOne` methods. -The RowDecoder type associated with the FetchRequest does not have to be Row, DatabaseValueConvertible, or FetchableRecord. See the [Customized Decoding of Database Rows](#customized-decoding-of-database-rows) chapter for more information. +The RowDecoder type associated with the FetchRequest does not have to be Row, DatabaseValueConvertible, or FetchableRecord. See the [Beyond FetchableRecord] chapter for more information. ## Migrations @@ -5228,7 +5320,7 @@ Our introduction above has introduced important techniques. It uses [row adapter But we may want to make it more usable and robust: 1. It's generally easier to consume records than raw rows. -2. Joined records not always need all columns from a table (see `TableRecord.databaseSelection` in [Columns Selected by a Request](#columns-selected-by-a-request)). +2. Joined records not always need all columns from a table (see `TableRecord.databaseSelection` in [Columns Selected by a Request]). 3. Building row adapters is long and error prone. To address the first bullet, let's define a record that holds our player, optional team, and maximum score. Since it can decode database rows, it adopts the [FetchableRecord] protocol: @@ -7487,7 +7579,19 @@ This protocol has been renamed [FetchableRecord] in GRDB 3.0. This protocol has been renamed [TableRecord] in GRDB 3.0. +#### Customized Decoding of Database Rows + +This chapter has been renamed [Beyond FetchableRecord]. + +[Beyond FetchableRecord]: #beyond-fetchablerecord +[Columns Selected by a Request]: #columns-selected-by-a-request +[Conflict Resolution]: #conflict-resolution +[Customizing the Persistence Methods]: #customizing-the-persistence-methods +[The Implicit RowID Primary Key]: #the-implicit-rowid-primary-key +[The userInfo Dictionary]: #the-userinfo-dictionary +[JSON Columns]: #json-columns [FetchableRecord]: #fetchablerecord-protocol [PersistableRecord]: #persistablerecord-protocol [Record Comparison]: #record-comparison +[Record Customization Options]: #record-customization-options [TableRecord]: #tablerecord-protocol diff --git a/Tests/GRDBTests/DatabaseSavepointTests.swift b/Tests/GRDBTests/DatabaseSavepointTests.swift index 64e4dbaa0e..4f47e35a9f 100644 --- a/Tests/GRDBTests/DatabaseSavepointTests.swift +++ b/Tests/GRDBTests/DatabaseSavepointTests.swift @@ -95,7 +95,6 @@ class DatabaseSavepointTests: GRDBTestCase { try db.create(table: "test") { t in t.column("value", .integer).unique() } - print(self.lastSQLQuery) try db.execute("BEGIN TRANSACTION") XCTAssertTrue(db.isInsideTransaction) try db.execute("INSERT INTO test (value) VALUES (?)", arguments: [1]) diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index fc0ae3a832..bda0dadb3c 100644 --- a/Tests/GRDBTests/DatabaseValueConversionTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionTests.swift @@ -57,9 +57,6 @@ class DatabaseValueConversionTests : GRDBTestCase { do { // test T.fetchOne let sqliteConversion = try T.fetchOne(db, sql) - if let s = sqliteConversion as? String { - print(s.utf8.map { $0 }) - } XCTAssert( sqliteConversion == expectedSQLiteConversion, "unexpected SQLite conversion: \(stringRepresentation(sqliteConversion)) instead of \(stringRepresentation(expectedSQLiteConversion))", diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index c5bae1bbf6..fcfdc109f2 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -751,3 +751,293 @@ extension FetchableRecordDecodableTests { } } } + +// 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( + "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)! + 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( + "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)! + 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( + "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)! + 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( + "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)! + test(CustomizedRecord(row: row)) + } + } +} diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index fd340eda9f..65ac337a6d 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -474,3 +474,238 @@ extension MutablePersistableRecordEncodableTests { } } } + +// 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) + } + } + + @available(OSX 10.13, iOS 11.0, *) + 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 { + // This test relies on .sortedKeys option + if #available(OSX 10.13, iOS 11.0, *) { + 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 { + // This test relies on .sortedKeys option + if #available(OSX 10.13, iOS 11.0, *) { + 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 { + // This test relies on .sortedKeys option + if #available(OSX 10.13, iOS 11.0, *) { + 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\"]"]) + } + } + } +} + diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index 3314612aaf..f06d7a95b5 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -1009,9 +1009,8 @@ extension PersistableRecordTests { XCTFail("Could not find record in db") return } - - print(fetchModel.numbers.first!) - XCTAssertEqual(model.numbers, fetchModel.numbers) + + XCTAssertEqual(model.numbers, fetchModel.numbers) } }