Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/Record/EncodableRecord.swift
T

726 lines
26 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation // For JSONEncoder
/// A type that can encode itself in a database row.
///
/// To conform to `EncodableRecord`, provide an implementation for the
/// ``encode(to:)-k9pf`` method. This implementation is ready-made for
/// `Encodable` types.
///
/// Most of the time, your record types will get `EncodableRecord` conformance
/// through the ``MutablePersistableRecord`` or ``PersistableRecord`` protocols,
/// which provide persistence methods.
///
/// ## Topics
///
/// ### Encoding a Database Row
///
/// - ``encode(to:)-k9pf``
/// - ``PersistenceContainer``
///
/// ### Configuring Persistence for the Standard Encodable Protocol
///
/// - ``databaseColumnEncodingStrategy-5sx4v``
/// - ``databaseDataEncodingStrategy-9y0c7``
/// - ``databaseDateEncodingStrategy-2gtc1``
/// - ``databaseEncodingUserInfo-8upii``
/// - ``databaseJSONEncoder(for:)-6x62c``
/// - ``databaseUUIDEncodingStrategy-2t96q``
/// - ``DatabaseColumnEncodingStrategy``
/// - ``DatabaseDataEncodingStrategy``
/// - ``DatabaseDateEncodingStrategy``
/// - ``DatabaseUUIDEncodingStrategy``
///
/// ### Converting a Record to a Dictionary
///
/// - ``databaseDictionary``
///
/// ### Comparing Records
///
/// - ``databaseChanges(from:)``
/// - ``databaseChanges(modify:)``
/// - ``databaseEquals(_:)``
public protocol EncodableRecord {
/// Encodes the record into the provided persistence container.
///
/// In your implementation of this method, store in the `container` argument
/// all values that should be stored in database columns.
///
/// Primary key columns, if any, must be included.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableRecord {
/// var id: Int64?
/// var name: String?
///
/// func encode(to container: inout PersistenceContainer) {
/// container["id"] = id
/// container["name"] = name
/// }
/// }
/// ```
///
/// It is undefined behavior to set different values for the same column.
/// Column names are case insensitive, so defining both "name" and "NAME"
/// is considered undefined behavior.
///
/// - throws: An error is thrown if the record can't be encoded to its
/// database representation.
func encode(to container: inout PersistenceContainer) throws
// MARK: - Customizing the Format of Database Columns
/// Contextual information made available to the
/// `Encodable.encode(to:)` method.
///
/// This property is dedicated to ``EncodableRecord`` types that also
/// conform to the standard `Encodable` protocol and use the default
/// ``encode(to:)-1mrt`` implementation.
///
/// The returned dictionary is returned by `Encoder.userInfo` when the
/// record is encoded.
///
/// For example:
///
/// ```swift
/// // A key that holds a encoder's name
/// let encoderName = CodingUserInfoKey(rawValue: "encoderName")!
///
/// 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 }
/// Returns the `JSONEncoder` that encodes the value for a given column.
///
/// This method is dedicated to ``EncodableRecord`` types that also conform
/// to the standard `Encodable` protocol and use the default
/// ``encode(to:)-1mrt`` implementation.
static func databaseJSONEncoder(for column: String) -> JSONEncoder
/// The strategy for encoding `Data` columns.
///
/// This property is dedicated to ``EncodableRecord`` types that also
/// conform to the standard `Encodable` protocol and use the default
/// ``encode(to:)-1mrt`` implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableRecord, Encodable {
/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text
///
/// // Encoded as SQL text. Data must contain valid UTF8 bytes.
/// var jsonData: Data
/// }
/// ```
static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get }
/// The strategy for encoding `Date` columns.
///
/// This property is dedicated to ``EncodableRecord`` types that also
/// conform to the standard `Encodable` protocol and use the default
/// ``encode(to:)-1mrt`` implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableRecord, Encodable {
/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970
///
/// // Encoded as an epoch timestamp
/// var creationDate: Date
/// }
/// ```
static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get }
/// The strategy for encoding `UUID` columns.
///
/// This property is dedicated to ``EncodableRecord`` types that also
/// conform to the standard `Encodable` protocol and use the default
/// ``encode(to:)-1mrt`` implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableRecord, Encodable {
/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
///
/// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
/// var uuid: UUID
/// }
/// ```
static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get }
/// The strategy for converting coding keys to column names.
///
/// This property is dedicated to ``EncodableRecord`` types that also
/// conform to the standard `Encodable` protocol and use the default
/// ``encode(to:)-1mrt`` implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableProtocol, Encodable {
/// static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
///
/// // Encoded in the 'player_id' column
/// var playerID: String
/// }
/// ```
static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy { get }
}
extension EncodableRecord {
/// Contextual information made available to the
/// `Encodable.encode(to:)` method.
///
/// The default implementation returns an empty dictionary.
public static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] {
[:]
}
/// Returns the `JSONEncoder` that encodes the value for a given column.
///
/// The default implementation returns a `JSONEncoder` with the
/// following properties:
///
/// - `dataEncodingStrategy`: `.base64`
/// - `dateEncodingStrategy`: `.millisecondsSince1970`
/// - `nonConformingFloatEncodingStrategy`: `.throw`
/// - `outputFormatting`: `.sortedKeys`
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.dateEncodingStrategy = .millisecondsSince1970
encoder.nonConformingFloatEncodingStrategy = .throw
// guarantee some stability in order to ease record comparison
encoder.outputFormatting = .sortedKeys
encoder.userInfo = databaseEncodingUserInfo
return encoder
}
/// Returns the default strategy for encoding `Data` columns:
/// ``DatabaseDataEncodingStrategy/deferredToData``.
public static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy {
.deferredToData
}
/// Returns the default strategy for encoding `Date` columns:
/// ``DatabaseDateEncodingStrategy/deferredToDate``.
public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy {
.deferredToDate
}
/// Returns the default strategy for encoding `UUID` columns:
/// ``DatabaseUUIDEncodingStrategy/deferredToUUID``.
public static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy {
.deferredToUUID
}
/// Returns the default strategy for converting coding keys to column names:
/// ``DatabaseColumnEncodingStrategy/useDefaultKeys``.
public static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy {
.useDefaultKeys
}
}
extension EncodableRecord {
/// A dictionary whose keys are the columns encoded in the
/// <doc:/documentation/GRDB/EncodableRecord/encode(to:)-k9pf> method.
///
/// - throws: An error is thrown if the record can't be encoded to its
/// database representation.
public var databaseDictionary: [String: DatabaseValue] {
get throws {
try Dictionary(PersistenceContainer(self).storage).mapValues { $0?.databaseValue ?? .null }
}
}
}
extension EncodableRecord {
// MARK: - Record Comparison
/// Returns a boolean indicating whether this record and the other record
/// have the same database representation.
public func databaseEquals(_ record: Self) -> Bool {
do {
return try PersistenceContainer(self).changesIterator(from: PersistenceContainer(record)).next() == nil
} catch {
// one record can't be encoded: they can't be identical in the database
return false
}
}
/// Returns a dictionary of values changed from the other record.
///
/// The keys of the dictionary are the column names for which record do not
/// share the same value. Values are the database values from the
/// `other` record.
///
/// Note that the `other` record does not have to have the same type of the
/// receiver record. When the two records don't define the same set of
/// columns in their <doc:/documentation/GRDB/EncodableRecord/encode(to:)-k9pf>
/// method, only the columns defined by the receiver are considered.
///
/// - throws: An error is thrown if one record can't be encoded to its
/// database representation.
public func databaseChanges(from record: some EncodableRecord)
throws -> [String: DatabaseValue]
{
let changes = try PersistenceContainer(self).changesIterator(from: PersistenceContainer(record))
return Dictionary(uniqueKeysWithValues: changes)
}
/// Modifies the record according to the provided `modify` closure, and
/// returns a dictionary of changed values.
///
/// The keys of the dictionary are the changed column names. Values are
/// the database values from the initial version record.
///
/// For example:
///
/// ```swift
/// var player = Player(id: 1, score: 1000, hasAward: false)
/// let changes = try player.databaseChanges {
/// $0.score = 1000
/// $0.hasAward = true
/// }
///
/// player.hasAward // true (changed)
///
/// changes["score"] // nil (not changed)
/// changes["hasAward"] // false (old value)
/// ```
///
/// - parameter modify: A closure that modifies the record.
public mutating func databaseChanges(modify: (inout Self) throws -> Void)
throws -> [String: DatabaseValue]
{
let container = try PersistenceContainer(self)
try modify(&self)
let changes = try PersistenceContainer(self).changesIterator(from: container)
return Dictionary(uniqueKeysWithValues: changes)
}
}
// MARK: - PersistenceContainer
/// A container for database values to store in a database row.
///
/// `PersistenceContainer` is the argument of the
/// ``EncodableRecord/encode(to:)-k9pf`` method.
public struct PersistenceContainer {
// fileprivate for Row(_:PersistenceContainer)
// The ordering of the OrderedDictionary helps generating always the same
// SQL queries, and hit the statement cache.
fileprivate var storage: OrderedDictionary<String, (any DatabaseValueConvertible)?>
/// The value associated with the given column.
public subscript(_ column: String) -> (any DatabaseValueConvertible)? {
get { self[caseInsensitive: column] }
set { storage.updateValue(newValue, forKey: column) }
}
/// The value associated with the given column.
public subscript(_ column: some ColumnExpression) -> (any DatabaseValueConvertible)? {
get { self[column.name] }
set { self[column.name] = newValue }
}
init() {
storage = OrderedDictionary()
}
init(minimumCapacity: Int) {
storage = OrderedDictionary(minimumCapacity: minimumCapacity)
}
/// Convenience initializer from a record
init<Record: EncodableRecord>(_ record: Record) throws {
self.init()
try record.encode(to: &self)
}
/// Convenience initializer from a database connection and a record
@usableFromInline
init(_ db: Database, _ record: some EncodableRecord & TableRecord) throws {
let databaseTableName = type(of: record).databaseTableName
let columnCount = try db.columns(in: databaseTableName).count
self.init(minimumCapacity: columnCount) // Optimization
try record.encode(to: &self)
}
/// Columns stored in the container, ordered like values.
var columns: [String] { Array(storage.keys) }
/// Values stored in the container, ordered like columns.
var values: [(any DatabaseValueConvertible)?] { Array(storage.values) }
/// Accesses the value associated with the given column, in a
/// case-insensitive fashion.
subscript(caseInsensitive column: String) -> (any DatabaseValueConvertible)? {
get {
if let value = storage[column] {
return value
}
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value
}
return nil
}
set {
if storage[column] != nil {
storage[column] = newValue
return
}
let lowercaseColumn = column.lowercased()
for key in storage.keys where key.lowercased() == lowercaseColumn {
storage[key] = newValue
return
}
storage[column] = newValue
}
}
// Returns nil if column is not defined
func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? {
let lowercaseColumn = column.lowercased()
for (key, value) in storage where key.lowercased() == lowercaseColumn {
return value?.databaseValue ?? .null
}
return nil
}
var isEmpty: Bool { storage.isEmpty }
/// An iterator over the (column, value) pairs
func makeIterator() -> IndexingIterator<OrderedDictionary<String, (any DatabaseValueConvertible)?>> {
storage.makeIterator()
}
@usableFromInline
func changesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> {
var newValueIterator = makeIterator()
return AnyIterator {
// Loop until we find a change, or exhaust columns:
while let (column, newValue) = newValueIterator.next() {
let oldValue = container[caseInsensitive: column]
let oldDbValue = oldValue?.databaseValue ?? .null
let newDbValue = newValue?.databaseValue ?? .null
if newDbValue != oldDbValue {
return (column, oldDbValue)
}
}
return nil
}
}
}
extension Row {
convenience init<Record: EncodableRecord>(_ record: Record) throws {
try self.init(PersistenceContainer(record))
}
convenience init(_ container: PersistenceContainer) {
self.init(Dictionary(container.storage))
}
}
// MARK: - DatabaseDataEncodingStrategy
/// `DatabaseDataEncodingStrategy` specifies how `EncodableRecord` types that
/// also adopt the standard `Encodable` protocol encode their `Data` properties
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
/// implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableRecord, Encodable {
/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text
///
/// // Encoded as SQL text. Data must contain valid UTF8 bytes.
/// var jsonData: Data
/// }
/// ```
public enum DatabaseDataEncodingStrategy {
/// Encodes `Data` columns as SQL blob.
case deferredToData
/// Encodes `Data` columns as SQL text. Data must contain valid UTF8 bytes.
case text
/// Encodes `Data` column as the result of the user-provided function.
case custom((Data) -> (any DatabaseValueConvertible)?)
func encode(_ data: Data) -> DatabaseValue {
switch self {
case .deferredToData:
return data.databaseValue
case .text:
guard let string = String(data: data, encoding: .utf8) else {
fatalError("Invalid UTF8 data")
}
return string.databaseValue
case .custom(let format):
return format(data)?.databaseValue ?? .null
}
}
}
// MARK: - DatabaseDateEncodingStrategy
/// `DatabaseDateEncodingStrategy` specifies how `EncodableRecord` types that
/// also adopt the standard `Encodable` protocol encode their `Date` properties
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
/// implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableRecord, Encodable {
/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970
///
/// // Encoded as an epoch timestamp
/// var creationDate: Date
/// }
/// ```
public enum DatabaseDateEncodingStrategy {
/// The strategy that uses formatting from the Date structure.
///
/// It encodes dates using the format "YYYY-MM-DD HH:MM:SS.SSS" in the
/// UTC time zone.
case deferredToDate
/// Encodes a Double: the number of seconds between the date and
/// midnight UTC on 1 January 2001
case timeIntervalSinceReferenceDate
/// Encodes a Double: the number of seconds between the date and
/// midnight UTC on 1 January 1970
case timeIntervalSince1970
/// Encodes an Int64: the number of seconds between the date and
/// midnight UTC on 1 January 1970
case secondsSince1970
/// Encodes an Int64: the number of milliseconds between the date and
/// midnight UTC on 1 January 1970
case millisecondsSince1970
/// Encodes dates according to the ISO 8601 and RFC 3339 standards
case iso8601
/// Encodes a String, according to the provided formatter
case formatted(DateFormatter)
/// Encodes the result of the user-provided function
case custom((Date) -> (any DatabaseValueConvertible)?)
private static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}()
func encode(_ date: Date) -> DatabaseValue {
switch self {
case .deferredToDate:
return date.databaseValue
case .timeIntervalSinceReferenceDate:
return date.timeIntervalSinceReferenceDate.databaseValue
case .timeIntervalSince1970:
return date.timeIntervalSince1970.databaseValue
case .millisecondsSince1970:
return Int64(floor(1000.0 * date.timeIntervalSince1970)).databaseValue
case .secondsSince1970:
return Int64(floor(date.timeIntervalSince1970)).databaseValue
case .iso8601:
return Self.iso8601Formatter.string(from: date).databaseValue
case .formatted(let formatter):
return formatter.string(from: date).databaseValue
case .custom(let format):
return format(date)?.databaseValue ?? .null
}
}
}
// MARK: - DatabaseUUIDEncodingStrategy
/// `DatabaseUUIDEncodingStrategy` specifies how `EncodableRecord` types that
/// also adopt the standard `Encodable` protocol encode their `UUID` properties
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
/// implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableRecord, Encodable {
/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
///
/// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
/// var uuid: UUID
/// }
/// ```
public enum DatabaseUUIDEncodingStrategy: Sendable {
/// The strategy that uses formatting from the UUID type.
///
/// It encodes UUIDs as 16-bytes data blobs.
case deferredToUUID
/// Encodes UUIDs as uppercased strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
case uppercaseString
/// Encodes UUIDs as lowercased strings such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f"
case lowercaseString
func encode(_ uuid: UUID) -> DatabaseValue {
switch self {
case .deferredToUUID:
return uuid.databaseValue
case .uppercaseString:
return uuid.uuidString.databaseValue
case .lowercaseString:
return uuid.uuidString.lowercased().databaseValue
}
}
}
// MARK: - DatabaseColumnEncodingStrategy
/// `DatabaseColumnEncodingStrategy` specifies how `EncodableRecord` types that
/// also adopt the standard `Encodable` protocol encode their coding keys into
/// database columns in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
/// implementation.
///
/// For example:
///
/// ```swift
/// struct Player: EncodableProtocol, Encodable {
/// static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
///
/// // Encoded in the 'player_id' column
/// var playerID: String
/// }
/// ```
public enum DatabaseColumnEncodingStrategy {
/// A key encoding strategy that doesnt change key names during encoding.
case useDefaultKeys
/// A key encoding strategy that converts camel-case keys to snake-case keys.
case convertToSnakeCase
/// A key encoding strategy defined by the closure you supply.
case custom((any CodingKey) -> String)
func column(forKey key: some CodingKey) -> String {
switch self {
case .useDefaultKeys:
return key.stringValue
case .convertToSnakeCase:
return Self._convertToSnakeCase(key.stringValue)
case let .custom(column):
return column(key)
}
}
// Copied straight from
// https://github.com/apple/swift-corelibs-foundation/blob/8d6398d76eaf886a214e0bb2bd7549d968f7b40e/Sources/Foundation/JSONEncoder.swift#L127
static func _convertToSnakeCase(_ stringKey: String) -> String {
//===----------------------------------------------------------------------===//
//
// This function is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
guard !stringKey.isEmpty else { return stringKey }
var words: [Range<String.Index>] = []
// The general idea of this algorithm is to split words on transition
// from lower to upper case, then on transition of >1 upper case
// characters to lowercase
//
// myProperty -> my_property
// myURLProperty -> my_url_property
//
// We assume, per Swift naming conventions, that the first character of
// the key is lowercase.
var wordStart = stringKey.startIndex
var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
// Find next uppercase character
while let upperCaseRange = stringKey.rangeOfCharacter(
from: CharacterSet.uppercaseLetters,
options: [], range: searchRange)
{
let untilUpperCase = wordStart..<upperCaseRange.lowerBound
words.append(untilUpperCase)
// Find next lowercase character
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
guard let lowerCaseRange = stringKey.rangeOfCharacter(
from: CharacterSet.lowercaseLetters,
options: [],
range: searchRange)
else {
// There are no more lower case letters. Just end here.
wordStart = searchRange.lowerBound
break
}
// Is the next lowercase letter more than 1 after the uppercase? If
// so, we encountered a group of uppercase letters that we should
// treat as its own word
let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
// The next character after capital is a lower case character
// and therefore not a word boundary.
// Continue searching for the next upper case for the boundary.
wordStart = upperCaseRange.lowerBound
} else {
// There was a range of >1 capital letters. Turn those into a
// word, stopping at the capital before the lower case character.
let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
// Next word starts at the capital before the lowercase we just found
wordStart = beforeLowerIndex
}
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
}
words.append(wordStart..<searchRange.upperBound)
let result = words
.map { (range) in stringKey[range].lowercased() }
.joined(separator: "_")
return result
}
}