I'm writing a SQL combinator which allows SQL fragments to be composed as a Monoid. I have roughly a type like this (this is a simplified implementation) :
data SQLFragment = { selects :: [String], froms :[String], wheres :: [String]}
instance Monoid SQL Fragment where ...
This allows to me to combine easily bits of SQL I use often and do things like :
email = select "email" <> from "user"
name = select "name" <> from "user"
administrators = from "user" <> where_ "isAdmin = 1"
toSql $ email <> name <> administrators
=> "SELECT email, name FROM user WHERE isAdmin = 1"
That works very well and I'm happy with it.
Now I use MySQL.Simple
and to be executed it needs to know the type of a row.
main = do
conn <- SQL.connect connectInfo
rows <- SQL.query_ conn $ toSql (email <> name <> administrators)
forM_ (rows :: [(String, String)]) print
Which is why I need the
rows :: [(String, String)]
To avoid to have add manually this explicit (and useless) type signature I had the following idea :
I add a phantom type to my SQLFragment
and use it to force the type of the query_
function. So I could have something like this
email = select "email" <> from "user" :: SQLFragment String
name = select "name" <> from "user" :: SQLFragment String
administrators = from "user" <> where_ "isAdmin = 1" :: SQLFragment ()
etc ...
Then I can do
query_ :: SQL.Connection -> SQLFragment a -> IO [a]
query_ con q = SQL.query_ conn (toSql q)
My first problem is I can't use <>
anymore because SQLFragment a
is not a Monoid
anymore.
The second is how do I implement my new <>
to compute correctly the phantom type ?
I found a way which I think is ugly and I hope there is a much better solution.
I created a typed version
of SQLFragment
and use phantom attribute which is a HList
.
data TQuery e = TQuery
{ fragment :: SQLFragment
, doNotUse :: e
}
then I create a new typed
operator : !<>!
which I don't undestand the type signature so I don't write it
(TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q') (e.*.e')
Now I can't combine my typed fragment and keep track of the type (even though it's not yet a tuple but something really weird).
To convert this weird type to a tuple I create a type family :
type family Result e :: *
and instantiate it for some tuples
Another solution would be probably to use a type family and write manually every combination of tuples
type instance Result (HList '[a]) = (SQL.Only a)
type instance Result (HList '[HList '[a], b]) = (a, b)
type instance Result (HList '[HList '[HList '[a], b], c]) = (a, b, c)
type instance Result (HList '[HList '[HList '[HList '[a], b], c], d]) = (a, b, c, d)
type instance Result (HList '[HList '[HList '[HList '[HList '[a], b], c], d], e]) = (a, b, c,d, e)
etc ...
And that works. I can write my function using the Result
family
execute :: (SQL.QueryResults (Result e)) =>
SQL.Connection -> TQuery e -> SQL.Connection -> IO [Result e]
execute conn (TQuery q _ ) = SQL.query_ conn (toSql q)
My main program looks like :
email = TQuery (select "email" <> from "user") ((undefined :: String ) .*. HNil)
name = TQuery (select "name" <> from "user" ) ((undefined :: String ) .*. HNil)
administrators = TQuery (from "user" <> where_ "isAdmin = 1") (HNil)
main = do
conn <- SQL.connect connectInfo
rows <- execute conn $ email !<>! name !<>! administrators
forM_ rows print
and it works!
However is there a better way to do it , especially without using HList
and if possible less extensions as possible ?
If I "hide" somehow the phantom type (so I can have a real Monoid
and be able to use <>
instead of !<>!
) is there a way to get the type back?
Consider using haskelldb which has the typed database query problem figured out. The records in haskelldb work fine, but they don't provide many operations, and the types are longer since they don't use -XDataKinds
.
I have some suggestions for your current code:
newtype TQuery (e :: [*]) = TQuery SQLFragment
is better because the e
is actually a phantom type. Then your append operation can look like:
(!<>!) :: TQuery a -> TQuery b -> TQuery (HAppendR a b)
TQuery a !<>! TQuery b = TQuery (a <> b)
Result
then looks much cleaner:
type family Result (a :: [*])
type instance Result '[a]) = (SQL.Only a)
type instance Result '[a, b] = (a, b)
type instance Result '[a, b, c] = (a, b, c)
type instance Result '[a, b, c, d] = (a, b, c, d)
type instance Result '[a, b, c, d, e] = (a, b, c,d, e)
-- so you might match the 10-tuple mysql-simple offers
If you want to stay with HList+mysql-simple and duplicate parts of haskelldb, an instance QueryResults (Record r)
is probably appropriate. An unreleased Read instance solves a very
similar problem and might be worth looking at.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With