Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/QueryInterface/Request/Association/Association.swift
T

315 lines
9.4 KiB
Swift

import Foundation
/// A type that defines a connection between two tables.
///
/// ``Association`` feeds methods of the ``JoinableRequest`` protocol. They are
/// built from a ``TableRecord`` type, or a ``Table`` instance.
///
/// ## Topics
///
/// ### Instance Methods
///
/// - ``forKey(_:)-247af``
/// - ``forKey(_:)-54yh6``
///
/// ### Associations To One
///
/// - ``BelongsToAssociation``
/// - ``HasOneAssociation``
/// - ``HasOneThroughAssociation``
/// - ``AssociationToOne``
///
/// ### Associations To Many
///
/// - ``HasManyAssociation``
/// - ``HasManyThroughAssociation``
/// - ``AssociationToMany``
///
/// ### Associations to Common Table Expressions
///
/// - ``JoinAssociation``
///
/// ### Supporting Types
///
/// - ``ForeignKey``
/// - ``Inflections``
public protocol Association: DerivableRequest {
// OriginRowDecoder and RowDecoder inherited from DerivableRequest provide
// type safety:
//
// Book.including(required: Book.author) // compiles
// Fruit.including(required: Book.author) // does not compile
/// The record type at the origin of the association.
///
/// In the ``BelongsToAssociation`` association below, it is `Book`:
///
/// ```swift
/// struct Book: TableRecord {
/// // BelongsToAssociation<Book, Author>
/// static let author = belongsTo(Author.self)
/// }
/// ```
associatedtype OriginRowDecoder
var _sqlAssociation: _SQLAssociation { get set }
/// Returns an association with the given key.
///
/// For example:
///
/// ```swift
/// struct Employee: FetchableRecord, TableRecord {
/// static let manager = belongsTo(Employee.self).forKey("manager")
/// static let subordinates = hasMany(Employee.self).forKey("subordinates")
/// }
///
/// struct EmployeeInfo: FetchableRecord, Decodable {
/// var employee: Employee
/// var manager: Employee? // property name matches the association key
/// var subordinates: [Employee] // property name matches the association key
/// }
///
/// try dbQueue.read { db in
/// let employeeInfos: [EmployeeInfo] = try Employee
/// .including(optional: Employee.manager)
/// .including(all: Employee.subordinates)
/// .asRequest(of: EmployeeInfo.self)
/// .fetchAll(db)
/// }
/// ```
func forKey(_ key: String) -> Self
}
extension Association {
/// Returns self modified with the *update* function.
func with(_ update: (inout Self) throws -> Void) rethrows -> Self {
var result = self
try update(&result)
return result
}
/// Returns self with destination relation modified with the *update* function.
fileprivate func withDestinationRelation(_ update: (inout SQLRelation) throws -> Void) rethrows -> Self {
var result = self
try update(&result._sqlAssociation.destination.relation)
return result
}
}
extension Association {
public func _including(all association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._including(all: association)
}
}
public func _including(optional association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._including(optional: association)
}
}
public func _including(required association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._including(required: association)
}
}
public func _joining(optional association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._joining(optional: association)
}
}
public func _joining(required association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._joining(required: association)
}
}
}
extension Association {
/// The association key defines how rows fetched from this association
/// should be consumed.
///
/// For example:
///
/// struct Player: TableRecord {
/// // The default key of this association is the name of the
/// // database table for teams, let's say "team":
/// static let team = belongsTo(Team.self)
/// }
/// print(Player.team.key) // Prints "team"
///
/// // Consume rows:
/// let request = Player.including(required: Player.team)
/// for row in Row.fetchAll(db, request) {
/// let team: Team = row["team"] // the association key
/// }
///
/// The key can be redefined with the `forKey` method:
///
/// let request = Player.including(required: Player.team.forKey("custom"))
/// for row in Row.fetchAll(db, request) {
/// let team: Team = row["custom"]
/// }
var key: SQLAssociationKey { _sqlAssociation.destination.key }
/// Returns an association with the given key.
public func forKey(_ codingKey: some CodingKey) -> Self {
forKey(codingKey.stringValue)
}
}
// TableRequest conformance
extension Association {
public func aliased(_ alias: TableAlias) -> Self {
withDestinationRelation { relation in
relation = relation.aliased(alias)
}
}
}
// SelectionRequest conformance
extension Association {
public func selectWhenConnected(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self {
withDestinationRelation { relation in
relation = relation.selectWhenConnected { db in
try selection(db).map(\.sqlSelection)
}
}
}
public func annotatedWhenConnected(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self {
withDestinationRelation { relation in
relation = relation.annotatedWhenConnected { db in
try selection(db).map(\.sqlSelection)
}
}
}
}
// FilteredRequest conformance
extension Association {
public func filterWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self {
withDestinationRelation { relation in
relation = relation.filterWhenConnected { db in
try predicate(db).sqlExpression
}
}
}
}
// OrderedRequest conformance
extension Association {
public func orderWhenConnected(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self {
withDestinationRelation { relation in
relation = relation.orderWhenConnected { db in
try orderings(db).map(\.sqlOrdering)
}
}
}
public func reversed() -> Self {
withDestinationRelation { relation in
relation = relation.reversed()
}
}
public func unordered() -> Self {
withDestinationRelation { relation in
relation = relation.unordered()
}
}
public func withStableOrder() -> Self {
withDestinationRelation { relation in
relation = relation.withStableOrder()
}
}
}
// TableRequest conformance
extension Association {
public var databaseTableName: String {
_sqlAssociation.destination.relation.source.tableName
}
}
// AggregatingRequest conformance
extension Association {
public func groupWhenConnected(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self {
withDestinationRelation { relation in
relation = relation.groupWhenConnected { db in
try expressions(db).map(\.sqlExpression)
}
}
}
public func havingWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self {
withDestinationRelation { relation in
relation = relation.havingWhenConnected { db in
try predicate(db).sqlExpression
}
}
}
}
// DerivableRequest conformance
extension Association {
public func distinct() -> Self {
withDestinationRelation { relation in
relation.isDistinct = true
}
}
public func with<RowDecoder>(_ cte: CommonTableExpression<RowDecoder>) -> Self {
withDestinationRelation { relation in
relation.ctes[cte.tableName] = cte.cte
}
}
}
// MARK: - AssociationToOne
/// An association that defines a to-one connection.
public protocol AssociationToOne: Association { }
extension AssociationToOne {
public func forKey(_ key: String) -> Self {
let associationKey = SQLAssociationKey.fixedSingular(key)
return with {
$0._sqlAssociation = $0._sqlAssociation.forDestinationKey(associationKey)
}
}
}
// MARK: - AssociationToMany
/// An association that defines a to-many connection.
///
/// ## Topics
///
/// ### Building Association Aggregates
///
/// - ``average(_:)``
/// - ``count``
/// - ``isEmpty``
/// - ``max(_:)``
/// - ``min(_:)``
/// - ``sum(_:)``
/// - ``total(_:)``
///
/// - ``AssociationAggregate``
public protocol AssociationToMany: Association { }
extension AssociationToMany {
public func forKey(_ key: String) -> Self {
let associationKey = SQLAssociationKey.fixedPlural(key)
return with {
$0._sqlAssociation = $0._sqlAssociation.forDestinationKey(associationKey)
}
}
}