Skip to content

Commit b4430b0

Browse files
committed
Fix missing timezone in Sqlite UTCTime string
1 parent 0e34ba9 commit b4430b0

5 files changed

Lines changed: 41 additions & 8 deletions

File tree

persistent-sqlite/ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog for persistent-sqlite
22

3+
## 2.13.3.1
4+
5+
* [1585](https://github.com/yesodweb/persistent/pull/1585)
6+
* Fix missing timezone "Z" in Sqlite UTCTime strings, e.g.
7+
"2025-04-12T06:53:42Z"
8+
39
## 2.13.3.0
410

511
* [#1524](https://github.com/yesodweb/persistent/pull/1524)

persistent-sqlite/Database/Sqlite.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,8 +486,13 @@ bind statement sqlData = do
486486
$ zip [1..] sqlData
487487
return ()
488488

489+
-- | Format UTCTime value as a ISO86091 string.
490+
-- It contains timezone "Z" which corresponds to UTC, e.g. "2025-04-12T06:53:42Z".
491+
-- Note: We manually format here to support "time" package >=1.6,
492+
-- but consider using `Data.Time.Format.ISO8601` iso8601Show and iso8601ParseM when bumping
493+
-- lower "time" package bound to >=1.9.
489494
format8601 :: UTCTime -> String
490-
format8601 = formatTime defaultTimeLocale "%FT%T%Q"
495+
format8601 = formatTime defaultTimeLocale "%FT%T%QZ"
491496

492497
foreign import ccall "sqlite3_column_type"
493498
columnTypeC :: Ptr () -> Int -> IO Int

persistent/ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog for persistent
22

3+
# 2.15.1.1
4+
5+
* [#1585](https://github.com/yesodweb/persistent/pull/1585)
6+
* Support parsing PersistField UTCTime from text with timezone, e.g. "2025-04-12T06:53:42Z".
7+
This is needed for Sqlite, which has no native datetime support but uses e.g. TEXT.
8+
39
# 2.15.1.0
410

511
* [#1519](https://github.com/yesodweb/persistent/pull/1519/files/9865a295f4545d30e55aacb6efc25f27f758e8ad#diff-5af2883367baae8f7f266df6a89fc2d1defb7572d94ed069e05c8135a883bc45)

persistent/Database/Persist/Class/PersistField.hs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ instance PersistField UTCTime where
311311
in
312312
case NonEmpty.nonEmpty (reads s) of
313313
Nothing ->
314-
case parse8601 s <|> parsePretty s of
314+
case parse8601 s <|> parse8601NoTimezone s <|> parsePretty s <|> parsePrettyNoTimezone s of
315315
Nothing -> Left $ fromPersistValueParseError "UTCTime" x
316316
Just x' -> Right x'
317317
Just matches ->
@@ -323,12 +323,20 @@ instance PersistField UTCTime where
323323
Right $ fst $ NonEmpty.last matches
324324
where
325325
#if MIN_VERSION_time(1,5,0)
326+
-- Note: consider using `Data.Time.Format.ISO8601` iso8601ParseM when bumping
327+
-- lower "time" package bound to >=1.9; this function require the timezone "Z",
328+
-- so best suitable when e.g. dropping support non-canonial "notimezone" and "pretty" parse variants
329+
-- in persistent 3.0.
326330
parseTime' = parseTimeM True defaultTimeLocale
327331
#else
328332
parseTime' = parseTime defaultTimeLocale
329333
#endif
330-
parse8601 = parseTime' "%FT%T%Q"
331-
parsePretty = parseTime' "%F %T%Q"
334+
parse8601 = parseTime' "%FT%T%QZ"
335+
parsePretty = parseTime' "%F %T%QZ"
336+
-- Before 2.13.3.1 persistent-sqlite was missing the timezone "Z" for UTC,
337+
-- which was only implicit, so these functions ensure backwards-compatibility.
338+
parse8601NoTimezone = parseTime' "%FT%T%Q"
339+
parsePrettyNoTimezone = parseTime' "%F %T%Q"
332340
fromPersistValue x@(PersistByteString s) =
333341
case reads $ unpack s of
334342
(d, _):_ -> Right d
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
module Database.Persist.ClassSpec where
22

3-
import Database.Persist.Class
43
import Data.Time
4+
import Database.Persist.Class
55
import Database.Persist.Types
66
import Test.Hspec
77

88
spec :: Spec
99
spec = describe "Class" $ do
1010
describe "PersistField" $ do
1111
describe "UTCTime" $ do
12-
it "fromPersistValue with format" $
12+
it "fromPersistValue with ISO8601 format including UTC timezone Z (canonical)" $
13+
fromPersistValue (PersistText "2018-02-27T10:49:42.123Z")
14+
`shouldBe` Right
15+
(UTCTime (fromGregorian 2018 02 27) (timeOfDayToTime (TimeOfDay 10 49 42.123)))
16+
it "fromPersistValue with ISO8601 format no timezone (backwards-compatibility)" $
17+
fromPersistValue (PersistText "2018-02-27T10:49:42.123")
18+
`shouldBe` Right
19+
(UTCTime (fromGregorian 2018 02 27) (timeOfDayToTime (TimeOfDay 10 49 42.123)))
20+
it "fromPersistValue with pretty format (backwards-compatibility)" $
1321
fromPersistValue (PersistText "2018-02-27 10:49:42.123")
14-
`shouldBe`
15-
Right (UTCTime (fromGregorian 2018 02 27) (timeOfDayToTime (TimeOfDay 10 49 42.123)))
22+
`shouldBe` Right
23+
(UTCTime (fromGregorian 2018 02 27) (timeOfDayToTime (TimeOfDay 10 49 42.123)))

0 commit comments

Comments
 (0)