-
Notifications
You must be signed in to change notification settings - Fork 107
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
(>=.) and friends have unsound types with respect to Maybe #357
Comments
A note about a consequent API change to fixing all the boolean operators to properly model tristate: you would need to fix stuff like q =
from $ table @Catgirls
`leftJoin` table @Catears
`on` (\(cg :& earb) -> just cg.id ==. earb.owner) where I am increasingly thinking that perhaps the sound comparison operators should be introduced into a new module that would become part of the default set when Experimental is actually stabilized, and then you could migrate module by module, because I can already see how much this might suck for industry code. |
I think this is duplicate with #130 - the writeup here is much more detailed though. 😁 |
I think you're correct! Maybe we should close that one :p |
I was quite disappointed when I found out about this. Sometimes nullability is encoded in the type of an type SqlValue a = SqlExpr (Value a)
type SqlEntity a = SqlExpr (Entity a)
type SqlMaybeEntity a = SqlExpr (Maybe (Entity a))
-- | Equality test for a nullable value vs. non-nullable value.
-- | Returns false when the nullable value is null.
infix 4 ?==.
(?==.) :: PersistField t => SqlValue (Maybe t) -> SqlValue t -> SqlValue Bool
x ?==. y = x `nonNullAnd` (==. y)
-- | Flipped `?==.`, equality test for a nullable value vs. non-nullable value.
-- | Returns false when the nullable value is null.
infix 4 .==?
(.==?) :: PersistField t => SqlValue t -> SqlValue (Maybe t) -> SqlValue Bool
x .==? y = y `nonNullAnd` (==. x)
-- | Project an SqlExpr that may be null to an expression, returning TRUE for null cases.
-- | In other words, Data.Foldable.all for Values.
isNullOr
:: PersistField a
=> SqlExpr (Value (Maybe a))
-> (SqlExpr (Value a) -> SqlExpr (Value Bool))
-> SqlExpr (Value Bool)
isNullOr field p = isNothing_ field ||. p (unsafeFromJustValue field)
where
-- This is safe in this context because of short-circuiting of ||.
unsafeFromJustValue :: SqlExpr (Value (Maybe a)) -> SqlExpr (Value a)
unsafeFromJustValue = coerce
-- | Project an SqlExpr that may be null to an expression, returning FALSE for null cases.
-- | In other words, Data.Foldable.any for Values.
nonNullAnd
:: PersistField a
=> SqlExpr (Value (Maybe a))
-> (SqlExpr (Value a) -> SqlExpr (Value Bool))
-> SqlExpr (Value Bool)
nonNullAnd field p = not_ (isNothing_ field) &&. p (unsafeFromJustValue field)
where
-- This is safe in this context because of short-circuiting of &&.
unsafeFromJustValue :: SqlExpr (Value (Maybe a)) -> SqlExpr (Value a)
unsafeFromJustValue = coerce
-- | Project an SqlEntity that may be null to an expression, returning TRUE for null cases.
-- | In other words, Data.Foldable.all for Entities.
entityIsNullOr
:: (PersistEntity a, PersistField a)
=> SqlExpr (Maybe (Entity a))
-> (SqlExpr (Entity a) -> SqlExpr (Value Bool))
-> SqlExpr (Value Bool)
entityIsNullOr me p = isNothing_ (entityAsValueMaybe me) ||. p (unsafeFromJustEntity me)
where
-- This is safe in this context because of short-circuiting of ||.
unsafeFromJustEntity :: SqlExpr (Maybe (Entity a)) -> SqlExpr (Entity a)
unsafeFromJustEntity = coerce
-- | Project an SqlEntity that may be null to an expression, returning FALSE for null cases.
-- | In other words, Data.Foldable.any for Entities.
nonNullEntityAnd
:: (PersistEntity a, PersistField a)
=> SqlExpr (Maybe (Entity a))
-> (SqlExpr (Entity a) -> SqlExpr (Value Bool))
-> SqlExpr (Value Bool)
nonNullEntityAnd me p = not_ (isNothing_ (entityAsValueMaybe me)) &&. p (unsafeFromJustEntity me)
where
-- This is safe in this context because of short-circuiting of &&.
unsafeFromJustEntity :: SqlExpr (Maybe (Entity a)) -> SqlExpr (Entity a)
unsafeFromJustEntity = coerce
-- | Project an SqlEntity that may be null to a query, guarding against null cases.
withNonNullEntity
:: PersistField (Entity a)
=> SqlMaybeEntity a
-> (SqlEntity a -> SqlQuery b)
-> SqlQuery b
withNonNullEntity me f = withNonNull (entityAsValueMaybe me) (f . valueAsEntity)
where
valueAsEntity :: SqlExpr (Value (Entity a)) -> SqlExpr (Entity a)
valueAsEntity = coerce |
If you use a comparison operator on a Maybe value, there is nothing stopping a null getting into your conditionals if the values going into
>=.
are actually Nothing. This may wind up ruining your day or otherwise creating ... funny runtime bugs, since you create aNULL
in aValue Bool
, which is not supposed to be there.I think that in the current type signatures,
val . Just
is probably universally an invitation for bugs with the sole exception of if the result goes into||.
.Real-world example
If you write a query like this and omit the
isNothing
check you can cause this issue:This could possibly be fixed in a way that only breaks suspicious uses (although my condolences extend to anyone with a large codebase dealing with migrating this; probably would want to keep around the old buggy signatures for compat under a different name, so you could just replace all usages and migrate piecewise):
cc @parsonsmatt, I'm unsure if this type level shenanigans is fully sound either
The text was updated successfully, but these errors were encountered: