-
Notifications
You must be signed in to change notification settings - Fork 115
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
runSelect type error when nullable fkey column is wrapped in newtype #539
Comments
Will have a look in more detail at this later, but just a quick comment to check: perhaps it should be a |
hmm, I'd really like not to have to nest a maybe inside of |
Okay, I tried |
There is an alternative, which is to define your own SQL type and its mapping to a Haskell type (see below). So I guess there are three options
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where
import Opaleye
import Opaleye.Internal.Inferrable (Inferrable(Inferrable))
import Data.Profunctor.Product.Default (Default, def)
import Data.Profunctor.Product.TH (makeAdaptorAndInstance')
import Database.PostgreSQL.Simple (Connection)
import Database.PostgreSQL.Simple.FromField (fromField)
data SqlUserPKey
data UserPKey = UserPKey Int
data MyTableFields a b = MyTableFields a b
$(makeAdaptorAndInstance' ''MyTableFields)
myTable :: Table
(MyTableFields (FieldNullable SqlUserPKey) (Field SqlInt4))
(MyTableFields (FieldNullable SqlUserPKey) (Field SqlInt4))
myTable = table "myTable" (pMyTableFields $ MyTableFields
(requiredTableField "myfield1")
(requiredTableField "myfield2"))
instance DefaultFromField SqlUserPKey UserPKey where
defaultFromField = fromPGSFieldParser ((fmap . fmap . fmap) UserPKey fromField)
instance Default (Inferrable FromField) SqlUserPKey UserPKey where
def = Inferrable def
example :: Connection -> IO [MyTableFields (Maybe UserPKey) Int]
example conn = runSelectI conn (selectTable myTable) |
Sorry, didn't mean to close this. |
I had attempted to go the route of defining my own custom sql type too, but struggled to figure out how to implement the classes needed to work with it. Trying again with the example you provided above (thanks btw), I still need implement some other classes, probably even more after I resolve these errors:
Having to manually implement a bunch of classes every time I want a newtyped column feels undesirable. After tinkering for a while, I discovered that I could get instance
DB.DefaultFromField sqlType haskell =>
PP.Default
DB.FromFields
(PKey' (DB.Column (DB.Nullable sqlType)))
(Maybe (PKey' haskell))
where
def = PP.dimap unPKey (fmap PKey) PP.def And then generalized it to instance
( DB.DefaultFromField sql haskell
, Coercible (wrapper (DB.Column (DB.Nullable sql))) (DB.Column (DB.Nullable sql))
, Coercible (wrapper haskell) haskell
) =>
PP.Default
DB.FromFields
(wrapper (DB.Column (DB.Nullable sql)))
(Maybe (wrapper haskell))
where
def =
PP.dimap
coerce
(fmap coerce)
( PP.def
@DB.FromFields
@(DB.Column (DB.Nullable sql))
@(Maybe haskell)
) so it could work with any newtype wrapper around Does this seem sensible or am I just wandering too far from the happy path? |
Did you define your type If you still can't get it to work then please your latest version to your GitHub repo and I'll take a look.
Yes, you most likely will.
Yeah it is. That's why the
Hmm, well it might work but it's also likely to be very fragile and break in hard to diagnose ways. I wouldn't recommend it, but you can try if you like! |
Yeah, I thought so. I think I just hadn't finished propogating the change to So I think I've got it working now with your suggestion, but I used a newtype around With the user pkey designed like this newtype PKey' a = PKey
{ unPKey :: a
}
$(PPTH.makeAdaptorAndInstance "pPKey" ''PKey')
newtype SqlPKey = SqlPKey {unSqlPKey :: DB.PGInt4}
instance PP.Default DB.ToFields PKey (Col.Column SqlPKey) where
def = PP.dimap coerce coerce (PP.def @DB.ToFields @Int @(DB.Column DB.PGInt4))
instance DB.DefaultFromField SqlPKey PKey where
defaultFromField = DB.fromPGSFieldParser ((fmap . fmap . fmap) PKey PGS.fromField)
instance PP.Default (Inferrable DB.FromField) SqlPKey PKey where
def = Inferrable PP.def
type PKey = PKey' Int
type PKeyReadField =
(DB.Field SqlPKey)
type PKeyWriteField =
() And using it as a nullable foreign key on another table like this type TaskRow =
DB.LocalTimestampedRow
( TaskRow'
(Maybe User.PKey) -- ownerId
PKey -- pkey
Uuid -- uuid
)
type WriteField =
DB.LocalTimestampedWriteField
( TaskRow'
(Maybe (DB.FieldNullable User.SqlPKey))
PKeyWriteField
UuidWriteField
)
type ReadField =
DB.LocalTimestampedReadField
( TaskRow'
(DB.FieldNullable User.SqlPKey)
PKeyReadField
UuidReadField
) allows me to retain Designing the data this way, newtyping the sql type instead of the whole field/column type, feels the most intuitive to read to me. It would be nice if we could make it so that newtyping an existing sql/pg type would just work and reuse all the classes implemented for the underlying type. |
@tomjaguarpaw do you think that would be a good idea? Having instances defined that would allow folks to newtype any instance
( PP.Default DB.ToFields haskellType (DB.Column sqlType)
, Coercible (wrapper haskellType) haskellType
, Coercible (wrapper sqlType) sqlType
) =>
PP.Default DB.ToFields (wrapper haskellType) (DB.Column (wrapper sqlType))
where
def = PP.dimap coerce coerce (PP.def @DB.ToFields @haskellType @(DB.Column sqlType))
instance
( Coercible (wrapper sqlType) sqlType
, Coercible (wrapper haskellType) haskellType
, PGS.FromField haskellType
) =>
DB.DefaultFromField (wrapper sqlType) (wrapper haskellType)
where
defaultFromField =
DB.fromPGSFieldParser $
(fmap . fmap . fmap) (coerce @haskellType @(wrapper haskellType)) PGS.fromField
With these orphan instances implemented in my project, I can simply define my primary key type like so -- User.hs
newtype PKey' a = PKey
{ unPKey :: a
}
$(PPTH.makeAdaptorAndInstance "pPKey" ''PKey')
-- | Haskell PKey
type PKey = PKey' Int
-- | Opaleye Sql PKey
type SqlPKey = PKey' DB.PGInt4
type PKeyReadField =
(DB.Field SqlPKey)
type PKeyWriteField =
() -- Disallow writing to the pkey column And with things defined this way, I can define the nullable fkey on my other model the way I wanted, where the -- Task.hs
type TaskRow =
DB.LocalTimestampedRow
( TaskRow'
(Maybe User.PKey) -- ownerId
PKey -- pkey
Uuid -- uuid
)
type WriteField =
DB.LocalTimestampedWriteField
( TaskRow'
(Maybe (DB.FieldNullable User.SqlPKey))
PKeyWriteField
UuidWriteField
)
type ReadField =
DB.LocalTimestampedReadField
( TaskRow'
(DB.FieldNullable User.SqlPKey)
PKeyReadField
UuidReadField
) |
I've since been advised against using Coercible for this, as others have experienced significant deterioration of type inference in past attempts to implement classes in terms of Coercible. So, I guess I retract my question. |
FYI
Maybe there's a DerivingVia approach that would work well here. I agree with the advice you received that trying to do it with this super-powerful instance is likely to lead to a lot of breakage and frustration. |
I'm having trouble figuring out how to resolve a compiler error I'm getting at the call site of
runSelect
after I attempted to wrap a nullable foreign key column in a newtype. I can get it to compile fine if I remove the newtype in the Read- and WriteField. Here's a link to a repo and specific line of code where the error is, if anyone cares to run it locally to inspect closer.This is the table definition:
The query I'm trying to run looks like this (ignore the graphql stuff; I just wrapped
runSelect
to lift the resultingIO
into a custom monad formorpheus-graphql
):The type error I'm getting is this:
Would love to understand what I'm doing wrong and how to fix it, and what I should perhaps learn from this type error so that I can better interpret future errors like this that I run into.
The text was updated successfully, but these errors were encountered: