Skip to content
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

deriveEsqueletoRecord does not handle polymorphic records #383

Open
parsonsmatt opened this issue Dec 20, 2023 · 2 comments
Open

deriveEsqueletoRecord does not handle polymorphic records #383

parsonsmatt opened this issue Dec 20, 2023 · 2 comments

Comments

@parsonsmatt
Copy link
Collaborator

Consider the type:

data Record key = Record { key :: key, column :: Int }

We can't write deriveEsqueletoRecord here because there's a type error. We can't provide concrete things because deriveEsqueletoRecord takes a Name and not a Type, so we can't write deriveEsqueletoRecord @(Record Foo).

Ideally we can support polymorphic records.

@curranosaurus
Copy link

curranosaurus commented Jan 10, 2024

I hand-derived some instances that ended up working out for this case. See for example the following minimal example.

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}

module Lib where

import Control.Monad.Trans.State.Strict (evalStateT)
import Data.Bifunctor (first)
import Data.String (fromString)
import Database.Esqueleto.Experimental
import Database.Esqueleto.Record
import Database.Esqueleto.Internal.Internal
import Data.Proxy

data Record key = Record { key :: key, val :: Int }

data SqlRecord key = SqlRecord { key :: SqlExpr (Value key), val :: SqlExpr (Value Int) }

data SqlMaybeRecord key = SqlMaybeRecord { key :: SqlExpr (Value (Maybe key)), val :: SqlExpr (Value (Maybe Int)) }

instance PersistField (Key rec) => SqlSelect (SqlRecord (Key rec)) (Record (Key rec)) where
  sqlSelectCols identInfo SqlRecord { key, val } =
    sqlSelectCols identInfo (key :& val)
  sqlSelectColCount _ = sqlSelectColCount
    ( Proxy
        @( SqlExpr (Value (Key rec))
                :& SqlExpr (Value Int)
         )
    )
  sqlSelectProcessRow columns =
    first
      (fromString "Failed to parse Record: " <>)
      (evalStateT process columns)
    where
      process = do
        Value key <- takeColumns @(SqlExpr (Value (Key rec)))
        Value val <- takeColumns @(SqlExpr (Value Int))
        pure Record { key, val }

instance ToAliasReference (SqlRecord (Key rec)) where
  toAliasReference ident SqlRecord { key, val } =
    SqlRecord <$> toAliasReference ident key <*> toAliasReference ident val

instance ToAlias (SqlRecord (Key rec)) where
  toAlias SqlRecord { key, val } =
    SqlRecord <$> toAlias key <*> toAlias val

instance ToMaybe (SqlRecord (Key rec)) where
  type ToMaybeT (SqlRecord (Key rec)) = SqlMaybeRecord (Key rec)
  toMaybe SqlRecord { key, val } =
    SqlMaybeRecord
      { key = just key
      , val = just val
      }

I'm not actually certain if the Key rec thing is necessary, I added it in because it doesn't constrain my usecase and I figured it might make some of these instances more deducible.

@parsonsmatt
Copy link
Collaborator Author

Hmm. With a totally polymorphic field like key :: key, it's impossible to know how it will be instantiated, and therefore, what code to use. The three cases are SqlExpr (Value a), SqlExpr (Entity rec), and (for other esqueleto records) key (since the SqlExpr is baked into the record itself).

I think you'd need the type parameter to not affect the conversion logic. Or, to delegate more fully. Consider:

data R k = R { key :: k }

data SqlR k = SqlR { key :: k }

data SqlMaybeR k = SqlMaybeR { key :: k }

Then I think our instance becomes:

instance (SqlSelect sqlK valueK) => SqlSelect (SqlR sqlK) (R valueK)

Which should work in more generality.

I think a much easier alternative is if you can further constrain the type of key. Consider:

data Record a = Record { key :: Key a }

Now, we know that Key a is always going to be wrapped in SqlExpr (Value (Key a)), so we can work with it more easily.

That may also simplify the rest of your code, as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants