Skip to content

Commit de04410

Browse files
authored
fix(datastore): eager loaded associations depth fixes #503
1 parent 9f9d658 commit de04410

File tree

11 files changed

+258
-152
lines changed

11 files changed

+258
-152
lines changed

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/ModelSchema+SQLite.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ extension ModelField: SQLColumn {
9393
if let namespace = namespace {
9494
column = "\(namespace).\(column)"
9595
}
96-
return "as \(column.quoted())"
96+
return column.quoted()
9797
}
9898

9999
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/SQLStatement+Select.swift

Lines changed: 106 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,78 +9,49 @@ import Amplify
99
import Foundation
1010
import SQLite
1111

12-
/// Represents a `select` SQL statement associated with a `Model` instance and
13-
/// optionally composed by a `ConditionStatement`.
14-
struct SelectStatement: SQLStatement {
15-
16-
let modelType: Model.Type
17-
let conditionStatement: ConditionStatement?
18-
let paginationInput: QueryPaginationInput?
19-
20-
// TODO remove this once sorting support is added to DataStore
21-
// Used by plugin to order and limit results for system table queries
22-
let additionalStatements: String?
23-
let namespace = "root"
24-
25-
init(from modelType: Model.Type,
26-
predicate: QueryPredicate? = nil,
27-
paginationInput: QueryPaginationInput? = nil,
28-
additionalStatements: String? = nil) {
29-
self.modelType = modelType
30-
31-
var conditionStatement: ConditionStatement?
32-
if let predicate = predicate {
33-
let statement = ConditionStatement(modelType: modelType,
34-
predicate: predicate,
35-
namespace: namespace[...])
36-
conditionStatement = statement
37-
}
38-
self.conditionStatement = conditionStatement
39-
self.paginationInput = paginationInput
40-
self.additionalStatements = additionalStatements
41-
}
42-
43-
var stringValue: String {
12+
/// Support data structure used to hold information about `SelectStatement` than
13+
/// can be used later to parse the results.
14+
struct SelectStatementMetadata {
15+
16+
typealias ColumnMapping = [String: (ModelSchema, ModelField)]
17+
18+
let statement: String
19+
let columnMapping: ColumnMapping
20+
let bindings: [Binding?]
21+
22+
// TODO remove additionalStatements once sorting support is added to DataStore
23+
static func metadata(from modelType: Model.Type,
24+
predicate: QueryPredicate? = nil,
25+
paginationInput: QueryPaginationInput? = nil,
26+
additionalStatements: String? = nil) -> SelectStatementMetadata {
27+
let rootNamespace = "root"
4428
let schema = modelType.schema
4529
let fields = schema.columns
4630
let tableName = schema.name
31+
var columnMapping: ColumnMapping = [:]
4732
var columns = fields.map { field -> String in
48-
return field.columnName(forNamespace: namespace) + " " + field.columnAlias()
33+
columnMapping.updateValue((schema, field), forKey: field.name)
34+
return field.columnName(forNamespace: rootNamespace) + " as " + field.columnAlias()
4935
}
5036

5137
// eager load many-to-one/one-to-one relationships
52-
var joinStatements: [String] = []
53-
for foreignKey in schema.foreignKeys {
54-
let associatedModelType = foreignKey.requiredAssociatedModel
55-
let associatedSchema = associatedModelType.schema
56-
let associatedTableName = associatedModelType.schema.name
57-
58-
// columns
59-
let alias = foreignKey.name
60-
let associatedColumn = associatedSchema.primaryKey.columnName(forNamespace: alias)
61-
let foreignKeyName = foreignKey.columnName(forNamespace: "root")
62-
63-
// append columns from relationships
64-
columns += associatedSchema.columns.map { field -> String in
65-
return field.columnName(forNamespace: alias) + " " + field.columnAlias(forNamespace: alias)
66-
}
67-
68-
let joinType = foreignKey.isRequired ? "inner" : "left outer"
69-
70-
joinStatements.append("""
71-
\(joinType) join \(associatedTableName) as \(alias)
72-
on \(associatedColumn) = \(foreignKeyName)
73-
""")
74-
}
38+
let joinStatements = joins(from: schema)
39+
columns += joinStatements.columns
40+
columnMapping.merge(joinStatements.columnMapping) { _, new in new }
7541

7642
var sql = """
7743
select
7844
\(joinedAsSelectedColumns(columns))
79-
from \(tableName) as root
80-
\(joinStatements.joined(separator: "\n"))
45+
from \(tableName) as "\(rootNamespace)"
46+
\(joinStatements.statements.joined(separator: "\n"))
8147
""".trimmingCharacters(in: .whitespacesAndNewlines)
8248

83-
if let conditionStatement = conditionStatement {
49+
var bindings: [Binding?] = []
50+
if let predicate = predicate {
51+
let conditionStatement = ConditionStatement(modelType: modelType,
52+
predicate: predicate,
53+
namespace: rootNamespace[...])
54+
bindings.append(contentsOf: conditionStatement.variables)
8455
sql = """
8556
\(sql)
8657
where 1 = 1
@@ -101,12 +72,86 @@ struct SelectStatement: SQLStatement {
10172
\(paginationInput.sqlStatement)
10273
"""
10374
}
75+
return SelectStatementMetadata(statement: sql,
76+
columnMapping: columnMapping,
77+
bindings: bindings)
78+
}
10479

105-
return sql
80+
struct JoinStatement {
81+
let columns: [String]
82+
let statements: [String]
83+
let columnMapping: ColumnMapping
84+
}
85+
86+
/// Walk through the associations recursively to generate join statements.
87+
///
88+
/// Implementation note: this should be revisited once we define support
89+
/// for explicit `eager` vs `lazy` associations.
90+
private static func joins(from schema: ModelSchema) -> JoinStatement {
91+
var columns: [String] = []
92+
var joinStatements: [String] = []
93+
var columnMapping: ColumnMapping = [:]
94+
95+
func visitAssociations(node: ModelSchema, namespace: String = "root") {
96+
for foreignKey in node.foreignKeys {
97+
let associatedModelType = foreignKey.requiredAssociatedModel
98+
let associatedSchema = associatedModelType.schema
99+
let associatedTableName = associatedModelType.schema.name
100+
101+
// columns
102+
let alias = namespace == "root" ? foreignKey.name : "\(namespace).\(foreignKey.name)"
103+
let associatedColumn = associatedSchema.primaryKey.columnName(forNamespace: alias)
104+
let foreignKeyName = foreignKey.columnName(forNamespace: namespace)
105+
106+
// append columns from relationships
107+
columns += associatedSchema.columns.map { field -> String in
108+
columnMapping.updateValue((associatedSchema, field), forKey: "\(alias).\(field.name)")
109+
return field.columnName(forNamespace: alias) + " as " + field.columnAlias(forNamespace: alias)
110+
}
111+
112+
let joinType = foreignKey.isRequired ? "inner" : "left outer"
113+
114+
joinStatements.append("""
115+
\(joinType) join \(associatedTableName) as "\(alias)"
116+
on \(associatedColumn) = \(foreignKeyName)
117+
""")
118+
visitAssociations(node: associatedModelType.schema,
119+
namespace: alias)
120+
}
121+
}
122+
visitAssociations(node: schema)
123+
124+
return JoinStatement(columns: columns,
125+
statements: joinStatements,
126+
columnMapping: columnMapping)
127+
}
128+
129+
}
130+
131+
/// Represents a `select` SQL statement associated with a `Model` instance and
132+
/// optionally composed by a `ConditionStatement`.
133+
struct SelectStatement: SQLStatement {
134+
135+
let modelType: Model.Type
136+
let metadata: SelectStatementMetadata
137+
138+
init(from modelType: Model.Type,
139+
predicate: QueryPredicate? = nil,
140+
paginationInput: QueryPaginationInput? = nil,
141+
additionalStatements: String? = nil) {
142+
self.modelType = modelType
143+
self.metadata = .metadata(from: modelType,
144+
predicate: predicate,
145+
paginationInput: paginationInput,
146+
additionalStatements: additionalStatements)
147+
}
148+
149+
var stringValue: String {
150+
metadata.statement
106151
}
107152

108153
var variables: [Binding?] {
109-
return conditionStatement?.variables ?? []
154+
metadata.bindings
110155
}
111156

112157
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/Statement+AnyModel.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ import Amplify
99
import SQLite
1010

1111
extension Statement {
12-
public func convert(toUntypedModel modelType: Model.Type) throws -> [Model] {
12+
func convert(toUntypedModel modelType: Model.Type,
13+
using statement: SelectStatement) throws -> [Model] {
1314
var models = [Model]()
14-
var convertedCache: ConvertCache = [:]
1515

1616
for row in self {
17-
let modelValues = try mapEach(row: row,
18-
to: modelType,
19-
cache: &convertedCache)
17+
let modelValues = try convert(row: row, to: modelType, using: statement)
2018
let untypedModel = try convert(toAnyModel: modelType, modelDictionary: modelValues)
2119
models.append(untypedModel)
2220
}

0 commit comments

Comments
 (0)