Tighten SQLJoin
This commit is contained in:
parent
df84654554
commit
784ebcc8e9
@ -246,28 +246,28 @@ extension Association {
|
||||
/// associated record are selected. The returned association does not
|
||||
/// require that the associated database table contains a matching row.
|
||||
public func including<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.sqlAssociation.relation(from: $0, joinOperator: .optional) }
|
||||
return mapRelation { association.sqlAssociation.relation(from: $0, joinKind: .optional) }
|
||||
}
|
||||
|
||||
/// Creates an association that includes another one. The columns of the
|
||||
/// associated record are selected. The returned association requires
|
||||
/// that the associated database table contains a matching row.
|
||||
public func including<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.sqlAssociation.relation(from: $0, joinOperator: .required) }
|
||||
return mapRelation { association.sqlAssociation.relation(from: $0, joinKind: .required) }
|
||||
}
|
||||
|
||||
/// Creates an association that joins another one. The columns of the
|
||||
/// associated record are not selected. The returned association does not
|
||||
/// require that the associated database table contains a matching row.
|
||||
public func joining<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, joinOperator: .optional) }
|
||||
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, joinKind: .optional) }
|
||||
}
|
||||
|
||||
/// Creates an association that joins another one. The columns of the
|
||||
/// associated record are not selected. The returned association requires
|
||||
/// that the associated database table contains a matching row.
|
||||
public func joining<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, joinOperator: .required) }
|
||||
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, joinKind: .required) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,7 +402,7 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
// SQLAssociation is a non-empty array of association elements
|
||||
private struct Element {
|
||||
var key: String
|
||||
var joinCondition: JoinCondition
|
||||
var condition: SQLJoin.Condition
|
||||
var relation: SQLRelation
|
||||
}
|
||||
private var head: Element
|
||||
@ -415,8 +415,8 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
self.tail = tail
|
||||
}
|
||||
|
||||
init(key: String, joinCondition: JoinCondition, relation: SQLRelation) {
|
||||
head = Element(key: key, joinCondition: joinCondition, relation: relation)
|
||||
init(key: String, condition: SQLJoin.Condition, relation: SQLRelation) {
|
||||
head = Element(key: key, condition: condition, relation: relation)
|
||||
tail = []
|
||||
}
|
||||
|
||||
@ -440,10 +440,10 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
}
|
||||
|
||||
/// Support for joining methods joining(optional:), etc.
|
||||
func relation(from origin: SQLRelation, joinOperator: JoinOperator) -> SQLRelation {
|
||||
func relation(from origin: SQLRelation, joinKind: SQLJoin.Kind) -> SQLRelation {
|
||||
let headJoin = SQLJoin(
|
||||
joinOperator: joinOperator,
|
||||
joinCondition: head.joinCondition,
|
||||
kind: joinKind,
|
||||
condition: head.condition,
|
||||
relation: head.relation)
|
||||
|
||||
// Recursion step: remove one element from tail by shifting the next
|
||||
@ -463,10 +463,10 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
}
|
||||
|
||||
let nextRelation = next.relation.select([]).appendingJoin(headJoin, forKey: head.key)
|
||||
let reducedHead = Element(key: next.key, joinCondition: next.joinCondition, relation: nextRelation)
|
||||
let reducedHead = Element(key: next.key, condition: next.condition, relation: nextRelation)
|
||||
let reducedTail = Array(tail.dropFirst())
|
||||
let reducedAssociation = SQLAssociation(head: reducedHead, tail: reducedTail)
|
||||
return reducedAssociation.relation(from: origin, joinOperator: joinOperator)
|
||||
return reducedAssociation.relation(from: origin, joinKind: joinKind)
|
||||
}
|
||||
|
||||
/// Support for (TableRecord & EncodableRecord).request(for:).
|
||||
@ -483,7 +483,7 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
func relation(to originTable: String, container originContainer: @escaping (Database) throws -> PersistenceContainer) -> SQLRelation {
|
||||
// Build a "pivot" relation whose filter is the pivot condition
|
||||
// injected with values contained in originContainer.
|
||||
let pivotCondition = pivot.joinCondition
|
||||
let pivotCondition = pivot.condition
|
||||
let pivotAlias = TableAlias()
|
||||
let pivotRelation = pivot.relation
|
||||
.qualified(with: pivotAlias)
|
||||
@ -499,7 +499,7 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
|
||||
// We use elements backward: join conditions have to be reversed.
|
||||
let reversedElements = zip([head] + tail, tail)
|
||||
.map { Element(key: $1.key, joinCondition: $0.joinCondition.reversed, relation: $1.relation.select([])) }
|
||||
.map { Element(key: $1.key, condition: $0.condition.reversed, relation: $1.relation.select([])) }
|
||||
.reversed()
|
||||
|
||||
// Empty tail?
|
||||
@ -510,6 +510,6 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
reversedHead.relation = pivotRelation.select([])
|
||||
let reversedTail = Array(reversedElements.dropFirst())
|
||||
let reversedAssociation = SQLAssociation(head: reversedHead, tail: reversedTail)
|
||||
return reversedAssociation.relation(from: head.relation, joinOperator: .required)
|
||||
return reversedAssociation.relation(from: head.relation, joinKind: .required)
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,13 +146,13 @@ extension TableRecord {
|
||||
destinationTable: Destination.databaseTableName,
|
||||
foreignKey: foreignKey)
|
||||
|
||||
let joinCondition = JoinCondition(
|
||||
let condition = SQLJoin.Condition(
|
||||
foreignKeyRequest: foreignKeyRequest,
|
||||
originIsLeft: true)
|
||||
|
||||
return BelongsToAssociation(sqlAssociation: SQLAssociation(
|
||||
key: key ?? Destination.databaseTableName,
|
||||
joinCondition: joinCondition,
|
||||
condition: condition,
|
||||
relation: Destination.all().relation))
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,14 +19,15 @@ struct ForeignKeyRequest: Equatable {
|
||||
self.destinationColumns = foreignKey?.destinationColumns
|
||||
}
|
||||
|
||||
func fetch(_ db: Database) throws -> ForeignKeyInfo {
|
||||
/// The (origin, destination) column pairs that join a left table to a right table.
|
||||
func fetchMapping(_ db: Database) throws -> [(origin: String, destination: String)] {
|
||||
if let originColumns = originColumns, let destinationColumns = 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 ForeignKeyInfo(destinationTable: destinationTable, mapping: mapping)
|
||||
return mapping
|
||||
}
|
||||
|
||||
// Incomplete information: let's look for schema foreign keys
|
||||
@ -56,7 +57,7 @@ struct ForeignKeyRequest: Equatable {
|
||||
if let foreignKey = foreignKeys.first {
|
||||
if foreignKeys.count == 1 {
|
||||
// Non-ambiguous
|
||||
return foreignKey
|
||||
return foreignKey.mapping
|
||||
} else {
|
||||
// Ambiguous: can't choose
|
||||
fatalError("Ambiguous foreign key from \(originTable) to \(destinationTable)")
|
||||
@ -70,13 +71,10 @@ struct ForeignKeyRequest: Equatable {
|
||||
let mapping = zip(originColumns, destinationColumns).map {
|
||||
(origin: $0, destination: $1)
|
||||
}
|
||||
return ForeignKeyInfo(destinationTable: destinationTable, mapping: mapping)
|
||||
return mapping
|
||||
}
|
||||
}
|
||||
|
||||
fatalError("Could not infer foreign key from \(originTable) to \(destinationTable)")
|
||||
}
|
||||
}
|
||||
|
||||
/// The (origin, destination) column pairs that join a left table to a right table.
|
||||
typealias ForeignKeyMapping = [(origin: String, destination: String)]
|
||||
|
||||
@ -146,13 +146,13 @@ extension TableRecord {
|
||||
destinationTable: databaseTableName,
|
||||
foreignKey: foreignKey)
|
||||
|
||||
let joinCondition = JoinCondition(
|
||||
let condition = SQLJoin.Condition(
|
||||
foreignKeyRequest: foreignKeyRequest,
|
||||
originIsLeft: false)
|
||||
|
||||
return HasManyAssociation(sqlAssociation: SQLAssociation(
|
||||
key: key ?? Destination.databaseTableName,
|
||||
joinCondition: joinCondition,
|
||||
condition: condition,
|
||||
relation: Destination.all().relation))
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,13 +148,13 @@ extension TableRecord {
|
||||
destinationTable: databaseTableName,
|
||||
foreignKey: foreignKey)
|
||||
|
||||
let joinCondition = JoinCondition(
|
||||
let condition = SQLJoin.Condition(
|
||||
foreignKeyRequest: foreignKeyRequest,
|
||||
originIsLeft: false)
|
||||
|
||||
return HasOneAssociation(sqlAssociation: SQLAssociation(
|
||||
key: key ?? Destination.databaseTableName,
|
||||
joinCondition: joinCondition,
|
||||
condition: condition,
|
||||
relation: Destination.all().relation))
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
public func including<A: Association>(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, joinOperator: .optional)
|
||||
association.sqlAssociation.relation(from: $0, joinKind: .optional)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,7 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
public func including<A: Association>(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, joinOperator: .required)
|
||||
association.sqlAssociation.relation(from: $0, joinKind: .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
public func joining<A: Association>(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.select([]).sqlAssociation.relation(from: $0, joinOperator: .optional)
|
||||
association.select([]).sqlAssociation.relation(from: $0, joinKind: .optional)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
public func joining<A: Association>(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.select([]).sqlAssociation.relation(from: $0, joinOperator: .required)
|
||||
association.select([]).sqlAssociation.relation(from: $0, joinKind: .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,134 +200,135 @@ extension SQLRelation {
|
||||
|
||||
// MARK: - SQLJoin
|
||||
|
||||
/// Not to be mismatched with SQL join operators (inner join, left join).
|
||||
///
|
||||
/// JoinOperator 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 JoinOperator {
|
||||
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, JoinCondition
|
||||
/// is the type we'll need to update.
|
||||
///
|
||||
/// JoinCondition 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 JoinCondition: Equatable {
|
||||
/// Definition of a foreign key
|
||||
var foreignKeyRequest: ForeignKeyRequest
|
||||
struct SQLJoin {
|
||||
|
||||
/// True if the table at the origin of the foreign key is on the left of
|
||||
/// the sql JOIN operator.
|
||||
/// Not to be mismatched with SQL join operators (inner join, left join).
|
||||
///
|
||||
/// Let's consider the `book.authorId -> author.id` foreign key.
|
||||
/// Its origin table is `book`.
|
||||
/// SQLJoin.Kind is designed to be hierarchically nested, unlike
|
||||
/// SQL join operators.
|
||||
///
|
||||
/// The origin table `book` is on the left of the JOIN operator for
|
||||
/// the BelongsTo association:
|
||||
/// Consider the following request for (A, B, C) tuples:
|
||||
///
|
||||
/// -- Book.including(required: Book.author)
|
||||
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
|
||||
/// let r = A.including(optional: A.b.including(required: B.c))
|
||||
///
|
||||
/// The origin table `book`is on the right of the JOIN operator for
|
||||
/// the HasMany and HasOne associations:
|
||||
/// It chains three associations, the first optional, the second required.
|
||||
///
|
||||
/// -- Author.including(required: Author.books)
|
||||
/// SELECT ... FROM author JOIN book ON author.id = book.authorId
|
||||
var originIsLeft: Bool
|
||||
|
||||
var reversed: JoinCondition {
|
||||
return JoinCondition(foreignKeyRequest: foreignKeyRequest, originIsLeft: !originIsLeft)
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Returns an SQL expression for the join condition.
|
||||
|
||||
/// 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 SQL expression -->
|
||||
/// <- the join condition -->
|
||||
///
|
||||
/// - 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.fetch(db).mapping
|
||||
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)) }
|
||||
/// 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)
|
||||
}
|
||||
|
||||
return columnMapping
|
||||
.map { $0.right.qualifiedExpression(with: rightAlias) == $0.left.qualifiedExpression(with: leftAlias) }
|
||||
.joined(operator: .and)
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SQLJoin {
|
||||
var joinOperator: JoinOperator
|
||||
var joinCondition: JoinCondition
|
||||
|
||||
var kind: Kind
|
||||
var condition: Condition
|
||||
var relation: SQLRelation
|
||||
}
|
||||
|
||||
@ -428,7 +429,7 @@ extension SQLSource {
|
||||
extension SQLJoin {
|
||||
/// Returns nil if joins can't be merged (conflict in condition, relation...)
|
||||
func merged(with other: SQLJoin) -> SQLJoin? {
|
||||
guard joinCondition == other.joinCondition else {
|
||||
guard condition == other.condition else {
|
||||
// can't merge
|
||||
return nil
|
||||
}
|
||||
@ -438,17 +439,17 @@ extension SQLJoin {
|
||||
return nil
|
||||
}
|
||||
|
||||
let mergedJoinOperator: JoinOperator
|
||||
switch (joinOperator, other.joinOperator) {
|
||||
let mergedKind: SQLJoin.Kind
|
||||
switch (kind, other.kind) {
|
||||
case (.required, _), (_, .required):
|
||||
mergedJoinOperator = .required
|
||||
mergedKind = .required
|
||||
default:
|
||||
mergedJoinOperator = .optional
|
||||
mergedKind = .optional
|
||||
}
|
||||
|
||||
return SQLJoin(
|
||||
joinOperator: mergedJoinOperator,
|
||||
joinCondition: joinCondition,
|
||||
kind: mergedKind,
|
||||
condition: condition,
|
||||
relation: mergedRelation)
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,20 +404,20 @@ private enum SQLQualifiedSource {
|
||||
|
||||
/// A "qualified" join, where all tables are identified with a table alias.
|
||||
private struct SQLQualifiedJoin {
|
||||
private let joinOperator: JoinOperator
|
||||
private let joinCondition: JoinCondition
|
||||
private let kind: SQLJoin.Kind
|
||||
private let condition: SQLJoin.Condition
|
||||
let relation: SQLQualifiedRelation
|
||||
|
||||
init(_ join: SQLJoin) {
|
||||
self.joinOperator = join.joinOperator
|
||||
self.joinCondition = join.joinCondition
|
||||
self.kind = join.kind
|
||||
self.condition = join.condition
|
||||
self.relation = SQLQualifiedRelation(join.relation)
|
||||
}
|
||||
|
||||
func sql(_ db: Database,_ context: inout SQLGenerationContext, leftAlias: TableAlias, isRequiredAllowed: Bool) throws -> String {
|
||||
var isRequiredAllowed = isRequiredAllowed
|
||||
var sql = ""
|
||||
switch joinOperator {
|
||||
switch kind {
|
||||
case .optional:
|
||||
isRequiredAllowed = false
|
||||
sql += "LEFT JOIN"
|
||||
@ -433,7 +433,7 @@ private struct SQLQualifiedJoin {
|
||||
|
||||
let rightAlias = relation.alias
|
||||
let filters = try [
|
||||
joinCondition.sqlExpression(db, leftAlias: leftAlias, rightAlias: rightAlias),
|
||||
condition.sqlExpression(db, leftAlias: leftAlias, rightAlias: rightAlias),
|
||||
relation.filterPromise.resolve(db)
|
||||
].compactMap { $0 }
|
||||
if !filters.isEmpty {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user