tomjaguarpaw / haskell-opaleye

Other
605 stars 115 forks source link

Add toFields to tutorials #478

Open tomjaguarpaw opened 4 years ago

tomjaguarpaw commented 4 years ago

https://github.com/tomjaguarpaw/haskell-opaleye/issues/433#issuecomment-542335898

uliSchuster commented 4 years ago

The Haddock documentation for ´toFields´ provides an example with runInsert. However, runInsert is deprecated and should be replaced with runInsert_. It would be great to have an example how to use toFields when setting up an Insert record. How exactly should the iRows field look like?

tomjaguarpaw commented 4 years ago

Yes, see d4c433984f8efb369a0a21220d249689885c5553 (which will be in 0.7).

uliSchuster commented 4 years ago

Thanks for the updated documentation! Unfortunately, I still don't understand how to use the toFields mapping when the insert column-record has an optional field - in my case, an autogenerated key - probably because I still do not fully understand the product-profunctor magic.

Simplified setup:

-- Helper
type F field = OE.Field field 

-- Newtype wrapper for distinct primary keys
newtype UserIdT a = UserId {getUserId :: a}
  deriving (Eq, Show, Display)
$(makeAdaptorAndInstance "pUserId" ''UserIdT)

-- Table definition
type UserIdField = UserIdT (F OE.SqlInt8)
type OptionalUserIdField = UserIdT (Maybe (F OE.SqlInt8))
type UserId = UserIdT Int64
data UserT key username
  = User
      { userKey :: !key,
        userUsername :: !username }

type UserW
  = UserT
      OptionalUserIdField -- autogenerated key
      (F OE.SqlText) -- username

type UserR
  = UserT
      UserIdField -- key
      (F OE.SqlText) -- username

-- The concrete Haskell type, including the key. However, the key is not optional here.
type User
  = UserT UserId -- key
                Text -- username

$(makeAdaptorAndInstance "pUser" ''UserT)
userTable :: OE.Table UserW UserR
userTable = OE.tableWithSchema
  schemaName
  userTableName
  (pUser User { userKey      = pUserId (UserId (OE.tableField "id"))
              , userUsername = OE.tableField "username"
              }
  )

How does the haskells record need to look like so that toFields can correctly convert it into a Fields record? Do I need to wrap the key into a Maybe and set it to Nothing? With the above definition, I cannot construct a proper Insert:

newUser = User
  { UT.userKey      = UT.UserId 123
  , UT.userUsername = "tomTester" }

-- does not type check, probably because the key is not optional.
insertSpec = OE.Insert { OE.iTable      = userTable
                       , OE.iRows       = map OE.toFields [newUser]
                       , OE.iReturning  = OE.rCount
                       , OE.iOnConflict = Nothing
                       }
tomjaguarpaw commented 4 years ago

Hi Uli, the quick answer is to use

newUser = User
  { userKey      = UserId (Nothing :: Maybe Int64)
  , userUsername = "tomTester" }

I will think about how to make this more obvious and get back to you.

uliSchuster commented 4 years ago

Ok. Basically, I need to define different haskells types for reading and writing to the DB - the User type I defined above for reading, and the type of newUser from your answer for writing to the DB?

uliSchuster commented 4 years ago

Thanks for the help so far. I have one more issue to make insertions work. In the example above, the insertSpec simply returned the number of correctly inserted row. In a real application, though. with autogenerated keys, I would like the insert to return the primary key of the newly inserted row. I tried to modify the insertSpec as follows:

insertSpec = OE.Insert { OE.iTable      = userTable
                       , OE.iRows       = map OE.toFields [newUser]
                       , OE.iReturning  = OE.rReturning (\User{userKey = pk} -> pk)
                       , OE.iOnConflict = Nothing
                       }

However, I get the following type checker error

• Couldn't match type ‘[haskells0]’ with ‘UserIdT Int64’
  Expected type: Returning
                   (UserT
                      (UserIdT (Column PGInt8))
                      (Column PGText)
                      (Column PGText)
                      (Column (Nullable PGText))
                      (Column (Nullable PGText))
                      (Column PGText))
                   UserId
    Actual type: Returning
                   (UserT
                      (UserIdT (Column PGInt8))
                      (Column PGText)
                      (Column PGText)
                      (Column (Nullable PGText))
                      (Column (Nullable PGText))
                      (Column PGText))
                   [haskells0]

Again, I guess I am lost with the product pro functor magic going on here. The type of the private key is indeed UserIdT Int64 (a newtype wrapper). How should I annotate that type?

tomjaguarpaw commented 4 years ago

It will return a list of values, even if you only insert one row. You will have to make sure you treat the the return value as something of type [UserId] rather than UserId.

uliSchuster commented 4 years ago

That was it, thanks a lot for pointing out my mistake. Indeed, it was right in front of me: The error message quoted above was asking for a '[haskells0]' value, which clearly is a list of haskells, yet I didn't realize the list, but instead was confused by generic 'haskells0' type variable.

tomjaguarpaw commented 4 years ago

Yes, the unhelpful error message is because of the type class constraint. I can make a version of runInsert_ that is more inferrable, like runSelectI.

tomjaguarpaw commented 4 years ago

I added rReturningI to 0.7.1.0. It will make the error message much better in this case.