-
Notifications
You must be signed in to change notification settings - Fork 293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generalizing insert
and gracefully handling defaults
#995
Comments
Not commenting on the design, but this would definitely improve persistent for us. We currently use triggers to set created/updated at:
and insert zeroed out UTCTimes:
Which is not too bad, but hacky. Because of it, we often don't include the I think this feature could also be useful when you add new fields with a default, that you usually don't care about but sometimes do. For example if I add an Potentially if this feature is well received, and this would be a huge breaking change and have some downsides, it would obviate Also as a side note just in case it was copied from real code, if you have:
You may get improved performance from uuid_generate_v1() or uuid_generate_v1mc(), since subsequent UUIDs will sort next to each in the database table (is my understanding) |
I like this idea! However, it would be a pretty huge breaking change as described at the moment. Do you think there might be a sensible route to having Also, it should always be possible to go from I'd suggest keeping the |
Yeah - a function like
Yes, definitely. Good call!
I'm not inclined to remove
where But that's only one way to approach data factorization. |
The breaking change could be phased in over time: V1: Add But I think given that for most cases existing code will just work, I'd be in favor of a straight breaking change |
I would love to see this feature implemented. However I think it's important for this to effectively replace Currently we only deal with Keeping around |
I think that we can keep newtype Entity record = Entity { entityVal :: record }
entityKey :: (PersistEntity record) => Entity record -> Key record It is still useful as an instance hanger (as @hdgarrood mentioned), and this should be backwards compatible with most uses of it. |
In your description you outline two options, but that's not exhaustive. A third option is that when you're inserting you should be providing SQL expressions, not Haskell values. When you do that, you can insert a insert User { userCreatedAt = nowExpr } This is like writing I'm not sure this fits in with the design of persistent though, but thought it should be mentioned. My other thoughts are that having
But I only think it's insert User { userCreatedAt = Default } With that, I'd probably look at having some kind of "create default with essential data function": defaultUser :: NonDefaultFields -> User I should clarify that I'm not a |
I'm also not a persistent user currently, but am curious to see how this evolves. |
This is probably more in scope for a library like
Yeah I'd agree with that. Perhaps instead of using
I wish Haskell had better structural typing support in general such as As it stands I don't see a way to make this overly clean:
The latter scales better when you have lots of default fields, but it's still kinda ugly. Something like:
Would be much nicer but would require proper first class anonymous record support. |
Based on some preliminary testing, we can define: type family New record = result | result -> record and this has the same inference properties that we want, so that: data User = User { userName :: String, userAge :: Int, userId :: Int }
data NewUser = NewUser { newUserName :: String, newUserAge :: Int }
type instance New User = NewUser
class PersistEntity record where
insert :: New record -> IO record
instance PersistEntity User where
insert NewUser {..} = pure User
{ userName = newUserName
, userAge = newUserAge
, userId = random
} infers nicely and correctly. We can also have: data NoAutogen = NoAutogen { a :: Int, b :: Char }
type instance New NoAutogen = NoAutogen and the inference is A-OK. |
Marking myself in agreement with what has already been stated. However I would like to raise my support for keeping |
That’s what
|
@tysonzero moved my comment over to #1037 (comment) |
I think this is a great change. It would make adding "#281: syntax for updatedAt or other database performed updates" a breeze |
Raise your hand if you've been frustrated by needing to specify a time or use
Maybe
forcreatedAt UTCTime
fields.If you're one of the few folks that hasn't, well, let me introduce the problem.
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 adefault
in the database, we don't need to specify it. This pattern is so common that we have theEntity
type, which includes theKey entity
for theentity
.Then we want to record when a user is created.
Database users are accustomed to writing a schema like:
And the
persistent
library happily supports thedefault=
syntax, which does The Right Thing with migrations.Unfortunately, we reach a problem when we go to insert a new value.
insert
's type isinsert :: entity -> SqlPersistT m (Key entity)
. AnduserCreatedAt :: UTCTime
- it's a required field!! So now we have two options:Make the timestamp in Haskell and forego the database default, writing:
But this gets really annoying as the
User
gets 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
:The database defaulting mechanism works out here, hooray. But now we have to care about
Maybe
at every use site! Gross.So here's my plan:
PersistEntity
class with an associated typeNew
:insert
to be: `insert :: (PersistEntity entity) => New entity -> SqlPersistT m (Key entity)default=
clauses, definetype New User = User
default=
clauses,NewUser
with the required fields of aUser
andMaybe
fields for anydefault
able typestype New User = NewUser
In the QQ syntax, we can introduce a new attribute
!default-only
, and any field with adefault-only
attribute does not include that field in theNew
type. So we could write this: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 theNewUser
, and an attribute!allow-override
, which puts aMaybe
in theNew
record.This also helps solve some of the issues with custom
Id
andPrimary
declarations. For example, consider this Person type with aUUID
: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 definetype New Person = NewPerson
:But if we specify a default, then we can have this pairing:
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.
The text was updated successfully, but these errors were encountered: