Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix associations altered by another association #677

Merged
merged 6 commits into from
Jan 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ All notable changes to this project will be documented in this file.
GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: APIs flagged [**:fire: EXPERIMENTAL**](README.md#what-are-experimental-features). Those are unstable, and may break between any two minor releases of the library.


<!--
[Next Release](#next-release)
-->


#### 4.x Releases
Expand Down Expand Up @@ -61,9 +59,11 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
- [0.110.0](#01100), ...


<!--
## Next Release
-->

**Fixed**

- [#677](https://github.com/groue/GRDB.swift/pull/677): Fix associations altered by another association


## 4.8.0
Expand Down
51 changes: 35 additions & 16 deletions GRDB/QueryInterface/SQL/SQLAssociation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public /* TODO: internal */ struct SQLAssociation {
// All steps, from pivot to destination. Never empty.
private(set) var steps: [SQLAssociationStep]
var keyPath: [String] {
return steps.map { $0.key.name(for: $0.cardinality) }
return steps.map { $0.keyName }
}

var destination: SQLAssociationStep {
Expand Down Expand Up @@ -189,25 +189,27 @@ public /* TODO: internal */ struct SQLAssociation {
// 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) -> SQLAssociationStep in
// Intermediate steps are not included in the selection, and
// don't have any child.
let relation = step.relation.select([]).deletingChildren()
var filteredSteps = steps
filteredSteps[0] = pivot.mapRelation { _ in filteredPivotRelation }
let reversedSteps = zip(filteredSteps, filteredSteps.dropFirst())
.map({ (step, nextStep) -> SQLAssociationStep in
// Intermediate steps are not selected, and including(all:)
// children are useless:
let relation = step.relation
.selectOnly([])
.filteringChildren { $0.kind.cardinality == .toOne }

// Don't interfere with user-defined keys that could be added later
let key = step.key.mapName { "grdb_\($0)" }

return SQLAssociationStep(
key: step.key,
key: key,
condition: nextStep.condition.reversed,
relation: relation,
cardinality: step.cardinality)
}
cardinality: .toOne)
})
.reversed()

var reversedAssociation = SQLAssociation(steps: Array(reversedSteps))
// Replace pivot with the filtered one (not included in the selection,
// without children).
reversedAssociation = reversedAssociation.mapDestinationRelation { _ in
filteredPivotRelation.select([]).deletingChildren()
}
let reversedAssociation = SQLAssociation(steps: Array(reversedSteps))
return destination.relation.appendingChild(for: reversedAssociation, kind: .oneRequired)
}
}
Expand All @@ -218,6 +220,10 @@ struct SQLAssociationStep {
var relation: SQLRelation
var cardinality: SQLAssociationCardinality

var keyName: String {
return key.name(for: cardinality)
}

func mapRelation(_ transform: (SQLRelation) -> SQLRelation) -> SQLAssociationStep {
return SQLAssociationStep(
key: key,
Expand Down Expand Up @@ -350,4 +356,17 @@ enum SQLAssociationKey {
return name
}
}

func mapName(_ transform: (String) throws -> String) rethrows -> SQLAssociationKey {
switch self {
case .inflected(let name):
return try .inflected(transform(name))
case .fixedSingular(let name):
return try .fixedSingular(transform(name))
case .fixedPlural(let name):
return try .fixedPlural(transform(name))
case .fixed(let name):
return try .fixed(transform(name))
}
}
}
44 changes: 32 additions & 12 deletions GRDB/QueryInterface/SQL/SQLRelation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,14 @@ struct SQLRelation {
case .allPrefetched:
return [child.makeAssociationForKey(key)]
case .oneOptional, .oneRequired, .allNotPrefetched:
return child.relation.prefetchedAssociations.map {
$0.through(child.makeAssociationForKey(key))
return child.relation.prefetchedAssociations.map { association in
// Remove redundant pivot child
let pivotKey = association.pivot.keyName
let child = child.mapRelation { relation in
assert(relation.children[pivotKey] != nil)
return relation.removingChild(forKey: pivotKey)
}
return association.through(child.makeAssociationForKey(key))
}
}
}
Expand Down Expand Up @@ -196,6 +202,14 @@ extension SQLRelation {
return relation
}

/// Removes all selections from chidren
func selectOnly(_ selection: [SQLSelectable]) -> SQLRelation {
var relation = self
relation.selection = selection
relation.children = relation.children.mapValues { $0.mapRelation { $0.selectOnly([]) } }
return relation
}

func annotated(with selection: [SQLSelectable]) -> SQLRelation {
var relation = self
relation.selection.append(contentsOf: selection)
Expand Down Expand Up @@ -239,12 +253,6 @@ extension SQLRelation {
}

extension SQLRelation {
func deletingChildren() -> SQLRelation {
var relation = self
relation.children = [:]
return relation
}

/// Returns a relation extended with an association.
///
/// This method provides support for public joining methods such
Expand Down Expand Up @@ -359,6 +367,18 @@ extension SQLRelation {
}
return relation
}

func removingChild(forKey key: String) -> SQLRelation {
var relation = self
relation.children.removeValue(forKey: key)
return relation
}

func filteringChildren(_ included: (Child) throws -> Bool) rethrows -> SQLRelation {
var relation = self
relation.children = try relation.children.filter { try included($1) }
return relation
}
}

extension SQLRelation: _JoinableRequest {
Expand Down Expand Up @@ -616,16 +636,16 @@ struct SQLAssociationCondition: Equatable {
// Join on a multiple columns.
// ((table.a = 1) AND (table.b = 2)) OR ((table.a = 3) AND (table.b = 4)) ...
return leftRows
.map { leftRow in
.map({ leftRow in
// (table.a = 1) AND (table.b = 2)
columnMappings
.map { columns -> SQLExpression in
.map({ columns -> SQLExpression in
let rightColumn = QualifiedColumn(columns.right, alias: rightAlias)
let leftValue = leftRow[columns.left] as DatabaseValue
return rightColumn == leftValue
}
})
.joined(operator: .and)
}
})
.joined(operator: .or)
}
}
Expand Down
12 changes: 12 additions & 0 deletions GRDB/Utils/OrderedDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ struct OrderedDictionary<Key: Hashable, Value> {
return keys.map { dictionary[$0]! }
}

private init(keys: [Key], dictionary: [Key: Value]) {
assert(Set(keys) == Set(dictionary.keys))
self.keys = keys
self.dictionary = dictionary
}

/// Creates an empty ordered dictionary.
init() {
keys = []
Expand Down Expand Up @@ -108,6 +114,12 @@ struct OrderedDictionary<Key: Hashable, Value> {
}
}
}

func filter(_ isIncluded: ((key: Key, value: Value)) throws -> Bool) rethrows -> OrderedDictionary<Key, Value> {
let dictionary = try self.dictionary.filter(isIncluded)
let keys = self.keys.filter(dictionary.keys.contains)
return OrderedDictionary(keys: keys, dictionary: dictionary)
}
}

extension OrderedDictionary: Collection {
Expand Down
71 changes: 71 additions & 0 deletions Tests/GRDBTests/AssociationHasManySQLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -764,4 +764,75 @@ class AssociationHasManySQLTests: GRDBTestCase {
}
}
}

func testAssociationFilteredByOtherAssociation() throws {
struct Toy: TableRecord { }
struct Child: TableRecord {
static let toy = hasOne(Toy.self)
static let parent = belongsTo(Parent.self)
}
struct Parent: TableRecord, EncodableRecord {
static let children = hasMany(Child.self)
func encode(to container: inout PersistenceContainer) {
container["id"] = 1
}
}

let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "parent") { t in
t.autoIncrementedPrimaryKey("id")
}
try db.create(table: "child") { t in
t.column("parentId", .integer).references("parent")
}
try db.create(table: "toy") { t in
t.column("childId", .integer).references("child")
}
}

try dbQueue.inDatabase { db in
do {
let association = Parent.children.joining(required: Child.toy)
try assertEqualSQL(db, Parent.all().including(required: association), """
SELECT "parent".*, "child".* \
FROM "parent" JOIN "child" ON "child"."parentId" = "parent"."id" \
JOIN "toy" ON "toy"."childId" = "child"."rowid"
""")
try assertEqualSQL(db, Parent.all().joining(required: association), """
SELECT "parent".* FROM "parent" \
JOIN "child" ON "child"."parentId" = "parent"."id" \
JOIN "toy" ON "toy"."childId" = "child"."rowid"
""")
try assertEqualSQL(db, Parent().request(for: association), """
SELECT "child".* \
FROM "child" \
JOIN "toy" ON "toy"."childId" = "child"."rowid" \
WHERE "child"."parentId" = 1
""")
}
do {
// not a realistic use case, but testable anyway
let association = Parent.children.joining(required: Child.parent)
try assertEqualSQL(db, Parent.all().including(required: association), """
SELECT "parent1".*, "child".* \
FROM "parent" "parent1" \
JOIN "child" ON "child"."parentId" = "parent1"."id" \
JOIN "parent" "parent2" ON "parent2"."id" = "child"."parentId"
""")
try assertEqualSQL(db, Parent.all().joining(required: association), """
SELECT "parent1".* \
FROM "parent" "parent1" \
JOIN "child" ON "child"."parentId" = "parent1"."id" \
JOIN "parent" "parent2" ON "parent2"."id" = "child"."parentId"
""")
try assertEqualSQL(db, Parent().request(for: association), """
SELECT "child".* \
FROM "child" \
JOIN "parent" ON "parent"."id" = "child"."parentId" \
WHERE "child"."parentId" = 1
""")
}
}
}
}
Loading