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 bug with new nested where filters #8005

Closed
wants to merge 5 commits into from

Conversation

tjhiggins
Copy link

@tjhiggins tjhiggins commented Aug 2, 2021

Description of change

Found a bug in the most recent nested where filter feature:
#7805

When combining nested primary keys and non-primary keys the query builder would build an incorrect query.
Included a test case that illustrates the issue.

fixes #8082

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • npm run lint passes with this change
  • npm run test passes with this change
  • There are new or updated unit tests validating the change
  • The new commits follow conventions explained in CONTRIBUTING.md

Query builder would generate an incorrect query
If you mixed primary and non-primary keys
@tjhiggins
Copy link
Author

tjhiggins commented Aug 2, 2021

@imnotjames how do I check if an entity is a transformed class vs a plain filter object? struggling with the differences between:

const foundChild = await manager.findOne(Child, { parent: loadedParent });

// Expected: where parent.id = ?

and

.where({
    post: {
        category: {
            id: In([1, 2, 3]),
            name: In(["Foo", "Bar", "Baz"]),
        }
    }
})

// Expected: where category.id in (?, ?, ?) and name in (?, ?, ?)
// Use case: permissions filter on ids, but want to search on name

Edit:
Found what I was looking for:

const isEntityTarget = relation.inverseEntityMetadata.target === entity[key].constructor;

Running tests again now.

@tjhiggins
Copy link
Author

@imnotjames The node 14 test failed due to a circleci issue - could you please re-run?

@imnotjames
Copy link
Contributor

Doesn't the test pass if you write the code as

const isEntityTarget = relation.inverseEntityMetadata.target === entity[key].constructor;

if (isEntityTarget) {
    // .... shortcuts for entity target
}

// If nothing else, just return every property that's being passed to us.
const subPaths = this.createPropertyPath(relation.inverseEntityMetadata, entity[key])
    .map(p => `${path}.${p}`);
paths.push(...subPaths);

@tjhiggins
Copy link
Author

Doesn't the test pass if you write the code as

const isEntityTarget = relation.inverseEntityMetadata.target === entity[key].constructor;

if (isEntityTarget) {
    // .... shortcuts for entity target
}

// If nothing else, just return every property that's being passed to us.
const subPaths = this.createPropertyPath(relation.inverseEntityMetadata, entity[key])
    .map(p => `${path}.${p}`);
paths.push(...subPaths);

I tried that after I added the isEntityTarget check, but a couple tests failed. Realizing now that those tests might be invalid now and it now generates the correct sql. Let me know if I'm wrong:

6de9327#diff-fc53d8864169b2e2d0b175dc446c164524f59407c457d6b81a0fa1c004d8ca5cR206

Updated with the simple solution and changed those tests.

@tjhiggins
Copy link
Author

Nvm the simple solution doesn't play nice with updates:

1) embedded > embedded-many-to-one-case1
       owner side
         should insert, load, update and remove entities with embeddeds when embedded entity having ManyToOne relation2:
     EntityColumnNotFound: No entity column "counters.likedUser.id" was found.

@tjhiggins
Copy link
Author

@imnotjames Ready for another look. Would love your feedback on a better check for hasOnlyPrimaryKeys. Also please note that a couple tests that used to fail now pass - I'm not 100% if they are correct.

@imnotjames imnotjames self-requested a review August 4, 2021 23:44
@@ -11,7 +11,7 @@ async function createDotenvFiles() {
}

async function createYamlFiles() {
await fs.mkdir(path.join(__dirname, "configs/yaml"));
await fs.mkdir(path.join(__dirname, "configs/yaml"), { recursive: true });
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this change needed?

Copy link
Author

Choose a reason for hiding this comment

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

I wasn't able to run test-fast multiple times in a row. It would complain that the directory already existed.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a bigger problem that our tests have than just this. Can you omit this change for now just to simplify this PR & what it touches? If we have to revert or find the cause of something it makes it a whole lot easier if it's one PR for one change.

Comment on lines +1035 to +1037
if (relation.relationType === "one-to-many" || relation.relationType === "many-to-many") {
throw new Error(`Cannot query across ${relation.relationType} for property ${path}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Pretty sure this needs to remain outside the if (isEntityTarget || hasOnlyPrimaryKeys) { ... } block as it's unrelated to the shortcuts taken. We just do not currently support this kind of query at all.

Comment on lines +1017 to +1034
// In the case of a *-to-one, there's only ever one possible entity on the other side
// so if the join columns are all defined we can return just the relation itself
// because it will fetch only the join columns and do the lookup.
if (relation.relationType === "one-to-one" || relation.relationType === "many-to-one") {
const joinColumns = relation.joinColumns
.map(j => j.referencedColumn)
.filter((j): j is ColumnMetadata => !!j);

const hasAllJoinColumns = joinColumns.length > 0 && joinColumns.every(
column => column.getEntityValue(entity[key], false)
);

if (hasAllJoinColumns) {
paths.push(path);
continue;
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Does the hasOnlyPrimaryKeys actually apply here? Isn't it just in the case of an isEntityTarget?
Can this ever apply with hasOnlyPrimaryKeys?

Copy link
Author

Choose a reason for hiding this comment

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

I believe you're right and I can have this block only apply to isEntityTarget.

if (value) {
seenPrimaryColumns.add(columnName);
} else {
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we know this inner one is invalid, we know they all are. Do we want to break further up?

const seenPrimaryColumns = new Set<string>();
for (const primaryColumn of primaryColumns) {
const entityValueMap = primaryColumn.getEntityValueMap(entity[key]);
for (const [columnName, value] of Object.entries(entityValueMap || {})) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What about embedded entity primary keys? Is that a concern here?

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure - happy to hand off this pr to you if its easier.

Otherwise please point me to docs and I can try to add a test case.

for (const primaryColumn of primaryColumns) {
const entityValueMap = primaryColumn.getEntityValueMap(entity[key]);
for (const [columnName, value] of Object.entries(entityValueMap || {})) {
if (value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is null a valid PK value? I know 0 is. So is an empty string. Is that something we should be handling here? Only checking for undefined instead?

}
}
}
const hasOnlyPrimaryKeys = primaryColumns.length > 0 && seenPrimaryColumns.size === Object.keys(entity[key]).length;
Copy link
Contributor

Choose a reason for hiding this comment

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

If it's only primary columns do we actually care if it hits the optimizations?

In theory, it would be the same either way, wouldn't it?

Copy link
Author

Choose a reason for hiding this comment

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

The update flow uses this method and fails if you pass contact.id vs contactId.
ex. #8005 (comment)
That's the reason I added the hasOnlyPrimaryKeys check back.

@pleerock
Copy link
Member

Closing in favour of #8082

@pleerock pleerock closed this Feb 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cannot Query PK with FindOperator and other values correctly
3 participants