Skip to content

Support "inserting" Model values that are isomorphic to a real underlying Entity #6

@cdparks

Description

@cdparks

TLDR; It would be nice to be able to specify a non-database-level type in an HGraph and have an equivalent value of a different type be actually inserted into the database.

Long Version

Sometimes the database-level representation of a value is difficult to use or fails to properly constrain its inhabitants. For example, suppose we have a database table with two nullable fields, but at the application level we expect to always have values for both fields or neither field:

share [mkPersist sqlSettings, mkMigrate "migration"] [persistLowerCase|
  Raw sql=foo
    bar Maybe Int
    baz Maybe Int
|]

By default, Raw would look like this in Haskell:

  data Raw = Raw { rawBar :: Maybe Int, rawBaz :: Maybe Int }

However, this admits more values than we want. We might prefer this representation:

  newtype Nice = Nice (Maybe (Int, Int))

Then we can use a type family and some other machinery...

type family RawEntity r :: *

data Model record
  = Model
  { modelKey :: Key (RawEntity record)
  , modelVal :: record
  }

-- Horrible lawless typeclass forgive meeee
class IsModel record where
  fromRaw :: RawEntity record -> record
  toRaw   :: record -> RawEntity record

fromRawEntity
  :: (PersistEntity (RawEntity record), IsModel record)
  => Entity (RawEntity record)
  -> Model record
fromRawEntity (Entity key val) = Model key (fromRaw val)

toRawEntity
  :: (PersistEntity (RawEntity record), IsModel record)
  => Model record
  -> Entity (RawEntity record)
toRawEntity (Model key val) = Entity key (toRaw val)

... to convert between an Entity Raw and a Model Nice:

  type instance RawEntity Nice = Raw
  instance IsModel Nice where
    fromRaw (Raw mBar mBaz) = Nice (liftA2 (,) mBar mBaz)
    toRaw (Nice mPair) = Raw (fst <$> mPair) (snd <$> mPair)

  nice = fromRawEntity (Entity someRawId (Raw Nothing Nothing))
  raw  = toRawEntity (PseudoEntity someRawId (Nice Nothing))

Anyhoo, I made a half-hearted attempt to write an InsertElement instance for something like this:

instance
  (IsModel record, RawEntity record ~ raw, InsertElement raw (Entity raw) backend baseBackend) =>
  InsertElement record (Model record) backend baseBackend where
  insertElement model = do
    Entity key raw <- insertElement $ toRaw model
    return $ Model key $ fromRaw raw

But that blows up with a fundeps conflict because the other instance we use looks like this:

instance
  (PersistEntity a, PersistEntityBackend a ~ baseBackend, BaseBackend backend ~ baseBackend, PersistStoreWrite backend) =>
  InsertElement a (Entity a) backend baseBackend where ...

In InsertElement a b backend baseBackend, a is supposed to uniquely determine b, and I'm trying to add an instance where b is Model a for a, but we've already fixed b as Entity a for a.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions