GRDB.swift/GRDB/QueryInterface/SQLRelation.swift
2019-03-15 08:41:57 +01:00

456 lines
16 KiB
Swift

/// A "relation" as defined by the [relational terminology](https://en.wikipedia.org/wiki/Relational_database#Terminology):
///
/// > A set of tuples sharing the same attributes; a set of columns and rows.
struct SQLRelation {
var source: SQLSource
var selection: [SQLSelectable]
var filterPromise: DatabasePromise<SQLExpression?>
var ordering: SQLRelation.Ordering
var joins: OrderedDictionary<String, SQLJoin>
init(
source: SQLSource,
selection: [SQLSelectable] = [],
filterPromise: DatabasePromise<SQLExpression?> = DatabasePromise(value: nil),
ordering: SQLRelation.Ordering = SQLRelation.Ordering(),
joins: OrderedDictionary<String, SQLJoin> = [:])
{
self.source = source
self.selection = selection
self.filterPromise = filterPromise
self.ordering = ordering
self.joins = joins
}
}
extension SQLRelation {
func select(_ selection: [SQLSelectable]) -> SQLRelation {
var relation = self
relation.selection = selection
return relation
}
func annotated(with selection: [SQLSelectable]) -> SQLRelation {
var relation = self
relation.selection.append(contentsOf: selection)
return relation
}
func filter(_ predicate: @escaping (Database) throws -> SQLExpressible) -> SQLRelation {
var relation = self
relation.filterPromise = relation.filterPromise.flatMap { filter in
if let filter = filter {
return DatabasePromise { try filter && predicate($0) }
} else {
return DatabasePromise { try predicate($0).sqlExpression }
}
}
return relation
}
func order(_ orderings: @escaping (Database) throws -> [SQLOrderingTerm]) -> SQLRelation {
return order(SQLRelation.Ordering(orderings: orderings))
}
func reversed() -> SQLRelation {
return order(ordering.reversed)
}
private func order(_ ordering: SQLRelation.Ordering) -> SQLRelation {
var relation = self
relation.ordering = ordering
return relation
}
func unordered() -> SQLRelation {
return order(SQLRelation.Ordering())
}
func appendingJoin(_ join: SQLJoin, forKey key: String) -> SQLRelation {
var relation = self
if let existingJoin = relation.joins.removeValue(forKey: key) {
guard let mergedJoin = existingJoin.merged(with: join) else {
// can't merge
fatalError("The association key \"\(key)\" is ambiguous. Use the Association.forKey(_:) method is order to disambiguate.")
}
relation.joins.appendValue(mergedJoin, forKey: key)
} else {
relation.joins.appendValue(join, forKey: key)
}
return relation
}
func qualified(with alias: TableAlias) -> SQLRelation {
var relation = self
relation.source = source.qualified(with: alias)
return relation
}
}
// MARK: - SQLSource
enum SQLSource {
case table(tableName: String, alias: TableAlias?)
indirect case query(SQLSelectQuery)
func qualified(with alias: TableAlias) -> SQLSource {
switch self {
case .table(let tableName, let sourceAlias):
if let sourceAlias = sourceAlias {
alias.becomeProxy(of: sourceAlias)
return self
} else {
alias.setTableName(tableName)
return .table(tableName: tableName, alias: alias)
}
case .query(let query):
return .query(query.qualified(with: alias))
}
}
}
// MARK: - SQLRelation.Ordering
extension SQLRelation {
/// SQLRelation.Ordering provides the order clause to SQLRelation.
struct Ordering {
private enum Element {
case terms(DatabasePromise<[SQLOrderingTerm]>)
case ordering(SQLRelation.Ordering)
var reversed: Element {
switch self {
case .terms(let terms):
return .terms(terms.map { $0.map { $0.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.qualifiedOrdering(with: alias) } })
case .ordering(let ordering):
return .ordering(ordering.qualified(with: alias))
}
}
func resolve(_ db: Database) throws -> [SQLOrderingTerm] {
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 {
return 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 -> [SQLOrderingTerm]) {
self.init(
elements: [.terms(DatabasePromise(orderings))],
isReversed: false)
}
var reversed: Ordering {
return Ordering(
elements: elements,
isReversed: !isReversed)
}
func qualified(with alias: TableAlias) -> Ordering {
return Ordering(
elements: elements.map { $0.qualified(with: alias) },
isReversed: isReversed)
}
func appending(_ ordering: Ordering) -> Ordering {
return Ordering(
elements: elements + [.ordering(ordering)],
isReversed: isReversed)
}
func resolve(_ db: Database) throws -> [SQLOrderingTerm] {
if isReversed {
return try elements.flatMap { try $0.reversed.resolve(db) }
} else {
return try elements.flatMap { try $0.resolve(db) }
}
}
}
}
// MARK: - SQLJoin
struct SQLJoin {
/// Not to be mismatched with SQL join operators (inner join, left join).
///
/// SQLJoin.Kind is designed to be hierarchically nested, unlike
/// SQL join operators.
///
/// Consider the following request for (A, B, C) tuples:
///
/// let r = A.including(optional: A.b.including(required: B.c))
///
/// It chains three associations, the first optional, the second required.
///
/// It looks like it means: "Give me all As, along with their Bs, granted those
/// Bs have their Cs. For As whose B has no C, give me a nil B".
///
/// It can not be expressed as one left join, and a regular join, as below,
/// Because this would not honor the first optional:
///
/// -- dubious
/// SELECT a.*, b.*, c.*
/// FROM a
/// LEFT JOIN b ON ...
/// JOIN c ON ...
///
/// Instead, it should:
/// - allow (A + missing (B + C))
/// - prevent (A + (B + missing C)).
///
/// This can be expressed in SQL with two left joins, and an extra condition:
///
/// -- likely correct
/// SELECT a.*, b.*, c.*
/// FROM a
/// LEFT JOIN b ON ...
/// LEFT JOIN c ON ...
/// WHERE NOT((b.id IS NOT NULL) AND (c.id IS NULL)) -- no B without C
///
/// This is currently not implemented, and requires a little more thought.
/// I don't even know if inventing a whole new way to perform joins should even
/// be on the table. But we have a hierarchical way to express joined queries,
/// and they have a meaning:
///
/// // what is my meaning?
/// A.including(optional: A.b.including(required: B.c))
enum Kind {
case required, optional
}
/// The condition that links two joined tables.
///
/// Currently, we only support one kind of join condition: foreign keys.
///
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
/// <- the join condition -->
///
/// When we eventually add support for new ways to join tables, Condition
/// is the type we'll need to update.
///
/// Condition equality allows merging of associations:
///
/// // request1 and request2 are equivalent
/// let request1 = Book
/// .including(required: Book.author)
/// let request2 = Book
/// .including(required: Book.author)
/// .including(required: Book.author)
///
/// // request3 and request4 are equivalent
/// let request3 = Book
/// .including(required: Book.author.filter(condition1 && condition2))
/// let request4 = Book
/// .joining(required: Book.author.filter(condition1))
/// .including(optional: Book.author.filter(condition2))
struct Condition: Equatable {
/// Definition of a foreign key
var foreignKeyRequest: ForeignKeyRequest
/// 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
var originIsLeft: Bool
var reversed: Condition {
return Condition(foreignKeyRequest: foreignKeyRequest, originIsLeft: !originIsLeft)
}
/// Returns an SQL expression for the join condition.
///
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
/// <- the SQL expression -->
///
/// - parameter db: A database connection.
/// - 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.
/// - Returns: An SQL expression.
func sqlExpression(_ db: Database, leftAlias: TableAlias, rightAlias: TableAlias) throws -> SQLExpression {
let foreignKeyMapping = try foreignKeyRequest.fetchMapping(db)
let columnMapping: [(left: Column, right: Column)]
if originIsLeft {
columnMapping = foreignKeyMapping.map { (left: Column($0.origin), right: Column($0.destination)) }
} else {
columnMapping = foreignKeyMapping.map { (left: Column($0.destination), right: Column($0.origin)) }
}
return columnMapping
.map { $0.right.qualifiedExpression(with: rightAlias) == $0.left.qualifiedExpression(with: leftAlias) }
.joined(operator: .and)
}
}
var kind: Kind
var condition: Condition
var relation: SQLRelation
}
// 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) -> SQLRelation? {
guard let mergedSource = source.merged(with: other.source) else {
// can't merge
return nil
}
let mergedFilterPromise: DatabasePromise<SQLExpression?> = filterPromise.flatMap { expression in
return DatabasePromise { db in
let otherExpression = try other.filterPromise.resolve(db)
let expressions = [expression, otherExpression].compactMap { $0 }
if expressions.isEmpty {
return nil
} else {
return expressions.joined(operator: .and)
}
}
}
var mergedJoins: OrderedDictionary<String, SQLJoin> = [:]
for (key, join) in joins {
if let otherJoin = other.joins[key] {
guard let mergedJoin = join.merged(with: otherJoin) else {
// can't merge
return nil
}
mergedJoins.appendValue(mergedJoin, forKey: key)
} else {
mergedJoins.appendValue(join, forKey: key)
}
}
for (key, join) in other.joins where mergedJoins[key] == nil {
mergedJoins.appendValue(join, forKey: key)
}
// replace selection unless empty
let mergedSelection = other.selection.isEmpty ? selection : other.selection
// replace ordering unless empty
let mergedOrdering = other.ordering.isEmpty ? ordering : other.ordering
return SQLRelation(
source: mergedSource,
selection: mergedSelection,
filterPromise: mergedFilterPromise,
ordering: mergedOrdering,
joins: mergedJoins)
}
}
extension SQLSource {
/// Returns nil if sources can't be merged (conflict in tables, aliases...)
func merged(with other: SQLSource) -> SQLSource? {
switch (self, other) {
case let (.table(tableName: tableName, alias: alias), .table(tableName: otherTableName, alias: otherAlias)):
guard tableName == otherTableName else {
// can't merge
return nil
}
switch (alias, otherAlias) {
case (nil, nil):
return .table(tableName: tableName, alias: nil)
case let (alias?, nil), let (nil, alias?):
return .table(tableName: tableName, alias: alias)
case let (alias?, otherAlias?):
guard let mergedAlias = alias.merged(with: otherAlias) else {
// can't merge
return nil
}
return .table(tableName: tableName, alias: mergedAlias)
}
default:
// can't merge
return nil
}
}
}
extension SQLJoin {
/// Returns nil if joins can't be merged (conflict in condition, relation...)
func merged(with other: SQLJoin) -> SQLJoin? {
guard condition == other.condition else {
// can't merge
return nil
}
guard let mergedRelation = relation.merged(with: other.relation) else {
// can't merge
return nil
}
let mergedKind: SQLJoin.Kind
switch (kind, other.kind) {
case (.required, _), (_, .required):
mergedKind = .required
default:
mergedKind = .optional
}
return SQLJoin(
kind: mergedKind,
condition: condition,
relation: mergedRelation)
}
}