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(datastore): eager loaded associations depth #520

Merged
merged 10 commits into from Jun 5, 2020

Conversation

drochetti
Copy link
Contributor

@drochetti drochetti commented Jun 4, 2020

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.

@drochetti drochetti added the datastore Issues related to the DataStore category label Jun 4, 2020
@drochetti drochetti marked this pull request as ready for review June 4, 2020 19:27
@drochetti drochetti requested review from lawmicha and wooj2 June 4, 2020 19:27
let columnMapping: ColumnMapping
let bindings: [Binding?]

// TODO remove additionalStatements once sorting support is added to DataStore
Copy link
Member

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,
Copy link
Member

@royjit royjit Jun 5, 2020

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'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing, suggestions?

Copy link
Member

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?

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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:)

Copy link
Contributor Author

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
Copy link
Member

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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]?

Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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

Comment on lines +111 to +114
joinStatements.append("""
\(joinType) join \(associatedTableName) as "\(alias)"
on \(associatedColumn) = \(foreignKeyName)
""")
Copy link
Contributor

@wooj2 wooj2 Jun 5, 2020

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.

Copy link
Contributor Author

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,
Copy link
Member

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,
Copy link
Member

@royjit royjit Jun 5, 2020

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.

Copy link
Contributor Author

@drochetti drochetti Jun 5, 2020

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.

/// - 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]
Copy link
Member

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

let associations = schema.fields.values.filter {
$0.isArray && $0.hasAssociation
}
let prefix = field.name.replacingOccurrences(of: field.name, with: "")
Copy link
Member

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 ""?

Copy link
Contributor Author

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."

@drochetti drochetti merged commit de04410 into master Jun 5, 2020
@drochetti drochetti deleted the fix/sql-statement-decoding branch June 5, 2020 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
datastore Issues related to the DataStore category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants