valderman / selda

A type-safe, high-level SQL library for Haskell
https://selda.link
MIT License
478 stars 58 forks source link

create Assignments for new Row from existing Row #146

Open cdepillabout opened 3 years ago

cdepillabout commented 3 years ago

I'd like to be able to create an Assignment list to pass to new from an existing Row.

For example, imagine I have two datatypes like the following:

data Foo = Foo
  { a :: Int
  , b :: Int
  }
  deriving stock (Generic, Show)
  deriving anyclass (SqlRow)

data Bar = Bar
  { a :: Int
  , b :: Int
  , c :: Int
  , d :: Int
  }
  deriving stock (Generic, Show)
  deriving anyclass (SqlRow)

As you can see, these two types are very similar. Bar has all the fields of Foo, plus a field called c and d.

I'd like to have a function with the following signature:

assignmentsFor :: forall b s a. (Relational a, Relational b) => Row s a -> [Assignments s b]

If you allow the types to be specialized, I want to use it with the following specialized type:

assignmentsFor :: Row s Foo -> [Assignments s Bar]

Essentially, I want to convert a Foo into a list of assignments for creating a Bar.

I want to use it like the following:

f :: m [Bar]
f = query $ do
  foo <- select (fooTable :: Table Foo)
  pure $ new $
    assignmentsFor foo <>
    [ #c := 3
    , #d := 4
    ]

I'm new to Selda, so I'm not entirely up to speed on the API, but it doesn't appear that Selda exposes enough of the internal API to allow me to write this function?

However, my guess is that Selda does have all the required information to be able to write this assignmentsFor function.

cdepillabout commented 3 years ago

After playing around with it a little more, I was able to find a hacky, unsafe way to do what I want:

unsafeRowSelect
  :: forall row2 row1 s a
   . SqlType a
  => Row s row1
  -> Selector row1 a
  -> Assignment s row2
unsafeRowSelect r selec = coerce selec := r ! selec -- I think the coerce here is the unsafe part

class MapOverSelectors a t where
  mapOverSelectors
    :: forall s u
     . (forall x. SqlType x => Selector t x -> Assignment s u)
    -> a
    -> [Assignment s u]

instance (MapOverSelectors a t, MapOverSelectors b t) => MapOverSelectors (a :*: b) t where
  mapOverSelectors
    :: forall s u. (forall x. SqlType x => Selector t x -> Assignment s u)
    -> (a :*: b)
    -> [Assignment s u]
  mapOverSelectors f (a :*: b) = mapOverSelectors f a <> mapOverSelectors f b

instance SqlType a => MapOverSelectors (Selector t a) t where
  mapOverSelectors
    :: forall s u
     . (forall x. SqlType x => Selector t x -> Assignment s u)
    -> Selector t a
    -> [Assignment s u]
  mapOverSelectors f s = [f s]

unsafeRowToAssignments
  :: forall u s t
   . (Relational t, MapOverSelectors (Selectors t) t, GSelectors t (Rep t))
  => Table t
  -> Row s t
  -> [Assignment s u]
unsafeRowToAssignments tbl row =
  let allSelectors = selectors tbl
  in mapOverSelectors (\s -> unsafeRowSelect row s :: Assignment s u) allSelectors

I use it like the following:

f :: m [Bar]
f = query $ do
  fooRow <- select (fooTable :: Table Foo)
  pure $ new $
    (unsafeRowToAssignments fooTable fooRow :: [Assignment _ Bar]) <>
    [ #c := 3
    , #d := 4
    ]

This works, but it is unsafe. I have to manually make sure the rows for Foo and Bar line up.

However, it seems like this could be made safe if you took advantage of some of the internals of Selda.


On a related note, I think Selector needs some type roles. In general, it does not appear safe to coerce from Selector t a to Selector u b.

Without explicitly specifying type roles, I believe Selector gets the roles Selector phantom phantom, which means any sort of coercion is possible. However, I think we probably want to set the roles Selector representational representational.

Of course, setting roles would make my trick here no longer work without unsafeCoerce, but that is probably a good thing.

valderman commented 3 years ago

This would indeed be useful, but I'm thinking it might be a bit more user friendly to forego the intermediate Assignment list completely and instead add a function like columnsFrom :: Row s a -> Row s b and then use it together with with?

In this way, your example would look something like this:

f :: m [Bar]
f = query $ do
  fooRow <- select (fooTable :: Table Foo)
  pure $ columnsFrom fooRow `with`
    [ #c := 3
    , #d := 4
    ]

...which I think looks a bit nicer. This would have the added bonus of making it clearer that the assignment list overrides the columns from the old row if a column is present in both. What do you think?

cdepillabout commented 3 years ago

a bit more user friendly to forego the intermediate Assignment list completely and instead add a function like columnsFrom :: Row s a -> Row s b and then use it together with with?

That sounds like a much nicer API!


Oh, and maybe I should have created a new issue for this, but I think the more important part of this issue is about adding roles to Selector. Without proper roles, it is possible to coerce a Selector to types that don't make any sense. Without proper roles, there is nothing stopping you from doing something that's not type safe.