content: {-|
  Module: Squeal.PostgreSQL.Expression.Time
  Description: date/time functions and operators
  Copyright: (c) Eitan Chatav, 2019
  Maintainer: eitan@morphism.tech
  Stability: experimental
  
  date/time functions and operators
  -}
  
  {-# LANGUAGE
      DataKinds
    , DeriveGeneric
    , FunctionalDependencies
    , LambdaCase
    , MultiParamTypeClasses
    , OverloadedStrings
    , PolyKinds
    , RankNTypes
    , TypeFamilies
    , TypeOperators
    , UndecidableInstances
  #-}
  
  module Squeal.PostgreSQL.Expression.Time
    ( -- * Time Operation
      TimeOp (..)
      -- * Time Function
    , currentDate
    , currentTime
    , currentTimestamp
    , dateTrunc
    , localTime
    , localTimestamp
    , now
    , makeDate
    , makeTime
    , makeTimestamp
    , makeTimestamptz
    , atTimeZone
    , PGAtTimeZone
      -- * Interval
    , interval_
    , TimeUnit (..)
    ) where
  
  import Data.Fixed
  import Data.String
  import GHC.TypeLits
  
  import qualified GHC.Generics as GHC
  import qualified Generics.SOP as SOP
  
  import Squeal.PostgreSQL.Expression
  import Squeal.PostgreSQL.Render
  import Squeal.PostgreSQL.Type.List
  import Squeal.PostgreSQL.Type.Schema
  
  -- $setup
  -- >>> import Squeal.PostgreSQL
  
  -- | >>> printSQL currentDate
  -- CURRENT_DATE
  currentDate :: Expr (null 'PGdate)
  currentDate = UnsafeExpression "CURRENT_DATE"
  
  -- | >>> printSQL currentTime
  -- CURRENT_TIME
  currentTime :: Expr (null 'PGtimetz)
  currentTime = UnsafeExpression "CURRENT_TIME"
  
  -- | >>> printSQL currentTimestamp
  -- CURRENT_TIMESTAMP
  currentTimestamp :: Expr (null 'PGtimestamptz)
  currentTimestamp = UnsafeExpression "CURRENT_TIMESTAMP"
  
  -- | >>> printSQL localTime
  -- LOCALTIME
  localTime :: Expr (null 'PGtime)
  localTime = UnsafeExpression "LOCALTIME"
  
  -- | >>> printSQL localTimestamp
  -- LOCALTIMESTAMP
  localTimestamp :: Expr (null 'PGtimestamp)
  localTimestamp = UnsafeExpression "LOCALTIMESTAMP"
  
  -- | Current date and time (equivalent to `currentTimestamp`)
  --
  -- >>> printSQL now
  -- now()
  now :: Expr (null 'PGtimestamptz)
  now = UnsafeExpression "now()"
  
  {-|
  Create date from year, month and day fields
  
  >>> printSQL (makeDate (1984 :* 7 *: 3))
  make_date((1984 :: int4), (7 :: int4), (3 :: int4))
  -}
  makeDate :: '[ null 'PGint4, null 'PGint4, null 'PGint4 ] ---> null 'PGdate
  makeDate = unsafeFunctionN "make_date"
  
  {-|
  Create time from hour, minute and seconds fields
  
  >>> printSQL (makeTime (8 :* 15 *: 23.5))
  make_time((8 :: int4), (15 :: int4), (23.5 :: float8))
  -}
  makeTime :: '[ null 'PGint4, null 'PGint4, null 'PGfloat8 ] ---> null 'PGtime
  makeTime = unsafeFunctionN "make_time"
  
  {-|
  Create timestamp from year, month, day, hour, minute and seconds fields
  
  >>> printSQL (makeTimestamp (2013 :* 7 :* 15 :* 8 :* 15 *: 23.5))
  make_timestamp((2013 :: int4), (7 :: int4), (15 :: int4), (8 :: int4), (15 :: int4), (23.5 :: float8))
  -}
  makeTimestamp ::
    '[ null 'PGint4, null 'PGint4, null 'PGint4
     , null 'PGint4, null 'PGint4, null 'PGfloat8 ] ---> null 'PGtimestamp
  makeTimestamp = unsafeFunctionN "make_timestamp"
  
  {-|
  Create timestamp with time zone from
  year, month, day, hour, minute and seconds fields;
  the current time zone is used
  
  >>> printSQL (makeTimestamptz (2013 :* 7 :* 15 :* 8 :* 15 *: 23.5))
  make_timestamptz((2013 :: int4), (7 :: int4), (15 :: int4), (8 :: int4), (15 :: int4), (23.5 :: float8))
  -}
  makeTimestamptz ::
    '[ null 'PGint4, null 'PGint4, null 'PGint4
     , null 'PGint4, null 'PGint4, null 'PGfloat8 ] ---> null 'PGtimestamptz
  makeTimestamptz = unsafeFunctionN "make_timestamptz"
  
  {-|
  Truncate a timestamp with the specified precision
  
  >>> printSQL $ dateTrunc Quarter (makeTimestamp (2010 :* 5 :* 6 :* 14 :* 45 *: 11.4))
  date_trunc('quarter', make_timestamp((2010 :: int4), (5 :: int4), (6 :: int4), (14 :: int4), (45 :: int4), (11.4 :: float8)))
  -}
  dateTrunc
    :: time `In` '[ 'PGtimestamp, 'PGtimestamptz ]
    => TimeUnit -> null time --> null time
  dateTrunc tUnit args = unsafeFunctionN "date_trunc" (timeUnitExpr *: args)
    where
    timeUnitExpr :: forall grp lat with db params from null0.
      Expression grp lat with db params from (null0 'PGtext)
    timeUnitExpr = UnsafeExpression . singleQuotedUtf8 . renderSQL $ tUnit
  
  -- | Calculate the return time type of the `atTimeZone` `Operator`.
  type family PGAtTimeZone ty where
    PGAtTimeZone 'PGtimestamptz = 'PGtimestamp
    PGAtTimeZone 'PGtimestamp = 'PGtimestamptz
    PGAtTimeZone 'PGtimetz = 'PGtimetz
    PGAtTimeZone pg = TypeError
      ( 'Text "Squeal type error: AT TIME ZONE cannot be applied to "
        ':<>: 'ShowType pg )
  
  {-|
  Convert a timestamp, timestamp with time zone, or time of day with timezone to a different timezone using an interval offset or specific timezone denoted by text. When using the interval offset, the interval duration must be less than one day or 24 hours.
  
  >>> printSQL $ (makeTimestamp (2009 :* 7 :* 22 :* 19 :* 45 *: 11.4)) `atTimeZone` (interval_ 8 Hours)
  (make_timestamp((2009 :: int4), (7 :: int4), (22 :: int4), (19 :: int4), (45 :: int4), (11.4 :: float8)) AT TIME ZONE (INTERVAL '8.000 hours'))
  
  >>> :{
   let
     timezone :: Expr (null 'PGtext)
     timezone = "EST"
   in printSQL $ (makeTimestamptz (2015 :* 9 :* 15 :* 4 :* 45 *: 11.4)) `atTimeZone` timezone
  :}
  (make_timestamptz((2015 :: int4), (9 :: int4), (15 :: int4), (4 :: int4), (45 :: int4), (11.4 :: float8)) AT TIME ZONE (E'EST' :: text))
  -}
  atTimeZone
    :: zone `In` '[ 'PGtext, 'PGinterval]
    => Operator (null time) (null zone) (null (PGAtTimeZone time))
  atTimeZone = unsafeBinaryOp "AT TIME ZONE"
  
  {-|
  Affine space operations on time types.
  -}
  class TimeOp time diff | time -> diff where
    {-|
    >>> printSQL (makeDate (1984 :* 7 *: 3) !+ 365)
    (make_date((1984 :: int4), (7 :: int4), (3 :: int4)) + (365 :: int4))
    -}
    (!+) :: Operator (null time) (null diff) (null time)
    (!+) = unsafeBinaryOp "+"
    {-|
    >>> printSQL (365 +! makeDate (1984 :* 7 *: 3))
    ((365 :: int4) + make_date((1984 :: int4), (7 :: int4), (3 :: int4)))
    -}
    (+!) :: Operator (null diff) (null time) (null time)
    (+!) = unsafeBinaryOp "+"
    {-|
    >>> printSQL (makeDate (1984 :* 7 *: 3) !- 365)
    (make_date((1984 :: int4), (7 :: int4), (3 :: int4)) - (365 :: int4))
    -}
    (!-) :: Operator (null time) (null diff) (null time)
    (!-) = unsafeBinaryOp "-"
    {-|
    >>> printSQL (makeDate (1984 :* 7 *: 3) !-! currentDate)
    (make_date((1984 :: int4), (7 :: int4), (3 :: int4)) - CURRENT_DATE)
    -}
    (!-!) :: Operator (null time) (null time) (null diff)
    (!-!) = unsafeBinaryOp "-"
  instance TimeOp 'PGtimestamp 'PGinterval
  instance TimeOp 'PGtimestamptz 'PGinterval
  instance TimeOp 'PGtime 'PGinterval
  instance TimeOp 'PGtimetz 'PGinterval
  instance TimeOp 'PGinterval 'PGinterval
  instance TimeOp 'PGdate 'PGint4
  infixl 6 !+
  infixl 6 +!
  infixl 6 !-
  infixl 6 !-!
  
  -- | A `TimeUnit` to use in `interval_` construction.
  data TimeUnit
    = Years | Quarter | Months | Weeks | Days
    | Hours | Minutes | Seconds
    | Microseconds | Milliseconds
    | Decades | Centuries | Millennia
    deriving (Eq, Ord, Show, Read, Enum, GHC.Generic)
  instance SOP.Generic TimeUnit
  instance SOP.HasDatatypeInfo TimeUnit
  instance RenderSQL TimeUnit where
    renderSQL = \case
      Years -> "years"
      Quarter -> "quarter"
      Months -> "months"
      Weeks -> "weeks"
      Days -> "days"
      Hours -> "hours"
      Minutes -> "minutes"
      Seconds -> "seconds"
      Microseconds -> "microseconds"
      Milliseconds -> "milliseconds"
      Decades -> "decades"
      Centuries -> "centuries"
      Millennia -> "millennia"
  
  -- | >>> printSQL $ interval_ 7 Days
  -- (INTERVAL '7.000 days')
  interval_ :: Milli -> TimeUnit -> Expr (null 'PGinterval)
  interval_ num unit = UnsafeExpression . parenthesized $ "INTERVAL" <+>
    "'" <> fromString (show num) <+> renderSQL unit <> "'"
  