0

I'm trying to use Persistent with Servant, so I don't have the luxury of automatically parsing URL segments into Persistent keys. Instead, I've set up my routes to require an Int64, and I want to retrieve a record using that to perform primary key lookup.

Everything I've found points to using toSqlKey to convert an integer to a key, so I tried to write a very simple function that would do that for me:

runDB :: (MonadBaseControl IO m, MonadIO m) => (SqlPersistT (NoLoggingT (ResourceT m))) a -> m a
runDB actions = do
  filename <- liftIO $ getEnv "SQLITE_FILENAME"
  runSqlite (pack filename) actions

getRecordByKey :: Int64 -> IO (Maybe (Entity Record))
getRecordByKey recordId = runDB $ get (toSqlKey recordId)

Unfortunately, this didn't work; I got the following type error:

Couldn't match expected type ‘PersistEntityBackend
                                (Entity Record)’
            with actual type ‘SqlBackend’
In the second argument of ‘($)’, namely ‘get (toSqlKey recordId)’
In the expression: runDB $ get (toSqlKey recordId)
In an equation for ‘getRecordByKey’:
    getRecordByKey recordId = runDB $ get (toSqlKey recordId)

I sort of understand this error—I looked up the types for get and toSqlKey, and they include the relevant constraints:

get :: (MonadIO m, backend ~ PersistEntityBackend val, PersistEntity val) => Key val -> ReaderT backend m (Maybe val)
toSqlKey :: ToBackendKey SqlBackend record => Int64 -> Key record

If I understand correctly, backend and PersistEntityBackend val need to be the same type, but toSqlKey enforces the SqlBackend constraint, so the types mismatch. My intuition tells me that PersistentEntityBackend (Entity Record) should be SqlBackend, but evidently I'm wrong there. I don't know why or how, though.

Anyway, I don't know if I'm right or wrong in that analysis, but either way, I'm not sure how to fix this or what the proper way of doing this is. How can/should I be getting a record from my database given an integer?

Community
  • 1
  • 1
Alexis King
  • 43,109
  • 15
  • 131
  • 205
  • it's one of these things that's hard from afar - but I don't think you need the `liftIO $ return row` at all - just `runDB $ get (toSqlKey recordId)` should be enough – Random Dev Nov 26 '15 at 08:17
  • @Carsten Ah! You're quite right about that second point—I was doing more before I stripped it down in an attempt to debug the issue. As for the code, I think this is pretty much everything that's relevant, given that it doesn't actually depend on any aspect of my model. Let me know if you need more context, but I was able to drop this into an isolated sandbox and reproduce the error pretty trivially. – Alexis King Nov 26 '15 at 08:23
  • would you mind trying something (it would take me some time to get everything into a sandbox): change `runDB` to just `SqlPersistT IO a -> IO a` and remove the type-signature from `recordByKey` because I am really sure that this should work and it's most likely some of the nasty type stuff in there with the monad-transformers – Random Dev Nov 26 '15 at 08:29
  • did you try ``toPersistKey`` from ``Database.Persist.Base``? – Tobi Nary Nov 26 '15 at 08:31
  • @JanGreve `Database.Persist.Base` doesn't seem to exist in any remotely recent version of persist...? – Alexis King Nov 26 '15 at 08:41
  • @Carsten I can't easily do that because `runSqlite` includes those monad transformers, so I don't really have control over that type. – Alexis King Nov 26 '15 at 08:43
  • @AlexisKing, oops. My bad. Sorry. Google had some deep link into a old haddock. – Tobi Nary Nov 26 '15 at 08:43
  • To test @Carstens hinch, could you remove the type signature and test if the inferencer can figure it out and if there is something wrong? – Tobi Nary Nov 26 '15 at 08:54
  • @JanGreve yep that was the idea - and yep it works (in my experience that's the best way to use persistent: don't give signatures and look afterwards) – Random Dev Nov 26 '15 at 08:55
  • @Carsten That is mine, too :) Glad to see not being alien with that approach :) – Tobi Nary Nov 26 '15 at 08:57

2 Answers2

2

this works for me (may depend on the versions of your packages ... sadly):

{-# LANGUAGE FlexibleContexts #-}
module Stackoverlflow where

import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Logger(NoLoggingT)
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Resource (ResourceT)
import Data.Int (Int64)
import Data.Text (pack)
import Database.Persist.Class (ToBackendKey, get)
import Database.Persist.Sql (SqlBackend, SqlPersistT, toSqlKey)
import Database.Persist.Sqlite(runSqlite)
import Database.Persist.Types (Entity)
import System.Environment (getEnv)

runDB :: (MonadBaseControl IO m, MonadIO m) =>
        (SqlPersistT (NoLoggingT (ResourceT m))) a -> m a
runDB actions = do
  filename <- liftIO $ getEnv "SQLITE_FILENAME"
  runSqlite (pack filename) actions

getRecordByKey :: (MonadIO m, ToBackendKey SqlBackend val, MonadBaseControl IO m) =>
                 Int64 -> m (Maybe val)
getRecordByKey recordId = runDB $ get (toSqlKey recordId)

as you can see I just added alot of types annotations (well GHC did after I removed the signatures and ask it to tell me ;))

also note that I don't have your Record so you should easily be able to get rid of the ... val stuff!

Random Dev
  • 51,810
  • 9
  • 92
  • 119
  • Ah, I see, we have the same approach to complex type definitions;) – Tobi Nary Nov 26 '15 at 08:56
  • Well, that's one way to do it... and probably the better way. As a result of this, though, I think I found out what the original problem was: `get` doesn't return a `Maybe (Entity Record)`, it returns a `Maybe Record`. After all that, *that* was the problem. Even with that knowledge, the error message is still incomprehensible, but hey, so it goes. :P – Alexis King Nov 26 '15 at 09:06
  • Ohh, I see it now. The issue was that `PersistEntityBackend (Entity Record)` doesn't exist, but `PersistEntityBackend Record` does, and it is a `SqlBackend`. Thanks for your help! – Alexis King Nov 26 '15 at 09:08
  • no problem: happens to me all the time - don't judge me but for persistent and co. I let ghc-mod/emacs write the signatures (it's much smarter than me ^^) – Random Dev Nov 26 '15 at 09:18
1

And what about making Key record directly an instance of FromText/ToText and use the keys in the URL directly?

{-# LANGUAGE FlexibleContexts               #-}
{-# LANGUAGE UndecidableInstances               #-}
instance ToBackendKey SqlBackend record => FromText (Key record) where
  fromText k = toSqlKey <$> fromText k
instance ToBackendKey SqlBackend record => ToText (Key record) where
  toText = toText . fromSqlKey
ondra
  • 9,122
  • 1
  • 25
  • 34