lykahb / groundhog

This library maps datatypes to a relational model, in a way similar to what ORM libraries do in OOP. See the tutorial https://www.schoolofhaskell.com/user/lykahb/groundhog for introduction
http://hackage.haskell.org/package/groundhog
176 stars 39 forks source link

Embedded sum types #5

Open lykahb opened 11 years ago

lykahb commented 11 years ago

Implementing embedded sum types will allow more flexible mapping between types and DB. Field types like (Either Int String) or (Maybe MyEmbedded) where Maybe is treated as another embedded sum type will become possible.

We have to specify precise behavior for the following operations before starting implementation. The main obstacle for the feature is semantics of accessing subfields that have a sum type in their chain. Such subfields can appear in projections, conditions, updates, etc.

Mapping embedded sum datatype to columns:

The discriminator column and nullable columns for the fields from all constructors.

Conversion to PersistValues:

toPersistValues returns: discriminator value, NULLs for the columns in the other constructors. Note that since we cannot call toPersistValues for values in the other constructors, we must know the number of columns to pad values with nulls. To get this number we can either analyze dbType (slow) or create a new PersistField function numberOfColumns.

Creating data values:

fromPersistValues chooses constructor basing on the discriminator value, skips the NULLs for the other constructors using number of columns for their field types.

Example:

data Sum = One Int | Two String | Three (Int, String) columns: sumDiscr INTEGER NOT NULL, one INTEGER, two VARCHAR, three#val0 INTEGER, three#val1 VARCHAR toPersistValues (Two "abc") -> [PersistInt64 1, PersistNull, PersistString "abc", PersistNull, PersistNull]

Subfields:

Consider data MyEntity = MyEntity {myEither :: Either Int Sum} project (MyEither ~> LeftSelector) project (MyEither ~> RightSelector). In this case all values of Sum including its discriminator may be NULLs.

Since any column of a embedded sum type can be NULL, the final type of selector chains must have Maybe (or another optional type), eg, SubField v c (Maybe a). Example: MyEither ~> LeftSelector :: SubField MyEntity MyEntityConstructor (Maybe Int) MyEither ~> RightSelector :: SubField MyEntity MyEntityConstructor (Maybe Sum) MyEither ~> RightSelector ~> TwoSelector :: SubField MyEntity MyEntityConstructor (Maybe String)

instance Embedded (Either a b) where data Selector (Either a b) c where LeftSelector :: Selector v c (Maybe a) RightSelector :: Selector v c (Maybe b) selectorNum LeftSelector = 0 selectorNum RightSelector = 1

But in this case to access nested sum types, operator ~> should be applicable both to (emb :: f db r a) and (emb :: f db r (Maybe a)) which requires some class hackery. Also we will have nested Maybes and it is not clear how a single PersistField (Maybe a) instance can handle them.

If the fields we select are nullable themselves, we should distinguish between nulls in case when our type has different constructor and when constructor matches, but field is null. Either Int (Maybe Int). To do this we may select the discriminator column for the innermost embedded type. But in this case the number of columns in comparison expressions will not match.

Another approach for projection is to use another subfield type for a chain that contains a sum type. A separate Projection instance will add Maybe. Something like (Projection (SumTypeSubField a) db r (Maybe a)).

mightybyte commented 11 years ago

+1 for this. I was looking at using groundhog the other day and embedded sum types were exactly what I needed.

wraithm commented 9 years ago

Yeah! This would be great :)

lykahb commented 9 years ago

I think it would be a useful feature but it has many cases where behavior is undefined. For example when we try to access a value inside of an embedded type created with a different constructor:

k <- insert $ MyEntity $ Right (One 1)
update [MyEither ~> LeftSelector =. 1] $ AutoKeyField ==. k