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

HasField for SqlExpr (Maybe (Entity a)) joins Maybe #422

Merged
merged 8 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/haskell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
# mysql database: 'esqutest' # Optional, default value is "test". The specified database which will be create
# mysql user: 'travis' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too
# mysql password: 'esqutest' # Required if "mysql user" exists. The password for the "mysql user"
- run: sudo apt-get update && sudo apt-get install -y libpcre3-dev
- run: cabal v2-update
- run: cabal v2-freeze $CONFIG
- uses: actions/cache@v4
Expand Down
18 changes: 18 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
3.6.0.0
=======
- @parsonsmatt
- [#422](https://github.com/bitemyapp/esqueleto/pull/422)
- The instance of `HasField` for `SqlExpr (Maybe (Entity a))` joins
`Maybe` values together. This means that if you `leftJoin` a table
with a `Maybe` column, the result will be a `SqlExpr (Value (Maybe
typ))`, instead of `SqlExpr (Value (Maybe (Maybe typ)))`.
- To make this a less breaking change, `joinV` has been given a similar
behavior. If the input type to `joinV` is `Maybe (Maybe typ)`, then
the result becomes `Maybe typ`. If the input type is `Maybe typ`, then
the output is also `Maybe typ`. The `joinV'` function is given as an
alternative with monomorphic behavior.
- The `just` function is also modified to avoid nesting `Maybe`.
Likewise, `just'` is provided to give monomorphic behavior.
- `subSelect`, `max_`, `min_`, and `coalesce` were all
given `Nullable` output types as well. This should help to reduce the
incidence of nested `Maybe`.
- The operator `??.` was introduced which can do nested `Maybe`. You may
want this if you have type inference issues with `?.` combining
`Maybe`.
- [#420](https://github.com/bitemyapp/esqueleto/pull/420)
- Add a fixity declaration to `?.`
- [#412](https://github.com/bitemyapp/esqueleto/pull/412)
Expand Down
2 changes: 1 addition & 1 deletion src/Database/Esqueleto.hs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ module Database.Esqueleto {-# WARNING "This module will switch over to the Exper
where_, on, groupBy, orderBy, asc, desc, limit, offset
, distinct, distinctOn, don, distinctOnOrderBy, having, locking
, (^.), (?.)
, val, isNothing, just, nothing, joinV, withNonNull
, val, isNothing, just, just', nothing, joinV, joinV', withNonNull
, countRows, count, countDistinct
, not_, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.)
, between, (+.), (-.), (/.), (*.)
Expand Down
4 changes: 0 additions & 4 deletions src/Database/Esqueleto/Experimental/ToMaybe.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ module Database.Esqueleto.Experimental.ToMaybe
import Database.Esqueleto.Internal.Internal hiding (From(..), from, on)
import Database.Esqueleto.Internal.PersistentImport (Entity(..))

type family Nullable a where
Nullable (Maybe a) = a
Nullable a = a

class ToMaybe a where
type ToMaybeT a
toMaybe :: a -> ToMaybeT a
Expand Down
130 changes: 114 additions & 16 deletions src/Database/Esqueleto/Internal/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
import Data.Kind (Type)
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import qualified Data.Monoid as Monoid

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.6)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.10)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.10)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.8)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.8)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.2)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.2)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.6)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.6)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.4)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.4)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.8)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.8)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.10)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.10)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.0)

The qualified import of ‘Data.Monoid’ is redundant

Check warning on line 67 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.0)

The qualified import of ‘Data.Monoid’ is redundant
import Data.Proxy (Proxy(..))
import Data.Set (Set)
import qualified Data.Set as Set
Expand All @@ -74,7 +74,7 @@
import Data.Typeable (Typeable)
import Database.Esqueleto.Internal.ExprParser (TableAccess(..), parseOnExpr)
import Database.Esqueleto.Internal.PersistentImport
import Database.Persist (EntityNameDB(..), FieldNameDB(..), SymbolToField(..))

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.10)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.10)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.8)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.8)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.2)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.2)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.6)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.6)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.4)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.4)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.8)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.8)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.10)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 8.10)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.0)

