Open derrickbeining opened 2 years ago
Will have a look in more detail at this later, but just a quick comment to check: perhaps it should be a User.PKey (Maybe a)
rather than a Maybe (User.PKey a)
?
hmm, I'd really like not to have to nest a maybe inside of User.PKey
. Do you think that because the WriteField definition has the Maybe inside of PKey' ? I only did that because that's how I've seen all examples of optional newtyped columns done.
Okay, I tried User.PKey (Maybe Int)
and I can get it to compile with that... but why??? And is there someway I can make Maybe User.PKey
work?
User.PKey (Maybe Int)
is the natural consequence of treating User.PKey
as a collection of fields, which is what the product-profunctors machinery does. Suppose that instead of a single field your primary key was a composite key of a pair of nullable fields. Then you absolutely wouldn't be able to map it to Maybe (User.PKey Int Int)
without losing information. It would have to be User.PKey (Maybe Int) (Maybe Int)
. Having Maybe
on the inside of single-field PKey
is consistent with that idea.
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
Maybe
inside User.PKey
Maybe
outside User.PKey
by defining your own SQL type{-# 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:
• No instance for (PP.Default
DB.ToFields Int (Col.Column SqlUserPKey))
arising from a use of ‘DB.toFields’
• In the second argument of ‘(.===)’, namely ‘DB.toFields pkey’
In the second argument of ‘($)’, namely
‘pkey' (DB.record user) .=== DB.toFields pkey’
In a stmt of a 'do' block:
_ <- DB.where_ $ pkey' (DB.record user) .=== DB.toFields pkey
|
237 | DB.where_ $ pkey' (DB.record user) .=== DB.toFields pkey
| ^^^^^^^^^^^^^^^^
/Users/derrickbeining/dev/pinata-dev/Pinata/systems/Haskell/src/Pinata/Model/User.hs:270:9-46: error:
• Could not deduce (DB.DefaultFromField SqlUserPKey Int)
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 Maybe User.PKey
to work by implementing
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 FieldNullable sqlType
.
Does this seem sensible or am I just wandering too far from the happy path?
after I resolve these errors:
Did you define your type data UserPKey = UserPKey Int
? You should be converting that to and from Field SqlUserPKey
but it looks like you are trying to convert naked Int
instead.
If you still can't get it to work then please your latest version to your GitHub repo and I'll take a look.
I still need implement some other classes
Yes, you most likely will.
Having to manually implement a bunch of classes every time I want a newtyped column feels undesirable.
Yeah it is. That's why the Default
machinery exists: it basically implements all those classes for you, almost for free.
Does this seem sensible or am I just wandering too far from the happy path?
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!
Did you define your type data UserPKey = UserPKey Int?
Yeah, I thought so. I think I just hadn't finished propogating the change to SqlUserKey
through the rest of the code.
So I think I've got it working now with your suggestion, but I used a newtype around PGInt4
instead of an empty data declaration.
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 User.PKey
as User.PKey' Int
instead of having to make it User.PKey' (Maybe Int)
.
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 sqlType
like SqlInt4
etc and everything still just work as before? I think all that's need is these two instances:
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 Maybe
can remain on the outside of the newtype like this:
-- 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 type PKeyWriteField = ()
suggests you are using readOnly
. readOnly
seems to be problematic (see https://github.com/tomjaguarpaw/haskell-opaleye/issues/535) and so I am anticipating deprecating it.
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.
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.
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.