-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Find, Destroy or Update methods ignore undefined type attributes #4639
Comments
@Josebaseba Thanks for posting, we'll take a look as soon as possible. For help with questions about Sails, click here. If you’re interested in hiring @sailsbot and her minions in Austin, click here. |
I found the same issue before. Good thing it was not in prod. |
We just updated an entire database collection on accident IN prod due to this. |
This is intentional, but it's not too late to change the behavior for v1, so let's definitely discuss. tldr; I wrote a big long thing, so if you don't have time to read it all, please just skip down to "Questions" below :) Why it works this way nowThe "newRecord" and "valuesToSet" side of things have always done this (stripped keys with undefined values). I realize this isn't what we're talking about here-- just making sure that's clear from the get-go so we're on the same page (and for anyone stumbling on this issue down the road.) For criteria (and specifically for the That might be for convenience within your code-- e.g. letting us do this: // Look up all facilities if the logged-in user is an admin,
// otherwise look up just the facilities she owns.
// (Also, if the name search or country filters were specified, apply them)
var facilities = await Facility.find({
owner: loggedInUser.isAdmin ? undefined : this.req.session.userId,
name: inputs.name ? { contains: inputs.name } : undefined,
country: inputs.country ? { contains: inputs.country } : undefined,
isSoftDeleted: false
}); Instead of this (having to build the // Look up all facilities if the logged-in user is an admin,
// otherwise look up just the facilities she owns.
// (Also, if the name search or country filters were specified, apply them)
var constraintsByAttrName = {
isSoftDeleted: false
};
if (!loggedInUser.isAdmin) {
constraintsByAttrName.owner = loggedInUser.id;
}
if (inputs.name) {
constraintsByAttrName.name = inputs.name;
}
if (inputs.country) {
constraintsByAttrName.country = inputs.country;
}
var facilities = await Facility.find(constraintsByAttrName); We could try refactoring it a bit, which makes the code more concise -- but doing so makes it considerably more opaque, since you still have to address the non-fungibility of // Look up all facilities if the logged-in user is an admin,
// otherwise look up just the facilities she owns.
// (Also, if the name search or country filters were specified, apply them)
var constraintsByAttrName = {
owner: loggedInUser.isAdmin ? undefined : this.req.session.userId,
name: inputs.name ? { contains: inputs.name } : undefined,
country: inputs.country ? { contains: inputs.country } : undefined,
isSoftDeleted: false
};
_.each(_.keys(constraintsByAttrName), (attrName)=>{
if (constraintsByAttrName[attrName] === undefined) {
delete constraintsByAttrName[attrName];
}
});
var facilities = await Facility.find(whereClause); You probably also noticed in the example above that part of the reason it got way worse was because our use case involved an optional "AND" filter as a parameter. That's where this makes the most difference, it seems. Here's another example of where relying on undefined-stripping saves code complexity / development time / frustration: {
description: 'Archive posts more than 45 days old (within the additional geographical boundaries, if specified.)',
inputs: {
minLat: { type: 'number' },
maxLat: { type: 'number' },
minLong: { type: 'number' },
maxLong: { type: 'number' },
},
fn: async function(inputs, exits){
await Post.update({
createdAt: { '<': Date.now()-(1000*60*60*24*45) },
minLat: inputs.minLat,
maxLat: inputs.maxLat,
minLong: inputs.minLong,
maxLong: inputs.maxLong,
}).set({ isArchived: true });
return exits.success();
}
} As you can imagine, the above code would be a lot longer if we couldn't rely on the stripping-out of undefined keys in criteria. (This is definitely less of a big deal now that we can take advantage of async/await -- but it's still a material advantage that helps keeps code more concise.) Of course, we have to contrast that with the downsides. ThoughtsAll that said, I've had similar experiences to what @Josebaseba @Xandrak and @luislobo are describing, and I think some kind of configurable warning, or failing with an error (e.g. assuming this was disable-able using We'd also need to think through how any change here would affect recursive parsing-- or possibly just try adding the warning and then try a ton of different things (currently we're relying on this rhs-stripping logic in the generic One last thing on that: There are a few other cases where the
Here's a more concrete example: // If labels array turns out to be empty, it's treated as a "void" term,
// and thus (since it's at the top level, i.e. an "and") no issues are matched.
await Issue.find({
primaryLabel: { in: _.without(inputs.labels, 'bug') }
}); // Similar to above, but in this case, it's more clear how its treatment depends on the context:
//
// In this case, if the array passed to `in` turns up empty, it's still understood as void,
// which then collapses that disjunct in the "or" predicate into `false`.
// Since `false` is the same thing as the void term, it matches _nothing_.
//
// But this time, since we're in an "or" context instead an "and", we don't make the
// entire `where` clause void. Instead, this acts like `||false`, which means it's effectively
// disregarded and pruned out of the "or" predicate, leaving only the second disjunct.
//
// So at the end of the day, if the array provided for `in` turns up empty, we end up
// fetching _all_ issues that were created by user #13.
await Issue.find({
or: [
{ primaryLabel: { in: _.without(inputs.labels, 'bug') } },
{ creator: 13 }
]
}); // Here's another one. In this case, you only fetch issues that WEREN'T
// created by members of the specified organization. But if the only users
// that exist are members of the specified organization (or perhaps if there're
// no users at all), then this ends up as an empty array. Since that's inside of
// a "nin", rather than an "in", it is understood as "universal" rather than "void".
//
// In other words, since there are no users in the specified org, excluding those
// users is basically the same as matching any user-- any "creator".
// Furthermore, in this case, since we're effectively inside an "and"
// (b/c we're at the top level of the "where" clause), the universal term is
// understood like `&&true` -- and effectively ignored.
//
// So at the end of the day, all issues that have `isDeleted: false` are matched
// and returned.
var orgMembers = await User.find({ memberOf: inputs.organizationId });
await Issue.find({
isDeleted: false,
creator: { nin: _.pluck(orgMembers, 'id') }
}); // Finally, this example demonstrates how all the pieces fit together,
// as well as how the stripping of `undefined` values works within
// "and"/"or" predicates, and within "in"/"nin" constraints.
//
// This example matches all issues whose primary label is one of the specified
// labels (except "bug"), and also matches any other issues which were
// created by the logged-in user. If there is no logged-in user, then the `undefined`
// is ignored. (In an "or" predicate, an `undefined` disjunct operates like `||false`.
// In an "and" predicate, an `undefined` conjunct operates like `&&true`. i.e. in
// either case, it is ignored.)
await Issue.find({
or: [
{ primaryLabel: { in: _.without(inputs.labels, 'bug') } },
this.req.session.userId ? { creator: this.req.session.userId } : undefined
]
}); Questions@luislobo @Xandrak @Josebaseba @sgress454:
|
Hi @mikermcneil, thanks for the answer it's good to know that this is an intentional feature and not a bug. First, answering your question:
So, I'll add an option in the |
I like @Josebaseba suggestion, but, in addition to that, I would add an exception whenever a "value" in a where clause is undefined, in the case |
@mikermcneil @Josebaseba @luislobo The explanation makes sense to me, but I don't count :P. As far as impact, I've only experienced the problem when running an After mulling this over for quite some time, I don't think that the answer is to strip out When I see this example:
I don't intuitively read it as, "If the logged in user is an admin, then find all facilities". I read it as, "If the logged in user is an admin, then find all facilities that don't have an owner". Casting undefined values to the proper base value would support that. It would mean an extra step if you did intend to find all facilities when the user is an admin, but I'd argue that it's worth it for the clarity provided in all cases. I took some time to look into how various other software handles this case, and it's all over the map. The Mongo client app returns an error if you run a query with an undefined value, but the Node driver seems to translate it into an
and you'll just always get zero results (even if there are records where So, good arguments all around. Casting to the base value is the most intuitive for me and seems like the most consistent thing with our scheme in general, and has the benefit of not throwing errors which might not be seen until production. And in debugging, in my recent experience the question of "why didn't any rows update?" is a lot easier to figure out then, "why did ALL of my rows update"? |
@sgress454 That is valid, unless, you do have values that are NULL, and are affected by your update, but you didn't intend to update those. In the case of an I don't think that doing any smart thing/asumption about an Although we do Pull Requests review and even sometimes Pair review, not all developers are senior or "awake" 100% of the time... Errors happen, validations are skipped, data is deleted. Massive Devops suicide happen. |
Also, databases might even handle NULL differently, depending on it's configuration (ansinull, in some of them) so, it's not that easy neither to assume something like that. Some references http://sqlanywhere-forum.sap.com/questions/9285/is-null-vs-isnull-vs-null-correct-usage http://dcx.sybase.com/index.html#1201/en/dbreference/nulls.html https://www.postgresql.org/docs/8.3/static/functions-comparison.html |
@luislobo Throwing errors when undefined values are detected is certainly the safest way to go. If we were out of beta I'd be more hesitant since it'd be a significant breaking change, but in this case, I'm on board. So it sounds like the current proposal is:
Examples:
|
@mikermcneil @sgress454 Thanks for taking the time to address this! Let me clarify that it was actually my code error that brought this to light--unfortunately for us, it was in production. So I am not blaming Sails or anything. This could have been avoided with a subtle switch from using a ...
// code before fix
model.find(findThis)
.then((foundModel) => {
model.update({ id: foundModel.id }, updateModelValues)
})
...
// code after fix
model.find(findThis)
.each((foundModel) => {
model.update({ id: foundModel.id }, updateModelValues)
})
... We are now wrapping every update/destroy in our API within a check to make sure the criteria passed in is never At any rate, we did not, however, expect the default behavior for an @mikermcneil To answer your previous questions:
@Josebaseba @luislobo Thanks for your input and bringing this issue to light. |
@sgress454 I agree with your last comment. |
@mikermcneil @sgress454 guys, this is a big deal, right now my production DB got all the collection updated at the same time, even if I went query by query making sure that everything was under control and right now I'm screwed... Is there anything that I can do to turn off this "feature"? |
@Josebaseba sorry to hear the upgrade is causing you trouble. Thanks for following up. We're relying on undefined-stripping in "where" clauses a lot, and in new code, it definitely saves time without introducing problems (it reduces the complexity of writing code to do optional filters quite a lot). Thus I'm still not convinced that we should change this default behavior. That said, I understand you might need an immediate change to help through the upgrade. I like @sgress454's proposal, with three tweaks: @Josebaseba if you've got a more immediate-term need and can't wait for the new model setting (it'll need to be added, tested, and documented), I think the best bet is to modify the code in normalize-where-clause I highlighted above and use that.
|
(BTW since that's such a mouthful, let's also, in parallel, down the road, see about coming up with a definition of what we want "strict mode" to look like in the future. It might make sense to lump a few similar features with the same motivations into that bucket) |
@mikermcneil I like it making it a model setting, and defaulting to NOT allow the stripping of undefined conjuncts. |
Thanks for the quick answer Mike, I like the idea of making it a model setting too. I'm gonna try to modify the code with a fork now. Luckily I found the buggy entry point, now I have to figure out how to modify all the documents back to their previous values changing the DB as little as possible. Another case that as a dev we must be careful with this feature: // req.param('id') === 10
if (something) { res.ok(); } // we miss here a return
// req.param('id') -> undefined
User.update({id: req.param('id')}, {someData}) This wasn't my case but I found that issue while I was developing, it can be tricky. Our issue was related with the fact that the findOne doesn't work anymore if it finds more than one value so after that error, a bad update and... 💥 💥 Anyway, thanks guys! |
@Josebaseba,@sailsbot,@luislobo,@Xandrak,@mikermcneil,@sgress454: Hello, I'm a repo bot-- nice to meet you! It has been 30 days since there have been any updates or new comments on this page. If this issue has been resolved, feel free to disregard the rest of this message and simply close the issue if possible. On the other hand, if you are still waiting on a patch, please post a comment to keep the thread alive (with any new information you can provide). If no further activity occurs on this thread within the next 3 days, the issue will automatically be closed. Thanks so much for your help! |
@mikermcneil bump |
@Josebaseba,@sailsbot,@luislobo,@Xandrak,@mikermcneil,@sgress454: Hello, I'm a repo bot-- nice to meet you! It has been 30 days since there have been any updates or new comments on this page. If this issue has been resolved, feel free to disregard the rest of this message and simply close the issue if possible. On the other hand, if you are still waiting on a patch, please post a comment to keep the thread alive (with any new information you can provide). If no further activity occurs on this thread within the next 3 days, the issue will automatically be closed. Thanks so much for your help! |
ping ping ping |
@mikermcneil @sgress454 I didn't follow all the latest commits in Waterline (and friends), but may be you already handled what we have been discussing here? |
@Josebaseba,@sailsbot,@luislobo,@Xandrak,@mikermcneil,@sgress454: Hello, I'm a repo bot-- nice to meet you! It has been 30 days since there have been any updates or new comments on this page. If this issue has been resolved, feel free to disregard the rest of this message and simply close the issue if possible. On the other hand, if you are still waiting on a patch, please post a comment to keep the thread alive (with any new information you can provide). If no further activity occurs on this thread within the next 3 days, the issue will automatically be closed. Thanks so much for your help! |
bump! @mikermcneil Is there anything I can do to help with this one? I don't know its current status, though |
@Josebaseba,@sailsbot,@luislobo,@Xandrak,@mikermcneil,@sgress454: Hello, I'm a repo bot-- nice to meet you! It has been 30 days since there have been any updates or new comments on this page. If this issue has been resolved, feel free to disregard the rest of this message and simply close the issue if possible. On the other hand, if you are still waiting on a patch, please post a comment to keep the thread alive (with any new information you can provide). If no further activity occurs on this thread within the next 3 days, the issue will automatically be closed. Thanks so much for your help! |
bump! |
Hi @Josebaseba, @luislobo, @Xandrak, @mikermcneil, @sgress454 I've opened balderdashy/waterline#1545 which is an attempt at implementing the solution as outlined by @mikermcneil Please have a look and let me know what you think! |
Hey @davidreghay it looks good to me. Thanks! |
Thanks y'all-- slammed today but will take a look asap |
@Josebaseba,@sailsbot,@luislobo,@Xandrak,@mikermcneil,@sgress454,@davidreghay: Hello, I'm a repo bot-- nice to meet you! It has been 30 days since there have been any updates or new comments on this page. If this issue has been resolved, feel free to disregard the rest of this message and simply close the issue if possible. On the other hand, if you are still waiting on a patch, please post a comment to keep the thread alive (with any new information you can provide). If no further activity occurs on this thread within the next 3 days, the issue will automatically be closed. Thanks so much for your help! |
I was curious to see what the best practices around this issue are in other Node ORM's. I made a simple test of 6 ORM packages, including Waterline, and included the results below. Waterline:var users = await User.update({ 'id': undefined }, { lastName: 'Spaghetti' }).fetch(); update `user` set `lastName` = 'Spaghetti' limit 9007199254740991 Loopback:User.updateAll({id: undefined}, {name: 'Fred'}, function(err, info) {}); UPDATE `User` SET `name`='Fred' Same behavior as Waterline, intentionally changes {id: undefined} to {}. Configuration option "normalizeUndefinedInQuery" available to modify this behavior. Sequelize:await User.update({ favorite_color: 'red' }, { where: { username: undefined } }); UPDATE `users` SET `favorite_color`='red',`updatedAt`='2018-06-02 13:22:51' WHERE `id` = NULL Bookshelf (Knex Query Builder):await User.where({ 'id': undefined }).save(
{ name: 'Fred' },
{ method: 'update', patch: true }
);
TypeORM:await getConnection()
.createQueryBuilder()
.update(User)
.set({ firstName: "Mickie", lastName: "Mouse" })
.where("id = :id", { id: undefined })
.execute(); UPDATE `user` SET `firstName` = 'Mickie', `lastName` = 'Mouse' WHERE `id` = NULL Node-ORM2 (No longer actively maintained):Person.find({ id: undefined }, function (err, people) {}); SELECT `name`, `surname`, `age`, `male`, `id` FROM `person` WHERE `id` IS NULL |
Hi @Josebaseba, @luislobo, @sreed101 and @davidreghay, |
@raqem good news! Which is going to be the repo? balderdashy/sails? or some other one? |
@luislobo - They will all be located in balderdashy/sails - yes. 👍 |
What's the current status of this issue? I've just been bitten by |
For anyone still following this, I've forked I intend to keep that lib and the underlying fork of @mikermcneil I'd be interested in hearing your thoughts on the approach in that fork (visible at balderdashy/waterline@master...alxndrsn:master); if these are of interest then I'd be happy to fix them up for a PR into the official |
@mikermcneil @raqem This is a very big change and any repos migrating from 0.12 to 1.0 could completely miss this and have major issues with the database. There is no mention of this issue in 1.0 Migration Docs. |
Thanks for the heads up @himankpathak, we've updated our migration docs to include this information. |
@eashaw I'm also facing this error with mysql db, any help |
Hi @latifasaee, are you using |
Waterline version: 0.13.0-rc11
Node version: 4.5
NPM version: 2.15.9
Operating system: MacOs
Hi guys, I found an issue that I'm not sure if it's a bug or something that you have considered. If you do something like this:
This is gonna update ALL the users in the database, because the mongo query is gonna be something like this {}. The same thing happens with find/destroy methods.
So imagine that in your code you don't realize data X value is undefined and you lauch a destroy against it, you'll end up dropping all the collection with no clue about what just happened.
Thanks for your time.
The text was updated successfully, but these errors were encountered: