-
Notifications
You must be signed in to change notification settings - Fork 1
Change render format #11
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
Changes from 9 commits
2f48f51
3709438
9e07be0
8e880dc
4a05661
ebae7cd
94a797c
e588d09
6ef838f
db755e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,12 @@ import qualified Database.PostgreSQL.Simple.TypeInfo.Static as Postgres | |
| -- - 'Data.Time.CalendarDiffTime': Does not handle days. Embeds a | ||
| -- @NominalDiffTime@. Is not bounded. | ||
| -- - 'Data.Time.CalendarDiffDays': Does not handle seconds. Is not bounded. | ||
| -- | ||
| -- WARNING: The PostgreSQL interval parser is broken in versions prior to 15. | ||
| -- It is not possible to round trip all intervals through PostgreSQL on those | ||
| -- versions. You should upgrade to at least PostgreSQL version 15. For more | ||
| -- information, see this patch: | ||
| -- <https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=e39f99046> | ||
| data Interval = MkInterval | ||
| { months :: !Int.Int32, | ||
| days :: !Int.Int32, | ||
|
|
@@ -47,7 +53,7 @@ instance Postgres.FromField Interval where | |
| fromField = Postgres.attoFieldParser (== Postgres.intervalOid) parse | ||
|
|
||
| -- | Uses 'render'. Always includes an @interval@ prefix, like | ||
| -- @interval '\@ 0 mon -1 day +2 us'@. | ||
| -- @interval '...'@. | ||
| instance Postgres.ToField Interval where | ||
| toField = Postgres.Plain . ("interval '" <>) . (<> "'") . render | ||
|
|
||
|
|
@@ -204,24 +210,34 @@ add x y = | |
| <*> Function.on safeAdd microseconds x y | ||
|
|
||
| -- | Renders an interval to a 'Builder'. This always has the same format: | ||
| -- @"\@ X mon Y day Z us"@, where @X@, @Y@, and @Z@ are signed integers. | ||
| -- @"\@ A mon B day C hour D min E sec F us"@, where @A@, @B@, @C@, @D@, @E@, | ||
| -- and @F@ are signed integers. | ||
| -- | ||
| -- This is not the most compact format, but it is very easy to interpret and | ||
| -- does not require dealing with decimals (which could introduce precision | ||
| -- problems). | ||
| -- | ||
| -- >>> render MkInterval { months = 0, days = -1, microseconds = 2 } | ||
| -- "@ 0 mon -1 day +2 us" | ||
| -- "@ 0 mon -1 day 0 hour 0 min 0 sec +2 us" | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the main change. This new format supports a wider range of intervals before overflowing older versions of Postgres. |
||
| render :: Interval -> Builder.Builder | ||
| render x = | ||
| let signed :: (Num a, Ord a) => (a -> Builder.Builder) -> a -> Builder.Builder | ||
| signed f n = (if n > 0 then "+" else "") <> f n | ||
| (t1, u) = quotRem (microseconds x) 1000000 | ||
| (t2, s) = quotRem t1 60 | ||
| (h, m) = quotRem t2 60 | ||
| in "@ " | ||
| <> signed Builder.int32Dec (months x) | ||
| <> " mon " | ||
| <> signed Builder.int32Dec (days x) | ||
| <> " day " | ||
| <> signed Builder.int64Dec (microseconds x) | ||
| <> signed Builder.int64Dec h | ||
| <> " hour " | ||
| <> signed Builder.int64Dec m | ||
| <> " min " | ||
| <> signed Builder.int64Dec s | ||
| <> " sec " | ||
| <> signed Builder.int64Dec u | ||
| <> " us" | ||
|
|
||
| -- | Parses an interval. This is not a general purpose parser. It only supports | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,19 @@ | ||
| {-# LANGUAGE NumDecimals #-} | ||
| {-# LANGUAGE OverloadedStrings #-} | ||
|
|
||
| import qualified Control.Exception as Exception | ||
| import qualified Control.Monad as Monad | ||
| import qualified Data.Attoparsec.ByteString.Char8 as Attoparsec | ||
| import qualified Data.ByteString as ByteString | ||
| import qualified Data.ByteString.Builder as Builder | ||
| import qualified Data.ByteString.Lazy as LazyByteString | ||
| import qualified Data.Int as Int | ||
| import qualified Data.Text as Text | ||
| import qualified Database.PostgreSQL.Simple as Postgres | ||
| import qualified Database.PostgreSQL.Simple.Interval.Unstable as I | ||
| import qualified Database.PostgreSQL.Simple.ToField as Postgres | ||
| import qualified Test.Hspec as H | ||
| import qualified Text.Read as Read | ||
|
|
||
| main :: IO () | ||
| main = H.hspec spec | ||
|
|
@@ -105,15 +110,19 @@ spec = H.describe "Database.PostgreSQL.Simple.Interval" $ do | |
| H.describe "render" $ do | ||
| H.it "works with zero" $ do | ||
| let actual = Builder.toLazyByteString $ I.render I.zero | ||
| actual `H.shouldBe` "@ 0 mon 0 day 0 us" | ||
| actual `H.shouldBe` "@ 0 mon 0 day 0 hour 0 min 0 sec 0 us" | ||
|
|
||
| H.it "works with positive components" $ do | ||
| let actual = Builder.toLazyByteString . I.render $ I.MkInterval 1 2 3 | ||
| actual `H.shouldBe` "@ +1 mon +2 day +3 us" | ||
| actual `H.shouldBe` "@ +1 mon +2 day 0 hour 0 min 0 sec +3 us" | ||
|
|
||
| H.it "works with negative components" $ do | ||
| let actual = Builder.toLazyByteString . I.render $ I.MkInterval (-3) (-2) (-1) | ||
| actual `H.shouldBe` "@ -3 mon -2 day -1 us" | ||
| actual `H.shouldBe` "@ -3 mon -2 day 0 hour 0 min 0 sec -1 us" | ||
|
|
||
| H.it "works with time components" $ do | ||
| let actual = Builder.toLazyByteString . I.render $ I.MkInterval 0 0 3723000004 | ||
| actual `H.shouldBe` "@ 0 mon 0 day +1 hour +2 min +3 sec +4 us" | ||
|
|
||
| H.describe "parse" $ do | ||
| H.it "fails with invalid input" $ do | ||
|
|
@@ -143,13 +152,42 @@ spec = H.describe "Database.PostgreSQL.Simple.Interval" $ do | |
| let actual = Attoparsec.parseOnly I.parse input | ||
| actual `H.shouldBe` Right interval | ||
|
|
||
| H.describe "integration" $ do | ||
| Monad.forM_ intervalStyles $ \(style, field) -> do | ||
| H.describe ("with style " <> show style) $ do | ||
| Monad.forM_ examples $ \example -> do | ||
| H.it ("round trips " <> show (field example)) $ do | ||
| Postgres.withConnect Postgres.defaultConnectInfo $ \connection -> do | ||
| let interval = exampleInterval example | ||
| result <- Exception.try . Postgres.withTransaction connection $ do | ||
| Monad.void $ Postgres.execute connection "set local intervalstyle = ?" [style] | ||
| Postgres.query connection "select ?" [interval] | ||
| case result of | ||
| Right actual -> actual `H.shouldBe` [Postgres.Only interval] | ||
| Left somePostgresqlException -> do | ||
| rows <- Postgres.query_ connection "select version()" | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably use |
||
| case rows of | ||
| Postgres.Only text : _ | ||
| | _ : rawVersion : _ <- Text.words text, | ||
| Just version <- Read.readMaybe (Text.unpack rawVersion), | ||
| version < (15 :: Double) -> | ||
| H.pendingWith $ "interval parsing broken with PostgreSQL version " <> show version | ||
| _ -> Exception.throwIO (somePostgresqlException :: Postgres.SomePostgreSqlException) | ||
|
|
||
| data IntervalStyle | ||
| = Iso8601 | ||
| | Postgres | ||
| | PostgresVerbose | ||
| | SqlStandard | ||
| deriving (Eq, Show) | ||
|
|
||
| instance Postgres.ToField IntervalStyle where | ||
| toField style = Postgres.Plain $ case style of | ||
| Iso8601 -> "iso_8601" | ||
| Postgres -> "postgres" | ||
| PostgresVerbose -> "postgres_verbose" | ||
| SqlStandard -> "sql_standard" | ||
|
|
||
| data Example = MkExample | ||
| { exampleInterval :: I.Interval, | ||
| exampleIso8601 :: ByteString.ByteString, | ||
|
|
||
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.
As discovered here: yesodweb/persistent#1604 (comment)