Skip to content
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

PersistLiteral support for SQL keywords #1122

Merged
merged 16 commits into from
Nov 4, 2020

Conversation

ldub
Copy link
Contributor

@ldub ldub commented Sep 9, 2020

When mapping to a generated column such as the following, the toPersistValue must output DEFAULT, without quotes.

example_column text GENERATED ALWAYS AS (COALESCE(field_one, field_two)) STORED

Currently, PersistDbSpecific will quote things it outputs (in persistent-postgresql), but an insert of 'DEFAULT' would be rejected by Postgres.

This PR adds support for a PersistLiteral constructor on PersistValue, meant to explicitly never escape its output in any database system. I've added an implementation for MySQL, PostgreSQL, and sqlite, as well as a test that uses a schema with a GENERATED column. My colleague @friedbrice said he would be happy to look into implementing this for the other db systems that I haven't added support for.

A note on tests:
GENERATED columns are supported in MySQL, PostgreSQL, and sqlite. While sqlite's documentation says its available from 3.31.0, my testing shows that it does not actually work in 3.31.0:

sqlite version testing output

does not work with current sqlite 3.31.0

❯ /usr/local/opt/sqlite/bin/sqlite3 --version
3.30.1 2019-10-10 20:19:45 18db032d058f1436ce3dea84081f4ee5a0f2259ad97301d43c426bc7f3df1b0b

❯ /usr/local/opt/sqlite/bin/sqlite3
SQLite version 3.30.1 2019-10-10 20:19:45
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> CREATE TABLE persist_literal_field_test_table (id serial primary key, field_one text, field_two text, field_three text GENERATED ALWAYS AS (COALESCE(field_one, field_two)) STORED);
Error: near "AS": syntax error
sqlite>

works with newer sqlite 3.33.0

❯ /usr/local/opt/sqlite/bin/sqlite3 --version
3.33.0 2020-08-14 13:23:32 fca8dc8b578f215a969cd899336378966156154710873e68b3d9ac5881b0ff3f

❯ sqlite3
SQLite version 3.33.0 2020-08-14 13:23:32
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> CREATE TABLE persist_literal_field_test_table (id serial primary key, field_one text, field_two text, field_three text GENERATED ALWAYS AS (COALESCE(field_one, field_two)) STORED);
sqlite>

If the sqlite.c "amalgamation" were to be updated to the latest 3.33.0, the test for sqlite will work. Currently it fails. Something need to be done about that and I would appreciate suggestions.

After submitting your PR:

  • Update the Changelog.md file with a link to your PR
  • Bumped the version number if there isn't an (unreleased) on the Changelog
  • Check that CI passes (or if it fails, for reasons unrelated to your change, like CI timeouts)

@@ -552,6 +554,19 @@ instance PGFF.FromField Unknown where
instance PGTF.ToField Unknown where
toField (Unknown a) = PGTF.Escape a

newtype UnknownLiteral = UnknownLiteral { unUnknownLiteral :: ByteString }
deriving (Eq, Show, Read, Ord, Typeable)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deriving (Eq, Show, Read, Ord, Typeable)
deriving (Eq, Show, Read, Ord)

Typeable is automatically derived since iirc GHC 7.10

newtype UnknownLiteral = UnknownLiteral { unUnknownLiteral :: ByteString }
deriving (Eq, Show, Read, Ord, Typeable)

instance PGFF.FromField UnknownLiteral where
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this instance used?

@MaxGabriel
Copy link
Member

This would also work for calling functions, right? eg if I want to set a column to the database’s NOW() value or some other function?

@MaxGabriel
Copy link
Member

MaxGabriel commented Sep 9, 2020

This is a fairly dangerous tool, since any dynamic value opens you up to SQL injection, right? Thoughts on adding more warning flags, or a name like UnsafeUnescapedLiteral?

(Is it possible to call the escaping function at all for this, eg to implement something like ARRAY(‘a’), or can this only be used for static values (eg DEFAULT)?

@friedbrice
Copy link
Contributor

friedbrice commented Sep 9, 2020

This would also work for calling functions, right? eg if I want to set a column to the database’s NOW() value or some other function?

Yes, it would be governed by specific instances of PersistField. For example

newtype ComputedColumn a = ComputedColumn a

instance PersistField a => PersistField (ComputedColumn a) where
    toPersistValue _ = PersistLiteral "DEFAULT"
    fromPersistValue x = ComputedColumn <$> fromPersistValue x

instance PersistFieldSql a => PersistFieldSql (ComputedColumn a) where
    sqlType _ = sqlType (Proxy @a)

And that's just because that's how Postgres allows you to insert a row into a table that has a computed column.

If you wanted to use now(), it'd be

newtype Timestamp = Timestamp UTCTime

instance PersistField Timestamp where
    toPersistValue _ = PersistLiteral "now()"
    fromPersistValue x = Timestamp <$> fromPersistValue x

instance PersistFieldSql Timestamp where
    sqlType _ = sqlType (Proxy @UTCTime)

@friedbrice
Copy link
Contributor

friedbrice commented Sep 9, 2020

This is a fairly dangerous tool, since any dynamic value opens you up to SQL injection, right? Thoughts on adding more warning flags, or a name like UnsafeUnescapedLiteral?

(Is it possible to call the escaping function at all for this, eg to implement something like ARRAY(‘a’), or can this only be used for static values (eg DEFAULT)?

In both use cases I give above, the literal happens to be completely static. I wonder if this suggests that a more-limited API would be better. Perhaps there's a way to use a quasiquote to ensures that the implementation of toPersistValue is completely determined at time of compile. (This would also involve hiding the PersistLiteral constructor, or perhaps exporting it from some Unsafe module with a "USE AT YOUR OWN RISK!" admonission.)

@friedbrice
Copy link
Contributor

@parsonsmatt I see you lurking in this thread. IIRC, you were working on something for timestamps. I'm interested to hear how this concept relates to your work. Does your work subsume this concept? Are they orthogonal?

@ldub
Copy link
Contributor Author

ldub commented Sep 9, 2020

In persistent currently, PersistDbSpecific only escapes values in persistent-postgresql but not persistent-mysql or persistent-sqlite. This explains @MaxGabriel 's questions about "I’m confused how [PersistLiteral can have] the same implementation as PersistDbSpecific" in the SQLite and MySQL implementations.

It may be that is instead a bug in persistent-postgresql's implementation of PersistDbSpecific. I thought this was not the case because the documentation shows an example of using it with PostGIS, however on second reading I think that implementation is broken because both toPersistValue and toPoint will escape the POINT(44 44) literal with single quotes.

I will try to demonstrate that this is broken by adding a test.

@parsonsmatt
Copy link
Collaborator

So, having a UnsafeUnescaped thing in PersistValue is necessary and useful, IMO.

Using it for computed columns is very, very, very interesting, but I think we need to be cautious about exactly how it is used. toPersistValue is not only used with insert but also in a few other cases. Consider an embedded entity:

Thing
    name String
    createdAt Timestamp

HasStuff
  thing Thing

The embedded Thing uses the PersistMap constructor:

entityToPersistValueHelper :: (PersistEntity record) => record -> PersistValue
entityToPersistValueHelper entity = PersistMap $ zip columnNames fieldsAsPersistValues
    where
        columnNames = map (unHaskellName . fieldHaskell) (entityFields (entityDef (Just entity)))
        fieldsAsPersistValues = map toPersistValue $ toPersistFields entity

Which means we get toPersistValue called on the entity before embedding it. This means that performing this insert:

insert (HasStuff (Thing "hello" (Timestamp dummyUTCTime)))

will result in the following record in the database:

sql> select * from has_stuff;

id | thing
---+-----
1  | {"thingName":"hello","thingCreatedAt":"now()"}

which is really not what we want.

@friedbrice
Copy link
Contributor

friedbrice commented Sep 9, 2020

Using it for computed columns is very, very, very interesting, but I think we need to be cautious about exactly how it is used. toPersistValue is not only used with insert but also in a few other cases.

My first instinct is the following workaround:

class PersistField a where
    toPersistValue :: a -> PersistValue
    fromPersistValue :: a -> Either Text PersistValue

    insertPersistValue :: a -> PersistValue
    insertPersistValue = toPersistValue

So users could provide their own insertPersistValue that does special database logic. I feel like this is a Bad Idea™, but I can't really see why immediately. What are people's thoughts?

@friedbrice
Copy link
Contributor

With the above class definition, we'd change insert so that it used insertPersistValue instead of toPersistValue, and we'd write our PersistField instances as so:

newtype ComputedColumn a = ComputedColumn a

instance PersistField a => PersistField (ComputedColumn a) where
    toPersistValue (ComputedColumn x) = toPersistValue x
    fromPersistValue x = ComputedColumn <$> fromPersistValue x
    insertPersistValue _ = PersistLiteral "DEFAULT"

instance PersistFieldSql a => PersistFieldSql (ComputedColumn a) where
    sqlType _ = sqlType (Proxy @a)


newtype Timestamp = Timestamp UTCTime

instance PersistField Timestamp where
    toPersistValue (Timestamp x) = toPersistValue x
    fromPersistValue x = Timestamp <$> fromPersistValue x
    insertPersistValue _ = PersistLiteral "now()"

instance PersistFieldSql Timestamp where
    sqlType _ = sqlType (Proxy @UTCTime)

@parsonsmatt
Copy link
Collaborator

The thing with tracking insert writes is in here: #995

I don't think that adding an additional responsibility to PersistField is appropriate

Copy link
Collaborator

@parsonsmatt parsonsmatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach a lot. Just needs changelog entries in the relevant spots.


instance forall a. PersistField a => PersistField (NullableGenerated a) where
toPersistValue (NullableGenerated _) = PersistLiteral "DEFAULT"
fromPersistValue g = NullableGenerated <$> fromPersistValue g
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a useful pattern for describing how this works but I really can't recommend using it in app code. Having it present in the tests is a bit worrying to me, as well, as other people might mimic it and then come across bugs for it.

@parsonsmatt parsonsmatt added this to the 2.11 milestone Sep 18, 2020
@parsonsmatt
Copy link
Collaborator

Travis is failing because that version of Postgres doesn't support GENERATED ,looks like. We can either upgrade Travis or not use GENERATED in the schema - slight preference to drop GENERATED since that'd make it easier to run tests locally too for older OSes.

@parsonsmatt
Copy link
Collaborator

I've made a PR that will add GENERATED column support, if Travis accepts it: #1141 Once that has landed, if you merge master, then it should work, and we can get this released 😄

@parsonsmatt
Copy link
Collaborator

Actually, I have switched CI to GitHub Actions with postgres 12 installed. Can you merge master? That should kick off the right CI which should get this verified in.

@@ -377,6 +377,7 @@ data PersistValue = PersistText Text
| PersistMap [(Text, PersistValue)]
| PersistObjectId ByteString -- ^ Intended especially for MongoDB backend
| PersistArray [PersistValue] -- ^ Intended especially for PostgreSQL backend for text arrays
| PersistLiteral ByteString -- ^ Using 'PersistLiteral' you can customize PersistField instances to output unescaped SQL
| PersistDbSpecific ByteString -- ^ Using 'PersistDbSpecific' allows you to use types specific to a particular backend
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, here's a migration plan that can add this feature safely and manage #1123

  1. PersistLiteral is introduced as the "unescaping" variant.
  2. We add a constructor PersistLiteralEscaped that has the escaping behavior consistently across DB packages.
  3. Add a deprecation notice to PersistDbSpecific specifying what's changed and how. Mention that it will be removed in the next major version release of the library. This warning should percolate through the ecosystem and packages will update.

Then, in the next major version, we can make the behavior breaking changes in the relevant database packages, unifying how postgres/mysql/sqlite use these constructors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add.

@friedbrice
Copy link
Contributor

@parsonsmatt I'm working on getting it up to date and making sure all the tests pass. Will merge when finished. Thanks!

@friedbrice
Copy link
Contributor

@parsonsmatt It looks like I'm not going to be able to get this to work with Sqlite in its current state. We can do a few things: (1) merge and just say Sqlite generated columns are not supported [i'm not too keen on that one], (2) rework the feature, probably adding a field to EntityDef and pushing the merge out to the next major release. What are your thoughts?

@parsonsmatt
Copy link
Collaborator

2.11 is going to be a major release and so additional fields on EntityDef or similar are totally fine :) Can you describe the issues you're running into with it?

