{} evaluating to true especially with where is confusing. We had cases which a frontend bug caused one variable to be undefined and the query was sent with where: {} instead of where: {key:value} and the entire table got cleared (acepsc).
where: {} matching all the rows.We need a special construct to match all the cases (that evaluates to true). An empty {} should mean nothing so that we can avoid accidental wiping out of the entire table using where: {}. This is bound to happen with frontend languages like JS and we should be a little forgiving there as it is our primary target.
Changing the behaviour of {} will be a huge breaking change. Instead we can try to solve the mutations case where this becomes an issue.
(delete|update)_table_by_pk top level field. These will automatically restrict the mutation's surface to one row.allow_mutating_all_rows in update and delete permissions. This will not allow where to be {} in mutations. How about adding a parameter to delete mutations that specifies the behaviour of where: {}? eg, allow_delete_all: boolean? if it defaulted to true it wouldn't change the existing behaviour.
There seems to be a few things going on together here. Calling a bulk delete with "where": {} is definitely asking to delete the whole dataset, but the real issue seems to be a) it鈥檚 too easy to do that by mistake generally, and b) specifically because JS sends {} when you meant { "foo": null } but foo was undefined instead of null.
If we鈥檙e using objects to encode sets of filters, I don鈥檛 see a way around this, keeping the API as it is. The onus seems to be on the user to sanitize the inputs.
If the alternative is changing the API, I definitely don鈥檛 think the solution is to change the meaning of {}. Some other possibilities:
"where": {}, and not turning {} into false. But even the flag is an incomplete solution. It鈥檚 better certainly, but I can imagine a case with something like "where": {"foo": true, bar: $bar}, where foo is never or very rarely false (say 1% of the rows) in the table, and then if bar is undefined, you still wipe out 99% of the rows.@paf31 That is a perfect summary! The best approach would be to make it harder for {} to happen in unexpected scenarios, without changing the semantics of {}.
In a brief discussion with @0x777 and @rikinsk the proposed next step is to work on preventing the automatic collapsing of where: {id: {_eq: null} } to where: {id: null} to where: {}.
The case where users don't sanitize their input and inadvertently create a where: {} in their query itself, is not something we can solve very well without changing the semantics of {}. For example, this might happen when the value is undefined: JSON.stringify({where: {id: {_eq: undefined}}}) becomes {where: {id: {}}} which evaluates to where id is true. At this point, Hasura doesn't know if this behaviour was intended or not.
馃憠 To fix: Disable collapsing of null into {} inside the Hasura BoolExp.
@0x777 If you can put together the RFC for this, then we can get started!
In a brief discussion with @0x777 and @rikinsk the proposed next step is to work on preventing the automatic collapsing of
where: {id: {_eq: null} }towhere: {id: null}towhere: {}.
That case sounds like a real bug to me though. Passing undefined is not so much collapsing as the value never getting encoded in the first place. If you encode null, I don't think it should be equivalent to passing nothing at all. If the query isn't meaningful, we could throw the equivalent of a type error.
I propose that we fix this as part of #4111. This would be a breaking change, though as has already been discussed, it seems just as likely that any situations where it comes up are bugs, not intended uses. I believe the potential for harm is significant enough that we should make the change, but if we receive reports of breakage during beta, we could add a temporary flag to re-enable the old behavior.
Our current solution in #4111 is more sophisticated than what has been described in this thread so far, and I believe it addresses the mentioned concerns in a neat way. Here鈥檚 the overview:
We do not change the behavior of where: {}鈥攊f you write that in a query, it is equivalent to no where argument at all.
Likewise, we do not change the behavior of where: {foo: {}}, which behaves the same as where: {}.
We do change the behavior of where: {foo: {_eq: null}}. This is now an error; it is not equivalent to omitting the _eq completely.
This is intensely strange from the perspective of the GraphQL type system, since the type of the _eq field remains nullable. As discussed above, it has to be, or it could not be omitted. But we reject any situations where a null value is actually passed with an error that says we expect a non-null value.
This behavior is explicitly permitted by the GraphQL specification. From the section on Input Objects:
If the value
nullwas provided for an input object field, and the field鈥檚 type is not a non鈥恘ull type, an entry in the coerced unordered map is given the valuenull. In other words, there is a semantic difference between the explicitly provided valuenullversus having not provided a value.
Emphasis mine. Again, this is completely bizarre from the point of view of the GraphQL type system, but it is completely logical from the point of view of the user! The user does not think of _eq as accepting a nullable column, they think of it as an optional argument that accepts a non-nullable column. The GraphQL type system treats these identically, but the specification gives us an out: we can鈥檛 give them different types, but we can give them different semantics.
This neatly solves the common problem where a variable accidentally ends up undefined, since we would now fail with an error, not delete the user鈥檚 database. At the same time, if someone explicitly writes code to omit the condition completely, we accept it. This seems like it鈥檚 clearly the right behavior to me.
A breaking change from "probably behavior you didn't intend" to "refuse to do sketchy thing; explain situation to user" seems safe to me!
This sounds great and certainly fits with how I'd be making the API calls in the .Net world. It would allow me to avoid writing code for generic checks to make sure no parameters are null or checking every one individually.
The behaviour as decribed in https://github.com/hasura/graphql-engine/issues/704#issuecomment-635571407 is now released in v1.3.4-beta.1
That kind of change in a minor version update seems dangerous to me. I think quite a few people will mindlessly upgrade those
Most helpful comment
I propose that we fix this as part of #4111. This would be a breaking change, though as has already been discussed, it seems just as likely that any situations where it comes up are bugs, not intended uses. I believe the potential for harm is significant enough that we should make the change, but if we receive reports of breakage during beta, we could add a temporary flag to re-enable the old behavior.
Our current solution in #4111 is more sophisticated than what has been described in this thread so far, and I believe it addresses the mentioned concerns in a neat way. Here鈥檚 the overview:
We do not change the behavior of
where: {}鈥攊f you write that in a query, it is equivalent to nowhereargument at all.Likewise, we do not change the behavior of
where: {foo: {}}, which behaves the same aswhere: {}.We do change the behavior of
where: {foo: {_eq: null}}. This is now an error; it is not equivalent to omitting the_eqcompletely.This is intensely strange from the perspective of the GraphQL type system, since the type of the
_eqfield remains nullable. As discussed above, it has to be, or it could not be omitted. But we reject any situations where anullvalue is actually passed with an error that says we expect a non-null value.This behavior is explicitly permitted by the GraphQL specification. From the section on Input Objects:
Emphasis mine. Again, this is completely bizarre from the point of view of the GraphQL type system, but it is completely logical from the point of view of the user! The user does not think of
_eqas accepting a nullable column, they think of it as an optional argument that accepts a non-nullable column. The GraphQL type system treats these identically, but the specification gives us an out: we can鈥檛 give them different types, but we can give them different semantics.This neatly solves the common problem where a variable accidentally ends up
undefined, since we would now fail with an error, not delete the user鈥檚 database. At the same time, if someone explicitly writes code to omit the condition completely, we accept it. This seems like it鈥檚 clearly the right behavior to me.