Making HaskellDB slightly more type-safe

I was just discussing HaskellDB’s major flaws with Oliver Charles and I noted that one huge problem is that the type of update does not restrict the record given to make the update. Its type is

update :: (ShowLabels s, ToPrimExprs s)
       => Database             -- ^ Database.
       -> Table r              -- ^ Entity.
       -> (Rel r -> Expr Bool) -- ^ Predicate.
       -> (Rel r -> Record s)  -- ^ Updates.
       -> IO ()

which is straight-forward enough. The problem is the “updates” argument, which will allow me to write a bogus field that does not belong to r, like

update mytable
       (\row -> row!field .==. whatever)
       (\row -> badfield <<- whatever)

This problem actually bit me in the ass in production once before. That is not an exciting bug to have.

So I thought, we need to prove that for the type above, s <: r (read as “s is a subtype of r”). How do we express that? How about a type class.

The type-class can be

class Subset sub super

But how to implement it? Well, we need to say that for every field of sub, that field is also a field of super. That’s made easy for us, because HaskellDB already has a HasField field record class for exactly that!

instance (HasField field super,Subset sub super) =>
         Subset (RecCons field typ sub) super

This is similar to traversing a list at the value level, with RecCons field type sub like a pattern-match on the current element. You can read it as:

sub is a subset of super, if super has the field of the head of the list, and the tail is a subset of super

So far so good. Now we need a base case, to cover the last element of the list:

instance Subset RecNil super

And we’re done. Update now becomes

update :: (Subset s r,ShowLabels s, ToPrimExprs s)
       => Database             -- ^ Database.
       -> Table r              -- ^ Entity.
       -> (Rel r -> Expr Bool) -- ^ Predicate.
       -> (Rel r -> Record s)  -- ^ Updates.
       -> IO ()

Testing this on my codebase actually found a bug in which I was using the wrong field!

I will send this to the maintainer of HaskellDB as it’s a glaring bug waiting to happen to someone.