@@ -76,119 +76,127 @@ extension Statement: StatementModelConvertible {
76
76
// parse each row of the result
77
77
let iter = makeIterator ( )
78
78
while let row = try iter. failableNext ( ) {
79
- let modelDictionary = try convert ( row: row, withSchema: modelSchema, using: statement, eagerLoad: eagerLoad)
80
- elements. append ( modelDictionary)
79
+ if let modelDictionary = try convert ( row: row, withSchema: modelSchema, using: statement, eagerLoad: eagerLoad) {
80
+ elements. append ( modelDictionary)
81
+ }
81
82
}
82
83
return elements
83
84
}
84
-
85
- func convert( row: Element ,
86
- withSchema modelSchema: ModelSchema ,
87
- using statement: SelectStatement ,
88
- eagerLoad: Bool = true ) throws -> ModelValues {
89
- let columnMapping = statement. metadata. columnMapping
90
- let modelDictionary = ( [ : ] as ModelValues ) . mutableCopy ( )
91
- var skipColumns = Set < String > ( )
92
- var foreignKeyValues = [ ( String, Binding? ) ] ( )
93
- for (index, value) in row. enumerated ( ) {
94
- let column = columnNames [ index]
95
- guard let ( schema, field) = columnMapping [ column] else {
96
- logger. debug ( " [LazyLoad] Foreign key ` \( column) ` was found in the SQL result set with value: \( value) " )
97
- foreignKeyValues. append ( ( column, value) )
98
- continue
85
+
86
+ func convert(
87
+ row: Element ,
88
+ withSchema modelSchema: ModelSchema ,
89
+ using statement: SelectStatement ,
90
+ eagerLoad: Bool = true ,
91
+ path: [ String ] = [ ]
92
+ ) throws -> ModelValues ? {
93
+ guard let maxDepth = columnNames. map ( { $0. split ( separator: " . " ) . count } ) . max ( ) ,
94
+ path. count < maxDepth
95
+ else {
96
+ return nil
97
+ }
98
+
99
+ let modelValues = try modelSchema. fields. mapValues {
100
+ try convert ( field: $0, schema: modelSchema, using: statement, from: row, eagerLoad: eagerLoad, path: path)
101
+ }
102
+
103
+ if modelValues. values. contains ( where: { $0 != nil } ) {
104
+ return modelValues. merging ( schemeMetadata ( schema: modelSchema, element: row, path: path) ) { $1 }
105
+ } else {
106
+ return nil
107
+ }
108
+ }
109
+
110
+ private func convert(
111
+ field: ModelField ,
112
+ schema: ModelSchema ,
113
+ using statement: SelectStatement ,
114
+ from element: Element ,
115
+ eagerLoad: Bool ,
116
+ path: [ String ]
117
+ ) throws -> Any ? {
118
+
119
+ switch ( field. type, eagerLoad) {
120
+ case ( . collection, _) :
121
+ return convertCollection ( field: field, schema: schema, from: element, path: path)
122
+
123
+ case ( . model, false ) :
124
+ let foreignKey = field. association. map ( getTargetNames) . map {
125
+ field. foreignKeySqlName ( withAssociationTargets: $0)
99
126
}
100
-
101
- let modelValue = try SQLiteModelValueConverter . convertToSource (
102
- from: value,
103
- fieldType: field. type
104
- )
105
- // Check if the value for the primary key is `nil`. This is when an associated model does not exist.
106
- // To create a decodable `modelDictionary` that can be decoded to the Model types, the entire
107
- // object at this particular key should be set to `nil`. The following code does that by dropping the last
108
- // path from the column, for example given "blog.id" `column` has a nil `modelValue`, then store the
109
- // keypath in `skipColumns`, which will be used to set modelDictionary["blog"] to nil later on.
110
- //
111
- // `skipColumns` keeps track of these scenarios. At this point in the code we cannot make an assumption
112
- // about the ordering of the columns, it may handle "blog.description", then "blog.id", then "blog.title".
113
- // The code will perform the following:
114
- // 1. set ["blog"]["description"] = `nil`, because it has not encountered `nil` primary key
115
- // 2. store skipColumn["blog"], because the value is the primary key and is nil
116
- // 3. skip setting modelDictionary["blog"]["title"], because `skipColumn` has been set for "blog".
117
- if field. isPrimaryKey && modelValue == nil {
118
- let keyPathParent = column. dropLastPath ( )
119
- skipColumns. insert ( keyPathParent)
127
+ let targetBinding = foreignKey. flatMap {
128
+ getValue ( from: element, by: path + [ $0] )
120
129
}
130
+ return DataStoreModelDecoder . lazyInit ( identifier: targetBinding)
121
131
122
- if skipColumns. isEmpty {
123
- modelDictionary. updateValue ( modelValue, forKeyPath: column)
124
- } else {
125
- let keyPathParent = column. dropLastPath ( )
126
- if !skipColumns. contains ( keyPathParent) {
127
- modelDictionary. updateValue ( modelValue, forKeyPath: column)
128
- }
132
+ case let ( . model( modelName) , true ) :
133
+ guard let modelSchema = getModelSchema ( for: modelName, with: statement)
134
+ else {
135
+ return nil
129
136
}
130
137
131
- // create lazy list for "many" relationships
132
- // this code only executes once when the `id` is the primary key of the current model
133
- // and runs in an iteration over all of the associations
134
- // For example, when the value is the `id` of the Blog, then the field.isPrimaryKey is satisfied.
135
- // Every association of the Blog, such as the has-many Post is populated with the List with
136
- // associatedId == blog's id. This way, the list of post can be lazily loaded later using the associated id.
137
- if let id = modelValue as? String ,
138
- ( field. name == ModelIdentifierFormat . Custom. sqlColumnName || // this is the `@@primaryKey` (CPK)
139
- ( schema. primaryKey. fields. count == 1 // or there's only one primary key (not composite key)
140
- && schema. primaryKey. indexOfField ( named: field. name) != nil ) ) { // and this field is the primary key
141
- let associations = schema. fields. values. filter {
142
- $0. isArray && $0. hasAssociation
143
- }
144
- let prefix = column. replacingOccurrences ( of: field. name, with: " " )
145
- associations. forEach { association in
146
- let associatedField = association. associatedField? . name
147
- let lazyList = DataStoreListDecoder . lazyInit ( associatedId: id,
148
- associatedWith: associatedField)
149
- let listKeyPath = prefix + association. name
150
- modelDictionary. updateValue ( lazyList, forKeyPath: listKeyPath)
151
- }
138
+ return try convert (
139
+ row: element,
140
+ withSchema: modelSchema,
141
+ using: statement,
142
+ eagerLoad: eagerLoad,
143
+ path: path + [ field. name] )
144
+
145
+ default :
146
+ let value = getValue ( from: element, by: path + [ field. name] )
147
+ return try SQLiteModelValueConverter . convertToSource ( from: value, fieldType: field. type)
148
+ }
149
+ }
150
+
151
+ private func getModelSchema( for modelName: ModelName , with statement: SelectStatement ) -> ModelSchema ? {
152
+ return statement. metadata. columnMapping. values. first { $0. 0 . name == modelName } . map { $0. 0 }
153
+ }
154
+
155
+ private func convertCollection( field: ModelField , schema: ModelSchema , from element: Element , path: [ String ] ) -> Any ? {
156
+ if field. isArray && field. hasAssociation,
157
+ case let . some( . hasMany( associatedFieldName: associatedFieldName) ) = field. association
158
+ {
159
+ let primeryKeyName = schema. primaryKey. isCompositeKey
160
+ ? ModelIdentifierFormat . Custom. sqlColumnName
161
+ : schema. primaryKey. fields. first. flatMap { $0. name }
162
+ let primeryKeyValue = primeryKeyName. flatMap { getValue ( from: element, by: path + [ $0] ) }
163
+
164
+ return primeryKeyValue. map {
165
+ DataStoreListDecoder . lazyInit ( associatedId: String ( describing: $0) , associatedWith: associatedFieldName)
152
166
}
153
167
}
154
168
155
- // the `skipColumns` is sorted from longest to shortest since we eager load all belongs-to, for the example of
156
- // a Comment, that belongs to a Post, that belongs to a Blog. Comment has nil Post and nil Blog. The
157
- // `skipColumns` will be populated as
158
- // - skipColumns["post"]
159
- // - skipColumns["post.blog"]
160
- //
161
- // By ordering it as "post.blog" then "post", before setting the `nil` values for those keypaths, then the
162
- // shortest keypath will the last key in the `modelDictionary`. modelDictionary["post"]["blog"] will be
163
- // replaced with modelDictionary["post"] = nil.
164
- //
165
- // If there does exist a post but no blog on the post, then the following `skipColumns` would be populated:
166
- // - skipColumns["post.blog"] (since only primary key for blog does not exist, post object exists)
167
- // and the resulting modelDictionary will be populated:
168
- // modelDictionary["post"]["id"] = <ID>
169
- // modelDictionary["post"][<remaining required/optional fields of post>] = <values>
170
- // modelDictionary["post"]["blog"] = nil
171
- let sortedColumns = skipColumns. sorted ( by: { $0. count > $1. count } )
172
- for skipColumn in sortedColumns {
173
- modelDictionary. updateValue ( nil , forKeyPath: skipColumn)
169
+ return nil
170
+ }
171
+
172
+ private func getTargetNames( assoication: ModelAssociation ) -> [ String ] {
173
+ switch assoication {
174
+ case let . hasOne( associatedFieldName: _, targetNames: targets) :
175
+ return targets
176
+ case let . belongsTo( associatedFieldName: _, targetNames: targets) :
177
+ return targets
178
+ case . hasMany:
179
+ return [ ]
174
180
}
175
- modelDictionary [ " __typename " ] = modelSchema. name
176
-
177
- // `foreignKeyValues` are all foreign keys and their values that can be added to the object for lazy loading
178
- // belongs to associations.
179
- if !eagerLoad {
180
- for foreignKeyValue in foreignKeyValues {
181
- let foreignColumnName = foreignKeyValue. 0
182
- if let foreignModelField = modelSchema. foreignKeys. first ( where: { modelField in
183
- modelField. sqlName == foreignColumnName
184
- } ) {
185
- modelDictionary [ foreignModelField. name] = DataStoreModelDecoder . lazyInit ( identifier: foreignKeyValue. 1 )
186
- }
187
- }
181
+ }
182
+
183
+ private func getValue( from element: Element , by path: [ String ] ) -> Binding ? {
184
+ return columnNames. firstIndex { $0 == path. fieldPath }
185
+ . flatMap { element [ $0] }
186
+ }
187
+
188
+ private func schemeMetadata( schema: ModelSchema , element: Element , path: [ String ] ) -> ModelValues {
189
+ var metadata = [
190
+ " __typename " : schema. name,
191
+ ]
192
+
193
+ if schema. primaryKey. isCompositeKey,
194
+ let compositeKey = getValue ( from: element, by: path + [ ModelIdentifierFormat . Custom. sqlColumnName] )
195
+ {
196
+ metadata. updateValue ( String ( describing: compositeKey) , forKey: ModelIdentifierFormat . Custom. sqlColumnName)
188
197
}
189
-
190
- // swiftlint:disable:next force_cast
191
- return modelDictionary as! ModelValues
198
+
199
+ return metadata
192
200
}
193
201
}
194
202
@@ -285,3 +293,9 @@ extension String {
285
293
}
286
294
}
287
295
}
296
+
297
+ extension Array where Element == String {
298
+ var fieldPath : String {
299
+ self . filter { !$0. isEmpty } . joined ( separator: " . " )
300
+ }
301
+ }
0 commit comments