@friedbrice
Copy link
Contributor

friedbrice commented Oct 31, 2020

Yeah, we're using this PersistLiteral for generated fields, right? Say you have a table table foo (foo1 text, foo2 text generated as 'always this exact text'). By hand, you'd insert like so: insert into foo(foo1) values ('yup'); But since we're doing things programatically, we generate a prepared statement that looks like this: insert into foo(foo1, foo2) values (?,?);. This seems like it should be broken right away, since we're specifying a generated column. However, Postgres and MySQL allow the literal DEFAULT to be used when inserting into a generated column, and so to emulate generated columns, we write the PersistValue instance to always serialize anything as the literal DEFAULT, and that works for us, in MySQL and Postgres. However, in Sqlite, the prepared statement itself is an error, and the database stops your right there in your tracks. You're not allowed to specify a generate field, even if you use the literal DEFAULT keyword.

So, we'll probably want to keep PersistLiteral for other reasons, but stop using it for default generated columns. For default generated columns, we'll instead want to add a field to EntityDef FieldDef that says whether or not a field is generated and allows the various backends to case on that field explicitly themselves. What do you think?

@friedbrice
Copy link
Contributor

I pushed my latest working tree, but it won't get past CI. You'll find persistent-sqlite will fail on PersistLiteralTestSQL.

