312 lines
10 KiB
Swift
312 lines
10 KiB
Swift
// MARK: - _SQLAssociation
|
|
|
|
/// An SQL association is a non-empty chain of steps which starts at the
|
|
/// "pivot" and ends on the "destination":
|
|
///
|
|
/// // SELECT origin.*, destination.*
|
|
/// // FROM origin
|
|
/// // JOIN pivot ON ...
|
|
/// // JOIN ...
|
|
/// // JOIN ...
|
|
/// // JOIN destination ON ...
|
|
/// Origin.including(required: association)
|
|
///
|
|
/// For direct associations such as BelongTo or HasMany, the chain contains a
|
|
/// single element, the "destination", without intermediate step:
|
|
///
|
|
/// // "Origin" belongsTo "destination":
|
|
/// // SELECT origin.*, destination.*
|
|
/// // FROM origin
|
|
/// // JOIN destination ON destination.originId = origin.id
|
|
/// let association = Origin.belongsTo(Destination.self)
|
|
/// Origin.including(required: association)
|
|
///
|
|
/// Indirect associations such as HasManyThrough have one or several
|
|
/// intermediate steps:
|
|
///
|
|
/// // "Origin" has many "destination" through "pivot":
|
|
/// // SELECT origin.*, destination.*
|
|
/// // FROM origin
|
|
/// // JOIN pivot ON pivot.originId = origin.id
|
|
/// // JOIN destination ON destination.id = pivot.destinationId
|
|
/// let association = Origin.hasMany(
|
|
/// Destination.self,
|
|
/// through: Origin.hasMany(Pivot.self),
|
|
/// via: Pivot.belongsTo(Destination.self))
|
|
/// Origin.including(required: association)
|
|
///
|
|
/// // "Origin" has many "destination" through "pivot1" and "pivot2":
|
|
/// // SELECT origin.*, destination.*
|
|
/// // FROM origin
|
|
/// // JOIN pivot1 ON pivot1.originId = origin.id
|
|
/// // JOIN pivot2 ON pivot2.pivot1Id = pivot1.id
|
|
/// // JOIN destination ON destination.id = pivot.destinationId
|
|
/// let association = Origin.hasMany(
|
|
/// Destination.self,
|
|
/// through: Origin.hasMany(Pivot1.self),
|
|
/// via: Pivot1.hasMany(
|
|
/// Destination.self,
|
|
/// through: Pivot1.hasMany(Pivot2.self),
|
|
/// via: Pivot2.belongsTo(Destination.self)))
|
|
/// Origin.including(required: association)
|
|
public struct _SQLAssociation {
|
|
// All steps, from pivot to destination. Never empty.
|
|
private(set) var steps: [SQLAssociationStep]
|
|
var keyPath: [String] { steps.map(\.keyName) }
|
|
|
|
var destination: SQLAssociationStep {
|
|
get { steps[steps.count - 1] }
|
|
set { steps[steps.count - 1] = newValue }
|
|
}
|
|
|
|
var pivot: SQLAssociationStep {
|
|
get { steps[0] }
|
|
set { steps[0] = newValue }
|
|
}
|
|
|
|
init(steps: [SQLAssociationStep]) {
|
|
assert(!steps.isEmpty)
|
|
self.steps = steps
|
|
}
|
|
|
|
init(
|
|
key: SQLAssociationKey,
|
|
condition: SQLAssociationCondition,
|
|
relation: SQLRelation,
|
|
cardinality: SQLAssociationCardinality)
|
|
{
|
|
let step = SQLAssociationStep(
|
|
key: key,
|
|
condition: condition,
|
|
relation: relation,
|
|
cardinality: cardinality)
|
|
self.init(steps: [step])
|
|
}
|
|
|
|
/// Changes the destination key
|
|
func forDestinationKey(_ key: SQLAssociationKey) -> Self {
|
|
with {
|
|
$0.destination.key = key
|
|
}
|
|
}
|
|
|
|
/// Returns a new association
|
|
func through(_ other: _SQLAssociation) -> Self {
|
|
_SQLAssociation(steps: other.steps + steps)
|
|
}
|
|
|
|
/// Returns the destination of the association, reversing the association
|
|
/// up to the pivot.
|
|
///
|
|
/// This method feeds `TableRecord.request(for:)`, and allows
|
|
/// `including(all:)` to prefetch associated records.
|
|
func destinationRelation() -> SQLRelation {
|
|
if steps.count == 1 {
|
|
return destination.relation
|
|
}
|
|
|
|
// This is an indirect join from origin to destination, through
|
|
// some intermediate steps:
|
|
//
|
|
// SELECT destination.*
|
|
// FROM destination
|
|
// JOIN pivot ON (pivot.destinationId = destination.id) AND (pivot.originId = 1)
|
|
//
|
|
// let association = Origin.hasMany(
|
|
// Destination.self,
|
|
// through: Origin.hasMany(Pivot.self),
|
|
// via: Pivot.belongsTo(Destination.self))
|
|
// Origin(id: 1).request(for: association)
|
|
let reversedSteps = zip(steps, steps.dropFirst())
|
|
.map { (step, nextStep) in
|
|
// Intermediate steps are not selected, and including(all:)
|
|
// children can't impact the destination relation:
|
|
let relation = step.relation
|
|
.selectOnly([])
|
|
.removingPrefetchedAssociations()
|
|
|
|
// Don't interfere with user-defined keys that could be added later
|
|
let key = step.key.with {
|
|
$0.baseName = "grdb_\($0.baseName)"
|
|
}
|
|
|
|
return SQLAssociationStep(
|
|
key: key,
|
|
condition: nextStep.condition.reversed(to: step.relation.source.tableName),
|
|
relation: relation,
|
|
cardinality: .toOne)
|
|
}
|
|
.reversed()
|
|
let reversedAssociation = _SQLAssociation(steps: Array(reversedSteps))
|
|
return destination.relation.appendingChild(for: reversedAssociation, kind: .oneRequired)
|
|
}
|
|
}
|
|
|
|
extension _SQLAssociation: Refinable { }
|
|
|
|
struct SQLAssociationStep: Refinable {
|
|
var key: SQLAssociationKey
|
|
var condition: SQLAssociationCondition
|
|
var relation: SQLRelation
|
|
var cardinality: SQLAssociationCardinality
|
|
|
|
var keyName: String { key.name(singular: cardinality.isSingular) }
|
|
}
|
|
|
|
enum SQLAssociationCardinality {
|
|
case toOne
|
|
case toMany
|
|
|
|
var isSingular: Bool {
|
|
switch self {
|
|
case .toOne:
|
|
return true
|
|
case .toMany:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SQLAssociationKey
|
|
|
|
/// Associations are meant to be consumed, most often into Decodable records.
|
|
///
|
|
/// Those records have singular or plural property names, and we want
|
|
/// associations to be able to fill those singular or plural names
|
|
/// automatically, so that the user does not have to perform explicit
|
|
/// decoding configuration.
|
|
///
|
|
/// Those plural or singular names are not decided when the association is
|
|
/// defined. For example, the Author.books association, which looks plural, may
|
|
/// actually generate "book" or "books" depending on the context:
|
|
///
|
|
/// struct Author: TableRecord {
|
|
/// static let books = hasMany(Book.self)
|
|
/// }
|
|
/// struct Book: TableRecord {
|
|
/// }
|
|
///
|
|
/// // "books"
|
|
/// struct AuthorInfo: FetchableRecord, Decodable {
|
|
/// var author: Author
|
|
/// var books: [Book]
|
|
/// }
|
|
/// let request = Author.including(all: Author.books)
|
|
/// let authorInfos = try AuthorInfo.fetchAll(db, request)
|
|
///
|
|
/// "book"
|
|
/// struct AuthorInfo: FetchableRecord, Decodable {
|
|
/// var author: Author
|
|
/// var book: Book
|
|
/// }
|
|
/// let request = Author.including(required: Author.books)
|
|
/// let authorInfos = try AuthorInfo.fetchAll(db, request)
|
|
///
|
|
/// "bookCount"
|
|
/// struct AuthorInfo: FetchableRecord, Decodable {
|
|
/// var author: Author
|
|
/// var bookCount: Int
|
|
/// }
|
|
/// let request = Author.annotated(with: Author.books.count)
|
|
/// let authorInfos = try AuthorInfo.fetchAll(db, request)
|
|
///
|
|
/// The SQLAssociationKey type aims at providing the necessary support for
|
|
/// those various inflections.
|
|
enum SQLAssociationKey: Refinable {
|
|
/// A key that is inflected in singular and plural contexts.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// struct Author: TableRecord {
|
|
/// static let databaseTableName = "authors"
|
|
/// }
|
|
/// struct Book: TableRecord {
|
|
/// let author = belongsTo(Author.self)
|
|
/// }
|
|
///
|
|
/// let request = Book.including(required: Book.author)
|
|
/// let row = try Row.fetchOne(db, request)!
|
|
/// row.scopes["author"] // singularized "authors" table name
|
|
case inflected(String)
|
|
|
|
/// A key that is inflected in plural contexts, but stricly honors
|
|
/// user-provided name in singular contexts.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// struct Country: TableRecord {
|
|
/// let demographics = hasOne(Demographics.self, key: "demographics")
|
|
/// }
|
|
///
|
|
/// let request = Country.including(required: Country.demographics)
|
|
/// let row = try Row.fetchOne(db, request)!
|
|
/// row.scopes["demographics"] // not singularized
|
|
case fixedSingular(String)
|
|
|
|
/// A key that is inflected in singular contexts, but stricly honors
|
|
/// user-provided name in plural contexts.
|
|
/// See .inflected and .fixedSingular for some context.
|
|
case fixedPlural(String)
|
|
|
|
/// A key that is never inflected.
|
|
case fixed(String)
|
|
|
|
var baseName: String {
|
|
get {
|
|
switch self {
|
|
case let .inflected(name),
|
|
let .fixedSingular(name),
|
|
let .fixedPlural(name),
|
|
let .fixed(name):
|
|
return name
|
|
}
|
|
}
|
|
set {
|
|
switch self {
|
|
case .inflected:
|
|
self = .inflected(newValue)
|
|
case .fixedSingular:
|
|
self = .fixedSingular(newValue)
|
|
case .fixedPlural:
|
|
self = .fixedPlural(newValue)
|
|
case .fixed:
|
|
self = .fixed(newValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
func name(singular: Bool) -> String {
|
|
if singular {
|
|
return singularizedName
|
|
} else {
|
|
return pluralizedName
|
|
}
|
|
}
|
|
|
|
var pluralizedName: String {
|
|
switch self {
|
|
case .inflected(let name):
|
|
return name.pluralized
|
|
case .fixedSingular(let name):
|
|
return name.pluralized
|
|
case .fixedPlural(let name):
|
|
return name
|
|
case .fixed(let name):
|
|
return name
|
|
}
|
|
}
|
|
|
|
var singularizedName: String {
|
|
switch self {
|
|
case .inflected(let name):
|
|
return name.singularized
|
|
case .fixedSingular(let name):
|
|
return name
|
|
case .fixedPlural(let name):
|
|
return name.singularized
|
|
case .fixed(let name):
|
|
return name
|
|
}
|
|
}
|
|
}
|