data X = X { x :: Entity Foo }
deriveEsqueletoRecord ''X
getX :: SqlQuery X
and I'm subSelecting that in another record:
data Y = Y { y :: Maybe X }
deriveEsqueletoRecord ''Y
getY = do
pure Y
{ y = subSelect getX
}
Unfortunately this fails for a few reasons:
subSelect only works on a SqlExpr (Value a) - fixable with a subSelectRecord :: SqlQuery a -> SqlExpr (Maybe a).
The codegen for SqlY creates the field y :: SqlExpr (Value (Maybe X)). The SqlExpr (Value (Maybe _)) should be "folded in" to the Maybe _ bit.
Here's the code that determines the field type:
-- | Transforms a record field type into a corresponding `SqlExpr` type.
--
-- * @'Entity' x@ is transformed into @'SqlExpr' ('Entity' x)@.
-- * @'Maybe' ('Entity' x)@ is transformed into @'SqlExpr' ('Maybe' ('Entity' x))@.
-- * @x@ is transformed into @'SqlExpr' ('Value' x)@.
-- * If there exists an instance @'SqlSelect' sql x@, then @x@ is transformed into @sql@.
--
-- This function should match `sqlSelectProcessRowPat`.
sqlFieldType :: Type -> Q Type
sqlFieldType fieldType = do
maybeSqlType <- reifySqlSelectType fieldType
pure $
flip fromMaybe maybeSqlType $
case fieldType of
-- Entity x -> SqlExpr (Entity x)
AppT (ConT ((==) ''Entity -> True)) _innerType -> AppT (ConT ''SqlExpr) fieldType
-- Maybe (Entity x) -> SqlExpr (Maybe (Entity x))
(ConT ((==) ''Maybe -> True))
`AppT` ((ConT ((==) ''Entity -> True))
`AppT` _innerType) -> AppT (ConT ''SqlExpr) fieldType
-- x -> SqlExpr (Value x)
_ -> (ConT ''SqlExpr)
`AppT` ((ConT ''Value)
`AppT` fieldType)
And reifySqlSelectType:
-- Given a type, find the corresponding SQL type.
--
-- If there exists an instance `SqlSelect sql ty`, then the SQL type for `ty`
-- is `sql`.
--
-- This function definitely works for records and instances generated by this
-- module, and might work for instances outside of it.
reifySqlSelectType :: Type -> Q (Maybe Type)
reifySqlSelectType originalType = do
-- Here we query the compiler for Instances of `SqlSelect a $(originalType)`;
-- the API for this is super weird, it interprets a list of types as being
-- applied as successive arguments to the typeclass name.
--
-- See: https://gitlab.haskell.org/ghc/ghc/-/issues/21825
--
-- >>> reifyInstances ''SqlSelect [VarT (mkName "a"), ConT ''MyRecord]
-- [ InstanceD Nothing
-- []
-- (AppT (AppT (ConT Database.Esqueleto.Internal.Internal.SqlSelect)
-- (ConT Ghci3.SqlMyRecord))
-- (ConT Ghci3.MyRecord))
-- []
-- ]
tyVarName <- newName "a"
instances <- reifyInstances ''SqlSelect [VarT tyVarName, originalType]
-- Given the original type (`originalType`) and an instance type for a
-- `SqlSelect` instance, get the SQL type which corresponds to the original
-- type.
let extractSqlRecord :: Type -> Type -> Maybe Type
extractSqlRecord originalTy instanceTy =
case instanceTy of
(ConT ((==) ''SqlSelect -> True))
`AppT` sqlTy
`AppT` ((==) originalTy -> True) -> Just sqlTy
_ -> Nothing
-- Filter `instances` to the instances which match `originalType`.
filteredInstances :: [Type]
filteredInstances =
flip mapMaybe instances
(\case InstanceD _overlap
_constraints
(extractSqlRecord originalType -> Just sqlRecord)
_decs ->
Just sqlRecord
_ -> Nothing)
pure $ listToMaybe filteredInstances
I've got a record I'm
deriveEsqueletoRecord
ing:and I'm
subSelect
ing that in another record:Unfortunately this fails for a few reasons:
subSelect
only works on aSqlExpr (Value a)
- fixable with asubSelectRecord :: SqlQuery a -> SqlExpr (Maybe a)
.SqlY
creates the fieldy :: SqlExpr (Value (Maybe X))
. TheSqlExpr (Value (Maybe _))
should be "folded in" to theMaybe _
bit.Here's the code that determines the field type:
And
reifySqlSelectType
:Need to experiment a bit with this