@friedbrice
Copy link
Contributor

friedbrice commented Oct 31, 2020

The failed test can be fixed in Database.Persist.Sqlite.insertSql' if EntityDef FieldDef had an extra field that insertSql' could case on when building the prepared statement. (See https://github.com/ldub/persistent/blob/5e9fc69ab0e80a5d6ec1cbd77e13e461671b480c/persistent-sqlite/Database/Persist/Sqlite.hs#L327)

Copy link
Collaborator

@parsonsmatt parsonsmatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few style suggestions but I'm otherwise happy to accept the PR for inclusion with 2.11 release.

persistent/Database/Persist/Sql/Internal.hs Outdated Show resolved Hide resolved
persistent/Database/Persist/Sql/Internal.hs Outdated Show resolved Hide resolved
persistent/Database/Persist/Sql/Internal.hs Outdated Show resolved Hide resolved
persistent/Database/Persist/Types/Base.hs Show resolved Hide resolved
persistent/Database/Persist/Types/Base.hs Show resolved Hide resolved
| Just x <- T.stripPrefix "maxlen=" raw -> case reads (T.unpack x) of
[(n, s)] | all isSpace s -> FieldAttrMaxlen n
_ -> error $ "Could not parse maxlen field with value " <> show raw
| otherwise -> FieldAttrOther raw
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a case expression.

fmap $ \case
    "Maybe" -> FieldAttrMaybe
    "nullable" -> FieldAttrNullable
    -- etc
    raw ->
        asum
            [ FieldAttrRefrence <$> T.stripPrefix "reference=" raw 
            , FieldAttrConstraint <$> T.stripPrefix "constraint=" raw
            -- etc...
            ]

persistent/Database/Persist/Types/Base.hs Outdated Show resolved Hide resolved
persistent-sqlite/test/main.hs Outdated Show resolved Hide resolved
friedbrice and others added 9 commits November 2, 2020 11:02
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
in Postgresql and MySQL backends
@friedbrice
Copy link
Contributor

friedbrice commented Nov 3, 2020

The MySQL test suite passes on my machine but fail on CI. I thought it might be a version issue, but it looks like CI is using the latest major version, just like I am. I am investigating.

-- and serialization in backend-specific ways.
--
-- While we endeavor to, we can't forsee all use cases for all backends,
-- and so 'FieldAttr' is extensible through its constructor 'FieldAttrOther'.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@@ -882,7 +907,7 @@ findAlters edef allDefs col@(Column name isNull type_ def _defConstraintName max
-- | Prints the part of a @CREATE TABLE@ statement about a given
-- column.
showColumn :: Column -> String
showColumn (Column n nu t def _defConstraintName maxLen ref) = concat
showColumn (Column n nu t def _gen _defConstraintName maxLen ref) = concat
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is currently failing on MySQL because the column isn't being generated. Using _gen here should fix that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.0

Maybe a merge conflict X(

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect what's going on locally is that, your database was setup to have the GENERATED from a prior commit, and then this stuff ignores the GENERATED expression when making migrations, so it doesn't un-generate it. So your local code isn't migrating away from GENERATED, and it's already GENERATED, so it passes the test. But in CI we're making the migration from scratch, so it doesn't get GENERATED.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to scrub the database after each test...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then this stuff ignores the GENERATED expression when making migrations, so it doesn't un-generate it.

That seems like a grave oversight on my part.

Copy link
Contributor

@friedbrice friedbrice Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do migrations currently handle a field that goes from having a default to not having a default? Nevermind, I found it.

@friedbrice
Copy link
Contributor

friedbrice commented Nov 4, 2020

Okay, so. This patch is a lot different from what I originally described (#1122 (comment)). The original didn't work because column generation turned out to be too backend-specific (e.g. still haven't gotten Sqlite working), and because the original plan would have broke things like selectList (as we need to be able to serialize things correctly to create Filters). I am happy with the present solution, though, as it feels closer to the spirit of the rest of Persistent.

PS: This should get us automatic updatedAt timestamps, but unfortunately not createdAt timestamps.

@parsonsmatt
Copy link
Collaborator

I think we can support this in SQLite without too much fuss.

Here's the definition of insert for SqlBackend:

    insert val = do
        conn <- ask
        let esql = connInsertSql conn t vals
        key <-
            case esql of
                ISRSingle sql -> withRawQuery sql vals $ do
                    x <- CL.head
                    case x of
                        Just [PersistInt64 i] -> case keyFromValues [PersistInt64 i] of
                            Left err -> error $ "SQL insert: keyFromValues: PersistInt64 " `mappend` show i `mappend` " " `mappend` unpack err
                            Right k -> return k
                        Nothing -> error $ "SQL insert did not return a result giving the generated ID"
                        Just vals' -> case keyFromValues vals' of
                            Left e -> error $ "Invalid result from a SQL insert, got: " ++ show vals' ++ ". Error was: " ++ unpack e
                            Right k -> return k

                ISRInsertGet sql1 sql2 -> do
                    rawExecute sql1 vals
                    withRawQuery sql2 [] $ do
                        mm <- CL.head
                        let m = maybe
                                  (Left $ "No results from ISRInsertGet: " `mappend` tshow (sql1, sql2))
                                  Right mm

                        -- TODO: figure out something better for MySQL
                        let convert x =
                                case x of
                                    [PersistByteString i] -> case readInteger i of -- mssql
                                                            Just (ret,"") -> [PersistInt64 $ fromIntegral ret]
                                                            _ -> x
                                    _ -> x
                            -- Yes, it's just <|>. Older bases don't have the
                            -- instance for Either.
                            onLeft Left{} x = x
                            onLeft x _ = x

                        case m >>= (\x -> keyFromValues x `onLeft` keyFromValues (convert x)) of
                            Right k -> return k
                            Left err -> throw $ "ISRInsertGet: keyFromValues failed: " `mappend` err
                ISRManyKeys sql fs -> do
                    rawExecute sql vals
                    case entityPrimary t of
                       Nothing -> error $ "ISRManyKeys is used when Primary is defined " ++ show sql
                       Just pdef ->
                            let pks = map fieldHaskell $ compositeFields pdef
                                keyvals = map snd $ filter (\(a, _) -> let ret=isJust (find (== a) pks) in ret) $ zip (map fieldHaskell $ entityFields t) fs
                            in  case keyFromValues keyvals of
                                    Right k -> return k
                                    Left e  -> error $ "ISRManyKeys: unexpected keyvals result: " `mappend` unpack e

        return key
      where
        tshow :: Show a => a -> Text
        tshow = T.pack . show
        throw = liftIO . throwIO . userError . T.unpack
        t = entityDef $ Just val
        vals = map toPersistValue $ toPersistFields val

Next up we have SQLite's insertSql' definition:

insertSql' :: EntityDef -> [PersistValue] -> InsertSqlResult
insertSql' ent vals =
  case entityPrimary ent of
    Just _ ->
      ISRManyKeys sql vals
        where sql = T.concat
                [ "INSERT INTO "
                , escape $ entityDB ent
                , "("
                , T.intercalate "," $ map (escape . fieldDB) $ entityFields ent
                , ") VALUES("
                , T.intercalate "," (map (const "?") $ entityFields ent)
                , ")"
                ]
    Nothing ->
      ISRInsertGet ins sel
        where
          sel = T.concat
              [ "SELECT "
              , escape $ fieldDB (entityId ent)
              , " FROM "
              , escape $ entityDB ent
              , " WHERE _ROWID_=last_insert_rowid()"
              ]
          ins = T.concat
              [ "INSERT INTO "
              , escape $ entityDB ent
              , if null (entityFields ent)
                    then " VALUES(null)"
                    else T.concat
                      [ "("
                      , T.intercalate "," $ map (escape . fieldDB) $ entityFields ent
                      , ") VALUES("
                      , T.intercalate "," (map (const "?") $ entityFields ent)
                      , ")"
                      ]
              ]

The logic kinda ping-pongs here, so it can be easy to get lost.

  1. We call connInsertSql conn t (vals = map toPersistValue $ toPersistFields val). The vals argument is only used in the case that the entity has a Primary key definition. insertSql' checks for it and then stuffs it in the ISRManyKeys unchanged.
  2. In insert, we use vals as the value to pass to rawExecute and rawQuery. In the ISRManyKeys result, we execute the query and the convert the vals we inserted into a Key entity in a somewhat hacky way.

So we have three touch points:

  1. Generating the list of fields to insert - map (escape . fieldDB) $ entityFields ent
  2. Generating the list of ? - map (const "?") $ entityFields ent
  3. Generating the list of values to use for the placeholders: map toPersistValue $ toPersistFields val
  1. and 2) are easy. entityFields :: EntityDef -> [FieldDef] and it would be trivial to filter isGenerated . entityFields. This fixes both of those spots.

  2. is trickier. toPersistFields is a class method, and it destroys information - PersistEntity e => e -> [SomePersistField]. SomePersistField is a GADT:

data SomePersistField = forall a. PersistField a => SomePersistField a

SomePersistField is only used in the TH code generation, so touching it won't be a big deal. But if we extend it to:

data SomePersistField where
  SomePersistField :: (PersistEntity rec, PersistField a) => EntityField rec a -> a -> SomePersistField

That gives us:

toInsertVals :: (PersistEntity rec) => rec -> [PersistValue]
toInsertVals = map toPersistValue . filter p . toPersistFields
  where
    p (SomePersistField efield val) = not (isGenerated (persistFieldDef efield))

Anyway I'm just thinking out loud, I'll write this out myself since it touches the code-gen :)

Thanks so much for all the great work!

@parsonsmatt parsonsmatt merged commit aacd5e4 into yesodweb:master Nov 4, 2020
@friedbrice
Copy link
Contributor

Nice explanation! I had written valuesToInsert essentially how you wrote toInsertVals, but the problem i was running up against was That by the time we get to insert we've already lost rec, as you keenly noticed.

Thanks :-)

@friedbrice
Copy link
Contributor

How did this compile?

persistent-zookeeper/Database/Persist/Zookeeper/Binary.hs

It has underscores left in it...

@parsonsmatt
Copy link
Collaborator

Thanks for reminding me, I forgot about #875 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants