-
Notifications
You must be signed in to change notification settings - Fork 199
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(datastore): eager loaded associations depth #520
Conversation
let columnMapping: ColumnMapping | ||
let bindings: [Binding?] | ||
|
||
// TODO remove additionalStatements once sorting support is added to DataStore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe create a github issue for this TODO to track?
let bindings: [Binding?] | ||
|
||
// TODO remove additionalStatements once sorting support is added to DataStore | ||
static func get(from modelType: Model.Type, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we rename to something different than 'get'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure thing, suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this just be the constructor for SelectStatementMetadata
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make metaData(from:)
, statementMetaData(from:)
or why not an init method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this just be the constructor for
SelectStatementMetadata
?
using the constructor is tricky because I have to execute the entire logic inside the constructor (no other functions, etc) because I need to initialize any struct property.
That's why I went with the static factory function, otherwise the constructor becomes a mess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was also thinking .selectStatementMetadata(from:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's an internal API, so I renamed to .metadata()
, feel free to change that later as it has no impact on customers.
|
||
/// Walk through the associations recursively to generate join statements. | ||
/// | ||
/// Implementation note: this should be revisited once we define support |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are doing eager loading now right? Let us add that to the comment.
internal extension Dictionary where Key == String, Value == Any? { | ||
|
||
/// Utility to create a `NSMutableDictionary` from a Swift `Dictionary<String, Any?>`. | ||
func mutableCopy() -> NSMutableDictionary { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this name should be something like nsMutableCopy... because we are converting a data structure (dictionary) to an NS data structure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure... but doesn't the return type make that clear?
/// - value: the value to be set | ||
/// - key: the key as a `String` | ||
func updateValue(_ value: Value?, forKey key: String) { | ||
setObject(value as Any, forKey: key as NSString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If value is nil, i'm not sure if this works, because I don't think NSDictionaries can hold nil
(i believe in obj-c we'd have to set it to [NSNull null]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, fixing it!
/// - Parameters: | ||
/// - value: the value to be set | ||
/// - keyPath: the key path as a `String` (e.g. "nested.key") | ||
func updateValue(_ value: Value?, forKeyPath keyPath: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm stupid question: what if we have something like nested..key
. will keyComponents
be 2 or 3? Maybe there's some extra sanity checks on the data that is being passed in here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's an invalid keyPath
. We could add sanity checks, but it wouldn't make a difference because this is used in a controlled environment. The keyPath
passed here is created by SelectStatement
, is select statement creates an invalid key, those tests will fail. If tests don't catch it, the SQL operation will fail before it gets here.
but good question, I don't know how https://developer.apple.com/documentation/objectivec/nsobject/1418139-setvalue behaves in those cases. It would be good to mimic its behavior
joinStatements.append(""" | ||
\(joinType) join \(associatedTableName) as "\(alias)" | ||
on \(associatedColumn) = \(foreignKeyName) | ||
""") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Being ultra paranoid, do we care to filter out names of tables that might impact these statements. Like... it would be bad if someone had a table called Null
or as
(or something malicious)... maybe something to do for another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they are escaped with double quotes, so you can even pass a SQL statement there and nothing would happen.
that said, the names here are a result of the models, so only valid Swift identifiers can be passed here.
let bindings: [Binding?] | ||
|
||
// TODO remove additionalStatements once sorting support is added to DataStore | ||
static func get(from modelType: Model.Type, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make metaData(from:)
, statementMetaData(from:)
or why not an init method?
paginationInput: QueryPaginationInput? = nil, | ||
additionalStatements: String? = nil) { | ||
self.modelType = modelType | ||
self.metadata = .get(from: modelType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot happens here in the init method, and I think this code path is sync with the developers code Amplify.DataStore.query
. Is this an expensive logic that can affect the query api?
In future we should add some performance test around this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to performance benchmarks
Is this an expensive logic that can affect the query api?
No, it's not expensive. It generates a string based on a Swift struct. Of course there are tons of optimizations we could do, like caching the queries, etc. But as of now I have no concerns.
AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/SQLStatement+Select.swift
Outdated
Show resolved
Hide resolved
/// - Returns: an array of `Model` of the specified type | ||
func convert<M: Model>(to modelType: M.Type) throws -> [M] | ||
func convert<M: Model>(to modelType: M.Type, | ||
using statement: SelectStatement) throws -> [M] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can just pass over ColumnMapping
instead of SelectStatement
AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/Statement+Model.swift
Outdated
Show resolved
Hide resolved
let associations = schema.fields.values.filter { | ||
$0.isArray && $0.hasAssociation | ||
} | ||
let prefix = field.name.replacingOccurrences(of: field.name, with: "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the prefix here? isn't it just ""?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it depends on how many levels deep you are in the object graph. It starts with ""
, it becomes "post."
then "post.blog."
Issue #503
The associations fetching code (inner/left joins) was only fetch one level deep, causing issues when decoding into Swift structs that had required properties that were missing from the SQL result set.
This change not only simplifies a lot the result set parsing, but also adds recursion to find all required associations of a given model to make sure data contracts will be fulfilled when using Swift decode.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.