diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Change log
+
+postgresql-simple-interval follows the [Package Versioning Policy](https://pvp.haskell.org). You can
+find release notes [on GitHub](https://github.com/MercuryTechnologies/postgresql-simple-interval/releases).
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,14 @@
+BSD Zero Clause License
+
+Copyright (c) 2025 Mercury Technologies, Inc.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# postgresql-simple-interval
+
+A simple interval type for PostgreSQL.
diff --git a/postgresql-simple-interval.cabal b/postgresql-simple-interval.cabal
new file mode 100644
--- /dev/null
+++ b/postgresql-simple-interval.cabal
@@ -0,0 +1,72 @@
+cabal-version: 3.0
+name: postgresql-simple-interval
+version: 0.2025.7.9
+synopsis: A simple interval type for PostgreSQL.
+description:
+  This library provides a simple interval type for PostgreSQL. It supports
+  round-tripping through Postgres without losing information.
+
+category: Database
+license: 0BSD
+license-file: LICENSE.txt
+maintainer: Mercury Technologies, Inc.
+extra-doc-files:
+  CHANGELOG.md
+  README.md
+
+source-repository head
+  location: https://github.com/MercuryTechnologies/postgresql-simple-interval
+  type: git
+
+flag pedantic
+  default: False
+  manual: True
+
+common library
+  build-depends:
+    attoparsec ^>=0.14.4,
+    base ^>={4.19, 4.20, 4.21},
+    bytestring ^>=0.12.1,
+
+  default-language: Haskell2010
+  ghc-options:
+    -Weverything
+    -Wno-all-missed-specialisations
+    -Wno-implicit-prelude
+    -Wno-missing-deriving-strategies
+    -Wno-missing-export-lists
+    -Wno-missing-kind-signatures
+    -Wno-missing-safe-haskell-mode
+    -Wno-prepositive-qualified-module
+    -Wno-safe
+    -Wno-unsafe
+
+  if flag(pedantic)
+    ghc-options: -Werror
+
+common executable
+  import: library
+  build-depends: postgresql-simple-interval
+  ghc-options:
+    -rtsopts
+    -threaded
+
+library
+  import: library
+  build-depends:
+    postgresql-simple ^>=0.7,
+    scientific ^>=0.3.8,
+
+  -- cabal-gild: discover source/library
+  exposed-modules:
+    Database.PostgreSQL.Interval
+    Database.PostgreSQL.Interval.Unstable
+
+  hs-source-dirs: source/library
+
+test-suite postgresql-simple-interval-test-suite
+  import: executable
+  build-depends: hspec ^>=2.11.12
+  hs-source-dirs: source/test-suite
+  main-is: Main.hs
+  type: exitcode-stdio-1.0
diff --git a/source/library/Database/PostgreSQL/Interval.hs b/source/library/Database/PostgreSQL/Interval.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Database/PostgreSQL/Interval.hs
@@ -0,0 +1,16 @@
+module Database.PostgreSQL.Interval
+  ( Unstable.Interval (..),
+    Unstable.zero,
+    Unstable.fromMicroseconds,
+    Unstable.fromMilliseconds,
+    Unstable.fromSeconds,
+    Unstable.fromMinutes,
+    Unstable.fromHours,
+    Unstable.fromDays,
+    Unstable.fromWeeks,
+    Unstable.fromMonths,
+    Unstable.fromYears,
+  )
+where
+
+import qualified Database.PostgreSQL.Interval.Unstable as Unstable
diff --git a/source/library/Database/PostgreSQL/Interval/Unstable.hs b/source/library/Database/PostgreSQL/Interval/Unstable.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Database/PostgreSQL/Interval/Unstable.hs
@@ -0,0 +1,344 @@
+{-# LANGUAGE LexicalNegation #-}
+{-# LANGUAGE NumDecimals #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Database.PostgreSQL.Interval.Unstable where
+
+import qualified Control.Applicative as Applicative
+import qualified Control.Monad as Monad
+import qualified Data.Attoparsec.ByteString.Char8 as A
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.Function as Function
+import qualified Data.Int as Int
+import qualified Data.Scientific as Scientific
+import qualified Database.PostgreSQL.Simple.FromField as Postgres
+import qualified Database.PostgreSQL.Simple.ToField as Postgres
+import qualified Database.PostgreSQL.Simple.TypeInfo.Static as Postgres
+
+-- | This type represents a PostgreSQL interval. Intervals can have month, day,
+-- and microsecond components. Each component is bounded, so they are not
+-- arbitrary precision. For more information about intervals, consult the
+-- PostgreSQL documentation:
+-- <https://www.postgresql.org/docs/17/datatype-datetime.html#DATATYPE-INTERVAL-INPUT>.
+--
+-- Note that the @time@ library provides several duration types that are not
+-- appropriate to use as PostgreSQL intervals:
+--
+-- - @NominalDiffTime@: Does not handle days or months. Allows up to picosecond
+--   precision. Is not bounded.
+-- - @CalendarDiffTime@: Does not handle days. Embeds a @NominalDiffTime@. Is
+--   not bounded.
+-- - @CalendarDiffDays@: Does not handle seconds. Is not bounded.
+data Interval = MkInterval
+  { months :: !Int.Int32,
+    days :: !Int.Int32,
+    microseconds :: !Int.Int64
+  }
+  deriving (Eq, Show)
+
+-- | Uses 'parse'. Ensures that the OID is 'Postgres.intervalOid'.
+instance Postgres.FromField Interval where
+  fromField = Postgres.attoFieldParser (== Postgres.intervalOid) parse
+
+-- | Uses 'render'. Always includes an @interval@ prefix, like
+-- @interval '2 months 3 days 4 microseconds'@.
+instance Postgres.ToField Interval where
+  toField = Postgres.Plain . ("interval " <>) . Postgres.inQuotes . render
+
+-- | The empty interval, representing no time at all.
+--
+-- >>> zero
+-- MkInterval {months = 0, days = 0, microseconds = 0}
+zero :: Interval
+zero = MkInterval 0 0 0
+
+-- | Creates an interval from a number of microseconds.
+--
+-- >>> fromMicroseconds 1
+-- MkInterval {months = 0, days = 0, microseconds = 1}
+fromMicroseconds :: Int.Int64 -> Interval
+fromMicroseconds x = zero {microseconds = x}
+
+-- | Creates an interval from a number of milliseconds. Returns 'Nothing' if
+-- the interval would overflow.
+--
+-- >>> fromMilliseconds 1
+-- Just (MkInterval {months = 0, days = 0, microseconds = 1000})
+-- >>> fromMilliseconds 9223372036854776
+-- Nothing
+fromMilliseconds :: Int.Int64 -> Maybe Interval
+fromMilliseconds =
+  fmap fromMicroseconds
+    . Bits.toIntegralSized
+    . (* 1e3)
+    . toInteger
+
+-- | Creates an interval from a number of seconds. Returns 'Nothing' if the
+-- interval would overflow.
+--
+-- >>> fromSeconds 1
+-- Just (MkInterval {months = 0, days = 0, microseconds = 1000000})
+-- >>> fromSeconds 9223372036855
+-- Nothing
+fromSeconds :: Int.Int64 -> Maybe Interval
+fromSeconds =
+  fmap fromMicroseconds
+    . Bits.toIntegralSized
+    . (* 1e6)
+    . toInteger
+
+-- | Creates an interval from a number of minutes. Returns 'Nothing' if the
+-- interval would overflow.
+--
+-- >>> fromMinutes 1
+-- Just (MkInterval {months = 0, days = 0, microseconds = 60000000})
+-- >>> fromMinutes 153722867281
+-- Nothing
+fromMinutes :: Int.Int64 -> Maybe Interval
+fromMinutes =
+  fmap fromMicroseconds
+    . Bits.toIntegralSized
+    . (* 60e6)
+    . toInteger
+
+-- | Creates an interval from a number of hours. Returns 'Nothing' if the
+-- interval would overflow.
+--
+-- >>> fromHours 1
+-- Just (MkInterval {months = 0, days = 0, microseconds = 3600000000})
+-- >>> fromHours 2562047789
+-- Nothing
+fromHours :: Int.Int64 -> Maybe Interval
+fromHours =
+  fmap fromMicroseconds
+    . Bits.toIntegralSized
+    . (* 3600e6)
+    . toInteger
+
+-- | Creates an interval from a number of days.
+--
+-- >>> fromDays 1
+-- MkInterval {months = 0, days = 1, microseconds = 0}
+fromDays :: Int.Int32 -> Interval
+fromDays x = zero {days = x}
+
+-- | Creates an interval from a number of weeks. Returns 'Nothing' if the
+-- interval would overflow.
+--
+-- >>> fromWeeks 1
+-- Just (MkInterval {months = 0, days = 7, microseconds = 0})
+-- >>> fromWeeks 306783379
+-- Nothing
+fromWeeks :: Int.Int32 -> Maybe Interval
+fromWeeks =
+  fmap fromDays
+    . Bits.toIntegralSized
+    . (* 7)
+    . toInteger
+
+-- | Creates an interval from a number of months.
+--
+-- >>> fromMonths 1
+-- MkInterval {months = 1, days = 0, microseconds = 0}
+fromMonths :: Int.Int32 -> Interval
+fromMonths x = zero {months = x}
+
+-- | Creates an interval from a number of years. Returns 'Nothing' if the
+-- interval would overflow.
+--
+-- >>> fromYears 1
+-- Just (MkInterval {months = 12, days = 0, microseconds = 0})
+-- >>> fromYears 178956971
+-- Nothing
+fromYears :: Int.Int32 -> Maybe Interval
+fromYears =
+  fmap fromMonths
+    . Bits.toIntegralSized
+    . (* 12)
+    . toInteger
+
+-- | Adds two intervals. Returns 'Nothing' if the result would overflow.
+--
+-- >>> add (fromMonths 1) (fromDays 2)
+-- Just (MkInterval {months = 1, days = 2, microseconds = 0})
+-- >>> add (fromDays 2147483647) (fromDays 1)
+-- Nothing
+add :: Interval -> Interval -> Maybe Interval
+add x y =
+  let safeAdd :: (Bits.Bits a, Integral a) => a -> a -> Maybe a
+      safeAdd n = Bits.toIntegralSized . Function.on (+) toInteger n
+   in MkInterval
+        <$> Function.on safeAdd months x y
+        <*> Function.on safeAdd days x y
+        <*> Function.on safeAdd microseconds x y
+
+-- | Renders an interval to a 'Builder'. This always has the same format:
+-- @"X months Y days Z microseconds"@, where @X@, @Y@, and @Z@ 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 months -1 days +2 microseconds"
+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
+   in signed Builder.int32Dec (months x)
+        <> " months "
+        <> signed Builder.int32Dec (days x)
+        <> " days "
+        <> signed Builder.int64Dec (microseconds x)
+        <> " microseconds"
+
+-- | Parses an interval. This is not a general purpose parser. It only supports
+-- the formats that PostgreSQL generates. For example, it will fail to parse an
+-- interval like @"1 week"@ because PostgreSQL never uses weeks when rendering
+-- intervals.
+parse :: A.Parser Interval
+parse =
+  -- Start with parsers that have non-empty prefixes, in order to avoid
+  -- ambiguity. Neither of the `postgres` nor `sql_standard` interval styles
+  -- have a prefix (or suffix), so whichever one is attempted first needs to
+  -- make sure it has consumed all of the input.
+  A.choice $
+    parseInfinities
+      : fmap
+        (fromComponents =<<)
+        [ parseIso8601,
+          parsePostgresVerbose,
+          parsePostgres <* A.endOfInput,
+          parseSqlStandard <* A.endOfInput
+        ]
+
+parseInfinities :: A.Parser Interval
+parseInfinities =
+  -- Both `-infinity` and `infinity` are new as of PostgreSQL 17.0.
+  -- https://www.postgresql.org/message-id/E1r2rB1-005PHm-UL%40gemulon.postgresql.org
+  A.choice
+    [ MkInterval minBound minBound minBound <$ "-infinity",
+      MkInterval maxBound maxBound maxBound <$ "infinity"
+    ]
+
+parseIso8601 :: A.Parser [Component]
+parseIso8601 = do
+  Monad.void "P"
+  dates <-
+    A.many' $
+      A.choice
+        [ Years <$> A.signed A.decimal <* "Y",
+          Months <$> A.signed A.decimal <* "M",
+          Days <$> A.signed A.decimal <* "D"
+        ]
+  times <- A.option [] $ do
+    Monad.void "T"
+    A.many' $
+      A.choice
+        [ Hours <$> A.signed A.decimal <* "H",
+          Minutes <$> A.signed A.decimal <* "M",
+          Microseconds <$> A.signed A.scientific <* "S"
+        ]
+  pure $ dates <> times
+
+parsePostgresVerbose :: A.Parser [Component]
+parsePostgresVerbose = do
+  Monad.void "@ "
+  components <-
+    flip A.sepBy " " $
+      A.choice
+        [ Years <$> A.signed A.decimal <* maybePlural " year",
+          Months <$> A.signed A.decimal <* maybePlural " mon",
+          Days <$> A.signed A.decimal <* maybePlural " day",
+          Hours <$> A.signed A.decimal <* maybePlural " hour",
+          Minutes <$> A.signed A.decimal <* maybePlural " min",
+          Microseconds <$> A.signed A.scientific <* A.option "" (maybePlural " sec")
+        ]
+  ago <- A.option "" " ago"
+  pure $ negateComponentsWhen (not $ ByteString.null ago) components
+
+parsePostgres :: A.Parser [Component]
+parsePostgres = do
+  dates <-
+    flip A.sepBy " " $
+      A.choice
+        [ Years <$> A.signed A.decimal <* maybePlural " year",
+          Months <$> A.signed A.decimal <* maybePlural " mon",
+          Days <$> A.signed A.decimal <* maybePlural " day"
+        ]
+  time <- A.option [] $ A.skipSpace *> parseTime
+  pure $ dates <> time
+
+parseSqlStandard :: A.Parser [Component]
+parseSqlStandard = do
+  let parseYearsAndMonths = do
+        sign <- parseSign
+        years <- Years <$> A.decimal <* "-"
+        months_ <- Months <$> A.decimal
+        pure $ negateComponentsWhen (sign == "-") [years, months_]
+  let parseDays = (: []) . Days <$> A.signed A.decimal
+  let parsers = [parseYearsAndMonths, parseTime, parseDays]
+  mconcat <$> A.sepBy1 (A.choice parsers) " "
+
+parseTime :: A.Parser [Component]
+parseTime = do
+  sign <- parseSign
+  hours <- Hours <$> A.decimal <* ":"
+  minutes <- Minutes <$> A.decimal <* ":"
+  micros <- Microseconds <$> A.scientific
+  pure $ negateComponentsWhen (sign == "-") [hours, minutes, micros]
+
+parseSign :: A.Parser ByteString.ByteString
+parseSign = A.choice ["-", "+", ""]
+
+maybePlural :: ByteString.ByteString -> A.Parser ByteString.ByteString
+maybePlural word = (<>) <$> A.string word <*> A.option "" "s"
+
+-- | One component of an interval. This is used to retain arbitrary precision
+-- for as long as possible before converting. It also shows which components
+-- are accepted, like years and months.
+data Component
+  = Years !Integer
+  | Months !Integer
+  | Days !Integer
+  | Hours !Integer
+  | Minutes !Integer
+  | Microseconds !Scientific.Scientific
+  deriving (Eq, Show)
+
+-- | Converts a 'Component' to an 'Interval'. Returns 'Nothing' if the
+-- component would overflow.
+fromComponent :: Component -> Maybe Interval
+fromComponent c = case c of
+  Years y -> fromYears =<< Bits.toIntegralSized y
+  Months m -> fromMonths <$> Bits.toIntegralSized m
+  Days d -> fromDays <$> Bits.toIntegralSized d
+  Hours h -> fromHours =<< Bits.toIntegralSized h
+  Minutes m -> fromMinutes =<< Bits.toIntegralSized m
+  Microseconds u -> fromMicroseconds <$> Scientific.toBoundedInteger (u * 1e6)
+
+-- | Converts a list of 'Component's to an 'Interval'. Returns 'Nothing' if any
+-- of the components would overflow, or if adding any of them together would
+-- overflow.
+fromComponents ::
+  (Applicative.Alternative f, Traversable t) =>
+  t Component ->
+  f Interval
+fromComponents =
+  maybe Applicative.empty pure
+    . (Monad.foldM add zero Monad.<=< traverse fromComponent)
+
+negateComponent :: Component -> Component
+negateComponent c = case c of
+  Years y -> Years -y
+  Months m -> Months -m
+  Days d -> Days -d
+  Hours h -> Hours -h
+  Minutes m -> Minutes -m
+  Microseconds u -> Microseconds -u
+
+negateComponentsWhen :: (Functor f) => Bool -> f Component -> f Component
+negateComponentsWhen p = if p then fmap negateComponent else id
diff --git a/source/test-suite/Main.hs b/source/test-suite/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/test-suite/Main.hs
@@ -0,0 +1,222 @@
+{-# LANGUAGE LexicalNegation #-}
+{-# LANGUAGE NumDecimals #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+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.Char8 as Ascii
+import qualified Data.Int as Int
+import qualified Database.PostgreSQL.Interval.Unstable as I
+import qualified Test.Hspec as H
+
+main :: IO ()
+main = H.hspec spec
+
+spec :: H.Spec
+spec = H.describe "Database.PostgreSQL.Interval" $ do
+  H.describe "zero" $ do
+    H.it "works" $ do
+      I.zero `H.shouldBe` I.MkInterval 0 0 0
+
+  H.describe "add" $ do
+    H.it "succeeds with no overflow" $ do
+      let actual = I.add (I.MkInterval 1 2 3) (I.MkInterval 4 5 6)
+      actual `H.shouldBe` Just (I.MkInterval 5 7 9)
+
+    H.it "fails with positive month overflow" $ do
+      let actual = I.add (I.fromMonths maxBound) (I.fromMonths 1)
+      actual `H.shouldBe` Nothing
+
+    H.it "fails with negative month overflow" $ do
+      let actual = I.add (I.fromMonths minBound) (I.fromMonths -1)
+      actual `H.shouldBe` Nothing
+
+    H.it "fails with positive day overflow" $ do
+      let actual = I.add (I.fromDays maxBound) (I.fromDays 1)
+      actual `H.shouldBe` Nothing
+
+    H.it "fails with negative day overflow" $ do
+      let actual = I.add (I.fromDays minBound) (I.fromDays -1)
+      actual `H.shouldBe` Nothing
+
+    H.it "fails with positive microsecond overflow" $ do
+      let actual = I.add (I.fromMicroseconds maxBound) (I.fromMicroseconds 1)
+      actual `H.shouldBe` Nothing
+
+    H.it "fails with negative microsecond overflow" $ do
+      let actual = I.add (I.fromMicroseconds minBound) (I.fromMicroseconds -1)
+      actual `H.shouldBe` Nothing
+
+  H.describe "fromMicroseconds" $ do
+    H.it "works" $ do
+      I.fromMicroseconds 1 `H.shouldBe` I.MkInterval 0 0 1
+
+  H.describe "fromMilliseconds" $ do
+    H.it "succeeds with no overflow" $ do
+      I.fromMilliseconds 1 `H.shouldBe` Just (I.MkInterval 0 0 1e3)
+
+    H.it "fails with overflow" $ do
+      I.fromMilliseconds maxBound `H.shouldBe` Nothing
+
+  H.describe "fromSeconds" $ do
+    H.it "succeeds with no overflow" $ do
+      I.fromSeconds 1 `H.shouldBe` Just (I.MkInterval 0 0 1e6)
+
+    H.it "fails with overflow" $ do
+      I.fromSeconds maxBound `H.shouldBe` Nothing
+
+  H.describe "fromMinutes" $ do
+    H.it "succeeds with no overflow" $ do
+      I.fromMinutes 1 `H.shouldBe` Just (I.MkInterval 0 0 60e6)
+
+    H.it "fails with overflow" $ do
+      I.fromMinutes maxBound `H.shouldBe` Nothing
+
+  H.describe "fromHours" $ do
+    H.it "succeeds with no overflow" $ do
+      I.fromHours 1 `H.shouldBe` Just (I.MkInterval 0 0 3600e6)
+
+    H.it "fails with overflow" $ do
+      I.fromHours maxBound `H.shouldBe` Nothing
+
+  H.describe "fromDays" $ do
+    H.it "works" $ do
+      I.fromDays 1 `H.shouldBe` I.MkInterval 0 1 0
+
+  H.describe "fromWeeks" $ do
+    H.it "succeeds with no overflow" $ do
+      I.fromWeeks 1 `H.shouldBe` Just (I.MkInterval 0 7 0)
+
+    H.it "fails with overflow" $ do
+      I.fromWeeks maxBound `H.shouldBe` Nothing
+
+  H.describe "fromMonths" $ do
+    H.it "works" $ do
+      I.fromMonths 1 `H.shouldBe` I.MkInterval 1 0 0
+
+  H.describe "fromYears" $ do
+    H.it "succeeds with no overflow" $ do
+      I.fromYears 1 `H.shouldBe` Just (I.MkInterval 12 0 0)
+
+    H.it "fails with overflow" $ do
+      I.fromYears maxBound `H.shouldBe` Nothing
+
+  H.describe "render" $ do
+    H.it "works with zero" $ do
+      let actual = Builder.toLazyByteString $ I.render I.zero
+      actual `H.shouldBe` "0 months 0 days 0 microseconds"
+
+    H.it "works with positive components" $ do
+      let actual = Builder.toLazyByteString . I.render $ I.MkInterval 1 2 3
+      actual `H.shouldBe` "+1 months +2 days +3 microseconds"
+
+    H.it "works with negative components" $ do
+      let actual = Builder.toLazyByteString . I.render $ I.MkInterval -3 -2 -1
+      actual `H.shouldBe` "-3 months -2 days -1 microseconds"
+
+  H.describe "parse" $ do
+    H.it "fails with invalid input" $ do
+      let actual = Attoparsec.parseOnly I.parse "invalid"
+      actual `H.shouldBe` Left "Failed reading: empty"
+
+    H.it "succeeds with positive infinity" $ do
+      let actual = Attoparsec.parseOnly I.parse "infinity"
+      actual `H.shouldBe` Right (I.MkInterval maxBound maxBound maxBound)
+
+    H.it "succeeds with negative infinity" $ do
+      let actual = Attoparsec.parseOnly I.parse "-infinity"
+      actual `H.shouldBe` Right (I.MkInterval minBound minBound minBound)
+
+    Monad.forM_ intervalStyles $ \(_, field) ->
+      Monad.forM_ examples $ \example -> do
+        let input = field example
+        H.it ("succeeds with " <> Ascii.unpack input) $ do
+          let actual = Attoparsec.parseOnly I.parse input
+          actual `H.shouldBe` Right (exampleInterval example)
+
+data IntervalStyle
+  = Iso8601
+  | Postgres
+  | PostgresVerbose
+  | SqlStandard
+  deriving (Eq, Show)
+
+data Example = MkExample
+  { exampleInterval :: I.Interval,
+    exampleIso8601 :: ByteString.ByteString,
+    examplePostgres :: ByteString.ByteString,
+    examplePostgresVerbose :: ByteString.ByteString,
+    exampleSqlStandard :: ByteString.ByteString
+  }
+  deriving (Eq, Show)
+
+intervalStyles :: [(IntervalStyle, Example -> ByteString.ByteString)]
+intervalStyles =
+  [ (Iso8601, exampleIso8601),
+    (Postgres, examplePostgres),
+    (PostgresVerbose, examplePostgresVerbose),
+    (SqlStandard, exampleSqlStandard)
+  ]
+
+mkExample ::
+  Int.Int32 ->
+  Int.Int32 ->
+  Int.Int64 ->
+  ByteString.ByteString ->
+  ByteString.ByteString ->
+  ByteString.ByteString ->
+  ByteString.ByteString ->
+  Example
+mkExample m d s iso8601 postgres postgresVerbose sqlStandard =
+  MkExample
+    { exampleInterval = I.MkInterval {I.months = m, I.days = d, I.microseconds = s},
+      exampleIso8601 = iso8601,
+      examplePostgres = postgres,
+      examplePostgresVerbose = postgresVerbose,
+      exampleSqlStandard = sqlStandard
+    }
+
+examples :: [Example]
+examples =
+  [ mkExample 0 0 0 "PT0S" "00:00:00" "@ 0" "0",
+    mkExample 1 0 0 "P1M" "1 mon" "@ 1 mon" "0-1",
+    mkExample -1 0 0 "P-1M" "-1 mons" "@ 1 mon ago" "-0-1",
+    mkExample 3 0 0 "P3M" "3 mons" "@ 3 mons" "0-3",
+    mkExample 6 0 0 "P6M" "6 mons" "@ 6 mons" "0-6",
+    mkExample 12 0 0 "P1Y" "1 year" "@ 1 year" "1-0",
+    mkExample -12 0 0 "P-1Y" "-1 years" "@ 1 year ago" "-1-0",
+    mkExample 13 0 0 "P1Y1M" "1 year 1 mon" "@ 1 year 1 mon" "1-1",
+    mkExample -13 0 0 "P-1Y-1M" "-1 years -1 mons" "@ 1 year 1 mon ago" "-1-1",
+    mkExample 24 0 0 "P2Y" "2 years" "@ 2 years" "2-0",
+    mkExample 0 1 0 "P1D" "1 day" "@ 1 day" "1 0:00:00",
+    mkExample 0 -1 0 "P-1D" "-1 days" "@ 1 day ago" "-1 0:00:00",
+    mkExample 0 2 0 "P2D" "2 days" "@ 2 days" "2 0:00:00",
+    mkExample 0 7 0 "P7D" "7 days" "@ 7 days" "7 0:00:00",
+    mkExample 0 0 1 "PT0.000001S" "00:00:00.000001" "@ 0.000001 secs" "0:00:00.000001",
+    mkExample 0 0 -1 "PT-0.000001S" "-00:00:00.000001" "@ 0.000001 secs ago" "-0:00:00.000001",
+    mkExample 0 0 1e3 "PT0.001S" "00:00:00.001" "@ 0.001 secs" "0:00:00.001",
+    mkExample 0 0 1e6 "PT1S" "00:00:01" "@ 1 sec" "0:00:01",
+    mkExample 0 0 -1e6 "PT-1S" "-00:00:01" "@ 1 sec ago" "-0:00:01",
+    mkExample 0 0 2e6 "PT2S" "00:00:02" "@ 2 secs" "0:00:02",
+    mkExample 0 0 60e6 "PT1M" "00:01:00" "@ 1 min" "0:01:00",
+    mkExample 0 0 -60e6 "PT-1M" "-00:01:00" "@ 1 min ago" "-0:01:00",
+    mkExample 0 0 120e6 "PT2M" "00:02:00" "@ 2 mins" "0:02:00",
+    mkExample 0 0 3600e6 "PT1H" "01:00:00" "@ 1 hour" "01:00:00",
+    mkExample 0 0 -3600e6 "PT-1H" "-01:00:00" "@ 1 hour ago" "-01:00:00",
+    mkExample 0 0 7200e6 "PT2H" "02:00:00" "@ 2 hours" "02:00:00",
+    mkExample 0 0 86400e6 "PT24H" "24:00:00" "@ 24 hours" "24:00:00",
+    mkExample 1 1 1e6 "P1M1DT1S" "1 mon 1 day 00:00:01" "@ 1 mon 1 day 1 sec" "+0-1 +1 +0:00:01",
+    mkExample -1 -1 -1e6 "P-1M-1DT-1S" "-1 mons -1 days -00:00:01" "@ 1 mon 1 day 1 sec ago" "-0-1 -1 -0:00:01",
+    mkExample -1 1 1e6 "P-1M1DT1S" "-1 mons +1 day 00:00:01" "@ 1 mon -1 days -1 sec ago" "-0-1 +1 +0:00:01",
+    mkExample 1 -1 1e6 "P1M-1DT1S" "1 mon -1 days +00:00:01" "@ 1 mon -1 days 1 sec" "+0-1 -1 +0:00:01",
+    mkExample 1 1 -1e6 "P1M1DT-1S" "1 mon 1 day -00:00:01" "@ 1 mon 1 day -1 sec" "+0-1 +1 -0:00:01",
+    mkExample 14 3 14706000007 "P1Y2M3DT4H5M6.000007S" "1 year 2 mons 3 days 04:05:06.000007" "@ 1 year 2 mons 3 days 4 hours 5 mins 6.000007 secs" "+1-2 +3 +4:05:06.000007",
+    mkExample maxBound 0 0 "P178956970Y7M" "178956970 years 7 mons" "@ 178956970 years 7 mons" "178956970-7",
+    mkExample minBound 0 0 "P-178956970Y-8M" "-178956970 years -8 mons" "@ 178956970 years 8 mons ago" "-178956970-8",
+    mkExample 0 maxBound 0 "P2147483647D" "2147483647 days" "@ 2147483647 days" "2147483647 0:00:00",
+    mkExample 0 minBound 0 "P-2147483648D" "-2147483648 days" "@ 2147483648 days ago" "-2147483648 0:00:00",
+    mkExample 0 0 maxBound "PT2562047788H54.775807S" "2562047788:00:54.775807" "@ 2562047788 hours 54.775807 secs" "2562047788:00:54.775807",
+    mkExample 0 0 minBound "PT-2562047788H-54.775808S" "-2562047788:00:54.775808" "@ 2562047788 hours 54.775808 secs ago" "-2562047788:00:54.775808"
+  ]
