Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/QueryInterface/SQL/SQLRelation.swift
T
2025-01-28 12:28:03 +08:00

1263 lines
46 KiB
Swift

/// A "relation", as defined by the [relational
/// terminology](https://en.wikipedia.org/wiki/Relational_database#Terminology),
/// is "a set of tuples sharing the same attributes; a set of columns and rows."
///
/// WITH ... -- ctes
/// SELECT ... -- selectionPromise
/// FROM ... -- source
/// JOIN ... -- children
/// WHERE ... -- filterPromise
/// GROUP BY ... -- groupPromise
/// HAVING ... -- havingExpressionPromise
/// ORDER BY ... -- ordering
/// LIMIT ... -- limit
///
/// ## Promises
///
/// Most relation elements are "promises" which are resolved when a database
/// connection is available. This is how we can implement requests such
/// as `Record.filter(key: 1)` or `Record.orderByPrimaryKey()`: both need a
/// database connection in order to introspect the primary key. For example:
///
/// // SELECT * FROM country ORDER BY code
/// // ~~~~~~~~~~~~~
/// // primary key infered from the database schema
/// Country.orderByPrimaryKey()
///
/// ## Children
///
/// Relations have children. A child is a link to another relation, and
/// provide support for joins and prefetched relations.
///
/// Relations and their children constitute a tree of relations, which the user
/// builds with associations:
///
/// // Builds a relation with two children:
/// Author
/// .including(required: Author.country)
/// .including(all: Author.books)
///
/// There are four kinds of children:
///
/// - `.oneOptional`:
///
/// Such children are left joined. They may, or not, be included in
/// the selection.
///
/// // SELECT book.*
/// // FROM book
/// // LEFT JOIN author ON author.id = book.id
/// Book.joining(optional: Book.author)
///
/// // SELECT book.*, author.*
/// // FROM book
/// // LEFT JOIN author ON author.id = book.id
/// Book.including(optional: Book.author)
///
/// - `.oneRequired`:
///
/// Such children are inner joined. They may, or not, be included in
/// the selection.
///
/// // SELECT book.*
/// // FROM book
/// // JOIN author ON author.id = book.id AND author.country = 'FR'
/// Book.joining(required: Book.author.filter(Column("country") == "FR"))
///
/// // SELECT book.*, author.*
/// // FROM book
/// // JOIN author ON author.id = book.id AND author.country = 'FR'
/// Book.including(required: Book.author.filter(Column("country") == "FR"))
///
/// - `.all`:
///
/// Such children are prefetched using an extra SQL request:
///
/// // SELECT * FROM countries;
/// // SELECT * FROM passport
/// // WHERE countryCode IN ('BE', 'DE', 'FR', ...);
/// Country.including(all: Country.passports)
///
/// - `.bridge`:
///
/// Such children are not joined, and not prefetched. They are used as
/// intermediate children towards a prefetched child. In the example
/// below, the country relation has a `.bridge` child to passports, and the
/// passport relation has an `.all` child to citizens.
///
/// // SELECT * FROM countries;
/// // SELECT citizens.* FROM citizens
/// // JOIN passport ON passport.citizenId = citizens.id
/// // AND passport.countryCode IN ('BE', 'DE', 'FR', ...);
/// Country.including(all: Country.citizens)
struct SQLRelation {
struct Child: Refinable {
enum Kind {
// Record.including(optional: association)
case oneOptional
// Record.including(required: association)
case oneRequired
// Record.including(all: association)
case all
// Record.including(all: associationThroughPivot)
case bridge
}
var kind: Kind
var condition: SQLAssociationCondition
var relation: SQLRelation
/// Returns whether this child can change the parent count.
///
/// Record.including(required: association) // true
/// Record.including(all: association) // false
var impactsParentCount: Bool {
switch kind {
case .oneRequired:
// INNER JOIN can clearly reduce the number of rows
return true
case .oneOptional:
// LEFT JOIN does not itself reduce the number of rows, but
// maybe the joined table is used somewhere in the relation, in
// a way that can reduce the number of rows.
return true
case .all, .bridge:
// HasMany associations are prefetched in another SQL request:
// they have no impact on this relation.
return false
}
}
init(kind: SQLRelation.Child.Kind, condition: SQLAssociationCondition, relation: SQLRelation) {
switch kind {
case .oneOptional, .oneRequired, .bridge:
if relation.isDistinct {
fatalError("Not implemented: join an association that selects DISTINCT rows")
}
if relation.groupPromise != nil || relation.havingExpressionPromise != nil {
fatalError("Not implemented: join an association with a GROUP BY clause")
}
if relation.limit != nil {
fatalError("Not implemented: join an association with a LIMIT clause")
}
case .all:
break
}
self.kind = kind
self.condition = condition
self.relation = relation
}
fileprivate func makeAssociationForKey(_ key: String) -> _SQLAssociation {
let key = SQLAssociationKey.fixed(key)
let cardinality: SQLAssociationCardinality
switch kind {
case .oneOptional, .oneRequired:
cardinality = .toOne
case .all, .bridge:
cardinality = .toMany
}
return _SQLAssociation(
key: key,
condition: condition,
relation: relation,
cardinality: cardinality)
}
}
var source: SQLSource
var selectionPromise: DatabasePromise<[SQLSelection]>
var filterPromise: DatabasePromise<SQLExpression>?
var ordering = SQLRelation.Ordering()
var ctes: OrderedDictionary<String, SQLCTE> = [:] // See also `allCTEs`
var children: OrderedDictionary<String, Child> = [:]
// Properties below MUST NOT be used when joining to-one associations.
// This is guaranteed by Child.init().
var isDistinct = false
var groupPromise: DatabasePromise<[SQLExpression]>?
var havingExpressionPromise: DatabasePromise<SQLExpression>?
var limit: SQLLimit?
}
extension SQLRelation {
/// Convenience factory methods which selects all rows from a table.
static func all(
fromTable tableName: String,
selection: @escaping (Database) -> [SQLSelection] = { _ in [.allColumns] })
-> Self
{
SQLRelation(
source: SQLSource(tableName: tableName, alias: nil),
selectionPromise: DatabasePromise(selection))
}
}
extension SQLRelation: Refinable {
func selectWhenConnected(_ selection: @escaping (Database) throws -> [SQLSelection]) -> Self {
with {
$0.selectionPromise = DatabasePromise(selection)
}
}
// Convenience
func select(_ selection: [SQLSelection]) -> Self {
selectWhenConnected { _ in selection }
}
// Convenience
func select(_ expressions: SQLExpression...) -> Self {
select(expressions.map { .expression($0) })
}
/// Sets the selection, removes all selections from children, and clears the
/// `isDistinct` flag.
func selectOnly(_ selection: [SQLSelection]) -> Self {
self
.select(selection)
.with {
$0.isDistinct = false
$0.children = children.mapValues { child in
child.with {
$0.relation = $0.relation.selectOnly([])
}
}
}
}
func annotatedWhenConnected(with selection: @escaping (Database) throws -> [SQLSelection]) -> Self {
with {
let old = $0.selectionPromise
$0.selectionPromise = DatabasePromise { db in
try old.resolve(db) + selection(db)
}
}
}
// Convenience
func annotated(with selection: [SQLSelection]) -> Self {
annotatedWhenConnected(with: { _ in selection })
}
func filterWhenConnected(_ predicate: @escaping (Database) throws -> SQLExpression) -> Self {
with {
if let old = $0.filterPromise {
$0.filterPromise = DatabasePromise { db in
try old.resolve(db) && predicate(db)
}
} else {
$0.filterPromise = DatabasePromise(predicate)
}
}
}
// Convenience
func filter(_ predicate: SQLExpression) -> Self {
filterWhenConnected { _ in predicate }
}
func orderWhenConnected(_ orderings: @escaping (Database) throws -> [SQLOrdering]) -> Self {
with {
$0.ordering = SQLRelation.Ordering(orderings: orderings)
}
}
func reversed() -> Self {
with {
$0.ordering = $0.ordering.reversed
}
}
func unordered() -> Self {
with {
$0.ordering = SQLRelation.Ordering()
$0.children = children.mapValues { child in
child.with {
$0.relation = $0.relation.unordered()
}
}
}
}
func withStableOrder() -> Self {
with { relation in
relation.ordering = relation.ordering.appending(Ordering(orderings: { [relation] db in
if try db.tableExists(source.tableName) {
// Order by primary key. Don't order by rowid because those are
// not stable: rowids can change after a vacuum.
return try db.primaryKey(source.tableName).columns.map { SQLExpression.column($0).sqlOrdering }
} else {
// Support for views: create a stable order from all columns:
// ORDER BY 1, 2, 3, ...
let columnCount = try SQLQueryGenerator(relation: relation).columnCount(db)
return (1...columnCount).map { SQL(sql: $0.description).sqlOrdering }
}
}))
relation.children = children.mapValues { child in
child.with {
$0.relation = $0.relation.withStableOrder()
}
}
}
}
// Remove ordering iff relation has no LIMIT clause
func unorderedUnlessLimited() -> Self {
if limit != nil {
return self
} else {
return unordered()
}
}
func groupWhenConnected(_ expressions: @escaping (Database) throws -> [SQLExpression]) -> Self {
with {
$0.groupPromise = DatabasePromise(expressions)
}
}
func havingWhenConnected(_ predicate: @escaping (Database) throws -> SQLExpression) -> Self {
with {
if let old = $0.havingExpressionPromise {
$0.havingExpressionPromise = DatabasePromise { db in
try old.resolve(db) && predicate(db)
}
} else {
$0.havingExpressionPromise = DatabasePromise(predicate)
}
}
}
func aliased(_ alias: TableAlias) -> Self {
with {
$0.source = $0.source.aliased(alias)
}
}
}
extension SQLRelation {
/// All prefetched associations (`including(all:)`), recursively
var prefetchedAssociations: [_SQLAssociation] {
children.flatMap { key, child -> [_SQLAssociation] in
switch child.kind {
case .all:
return [child.makeAssociationForKey(key)]
case .oneOptional, .oneRequired, .bridge:
return child.relation.prefetchedAssociations.map { association in
// Remove redundant pivot child
let pivotKey = association.pivot.keyName
let child = child.with {
assert($0.relation.children[pivotKey] != nil)
$0.relation = $0.relation.removingChild(forKey: pivotKey)
}
return association.through(child.makeAssociationForKey(key))
}
}
}
}
/// All common table expressions, including those of joined children.
var allCTEs: OrderedDictionary<String, SQLCTE> {
children.values.reduce(into: ctes) { (ctes, child) in
switch child.kind {
case .all, .bridge:
break
case .oneOptional, .oneRequired:
ctes.merge(child.relation.allCTEs, uniquingKeysWith: { (_, new) in new })
}
}
}
/// Returns a relation extended with an association.
///
/// This method provides support for public joining methods such
/// as `including(required:)`:
///
/// struct Destination: TableRecord { }
/// struct Origin: TableRecord {
/// static let destination = belongsTo(Destination.self)
/// }
///
/// // SELECT origin.*, destination.*
/// // FROM origin
/// // JOIN destination ON destination.id = origin.destinationId
/// let request = Origin.including(required: Origin.destination)
///
/// At low-level, this gives:
///
/// let sqlAssociation = Origin.destination._sqlAssociation
/// let origin = Origin.all().query.relation
/// let relation = origin.appending(sqlAssociation, kind: .oneRequired)
/// let query = SQLQuery(relation: relation)
/// let generator = SQLQueryGenerator(query)
/// let statement, _ = try generator.prepare(db)
/// print(statement.sql)
/// // SELECT origin.*, destination.*
/// // FROM origin
/// // JOIN destination ON destination.originId = origin.id
///
/// This method works for simple direct associations such as BelongsTo or
/// HasMany in the above examples, but also for indirect associations such
/// as HasManyThrough, which have any number of pivot relations between the
/// origin and the destination.
func appendingChild(for association: _SQLAssociation, kind: SQLRelation.Child.Kind) -> Self {
// Our goal here is to append a child with a correct (singular or
// plural) key, so that the user can decode it later under the expected
// name.
//
// To know if the child key should be singular or plural, we look at the
// association, which may be to-one or to-many, and at the kind of the
// child, which may be joined (singular), or prefetched (plural).
//
// We prefer the cardinality of the child kind, but for the specific
// case of the `.bridge` child kind, involved in prefetched
// has-many-through associations, where we user the cardinality of
// the association instead.
//
// By preferring the cardinality of the child kind in general, we make it
// possible to join to a plural association and decode it in a singular
// key. In the example below, we have a singular kind `.oneRequired`,
// a plural to-many association, and we use a singular key:
//
// // Decode a Player in the singular "player" key
// //
// // SELECT team.*, player.*
// // FROM team
// // JOIN player ON player.teamID = team.id
// Team.joining(required: Team.players)
//
// The exception for has-many-through associations exists because the
// pivot of the association may be singular, and may conflict with a
// plural association with the same association key, as in the example
// below:
//
// We want the child for the "captain" in the following request to be
// registered under the singular "player" key, and not the plural
// "players" key, so that it does not conflict with the other child
// named "players" (we can not have two distinct children with the
// same key):
//
// struct Team: TableRecord {
// static let players = hasMany(Player.self)
// static let captain = hasOne(Player.self).filter(Column("isCaptain") == true)
// static let captainAwards = hasMany(Award.self, through: captain, using: Player.awards)
// }
// struct Player: TableRecord {
// static let awards = hasMany(Award.self)
// }
// struct Award: TableRecord { }
// let request = Team
// .including(all: Team.captainAwards) // child "player" (with an "awards" child inside)
// .including(all: Team.players) // child "players"
let isSingular: Bool
switch kind {
case .oneOptional, .oneRequired:
isSingular = true
case .all:
isSingular = false
case .bridge:
isSingular = association.destination.cardinality.isSingular
}
let childKey = association.destination.key.name(singular: isSingular)
let child = SQLRelation.Child(
kind: kind,
condition: association.destination.condition,
relation: association.destination.relation)
let initialSteps = association.steps.dropLast()
if initialSteps.isEmpty {
// This is a direct join from origin to destination, without
// intermediate step.
//
// SELECT origin.*, destination.*
// FROM origin
// JOIN destination ON destination.id = origin.destinationId
//
// let association = Origin.belongsTo(Destination.self)
// Origin.including(required: association)
return appendingChild(child, forKey: childKey)
}
// This is an indirect join from origin to destination, through
// some pivot(s):
//
// 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)
//
// Let's recurse toward a direct join, by making a new association which
// ends on the last pivot, to which we join our destination:
var reducedAssociation = _SQLAssociation(steps: Array(initialSteps))
reducedAssociation.destination.relation = reducedAssociation.destination.relation
.select([]) // Intermediate steps are not prefetched
.appendingChild(child, forKey: childKey)
switch kind {
case .oneRequired, .oneOptional, .bridge:
return appendingChild(for: reducedAssociation, kind: kind)
case .all:
// Intermediate steps of an indirect association are not prefetched:
// use the `.bridge` kind.
//
// For example, the request below prefetches citizens, not
// intermediate passports:
//
// extension Country {
// static let passports = hasMany(Passport.self)
// static let citizens = hasMany(Citizens.self, through: passports, using: Passport.citizen)
// }
// let request = Country.including(all: Country.citizens)
return appendingChild(for: reducedAssociation, kind: .bridge)
}
}
private func appendingChild(_ child: SQLRelation.Child, forKey key: String) -> Self {
var relation = self
if let existingChild = relation.children.removeValue(forKey: key) {
guard let mergedChild = existingChild.merged(with: child) else {
// can't merge
fatalError("""
The association key \"\(key)\" is ambiguous. \
Use the Association.forKey(_:) method is order to disambiguate.
""")
}
relation.children.appendValue(mergedChild, forKey: key)
} else {
relation.children.appendValue(child, forKey: key)
}
return relation
}
func removingChild(forKey key: String) -> Self {
with {
$0.children.removeValue(forKey: key)
}
}
func filteringChildren(_ included: (Child) throws -> Bool) rethrows -> Self {
try with {
$0.children = try $0.children.filter { (_, child) in try included(child) }
}
}
/// Return a relation without any `.all` and `.bridge` children, recursively.
func removingPrefetchedAssociations() -> Self {
with {
$0.children = $0.children.compactMapValues { child in
switch child.kind {
case .all, .bridge:
return nil
case .oneRequired, .oneOptional:
return child.with {
$0.relation = $0.relation.removingPrefetchedAssociations()
}
}
}
}
}
}
extension SQLRelation {
func _including(all association: _SQLAssociation) -> Self {
appendingChild(for: association, kind: .all)
}
func _including(optional association: _SQLAssociation) -> Self {
appendingChild(for: association, kind: .oneOptional)
}
func _including(required association: _SQLAssociation) -> Self {
appendingChild(for: association, kind: .oneRequired)
}
func _joining(optional association: _SQLAssociation) -> Self {
// Remove association selection
let associationWithEmptySelection = association.with {
$0.destination.relation = $0.destination.relation.select([])
}
return appendingChild(for: associationWithEmptySelection, kind: .oneOptional)
}
func _joining(required association: _SQLAssociation) -> Self {
// Remove association selection
let associationWithEmptySelection = association.with {
$0.destination.relation = $0.destination.relation.select([])
}
return appendingChild(for: associationWithEmptySelection, kind: .oneRequired)
}
}
extension SQLRelation {
func fetchCount(_ db: Database) throws -> Int {
guard groupPromise == nil && limit == nil && ctes.isEmpty else {
// SELECT ... GROUP BY ...
// SELECT ... LIMIT ...
// WITH ... SELECT ...
return try fetchTrivialCount(db)
}
if children.contains(where: { $0.value.impactsParentCount }) { // TODO: not tested
// SELECT ... FROM ... JOIN ...
return try fetchTrivialCount(db)
}
let selection = try selectionPromise.resolve(db)
GRDBPrecondition(!selection.isEmpty, "Can't generate SQL with empty selection")
if selection.count == 1 {
guard let count = selection[0].count(distinct: isDistinct) else {
return try fetchTrivialCount(db)
}
var countRelation = self.unordered()
countRelation.isDistinct = false
switch count {
case .all:
countRelation = countRelation.select(.countAll)
case .distinct(let expression):
countRelation = countRelation.select(.countDistinct(expression))
}
return try QueryInterfaceRequest(relation: countRelation).fetchOne(db)!
} else {
// SELECT [DISTINCT] expr1, expr2, ... FROM tableName ...
guard !isDistinct else {
return try fetchTrivialCount(db)
}
// <https://github.com/groue/GRDB.swift/issues/1357>
guard selection.allSatisfy(\.isTriviallyCountable) else {
return try fetchTrivialCount(db)
}
// SELECT expr1, expr2, ... FROM tableName ...
// ->
// SELECT COUNT(*) FROM tableName ...
let countRelation = unordered().select(.countAll)
return try QueryInterfaceRequest(relation: countRelation).fetchOne(db)!
}
}
// SELECT COUNT(*) FROM (self)
private func fetchTrivialCount(_ db: Database) throws -> Int {
let countRequest: SQLRequest<Int> = "SELECT COUNT(*) FROM (\(SQLSubquery.relation(unordered())))"
return try countRequest.fetchOne(db)!
}
}
// MARK: - SQLLimit
struct SQLLimit {
let limit: Int
let offset: Int?
var sql: String {
if let offset {
return "\(limit) OFFSET \(offset)"
} else {
return "\(limit)"
}
}
}
// MARK: - SQLSource
struct SQLSource {
var tableName: String
var alias: TableAlias?
func aliased(_ alias: TableAlias) -> SQLSource {
if let sourceAlias = self.alias {
alias.becomeProxy(of: sourceAlias)
return self
} else {
alias.setTableName(tableName)
return SQLSource(tableName: tableName, alias: alias)
}
}
}
// MARK: - SQLRelation.Ordering
extension SQLRelation {
/// SQLRelation.Ordering provides the order clause to SQLRelation.
struct Ordering {
private enum Element {
case terms(DatabasePromise<[SQLOrdering]>)
case ordering(SQLRelation.Ordering)
var reversed: Element {
switch self {
case .terms(let terms):
return .terms(terms.map { $0.map(\.reversed) })
case .ordering(let ordering):
return .ordering(ordering.reversed)
}
}
func qualified(with alias: TableAlias) -> Element {
switch self {
case .terms(let terms):
return .terms(terms.map { $0.map { $0.qualified(with: alias) } })
case .ordering(let ordering):
return .ordering(ordering.qualified(with: alias))
}
}
func resolve(_ db: Database) throws -> [SQLOrdering] {
switch self {
case .terms(let terms):
return try terms.resolve(db)
case .ordering(let ordering):
return try ordering.resolve(db)
}
}
}
private var elements: [Element] = []
var isReversed: Bool
var isEmpty: Bool { elements.isEmpty }
private init(elements: [Element], isReversed: Bool) {
self.elements = elements
self.isReversed = isReversed
}
init() {
self.init(
elements: [],
isReversed: false)
}
init(orderings: @escaping (Database) throws -> [SQLOrdering]) {
self.init(
elements: [.terms(DatabasePromise(orderings))],
isReversed: false)
}
var reversed: Ordering {
Ordering(
elements: elements,
isReversed: !isReversed)
}
func qualified(with alias: TableAlias) -> Ordering {
Ordering(
elements: elements.map { $0.qualified(with: alias) },
isReversed: isReversed)
}
func appending(_ ordering: Ordering) -> Ordering {
Ordering(
elements: elements + [.ordering(ordering)],
isReversed: isReversed)
}
func resolve(_ db: Database) throws -> [SQLOrdering] {
if isReversed {
return try elements.flatMap { try $0.reversed.resolve(db) }
} else {
return try elements.flatMap { try $0.resolve(db) }
}
}
}
}
// MARK: - SQLAssociationCondition
/// The condition that links two tables of an association.
///
/// Conditions can feed a `JOIN` clause:
///
/// // SELECT ... FROM book JOIN author ON author.id = book.authorId
/// // ~~~~~~~~~~~~~~~~~~~~~~~~~
/// Book.including(required: Book.author)
///
/// Conditions help eager loading of to-many associations:
///
/// // SELECT * FROM author WHERE ...
/// // SELECT * FROM book WHERE author.id IN (1, 2, 3)
/// // ~~~~~~~~~~~~~~~~~~~~~~
/// Author.filter(...).including(all: Author.books)
///
/// Conditions help fetching associated records:
///
/// // SELECT * FROM book WHERE author.id = 1
/// // ~~~~~~~~~~~~~
/// author.request(for: Author.books)
enum SQLAssociationCondition {
/// A condition based on a foreign key.
case foreignKey(SQLForeignKeyCondition)
/// A condition based on a function that returns an expression.
///
/// The two arguments `left` and `right` are aliases for the left and right
/// tables in a `JOIN` clause:
///
/// // WITH bonus AS (...)
/// // SELECT * FROM player
/// // JOIN bonus ON player.id = bonus.playerID
/// // ~~~~~~~~~~~~~~~~~~~~~~~~~~
/// let bonus = CommonTableExpression(...)
/// let association = Player.association(to: bonus, on: { player, bonus in
/// player[Column("id")] == bonus[Column("playerID")]
/// })
/// Player.with(bonus).joining(required: association)
case expression((_ left: TableAlias, _ right: TableAlias) -> SQLExpression?)
/// The condition that does not constrain the two associated tables
/// in any way.
static let none = expression({ _, _ in nil })
func reversed(to destinationTable: String) -> SQLAssociationCondition {
switch self {
case let .foreignKey(foreignKey):
return .foreignKey(foreignKey.reversed(to: destinationTable))
case let .expression(condition):
return .expression { condition($1, $0) }
}
}
func joinExpression(
_ db: Database,
leftAlias: TableAlias,
rightAlias: TableAlias)
throws -> SQLExpression?
{
switch self {
case let .expression(condition):
return condition(leftAlias, rightAlias)
case let .foreignKey(foreignKey):
return try foreignKey
.joinMapping(db, from: leftAlias.tableName)
.joinExpression(leftAlias: leftAlias, rightAlias: rightAlias)
}
}
}
/// An association condition based on a foreign key.
struct SQLForeignKeyCondition: Equatable {
/// The destination table of an association.
///
/// In `Author.hasMany(Book.self)`, the destination is `book`.
var destinationTable: String
/// A user-provided foreign key. When nil, we introspect the database in
/// order to look for a foreign key in the schema.
var foreignKey: ForeignKey?
/// `originIsLeft` is true if the table at the origin of the foreign key is
/// on the left of the sql JOIN operator.
///
/// Let's consider the `book.authorId -> author.id` foreign key.
/// Its origin table is `book`.
///
/// The origin table `book` is on the left of the JOIN operator for
/// the `BelongsTo` association:
///
/// -- Book.including(required: Book.author)
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
/// ~~~~ ~~~~
///
/// The origin table `book`is on the right of the JOIN operator for
/// the `HasMany` and `HasOne` associations:
///
/// -- Author.including(required: Author.books)
/// SELECT ... FROM author JOIN book ON author.id = book.authorId
/// ~~~~ ~~~~
///
/// See also `ForeignKeyMapping.joinMapping(originIsLeft:)`
var originIsLeft: Bool
func reversed(to destinationTable: String) -> SQLForeignKeyCondition {
SQLForeignKeyCondition(
destinationTable: destinationTable,
foreignKey: foreignKey,
originIsLeft: !originIsLeft)
}
/// Turns the foreign key condition into a `JoinMapping` that can feed an
/// SQL JOIN clause.
func joinMapping(_ db: Database, from originTable: String) throws -> JoinMapping {
try foreignKeyRequest(from: originTable)
.fetchForeignKeyMapping(db)
.joinMapping(originIsLeft: originIsLeft)
}
private func foreignKeyRequest(from originTable: String) -> SQLForeignKeyRequest {
// Convert association destination/origin to
// foreign key destination/origin.
if originIsLeft {
return SQLForeignKeyRequest(
originTable: originTable,
destinationTable: destinationTable,
foreignKey: foreignKey)
} else {
return SQLForeignKeyRequest(
originTable: destinationTable,
destinationTable: originTable,
foreignKey: foreignKey)
}
}
}
extension JoinMapping {
/// Resolves the mapping into an SQL expression which involves only the
/// right table, and feeds left columns from `leftRows`.
///
/// For example, given `[(left: "id", right: "authorID")]`,
/// returns `right.authorID = 1` or `right.authorID IN (1, 2, 3)`.
///
/// - precondition: leftRows is not empty.
/// - precondition: leftRows contains all mapping left columns.
/// - precondition: All rows have the same layout: a column index returned
/// by `index(forColumn:)` refers to the same column in all rows.
func joinExpression<Rows>(leftRows: Rows)
-> SQLExpression
where Rows: Collection, Rows.Element: ColumnAddressable
{
guard let firstLeftRow = leftRows.first else {
// We could return `false.sqlExpression`.
//
// But we need to take care of database observation, and generate
// SQL that involves all used columns. Consider using a `DummyRow`.
fatalError("Provide at least one left row, or this method can't generate SQL that can be observed.")
}
let mappings = map { mapping in
guard let leftIndex = firstLeftRow.index(forColumn: mapping.left) else {
fatalError("Missing column: \(mapping.left)")
}
return (leftIndex: leftIndex, rightColumn: Column(mapping.right))
}
guard let mapping = mappings.first else {
// Degenerate case: no joining column
return true.sqlExpression
}
// SQLite doesn't match foreign keys on NULL: https://www.sqlite.org/foreignkeys.html
// > The foreign key constraint is satisfied if for each row in the
// > child table either one or more of the child key columns are NULL,
// > or there exists a row in the parent table for which each parent key
// > column contains a value equal to the value in its associated child
// > key column.
//
// Since a single NULL value satisfies the foreign key constraint
// without requiring a matching matching parent row, below we'll ignore
// left rows (children) that attempt at matching on NULL.
if mappings.count == 1 {
// Join on a single right column.
// table.a IN (1, 2, 3, ...)
// Unique database values and filter out NULL because SQLite doesn't
// match foreign keys on NULL
let leftIndex = mapping.leftIndex
var dbValues = Set(leftRows.map { $0.databaseValue(at: leftIndex) })
dbValues.remove(.null)
// Sort database values for nicer output.
return dbValues.sorted(by: <).contains(mapping.rightColumn)
} else {
// Join on a multiple columns.
// ((table.a = 1) AND (table.b = 2)) OR ((table.a = 3) AND (table.b = 4)) ...
return leftRows
.compactMap { leftRow -> SQLExpression? in
// (table.a = 1) AND (table.b = 2)
var conditions: [SQLExpression] = []
for mapping in mappings {
let dbValue = leftRow.databaseValue(at: mapping.leftIndex)
if dbValue.isNull {
// SQLite doesn't match foreign keys on NULL:
// give up this left row.
return nil
}
conditions.append(mapping.rightColumn == dbValue)
}
return conditions.joined(operator: .and)
}
.joined(operator: .or)
}
}
/// Resolves the condition into an SQL expression which involve both left
/// and right tables.
///
/// SELECT * FROM left JOIN right ON (right.a = left.b)
/// <---------------->
///
/// - parameter leftAlias: A TableAlias for the table on the left of the
/// JOIN operator.
/// - parameter rightAlias: A TableAlias for the table on the right of the
/// JOIN operator.
func joinExpression(leftAlias: TableAlias, rightAlias: TableAlias) -> SQLExpression {
map { rightAlias[$0.right] == leftAlias[$0.left] }.joined(operator: .and)
}
}
/// A protocol for row-like containers
protocol ColumnAddressable {
associatedtype ColumnIndex
func index(forColumn column: String) -> ColumnIndex?
func databaseValue(at index: ColumnIndex) -> DatabaseValue
}
/// A "row" that contains a dummy value for all columns
struct DummyRow: ColumnAddressable {
struct DummyIndex { }
func index(forColumn column: String) -> DummyIndex? { DummyIndex() }
func databaseValue(at index: DummyIndex) -> DatabaseValue { DatabaseValue(storage: .int64(1)) }
}
/// Row has columns
extension Row: ColumnAddressable {
func databaseValue(at index: Int) -> DatabaseValue { self[index] }
}
/// PersistenceContainer has columns
extension PersistenceContainer: ColumnAddressable {
func index(forColumn column: String) -> String? { column }
func databaseValue(at column: String) -> DatabaseValue {
self[caseInsensitive: column]?.databaseValue ?? .null
}
}
// MARK: - Merging
//
// "Merging" is an operation that takes two relations and, if they are
// compatible, gathers them into a merged relation.
//
// It is an important feature that allows the user to define associated requests
// in several steps. For example, in the sample code below, both requests are
// equivalent and generate the same SQL query, thanks to merging:
//
// let request1 = Book.including(required: Book.author)
//
// let request2 = Book
// .including(required: Book.author)
// .including(required: Book.author)
extension SQLRelation {
/// Returns nil if relations can't be merged (conflict in source, joins...)
func merged(with other: SQLRelation) -> Self? {
// Source
guard let mergedSource = source.merged(with: other.source) else {
// can't merge
return nil
}
// Filter: merge with AND
let mergedFilterPromise: DatabasePromise<SQLExpression>?
switch (filterPromise, other.filterPromise) {
case let (lhs?, rhs?):
mergedFilterPromise = DatabasePromise {
try lhs.resolve($0) && rhs.resolve($0)
}
case let (nil, promise?), let (promise?, nil):
mergedFilterPromise = promise
case (nil, nil):
mergedFilterPromise = nil
}
// Children: merge recursively
var mergedChildren: OrderedDictionary<String, SQLRelation.Child> = [:]
for (key, child) in children {
if let otherChild = other.children[key] {
guard let mergedChild = child.merged(with: otherChild) else {
// can't merge
return nil
}
mergedChildren.appendValue(mergedChild, forKey: key)
} else {
mergedChildren.appendValue(child, forKey: key)
}
}
for (key, child) in other.children where mergedChildren[key] == nil {
mergedChildren.appendValue(child, forKey: key)
}
// Selection: replace unless empty
let mergedSelectionPromise = DatabasePromise { db -> [SQLSelection] in
let otherSelection = try other.selectionPromise.resolve(db)
if otherSelection.isEmpty {
return try self.selectionPromise.resolve(db)
} else {
return otherSelection
}
}
// Ordering: prefer other
let mergedOrdering = other.ordering.isEmpty ? ordering : other.ordering
// Distinct
let mergedDistinct = isDistinct || other.isDistinct
// Grouping: prefer other
let mergedGroupPromise = other.groupPromise ?? groupPromise
// Having: merge with AND
let mergedHavingExpressionPromise: DatabasePromise<SQLExpression>?
switch (havingExpressionPromise, other.havingExpressionPromise) {
case let (lhs?, rhs?):
mergedHavingExpressionPromise = DatabasePromise {
try lhs.resolve($0) && rhs.resolve($0)
}
case let (nil, promise?), let (promise?, nil):
mergedHavingExpressionPromise = promise
case (nil, nil):
mergedHavingExpressionPromise = nil
}
// Limit: prefer other
let mergedLimit = other.limit ?? limit
// CTEs: merge & prefer other
let mergedCTEs = ctes.merging(other.ctes, uniquingKeysWith: { (_, other) in other })
return SQLRelation(
source: mergedSource,
selectionPromise: mergedSelectionPromise,
filterPromise: mergedFilterPromise,
ordering: mergedOrdering,
ctes: mergedCTEs,
children: mergedChildren,
isDistinct: mergedDistinct,
groupPromise: mergedGroupPromise,
havingExpressionPromise: mergedHavingExpressionPromise,
limit: mergedLimit)
}
}
extension SQLSource {
/// Returns nil if sources can't be merged (conflict in tables, aliases...)
func merged(with other: SQLSource) -> SQLSource? {
guard tableName == other.tableName else {
// can't merge
return nil
}
switch (alias, other.alias) {
case (nil, nil):
return SQLSource(tableName: tableName, alias: nil)
case let (alias?, nil), let (nil, alias?):
return SQLSource(tableName: tableName, alias: alias)
case let (alias?, otherAlias?):
guard let mergedAlias = alias.merged(with: otherAlias) else {
// can't merge
return nil
}
return SQLSource(tableName: tableName, alias: mergedAlias)
}
}
}
extension SQLAssociationCondition {
func merged(with other: SQLAssociationCondition) -> Self? {
switch (self, other) {
case let (.foreignKey(lhs), .foreignKey(rhs)):
if lhs == rhs {
return self
} else {
// can't merge
return nil
}
case let (.expression, .expression(rhs)):
// Can't compare functions: the last one wins
return .expression(rhs)
default:
// can't merge
return nil
}
}
}
extension SQLRelation.Child {
/// Returns nil if joins can't be merged (conflict in condition, relation...)
func merged(with other: SQLRelation.Child) -> Self? {
guard let mergedCondition = condition.merged(with: other.condition) else {
// can't merge
return nil
}
guard let mergedRelation = relation.merged(with: other.relation) else {
// can't merge
return nil
}
guard let mergedKind = kind.merged(with: other.kind) else {
// can't merge
return nil
}
return SQLRelation.Child(
kind: mergedKind,
condition: mergedCondition,
relation: mergedRelation)
}
}
extension SQLRelation.Child.Kind {
/// Returns nil if kinds can't be merged
func merged(with other: SQLRelation.Child.Kind) -> Self? {
switch (self, other) {
case (.oneRequired, .oneRequired),
(.oneRequired, .oneOptional),
(.oneOptional, .oneRequired):
// Equivalent to Record.including(required: association):
//
// Record
// .including(required: association)
// .including(optional: association)
return .oneRequired
case (.oneOptional, .oneOptional):
// Equivalent to Record.including(optional: association):
//
// Record
// .including(optional: association)
// .including(optional: association)
return .oneOptional
case (.all, .all):
// Equivalent to Record.including(all: association):
//
// Record
// .including(all: association)
// .including(all: association)
return .all
case (.all, .bridge),
(.bridge, .all):
// Record
// .including(all: associationToDestinationThroughPivot)
// .including(all: associationToPivot)
fatalError("Not implemented: merging a direct association and an indirect one with including(all:)")
case (.bridge, .bridge):
// Equivalent to Record.including(all: association)
//
// Record
// .including(all: association)
// .including(all: association)
return .bridge
default:
// Likely a programmer error:
//
// Record
// .including(all: Author.books.forKey("foo"))
// .including(optional: Author.books.forKey("foo"))
return nil
}
}
}