Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/QueryInterface/SQL/SQLForeignKeyRequest.swift
T

178 lines
7.1 KiB
Swift

/// SQLForeignKeyRequest looks for the foreign keys associations need to
/// join tables.
///
/// Columns mapping come from foreign keys, when they exist in the
/// database schema.
///
/// When the schema does not define any foreign key, we can still infer complete
/// mapping from partial information and primary keys.
struct SQLForeignKeyRequest {
let originTable: String
let destinationTable: String
let originColumns: [String]?
let destinationColumns: [String]?
init(originTable: String, destinationTable: String, foreignKey: ForeignKey?) {
self.originTable = originTable
self.destinationTable = destinationTable
self.originColumns = foreignKey?.originColumns
self.destinationColumns = foreignKey?.destinationColumns
}
/// The (origin, destination) column pairs that join a left table to a right table.
func fetchForeignKeyMapping(_ db: Database) throws -> ForeignKeyMapping {
if let originColumns, let destinationColumns {
// Total information: no need to query the database schema.
GRDBPrecondition(originColumns.count == destinationColumns.count, "Number of columns don't match")
let mapping = zip(originColumns, destinationColumns).map {
(origin: $0, destination: $1)
}
return mapping
}
// Incomplete information: let's look for schema foreign keys
//
// But maybe the tables are views. In this case, don't throw
// "no such table" error, because this is confusing for the user,
// as discovered in <https://github.com/groue/GRDB.swift/discussions/1481>.
// Instead, we'll crash with a clear message.
guard let originType = try tableType(db, for: originTable) else {
throw DatabaseError.noSuchTable(originTable)
}
if originType.isView {
if originColumns == nil {
fatalError("""
Could not infer foreign key from '\(originTable)' \
to '\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
} else {
let foreignKeys = try db.foreignKeys(on: originTable).filter { foreignKey in
if destinationTable.lowercased() != foreignKey.destinationTable.lowercased() {
return false
}
if let originColumns {
let originColumns = Set(originColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.origin.lowercased() })
if originColumns != foreignKeyColumns {
return false
}
}
if let destinationColumns {
// TODO: test
let destinationColumns = Set(destinationColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.destination.lowercased() })
if destinationColumns != foreignKeyColumns {
return false
}
}
return true
}
// Matching foreign key(s) found
if let foreignKey = foreignKeys.first {
if foreignKeys.count == 1 {
// Non-ambiguous
return foreignKey.mapping
} else {
// Ambiguous: can't choose
fatalError("""
Ambiguous foreign key from '\(originTable)' to \
'\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
}
}
// No matching foreign key found: use the destination primary key
if let originColumns {
guard let destinationType = try tableType(db, for: destinationTable) else {
throw DatabaseError.noSuchTable(destinationTable)
}
if destinationType.isView {
fatalError("""
Could not infer foreign key from '\(originTable)' \
to '\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition, \
with both origin and destination columns.
""")
}
let destinationColumns = try db.primaryKey(destinationTable).columns
if originColumns.count == destinationColumns.count {
let mapping = zip(originColumns, destinationColumns).map {
(origin: $0, destination: $1)
}
return mapping
}
}
fatalError("""
Could not infer foreign key from '\(originTable)' to \
'\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
private struct TableType {
var isView: Bool
}
private func tableType(_ db: Database, for name: String) throws -> TableType? {
for schemaID in try db.schemaIdentifiers() {
if try db.schema(schemaID).containsObjectNamed(name, ofType: .table) {
return TableType(isView: false)
}
if try db.schema(schemaID).containsObjectNamed(name, ofType: .view) {
return TableType(isView: true)
}
}
return nil
}
}
// Foreign key columns mapping
typealias ForeignKeyMapping = [(origin: String, destination: String)]
// Join columns mapping
typealias JoinMapping = [(left: String, right: String)]
extension ForeignKeyMapping {
/// Orient the foreign key mapping for a SQL join.
///
/// - parameter originIsLeft: Whether the table at the origin of a
/// foreign key is on the left of a JOIN clause.
///
/// For example, the two requests below use the same
/// `ForeignKeyMapping` from `book.authorID` (origin of the foreign key)
/// to `author.id` (destination).
///
/// In the first request, the book origin is on the left of the
/// join clause:
///
/// // SELECT book.*, author.*
/// // FROM book
/// // JOIN author ON author.id = book.authorID
/// Book.including(required: Book.author)
///
/// In the second request, the book origin is on the right of the
/// join clause:
///
/// // SELECT author.*, book.*
/// // FROM author
/// // JOIN book ON book.authorID = author.id
/// Author.including(required: Author.books)
func joinMapping(originIsLeft: Bool) -> JoinMapping {
if originIsLeft {
return map { (left: $0.origin, right: $0.destination) }
} else {
return map { (left: $0.destination, right: $0.origin) }
}
}
}