add iOS
This commit is contained in:
+314
@@ -0,0 +1,314 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+883
@@ -0,0 +1,883 @@
|
||||
import Foundation
|
||||
|
||||
extension AssociationToMany {
|
||||
private func makeAggregate(_ expression: SQLExpression) -> AssociationAggregate<OriginRowDecoder> {
|
||||
AssociationAggregate(preparation: BasePreparation(association: self, expression: expression))
|
||||
}
|
||||
|
||||
/// The number of associated records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // Fetch all teams with at least ten players:
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(Team.players.count >= 10)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The returned association aggregate is named `"[key]Count"`, where `key`
|
||||
/// is the association key. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var playerCount: Int
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: Team.players.count)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public var count: AssociationAggregate<OriginRowDecoder> {
|
||||
makeAggregate(.countDistinct(.fastPrimaryKey))
|
||||
.forKey("\(key.singularizedName)Count")
|
||||
}
|
||||
|
||||
/// Returns a boolean aggregate that is true if no associated
|
||||
/// records exist.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // Fetch all teams without any player
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(Team.players.isEmpty)
|
||||
/// .fetchAll(db)
|
||||
///
|
||||
/// // Fetch all teams without some player
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(Team.players.isEmpty == false)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The returned association aggregate is named `"hasNo[key]"`, where `key`
|
||||
/// is the association key. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var hasNoPlayer: Int
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: Team.players.isEmpty)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public var isEmpty: AssociationAggregate<OriginRowDecoder> {
|
||||
makeAggregate(.isEmpty(.countDistinct(.fastPrimaryKey)))
|
||||
.forKey("hasNo\(key.singularizedName.uppercasingFirstCharacter)")
|
||||
}
|
||||
|
||||
/// Returns the average of the given expression in associated records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // Fetch all teams whose average player score is greater than 1000
|
||||
/// let averageScore = Team.players.average(Column("score"))
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(averageScore >= 1000)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When the input expression is a ``ColumnExpression``, the returned
|
||||
/// association aggregate is named `"average[Key][Column]"`, where `key` is
|
||||
/// the association key. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var averagePlayerScore: Double
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let averageScore = Team.players.average(Column("score"))
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: averageScore)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public func average(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
|
||||
let aggregate = makeAggregate(.function("AVG", [expression.sqlExpression]))
|
||||
if let column = expression as? any ColumnExpression {
|
||||
let name = key.singularizedName
|
||||
return aggregate.forKey("average\(name.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
|
||||
} else {
|
||||
return aggregate
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the maximum value of the given expression in associated records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // Fetch all teams whose maximum player score is greater than 1000
|
||||
/// let maxScore = Team.players.max(Column("score"))
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(maxScore >= 1000)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When the input expression is a ``ColumnExpression``, the returned
|
||||
/// association aggregate is named `"maximum[Key][Column]"`, where `key` is
|
||||
/// the association key. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var maximumPlayerScore: Double
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let maxScore = Team.players.max(Column("score"))
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: maxScore)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public func max(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
|
||||
let aggregate = makeAggregate(.function("MAX", [expression.sqlExpression]))
|
||||
if let column = expression as? any ColumnExpression {
|
||||
let name = key.singularizedName
|
||||
return aggregate.forKey("max\(name.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
|
||||
} else {
|
||||
return aggregate
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the minimum value of the given expression in associated records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // Fetch all teams whose minimum player score is less than 1000
|
||||
/// let minScore = Team.players.min(Column("score"))
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(minScore < 1000)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When the input expression is a ``ColumnExpression``, the returned
|
||||
/// association aggregate is named `"minimum[Key][Column]"`, where `key` is
|
||||
/// the association key. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var minimumPlayerScore: Double
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let minScore = Team.players.min(Column("score"))
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: minScore)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public func min(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
|
||||
let aggregate = makeAggregate(.function("MIN", [expression.sqlExpression]))
|
||||
if let column = expression as? any ColumnExpression {
|
||||
let name = key.singularizedName
|
||||
return aggregate.forKey("min\(name.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
|
||||
} else {
|
||||
return aggregate
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sum of the given expression in associated records.
|
||||
///
|
||||
/// This aggregate invokes the `SUM` SQL function. See also ``total(_:)``
|
||||
/// and <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // Fetch all teams whose sum of player scores is greater than 1000
|
||||
/// let scoreSum = Team.players.sum(Column("score"))
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(scoreSum >= 1000)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When the input expression is a ``ColumnExpression``, the returned
|
||||
/// association aggregate is named `"[key][Column]Sum"`, where `key` is the
|
||||
/// association key. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var playerScoreSum: Double
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let scoreSum = Team.players.sum(Column("score"))
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: scoreSum)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public func sum(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
|
||||
let aggregate = makeAggregate(.function("SUM", [expression.sqlExpression]))
|
||||
if let column = expression as? any ColumnExpression {
|
||||
let name = key.singularizedName
|
||||
return aggregate.forKey("\(name)\(column.name.uppercasingFirstCharacter)Sum")
|
||||
} else {
|
||||
return aggregate
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sum of the given expression in associated records.
|
||||
///
|
||||
/// This aggregate invokes the `TOTAL` SQL function. See also ``sum(_:)``
|
||||
/// and <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // Fetch all teams whose sum of player scores is greater than 1000
|
||||
/// let totalScore = Team.players.total(Column("score"))
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(totalScore >= 1000)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When the input expression is a ``ColumnExpression``, the returned
|
||||
/// association aggregate is named `"[key][Column]Sum"`, where `key` is the
|
||||
/// association key. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var playerScoreSum: Double
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let totalScore = Team.players.total(Column("score"))
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: totalScore)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public func total(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
|
||||
let aggregate = makeAggregate(.function("TOTAL", [expression.sqlExpression]))
|
||||
if let column = expression as? any ColumnExpression {
|
||||
let name = key.singularizedName
|
||||
// Yes we use the `Sum` suffix instead of `Total`. Both `total(_:)`
|
||||
// and `sum(_:)` compute sums.
|
||||
return aggregate.forKey("\(name)\(column.name.uppercasingFirstCharacter)Sum")
|
||||
} else {
|
||||
return aggregate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A value aggregated from a population of associated records.
|
||||
///
|
||||
/// You build an `AssociationAggregate` from an ``AssociationToMany``.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // An association aggregate
|
||||
/// let playerCount = Team.players.count
|
||||
///
|
||||
/// // Fetch all teams with at least ten players:
|
||||
/// let teams: [Team] = try Team
|
||||
/// .having(playerCount >= 10)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Instance Methods
|
||||
///
|
||||
/// - ``forKey(_:)-1rvux``
|
||||
/// - ``forKey(_:)-1ua4j``
|
||||
///
|
||||
/// ### Top-Level Functions
|
||||
///
|
||||
/// - ``abs(_:)-43n8v``
|
||||
/// - ``cast(_:as:)-63ttx``
|
||||
/// - ``length(_:)-9dr2v``
|
||||
public struct AssociationAggregate<RowDecoder> {
|
||||
fileprivate let preparation: AssociationAggregatePreparation<RowDecoder>
|
||||
|
||||
/// The SQL name for the value of this aggregate. See forKey(_:).
|
||||
var key: String? = nil
|
||||
|
||||
/// Extends the request with the associated records used to compute the
|
||||
/// aggregate, and returns the aggregated expression.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// struct Author: TableRecord {
|
||||
/// static let books = hasMany(Book.self)
|
||||
/// }
|
||||
///
|
||||
/// // SELECT * FROM author
|
||||
/// var request = Author.all()
|
||||
///
|
||||
/// let aggregate = Author.books.count
|
||||
/// let expression = aggregate.prepare(&request)
|
||||
///
|
||||
/// // The request has been extended with associated records:
|
||||
/// //
|
||||
/// // SELECT author.* FROM author
|
||||
/// // LEFT JOIN book ON book.authorId = author.id
|
||||
/// // GROUP BY author.id
|
||||
/// request
|
||||
///
|
||||
/// // The aggregated value:
|
||||
/// //
|
||||
/// // COUNT(DISTINCT book.id)
|
||||
/// expression
|
||||
///
|
||||
/// The aggregated expression is not embedded in the extended request:
|
||||
///
|
||||
/// - We don't know yet if the aggregated expression will be used in the
|
||||
/// SQL selection, or in the HAVING clause.
|
||||
/// - It helps implementing aggregate operators such as `&&`, `+`, etc.
|
||||
func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
|
||||
preparation.prepare(&request)
|
||||
}
|
||||
}
|
||||
|
||||
extension AssociationAggregate: Refinable {
|
||||
/// Returns an aggregate that is selected in a column with the given name.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Team: FetchableRecord, TableRecord {
|
||||
/// static let players = Team.hasMany(Player.self)
|
||||
/// }
|
||||
///
|
||||
/// struct TeamInfo: FetchableRecord, Decodable {
|
||||
/// var team: Team
|
||||
/// var numberOfBooks: Int
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let playerCount = Team.players.count.forKey("numberOfBooks")
|
||||
///
|
||||
/// let infos: [TeamInfo] = try Team
|
||||
/// .annotated(with: playerCount)
|
||||
/// .asRequest(of: TeamInfo.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public func forKey(_ key: String) -> Self {
|
||||
with {
|
||||
$0.key = key
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an aggregate that is selected in a column named like the given
|
||||
/// coding key.
|
||||
///
|
||||
/// See ``forKey(_:)-1rvux``.
|
||||
public func forKey(_ key: some CodingKey) -> Self {
|
||||
forKey(key.stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AssociationAggregatePreparation
|
||||
|
||||
/// An abstract class that only exists as support for
|
||||
/// `AssociationAggregate.prepare(_:)`, which needs to prepare both query
|
||||
/// interface requests and associations through their conformance
|
||||
/// to `DerivableRequest`:
|
||||
///
|
||||
/// aggregate.prepare(&request)
|
||||
/// aggregate.prepare(&association)
|
||||
///
|
||||
/// We could have used a generic closure instead of this class... if only Swift
|
||||
/// would support generic closures.
|
||||
private class AssociationAggregatePreparation<RowDecoder> {
|
||||
func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
|
||||
fatalError("subclass must override")
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepares a request so that it can use association aggregates.
|
||||
private class BasePreparation<Association: AssociationToMany>:
|
||||
AssociationAggregatePreparation<Association.OriginRowDecoder>
|
||||
{
|
||||
private let association: Association
|
||||
private let expression: SQLExpression
|
||||
|
||||
init(association: Association, expression: SQLExpression) {
|
||||
self.association = association
|
||||
self.expression = expression
|
||||
}
|
||||
|
||||
override func prepare(_ request: inout some DerivableRequest<Association.OriginRowDecoder>) -> SQLExpression {
|
||||
// The fundamental request that supports association aggregate:
|
||||
//
|
||||
// SELECT parent.*
|
||||
// LEFT JOIN child ON child.parentID = parent.id
|
||||
// GROUP BY parent.id
|
||||
let tableAlias = TableAlias()
|
||||
request = request
|
||||
.joining(optional: association.aliased(tableAlias))
|
||||
.groupByPrimaryKey()
|
||||
|
||||
// The fundamental request can now be annotated, or filtered in the
|
||||
// having clause, with the association aggregate expression:
|
||||
// MIN(child.score), COUNT(DISTINCT child.id), etc.
|
||||
return expression.qualified(with: tableAlias)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms the expression of an aggregate.
|
||||
private class MapPreparation<RowDecoder>: AssociationAggregatePreparation<RowDecoder> {
|
||||
private let base: AssociationAggregatePreparation<RowDecoder>
|
||||
private let transform: (SQLExpression) -> SQLExpression
|
||||
|
||||
init(
|
||||
base: AssociationAggregatePreparation<RowDecoder>,
|
||||
transform: @escaping (SQLExpression) -> SQLExpression)
|
||||
{
|
||||
self.base = base
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
override func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
|
||||
transform(base.prepare(&request))
|
||||
}
|
||||
}
|
||||
|
||||
extension AssociationAggregate {
|
||||
/// Transforms the expression, and does not preserve key.
|
||||
fileprivate func map(_ transform: @escaping (SQLExpression) -> SQLExpression) -> Self {
|
||||
AssociationAggregate(preparation: MapPreparation(base: preparation, transform: transform))
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines the expressions of two aggregates.
|
||||
private class CombinePreparation<RowDecoder>: AssociationAggregatePreparation<RowDecoder> {
|
||||
private let lhs: AssociationAggregatePreparation<RowDecoder>
|
||||
private let rhs: AssociationAggregatePreparation<RowDecoder>
|
||||
private let combine: (_ lhs: SQLExpression, _ rhs: SQLExpression) -> SQLExpression
|
||||
|
||||
init(
|
||||
_ lhs: AssociationAggregatePreparation<RowDecoder>,
|
||||
_ rhs: AssociationAggregatePreparation<RowDecoder>,
|
||||
combine: @escaping (_ lhs: SQLExpression, _ rhs: SQLExpression) -> SQLExpression)
|
||||
{
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
self.combine = combine
|
||||
}
|
||||
|
||||
override func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
|
||||
let lhsExpression = lhs.prepare(&request)
|
||||
let rhsExpression = rhs.prepare(&request)
|
||||
return combine(lhsExpression, rhsExpression)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines the expression of two aggregates.
|
||||
private func combine<RowDecoder>(
|
||||
_ lhs: AssociationAggregate<RowDecoder>,
|
||||
_ rhs: AssociationAggregate<RowDecoder>,
|
||||
with combine: @escaping (_ lhs: SQLExpression, _ rhs: SQLExpression) -> SQLExpression)
|
||||
-> AssociationAggregate<RowDecoder>
|
||||
{
|
||||
AssociationAggregate(preparation: CombinePreparation(lhs.preparation, rhs.preparation, combine: combine))
|
||||
}
|
||||
|
||||
// MARK: - Logical Operators (AND, OR, NOT)
|
||||
|
||||
extension AssociationAggregate {
|
||||
/// A negated logical aggregate.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// Author.having(!Author.books.isEmpty)
|
||||
/// ```
|
||||
public static prefix func ! (aggregate: Self) -> Self {
|
||||
aggregate.map { !$0 }
|
||||
}
|
||||
|
||||
/// The `AND` SQL operator.
|
||||
public static func && (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: &&)
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
/// The `AND` SQL operator.
|
||||
public static func && (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 && rhs }
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
/// The `AND` SQL operator.
|
||||
public static func && (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs && $0 }
|
||||
}
|
||||
|
||||
|
||||
/// The `OR` SQL operator.
|
||||
public static func || (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: ||)
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
/// The `OR` SQL operator.
|
||||
public static func || (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 || rhs }
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
/// The `OR` SQL operator.
|
||||
public static func || (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs || $0 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Egality and Identity Operators (=, <>, IS, IS NOT)
|
||||
|
||||
extension AssociationAggregate {
|
||||
/// The `=` SQL operator.
|
||||
public static func == (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: ==)
|
||||
}
|
||||
|
||||
/// The `=` SQL operator.
|
||||
///
|
||||
/// When the right operand is nil, `IS NULL` is used instead of the
|
||||
/// `=` operator.
|
||||
public static func == (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
|
||||
lhs.map { $0 == rhs }
|
||||
}
|
||||
|
||||
/// The `=` SQL operator.
|
||||
///
|
||||
/// When the left operand is nil, `IS NULL` is used instead of the
|
||||
/// `=` operator.
|
||||
public static func == (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
|
||||
rhs.map { lhs == $0 }
|
||||
}
|
||||
|
||||
/// The `=` SQL operator.
|
||||
public static func == (lhs: Self, rhs: Bool) -> Self {
|
||||
lhs.map { $0 == rhs }
|
||||
}
|
||||
|
||||
/// The `=` SQL operator.
|
||||
public static func == (lhs: Bool, rhs: Self) -> Self {
|
||||
rhs.map { lhs == $0 }
|
||||
}
|
||||
|
||||
/// The `<>` SQL operator.
|
||||
public static func != (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: !=)
|
||||
}
|
||||
|
||||
/// The `<>` SQL operator.
|
||||
///
|
||||
/// When the right operand is nil, `IS NOT NULL` is used instead of the
|
||||
/// `<>` operator.
|
||||
public static func != (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
|
||||
lhs.map { $0 != rhs }
|
||||
}
|
||||
|
||||
/// The `<>` SQL operator.
|
||||
///
|
||||
/// When the left operand is nil, `IS NOT NULL` is used instead of the
|
||||
/// `<>` operator.
|
||||
public static func != (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
|
||||
rhs.map { lhs != $0 }
|
||||
}
|
||||
|
||||
/// The `<>` SQL operator.
|
||||
public static func != (lhs: Self, rhs: Bool) -> Self {
|
||||
lhs.map { $0 != rhs }
|
||||
}
|
||||
|
||||
/// The `<>` SQL operator.
|
||||
public static func != (lhs: Bool, rhs: Self) -> Self {
|
||||
rhs.map { lhs != $0 }
|
||||
}
|
||||
|
||||
/// The `IS` SQL operator.
|
||||
public static func === (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: ===)
|
||||
}
|
||||
|
||||
/// The `IS` SQL operator.
|
||||
public static func === (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
|
||||
lhs.map { $0 === rhs }
|
||||
}
|
||||
|
||||
/// The `IS` SQL operator.
|
||||
public static func === (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
|
||||
rhs.map { lhs === $0 }
|
||||
}
|
||||
|
||||
/// The `IS NOT` SQL operator.
|
||||
public static func !== (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: !==)
|
||||
}
|
||||
|
||||
/// The `IS NOT` SQL operator.
|
||||
public static func !== (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
|
||||
lhs.map { $0 !== rhs }
|
||||
}
|
||||
|
||||
/// The `IS NOT` SQL operator.
|
||||
public static func !== (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
|
||||
rhs.map { lhs !== $0 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Comparison Operators (<, >, <=, >=)
|
||||
|
||||
extension AssociationAggregate {
|
||||
/// The `<=` SQL operator.
|
||||
public static func <= (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: <=)
|
||||
}
|
||||
|
||||
/// The `<=` SQL operator.
|
||||
public static func <= (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 <= rhs }
|
||||
}
|
||||
|
||||
/// The `<=` SQL operator.
|
||||
public static func <= (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs <= $0 }
|
||||
}
|
||||
|
||||
/// The `<` SQL operator.
|
||||
public static func < (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: <)
|
||||
}
|
||||
|
||||
/// The `<` SQL operator.
|
||||
public static func < (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 < rhs }
|
||||
}
|
||||
|
||||
/// The `<` SQL operator.
|
||||
public static func < (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs < $0 }
|
||||
}
|
||||
|
||||
/// The `>` SQL operator.
|
||||
public static func > (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: >)
|
||||
}
|
||||
|
||||
/// The `>` SQL operator.
|
||||
public static func > (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 > rhs }
|
||||
}
|
||||
|
||||
/// The `>` SQL operator.
|
||||
public static func > (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs > $0 }
|
||||
}
|
||||
|
||||
/// The `>=` SQL operator.
|
||||
public static func >= (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: >=)
|
||||
}
|
||||
|
||||
/// The `>=` SQL operator.
|
||||
public static func >= (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 >= rhs }
|
||||
}
|
||||
|
||||
/// The `>=` SQL operator.
|
||||
public static func >= (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs >= $0 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arithmetic Operators (+, -, *, /)
|
||||
|
||||
extension AssociationAggregate {
|
||||
/// The `-` SQL operator.
|
||||
public static prefix func - (aggregate: Self) -> Self {
|
||||
aggregate.map { -$0 }
|
||||
}
|
||||
|
||||
/// The `+` SQL operator.
|
||||
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: +)
|
||||
}
|
||||
|
||||
/// The `+` SQL operator.
|
||||
public static func + (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 + rhs }
|
||||
}
|
||||
|
||||
/// The `+` SQL operator.
|
||||
public static func + (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs + $0 }
|
||||
}
|
||||
|
||||
/// The `-` SQL operator.
|
||||
public static func - (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: -)
|
||||
}
|
||||
|
||||
/// The `-` SQL operator.
|
||||
public static func - (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 - rhs }
|
||||
}
|
||||
|
||||
/// The `-` SQL operator.
|
||||
public static func - (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs - $0 }
|
||||
}
|
||||
|
||||
/// The `*` SQL operator.
|
||||
public static func * (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: *)
|
||||
}
|
||||
|
||||
/// The `*` SQL operator.
|
||||
public static func * (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 * rhs }
|
||||
}
|
||||
|
||||
/// The `*` SQL operator.
|
||||
public static func * (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs * $0 }
|
||||
}
|
||||
|
||||
/// The `/` SQL operator.
|
||||
public static func / (lhs: Self, rhs: Self) -> Self {
|
||||
combine(lhs, rhs, with: /)
|
||||
}
|
||||
|
||||
/// The `/` SQL operator.
|
||||
public static func / (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs.map { $0 / rhs }
|
||||
}
|
||||
|
||||
/// The `/` SQL operator.
|
||||
public static func / (lhs: some SQLExpressible, rhs: Self) -> Self {
|
||||
rhs.map { lhs / $0 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
extension AssociationAggregate {
|
||||
/// The `IFNULL` SQL function.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// Team.annotated(with: Team.players.min(Column("score")) ?? 0)
|
||||
/// ```
|
||||
///
|
||||
/// The returned aggregate has the same key as the input.
|
||||
public static func ?? (lhs: Self, rhs: some SQLExpressible) -> Self {
|
||||
lhs
|
||||
.map { $0 ?? rhs }
|
||||
.with { $0.key = lhs.key } // Preserve key
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ABS` SQL function.
|
||||
public func abs<RowDecoder>(_ aggregate: AssociationAggregate<RowDecoder>)
|
||||
-> AssociationAggregate<RowDecoder>
|
||||
{
|
||||
aggregate.map(abs)
|
||||
}
|
||||
|
||||
/// The `CAST` SQL function.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/lang_expr.html#castexpr>
|
||||
public func cast<RowDecoder>(
|
||||
_ aggregate: AssociationAggregate<RowDecoder>,
|
||||
as storageClass: Database.StorageClass)
|
||||
-> AssociationAggregate<RowDecoder>
|
||||
{
|
||||
aggregate
|
||||
.map { cast($0, as: storageClass) }
|
||||
.with { $0.key = aggregate.key } // Preserve key
|
||||
}
|
||||
|
||||
/// The `LENGTH` SQL function.
|
||||
public func length<RowDecoder>(_ aggregate: AssociationAggregate<RowDecoder>)
|
||||
-> AssociationAggregate<RowDecoder>
|
||||
{
|
||||
aggregate.map(length)
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/// Thes `BelongsToAssociation` sets up a one-to-one connection from a record
|
||||
/// type to another record type, such as each instance of the declaring record
|
||||
/// "belongs to" an instance of the other record.
|
||||
///
|
||||
/// For example, if your application includes authors and books, and each book
|
||||
/// is assigned its author, you'd declare the association this way:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Author: TableRecord { }
|
||||
/// struct Book: TableRecord {
|
||||
/// static let author = belongsTo(Author.self)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A `BelongsToAssociation` should be supported by an SQLite foreign key.
|
||||
///
|
||||
/// Foreign keys are the recommended way to declare relationships between
|
||||
/// database tables because not only will SQLite guarantee the integrity of your
|
||||
/// data, but GRDB will be able to use those foreign keys to automatically
|
||||
/// configure your associations.
|
||||
///
|
||||
/// You define the foreign key when you create database tables. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "author") { t in
|
||||
/// t.autoIncrementedPrimaryKey("id") // (1)
|
||||
/// t.column("name", .text)
|
||||
/// }
|
||||
/// try db.create(table: "book") { t in
|
||||
/// t.autoIncrementedPrimaryKey("id")
|
||||
/// t.belongsTo("author", onDelete: .cascade) // (2)
|
||||
/// .notNull() // (3)
|
||||
/// t.column("title", .text)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 1. The author table has a primary key.
|
||||
/// 2. The `book.authorId` column is used to link a book to the author it
|
||||
/// belongs to. This column is indexed in order to ease the selection of
|
||||
/// an author's books. A foreign key is defined from `book.authorId`
|
||||
/// column to `authors.id`, so that SQLite guarantees that no book refers
|
||||
/// to a missing author. The `onDelete: .cascade` option has SQLite
|
||||
/// automatically delete all of an author's books when that author is
|
||||
/// deleted. See <https://sqlite.org/foreignkeys.html#fk_actions> for
|
||||
/// more information.
|
||||
/// 3. Make the `book.authorId` column not null if you want SQLite to guarantee
|
||||
/// that all books have an author.
|
||||
///
|
||||
/// The example above uses auto-incremented primary keys. But generally
|
||||
/// speaking, all primary keys are supported.
|
||||
///
|
||||
/// If the database schema does not define foreign keys between tables, you can
|
||||
/// still use `BelongsToAssociation`. But your help is needed to define the
|
||||
/// missing foreign key:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Book: TableRecord {
|
||||
/// static let author = belongsTo(Author.self, using: ForeignKey(...))
|
||||
/// }
|
||||
/// ```
|
||||
public struct BelongsToAssociation<Origin, Destination> {
|
||||
public var _sqlAssociation: _SQLAssociation
|
||||
|
||||
init(
|
||||
to destinationRelation: SQLRelation,
|
||||
key: String?,
|
||||
using foreignKey: ForeignKey?)
|
||||
{
|
||||
let destinationTable = destinationRelation.source.tableName
|
||||
|
||||
let foreignKeyCondition = SQLForeignKeyCondition(
|
||||
destinationTable: destinationTable,
|
||||
foreignKey: foreignKey,
|
||||
originIsLeft: true)
|
||||
|
||||
let associationKey: SQLAssociationKey
|
||||
if let key {
|
||||
associationKey = .fixedSingular(key)
|
||||
} else {
|
||||
associationKey = .inflected(destinationTable)
|
||||
}
|
||||
|
||||
_sqlAssociation = _SQLAssociation(
|
||||
key: associationKey,
|
||||
condition: .foreignKey(foreignKeyCondition),
|
||||
relation: destinationRelation,
|
||||
cardinality: .toOne)
|
||||
}
|
||||
}
|
||||
|
||||
extension BelongsToAssociation: AssociationToOne {
|
||||
public typealias OriginRowDecoder = Origin
|
||||
public typealias RowDecoder = Destination
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/// The `HasManyAssociation` indicates a one-to-many connection between two
|
||||
/// record types, such as each instance of the declaring record "has many"
|
||||
/// instances of the other record.
|
||||
///
|
||||
/// For example, if your application includes authors and books, and each author
|
||||
/// is assigned zero or more books, you'd declare the association this way:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Book: TableRecord { }
|
||||
/// struct Author: TableRecord {
|
||||
/// static let books = hasMany(Book.self)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A `HasManyAssociation` should be supported by an SQLite foreign key.
|
||||
///
|
||||
/// Foreign keys are the recommended way to declare relationships between
|
||||
/// database tables because not only will SQLite guarantee the integrity of your
|
||||
/// data, but GRDB will be able to use those foreign keys to automatically
|
||||
/// configure your associations.
|
||||
///
|
||||
/// You define the foreign key when you create database tables. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "author") { t in
|
||||
/// t.autoIncrementedPrimaryKey("id") // (1)
|
||||
/// t.column("name", .text)
|
||||
/// }
|
||||
/// try db.create(table: "book") { t in
|
||||
/// t.autoIncrementedPrimaryKey("id")
|
||||
/// t.belongsTo("author", onDelete: .cascade) // (2)
|
||||
/// .notNull() // (3)
|
||||
/// t.column("title", .text)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 1. The author table has a primary key.
|
||||
/// belongs to. This column is indexed in order to ease the selection of
|
||||
/// an author's books. A foreign key is defined from `book.authorId`
|
||||
/// column to `authors.id`, so that SQLite guarantees that no book refers
|
||||
/// to a missing author. The `onDelete: .cascade` option has SQLite
|
||||
/// automatically delete all of an author's books when that author is
|
||||
/// deleted. See <https://sqlite.org/foreignkeys.html#fk_actions> for
|
||||
/// more information.
|
||||
/// 3. Make the `book.authorId` column not null if you want SQLite to guarantee
|
||||
/// that all books have an author.
|
||||
///
|
||||
/// The example above uses auto-incremented primary keys. But generally
|
||||
/// speaking, all primary keys are supported.
|
||||
///
|
||||
/// If the database schema does not define foreign keys between tables, you can
|
||||
/// still use `HasManyAssociation`. But your help is needed to define the
|
||||
/// missing foreign key:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Author: TableRecord {
|
||||
/// static let books = hasMany(Book.self, using: ForeignKey(...))
|
||||
/// }
|
||||
/// ```
|
||||
public struct HasManyAssociation<Origin, Destination> {
|
||||
public var _sqlAssociation: _SQLAssociation
|
||||
|
||||
init(
|
||||
to destinationRelation: SQLRelation,
|
||||
key: String?,
|
||||
using foreignKey: ForeignKey?)
|
||||
{
|
||||
let destinationTable = destinationRelation.source.tableName
|
||||
|
||||
let foreignKeyCondition = SQLForeignKeyCondition(
|
||||
destinationTable: destinationTable,
|
||||
foreignKey: foreignKey,
|
||||
originIsLeft: false)
|
||||
|
||||
let associationKey: SQLAssociationKey
|
||||
if let key {
|
||||
associationKey = .fixedPlural(key)
|
||||
} else {
|
||||
associationKey = .inflected(destinationTable)
|
||||
}
|
||||
|
||||
_sqlAssociation = _SQLAssociation(
|
||||
key: associationKey,
|
||||
condition: .foreignKey(foreignKeyCondition),
|
||||
relation: destinationRelation,
|
||||
cardinality: .toMany)
|
||||
}
|
||||
}
|
||||
|
||||
extension HasManyAssociation: AssociationToMany {
|
||||
public typealias OriginRowDecoder = Origin
|
||||
public typealias RowDecoder = Destination
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/// The `HasManyThroughAssociation` is often used to set up a many-to-many
|
||||
/// connection with another record. This association indicates that the
|
||||
/// declaring record can be matched with zero or more instances of another
|
||||
/// record by proceeding through a third record.
|
||||
///
|
||||
/// For example, consider the practice of passport delivery. One country
|
||||
/// "has many" citizens "through" its passports:
|
||||
///
|
||||
/// ```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 `HasManyThroughAssociation` is also useful for setting up "shortcuts"
|
||||
/// through nested associations. For example, if a document has many sections,
|
||||
/// and a section has many paragraphs, you may sometimes want to get a simple
|
||||
/// collection of all paragraphs in the document. You could set
|
||||
/// that up this way:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Paragraph: TableRecord { }
|
||||
///
|
||||
/// struct Section: TableRecord {
|
||||
/// static let paragraphs = hasMany(Paragraph.self)
|
||||
/// }
|
||||
///
|
||||
/// struct Document: TableRecord {
|
||||
/// static let sections = hasMany(Section.self)
|
||||
/// static let paragraphs = hasMany(Paragraph.self,
|
||||
/// through: sections,
|
||||
/// using: Section.paragraphs)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// As in the examples above, `HasManyThroughAssociation` is always built from
|
||||
/// two other associations. Those associations can be any ``Association``.
|
||||
public struct HasManyThroughAssociation<Origin, Destination> {
|
||||
public var _sqlAssociation: _SQLAssociation
|
||||
|
||||
init<Pivot, Target>(
|
||||
through pivot: Pivot,
|
||||
using target: Target)
|
||||
where Pivot: Association,
|
||||
Target: Association,
|
||||
Pivot.OriginRowDecoder == Origin,
|
||||
Pivot.RowDecoder == Target.OriginRowDecoder,
|
||||
Target.RowDecoder == Destination
|
||||
{
|
||||
_sqlAssociation = target._sqlAssociation.through(pivot._sqlAssociation)
|
||||
}
|
||||
}
|
||||
|
||||
extension HasManyThroughAssociation: AssociationToMany {
|
||||
public typealias OriginRowDecoder = Origin
|
||||
public typealias RowDecoder = Destination
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/// The `HasOneAssociation` indicates a one-to-one connection between two
|
||||
/// record types, such as each instance of the declaring record "has one"
|
||||
/// instances of the other record.
|
||||
///
|
||||
/// For example, if your application has one database table for countries, and
|
||||
/// another for their demographic profiles, you'd declare the association
|
||||
/// this way:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Demographics: TableRecord { }
|
||||
/// struct Country: TableRecord {
|
||||
/// static let demographics = hasOne(Demographics.self)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A `HasOneAssociation` should be supported by an SQLite foreign key.
|
||||
///
|
||||
/// Foreign keys are the recommended way to declare relationships between
|
||||
/// database tables because not only will SQLite guarantee the integrity of your
|
||||
/// data, but GRDB will be able to use those foreign keys to automatically
|
||||
/// configure your associations.
|
||||
///
|
||||
/// You define the foreign key when you create database tables. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "country") { t in
|
||||
/// t.primaryKey("code", .text) // (1)
|
||||
/// t.column("name", .text)
|
||||
/// }
|
||||
/// try db.create(table: "demographics") { t in
|
||||
/// t.autoIncrementedPrimaryKey("id")
|
||||
/// t.belongsTo("country", onDelete: .cascade) // (2)
|
||||
/// .notNull() // (3)
|
||||
/// .unique() // (4)
|
||||
/// t.column("population", .integer)
|
||||
/// t.column("density", .double)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 1. The country table has a primary key.
|
||||
/// 2. The `demographics.countryCode` column is used to link a demographic
|
||||
/// profile to the country it belongs to. This column is indexed in order
|
||||
/// to ease the selection of the demographics of a country. A foreign key
|
||||
/// is defined from `demographics.countryCode` column to `country.code`,
|
||||
/// so that SQLite guarantees that no profile refers to a missing
|
||||
/// country. The `onDelete: .cascade` option has SQLite automatically
|
||||
/// delete a profile when its country is deleted. See
|
||||
/// <https://sqlite.org/foreignkeys.html#fk_actions> for more information.
|
||||
/// 3. Make the `demographics.countryCode` column not null if you want SQLite to
|
||||
/// guarantee that all profiles are linked to a country.
|
||||
/// 4. Create a unique index on the `demographics.countryCode` column in order
|
||||
/// to guarantee the unicity of any country's demographics.
|
||||
///
|
||||
/// The example above uses a string primary for the country table. But generally
|
||||
/// speaking, all primary keys are supported.
|
||||
///
|
||||
/// If the database schema does not define foreign keys between tables, you can
|
||||
/// still use `HasOneAssociation`. But your help is needed to define the
|
||||
/// missing foreign key:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Demographics: TableRecord { }
|
||||
/// struct Country: TableRecord {
|
||||
/// static let demographics = hasOne(Demographics.self, using: ForeignKey(...)
|
||||
/// }
|
||||
/// ```
|
||||
public struct HasOneAssociation<Origin, Destination> {
|
||||
public var _sqlAssociation: _SQLAssociation
|
||||
|
||||
init(
|
||||
to destinationRelation: SQLRelation,
|
||||
key: String?,
|
||||
using foreignKey: ForeignKey?)
|
||||
{
|
||||
let destinationTable = destinationRelation.source.tableName
|
||||
|
||||
let foreignKeyCondition = SQLForeignKeyCondition(
|
||||
destinationTable: destinationTable,
|
||||
foreignKey: foreignKey,
|
||||
originIsLeft: false)
|
||||
|
||||
let associationKey: SQLAssociationKey
|
||||
if let key {
|
||||
associationKey = .fixedSingular(key)
|
||||
} else {
|
||||
associationKey = .inflected(destinationTable)
|
||||
}
|
||||
|
||||
_sqlAssociation = _SQLAssociation(
|
||||
key: associationKey,
|
||||
condition: .foreignKey(foreignKeyCondition),
|
||||
relation: destinationRelation,
|
||||
cardinality: .toOne)
|
||||
}
|
||||
}
|
||||
|
||||
extension HasOneAssociation: AssociationToOne {
|
||||
public typealias OriginRowDecoder = Origin
|
||||
public typealias RowDecoder = Destination
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/// The `HasOneThroughAssociation` sets up a one-to-one connection with
|
||||
/// another record. This association indicates that the declaring record can be
|
||||
/// matched with one instance of another record by proceeding through a third
|
||||
/// record.
|
||||
///
|
||||
/// For example, if each book belongs to a library, and each library has
|
||||
/// one address, then one knows where the book should be returned to:
|
||||
///
|
||||
/// ```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")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// As in the example above, `HasOneThroughAssociation` is always built from
|
||||
/// two other associations. Those associations can be any association that
|
||||
/// declares a to-one connection (``AssociationToOne``).
|
||||
public struct HasOneThroughAssociation<Origin, Destination> {
|
||||
public var _sqlAssociation: _SQLAssociation
|
||||
|
||||
init<Pivot, Target>(
|
||||
through pivot: Pivot,
|
||||
using target: Target)
|
||||
where Pivot: AssociationToOne,
|
||||
Target: AssociationToOne,
|
||||
Pivot.OriginRowDecoder == Origin,
|
||||
Pivot.RowDecoder == Target.OriginRowDecoder,
|
||||
Target.RowDecoder == Destination
|
||||
{
|
||||
_sqlAssociation = target._sqlAssociation.through(pivot._sqlAssociation)
|
||||
}
|
||||
}
|
||||
|
||||
extension HasOneThroughAssociation: AssociationToOne {
|
||||
public typealias OriginRowDecoder = Origin
|
||||
public typealias RowDecoder = Destination
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/// The `JoinAssociation` joins common table expression to regular
|
||||
/// tables or other common table expressions.
|
||||
public struct JoinAssociation<Origin, Destination> {
|
||||
public var _sqlAssociation: _SQLAssociation
|
||||
|
||||
/// Creates a `JoinAssociation` whose key is the table name of the relation.
|
||||
init(
|
||||
to relation: SQLRelation,
|
||||
condition: SQLAssociationCondition)
|
||||
{
|
||||
_sqlAssociation = _SQLAssociation(
|
||||
key: .inflected(relation.source.tableName),
|
||||
condition: condition,
|
||||
relation: relation,
|
||||
cardinality: .toOne)
|
||||
}
|
||||
}
|
||||
|
||||
extension JoinAssociation: AssociationToOne {
|
||||
public typealias OriginRowDecoder = Origin
|
||||
public typealias RowDecoder = Destination
|
||||
}
|
||||
+482
@@ -0,0 +1,482 @@
|
||||
/// A [common table expression](https://sqlite.org/lang_with.html) that can be
|
||||
/// used with the GRDB query interface.
|
||||
public struct CommonTableExpression<RowDecoder> {
|
||||
/// The table name of the common table expression.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // WITH answer AS (SELECT 42) ...
|
||||
/// let answer = CommonTableExpression(
|
||||
/// named: "answer",
|
||||
/// sql: "SELECT 42")
|
||||
/// answer.tableName // "answer"
|
||||
public var tableName: String {
|
||||
cte.tableName
|
||||
}
|
||||
|
||||
var cte: SQLCTE
|
||||
|
||||
/// Creates a common table expression from a request.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player) ...
|
||||
/// let p = CommonTableExpression(
|
||||
/// named: "p",
|
||||
/// request: Player.all(),
|
||||
/// type: Void.self)
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player) ...
|
||||
/// let p = CommonTableExpression(
|
||||
/// named: "p",
|
||||
/// request: SQLRequest<Player>(sql: "SELECT * FROM player"),
|
||||
/// type: Void.self)
|
||||
///
|
||||
/// - parameter recursive: Whether this common table expression needs a
|
||||
/// `WITH RECURSIVE` sql clause.
|
||||
/// - parameter tableName: The table name of the common table expression.
|
||||
/// - parameter columns: The columns of the common table expression. If nil,
|
||||
/// the columns are the columns of the request.
|
||||
/// - parameter request: A request.
|
||||
private init(
|
||||
recursive: Bool = false,
|
||||
named tableName: String,
|
||||
columns: [String]? = nil,
|
||||
request: some SQLSubqueryable,
|
||||
type: RowDecoder.Type)
|
||||
{
|
||||
self.cte = SQLCTE(
|
||||
tableName: tableName,
|
||||
columns: columns,
|
||||
sqlSubquery: request.sqlSubquery,
|
||||
isRecursive: recursive)
|
||||
}
|
||||
}
|
||||
|
||||
extension CommonTableExpression {
|
||||
/// Creates a common table expression from a request.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player) ...
|
||||
/// let p = CommonTableExpression<Void>(
|
||||
/// named: "p",
|
||||
/// request: Player.all())
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player) ...
|
||||
/// let p = CommonTableExpression<Void>(
|
||||
/// named: "p",
|
||||
/// request: SQLRequest<Player>(sql: "SELECT * FROM player"))
|
||||
///
|
||||
/// - parameter recursive: Whether this common table expression needs a
|
||||
/// `WITH RECURSIVE` sql clause.
|
||||
/// - parameter tableName: The table name of the common table expression.
|
||||
/// - parameter columns: The columns of the common table expression. If nil,
|
||||
/// the columns are the columns of the request.
|
||||
/// - parameter request: A request.
|
||||
public init(
|
||||
recursive: Bool = false,
|
||||
named tableName: String,
|
||||
columns: [String]? = nil,
|
||||
request: some SQLSubqueryable)
|
||||
{
|
||||
self.init(
|
||||
recursive: recursive,
|
||||
named: tableName,
|
||||
columns: columns,
|
||||
request: request,
|
||||
type: RowDecoder.self)
|
||||
}
|
||||
|
||||
/// Creates a common table expression from an SQL string and
|
||||
/// optional arguments.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
|
||||
/// let p = CommonTableExpression<Void>(
|
||||
/// named: "p",
|
||||
/// sql: "SELECT * FROM player WHERE name = ?",
|
||||
/// arguments: ["O'Brien"])
|
||||
///
|
||||
/// - parameter recursive: Whether this common table expression needs a
|
||||
/// `WITH RECURSIVE` sql clause.
|
||||
/// - parameter tableName: The table name of the common table expression.
|
||||
/// - parameter columns: The columns of the common table expression. If nil,
|
||||
/// the columns are the columns of the request.
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - parameter arguments: Statement arguments.
|
||||
public init(
|
||||
recursive: Bool = false,
|
||||
named tableName: String,
|
||||
columns: [String]? = nil,
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments())
|
||||
{
|
||||
self.init(
|
||||
recursive: recursive,
|
||||
named: tableName,
|
||||
columns: columns,
|
||||
request: SQLRequest(sql: sql, arguments: arguments),
|
||||
type: RowDecoder.self)
|
||||
}
|
||||
|
||||
/// Creates a common table expression from an SQL *literal*.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
|
||||
/// let name = "O'Brien"
|
||||
/// let p = CommonTableExpression<Void>(
|
||||
/// named: "p",
|
||||
/// literal: "SELECT * FROM player WHERE name = \(name)")
|
||||
///
|
||||
/// - parameter recursive: Whether this common table expression needs a
|
||||
/// `WITH RECURSIVE` sql clause.
|
||||
/// - parameter tableName: The table name of the common table expression.
|
||||
/// - parameter columns: The columns of the common table expression. If nil,
|
||||
/// the columns are the columns of the request.
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
public init(
|
||||
recursive: Bool = false,
|
||||
named tableName: String,
|
||||
columns: [String]? = nil,
|
||||
literal sqlLiteral: SQL)
|
||||
{
|
||||
self.init(
|
||||
recursive: recursive,
|
||||
named: tableName,
|
||||
columns: columns,
|
||||
request: SQLRequest(literal: sqlLiteral),
|
||||
type: RowDecoder.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension CommonTableExpression<Row> {
|
||||
/// Creates a common table expression from a request.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player) ...
|
||||
/// let p = CommonTableExpression(
|
||||
/// named: "p",
|
||||
/// request: Player.all())
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player) ...
|
||||
/// let p = CommonTableExpression(
|
||||
/// named: "p",
|
||||
/// request: SQLRequest<Player>(sql: "SELECT * FROM player"))
|
||||
///
|
||||
/// - parameter recursive: Whether this common table expression needs a
|
||||
/// `WITH RECURSIVE` sql clause.
|
||||
/// - parameter tableName: The table name of the common table expression.
|
||||
/// - parameter columns: The columns of the common table expression. If nil,
|
||||
/// the columns are the columns of the request.
|
||||
/// - parameter request: A request.
|
||||
public init(
|
||||
recursive: Bool = false,
|
||||
named tableName: String,
|
||||
columns: [String]? = nil,
|
||||
request: some SQLSubqueryable)
|
||||
{
|
||||
self.init(
|
||||
recursive: recursive,
|
||||
named: tableName,
|
||||
columns: columns,
|
||||
request: request,
|
||||
type: Row.self)
|
||||
}
|
||||
|
||||
/// Creates a common table expression from an SQL string and
|
||||
/// optional arguments.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
|
||||
/// let p = CommonTableExpression(
|
||||
/// named: "p",
|
||||
/// sql: "SELECT * FROM player WHERE name = ?",
|
||||
/// arguments: ["O'Brien"])
|
||||
///
|
||||
/// - parameter recursive: Whether this common table expression needs a
|
||||
/// `WITH RECURSIVE` sql clause.
|
||||
/// - parameter tableName: The table name of the common table expression.
|
||||
/// - parameter columns: The columns of the common table expression. If nil,
|
||||
/// the columns are the columns of the request.
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - parameter arguments: Statement arguments.
|
||||
public init(
|
||||
recursive: Bool = false,
|
||||
named tableName: String,
|
||||
columns: [String]? = nil,
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments())
|
||||
{
|
||||
self.init(
|
||||
recursive: recursive,
|
||||
named: tableName,
|
||||
columns: columns,
|
||||
request: SQLRequest(sql: sql, arguments: arguments),
|
||||
type: Row.self)
|
||||
}
|
||||
|
||||
/// Creates a common table expression from an SQL *literal*.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
|
||||
/// let name = "O'Brien"
|
||||
/// let p = CommonTableExpression(
|
||||
/// named: "p",
|
||||
/// literal: "SELECT * FROM player WHERE name = \(name)")
|
||||
///
|
||||
/// - parameter recursive: Whether this common table expression needs a
|
||||
/// `WITH RECURSIVE` sql clause.
|
||||
/// - parameter tableName: The table name of the common table expression.
|
||||
/// - parameter columns: The columns of the common table expression. If nil,
|
||||
/// the columns are the columns of the request.
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
public init(
|
||||
recursive: Bool = false,
|
||||
named tableName: String,
|
||||
columns: [String]? = nil,
|
||||
literal sqlLiteral: SQL)
|
||||
{
|
||||
self.init(
|
||||
recursive: recursive,
|
||||
named: tableName,
|
||||
columns: columns,
|
||||
request: SQLRequest(literal: sqlLiteral),
|
||||
type: Row.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension CommonTableExpression {
|
||||
var relationForAll: SQLRelation {
|
||||
.all(fromTable: tableName)
|
||||
}
|
||||
|
||||
/// Creates a request for all rows of the common table expression.
|
||||
///
|
||||
/// You can fetch from this request:
|
||||
///
|
||||
/// // WITH answer AS (SELECT 42 AS value)
|
||||
/// // SELECT * FROM answer
|
||||
/// struct Answer: Decodable, FetchableRecord {
|
||||
/// var value: Int
|
||||
/// }
|
||||
/// let cte = CommonTableExpression<Answer>(
|
||||
/// named: "answer",
|
||||
/// sql: "SELECT 42 AS value")
|
||||
/// let answer = try cte.all().with(cte).fetchOne(db)!
|
||||
/// print(answer.value) // prints 42
|
||||
///
|
||||
/// You can embed this request as a subquery:
|
||||
///
|
||||
/// // WITH answer AS (SELECT 42 AS value)
|
||||
/// // SELECT * FROM player
|
||||
/// // WHERE score = (SELECT * FROM answer)
|
||||
/// let answer = CommonTableExpression(
|
||||
/// named: "answer",
|
||||
/// sql: "SELECT 42 AS value")
|
||||
/// let players = try Player
|
||||
/// .filter(Column("score") == answer.all())
|
||||
/// .with(answer)
|
||||
/// .fetchAll(db)
|
||||
public func all() -> QueryInterfaceRequest<RowDecoder> {
|
||||
QueryInterfaceRequest(relation: relationForAll)
|
||||
}
|
||||
|
||||
/// An SQL expression that checks the inclusion of an expression in a
|
||||
/// common table expression.
|
||||
///
|
||||
/// let playerNameCTE = CommonTableExpression(
|
||||
/// named: "playerName",
|
||||
/// request: Player.select(Column("name"))
|
||||
///
|
||||
/// // name IN playerName
|
||||
/// playerNameCTE.contains(Column("name"))
|
||||
public func contains(_ element: some SQLExpressible) -> SQLExpression {
|
||||
SQLCollection.table(tableName).contains(element.sqlExpression)
|
||||
}
|
||||
}
|
||||
|
||||
/// A low-level common table expression
|
||||
struct SQLCTE {
|
||||
/// The table name of the common table expression.
|
||||
var tableName: String
|
||||
|
||||
/// The columns of the common table expression.
|
||||
///
|
||||
/// When nil, the CTE selects the columns of the request:
|
||||
///
|
||||
/// -- Columns a, b
|
||||
/// WITH t AS (SELECT 1 AS a, 2 AS b) ...
|
||||
///
|
||||
/// When not nil, `columns` provides the columns of the CTE:
|
||||
///
|
||||
/// -- Column id
|
||||
/// WITH t(id) AS (SELECT 1) ...
|
||||
/// ~~
|
||||
var columns: [String]?
|
||||
|
||||
/// The common table expression subquery.
|
||||
///
|
||||
/// WITH t AS (SELECT ...)
|
||||
/// ~~~~~~~~~~
|
||||
var sqlSubquery: SQLSubquery
|
||||
|
||||
/// Whether this common table expression needs a `WITH RECURSIVE`
|
||||
/// sql clause.
|
||||
var isRecursive: Bool
|
||||
|
||||
/// The number of columns in the common table expression.
|
||||
func columnCount(_ db: Database) throws -> Int {
|
||||
if let columns {
|
||||
// No need to hit the database
|
||||
return columns.count
|
||||
}
|
||||
|
||||
do {
|
||||
return try sqlSubquery.columnCount(db)
|
||||
} catch let error as DatabaseError where error.resultCode == .SQLITE_ERROR {
|
||||
// Maybe the CTE refers to other CTEs: https://github.com/groue/GRDB.swift/issues/1275
|
||||
// We can't modify the CTE request by creating or extending the
|
||||
// WITH clause with other CTEs, because we'd need to parse SQL.
|
||||
// So let's rewrite the error message, and guide the user towards
|
||||
// a more precise CTE definition:
|
||||
let message = [
|
||||
[
|
||||
"""
|
||||
Can't compute the number of columns in the \
|
||||
\(String(reflecting: tableName)) common table expression
|
||||
""",
|
||||
error.message,
|
||||
].compactMap { $0 }.joined(separator: ": "),
|
||||
"""
|
||||
Check the syntax of the SQL definition, or provide the \
|
||||
explicit list of selected columns with the `columns` parameter \
|
||||
in the CommonTableExpression initializer.
|
||||
""",
|
||||
].joined(separator: ". ")
|
||||
throw DatabaseError(
|
||||
resultCode: error.extendedResultCode,
|
||||
message: message,
|
||||
sql: error.sql,
|
||||
arguments: error.arguments,
|
||||
publicStatementArguments: error.publicStatementArguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CommonTableExpression {
|
||||
/// Creates an association to a common table expression that you can join
|
||||
/// or include in another request.
|
||||
///
|
||||
/// The key of the returned association is the table name of the common
|
||||
/// table expression.
|
||||
///
|
||||
/// - 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 func association<Destination>(
|
||||
to cte: CommonTableExpression<Destination>,
|
||||
on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible)
|
||||
-> JoinAssociation<RowDecoder, Destination>
|
||||
{
|
||||
JoinAssociation(
|
||||
to: cte.relationForAll,
|
||||
condition: .expression { condition($0, $1).sqlExpression })
|
||||
}
|
||||
|
||||
/// Creates an association to a common table expression that you can join
|
||||
/// or include in another request.
|
||||
///
|
||||
/// 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 func association<Destination>(
|
||||
to cte: CommonTableExpression<Destination>)
|
||||
-> JoinAssociation<RowDecoder, Destination>
|
||||
{
|
||||
JoinAssociation(to: cte.relationForAll, condition: .none)
|
||||
}
|
||||
|
||||
/// Creates an association to a table record that you can join
|
||||
/// or include in another request.
|
||||
///
|
||||
/// The key of the returned association is the table name of `Destination`.
|
||||
///
|
||||
/// - parameter destination: The record type at the other side of
|
||||
/// the association.
|
||||
/// - 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 func association<Destination>(
|
||||
to destination: Destination.Type,
|
||||
on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible)
|
||||
-> JoinAssociation<RowDecoder, Destination>
|
||||
where Destination: TableRecord
|
||||
{
|
||||
JoinAssociation(
|
||||
to: Destination.relationForAll,
|
||||
condition: .expression { condition($0, $1).sqlExpression })
|
||||
}
|
||||
|
||||
/// Creates an association to a table record that you can join
|
||||
/// or include in another request.
|
||||
///
|
||||
/// The key of the returned association is the table name of `Destination`.
|
||||
///
|
||||
/// - parameter destination: The record type at the other side of
|
||||
/// the association.
|
||||
/// - returns: An association to the common table expression.
|
||||
public func association<Destination>(
|
||||
to destination: Destination.Type)
|
||||
-> JoinAssociation<RowDecoder, Destination>
|
||||
where Destination: TableRecord
|
||||
{
|
||||
JoinAssociation(to: Destination.relationForAll, condition: .none)
|
||||
}
|
||||
|
||||
/// Creates an association to a table that you can join
|
||||
/// or include in another request.
|
||||
///
|
||||
/// The key of the returned association is the table name of `Destination`.
|
||||
///
|
||||
/// - parameter destination: The table at the other side of the association.
|
||||
/// - 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 func association<Destination>(
|
||||
to destination: Table<Destination>,
|
||||
on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible)
|
||||
-> JoinAssociation<RowDecoder, Destination>
|
||||
{
|
||||
JoinAssociation(
|
||||
to: destination.relationForAll,
|
||||
condition: .expression { condition($0, $1).sqlExpression })
|
||||
}
|
||||
|
||||
/// Creates an association to a table that you can join
|
||||
/// or include in another request.
|
||||
///
|
||||
/// The key of the returned association is the table name of `Destination`.
|
||||
///
|
||||
/// - parameter destination: The table at the other side of the association.
|
||||
/// - returns: An association to the common table expression.
|
||||
public func association<Destination>(
|
||||
to destination: Table<Destination>)
|
||||
-> JoinAssociation<RowDecoder, Destination>
|
||||
{
|
||||
JoinAssociation(to: destination.relationForAll, condition: .none)
|
||||
}
|
||||
}
|
||||
+1698
File diff suppressed because it is too large
Load Diff
+1570
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user