// MARK: - Associations to TableRecord extension TableRecord { /// Creates a ``BelongsToAssociation`` between `Self` and the /// destination `TableRecord` type. /// /// For example: /// /// ```swift /// struct Author: TableRecord { } /// struct Book: TableRecord { /// static let author = belongsTo(Author.self) /// } /// ``` /// /// The association lets you define requests that involve both the source /// and the destination type. /// /// For example, we can fetch all books with their author: /// /// ```swift /// struct BookInfo: FetchableRecord, Decodable { /// var book: Book /// var author: Author /// } /// /// try dbQueue.read { db in /// let request = Book /// .including(required: Book.author) /// .asRequest(of: BookInfo.self) /// let bookInfos = try request.fetchAll(db) /// for bookInfo in bookInfos { /// print("\(bookInfo.book.title) by \(bookInfo.author.name)") /// } /// } /// ``` /// /// The association can also help fetching associated records: /// /// ```swift /// try dbQueue.read { db in /// let book: Book = ... /// let author: Author? = book /// .request(for: Book.author) /// .fetchOne(db) /// } /// ``` /// /// For more information about this association, /// see ``BelongsToAssociation``. /// /// Methods that build requests involving associations are defined in the /// ``JoinableRequest`` protocol. /// /// - parameters: /// - destination: The record type at the other side of the association. /// - key: The association key. By default, it /// is `Destination.databaseTableName`. /// - foreignKey: An eventual foreign key. You need to provide one when /// no foreign key exists to the destination table, or several foreign /// keys exist. public static func belongsTo( _ destination: Destination.Type, key: String? = nil, using foreignKey: ForeignKey? = nil) -> BelongsToAssociation where Destination: TableRecord { BelongsToAssociation( to: Destination.relationForAll, key: key, using: foreignKey) } /// Creates a ``HasManyAssociation`` between `Self` and the /// destination `TableRecord` type. /// /// For example: /// /// ```swift /// struct Book: TableRecord { } /// struct Author: TableRecord { /// static let books = hasMany(Book.self) /// } /// ``` /// /// The association lets you define requests that involve both the source /// and the destination type. /// /// For example, we can fetch all authors with all their books: /// /// ```swift /// struct AuthorInfo: FetchableRecord, Decodable { /// var author: Author /// var books: [Book] /// } /// /// try dbQueue.read { db in /// let request = Author /// .including(all: Author.books) /// .asRequest(of: AuthorInfo.self) /// let authorInfos = try request.fetchAll(db) /// for authorInfo in authorInfos { /// print("\(authorInfo.author.name) wrote \(authorInfo.books.count) books") /// } /// } /// ``` /// /// The association can also help fetching associated records: /// /// ```swift /// try dbQueue.read { db in /// let author: Author = ... /// let books: [Book] = author /// .request(for: Author.books) /// .fetchAll(db) /// } /// ``` /// /// For more information about this association, /// see ``HasManyAssociation``. /// /// Methods that build requests involving associations are defined in the /// ``JoinableRequest`` protocol. /// /// - parameters: /// - destination: The record type at the other side of the association. /// - key: The association key. By default, it /// is `Destination.databaseTableName`. /// - foreignKey: An eventual foreign key. You need to provide one when /// no foreign key exists to the destination table, or several foreign /// keys exist. public static func hasMany( _ destination: Destination.Type, key: String? = nil, using foreignKey: ForeignKey? = nil) -> HasManyAssociation where Destination: TableRecord { HasManyAssociation( to: Destination.relationForAll, key: key, using: foreignKey) } /// Creates a ``HasOneAssociation`` between `Self` and the /// destination `TableRecord` type. /// /// For example: /// /// ```swift /// struct Demographics: TableRecord { } /// struct Country: TableRecord { /// static let demographics = hasOne(Demographics.self) /// } /// ``` /// /// The association lets you define requests that involve both the source /// and the destination type. /// /// For example, we can fetch all countries with their eventual demographics: /// /// ```swift /// struct CountryInfo: FetchableRecord, Decodable { /// var country: Country /// var demographics: Demographics? /// } /// /// try dbQueue.read { db in /// let request = Country /// .including(optional: Country.demographics) /// .asRequest(of: CountryInfo.self) /// let countryInfos = try request.fetchAll(db) /// for countryInfo in countryInfos { /// if let demographics = countryInfo.demographics { /// print(""" /// \(countryInfo.country.name) has \ /// \(demographics.population) citizens. /// """) /// } /// } /// } /// ``` /// /// The association can also help fetching associated records: /// /// ```swift /// try dbQueue.read { db in /// let country: Country = ... /// let demographics: Demographics? = country /// .request(for: Country.demographics) /// .fetchOne(db) /// } /// ``` /// /// For more information about this association, /// see ``HasOneAssociation``. /// /// Methods that build requests involving associations are defined in the /// ``JoinableRequest`` protocol. /// /// - parameters: /// - destination: The record type at the other side of the association. /// - key: The association key. By default, it /// is `Destination.databaseTableName`. /// - foreignKey: An eventual foreign key. You need to provide one when /// no foreign key exists to the destination table, or several foreign /// keys exist. public static func hasOne( _ destination: Destination.Type, key: String? = nil, using foreignKey: ForeignKey? = nil) -> HasOneAssociation where Destination: TableRecord { HasOneAssociation( to: Destination.relationForAll, key: key, using: foreignKey) } } // MARK: - Associations to Table extension TableRecord { /// Creates a ``BelongsToAssociation`` between `Self` and the /// destination `Table`. /// /// For more information, see ``TableRecord/belongsTo(_:key:using:)-13t5r``. /// /// - parameters: /// - destination: The table at the other side of the association. /// - key: The association key. By default, it /// is `destination.tableName`. /// - foreignKey: An eventual foreign key. You need to provide one when /// no foreign key exists to the destination table, or several foreign /// keys exist. public static func belongsTo( _ destination: Table, key: String? = nil, using foreignKey: ForeignKey? = nil) -> BelongsToAssociation { BelongsToAssociation( to: destination.relationForAll, key: key, using: foreignKey) } /// Creates a ``HasManyAssociation`` between `Self` and the /// destination `Table`. /// /// For more information, see ``TableRecord/hasMany(_:key:using:)-45axo``. /// /// - parameters: /// - destination: The table at the other side of the association. /// - key: The association key. By default, it /// is `destination.tableName`. /// - foreignKey: An eventual foreign key. You need to provide one when /// no foreign key exists to the destination table, or several foreign /// keys exist. public static func hasMany( _ destination: Table, key: String? = nil, using foreignKey: ForeignKey? = nil) -> HasManyAssociation { HasManyAssociation( to: destination.relationForAll, key: key, using: foreignKey) } /// Creates a ``HasOneAssociation`` between `Self` and the /// destination `Table`. /// /// For more information, see ``TableRecord/hasOne(_:key:using:)-4g9tm``. /// /// - parameters: /// - destination: The table at the other side of the association. /// - key: The association key. By default, it /// is `destination.tableName`. /// - foreignKey: An eventual foreign key. You need to provide one when /// no foreign key exists to the destination table, or several foreign /// keys exist. public static func hasOne( _ destination: Table, key: String? = nil, using foreignKey: ForeignKey? = nil) -> HasOneAssociation { HasOneAssociation( to: destination.relationForAll, key: key, using: foreignKey) } } // MARK: - Associations to CommonTableExpression extension TableRecord { /// Creates an association to a common table expression. /// /// The key of the returned association is the table name of the common /// table expression. /// /// For example, you can build a request that fetches all chats with their /// latest message: /// /// ```swift /// let latestMessageRequest = Message /// .annotated(with: max(Column("date"))) /// .group(Column("chatID")) /// /// let latestMessageCTE = CommonTableExpression( /// named: "latestMessage", /// request: latestMessageRequest) /// /// let latestMessageAssociation = Chat.association( /// to: latestMessageCTE, /// on: { chat, latestMessage in /// chat[Column("id")] == latestMessage[Column("chatID")] /// }) /// /// // WITH latestMessage AS /// // (SELECT *, MAX(date) FROM message GROUP BY chatID) /// // SELECT chat.*, latestMessage.* /// // FROM chat /// // LEFT JOIN latestMessage ON chat.id = latestMessage.chatID /// let request = Chat /// .with(latestMessageCTE) /// .including(optional: latestMessageAssociation) /// ``` /// /// - parameter cte: A common table expression. /// - parameter condition: A function that returns the joining clause. /// - parameter left: A `TableAlias` for the left table. /// - parameter right: A `TableAlias` for the right table. /// - returns: An association to the common table expression. public static func association( to cte: CommonTableExpression, on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( to: cte.relationForAll, condition: .expression { condition($0, $1).sqlExpression }) } /// Creates an association to a common table expression. /// /// The key of the returned association is the table name of the common /// table expression. /// /// - parameter cte: A common table expression. /// - returns: An association to the common table expression. public static func association( to cte: CommonTableExpression) -> JoinAssociation { JoinAssociation(to: cte.relationForAll, condition: .none) } } // MARK: - "Through" Associations extension TableRecord { /// Creates a ``HasManyThroughAssociation`` between `Self` and the /// destination `TableRecord` type. /// /// For example: /// /// ```swift /// struct Citizen: TableRecord { } /// /// struct Passport: TableRecord { /// static let citizen = belongsTo(Citizen.self) /// } /// /// struct Country: TableRecord { /// static let passports = hasMany(Passport.self) /// static let citizens = hasMany(Citizen.self, /// through: passports, /// using: Passport.citizen) /// } /// ``` /// /// The association lets you define requests that involve both the source /// and the destination type. /// /// For example, we can fetch all countries with all their citizens: /// /// ```swift /// struct CountryInfo: FetchableRecord, Decodable { /// var country: Country /// var citizens: [Citizen] /// } /// /// try dbQueue.read { db in /// let request = Country /// .including(all: Country.citizens) /// .asRequest(of: CountryInfo.self) /// let countryInfos = try request.fetchAll(db) /// for countryInfo in countryInfos { /// print("\(countryInfo.country.name) has \(countryInfo.citizens.count) citizens") /// } /// } /// ``` /// /// The association can also help fetching associated records: /// /// ```swift /// try dbQueue.read { db in /// let country: Country = ... /// let citizens: [Citizen] = country /// .request(for: Country.citizens) /// .fetchAll(db) /// } /// ``` /// /// For more information about this association, /// see ``HasManyThroughAssociation``. /// /// Methods that build requests involving associations are defined in the /// ``JoinableRequest`` protocol. /// /// - parameters: /// - destination: The record type at the other side of the association. /// - pivot: An association from `Self` to the intermediate type. /// - target: A target association from the intermediate type to the /// destination type. /// - key: The association key. By default, it is the key of the target. public static func hasMany( _ destination: Target.RowDecoder.Type, through pivot: Pivot, using target: Target, key: String? = nil) -> HasManyThroughAssociation where Pivot: Association, Target: Association, Pivot.OriginRowDecoder == Self, Pivot.RowDecoder == Target.OriginRowDecoder { let association = HasManyThroughAssociation(through: pivot, using: target) if let key { return association.forKey(key) } else { return association } } /// Creates a ``HasOneThroughAssociation`` between `Self` and the /// destination `TableRecord` type. /// /// For example: /// /// ```swift /// struct Address: TableRecord { } /// /// struct Library: TableRecord { /// static let address = hasOne(Address.self) /// } /// /// struct Book: TableRecord { /// static let library = belongsTo(Library.self) /// static let returnAddress = hasOne(Address.self, /// through: library, /// using: Library.address, /// key: "returnAddress") /// } /// ``` /// /// The association lets you define requests that involve both the source /// and the destination type. /// /// For example, we can fetch all books with their return address: /// /// ```swift /// struct BookInfo: FetchableRecord, Decodable { /// var book: Book /// var returnAddress: Address /// } /// /// try dbQueue.read { db in /// let request = Book /// .including(required: Book.returnAddress) /// .asRequest(of: BookInfo.self) /// let bookInfos = try request.fetchAll(db) /// for bookInfo in bookInfos { /// print("\(bookInfo.book.title) must return to \(bookInfo.returnAddress)") /// } /// } /// ``` /// /// The association can also help fetching associated records: /// /// ```swift /// try dbQueue.read { db in /// let book: Book = ... /// let returnAddress: Address? = book /// .request(for: Book.returnAddress) /// .fetchOne(db) /// } /// ``` /// /// For more information about this association, /// see ``HasOneThroughAssociation``. /// /// Methods that build requests involving associations are defined in the /// ``JoinableRequest`` protocol. /// /// - parameters: /// - destination: The record type at the other side of the association. /// - pivot: An association from Self to the intermediate type. /// - target: A target association from the intermediate type to the /// destination type. /// - key: The association key. By default, it is the key of the target. public static func hasOne( _ destination: Target.RowDecoder.Type, through pivot: Pivot, using target: Target, key: String? = nil) -> HasOneThroughAssociation where Pivot: AssociationToOne, Target: AssociationToOne, Pivot.OriginRowDecoder == Self, Pivot.RowDecoder == Target.OriginRowDecoder { let association = HasOneThroughAssociation(through: pivot, using: target) if let key { return association.forKey(key) } else { return association } } } // MARK: - Request for associated records extension TableRecord where Self: EncodableRecord { /// Returns a request for the associated record(s). /// /// For example: /// /// ```swift /// struct Player: TableRecord, FetchableRecord { } /// struct Team: TableRecord, EncodableRecord { /// static let players = hasMany(Player.self) /// } /// /// try dbQueue.read { db in /// let team: Team = ... /// let players: [Player] = try team /// .request(for: Team.players) /// .fetchAll(db) /// } /// ``` public func request(for association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { switch association._sqlAssociation.pivot.condition { case .expression: // TODO: find a use case? fatalError("Not implemented: request association without any foreign key") case let .foreignKey(foreignKey): let destinationRelation = association ._sqlAssociation .with { $0.pivot.relation = $0.pivot.relation.filterWhenConnected { db in // Filter the pivot on self try foreignKey .joinMapping(db, from: Self.databaseTableName) .joinExpression(leftRows: [PersistenceContainer(db, self)]) } } .destinationRelation() return QueryInterfaceRequest(relation: destinationRelation) } } } // MARK: - Joining Methods extension TableRecord { /// Returns a request that fetches all records associated with each record /// in this request. /// /// For example, we can fetch authors along with their books: /// /// ```swift /// struct Author: TableRecord, FetchableRecord, Decodable { /// static let books = hasMany(Book.self) /// } /// struct Book: TableRecord, FetchableRecord, Decodable { } /// /// struct AuthorInfo: FetchableRecord, Decodable { /// var author: Author /// var books: [Book] /// } /// /// let authorInfos = try Author /// .including(all: Author.books) /// .asRequest(of: AuthorInfo.self) /// .fetchAll(db) /// ``` public static func including(all association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { all().including(all: association) } /// Returns a request that fetches the eventual record associated with each /// record of this request. /// /// For example, we can fetch books along with their eventual author: /// /// ```swift /// struct Author: TableRecord, FetchableRecord, Decodable { } /// struct Book: TableRecord, FetchableRecord, Decodable { /// static let author = belongsTo(Author.self) /// } /// /// struct BookInfo: FetchableRecord, Decodable { /// var book: Book /// var author: Author? /// } /// /// let bookInfos = try Book /// .including(optional: Book.author) /// .asRequest(of: BookInfo.self) /// .fetchAll(db) /// ``` public static func including(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { all().including(optional: association) } /// Returns a request that fetches the record associated with each record in /// this request. Records that do not have an associated record /// are discarded. /// /// For example, we can fetch books along with their eventual author: /// /// ```swift /// struct Author: TableRecord, FetchableRecord, Decodable { } /// struct Book: TableRecord, FetchableRecord, Decodable { /// static let author = belongsTo(Author.self) /// } /// /// struct BookInfo: FetchableRecord, Decodable { /// var book: Book /// var author: Author /// } /// /// let bookInfos = try Book /// .including(required: Book.author) /// .asRequest(of: BookInfo.self) /// .fetchAll(db) /// ``` public static func including(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { all().including(required: association) } /// Returns a request that joins each record of this request to its /// eventual associated record. public static func joining(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { all().joining(optional: association) } /// Returns a request that joins each record of this request to its /// associated record. Records that do not have an associated record /// are discarded. /// /// For example, we can fetch only books whose author is French: /// /// ```swift /// struct Author: TableRecord, FetchableRecord, Decodable { } /// struct Book: TableRecord, FetchableRecord, Decodable { /// static let author = belongsTo(Author.self) /// } /// /// let frenchAuthors = Book.author.filter(Column("countryCode") == "FR") /// let bookInfos = try Book /// .joining(required: frenchAuthors) /// .fetchAll(db) /// ``` public static func joining(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { all().joining(required: association) } /// Returns a request with the columns of the eventual associated record /// appended to the record selection. /// /// The record selection is determined by /// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns. /// /// For example: /// /// ```swift /// // SELECT player.*, team.color /// // FROM player LEFT JOIN team ... /// let teamColor = Player.team.select(Column("color")) /// let request = Player.annotated(withOptional: teamColor) /// ``` /// /// See ``JoinableRequest/annotated(withOptional:)`` for more information. public static func annotated(withOptional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { all().annotated(withOptional: association) } /// Returns a request with the columns of the associated record appended to /// the record selection. Records that do not have an associated record /// are discarded. /// /// The record selection is determined by /// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns. /// /// For example: /// /// ```swift /// // SELECT player.*, team.color /// // FROM player JOIN team ... /// let teamColor = Player.team.select(Column("color")) /// let request = Player.annotated(withRequired: teamColor) /// ``` /// /// See ``JoinableRequest/annotated(withRequired:)`` for more information. public static func annotated(withRequired association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == Self { all().annotated(withRequired: association) } } // MARK: - Aggregates extension TableRecord { /// Returns a request with the given association aggregates appended to /// the record selection. /// /// The record selection is determined by /// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns. /// /// For example: /// /// ```swift /// struct Author: TableRecord, FetchableRecord, Decodable { /// static let books = hasMany(Book.self) /// } /// struct Book: TableRecord, FetchableRecord, Decodable { } /// /// struct AuthorInfo: FetchableRecord, Decodable { /// var author: Author /// var bookCount: Int /// } /// /// // SELECT author.*, COUNT(DISTINCT book.id) AS bookCount /// // FROM author /// // LEFT JOIN book ON book.authorId = author.id /// // GROUP BY author.id /// let authorInfos = try Author /// .annotated(with: Author.books.count) /// .asRequest(of: AuthorInfo.self) /// .fetchAll(db) /// ``` public static func annotated(with aggregates: AssociationAggregate...) -> QueryInterfaceRequest { all().annotated(with: aggregates) } /// Returns a request with the given association aggregates appended to /// the record selection. /// /// The record selection is determined by /// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns. /// /// For example: /// /// ```swift /// struct Author: TableRecord, FetchableRecord, Decodable { /// static let books = hasMany(Book.self) /// } /// struct Book: TableRecord, FetchableRecord, Decodable { } /// /// struct AuthorInfo: FetchableRecord, Decodable { /// var author: Author /// var bookCount: Int /// } /// /// // SELECT author.*, COUNT(DISTINCT book.id) AS bookCount /// // FROM author /// // LEFT JOIN book ON book.authorId = author.id /// // GROUP BY author.id /// let authorInfos = try Author /// .annotated(with: [Author.books.count]) /// .asRequest(of: AuthorInfo.self) /// .fetchAll(db) /// ``` public static func annotated(with aggregates: [AssociationAggregate]) -> QueryInterfaceRequest { all().annotated(with: aggregates) } /// Returns a request filtered according to the provided /// association aggregate. /// /// For example: /// /// ```swift /// struct Author: TableRecord, FetchableRecord { /// static let books = hasMany(Book.self) /// } /// struct Book: TableRecord, FetchableRecord { } /// /// // SELECT author.* /// // FROM author /// // LEFT JOIN book ON book.authorId = author.id /// // GROUP BY author.id /// // HAVING COUNT(DISTINCT book.id) > 5 /// let authors = try Author /// .having(Author.books.count > 5) /// .fetchAll(db) /// ``` public static func having(_ predicate: AssociationAggregate) -> QueryInterfaceRequest { all().having(predicate) } }