The import of ‘Database.Persist’ is redundant

Check warning on line 77 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.0)

The import of ‘Database.Persist’ is redundant
import qualified Database.Persist
import Database.Persist.Sql.Util
( entityColumnCount
Expand Down Expand Up @@ -446,9 +446,9 @@
--
-- @since 3.2.0
subSelect
:: PersistField a
:: (PersistField a, NullableFieldProjection a a')
=> SqlQuery (SqlExpr (Value a))
-> SqlExpr (Value (Maybe a))
-> SqlExpr (Value (Maybe a'))
subSelect query = just (subSelectUnsafe (query <* limit 1))

-- | Execute a subquery @SELECT@ in a 'SqlExpr'. This function is a shorthand
Expand Down Expand Up @@ -599,12 +599,28 @@
where_ $ not_ $ isNothing field
f $ veryUnsafeCoerceSqlExprValue field

-- | Project an 'EntityField' of a nullable entity. The result type will be
-- 'Nullable', meaning that nested 'Maybe' won't be produced here.
--
-- As of v3.6.0.0, this will attempt to combine nested 'Maybe'. If you want to
-- keep nested 'Maybe', then see '??.'.
(?.) :: (PersistEntity val , PersistField typ)
=> SqlExpr (Maybe (Entity val))
-> EntityField val typ
-> SqlExpr (Value (Maybe (Nullable typ)))
ent ?. field = veryUnsafeCoerceSqlExprValue (ent ??. field)

-- | Project a field of an entity that may be null.
(?.) :: ( PersistEntity val , PersistField typ)
--
-- This variant will produce a nested 'Maybe' if you select a 'Maybe' column.
-- If you want to collapse 'Maybe', see '?.'.
--
-- @since 3.6.0.0
(??.) :: ( PersistEntity val , PersistField typ)
=> SqlExpr (Maybe (Entity val))
-> EntityField val typ
-> SqlExpr (Value (Maybe typ))
ERaw m f ?. field = just (ERaw m f ^. field)
ERaw m f ??. field = just (ERaw m f ^. field)

-- | Lift a constant value from Haskell-land to the query.
val :: PersistField typ => typ -> SqlExpr (Value typ)
Expand Down Expand Up @@ -656,18 +672,52 @@
-- | Analogous to 'Just', promotes a value of type @typ@ into
-- one of type @Maybe typ@. It should hold that @'val' . Just
-- === just . 'val'@.
just :: SqlExpr (Value typ) -> SqlExpr (Value (Maybe typ))
--
-- This function will try not to produce a nested 'Maybe'. This is in accord
-- with how SQL represents @NULL@. That means that @'just' . 'just' = 'just'@.
-- This behavior was changed in v3.6.0.0. If you want to produce nested 'Maybe',
-- see 'just''.
just
:: (NullableFieldProjection typ typ')
=> SqlExpr (Value typ) -> SqlExpr (Value (Maybe typ'))
just = veryUnsafeCoerceSqlExprValue

-- | Like 'just', but this function does not try to collapse nested 'Maybe'.
-- This may be useful if you have type inference problems with 'just'.
--
-- @since 3.6.0.0
just' :: SqlExpr (Value typ) -> SqlExpr (Value (Maybe typ))
just' = veryUnsafeCoerceSqlExprValue

-- | @NULL@ value.
nothing :: SqlExpr (Value (Maybe typ))
nothing = unsafeSqlValue "NULL"

-- | Join nested 'Maybe's in a 'Value' into one. This is useful when
-- calling aggregate functions on nullable fields.
joinV :: SqlExpr (Value (Maybe (Maybe typ))) -> SqlExpr (Value (Maybe typ))
--
-- As of v3.6.0.0, this function will attempt to work on both @'SqlExpr'
-- ('Value' ('Maybe' a))@ as well as @'SqlExpr' ('Value' ('Maybe' ('Maybe' a)))@
-- inputs to make transitioning to 'NullableFieldProjection' easier. This may
-- make type inference worse in some cases. If you want the monomorphic variant,
-- see 'joinV''
joinV
:: (NullableFieldProjection typ typ')
=> SqlExpr (Value (Maybe typ))
-> SqlExpr (Value (Maybe typ'))
joinV = veryUnsafeCoerceSqlExprValue

-- | Like 'joinV', but monomorphic: the input type only works on @'SqlExpr'
-- ('Value' (Maybe (Maybe a)))@.
--
-- This function may be useful if you have type inference issues with 'joinV'.
--
-- @since 3.6.0.0
joinV'
:: SqlExpr (Value (Maybe (Maybe typ)))
-> SqlExpr (Value (Maybe typ))
joinV' = veryUnsafeCoerceSqlExprValue


countHelper :: Num a => TLB.Builder -> TLB.Builder -> SqlExpr (Value typ) -> SqlExpr (Value a)
countHelper open close v =
Expand Down Expand Up @@ -873,12 +923,21 @@

sum_ :: (PersistField a, PersistField b) => SqlExpr (Value a) -> SqlExpr (Value (Maybe b))
sum_ = unsafeSqlFunction "SUM"
min_ :: (PersistField a) => SqlExpr (Value a) -> SqlExpr (Value (Maybe a))
min_ = unsafeSqlFunction "MIN"
max_ :: (PersistField a) => SqlExpr (Value a) -> SqlExpr (Value (Maybe a))
max_ = unsafeSqlFunction "MAX"
avg_ :: (PersistField a, PersistField b) => SqlExpr (Value a) -> SqlExpr (Value (Maybe b))
avg_ = unsafeSqlFunction "AVG"

min_
:: (PersistField a)
=> SqlExpr (Value a)
-> SqlExpr (Value (Maybe (Nullable a)))
min_ = unsafeSqlFunction "MIN"

max_
:: (PersistField a)
=> SqlExpr (Value a)
-> SqlExpr (Value (Maybe (Nullable a)))
max_ = unsafeSqlFunction "MAX"

avg_ :: (PersistField a, PersistField b) => SqlExpr (Value a) -> SqlExpr (Value (Maybe b))
avg_ = unsafeSqlFunction "AVG"

-- | Allow a number of one type to be used as one of another
-- type via an implicit cast. An explicit cast is not made,
Expand Down Expand Up @@ -913,7 +972,10 @@
-- documentation.
--
-- @since 1.4.3
coalesce :: PersistField a => [SqlExpr (Value (Maybe a))] -> SqlExpr (Value (Maybe a))
coalesce
:: (PersistField a, NullableFieldProjection a a')
=> [SqlExpr (Value (Maybe a))]
-> SqlExpr (Value (Maybe a'))
coalesce = unsafeSqlFunctionParens "COALESCE"

-- | Like @coalesce@, but takes a non-nullable SqlExpression
Expand Down Expand Up @@ -1411,7 +1473,7 @@
unique = finalR uniqueConstructor
-- there must be a better way to get the constrain name from a unique, make this not a list search
filterF = (==) (persistUniqueToFieldNames unique) . uniqueFields
uniqueDef = head . filter filterF . getEntityUniques . entityDef $ proxy

Check warning on line 1476 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.10)

In the use of ‘head’

Check warning on line 1476 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.10)

In the use of ‘head’

Check warning on line 1476 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.8)

In the use of ‘head’

Check warning on line 1476 in src/Database/Esqueleto/Internal/Internal.hs

View workflow job for this annotation

GitHub Actions / build (3.10.2.1, 9.8)

In the use of ‘head’

-- | Render updates to be use in a SET clause for a given sql backend.
--
Expand Down Expand Up @@ -2478,11 +2540,43 @@
--
-- @since 3.5.4.0
instance
(PersistEntity rec, PersistField typ, SymbolToField sym rec typ)
(PersistEntity rec, PersistField typ, PersistField typ', SymbolToField sym rec typ
, NullableFieldProjection typ typ'
, HasField sym (SqlExpr (Maybe (Entity rec))) (SqlExpr (Value (Maybe typ')))
)
=>
HasField sym (SqlExpr (Maybe (Entity rec))) (SqlExpr (Value (Maybe typ)))
HasField sym (SqlExpr (Maybe (Entity rec))) (SqlExpr (Value (Maybe typ')))
where
getField expr = expr ?. symbolToField @sym
getField expr = veryUnsafeCoerceSqlExpr (expr ?. symbolToField @sym)

-- | The 'NullableFieldProjection' type is used to determine whether
-- a 'Maybe' should be stripped off or not. This is used in the 'HasField'
-- for @'SqlExpr' ('Maybe' ('Entity' a))@ to allow you to only have
-- a single level of 'Maybe'.
--
-- @
-- MyTable
-- column Int Maybe
-- someTableId SomeTableId
--
-- select $ do
-- (_ :& maybeMyTable) <-
-- from $ table @SomeTable
-- `leftJoin` table @MyTable
-- `on` do
-- \(someTable :& maybeMyTable) ->
-- just someTable.id ==. maybeMyTable.someTableId
-- where_ $ maybeMyTable.column ==. just (val 10)
-- pure maybeMyTable
-- @
--
-- Without this class, projecting a field with type @'Maybe' typ@ would
-- have resulted in a @'SqlExpr' ('Value' ('Maybe' ('Maybe' typ)))@.
--
-- @since 3.6.0.0
class NullableFieldProjection typ typ'
instance {-# incoherent #-} (typ ~ typ') => NullableFieldProjection (Maybe typ) typ'
instance {-# overlappable #-} (typ ~ typ') => NullableFieldProjection typ typ'

-- | Data type to support from hack
data PreprocessedFrom a = PreprocessedFrom a FromClause
Expand Down Expand Up @@ -4263,3 +4357,7 @@
(\(oneOld, manyOld) (_, manyNew) -> (oneOld, manyNew ++ manyOld ))
(entityKey one)
(entityVal one, [many])

type family Nullable a where
Nullable (Maybe a) = a
Nullable a = a
2 changes: 1 addition & 1 deletion src/Database/Esqueleto/Legacy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ module Database.Esqueleto.Legacy
where_, on, groupBy, orderBy, asc, desc, limit, offset
, distinct, distinctOn, don, distinctOnOrderBy, having, locking
, (^.), (?.)
, val, isNothing, just, nothing, joinV, withNonNull
, val, isNothing, just, just', nothing, joinV, joinV', withNonNull
, countRows, count, countDistinct
, not_, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.)
, between, (+.), (-.), (/.), (*.)
Expand Down
31 changes: 30 additions & 1 deletion test/Common/Test.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
Expand Down Expand Up @@ -2487,7 +2488,7 @@ testOverloadedRecordDot = describe "OverloadedRecordDot" $ do
pure bp.title
describe "with SqlExpr (Maybe (Entity rec))" $ do
itDb "lets you project from a Maybe record" $ do
select $ do
void $ select $ do
p :& mbp <- Experimental.from $
table @Person
`leftJoin` table @BlogPost
Expand All @@ -2496,6 +2497,34 @@ testOverloadedRecordDot = describe "OverloadedRecordDot" $ do
just p.id ==. mbp.authorId
pure (p.id, mbp.title)

itDb "joins Maybe together" $ do
void $ select $ do
deed :& lord <-
Experimental.from $
table @Deed
`leftJoin` table @Lord
`Experimental.on` do
\(deed :& lord) ->
lord.id ==. just deed.ownerId
where_ $ lord.dogs >=. just (val 10)
where_ $ joinV lord.dogs >=. just (just (val 10))
where_ $ lord.dogs >=. just (val (Just 10))

itDb "i didn't bork ?." $ do
weights <- select $ do
(pro :& per) <- Experimental.from $
table @Profile
`leftJoin` table @Person
`Experimental.on` do
\(pro :& per) ->
just (pro ^. #person) ==. per ?. #id
&&. just pro.person ==. per ?. PersonId
pure $ per ?. #weight
asserting $ do
weights `shouldBe` ([] :: [Value (Maybe Int)])



#else
it "is only supported in GHC 9.2 or above" $ \_ -> do
pending
Expand Down
Loading