yesodweb / yesodweb/persistent
Generalizing `insert` and gracefully handling defaults
Nobody has claimed this yet.
- Dominant language
- Haskell
- Stars
- 486
- Forks
- 306
- PR merge metrics
- No merged PRs in 30d
Description
Raise your hand if you've been frustrated by needing to specify a time or use Maybe for createdAt UTCTime fields.
If you're one of the few folks that hasn't, well, let me introduce the problem.
User
name String
This is fine and easy. To insert a new user into the database, we write insert User { userName = "Matt" }. There is an implied surrogate key that is associated, and it should be an auto-incrementing integer. Because it has a default in the database, we don't need to specify it. This pattern is so common that we have the Entity type, which includes the Key entity for the entity.
Then we want to record when a user is created.
User
name String
createdAt UTCTIme
Database users are accustomed to writing a schema like:
CREATE TABLE user (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT=NOW()
);
And the persistent library happily supports the default= syntax, which does The Right Thing with migrations.
User
name String
createdAt UTCTIme default=NOW()
Unfortunately, we reach a problem when we go to insert a new value. insert's type is insert :: entity -> SqlPersistT m (Key entity). And userCreatedAt :: UTCTime - it's a required field!! So now we have two options:
-
Make the timestamp in Haskell and forego the database default, writing:
newUser :: String -> SqlPersistT m UserId newUser userName = do userCreatedAt <- liftIO getCurrentTime insert User {..}But this gets really annoying as the
Usergets additional arguments, and people really don't like writing this out when the database defaulting mechanism is designed to provide exactly this. -
Make the definition nullable and provide
Nothing:insert User { userName = "Matt", userCreatedAt = Nothing }The database defaulting mechanism works out here, hooray. But now we have to care about
Maybeat every use site! Gross.
So here's my plan:
- Extend the
PersistEntityclass with an associated typeNew:class PersistEntity entity where type New entity :: * - Change the signature of
insertto be: `insert :: (PersistEntity entity) => New entity -> SqlPersistT m (Key entity) - For a definition with no
default=clauses, definetype New User = User - For a definition with
default=clauses, - define a datatype
NewUserwith the required fields of aUserandMaybefields for anydefaultable types - define
type New User = NewUser
In the QQ syntax, we can introduce a new attribute !default-only, and any field with a default-only attribute does not include that field in the New type. So we could write this:
User
name String
createdAt UTCTime default=NOW() !default-only
and we'd be able to write simply insert NewUser { newUserName = "Matt" } and it Just Works, precisely like you'd want it to.
Alternatively, we might want to default to default= things not being in the NewUser, and an attribute !allow-override, which puts a Maybe in the New record.
This also helps solve some of the issues with custom Id and Primary declarations. For example, consider this Person type with a UUID:
Person
Id UUID
name String
With this example, it's a SQL-time error to do insert Person { personName "Matt" } - there won't be a default given for the UUID. So in this case, we actually want to define type New Person = NewPerson:
data NewPerson = NewPerson
{ newPersonId :: UUID
, newPersonName :: String
}
But if we specify a default, then we can have this pairing:
Person
Id UUID default=uuid_generate_v4()
name String
instance PersistEntity Person where
type New Person = Person
This design seems to work pretty well to solve all the pain points I experience with this stuff. I'm curious if anyone else has any input on pain points that may be addressed by this, or if there are design flaws that I haven't considered.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the PersistEntity class and the current insert signature, then compare the proposed New associated type with default=, !default-only, custom Id, and Primary declarations described here. Resolve the design alternatives and document the chosen behavior before implementing it; done means inserts handle required, defaultable, and custom-key fields without the stated workarounds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- haskell
- Domain
- databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100