add swiftUI code

This commit is contained in:
zeus
2025-01-22 14:09:10 +08:00
parent 68e7b7347c
commit 8a99853829
2531 changed files with 486215 additions and 0 deletions
@@ -0,0 +1,725 @@
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
}
}