Open parsonsmatt opened 6 months ago
I hand-derived some instances that ended up working out for this case. See for example the following minimal example.
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
module Lib where
import Control.Monad.Trans.State.Strict (evalStateT)
import Data.Bifunctor (first)
import Data.String (fromString)
import Database.Esqueleto.Experimental
import Database.Esqueleto.Record
import Database.Esqueleto.Internal.Internal
import Data.Proxy
data Record key = Record { key :: key, val :: Int }
data SqlRecord key = SqlRecord { key :: SqlExpr (Value key), val :: SqlExpr (Value Int) }
data SqlMaybeRecord key = SqlMaybeRecord { key :: SqlExpr (Value (Maybe key)), val :: SqlExpr (Value (Maybe Int)) }
instance PersistField (Key rec) => SqlSelect (SqlRecord (Key rec)) (Record (Key rec)) where
sqlSelectCols identInfo SqlRecord { key, val } =
sqlSelectCols identInfo (key :& val)
sqlSelectColCount _ = sqlSelectColCount
( Proxy
@( SqlExpr (Value (Key rec))
:& SqlExpr (Value Int)
)
)
sqlSelectProcessRow columns =
first
(fromString "Failed to parse Record: " <>)
(evalStateT process columns)
where
process = do
Value key <- takeColumns @(SqlExpr (Value (Key rec)))
Value val <- takeColumns @(SqlExpr (Value Int))
pure Record { key, val }
instance ToAliasReference (SqlRecord (Key rec)) where
toAliasReference ident SqlRecord { key, val } =
SqlRecord <$> toAliasReference ident key <*> toAliasReference ident val
instance ToAlias (SqlRecord (Key rec)) where
toAlias SqlRecord { key, val } =
SqlRecord <$> toAlias key <*> toAlias val
instance ToMaybe (SqlRecord (Key rec)) where
type ToMaybeT (SqlRecord (Key rec)) = SqlMaybeRecord (Key rec)
toMaybe SqlRecord { key, val } =
SqlMaybeRecord
{ key = just key
, val = just val
}
I'm not actually certain if the Key rec
thing is necessary, I added it in because it doesn't constrain my usecase and I figured it might make some of these instances more deducible.
Hmm. With a totally polymorphic field like key :: key
, it's impossible to know how it will be instantiated, and therefore, what code to use. The three cases are SqlExpr (Value a)
, SqlExpr (Entity rec)
, and (for other esqueleto records) key
(since the SqlExpr
is baked into the record itself).
I think you'd need the type parameter to not affect the conversion logic. Or, to delegate more fully. Consider:
data R k = R { key :: k }
data SqlR k = SqlR { key :: k }
data SqlMaybeR k = SqlMaybeR { key :: k }
Then I think our instance becomes:
instance (SqlSelect sqlK valueK) => SqlSelect (SqlR sqlK) (R valueK)
Which should work in more generality.
I think a much easier alternative is if you can further constrain the type of key
. Consider:
data Record a = Record { key :: Key a }
Now, we know that Key a
is always going to be wrapped in SqlExpr (Value (Key a))
, so we can work with it more easily.
That may also simplify the rest of your code, as well.
Consider the type:
We can't write
deriveEsqueletoRecord
here because there's a type error. We can't provide concrete things becausederiveEsqueletoRecord
takes aName
and not aType
, so we can't writederiveEsqueletoRecord @(Record Foo)
.Ideally we can support polymorphic records.