yesodweb / yesodweb/persistent

Unify approaches to polymorphism

Open
#1,302 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Haskell
Stars
486
Forks
306
PR merge metrics
No merged PRs in 30d

Description

Right now, persistent has a somewhat confusing array of techniques for providing polymorphism.

The intent is to allow queries and database actions to be backend agnostic.

f 
    :: (MonadIO m, PersistStoreRead backend) 
    => ReaderT backend m a

This function works for any backend that implements the PersistStoreRead class.

That class has this (simplified) definition:

class PersistStoreRead backend where
    get 
        :: ( MonadIO m
            , PersistEntity record
            , PersistEntityBackend record ~ backend
            )
        => Key entity
        -> ReaderT backend m (Maybe record)

This works pretty well - basically every possible persistent backend can support this.

However, we come to a problem with upsert. This is not natively handled by all backends, so we provide somewhat dumb fallbacks, and allow instances to provide better behavior.

Simplifying a bit, we have:

class (PersistStore backend) => PersistUnique backend where
    upsertBy 
        :: (MonadIO m, PersistRecordBackend record backend)
        => Unique record
        -> record
        -> [Update record]
        -> ReaderT backend m (Entity record)
    upsertBy = defaultUpsertBy 

defaultUpsertBy performs two database actions, while an efficient override might be able to do it in a single database action.

So how does SqlBackend work? Again, simplifying it a bit, we have:

instance PersistUnique SqlBackend where
    upsertBy uniqueKey record updates = do
        conn <- ask
        case connUpsertSql conn of
            Nothing -> 
                defaultUpsertBy uniqueKey record updates
            Just upsertSql -> do
                -- run the optimized action

So, SqlBackend, as it happens, is not guaranteed to have an efficient implementation of upsert. So we have a record field like:

data SqlBackend = SqlBackend
    { connUpsertSql :: Maybe MkUpsertSql
    }

If we're producing a backend for Postgres, which does have an efficient upsert, then we put a Just connUpsertSqlFunction in the record. If we're producing a backend for MySql (which does not yet support it? idk) then we write Nothing for the field, and we use the default slow implementation.

We've now got two approaches for polymorphism - one is adding Maybe fields to a record, and the other is adding a type class for the relevant operations. This is unsatisfying.

Considerations

We want:

  1. To provide a uniform interface for database access.
  2. To allow specific database backends to provide more efficient implementations of operations.
  3. For people to write programs that can operate against different database backends.

What isn't great:

  1. Lots of different ways to accomplish the same basic thing
  2. Confusion around how this stuff all works
  3. Friction around adding new features in a backwards-compatible way.

The PR #1298 adds a new type class and a new record field to SqlBackend to support streaming rows. By all accounts, it's doing everything right - the existing conventions are followed perfectly.

Alternatives

How else can we do this?

We want for eg MongoContext and SqlBackend to work, and we also want for postgresql and mysql to work for upsert, despite sharing a SqlBackend.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the PersistStoreRead and PersistUnique classes, SqlBackend's connUpsertSql field, and PR #1298's streaming-row changes. Compare the type-class and record-field approaches for backend-specific behavior; done means an agreed, documented design that covers MongoContext, SqlBackend, PostgreSQL, and MySQL without leaving the implementation scope ambiguous.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell
Domain
backend-api-design, databases
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.