{-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances, DefaultSignatures, DeriveGeneric #-}
module Database.Selda.SqlType
( SqlType (..), SqlEnum (..)
, Lit (..), UUID, RowID, ID, SqlValue (..), SqlTypeRep (..)
, invalidRowId, isInvalidRowId, toRowId, fromRowId
, fromId, toId, invalidId, isInvalidId, untyped
, compLit, litType
, sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat
) where
import Control.Applicative ((<|>))
import Data.ByteString (ByteString, empty)
import qualified Data.ByteString.Lazy as BSL
import Data.Maybe (fromJust)
import Data.Proxy
import Data.Text (Text, pack, unpack)
import Data.Time
import Data.Typeable
import Data.UUID.Types (UUID, toString, fromByteString, nil)
import GHC.Generics (Generic)
sqlDateTimeFormat :: String
sqlDateTimeFormat = "%F %H:%M:%S%Q%z"
sqlDateFormat :: String
sqlDateFormat = "%F"
sqlTimeFormat :: String
sqlTimeFormat = "%H:%M:%S%Q%z"
data SqlTypeRep
= TText
| TRowID
| TInt
| TFloat
| TBool
| TDateTime
| TDate
| TTime
| TBlob
| TUUID
| TJSON
deriving (Show, Eq, Ord)
class Typeable a => SqlType a where
mkLit :: a -> Lit a
default mkLit :: (Typeable a, SqlEnum a) => a -> Lit a
mkLit = LCustom TText . LText . toText
sqlType :: Proxy a -> SqlTypeRep
sqlType _ = litType (defaultValue :: Lit a)
fromSql :: SqlValue -> a
default fromSql :: (Typeable a, SqlEnum a) => SqlValue -> a
fromSql = fromText . fromSql
defaultValue :: Lit a
default defaultValue :: (Typeable a, SqlEnum a) => Lit a
defaultValue = mkLit (minBound :: a)
class (Typeable a, Bounded a, Enum a) => SqlEnum a where
toText :: a -> Text
fromText :: Text -> a
instance {-# OVERLAPPABLE #-}
(Typeable a, Bounded a, Enum a, Show a, Read a) => SqlEnum a where
toText = pack . show
fromText = read . unpack
data Lit a where
LText :: !Text -> Lit Text
LInt :: !Int -> Lit Int
LDouble :: !Double -> Lit Double
LBool :: !Bool -> Lit Bool
LDateTime :: !UTCTime -> Lit UTCTime
LDate :: !Day -> Lit Day
LTime :: !TimeOfDay -> Lit TimeOfDay
LJust :: SqlType a => !(Lit a) -> Lit (Maybe a)
LBlob :: !ByteString -> Lit ByteString
LNull :: SqlType a => Lit (Maybe a)
LCustom :: SqlTypeRep -> Lit a -> Lit b
LUUID :: !UUID -> Lit UUID
litType :: Lit a -> SqlTypeRep
litType (LText{}) = TText
litType (LInt{}) = TInt
litType (LDouble{}) = TFloat
litType (LBool{}) = TBool
litType (LDateTime{}) = TDateTime
litType (LDate{}) = TDate
litType (LTime{}) = TTime
litType (LJust x) = litType x
litType (LBlob{}) = TBlob
litType (x@LNull) = sqlType (proxyFor x)
where
proxyFor :: Lit (Maybe a) -> Proxy a
proxyFor _ = Proxy
litType (LCustom t _) = t
litType (LUUID{}) = TUUID
instance Eq (Lit a) where
a == b = compLit a b == EQ
instance Ord (Lit a) where
compare = compLit
litConTag :: Lit a -> Int
litConTag (LText{}) = 0
litConTag (LInt{}) = 1
litConTag (LDouble{}) = 2
litConTag (LBool{}) = 3
litConTag (LDateTime{}) = 4
litConTag (LDate{}) = 5
litConTag (LTime{}) = 6
litConTag (LJust{}) = 7
litConTag (LBlob{}) = 8
litConTag (LNull) = 9
litConTag (LCustom{}) = 10
litConTag (LUUID{}) = 11
compLit :: Lit a -> Lit b -> Ordering
compLit (LText x) (LText x') = x `compare` x'
compLit (LInt x) (LInt x') = x `compare` x'
compLit (LDouble x) (LDouble x') = x `compare` x'
compLit (LBool x) (LBool x') = x `compare` x'
compLit (LDateTime x) (LDateTime x') = x `compare` x'
compLit (LDate x) (LDate x') = x `compare` x'
compLit (LTime x) (LTime x') = x `compare` x'
compLit (LBlob x) (LBlob x') = x `compare` x'
compLit (LJust x) (LJust x') = x `compLit` x'
compLit (LCustom _ x) (LCustom _ x') = x `compLit` x'
compLit (LUUID x) (LUUID x') = x `compare` x'
compLit a b = litConTag a `compare` litConTag b
data SqlValue where
SqlInt :: !Int -> SqlValue
SqlFloat :: !Double -> SqlValue
SqlString :: !Text -> SqlValue
SqlBool :: !Bool -> SqlValue
SqlBlob :: !ByteString -> SqlValue
SqlUTCTime :: !UTCTime -> SqlValue
SqlTime :: !TimeOfDay -> SqlValue
SqlDate :: !Day -> SqlValue
SqlNull :: SqlValue
instance Show SqlValue where
show (SqlInt n) = "SqlInt " ++ show n
show (SqlFloat f) = "SqlFloat " ++ show f
show (SqlString s) = "SqlString " ++ show s
show (SqlBool b) = "SqlBool " ++ show b
show (SqlBlob b) = "SqlBlob " ++ show b
show (SqlUTCTime t) = "SqlUTCTime " ++ show t
show (SqlTime t) = "SqlTime " ++ show t
show (SqlDate d) = "SqlDate " ++ show d
show (SqlNull) = "SqlNull"
instance Show (Lit a) where
show (LText s) = show s
show (LInt i) = show i
show (LDouble d) = show d
show (LBool b) = show b
show (LDateTime s) = show s
show (LDate s) = show s
show (LTime s) = show s
show (LBlob b) = show b
show (LJust x) = "Just " ++ show x
show (LNull) = "Nothing"
show (LCustom _ l) = show l
show (LUUID u) = toString u
newtype RowID = RowID Int
deriving (Eq, Ord, Typeable, Generic)
instance Show RowID where
show (RowID n) = show n
invalidRowId :: RowID
invalidRowId = RowID (-1)
isInvalidRowId :: RowID -> Bool
isInvalidRowId (RowID n) = n < 0
toRowId :: Int -> RowID
toRowId = RowID
fromRowId :: RowID -> Int
fromRowId (RowID n) = n
newtype ID a = ID {untyped :: RowID}
deriving (Eq, Ord, Typeable, Generic)
instance Show (ID a) where
show = show . untyped
toId :: Int -> ID a
toId = ID . toRowId
fromId :: ID a -> Int
fromId (ID i) = fromRowId i
invalidId :: ID a
invalidId = ID invalidRowId
isInvalidId :: ID a -> Bool
isInvalidId = isInvalidRowId . untyped
instance SqlType RowID where
mkLit (RowID n) = LCustom TRowID (LInt n)
sqlType _ = TRowID
fromSql (SqlInt x) = RowID x
fromSql v = error $ "fromSql: RowID column with non-int value: " ++ show v
defaultValue = mkLit invalidRowId
instance Typeable a => SqlType (ID a) where
mkLit (ID n) = LCustom TRowID (mkLit n)
sqlType _ = TRowID
fromSql = ID . fromSql
defaultValue = mkLit (ID invalidRowId)
instance SqlType Int where
mkLit = LInt
sqlType _ = TInt
fromSql (SqlInt x) = x
fromSql v = error $ "fromSql: int column with non-int value: " ++ show v
defaultValue = LInt 0
instance SqlType Double where
mkLit = LDouble
sqlType _ = TFloat
fromSql (SqlFloat x) = x
fromSql v = error $ "fromSql: float column with non-float value: " ++ show v
defaultValue = LDouble 0
instance SqlType Text where
mkLit = LText
sqlType _ = TText
fromSql (SqlString x) = x
fromSql v = error $ "fromSql: text column with non-text value: " ++ show v
defaultValue = LText ""
instance SqlType Bool where
mkLit = LBool
sqlType _ = TBool
fromSql (SqlBool x) = x
fromSql (SqlInt 0) = False
fromSql (SqlInt _) = True
fromSql v = error $ "fromSql: bool column with non-bool value: " ++ show v
defaultValue = LBool False
instance SqlType UTCTime where
mkLit = LDateTime
sqlType _ = TDateTime
fromSql (SqlUTCTime t) = t
fromSql (SqlString s) =
case withWeirdTimeZone sqlDateTimeFormat (unpack s) of
Just t -> t
_ -> error $ "fromSql: bad datetime string: " ++ unpack s
fromSql v = error $ "fromSql: datetime column with non-datetime value: " ++ show v
defaultValue = LDateTime $ UTCTime (ModifiedJulianDay 40587) 0
instance SqlType Day where
mkLit = LDate
sqlType _ = TDate
fromSql (SqlDate d) = d
fromSql (SqlString s) =
case parseTimeM True defaultTimeLocale sqlDateFormat (unpack s) of
Just t -> t
_ -> error $ "fromSql: bad date string: " ++ unpack s
fromSql v = error $ "fromSql: date column with non-date value: " ++ show v
defaultValue = LDate $ ModifiedJulianDay 40587
instance SqlType TimeOfDay where
mkLit = LTime
sqlType _ = TTime
fromSql (SqlTime s) = s
fromSql (SqlString s) =
case withWeirdTimeZone sqlTimeFormat (unpack s) of
Just t -> t
_ -> error $ "fromSql: bad time string: " ++ unpack s
fromSql v = error $ "fromSql: time column with non-time value: " ++ show v
defaultValue = LTime $ TimeOfDay 0 0 0
withWeirdTimeZone :: ParseTime t => String -> String -> Maybe t
withWeirdTimeZone fmt s =
parseTimeM True defaultTimeLocale fmt (s++"00")
<|> parseTimeM True defaultTimeLocale fmt s
<|> parseTimeM True defaultTimeLocale fmt (s++"+0000")
instance SqlType ByteString where
mkLit = LBlob
sqlType _ = TBlob
fromSql (SqlBlob x) = x
fromSql v = error $ "fromSql: blob column with non-blob value: " ++ show v
defaultValue = LBlob empty
instance SqlType BSL.ByteString where
mkLit = LCustom TBlob . LBlob . BSL.toStrict
sqlType _ = TBlob
fromSql (SqlBlob x) = BSL.fromStrict x
fromSql v = error $ "fromSql: blob column with non-blob value: " ++ show v
defaultValue = LCustom TBlob (LBlob empty)
instance SqlType UUID where
mkLit = LUUID
sqlType _ = TUUID
fromSql (SqlBlob x) = fromJust . fromByteString $ BSL.fromStrict x
fromSql v = error $ "fromSql: UUID column with non-blob value: " ++ show v
defaultValue = LUUID nil
instance SqlType a => SqlType (Maybe a) where
mkLit (Just x) = LJust $ mkLit x
mkLit Nothing = LNull
sqlType _ = sqlType (Proxy :: Proxy a)
fromSql (SqlNull) = Nothing
fromSql x = Just $ fromSql x
defaultValue = LNull
instance SqlType Ordering