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 /// 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 /// 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 /// 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: 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> { 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: 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 /// 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 /// 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 /// 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 /// 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 doesn’t 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] = [] // 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)..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..