-
Notifications
You must be signed in to change notification settings - Fork 301
persistent-postgresql: Implement Postgres interval support #1053
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
persistent-postgresql: Implement Postgres interval support #1053
Conversation
|
Can someone help me getting unstuck with this? |
parsonsmatt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well done! I think I've noticed the problem. The call to convertPV is telling postgresql-simple to use the FromField instance for ByteString, but we need to have it pick a FromField instance for PgInterval (which doesn't currently exist). Then, rather unfortunately, we need to convert it back to a ByteString to stuff it in the PersistDbSpecific.
| , (k PS.time, convertPV PersistTimeOfDay) | ||
| , (k PS.timestamp, convertPV (PersistUTCTime. localTimeToUTC utc)) | ||
| , (k PS.timestamptz, convertPV PersistUTCTime) | ||
| , (k PS.interval, convertPV PersistDbSpecific) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this line is wrong.
convertPV is defined as:
convertPV :: PGFF.FromField a => (a -> b) -> Getter b
convertPV f = (fmap f .) . PGFF.fromFieldThis means that it will pick a ByteString instance of FromField. But that's not what we want - we want an instance of FromField that picks NominalDiffTime, and then converts it to ByteString so it can be wrapped in PersistDbSpecific.
The current code path picks this instance when parsing:
-- | bytea, name, text, \"char\", bpchar, varchar, unknown
instance FromField SB.ByteString where
fromField f dat = if typeOid f == TI.byteaOid
then unBinary <$> fromField f dat
else doFromField f okText' pure datdoFromField is defined here:
doFromField :: forall a . (Typeable a)
=> Field -> Compat -> (ByteString -> Conversion a)
-> Maybe ByteString -> Conversion a
doFromField f isCompat cvt (Just bs)
| isCompat (typeOid f) = cvt bs
| otherwise = returnError Incompatible f "types incompatible"
doFromField f _ _ _ = returnError UnexpectedNull f ""There's our "types incompatible" error message. The isCompat function passed in is this thing, which appears to just check if the type is one of a few compatbiel text-like things:
okText' = eq TI.nameOid \/ eq TI.textOid \/ eq TI.charOid
\/ eq TI.bpcharOid \/ eq TI.varcharOid \/ eq TI.unknownOidNow... postgresql-simple doesn't have an instance of FromField NominalDiffTime. It does have an instance of ToField NominalDiffTime. So we need to provide an instance of PGTF.FromField PgInterval.
The instance of FromField UTCTime is here:
-- | timestamptz
instance FromField UTCTime where
fromField = ff TI.timestamptzOid "UTCTime" parseUTCTimeand parseUTCTime is a function ByteString -> Either String UTCTime. I think you could write a parseNominalDiffTime that worked here, and then use that, and we'd be set.
| , (1183, listOf PersistTimeOfDay) | ||
| , (1115, listOf PersistUTCTime) | ||
| , (1185, listOf PersistUTCTime) | ||
| , (1187, listOf PersistDbSpecific) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would also need to be updated to pick the right instance.
29b3567 to
3b81a37
Compare
|
@parsonsmatt This is what I have at the moment. Am confused about the negative decimal interval tests. Specifying a negative decimal value returns me a positive one. Also refering 8.5.4 Interval Input and mixed intervals seemed problematic. Using Debug.Trace gives me this. The After some more debugging, I found something wierd. The input to N.B. In the commit, for now the test is rigged to purposely pass. |
parsonsmatt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great progress! That's some odd behavior though.
| Nothing -> PGFF.returnError PGFF.UnexpectedNull f "" | ||
| Just dat -> case P.parseOnly (nominalDiffTime <* P.endOfInput) dat of | ||
| Left msg -> PGFF.returnError PGFF.ConversionFailed f msg | ||
| Right t -> return $ PgInterval t |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, this looks good!
| case P.parseOnly (P.signed P.rational <* P.char 's' <* P.endOfInput) bs of | ||
| Left _ -> Left $ fromPersistValueError "PgInterval" "Interval" x | ||
| Right i -> Right $ PgInterval i | ||
| fromPersistValue x = Left $ fromPersistValueError "PgInterval" "Interval" x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also looks good to me.
| , (k PS.time, convertPV PersistTimeOfDay) | ||
| , (k PS.timestamp, convertPV (PersistUTCTime. localTimeToUTC utc)) | ||
| , (k PS.timestamptz, convertPV PersistUTCTime) | ||
| , (k PS.interval, convertPV (PersistDbSpecific . pgIntervalToBs)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| , (1183, listOf PersistTimeOfDay) | ||
| , (1115, listOf PersistUTCTime) | ||
| , (1185, listOf PersistUTCTime) | ||
| , (1187, listOf (PersistDbSpecific . pgIntervalToBs)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| _ <- runMigrationSilent pgIntervalMigrate | ||
| rid <- insert $ PgIntervalDb $ PgInterval (-0.01) | ||
| r <- getJust rid | ||
| liftIO $ r @?= (PgIntervalDb $ PgInterval 0.01) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is the test that is "wrong", right? That's very odd.
| specs runDb = do | ||
| describe "Postgres Interval tests" $ do | ||
| it "test for insert and read of negative intervals" $ runDb $ do | ||
| _ <- runMigrationSilent pgIntervalMigrate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The migrations are usually stored higher up in the test tree so that individual cases don't have to deal with them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eg in persistent-postgresql, migrations are done in test/Main.hs at line ~100-120
|
|
||
| specs :: (MonadUnliftIO m, MonadFail m) => RunDb SqlBackend m -> Spec | ||
| specs runDb = do | ||
| describe "Postgres Interval tests" $ do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These round-trip tests are a great candidate for QuickCheck. We can write:
describe "pg interval" $ do
prop "round trips" $ \time -> runDb $ do
let example = PgIntervalDb $ PgInterval time
rid <- insert example
r <- getJust rid
liftIO $ r `shouldBe` exampleand it should catch edge casees.
|
In postgres, I get: So, let's look at the parser. We pick up the sign in the hour bit. Then, we calculate the time in seconds using this line. But suppose that we have an (s = 0.01) + 60 * (fromIntegral (m = 0)) + 60 * 60 * (fromIntegral (h = -0))
0.01 + 60 * 0 + 60 * 60 * -0
0.01 + 0 + 0
0.01So I think we need to multiply the absolute value of the signum h * (s + 60 * fromIntegral m + 60 * 60 * fromIntegral (abs h)) |
|
Thanks for the detailed help. Tracking the sign on Hour seems to be not enough. I think I would have to explicitly detect the sign during running of Parser and setup the sign myself. Edit: Got it working now. Will look into cleaning up the test based on points you mentioned earlier and also rebase. |
|
Hmm...The property seems to have caught the edge cases I guess due to rounding errors. |
|
|
3b81a37 to
e2c766a
Compare
|
Fixed the test and rebased. |
b183b15 to
a02a4e8
Compare
parsonsmatt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome! Just needs the changelog entry moved to be under the 2.11.0.0 :)
persistent-postgresql/ChangeLog.md
Outdated
|
|
||
| ## 2.11.1 | ||
|
|
||
| * Implement interval support. #1053 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * Implement interval support. #1053 | |
| * Implement interval support. [#1053](https://github.com/yesodweb/persistent/pull/1053) |
This will get batched in with persistent-2.11.0.0, since that is not yet released. Could you move the bullet down there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
a02a4e8 to
dc1c784
Compare
Implements yesodweb#638. Signed-off-by: Sanchayan Maity <maitysanchayan@gmail.com>
dc1c784 to
5b8ada2
Compare
|
Thanks so much @SanchayanMaity , this is great 😄 |
For issue #638
Signed-off-by: Sanchayan Maity maitysanchayan@gmail.com
Before submitting your PR, check that you've:
@sincedeclarations to the HaddockAfter submitting your PR: