From 794a26812523a138284710701c9fb1b64f700b0f Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Thu, 10 Jul 2025 10:36:50 -0500 Subject: [PATCH 01/22] Use Interval type from postgresql-simple-interval --- .../Database/Persist/Postgresql/Internal.hs | 136 +++++------------- .../Database/Persist/Postgresql/Interval.hs | 33 +++++ .../persistent-postgresql.cabal | 4 + persistent-postgresql/test/PgIntervalTest.hs | 12 ++ 4 files changed, 87 insertions(+), 98 deletions(-) create mode 100644 persistent-postgresql/Database/Persist/Postgresql/Interval.hs diff --git a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs index 0fbdcb771..4a03d912c 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} module Database.Persist.Postgresql.Internal @@ -13,26 +14,21 @@ import qualified Database.PostgreSQL.Simple.Internal as PG import qualified Database.PostgreSQL.Simple.ToField as PGTF import qualified Database.PostgreSQL.Simple.TypeInfo.Static as PS import qualified Database.PostgreSQL.Simple.Types as PG +import qualified Database.PostgreSQL.Simple.Interval as Interval import qualified Blaze.ByteString.Builder.Char8 as BBB -import qualified Data.Attoparsec.ByteString.Char8 as P -import Data.Bits ((.&.)) +import Control.Monad ((<=<)) +import Data.Bits (toIntegralSized) import Data.ByteString (ByteString) import qualified Data.ByteString.Builder as BB -import qualified Data.ByteString.Char8 as B8 -import Data.Char (ord) import Data.Data (Typeable) -import Data.Fixed (Fixed(..), Pico) -import Data.Int (Int64) +import Data.Fixed (Fixed(..), Micro, Pico) import qualified Data.IntMap as I import Data.Maybe (fromMaybe) -import Data.String.Conversions.Monomorphic (toStrictByteString) -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.Encoding as T -import Data.Time (NominalDiffTime, localTimeToUTC, utc) +import Data.Time (NominalDiffTime, localTimeToUTC, nominalDiffTimeToSeconds, secondsToNominalDiffTime, utc) import Database.Persist.Sql +import Database.Persist.Postgresql.Interval () -- | Newtype used to avoid orphan instances for @postgresql-simple@ classes. -- @@ -117,7 +113,7 @@ builtinGetters = I.fromList , (k PS.time, convertPV PersistTimeOfDay) , (k PS.timestamp, convertPV (PersistUTCTime. localTimeToUTC utc)) , (k PS.timestamptz, convertPV PersistUTCTime) - , (k PS.interval, convertPV (PersistLiteralEscaped . pgIntervalToBs)) + , (k PS.interval, convertPV $ toPersistValue @Interval.Interval) , (k PS.bit, convertPV PersistInt64) , (k PS.varbit, convertPV PersistInt64) , (k PS.numeric, convertPV PersistRational) @@ -148,7 +144,7 @@ builtinGetters = I.fromList , (1183, listOf PersistTimeOfDay) , (1115, listOf PersistUTCTime) , (1185, listOf PersistUTCTime) - , (1187, listOf (PersistLiteralEscaped . pgIntervalToBs)) + , (1187, listOf $ toPersistValue @Interval.Interval) , (1561, listOf PersistInt64) , (1563, listOf PersistInt64) , (1231, listOf PersistRational) @@ -188,97 +184,41 @@ unBinary (PG.Binary x) = x newtype PgInterval = PgInterval { getPgInterval :: NominalDiffTime } deriving (Eq, Show) -pgIntervalToBs :: PgInterval -> ByteString -pgIntervalToBs = toStrictByteString . show . getPgInterval - instance PGTF.ToField PgInterval where - toField (PgInterval t) = PGTF.toField t + toField = PGTF.toField . fromMaybe (error "PgInterval.toField") . pgIntervalToInterval instance PGFF.FromField PgInterval where - fromField f mdata = - if PGFF.typeOid f /= PS.typoid PS.interval - then PGFF.returnError PGFF.Incompatible f "" - else case mdata of - 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 - - where - toPico :: Integer -> Pico - toPico = MkFixed - - -- Taken from Database.PostgreSQL.Simple.Time.Internal.Parser - twoDigits :: P.Parser Int - twoDigits = do - a <- P.digit - b <- P.digit - let c2d c = ord c .&. 15 - return $! c2d a * 10 + c2d b - - -- Taken from Database.PostgreSQL.Simple.Time.Internal.Parser - seconds :: P.Parser Pico - seconds = do - real <- twoDigits - mc <- P.peekChar - case mc of - Just '.' -> do - t <- P.anyChar *> P.takeWhile1 P.isDigit - return $! parsePicos (fromIntegral real) t - _ -> return $! fromIntegral real - where - parsePicos :: Int64 -> B8.ByteString -> Pico - parsePicos a0 t = toPico (fromIntegral (t' * 10^n)) - where n = max 0 (12 - B8.length t) - t' = B8.foldl' (\a c -> 10 * a + fromIntegral (ord c .&. 15)) a0 - (B8.take 12 t) - - parseSign :: P.Parser Bool - parseSign = P.choice [P.char '-' >> return True, return False] - - -- Db stores it in [-]HHH:MM:SS.[SSSS] - -- For example, nominalDay is stored as 24:00:00 - interval :: P.Parser (Bool, Int, Int, Pico) - interval = do - s <- parseSign - h <- P.decimal <* P.char ':' - m <- twoDigits <* P.char ':' - ss <- seconds - if m < 60 && ss <= 60 - then return (s, h, m, ss) - else fail "Invalid interval" - - nominalDiffTime :: P.Parser NominalDiffTime - nominalDiffTime = do - (s, h, m, ss) <- interval - let pico = ss + 60 * (fromIntegral m) + 60 * 60 * (fromIntegral (abs h)) - return . fromRational . toRational $ if s then (-pico) else pico - -fromPersistValueError :: Text -- ^ Haskell type, should match Haskell name exactly, e.g. "Int64" - -> Text -- ^ Database type(s), should appear different from Haskell name, e.g. "integer" or "INT", not "Int". - -> PersistValue -- ^ Incorrect value - -> Text -- ^ Error message -fromPersistValueError haskellType databaseType received = T.concat - [ "Failed to parse Haskell type `" - , haskellType - , "`; expected " - , databaseType - , " from database, but received: " - , T.pack (show received) - , ". Potential solution: Check that your database schema matches your Persistent model definitions." - ] + fromField f = + maybe (PGFF.returnError PGFF.ConversionFailed f "invalid interval") pure + . intervalToPgInterval + <=< PGFF.fromField f instance PersistField PgInterval where - toPersistValue = PersistLiteralEscaped . pgIntervalToBs - fromPersistValue (PersistLiteral_ DbSpecific bs) = - fromPersistValue (PersistLiteralEscaped bs) - fromPersistValue x@(PersistLiteral_ Escaped bs) = - 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 + toPersistValue = toPersistValue . fromMaybe (error "PgInterval.toPersistValue") . pgIntervalToInterval + fromPersistValue = + maybe (Left "invalid interval") pure + . intervalToPgInterval + <=< fromPersistValue instance PersistFieldSql PgInterval where sqlType _ = SqlOther "interval" - +pgIntervalToInterval :: PgInterval -> Maybe Interval.Interval +pgIntervalToInterval = fmap Interval.fromMicroseconds + . toIntegralSized + . (\ (MkFixed x) -> x) + . (realToFrac :: Pico -> Micro) + . nominalDiffTimeToSeconds + . getPgInterval + +intervalToPgInterval :: Interval.Interval -> Maybe PgInterval +intervalToPgInterval interval + | Interval.months interval /= 0 = Nothing + | Interval.days interval /= 0 = Nothing + | otherwise = Just + . PgInterval + . secondsToNominalDiffTime + . (realToFrac :: Micro -> Pico) + . MkFixed + . toInteger + $ Interval.microseconds interval diff --git a/persistent-postgresql/Database/Persist/Postgresql/Interval.hs b/persistent-postgresql/Database/Persist/Postgresql/Interval.hs new file mode 100644 index 000000000..fa35a6505 --- /dev/null +++ b/persistent-postgresql/Database/Persist/Postgresql/Interval.hs @@ -0,0 +1,33 @@ +{-# OPTIONS_GHC -Wno-orphans #-} +{-# LANGUAGE OverloadedStrings #-} + +module Database.Persist.Postgresql.Interval where + +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Builder as Builder +import qualified Data.ByteString.Char8 as Ascii +import qualified Data.ByteString.Lazy as LazyByteString +import qualified Data.Text as Text +import qualified Database.Persist as Persist +import qualified Database.Persist.Sql as Persist +import qualified Database.PostgreSQL.Simple.Interval.Unstable as Interval +import qualified Database.PostgreSQL.Simple.ToField as Postgres + +instance Persist.PersistField Interval.Interval where + fromPersistValue persistValue = case persistValue of + Persist.PersistLiteral_ Persist.Unescaped byteString + | Just withoutPrefix <- Ascii.stripPrefix "interval '" byteString, + Just withoutSuffix <- Ascii.stripSuffix "'" withoutPrefix, + Right interval <- A.parseOnly Interval.parse withoutSuffix -> Right interval + _ -> Left $ "invalid interval: " <> Text.pack (show persistValue) + + toPersistValue = + Persist.PersistLiteral_ Persist.Unescaped + . LazyByteString.toStrict + . Builder.toLazyByteString + . ("interval " <>) + . Postgres.inQuotes + . Interval.render + +instance Persist.PersistFieldSql Interval.Interval where + sqlType = const $ Persist.SqlOther "interval" diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index 300ac4c81..03633f351 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -28,6 +28,7 @@ library , persistent >=2.13.3 && <3 , postgresql-libpq >=0.9.4.2 && <0.12 , postgresql-simple >=0.6.1 && <0.8 + , postgresql-simple-interval ==0.2025.7.10 , resource-pool , resourcet >=1.1.9 , string-conversions @@ -40,6 +41,7 @@ library exposed-modules: Database.Persist.Postgresql Database.Persist.Postgresql.Internal + Database.Persist.Postgresql.Interval Database.Persist.Postgresql.JSON ghc-options: -Wall @@ -67,6 +69,7 @@ test-suite test ghc-options: -Wall build-depends: aeson + , attoparsec , base >=4.9 && <5 , bytestring , containers @@ -82,6 +85,7 @@ test-suite test , persistent-postgresql , persistent-qq , persistent-test + , postgresql-simple-interval , QuickCheck , quickcheck-instances , resourcet diff --git a/persistent-postgresql/test/PgIntervalTest.hs b/persistent-postgresql/test/PgIntervalTest.hs index 18ed2e516..0e8e4a31c 100644 --- a/persistent-postgresql/test/PgIntervalTest.hs +++ b/persistent-postgresql/test/PgIntervalTest.hs @@ -19,12 +19,18 @@ import PgInit import Data.Time.Clock (NominalDiffTime) import Database.Persist.Postgresql (PgInterval(..)) import Test.Hspec.QuickCheck +import qualified Database.Persist.Postgresql.Interval as Interval +import qualified Database.PostgreSQL.Simple.Interval as Interval share [mkPersist sqlSettings, mkMigrate "pgIntervalMigrate"] [persistLowerCase| PgIntervalDb interval_field PgInterval deriving Eq deriving Show + +IntervalDb + interval_field Interval.Interval + deriving Eq Show |] -- Postgres Interval has a 1 microsecond resolution, while NominalDiffTime has @@ -41,3 +47,9 @@ specs = do rid <- insert eg r <- getJust rid liftIO $ r `shouldBe` eg + + prop "interval round trips" $ \(m, d, u) -> runConnAssert $ do + let expected = IntervalDb $ Interval.MkInterval m d u + key <- insert expected + actual <- getJust key + liftIO $ actual `shouldBe` expected From 208c58cdcc90f404c22599cde48f014f031e665a Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Thu, 10 Jul 2025 10:48:34 -0500 Subject: [PATCH 02/22] Allow building with older versions of base --- cabal.project | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cabal.project b/cabal.project index 9ad070ab0..9a74fa7a4 100644 --- a/cabal.project +++ b/cabal.project @@ -19,6 +19,9 @@ allow-newer: , postgresql-simple:template-haskell , bytestring-lexing:base +allow-older: + postgresql-simple-interval:base + source-repository-package type: git location: https://github.com/parsonsmatt/mysql From f8a9aa6627bab825e8bfa559c57fc8978e62f066 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Thu, 10 Jul 2025 10:52:56 -0500 Subject: [PATCH 03/22] Allow older bytestring --- cabal.project | 1 + 1 file changed, 1 insertion(+) diff --git a/cabal.project b/cabal.project index 9a74fa7a4..8580e9d04 100644 --- a/cabal.project +++ b/cabal.project @@ -21,6 +21,7 @@ allow-newer: allow-older: postgresql-simple-interval:base + , postgresql-simple-interval:bytestring source-repository-package type: git From 95948779dea81979ace9b88658b893a7f96b7de7 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Thu, 10 Jul 2025 13:24:40 -0500 Subject: [PATCH 04/22] Fix imports --- cabal.project | 9 +++++---- .../Database/Persist/Postgresql/Internal.hs | 12 ++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/cabal.project b/cabal.project index 8580e9d04..1423283bc 100644 --- a/cabal.project +++ b/cabal.project @@ -19,10 +19,6 @@ allow-newer: , postgresql-simple:template-haskell , bytestring-lexing:base -allow-older: - postgresql-simple-interval:base - , postgresql-simple-interval:bytestring - source-repository-package type: git location: https://github.com/parsonsmatt/mysql @@ -35,3 +31,8 @@ source-repository-package -- type: git -- location: https://github.com/ysangkok/hedis -- tag: 6f36989836b49974f51a6ee8edaf156490590980 + +source-repository-package + type: git + location: https://github.com/MercuryTechnologies/postgresql-simple-interval + tag: c2e9e886162a37cc177986d223600567c2a156e8 diff --git a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs index 7ca9e8910..8b757d009 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs @@ -48,29 +48,25 @@ import Control.Monad.Except import Control.Monad.IO.Unlift (MonadIO (..)) import Control.Monad.Trans.Class (lift) import Data.Acquire (with) -import qualified Data.Attoparsec.ByteString.Char8 as P -import Data.Bits ((.&.)) +import Data.Bits (toIntegralSized) import Data.ByteString (ByteString) import qualified Data.ByteString.Builder as BB -import qualified Data.ByteString.Char8 as B8 -import Data.Char (ord) import Data.Conduit import qualified Data.Conduit.List as CL import Data.Data (Typeable) import Data.Either (partitionEithers) -import Data.Fixed (Fixed (..), Pico) +import Data.Fixed (Fixed (..), Micro, Pico) import Data.Function (on) -import Data.Int (Int64) import qualified Data.IntMap as I import Data.List as List (find, foldl', groupBy, sort) import qualified Data.List.NonEmpty as NEL import qualified Data.Map as Map import Data.Maybe -import Data.String.Conversions.Monomorphic (toStrictByteString) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T -import Data.Time (NominalDiffTime, localTimeToUTC, utc) +import Data.Time (NominalDiffTime, localTimeToUTC, nominalDiffTimeToSeconds, secondsToNominalDiffTime, utc) +import Database.Persist.Postgresql.Interval () import Database.Persist.Sql import qualified Database.Persist.Sql.Util as Util From 080ef87e804e9bcda059faad37a4265d00d8a8c7 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Thu, 10 Jul 2025 15:24:08 -0500 Subject: [PATCH 05/22] Avoid generating invalid intervals --- persistent-postgresql/test/PgIntervalTest.hs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/persistent-postgresql/test/PgIntervalTest.hs b/persistent-postgresql/test/PgIntervalTest.hs index 0e8e4a31c..97cd1226b 100644 --- a/persistent-postgresql/test/PgIntervalTest.hs +++ b/persistent-postgresql/test/PgIntervalTest.hs @@ -16,10 +16,11 @@ module PgIntervalTest where import PgInit -import Data.Time.Clock (NominalDiffTime) +import Data.Time.Clock (secondsToNominalDiffTime) +import Data.Fixed (Fixed(MkFixed)) import Database.Persist.Postgresql (PgInterval(..)) import Test.Hspec.QuickCheck -import qualified Database.Persist.Postgresql.Interval as Interval +import Database.Persist.Postgresql.Interval () import qualified Database.PostgreSQL.Simple.Interval as Interval share [mkPersist sqlSettings, mkMigrate "pgIntervalMigrate"] [persistLowerCase| @@ -33,17 +34,11 @@ IntervalDb deriving Eq Show |] --- Postgres Interval has a 1 microsecond resolution, while NominalDiffTime has --- picosecond resolution. Round to the nearest microsecond so that we can be --- fine in the tests. -truncate' :: NominalDiffTime -> NominalDiffTime -truncate' x = (fromIntegral (round (x * 10^6))) / 10^6 - specs :: Spec specs = do describe "Postgres Interval Property tests" $ do - prop "Round trips" $ \time -> runConnAssert $ do - let eg = PgIntervalDb $ PgInterval (truncate' time) + prop "Round trips" $ \int64 -> runConnAssert $ do + let eg = PgIntervalDb . PgInterval . secondsToNominalDiffTime . MkFixed $ toInteger (int64 :: Int64) * 1000000 rid <- insert eg r <- getJust rid liftIO $ r `shouldBe` eg From 3ea86b009eec38e024c62e1fcfabfe5ee52fc4f5 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Thu, 10 Jul 2025 15:27:41 -0500 Subject: [PATCH 06/22] Fix formatting --- .../Database/Persist/Postgresql/Internal.hs | 59 +++++++++++-------- .../Database/Persist/Postgresql/Interval.hs | 31 +++++----- persistent-postgresql/test/PgIntervalTest.hs | 16 +++-- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs index 8b757d009..54f2eae4d 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs @@ -36,10 +36,10 @@ module Database.Persist.Postgresql.Internal import qualified Database.PostgreSQL.Simple as PG import qualified Database.PostgreSQL.Simple.FromField as PGFF import qualified Database.PostgreSQL.Simple.Internal as PG +import qualified Database.PostgreSQL.Simple.Interval as Interval import qualified Database.PostgreSQL.Simple.ToField as PGTF import qualified Database.PostgreSQL.Simple.TypeInfo.Static as PS import qualified Database.PostgreSQL.Simple.Types as PG -import qualified Database.PostgreSQL.Simple.Interval as Interval import qualified Blaze.ByteString.Builder.Char8 as BBB import Control.Arrow @@ -65,7 +65,13 @@ import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T -import Data.Time (NominalDiffTime, localTimeToUTC, nominalDiffTimeToSeconds, secondsToNominalDiffTime, utc) +import Data.Time + ( NominalDiffTime + , localTimeToUTC + , nominalDiffTimeToSeconds + , secondsToNominalDiffTime + , utc + ) import Database.Persist.Postgresql.Interval () import Database.Persist.Sql import qualified Database.Persist.Sql.Util as Util @@ -240,39 +246,44 @@ instance PGTF.ToField PgInterval where instance PGFF.FromField PgInterval where fromField f = - maybe (PGFF.returnError PGFF.ConversionFailed f "invalid interval") pure - . intervalToPgInterval - <=< PGFF.fromField f + maybe (PGFF.returnError PGFF.ConversionFailed f "invalid interval") pure + . intervalToPgInterval + <=< PGFF.fromField f instance PersistField PgInterval where - toPersistValue = toPersistValue . fromMaybe (error "PgInterval.toPersistValue") . pgIntervalToInterval + toPersistValue = + toPersistValue + . fromMaybe (error "PgInterval.toPersistValue") + . pgIntervalToInterval fromPersistValue = - maybe (Left "invalid interval") pure - . intervalToPgInterval - <=< fromPersistValue + maybe (Left "invalid interval") pure + . intervalToPgInterval + <=< fromPersistValue instance PersistFieldSql PgInterval where sqlType _ = SqlOther "interval" pgIntervalToInterval :: PgInterval -> Maybe Interval.Interval -pgIntervalToInterval = fmap Interval.fromMicroseconds - . toIntegralSized - . (\ (MkFixed x) -> x) - . (realToFrac :: Pico -> Micro) - . nominalDiffTimeToSeconds - . getPgInterval +pgIntervalToInterval = + fmap Interval.fromMicroseconds + . toIntegralSized + . (\(MkFixed x) -> x) + . (realToFrac :: Pico -> Micro) + . nominalDiffTimeToSeconds + . getPgInterval intervalToPgInterval :: Interval.Interval -> Maybe PgInterval intervalToPgInterval interval - | Interval.months interval /= 0 = Nothing - | Interval.days interval /= 0 = Nothing - | otherwise = Just - . PgInterval - . secondsToNominalDiffTime - . (realToFrac :: Micro -> Pico) - . MkFixed - . toInteger - $ Interval.microseconds interval + | Interval.months interval /= 0 = Nothing + | Interval.days interval /= 0 = Nothing + | otherwise = + Just + . PgInterval + . secondsToNominalDiffTime + . (realToFrac :: Micro -> Pico) + . MkFixed + . toInteger + $ Interval.microseconds interval -- | Indicates whether a Postgres Column is safe to drop. -- diff --git a/persistent-postgresql/Database/Persist/Postgresql/Interval.hs b/persistent-postgresql/Database/Persist/Postgresql/Interval.hs index fa35a6505..7d5c5672b 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Interval.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Interval.hs @@ -1,5 +1,5 @@ -{-# OPTIONS_GHC -Wno-orphans #-} {-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -Wno-orphans #-} module Database.Persist.Postgresql.Interval where @@ -14,20 +14,21 @@ import qualified Database.PostgreSQL.Simple.Interval.Unstable as Interval import qualified Database.PostgreSQL.Simple.ToField as Postgres instance Persist.PersistField Interval.Interval where - fromPersistValue persistValue = case persistValue of - Persist.PersistLiteral_ Persist.Unescaped byteString - | Just withoutPrefix <- Ascii.stripPrefix "interval '" byteString, - Just withoutSuffix <- Ascii.stripSuffix "'" withoutPrefix, - Right interval <- A.parseOnly Interval.parse withoutSuffix -> Right interval - _ -> Left $ "invalid interval: " <> Text.pack (show persistValue) + fromPersistValue persistValue = case persistValue of + Persist.PersistLiteral_ Persist.Unescaped byteString + | Just withoutPrefix <- Ascii.stripPrefix "interval '" byteString + , Just withoutSuffix <- Ascii.stripSuffix "'" withoutPrefix + , Right interval <- A.parseOnly Interval.parse withoutSuffix -> + Right interval + _ -> Left $ "invalid interval: " <> Text.pack (show persistValue) - toPersistValue = - Persist.PersistLiteral_ Persist.Unescaped - . LazyByteString.toStrict - . Builder.toLazyByteString - . ("interval " <>) - . Postgres.inQuotes - . Interval.render + toPersistValue = + Persist.PersistLiteral_ Persist.Unescaped + . LazyByteString.toStrict + . Builder.toLazyByteString + . ("interval " <>) + . Postgres.inQuotes + . Interval.render instance Persist.PersistFieldSql Interval.Interval where - sqlType = const $ Persist.SqlOther "interval" + sqlType = const $ Persist.SqlOther "interval" diff --git a/persistent-postgresql/test/PgIntervalTest.hs b/persistent-postgresql/test/PgIntervalTest.hs index d8153e123..e99edf30b 100644 --- a/persistent-postgresql/test/PgIntervalTest.hs +++ b/persistent-postgresql/test/PgIntervalTest.hs @@ -17,13 +17,13 @@ module PgIntervalTest where -import PgInit +import Data.Fixed (Fixed (MkFixed)) import Data.Time.Clock (secondsToNominalDiffTime) -import Data.Fixed (Fixed(MkFixed)) -import Database.Persist.Postgresql (PgInterval(..)) -import Test.Hspec.QuickCheck +import Database.Persist.Postgresql (PgInterval (..)) import Database.Persist.Postgresql.Interval () import qualified Database.PostgreSQL.Simple.Interval as Interval +import PgInit +import Test.Hspec.QuickCheck share [mkPersist sqlSettings, mkMigrate "pgIntervalMigrate"] @@ -42,13 +42,17 @@ specs :: Spec specs = do describe "Postgres Interval Property tests" $ do prop "Round trips" $ \int64 -> runConnAssert $ do - let eg = PgIntervalDb . PgInterval . secondsToNominalDiffTime . MkFixed $ toInteger (int64 :: Int64) * 1000000 + let + eg = + PgIntervalDb . PgInterval . secondsToNominalDiffTime . MkFixed $ + toInteger (int64 :: Int64) * 1000000 rid <- insert eg r <- getJust rid liftIO $ r `shouldBe` eg prop "interval round trips" $ \(m, d, u) -> runConnAssert $ do - let expected = IntervalDb $ Interval.MkInterval m d u + let + expected = IntervalDb $ Interval.MkInterval m d u key <- insert expected actual <- getJust key liftIO $ actual `shouldBe` expected From 87387b0a4a1513669da6b35c0bb97dd607fbcbdf Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Fri, 11 Jul 2025 09:11:22 -0500 Subject: [PATCH 07/22] Use upstream Persistent instances --- cabal.project | 2 +- .../Database/Persist/Postgresql/Internal.hs | 1 - .../Database/Persist/Postgresql/Interval.hs | 34 ------------------- .../persistent-postgresql.cabal | 1 - persistent-postgresql/test/PgIntervalTest.hs | 11 +++--- 5 files changed, 8 insertions(+), 41 deletions(-) delete mode 100644 persistent-postgresql/Database/Persist/Postgresql/Interval.hs diff --git a/cabal.project b/cabal.project index 1423283bc..5a6f1e02e 100644 --- a/cabal.project +++ b/cabal.project @@ -35,4 +35,4 @@ source-repository-package source-repository-package type: git location: https://github.com/MercuryTechnologies/postgresql-simple-interval - tag: c2e9e886162a37cc177986d223600567c2a156e8 + tag: aa85e163194fb2be4d884f3cdebf4d699b96601a diff --git a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs index 54f2eae4d..cc06633b4 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs @@ -72,7 +72,6 @@ import Data.Time , secondsToNominalDiffTime , utc ) -import Database.Persist.Postgresql.Interval () import Database.Persist.Sql import qualified Database.Persist.Sql.Util as Util diff --git a/persistent-postgresql/Database/Persist/Postgresql/Interval.hs b/persistent-postgresql/Database/Persist/Postgresql/Interval.hs deleted file mode 100644 index 7d5c5672b..000000000 --- a/persistent-postgresql/Database/Persist/Postgresql/Interval.hs +++ /dev/null @@ -1,34 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -Wno-orphans #-} - -module Database.Persist.Postgresql.Interval where - -import qualified Data.Attoparsec.ByteString.Char8 as A -import qualified Data.ByteString.Builder as Builder -import qualified Data.ByteString.Char8 as Ascii -import qualified Data.ByteString.Lazy as LazyByteString -import qualified Data.Text as Text -import qualified Database.Persist as Persist -import qualified Database.Persist.Sql as Persist -import qualified Database.PostgreSQL.Simple.Interval.Unstable as Interval -import qualified Database.PostgreSQL.Simple.ToField as Postgres - -instance Persist.PersistField Interval.Interval where - fromPersistValue persistValue = case persistValue of - Persist.PersistLiteral_ Persist.Unescaped byteString - | Just withoutPrefix <- Ascii.stripPrefix "interval '" byteString - , Just withoutSuffix <- Ascii.stripSuffix "'" withoutPrefix - , Right interval <- A.parseOnly Interval.parse withoutSuffix -> - Right interval - _ -> Left $ "invalid interval: " <> Text.pack (show persistValue) - - toPersistValue = - Persist.PersistLiteral_ Persist.Unescaped - . LazyByteString.toStrict - . Builder.toLazyByteString - . ("interval " <>) - . Postgres.inQuotes - . Interval.render - -instance Persist.PersistFieldSql Interval.Interval where - sqlType = const $ Persist.SqlOther "interval" diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index 4d84b2407..e092fc6ed 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -41,7 +41,6 @@ library exposed-modules: Database.Persist.Postgresql Database.Persist.Postgresql.Internal - Database.Persist.Postgresql.Interval Database.Persist.Postgresql.JSON ghc-options: -Wall diff --git a/persistent-postgresql/test/PgIntervalTest.hs b/persistent-postgresql/test/PgIntervalTest.hs index e99edf30b..e336386c7 100644 --- a/persistent-postgresql/test/PgIntervalTest.hs +++ b/persistent-postgresql/test/PgIntervalTest.hs @@ -17,10 +17,9 @@ module PgIntervalTest where -import Data.Fixed (Fixed (MkFixed)) +import Data.Fixed (Fixed (MkFixed), Micro, Pico) import Data.Time.Clock (secondsToNominalDiffTime) import Database.Persist.Postgresql (PgInterval (..)) -import Database.Persist.Postgresql.Interval () import qualified Database.PostgreSQL.Simple.Interval as Interval import PgInit import Test.Hspec.QuickCheck @@ -44,8 +43,12 @@ specs = do prop "Round trips" $ \int64 -> runConnAssert $ do let eg = - PgIntervalDb . PgInterval . secondsToNominalDiffTime . MkFixed $ - toInteger (int64 :: Int64) * 1000000 + PgIntervalDb + . PgInterval + . secondsToNominalDiffTime + . (realToFrac :: Micro -> Pico) + . MkFixed + $ toInteger (int64 :: Int64) rid <- insert eg r <- getJust rid liftIO $ r `shouldBe` eg From 2e6a88a963e849c3a57422f1b5fbb002a30818f4 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Fri, 11 Jul 2025 10:21:34 -0500 Subject: [PATCH 08/22] Upgrade to postgresql-simple-interval 0.2025.7.11 --- cabal.project | 5 ----- persistent-postgresql/persistent-postgresql.cabal | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cabal.project b/cabal.project index 5a6f1e02e..9ad070ab0 100644 --- a/cabal.project +++ b/cabal.project @@ -31,8 +31,3 @@ source-repository-package -- type: git -- location: https://github.com/ysangkok/hedis -- tag: 6f36989836b49974f51a6ee8edaf156490590980 - -source-repository-package - type: git - location: https://github.com/MercuryTechnologies/postgresql-simple-interval - tag: aa85e163194fb2be4d884f3cdebf4d699b96601a diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index e092fc6ed..b0c282d16 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -28,7 +28,7 @@ library , persistent >=2.13.3 && <3 , postgresql-libpq >=0.9.4.2 && <0.12 , postgresql-simple >=0.6.1 && <0.8 - , postgresql-simple-interval ==0.2025.7.10 + , postgresql-simple-interval ==0.2025.7.11 , resource-pool , resourcet >=1.1.9 , string-conversions From 15ac84b87d131e907dc25510ababa4b143096260 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Fri, 11 Jul 2025 10:30:34 -0500 Subject: [PATCH 09/22] Avoid building dependencies separately --- .github/workflows/haskell.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/haskell.yml b/.github/workflows/haskell.yml index aab41f640..5049a8d9c 100644 --- a/.github/workflows/haskell.yml +++ b/.github/workflows/haskell.yml @@ -86,7 +86,6 @@ jobs: restore-keys: | ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} ${{ runner.os }}-${{ matrix.ghc }}- - - run: cabal v2-build all --disable-optimization --only-dependencies $CONFIG - run: cabal v2-build all --disable-optimization $CONFIG - run: cabal v2-test all --disable-optimization $CONFIG --test-options "--fail-on-focus" - run: cabal v2-bench all --disable-optimization $CONFIG From c2821cc3e1b27ae7da54e953e41421d5c81953ca Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Sat, 12 Jul 2025 10:09:57 -0500 Subject: [PATCH 10/22] Upgrade to postgresql-simple-interval 0.2025.7.12 --- persistent-postgresql/persistent-postgresql.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index b0c282d16..d682856e1 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -28,7 +28,7 @@ library , persistent >=2.13.3 && <3 , postgresql-libpq >=0.9.4.2 && <0.12 , postgresql-simple >=0.6.1 && <0.8 - , postgresql-simple-interval ==0.2025.7.11 + , postgresql-simple-interval ==0.2025.7.12 , resource-pool , resourcet >=1.1.9 , string-conversions From a93db23f4d3ce3bf8d42a4f35c2c895633c21df7 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Sat, 12 Jul 2025 10:40:38 -0500 Subject: [PATCH 11/22] Limit microsecond range --- persistent-postgresql/test/PgIntervalTest.hs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/persistent-postgresql/test/PgIntervalTest.hs b/persistent-postgresql/test/PgIntervalTest.hs index e336386c7..be9f67cb7 100644 --- a/persistent-postgresql/test/PgIntervalTest.hs +++ b/persistent-postgresql/test/PgIntervalTest.hs @@ -37,6 +37,19 @@ IntervalDb deriving Eq Show |] +clamp :: Ord a => a -> a -> a +clamp lo hi = max lo . min hi + +-- Before version 15, PostgreSQL can't parse all possible intervals. +-- Each component is limited to the range of Int32. +-- So anything beyond 2,147,483,647 hours will fail to parse. + +leastMicroseconds :: Int64 +leastMicroseconds = -2147483648 * 60 * 60 * 1000000 + +mostMicroseconds :: Int64 +mostMicroseconds = 2147483647 * 60 * 60 * 1000000 + specs :: Spec specs = do describe "Postgres Interval Property tests" $ do @@ -48,14 +61,15 @@ specs = do . secondsToNominalDiffTime . (realToFrac :: Micro -> Pico) . MkFixed - $ toInteger (int64 :: Int64) + . toInteger + $ clamp leastMicroseconds mostMicroseconds int64 rid <- insert eg r <- getJust rid liftIO $ r `shouldBe` eg prop "interval round trips" $ \(m, d, u) -> runConnAssert $ do let - expected = IntervalDb $ Interval.MkInterval m d u + expected = IntervalDb . Interval.MkInterval m d $ clamp leastMicroseconds mostMicroseconds u key <- insert expected actual <- getJust key liftIO $ actual `shouldBe` expected From aec020a9f5ee428976679063899d41c2fc526ff8 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Sat, 12 Jul 2025 12:37:32 -0500 Subject: [PATCH 12/22] Fix clamp --- persistent-postgresql/test/PgIntervalTest.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/persistent-postgresql/test/PgIntervalTest.hs b/persistent-postgresql/test/PgIntervalTest.hs index be9f67cb7..97582cb1a 100644 --- a/persistent-postgresql/test/PgIntervalTest.hs +++ b/persistent-postgresql/test/PgIntervalTest.hs @@ -37,7 +37,7 @@ IntervalDb deriving Eq Show |] -clamp :: Ord a => a -> a -> a +clamp :: (Ord a) => a -> a -> a -> a clamp lo hi = max lo . min hi -- Before version 15, PostgreSQL can't parse all possible intervals. From 517e0c7ba5da7a2749ef76c2884d6ed7f53ca84f Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 14 Jul 2025 09:35:41 -0500 Subject: [PATCH 13/22] Fix tests --- persistent-postgresql/test/PgIntervalTest.hs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/persistent-postgresql/test/PgIntervalTest.hs b/persistent-postgresql/test/PgIntervalTest.hs index 97582cb1a..0c42ade68 100644 --- a/persistent-postgresql/test/PgIntervalTest.hs +++ b/persistent-postgresql/test/PgIntervalTest.hs @@ -44,11 +44,8 @@ clamp lo hi = max lo . min hi -- Each component is limited to the range of Int32. -- So anything beyond 2,147,483,647 hours will fail to parse. -leastMicroseconds :: Int64 -leastMicroseconds = -2147483648 * 60 * 60 * 1000000 - -mostMicroseconds :: Int64 -mostMicroseconds = 2147483647 * 60 * 60 * 1000000 +microsecondLimit :: Int64 +microsecondLimit = 2147483647 * 60 * 60 * 1000000 specs :: Spec specs = do @@ -62,14 +59,16 @@ specs = do . (realToFrac :: Micro -> Pico) . MkFixed . toInteger - $ clamp leastMicroseconds mostMicroseconds int64 + $ clamp (-microsecondLimit) microsecondLimit int64 rid <- insert eg r <- getJust rid liftIO $ r `shouldBe` eg prop "interval round trips" $ \(m, d, u) -> runConnAssert $ do let - expected = IntervalDb . Interval.MkInterval m d $ clamp leastMicroseconds mostMicroseconds u + expected = + IntervalDb . Interval.MkInterval m d $ + clamp (-microsecondLimit) microsecondLimit u key <- insert expected actual <- getJust key liftIO $ actual `shouldBe` expected From cb00db5cb19647eb533f2ccd56a823ffc31ebf3b Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 14 Jul 2025 09:50:37 -0500 Subject: [PATCH 14/22] Remove unnecessary dependency --- persistent-postgresql/persistent-postgresql.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index d682856e1..51fd5fd21 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -68,7 +68,6 @@ test-suite test ghc-options: -Wall build-depends: aeson - , attoparsec , base >=4.9 && <5 , bytestring , containers From 027f902f7ca5810cc32a5a64e1409c20cb101bc9 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Tue, 2 Sep 2025 09:39:06 -0500 Subject: [PATCH 15/22] Upgrade to postgresql-simple-interval 0.2025.8.27 --- persistent-postgresql/persistent-postgresql.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index 51fd5fd21..48fa9333f 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -28,7 +28,7 @@ library , persistent >=2.13.3 && <3 , postgresql-libpq >=0.9.4.2 && <0.12 , postgresql-simple >=0.6.1 && <0.8 - , postgresql-simple-interval ==0.2025.7.12 + , postgresql-simple-interval ==0.2025.8.27 , resource-pool , resourcet >=1.1.9 , string-conversions From 913f4adbc7538f99c4f3c209a4b8c42aa2486bdf Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 8 Sep 2025 09:03:43 -0500 Subject: [PATCH 16/22] Upgrade to postgresql-simple-interval 0.2025.9.5 --- .../Database/Persist/Postgresql/Internal.hs | 29 +++++++------------ .../persistent-postgresql.cabal | 2 +- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs index cc06633b4..b37c5304d 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs @@ -241,7 +241,7 @@ newtype PgInterval = PgInterval {getPgInterval :: NominalDiffTime} deriving (Eq, Show) instance PGTF.ToField PgInterval where - toField = PGTF.toField . fromMaybe (error "PgInterval.toField") . pgIntervalToInterval + toField = PGTF.toField . pgIntervalToInterval instance PGFF.FromField PgInterval where fromField f = @@ -252,7 +252,6 @@ instance PGFF.FromField PgInterval where instance PersistField PgInterval where toPersistValue = toPersistValue - . fromMaybe (error "PgInterval.toPersistValue") . pgIntervalToInterval fromPersistValue = maybe (Left "invalid interval") pure @@ -262,27 +261,19 @@ instance PersistField PgInterval where instance PersistFieldSql PgInterval where sqlType _ = SqlOther "interval" -pgIntervalToInterval :: PgInterval -> Maybe Interval.Interval +pgIntervalToInterval :: PgInterval -> Interval.Interval pgIntervalToInterval = - fmap Interval.fromMicroseconds - . toIntegralSized - . (\(MkFixed x) -> x) - . (realToFrac :: Pico -> Micro) - . nominalDiffTimeToSeconds + Interval.fromTimeSaturating mempty . getPgInterval intervalToPgInterval :: Interval.Interval -> Maybe PgInterval -intervalToPgInterval interval - | Interval.months interval /= 0 = Nothing - | Interval.days interval /= 0 = Nothing - | otherwise = - Just - . PgInterval - . secondsToNominalDiffTime - . (realToFrac :: Micro -> Pico) - . MkFixed - . toInteger - $ Interval.microseconds interval +intervalToPgInterval interval = + let + (calendarDiffDays, nominalDiffTime) = Interval.intoTime interval + in + if calendarDiffDays == mempty + then Just $ PgInterval nominalDiffTime + else Nothing -- | Indicates whether a Postgres Column is safe to drop. -- diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index 48fa9333f..56581333c 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -28,7 +28,7 @@ library , persistent >=2.13.3 && <3 , postgresql-libpq >=0.9.4.2 && <0.12 , postgresql-simple >=0.6.1 && <0.8 - , postgresql-simple-interval ==0.2025.8.27 + , postgresql-simple-interval ==0.2025.9.5 , resource-pool , resourcet >=1.1.9 , string-conversions From 0d27391125b43f102c6d9c2fafbf0bf03df996cc Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 8 Sep 2025 09:11:54 -0500 Subject: [PATCH 17/22] Add more documentation to PgInterval --- .../Database/Persist/Postgresql/Internal.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs index b37c5304d..c48381cb4 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs @@ -237,6 +237,11 @@ unBinary (PG.Binary x) = x -- | Represent Postgres interval using NominalDiffTime -- -- @since 2.11.0.0 +-- +-- Note that this type cannot be losslessly round tripped through PostgreSQL. +-- For example the value @'PgInterval' 0.0000009@ will truncate extra +-- precision. And the value @'PgInterval' 9223372036854.775808@ will overflow. +-- Use the 'Interval.Interval' type if that is a problem for you. newtype PgInterval = PgInterval {getPgInterval :: NominalDiffTime} deriving (Eq, Show) From bcc8404229e8c0ae2d4b8ab4fa8168f491d33151 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 8 Sep 2025 10:54:07 -0500 Subject: [PATCH 18/22] Update change log --- persistent-postgresql/ChangeLog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/persistent-postgresql/ChangeLog.md b/persistent-postgresql/ChangeLog.md index b91d977df..1e2b0243f 100644 --- a/persistent-postgresql/ChangeLog.md +++ b/persistent-postgresql/ChangeLog.md @@ -1,5 +1,13 @@ # Changelog for persistent-postgresql +## 2.14.0.0 + +* [#1604](https://github.com/yesodweb/persistent/pull/1604) + * Changed the representation of intervals to use the `Interval` type from [the `postgresql-simple-interval` package](https://hackage.haskell.org/package/postgresql-simple-interval). + This changes the behavior of `PgInterval` for very small and very large values. + * Previously `PgInterval 0.000_000_9` would be rounded to `0.000_001` seconds, but now it is truncated to 0 seconds. + * Previously `PgInterval 9_223_372_036_854.775_808` would overflow and throw a SQL error, but now it saturates to `9_223_372_036_854.775_807` seconds. + ## 2.13.7.0 * [#1600](https://github.com/yesodweb/persistent/pull/1600) From 4f5f5a719d9a25d84289264dde013dfb0baf8063 Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 8 Sep 2025 10:57:59 -0500 Subject: [PATCH 19/22] Update change log --- persistent-postgresql/ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/persistent-postgresql/ChangeLog.md b/persistent-postgresql/ChangeLog.md index 1e2b0243f..4500e1aac 100644 --- a/persistent-postgresql/ChangeLog.md +++ b/persistent-postgresql/ChangeLog.md @@ -7,6 +7,7 @@ This changes the behavior of `PgInterval` for very small and very large values. * Previously `PgInterval 0.000_000_9` would be rounded to `0.000_001` seconds, but now it is truncated to 0 seconds. * Previously `PgInterval 9_223_372_036_854.775_808` would overflow and throw a SQL error, but now it saturates to `9_223_372_036_854.775_807` seconds. + * The SQL representation of `PgInterval` now always includes the `interval` prefix, like `interval '1 second'`. ## 2.13.7.0 From 398e084b29ddfa0cb4d2c6b68b1b4a57cea57e9c Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 8 Sep 2025 11:18:38 -0500 Subject: [PATCH 20/22] Update postgresql-simple-interval version constraint --- persistent-postgresql/persistent-postgresql.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/persistent-postgresql/persistent-postgresql.cabal b/persistent-postgresql/persistent-postgresql.cabal index 56581333c..982715227 100644 --- a/persistent-postgresql/persistent-postgresql.cabal +++ b/persistent-postgresql/persistent-postgresql.cabal @@ -28,7 +28,7 @@ library , persistent >=2.13.3 && <3 , postgresql-libpq >=0.9.4.2 && <0.12 , postgresql-simple >=0.6.1 && <0.8 - , postgresql-simple-interval ==0.2025.9.5 + , postgresql-simple-interval >=1 && < 1.1 , resource-pool , resourcet >=1.1.9 , string-conversions From d235d4d2f387490e5e7dca17c5500a24d25eb6cb Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 8 Sep 2025 11:25:02 -0500 Subject: [PATCH 21/22] Rerun CI From 285647a9e57d8733087a8f36a01f66a316848a3b Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Tue, 9 Sep 2025 10:43:31 -0500 Subject: [PATCH 22/22] Move `@since` annotation to the end Co-authored-by: Matt Parsons --- persistent-postgresql/Database/Persist/Postgresql/Internal.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs index c48381cb4..e4092e876 100644 --- a/persistent-postgresql/Database/Persist/Postgresql/Internal.hs +++ b/persistent-postgresql/Database/Persist/Postgresql/Internal.hs @@ -236,12 +236,12 @@ unBinary (PG.Binary x) = x -- | Represent Postgres interval using NominalDiffTime -- --- @since 2.11.0.0 --- -- Note that this type cannot be losslessly round tripped through PostgreSQL. -- For example the value @'PgInterval' 0.0000009@ will truncate extra -- precision. And the value @'PgInterval' 9223372036854.775808@ will overflow. -- Use the 'Interval.Interval' type if that is a problem for you. +-- +-- @since 2.11.0.0 newtype PgInterval = PgInterval {getPgInterval :: NominalDiffTime} deriving (Eq, Show)