Skip to content

Commit 1239178

Browse files
committed
Improve diagnostic when an association to/from view are lacking an explicit ForeignKey
Related discussion: #1481
1 parent e5a7c73 commit 1239178

File tree

1 file changed

+82
-26
lines changed

1 file changed

+82
-26
lines changed

GRDB/QueryInterface/SQL/SQLForeignKeyRequest.swift

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,41 +32,76 @@ struct SQLForeignKeyRequest {
3232
}
3333

3434
// Incomplete information: let's look for schema foreign keys
35-
let foreignKeys = try db.foreignKeys(on: originTable).filter { foreignKey in
36-
if destinationTable.lowercased() != foreignKey.destinationTable.lowercased() {
37-
return false
35+
//
36+
// But maybe the tables are views. In this case, don't throw
37+
// "no such table" error, because this is confusing for the user,
38+
// as discovered in <https://github.com/groue/GRDB.swift/discussions/1481>.
39+
// Instead, we'll crash with a clear message.
40+
41+
guard let originType = try tableType(db, for: originTable) else {
42+
throw DatabaseError.noSuchTable(originTable)
43+
}
44+
45+
if originType.isView {
46+
if originColumns == nil {
47+
fatalError("""
48+
Could not infer foreign key from '\(originTable)' \
49+
to '\(destinationTable)'. To fix this error, provide an \
50+
explicit `ForeignKey` in the association definition.
51+
""")
3852
}
39-
if let originColumns {
40-
let originColumns = Set(originColumns.lazy.map { $0.lowercased() })
41-
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.origin.lowercased() })
42-
if originColumns != foreignKeyColumns {
53+
} else {
54+
let foreignKeys = try db.foreignKeys(on: originTable).filter { foreignKey in
55+
if destinationTable.lowercased() != foreignKey.destinationTable.lowercased() {
4356
return false
4457
}
45-
}
46-
if let destinationColumns {
47-
// TODO: test
48-
let destinationColumns = Set(destinationColumns.lazy.map { $0.lowercased() })
49-
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.destination.lowercased() })
50-
if destinationColumns != foreignKeyColumns {
51-
return false
58+
if let originColumns {
59+
let originColumns = Set(originColumns.lazy.map { $0.lowercased() })
60+
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.origin.lowercased() })
61+
if originColumns != foreignKeyColumns {
62+
return false
63+
}
64+
}
65+
if let destinationColumns {
66+
// TODO: test
67+
let destinationColumns = Set(destinationColumns.lazy.map { $0.lowercased() })
68+
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.destination.lowercased() })
69+
if destinationColumns != foreignKeyColumns {
70+
return false
71+
}
5272
}
73+
return true
5374
}
54-
return true
55-
}
56-
57-
// Matching foreign key(s) found
58-
if let foreignKey = foreignKeys.first {
59-
if foreignKeys.count == 1 {
60-
// Non-ambiguous
61-
return foreignKey.mapping
62-
} else {
63-
// Ambiguous: can't choose
64-
fatalError("Ambiguous foreign key from \(originTable) to \(destinationTable)")
75+
76+
// Matching foreign key(s) found
77+
if let foreignKey = foreignKeys.first {
78+
if foreignKeys.count == 1 {
79+
// Non-ambiguous
80+
return foreignKey.mapping
81+
} else {
82+
// Ambiguous: can't choose
83+
fatalError("""
84+
Ambiguous foreign key from '\(originTable)' to \
85+
'\(destinationTable)'. To fix this error, provide an \
86+
explicit `ForeignKey` in the association definition.
87+
""")
88+
}
6589
}
6690
}
6791

6892
// No matching foreign key found: use the destination primary key
6993
if let originColumns {
94+
guard let destinationType = try tableType(db, for: destinationTable) else {
95+
throw DatabaseError.noSuchTable(destinationTable)
96+
}
97+
if destinationType.isView {
98+
fatalError("""
99+
Could not infer foreign key from '\(originTable)' \
100+
to '\(destinationTable)'. To fix this error, provide an \
101+
explicit `ForeignKey` in the association definition, \
102+
with both origin and destination columns.
103+
""")
104+
}
70105
let destinationColumns = try db.primaryKey(destinationTable).columns
71106
if originColumns.count == destinationColumns.count {
72107
let mapping = zip(originColumns, destinationColumns).map {
@@ -76,7 +111,28 @@ struct SQLForeignKeyRequest {
76111
}
77112
}
78113

79-
fatalError("Could not infer foreign key from \(originTable) to \(destinationTable)")
114+
fatalError("""
115+
Could not infer foreign key from '\(originTable)' to \
116+
'\(destinationTable)'. To fix this error, provide an \
117+
explicit `ForeignKey` in the association definition.
118+
""")
119+
}
120+
121+
private struct TableType {
122+
var isView: Bool
123+
}
124+
125+
private func tableType(_ db: Database, for name: String) throws -> TableType? {
126+
for schemaID in try db.schemaIdentifiers() {
127+
if try db.schema(schemaID).containsObjectNamed(name, ofType: .table) {
128+
return TableType(isView: false)
129+
}
130+
if try db.schema(schemaID).containsObjectNamed(name, ofType: .view) {
131+
return TableType(isView: true)
132+
}
133+
}
134+
135+
return nil
80136
}
81137
}
82138

0 commit comments

Comments
 (0)